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:
@@ -12,6 +12,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -30,8 +31,29 @@ import (
|
||||
// deliberately-unwired channel at scaffold time).
|
||||
type Config struct {
|
||||
// DBPath — sqlite database path. Default applied by Load if empty.
|
||||
// When encryption is configured, the file at this path is ciphertext
|
||||
// (AES-256-GCM); the daemon works on a tmpfs plaintext copy.
|
||||
DBPath string `json:"db_path"`
|
||||
|
||||
// DBKeyB64 — base64 (std encoding) of a raw 32-byte AES-256 key. Empty ⇒
|
||||
// the store is plaintext (dev/CI). Prefer DBKeyEnv over baking the key
|
||||
// into the config file. Exactly one of DBKeyB64/DBKeyEnv should be set.
|
||||
//
|
||||
// ponytail: raw key, no KDF — stdlib has no argon2/scrypt and x/crypto
|
||||
// isn't a dep. This is also the seam the L3 passkey cold-start key plugs
|
||||
// into later: the passkey op produces the 32 bytes and calls
|
||||
// store.OpenEncrypted directly, bypassing config.
|
||||
DBKeyB64 string `json:"db_key_b64,omitempty"`
|
||||
|
||||
// DBKeyEnv — name of an env var holding the base64 32-byte key. Takes
|
||||
// precedence over DBKeyB64. Lets systemd credentials / secrets managers
|
||||
// inject the key without it touching the config file.
|
||||
DBKeyEnv string `json:"db_key_env,omitempty"`
|
||||
|
||||
// DBTmpfs — plaintext working-copy path (RAM-backed). Empty ⇒ a stable
|
||||
// per-db path under /dev/shm. Only used when encryption is configured.
|
||||
DBTmpfs string `json:"db_tmpfs,omitempty"`
|
||||
|
||||
// SocketPath — the unix socket the IPC server listens on. Modules
|
||||
// connect here; the dir is created 0700, the socket chmod'd 0600 by
|
||||
// ipc.Listen. Default applied by Load if empty.
|
||||
@@ -322,6 +344,31 @@ func (c *Config) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DBEncryptionKey resolves the at-rest encryption key: DBKeyEnv (if set) wins
|
||||
// over DBKeyB64. Returns (nil, nil) when neither is set — the caller then opens
|
||||
// a plaintext store. A configured-but-invalid key is an error (fail closed,
|
||||
// never silently downgrade to plaintext).
|
||||
func (c *Config) DBEncryptionKey() ([]byte, error) {
|
||||
raw := c.DBKeyB64
|
||||
if c.DBKeyEnv != "" {
|
||||
raw = os.Getenv(c.DBKeyEnv)
|
||||
if raw == "" {
|
||||
return nil, fmt.Errorf("config: db_key_env %q is set but the env var is empty", c.DBKeyEnv)
|
||||
}
|
||||
}
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
key, err := base64.StdEncoding.DecodeString(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("config: db key is not valid base64: %w", err)
|
||||
}
|
||||
if len(key) != 32 {
|
||||
return nil, fmt.Errorf("config: db key must decode to 32 bytes, got %d", len(key))
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func defaultDataDir() string {
|
||||
if x := os.Getenv("XDG_DATA_HOME"); x != "" {
|
||||
return filepath.Join(x, "maven")
|
||||
|
||||
Reference in New Issue
Block a user