package seal import ( "context" "errors" "log/slog" "path/filepath" "testing" "git.wntrmute.dev/kyle/metacrypt/internal/barrier" "git.wntrmute.dev/kyle/metacrypt/internal/crypto" "git.wntrmute.dev/kyle/metacrypt/internal/db" ) func setupSeal(t *testing.T) (*Manager, func()) { t.Helper() dir := t.TempDir() database, err := db.Open(filepath.Join(dir, "test.db")) if err != nil { t.Fatalf("open db: %v", err) } if err := db.Migrate(database); err != nil { t.Fatalf("migrate: %v", err) } b := barrier.NewAESGCMBarrier(database) mgr := NewManager(database, b, slog.Default()) return mgr, func() { _ = database.Close() } } func TestSealInitializeAndUnseal(t *testing.T) { mgr, cleanup := setupSeal(t) defer cleanup() if err := mgr.CheckInitialized(); err != nil { t.Fatalf("CheckInitialized: %v", err) } if mgr.State() != StateUninitialized { t.Fatalf("state: got %v, want Uninitialized", mgr.State()) } password := []byte("test-password-123") // Use fast params for testing. params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1} if err := mgr.Initialize(context.Background(), password, params); err != nil { t.Fatalf("Initialize: %v", err) } if mgr.State() != StateUnsealed { t.Fatalf("state after init: got %v, want Unsealed", mgr.State()) } // Seal. if err := mgr.Seal(); err != nil { t.Fatalf("Seal: %v", err) } if mgr.State() != StateSealed { t.Fatalf("state after seal: got %v, want Sealed", mgr.State()) } // Unseal with correct password. if err := mgr.Unseal(password); err != nil { t.Fatalf("Unseal: %v", err) } if mgr.State() != StateUnsealed { t.Fatalf("state after unseal: got %v, want Unsealed", mgr.State()) } } func TestSealWrongPassword(t *testing.T) { mgr, cleanup := setupSeal(t) defer cleanup() _ = mgr.CheckInitialized() params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1} _ = mgr.Initialize(context.Background(), []byte("correct"), params) _ = mgr.Seal() err := mgr.Unseal([]byte("wrong")) if !errors.Is(err, ErrInvalidPassword) { t.Fatalf("expected ErrInvalidPassword, got: %v", err) } } func TestSealDoubleInitialize(t *testing.T) { mgr, cleanup := setupSeal(t) defer cleanup() _ = mgr.CheckInitialized() params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1} _ = mgr.Initialize(context.Background(), []byte("password"), params) err := mgr.Initialize(context.Background(), []byte("password"), params) if !errors.Is(err, ErrAlreadyInitialized) { t.Fatalf("expected ErrAlreadyInitialized, got: %v", err) } } func TestSealCheckInitializedPersists(t *testing.T) { dir := t.TempDir() dbPath := filepath.Join(dir, "test.db") // First: initialize. database, _ := db.Open(dbPath) _ = db.Migrate(database) b := barrier.NewAESGCMBarrier(database) mgr := NewManager(database, b, slog.Default()) _ = mgr.CheckInitialized() params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1} _ = mgr.Initialize(context.Background(), []byte("password"), params) _ = database.Close() // Second: reopen and check. database2, _ := db.Open(dbPath) defer func() { _ = database2.Close() }() b2 := barrier.NewAESGCMBarrier(database2) mgr2 := NewManager(database2, b2, slog.Default()) _ = mgr2.CheckInitialized() if mgr2.State() != StateSealed { t.Fatalf("state after reopen: got %v, want Sealed", mgr2.State()) } } func TestSealRotateMEK(t *testing.T) { mgr, cleanup := setupSeal(t) defer cleanup() ctx := context.Background() _ = mgr.CheckInitialized() password := []byte("test-password") params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1} _ = mgr.Initialize(ctx, password, params) // Create a DEK and write data through the barrier. b := mgr.Barrier() _ = b.CreateKey(ctx, "system") _ = b.CreateKey(ctx, "engine/ca/prod") _ = b.Put(ctx, "policy/rule1", []byte("policy-data")) _ = b.Put(ctx, "engine/ca/prod/cert", []byte("cert-data")) // Rotate MEK. if err := mgr.RotateMEK(ctx, password); err != nil { t.Fatalf("RotateMEK: %v", err) } // Data should still be readable. got, err := b.Get(ctx, "policy/rule1") if err != nil { t.Fatalf("Get after MEK rotation: %v", err) } if string(got) != "policy-data" { t.Fatalf("data: got %q", got) } got2, err := b.Get(ctx, "engine/ca/prod/cert") if err != nil { t.Fatalf("Get engine data after MEK rotation: %v", err) } if string(got2) != "cert-data" { t.Fatalf("data: got %q", got2) } // Seal and unseal with the same password should work // (the new MEK is now encrypted with the KWK). if err := mgr.Seal(); err != nil { t.Fatalf("Seal: %v", err) } if err := mgr.Unseal(password); err != nil { t.Fatalf("Unseal after MEK rotation: %v", err) } got3, err := b.Get(ctx, "engine/ca/prod/cert") if err != nil { t.Fatalf("Get after seal/unseal: %v", err) } if string(got3) != "cert-data" { t.Fatalf("data after seal/unseal: got %q", got3) } } func TestSealRotateMEKWrongPassword(t *testing.T) { mgr, cleanup := setupSeal(t) defer cleanup() ctx := context.Background() _ = mgr.CheckInitialized() params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1} _ = mgr.Initialize(ctx, []byte("correct"), params) err := mgr.RotateMEK(ctx, []byte("wrong")) if !errors.Is(err, ErrInvalidPassword) { t.Fatalf("expected ErrInvalidPassword, got: %v", err) } } func TestSealRotateMEKWhenSealed(t *testing.T) { mgr, cleanup := setupSeal(t) defer cleanup() ctx := context.Background() _ = mgr.CheckInitialized() params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1} _ = mgr.Initialize(ctx, []byte("password"), params) _ = mgr.Seal() err := mgr.RotateMEK(ctx, []byte("password")) if !errors.Is(err, ErrSealed) { t.Fatalf("expected ErrSealed, got: %v", err) } } func TestSealStateString(t *testing.T) { tests := []struct { want string state ServiceState }{ {want: "uninitialized", state: StateUninitialized}, {want: "sealed", state: StateSealed}, {want: "initializing", state: StateInitializing}, {want: "unsealed", state: StateUnsealed}, } for _, tt := range tests { if got := tt.state.String(); got != tt.want { t.Errorf("State(%d).String() = %q, want %q", tt.state, got, tt.want) } } }