store: at-rest encryption + schema-migration runner
Two spine infra items (feature-ranking #1, part of the migration prereq): - migrations.go: PRAGMA user_version runner, empty (no-op) migration slice, one tx per step, fail-closed. Mechanism in place before any real schema change needs it. - crypt.go: file-level at-rest encryption. On-disk file is always AES-256-GCM ciphertext; decrypted to a tmpfs working copy modernc sqlite operates on; re-encrypted atomically on Close, plaintext wiped, key zeroed. Pure stdlib, CGO stays off. Fails closed on wrong key/tamper, never falls back to plaintext. Key is a 32-byte seam (config db_key_b64/db_key_env today; the passkey-derived L3 cold-start key plugs into the same seam later). Chosen over cgo SQLCipher (would force libsqlcipher + CGO across the project) and over the ncruces page-level VFS (swaps the driver project-wide); noted as the upgrade path in a ponytail: comment. Threat model is disk-at-rest only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+14
-1
@@ -69,7 +69,20 @@ func run(args []string) error {
|
||||
defer stop()
|
||||
|
||||
// ----- store (the unlocked handle; core = the only key-holder) -----
|
||||
st, err := store.Open(ctx, cfg.DBPath)
|
||||
// The cold-start unlock dance (L3 passkey → key bytes) is not yet wired;
|
||||
// today the key comes from config/env. When a key is present the on-disk
|
||||
// file is ciphertext and we work on a tmpfs plaintext copy; no key ⇒
|
||||
// plaintext store (dev/CI). A configured-but-broken key fails closed.
|
||||
key, err := cfg.DBEncryptionKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var st *store.Store
|
||||
if key != nil {
|
||||
st, err = store.OpenEncrypted(ctx, cfg.DBPath, cfg.DBTmpfs, key)
|
||||
} else {
|
||||
st, err = store.Open(ctx, cfg.DBPath)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("open store: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user