feat(store): envelope DEK with admin/agent wrap slots

Open() now opens LOCKED; InitKeys generates a DEK sealed under both KEKs;
Unlock loads it from the role's slot (admin slot has no agent fallback).
s.key becomes the DEK, so account/mail crypto is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 22:52:21 +01:00
parent c52f30898b
commit cb0425f18d
4 changed files with 224 additions and 14 deletions
+8 -12
View File
@@ -5,29 +5,25 @@ import (
"testing"
)
func testKey() []byte {
k := make([]byte, 32)
for i := range k {
k[i] = byte(i)
}
return k
}
// openTemp opens a fresh store in a temp dir.
// openTemp opens a fresh store in a temp dir and initialises keys so that
// account tests (which do crypto) work without needing their own setup.
func openTemp(t *testing.T) *Store {
t.Helper()
p := filepath.Join(t.TempDir(), "emcli.db")
s, err := Open(p, testKey())
s, err := Open(p)
if err != nil {
t.Fatalf("Open: %v", err)
}
if err := s.InitKeys(k(0xAA), k(0xBB)); err != nil {
t.Fatalf("InitKeys: %v", err)
}
t.Cleanup(func() { s.Close() })
return s
}
func TestOpenCreatesSchemaAndIsIdempotent(t *testing.T) {
p := filepath.Join(t.TempDir(), "emcli.db")
s, err := Open(p, testKey())
s, err := Open(p)
if err != nil {
t.Fatalf("first Open: %v", err)
}
@@ -38,7 +34,7 @@ func TestOpenCreatesSchemaAndIsIdempotent(t *testing.T) {
s.Close()
// Re-open: must not error or duplicate.
s2, err := Open(p, testKey())
s2, err := Open(p)
if err != nil {
t.Fatalf("second Open: %v", err)
}