E2e integration test covering TLS + encryption + locked files in a push/pull cycle (integration/phase4_test.go). Final doc updates. Phase 4 complete. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
14 KiB
14 KiB
PROJECT_PLAN.md
Implementation plan for sgard. See ARCHITECTURE.md for design details.
Step 1: Project Scaffolding
Remove old C++ source files and set up the Go project.
- Remove old files:
sgard.cc,proto/,CMakeLists.txt,scripts/,.clang-format,.clang-tidy,.idea/,.trunk/ go mod init github.com/kisom/sgard- Add dependencies:
gopkg.in/yaml.v3,github.com/spf13/cobra - Create directory structure:
cmd/sgard/,manifest/,store/,garden/ - Set up
cmd/sgard/main.gowith cobra root command and--repopersistent flag - Update CLAUDE.md to reflect Go project
- Verify:
go build ./...compiles clean
Step 2: Manifest Package
Can be done in parallel with Step 3.
manifest/manifest.go:ManifestandEntrystructs with YAML tags- Entry types:
file,directory,link - Mode as string type to avoid YAML octal coercion
- Per-file
updatedtimestamp
- Entry types:
manifest/manifest.go:Load(path)andSave(path)functions- Save uses atomic write (write to
.tmp, rename)
- Save uses atomic write (write to
manifest/manifest_test.go: round-trip marshal/unmarshal, atomic save, entry type validation
Step 3: Store Package
Can be done in parallel with Step 2.
store/store.go:Storestruct withrootpathstore/store.go:Write(data) (hash, error)— hash content, write toblobs/XX/YY/<hash>store/store.go:Read(hash) ([]byte, error)— read blob by hashstore/store.go:Exists(hash) bool— check if blob existsstore/store.go:Delete(hash) error— remove a blobstore/store_test.go: write/read round-trip, integrity check, missing blob error
Step 4: Garden Core — Init and Add
Depends on Steps 2 and 3.
garden/hasher.go:HashFile(path) (string, error)— SHA-256 of a filegarden/garden.go:Gardenstruct tying manifest + store + root pathgarden/garden.go:Open(root) (*Garden, error)— load existing repogarden/garden.go:Init(root) (*Garden, error)— create new repo (dirs + empty manifest)garden/garden.go:Add(paths []string) error— hash files, store blobs, add manifest entriesgarden/garden_test.go: init creates correct structure, add stores blob and updates manifest- Wire up CLI:
cmd/sgard/init.go,cmd/sgard/add.go - Verify:
go build ./cmd/sgard && ./sgard init && ./sgard add ~/.bashrc
Step 5: Checkpoint and Status
Depends on Step 4.
garden/garden.go:Checkpoint(message string) error— re-hash all tracked files, store changed blobs, update manifest timestampsgarden/garden.go:Status() ([]FileStatus, error)— compare current hashes to manifest; report modified/missing/okgarden/garden_test.go: checkpoint detects changed files, status reports correctly- Wire up CLI:
cmd/sgard/checkpoint.go,cmd/sgard/status.go
Step 6: Restore
Depends on Step 5.
garden/garden.go:Restore(paths []string, force bool, confirm func) error- Restore all files if paths is empty, otherwise just the specified paths
- Timestamp comparison: skip prompt if manifest
updatedis newer than file mtime - Prompt user if file on disk is newer or times match (unless
--force) - Create parent directories as needed
- Recreate symlinks for
linktype entries - Set file permissions from manifest
mode
garden/garden_test.go: restore writes correct content, respects permissions, handles symlinks- Wire up CLI:
cmd/sgard/restore.go
Step 7: Remaining Commands
These can be done in parallel with each other.
garden/remove.go:Remove(paths []string) error— remove manifest entriesgarden/verify.go:Verify() ([]VerifyResult, error)— check blobs against manifest hashesgarden/list.go:List() []Entry— return all manifest entriesgarden/diff.go:Diff(path string) (string, error)— diff stored blob vs current file- Wire up CLI:
cmd/sgard/remove.go,cmd/sgard/verify.go,cmd/sgard/list.go,cmd/sgard/diff.go - Tests for each
Step 8: Polish
- Lint setup (golangci-lint config)
- Clock abstraction: inject
jonboulle/clockworkinto Garden for deterministic timestamp tests - End-to-end test: init → add → checkpoint → modify file → status → restore → verify
- Ensure
go vet ./...andgo test ./...pass clean - Update CLAUDE.md, ARCHITECTURE.md, PROGRESS.md
Phase 2: gRPC Remote Sync
Step 9: Proto Definitions + Code Gen
- Write
proto/sgard/v1/sgard.proto— 5 RPCs (PushManifest, PushBlobs, PullManifest, PullBlobs, Prune), all messages - Add Makefile target for protoc code generation
- Add grpc, protobuf, x/crypto deps to go.mod
- Update flake.nix devShell with protoc tools
- Verify:
go build ./sgardpbcompiles
Step 10: Garden Accessor Methods
Can be done in parallel with Step 11.
garden/garden.go:GetManifest(),BlobExists(),ReadBlob(),WriteBlob(),ReplaceManifest()- Tests for each accessor
- Verify:
go test ./garden/...
Step 11: Proto-Manifest Conversion
Can be done in parallel with Step 10.
server/convert.go:ManifestToProto,ProtoToManifest, entry helpersserver/convert_test.go: round-trip test- Verify:
go test ./server/...
Step 12: Server Implementation (No Auth)
Depends on Steps 9, 10, 11.
server/server.go: Server struct with RWMutex, 5 RPC handlers (+ Prune)- PushManifest: timestamp compare, compute missing blobs
- PushBlobs: receive stream, write to store, replace manifest
- PullManifest: return manifest
- PullBlobs: stream requested blobs (64 KiB chunks)
- Prune: remove orphaned blobs (added store.List + garden.ListBlobs/DeleteBlob)
server/server_test.go: in-process test with bufconn, push+pull+prune
Step 12b: Directory Recursion and Mirror Command
garden/garden.go:Addrecurses directories — walk all files/symlinks, add each as its own entrygarden/mirror.go:MirrorUp(paths []string) error— walk directory, add new files, remove entries for files gone from disk, re-hash changedgarden/mirror.go:MirrorDown(paths []string, force bool, confirm func(string) bool) error— restore all tracked files under path, delete anything not in manifestgarden/mirror_test.go: tests for recursive add, mirror up (detects new/removed), mirror down (cleans extras)cmd/sgard/mirror.go:sgard mirror up <path>,sgard mirror down <path> [--force]- Update existing add tests for directory recursion
Step 13: Client Library (No Auth)
Depends on Step 12.
client/client.go: Client struct,Push(),Pull(),Prune()methodsclient/client_test.go: integration tests (push+pull cycle, server newer, up-to-date, prune)
Step 14: SSH Key Auth
server/auth.go: AuthInterceptor, parse authorized_keys, verify SSH signaturesclient/auth.go: LoadSigner (ssh-agent or key file), SSHCredentials (PerRPCCredentials)server/auth_test.go: valid key, reject unauthenticated, reject unauthorized key, reject expired timestampclient/auth_test.go: metadata generation, no-transport-security- Integration tests: authenticated push/pull succeeds, unauthenticated is rejected
Step 15: CLI Wiring + Prune
Depends on Steps 13, 14.
garden/prune.go:Prune() (int, error)— collect referenced hashes, delete orphaned blobsgarden/prune_test.go: prune removes orphaned, keeps referencedserver/server.go: Prune RPC (done in Step 12)proto/sgard/v1/sgard.proto: Prune RPC (done in Step 9)client/client.go: Prune() method (done in Step 13)cmd/sgard/prune.go: local prune; with--remoteprunes remote insteadcmd/sgard/main.go: add--remote,--ssh-keypersistent flags, resolveRemote()cmd/sgard/push.go,cmd/sgard/pull.gocmd/sgardd/main.go: flags, garden open, auth interceptor, gRPC serve- Verify: both binaries compile
Step 16: Polish + Release
- Update ARCHITECTURE.md, README.md, CLAUDE.md, PROGRESS.md
- Update flake.nix (add sgardd, updated vendorHash)
- Update .goreleaser.yaml (add sgardd build)
- E2e integration test: init two repos, push from one, pull into other (with auth)
- Verify: all tests pass, full push/pull cycle works
Phase 3: Encryption
Step 17: Encryption Core (Passphrase Only)
manifest/manifest.go: addEncrypted,PlaintextHashfields to Entry; addEncryptionsection withKekSlotsmap to Manifestgarden/encrypt.go:EncryptInit(passphrase string) error— generate DEK, derive KEK via Argon2id, wrap DEK, store in manifest encryption sectiongarden/encrypt.go:UnlockDEK(prompt) error— read slots, try passphrase, unwrap DEK; cache in memory for command durationgarden/encrypt.go: encrypt/decrypt helpers using XChaCha20-Poly1305 (nonce + seal/open)garden/garden.go: modify Add to accept encrypt flag — encrypt blob before storing, setencrypted: trueandplaintext_hashon entrygarden/garden.go: modify Checkpoint to re-encrypt changed encrypted entries (compares plaintext_hash)garden/garden.go: modify Restore to decrypt encrypted blobs before writinggarden/diff.go: modify Diff to decrypt stored blob before diffinggarden/garden.go: modify Status to useplaintext_hashfor encrypted entries- Tests: 10 encryption tests (init, persist, unlock, add-encrypted, restore round-trip, checkpoint, status, diff, requires-DEK)
- Verify:
go test ./... && go vet ./... && golangci-lint run ./...
Step 18: FIDO2 Support
Depends on Step 17.
garden/encrypt_fido2.go: FIDO2Device interface, AddFIDO2Slot, unlockFIDO2, defaultFIDO2Labelgarden/encrypt.go: UnlockDEK tries fido2/* slots first (credential_id matching), falls back to passphrasegarden/encrypt_fido2_test.go: mock FIDO2 device, 6 tests (add slot, duplicate rejected, unlock via FIDO2, fallback to passphrase, persistence, encrypted round-trip with FIDO2)- Verify:
go test ./... && go vet ./... && golangci-lint run ./...
Step 19: Encryption CLI + Slot Management
Depends on Steps 17, 18.
cmd/sgard/encrypt.go:sgard encrypt init [--fido2],add-fido2 [--label],remove-slot,list-slots,change-passphrasegarden/encrypt.go:RemoveSlot,ListSlots,ChangePassphrasemethodscmd/sgard/add.go: add--encryptflag with passphrase prompt- Update proto: add
encrypted,plaintext_hashto ManifestEntry; add KekSlot, Encryption messages, encryption field on Manifest - Update
server/convert.go: full encryption section conversion (Encryption, KekSlot) - Verify: both binaries compile,
go test ./..., lint clean
Step 20: Encryption Polish + Release
- E2e test: full encryption lifecycle (init, add encrypted+plaintext, checkpoint, modify, status, restore, verify, diff, slot management, passphrase change)
- Update ARCHITECTURE.md, README.md, CLAUDE.md
- Update flake.nix vendorHash
- Verify: all tests pass, lint clean
Future Steps (Not Phase 3)
Phase 4: Hardening + Completeness
Step 21: Lock/Unlock Toggle Commands
garden/lock.go:Lock(paths),Unlock(paths)— toggle locked flag on existing entriescmd/sgard/lock.go:sgard lock <path>...,sgard unlock <path>...- Tests: lock/unlock existing entry, persist, error on untracked, checkpoint/status behavior changes (6 tests)
Step 22: Shell Completion
- Cobra provides built-in
sgard completionfor bash, zsh, fish, powershell — no code needed - README updated with shell completion installation instructions
Step 23: TLS Transport for sgardd
cmd/sgardd/main.go: add--tls-cert,--tls-keyflags- Server uses
credentials.NewTLS()when cert/key provided, insecure otherwise - Client: add
--tlsflag and--tls-cafor custom CA - Update
cmd/sgard/main.goanddialRemote()for TLS - Tests: TLS connection with self-signed cert (push/pull cycle, reject untrusted client)
- Update ARCHITECTURE.md and README.md
Step 24: DEK Rotation
garden/encrypt.go:RotateDEK(promptPassphrase, fido2Device)— generate new DEK, re-encrypt all encrypted blobs, re-wrap with all existing KEK slotscmd/sgard/encrypt.go:sgard encrypt rotate-dek- Tests: rotate DEK, verify decryption, verify plaintext untouched, FIDO2 re-wrap, requires-unlock (4 tests)
Step 25: Real FIDO2 Hardware Binding
- Evaluate approach: go-libfido2 CGo bindings (keys-pub/go-libfido2 v1.5.3)
garden/fido2_hardware.go: HardwareFIDO2 implementing FIDO2Device via libfido2 (//go:build fido2)garden/fido2_nohardware.go: stub returning nil (//go:build !fido2)cmd/sgard/fido2.go: unlockDEK helper, --fido2-pin flagcmd/sgard/encrypt.go: add-fido2 uses real hardware, encrypt init --fido2 registers slot, all unlock calls use FIDO2-first resolutionflake.nix: sgard-fido2 package variant, libfido2+pkg-config in devShell- Tests: existing mock-based tests still pass; hardware tests require manual testing with a FIDO2 key
Step 26: Test Cleanup
- Standardize all test calls — already use
AddOptions{}struct consistently (no legacy variadic patterns found) - Ensure all tests use
t.TempDir()consistently (audited, noos.MkdirTemp/ioutil.Tempusage) - Review lint config — added copyloopvar, durationcheck, makezero, nilerr, bodyclose linters
- Verify test coverage — added 3 tests: encrypted+locked, dir-only+locked, lock/unlock toggle on encrypted
- Fix stale API signatures in ARCHITECTURE.md (Add, Lock, Unlock, RotateDEK, HasEncryption, NeedsDEK)
Step 27: Phase 4 Polish + Release
- Update all docs (ARCHITECTURE.md, README.md, CLAUDE.md, PROGRESS.md)
- Update flake.nix vendorHash (done in Step 25)
- .goreleaser.yaml — no changes needed (CGO_ENABLED=0 is correct for release binaries)
- E2e test: integration/phase4_test.go covering TLS + encryption + locked files + push/pull
- Verify: all tests pass, lint clean, both binaries compile
Phase 5: Multi-Repo + Per-Machine Inclusion
(To be planned)
Phase 6: Manifest Signing
(To be planned — requires trust model design)