# Phase 2: Crypto & Config Seed Loader **Status:** ✅ Complete **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** backend ## Objective Implement AES-256 encryption for credential storage and the YAML seed config parser that imports into SQLite on first launch. ## Tasks - [x] Task 1: Implement AES-256-GCM encrypt/decrypt functions using Go stdlib `crypto/aes` + `crypto/cipher` - [x] Task 2: Key derivation from ENCRYPTION_KEY env var (SHA-256 hash to get 32 bytes) - [x] Task 3: Define YAML config structs matching the seed format from PLAN.md - [x] Task 4: Implement YAML parser — read and validate seed file - [x] Task 5: Implement seed importer — checks if DB is empty, if so imports YAML into SQLite via store CRUD - [x] Task 6: Encrypt credential fields (registry tokens, NPM password) during import - [x] Task 7: Create `docker-watcher.example.yaml` with documented example config - [x] Task 8: Wire seed import into `cmd/server/main.go` startup sequence ## Files to Modify/Create - `internal/crypto/crypto.go` — AES-256-GCM encrypt/decrypt - `internal/config/config.go` — YAML structs and parser - `internal/config/seed.go` — seed import logic (YAML → SQLite) - `docker-watcher.example.yaml` — example seed config - `cmd/server/main.go` — add seed import to startup ## Acceptance Criteria - Encrypt then decrypt round-trips correctly - Different plaintexts produce different ciphertexts (random nonce) - YAML parsing handles all fields from the seed format - Seed import creates projects, stages, registries, and settings in SQLite - Credentials are encrypted before storage - Import is idempotent — skipped if DB already has data ## Notes - ENCRYPTION_KEY is the only secret env var — everything else is encrypted in SQLite - Use GCM mode for authenticated encryption (integrity + confidentiality) - Seed import should be transactional — all or nothing - The example YAML should have placeholder values, not real credentials ## Review Checklist - [x] All tasks completed - [x] Crypto uses secure practices (random nonce, GCM, no ECB) - [x] No hardcoded keys or secrets - [x] YAML parsing validates required fields - [x] Import is transactional ## Handoff to Next Phase - `crypto.Encrypt(key, plaintext)` and `crypto.Decrypt(key, ciphertextHex)` handle AES-256-GCM encryption; ciphertext is hex-encoded with prepended nonce - `crypto.KeyFromEnv()` derives a `[32]byte` key from the `ENCRYPTION_KEY` env var via SHA-256 - `crypto.EncryptIfNotEmpty(key, value)` is a convenience wrapper that passes through empty strings unchanged - `config.ImportSeed(db, seedPath)` is the single entry point for seed import — called from `main.go` at startup - Import is idempotent: skipped if the DB already has projects or registries - Import is transactional: all inserts happen within a single SQLite transaction (rollback on any failure) - Registry `token` and settings `npm_password` are now stored encrypted in SQLite — later phases that read these fields must decrypt with `crypto.Decrypt(key, value)` - `store.DB()` method was added to expose the underlying `*sql.DB` for transaction use - Seed file path is configurable via `SEED_FILE` env var (default: `./docker-watcher.yaml`) - YAML validation ensures: `global.domain` is required, every project needs `image`, project registry references must exist, stages need `tag_pattern` - `go.sum` still does not exist — run `go mod tidy` when Go toolchain is available