Step 17: Encryption core — passphrase-only, selective per-file.

Manifest schema: Entry gains Encrypted, PlaintextHash fields.
Manifest gains Encryption section with KekSlots map (passphrase slot
with Argon2id params, salt, and wrapped DEK as base64).

garden/encrypt.go: EncryptInit (generate DEK, wrap with passphrase KEK),
UnlockDEK (derive KEK, unwrap), encryptBlob/decryptBlob using
XChaCha20-Poly1305 with random 24-byte nonces.

Modified operations:
- Add: optional encrypt flag, stores encrypted blob + plaintext_hash
- Checkpoint: detects changes via plaintext_hash, re-encrypts
- Restore: decrypts encrypted blobs before writing
- Diff: decrypts stored blob before comparing
- Status: compares against plaintext_hash for encrypted entries

10 tests covering init, persistence, unlock, add-encrypted, restore
round-trip, checkpoint, status, diff, requires-DEK guard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 08:50:53 -07:00
parent 582f2116d2
commit 3b961b5d8a
8 changed files with 737 additions and 39 deletions

View File

@@ -11,21 +11,41 @@ import (
// Entry represents a single tracked file, directory, or symlink.
type Entry struct {
Path string `yaml:"path"`
Hash string `yaml:"hash,omitempty"`
Type string `yaml:"type"`
Mode string `yaml:"mode,omitempty"`
Target string `yaml:"target,omitempty"`
Updated time.Time `yaml:"updated"`
Path string `yaml:"path"`
Hash string `yaml:"hash,omitempty"`
PlaintextHash string `yaml:"plaintext_hash,omitempty"`
Encrypted bool `yaml:"encrypted,omitempty"`
Type string `yaml:"type"`
Mode string `yaml:"mode,omitempty"`
Target string `yaml:"target,omitempty"`
Updated time.Time `yaml:"updated"`
}
// KekSlot describes a single KEK source that can unwrap the DEK.
type KekSlot struct {
Type string `yaml:"type"` // "passphrase" or "fido2"
Argon2Time int `yaml:"argon2_time,omitempty"` // passphrase only
Argon2Memory int `yaml:"argon2_memory,omitempty"` // passphrase only (KiB)
Argon2Threads int `yaml:"argon2_threads,omitempty"` // passphrase only
CredentialID string `yaml:"credential_id,omitempty"` // fido2 only (base64)
Salt string `yaml:"salt"` // base64-encoded
WrappedDEK string `yaml:"wrapped_dek"` // base64-encoded
}
// Encryption holds the encryption configuration embedded in the manifest.
type Encryption struct {
Algorithm string `yaml:"algorithm"`
KekSlots map[string]*KekSlot `yaml:"kek_slots"`
}
// Manifest is the top-level manifest describing all tracked entries.
type Manifest struct {
Version int `yaml:"version"`
Created time.Time `yaml:"created"`
Updated time.Time `yaml:"updated"`
Message string `yaml:"message,omitempty"`
Files []Entry `yaml:"files"`
Version int `yaml:"version"`
Created time.Time `yaml:"created"`
Updated time.Time `yaml:"updated"`
Message string `yaml:"message,omitempty"`
Files []Entry `yaml:"files"`
Encryption *Encryption `yaml:"encryption,omitempty"`
}
// New creates a new empty manifest with Version 1 and timestamps set to now.