package barrier import ( "context" "errors" "path/filepath" "testing" "git.wntrmute.dev/mc/metacrypt/internal/crypto" "git.wntrmute.dev/mc/metacrypt/internal/db" ) func setupBarrier(t *testing.T) (*AESGCMBarrier, 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 := NewAESGCMBarrier(database) return b, func() { _ = database.Close() } } func TestBarrierSealUnseal(t *testing.T) { b, cleanup := setupBarrier(t) defer cleanup() if !b.IsSealed() { t.Fatal("new barrier should be sealed") } mek, _ := crypto.GenerateKey() if err := b.Unseal(mek); err != nil { t.Fatalf("Unseal: %v", err) } if b.IsSealed() { t.Fatal("barrier should be unsealed") } if err := b.Seal(); err != nil { t.Fatalf("Seal: %v", err) } if !b.IsSealed() { t.Fatal("barrier should be sealed after Seal()") } } func TestBarrierPutGet(t *testing.T) { b, cleanup := setupBarrier(t) defer cleanup() ctx := context.Background() mek, _ := crypto.GenerateKey() _ = b.Unseal(mek) data := []byte("test value") if err := b.Put(ctx, "test/path", data); err != nil { t.Fatalf("Put: %v", err) } got, err := b.Get(ctx, "test/path") if err != nil { t.Fatalf("Get: %v", err) } if string(got) != string(data) { t.Fatalf("Get: got %q, want %q", got, data) } } func TestBarrierGetNotFound(t *testing.T) { b, cleanup := setupBarrier(t) defer cleanup() ctx := context.Background() mek, _ := crypto.GenerateKey() _ = b.Unseal(mek) _, err := b.Get(ctx, "nonexistent") if !errors.Is(err, ErrNotFound) { t.Fatalf("expected ErrNotFound, got: %v", err) } } func TestBarrierDelete(t *testing.T) { b, cleanup := setupBarrier(t) defer cleanup() ctx := context.Background() mek, _ := crypto.GenerateKey() _ = b.Unseal(mek) _ = b.Put(ctx, "test/delete-me", []byte("data")) if err := b.Delete(ctx, "test/delete-me"); err != nil { t.Fatalf("Delete: %v", err) } _, err := b.Get(ctx, "test/delete-me") if !errors.Is(err, ErrNotFound) { t.Fatalf("expected ErrNotFound after delete, got: %v", err) } } func TestBarrierList(t *testing.T) { b, cleanup := setupBarrier(t) defer cleanup() ctx := context.Background() mek, _ := crypto.GenerateKey() _ = b.Unseal(mek) _ = b.Put(ctx, "engine/ca/default/config", []byte("cfg")) _ = b.Put(ctx, "engine/ca/default/dek", []byte("key")) _ = b.Put(ctx, "engine/transit/main/config", []byte("cfg")) paths, err := b.List(ctx, "engine/ca/") if err != nil { t.Fatalf("List: %v", err) } if len(paths) != 2 { t.Fatalf("List: got %d paths, want 2", len(paths)) } } func TestBarrierSealedOperations(t *testing.T) { b, cleanup := setupBarrier(t) defer cleanup() ctx := context.Background() if _, err := b.Get(ctx, "test"); !errors.Is(err, ErrSealed) { t.Fatalf("Get when sealed: expected ErrSealed, got: %v", err) } if err := b.Put(ctx, "test", []byte("data")); !errors.Is(err, ErrSealed) { t.Fatalf("Put when sealed: expected ErrSealed, got: %v", err) } if err := b.Delete(ctx, "test"); !errors.Is(err, ErrSealed) { t.Fatalf("Delete when sealed: expected ErrSealed, got: %v", err) } if _, err := b.List(ctx, "test"); !errors.Is(err, ErrSealed) { t.Fatalf("List when sealed: expected ErrSealed, got: %v", err) } } func TestBarrierOverwrite(t *testing.T) { b, cleanup := setupBarrier(t) defer cleanup() ctx := context.Background() mek, _ := crypto.GenerateKey() _ = b.Unseal(mek) _ = b.Put(ctx, "test/overwrite", []byte("v1")) _ = b.Put(ctx, "test/overwrite", []byte("v2")) got, _ := b.Get(ctx, "test/overwrite") if string(got) != "v2" { t.Fatalf("overwrite: got %q, want %q", got, "v2") } } // --- DEK / Key Registry Tests --- func TestBarrierCreateKey(t *testing.T) { b, cleanup := setupBarrier(t) defer cleanup() ctx := context.Background() mek, _ := crypto.GenerateKey() _ = b.Unseal(mek) if err := b.CreateKey(ctx, "engine/ca/prod"); err != nil { t.Fatalf("CreateKey: %v", err) } // Duplicate should be a no-op. if err := b.CreateKey(ctx, "engine/ca/prod"); err != nil { t.Fatalf("CreateKey duplicate: %v", err) } keys, err := b.ListKeys(ctx) if err != nil { t.Fatalf("ListKeys: %v", err) } if len(keys) != 1 { t.Fatalf("expected 1 key, got %d", len(keys)) } if keys[0].KeyID != "engine/ca/prod" { t.Fatalf("key ID: got %q, want %q", keys[0].KeyID, "engine/ca/prod") } } func TestBarrierDEKEncryption(t *testing.T) { b, cleanup := setupBarrier(t) defer cleanup() ctx := context.Background() mek, _ := crypto.GenerateKey() _ = b.Unseal(mek) // Create a DEK for ca/prod. _ = b.CreateKey(ctx, "engine/ca/prod") // Write data under the engine path — should use DEK. data := []byte("engine secret data") if err := b.Put(ctx, "engine/ca/prod/config.json", data); err != nil { t.Fatalf("Put: %v", err) } got, err := b.Get(ctx, "engine/ca/prod/config.json") if err != nil { t.Fatalf("Get: %v", err) } if string(got) != string(data) { t.Fatalf("roundtrip: got %q, want %q", got, data) } // Verify the raw ciphertext is v2 format. var raw []byte err = b.db.QueryRowContext(ctx, "SELECT value FROM barrier_entries WHERE path = ?", "engine/ca/prod/config.json").Scan(&raw) if err != nil { t.Fatalf("read raw: %v", err) } if raw[0] != crypto.BarrierVersionV2 { t.Fatalf("expected v2 ciphertext, got version %d", raw[0]) } } func TestBarrierV1FallbackWithoutDEK(t *testing.T) { b, cleanup := setupBarrier(t) defer cleanup() ctx := context.Background() mek, _ := crypto.GenerateKey() _ = b.Unseal(mek) // Write system data without any DEK — should use v1 with MEK. data := []byte("system data") if err := b.Put(ctx, "policy/rule1", data); err != nil { t.Fatalf("Put: %v", err) } got, err := b.Get(ctx, "policy/rule1") if err != nil { t.Fatalf("Get: %v", err) } if string(got) != string(data) { t.Fatalf("roundtrip: got %q, want %q", got, data) } } func TestBarrierSystemDEK(t *testing.T) { b, cleanup := setupBarrier(t) defer cleanup() ctx := context.Background() mek, _ := crypto.GenerateKey() _ = b.Unseal(mek) // Create system DEK. _ = b.CreateKey(ctx, "system") // Write system data — should use system DEK with v2 format. data := []byte("system with dek") if err := b.Put(ctx, "policy/rule1", data); err != nil { t.Fatalf("Put: %v", err) } got, err := b.Get(ctx, "policy/rule1") if err != nil { t.Fatalf("Get: %v", err) } if string(got) != string(data) { t.Fatalf("roundtrip: got %q, want %q", got, data) } // Verify v2 format. var raw []byte _ = b.db.QueryRowContext(ctx, "SELECT value FROM barrier_entries WHERE path = ?", "policy/rule1").Scan(&raw) if raw[0] != crypto.BarrierVersionV2 { t.Fatalf("expected v2 ciphertext, got version %d", raw[0]) } } func TestBarrierRotateKey(t *testing.T) { b, cleanup := setupBarrier(t) defer cleanup() ctx := context.Background() mek, _ := crypto.GenerateKey() _ = b.Unseal(mek) _ = b.CreateKey(ctx, "engine/ca/prod") // Write some data. _ = b.Put(ctx, "engine/ca/prod/cert1", []byte("cert-data-1")) _ = b.Put(ctx, "engine/ca/prod/cert2", []byte("cert-data-2")) // Rotate the key. if err := b.RotateKey(ctx, "engine/ca/prod"); err != nil { t.Fatalf("RotateKey: %v", err) } // Data should still be readable. got, err := b.Get(ctx, "engine/ca/prod/cert1") if err != nil { t.Fatalf("Get after rotation: %v", err) } if string(got) != "cert-data-1" { t.Fatalf("data corrupted after rotation: %q", got) } got2, err := b.Get(ctx, "engine/ca/prod/cert2") if err != nil { t.Fatalf("Get after rotation: %v", err) } if string(got2) != "cert-data-2" { t.Fatalf("data corrupted after rotation: %q", got2) } // Check key version incremented. keys, _ := b.ListKeys(ctx) for _, k := range keys { if k.KeyID == "engine/ca/prod" && k.Version != 2 { t.Fatalf("expected version 2 after rotation, got %d", k.Version) } } } func TestBarrierRotateKeyNotFound(t *testing.T) { b, cleanup := setupBarrier(t) defer cleanup() ctx := context.Background() mek, _ := crypto.GenerateKey() _ = b.Unseal(mek) err := b.RotateKey(ctx, "nonexistent") if !errors.Is(err, ErrKeyNotFound) { t.Fatalf("expected ErrKeyNotFound, got: %v", err) } } func TestBarrierMigrateToV2(t *testing.T) { b, cleanup := setupBarrier(t) defer cleanup() ctx := context.Background() mek, _ := crypto.GenerateKey() _ = b.Unseal(mek) // Write v1 data (no DEKs registered, so it uses MEK). _ = b.Put(ctx, "policy/rule1", []byte("policy-data")) _ = b.Put(ctx, "engine/ca/prod/config", []byte("ca-config")) _ = b.Put(ctx, "engine/transit/main/key1", []byte("transit-key")) // Migrate. migrated, err := b.MigrateToV2(ctx) if err != nil { t.Fatalf("MigrateToV2: %v", err) } if migrated != 3 { t.Fatalf("expected 3 entries migrated, got %d", migrated) } // All data should still be readable. got, err := b.Get(ctx, "policy/rule1") if err != nil { t.Fatalf("Get policy after migration: %v", err) } if string(got) != "policy-data" { t.Fatalf("policy data: got %q", got) } got, err = b.Get(ctx, "engine/ca/prod/config") if err != nil { t.Fatalf("Get engine data after migration: %v", err) } if string(got) != "ca-config" { t.Fatalf("engine data: got %q", got) } // Running again should migrate 0 (all already v2). migrated2, err := b.MigrateToV2(ctx) if err != nil { t.Fatalf("MigrateToV2 second run: %v", err) } if migrated2 != 0 { t.Fatalf("expected 0 entries on second migration, got %d", migrated2) } } func TestBarrierSealUnsealPreservesDEKs(t *testing.T) { b, cleanup := setupBarrier(t) defer cleanup() ctx := context.Background() mek, _ := crypto.GenerateKey() _ = b.Unseal(mek) // Create DEK and write data. _ = b.CreateKey(ctx, "engine/ca/prod") _ = b.Put(ctx, "engine/ca/prod/secret", []byte("my-secret")) // Seal and unseal. _ = b.Seal() _ = b.Unseal(mek) // Data should still be readable (DEKs reloaded from barrier_keys). got, err := b.Get(ctx, "engine/ca/prod/secret") if err != nil { t.Fatalf("Get after seal/unseal: %v", err) } if string(got) != "my-secret" { t.Fatalf("data after seal/unseal: got %q", got) } } func TestBarrierReWrapKeys(t *testing.T) { b, cleanup := setupBarrier(t) defer cleanup() ctx := context.Background() mek, _ := crypto.GenerateKey() _ = b.Unseal(mek) _ = b.CreateKey(ctx, "system") _ = b.CreateKey(ctx, "engine/ca/prod") _ = b.Put(ctx, "policy/rule1", []byte("policy")) _ = b.Put(ctx, "engine/ca/prod/cert", []byte("cert")) // Re-wrap with new MEK. newMEK, _ := crypto.GenerateKey() if err := b.ReWrapKeys(ctx, newMEK); err != nil { t.Fatalf("ReWrapKeys: %v", err) } // Data should still be readable. got, _ := b.Get(ctx, "policy/rule1") if string(got) != "policy" { t.Fatalf("policy after rewrap: got %q", got) } got2, _ := b.Get(ctx, "engine/ca/prod/cert") if string(got2) != "cert" { t.Fatalf("cert after rewrap: got %q", got2) } // Seal and unseal with new MEK should work. _ = b.Seal() if err := b.Unseal(newMEK); err != nil { t.Fatalf("Unseal with new MEK: %v", err) } got3, err := b.Get(ctx, "engine/ca/prod/cert") if err != nil { t.Fatalf("Get after unseal with new MEK: %v", err) } if string(got3) != "cert" { t.Fatalf("data after new MEK unseal: got %q", got3) } }