package user import ( "context" "encoding/base64" "strings" "sync" "testing" "git.wntrmute.dev/kyle/metacrypt/internal/barrier" "git.wntrmute.dev/kyle/metacrypt/internal/engine" ) // memBarrier is an in-memory barrier for testing. type memBarrier struct { data map[string][]byte mu sync.RWMutex } func newMemBarrier() *memBarrier { return &memBarrier{data: make(map[string][]byte)} } func (m *memBarrier) Unseal(_ []byte) error { return nil } func (m *memBarrier) Seal() error { return nil } func (m *memBarrier) IsSealed() bool { return false } func (m *memBarrier) Get(_ context.Context, path string) ([]byte, error) { m.mu.RLock() defer m.mu.RUnlock() v, ok := m.data[path] if !ok { return nil, barrier.ErrNotFound } cp := make([]byte, len(v)) copy(cp, v) return cp, nil } func (m *memBarrier) Put(_ context.Context, path string, value []byte) error { m.mu.Lock() defer m.mu.Unlock() cp := make([]byte, len(value)) copy(cp, value) m.data[path] = cp return nil } func (m *memBarrier) Delete(_ context.Context, path string) error { m.mu.Lock() defer m.mu.Unlock() delete(m.data, path) return nil } func (m *memBarrier) List(_ context.Context, prefix string) ([]string, error) { m.mu.RLock() defer m.mu.RUnlock() var paths []string for k := range m.data { if strings.HasPrefix(k, prefix) { paths = append(paths, strings.TrimPrefix(k, prefix)) } } return paths, nil } func adminCaller() *engine.CallerInfo { return &engine.CallerInfo{Username: "admin", Roles: []string{"admin"}, IsAdmin: true} } func userCaller(name string) *engine.CallerInfo { return &engine.CallerInfo{Username: name, Roles: []string{"user"}, IsAdmin: false} } func guestCaller() *engine.CallerInfo { return &engine.CallerInfo{Username: "guest", Roles: []string{"guest"}, IsAdmin: false} } func setupEngine(t *testing.T) (*UserEngine, *memBarrier) { t.Helper() b := newMemBarrier() eng := NewUserEngine().(*UserEngine) //nolint:errcheck ctx := context.Background() config := map[string]interface{}{ "key_algorithm": "x25519", "sym_algorithm": "aes256-gcm", } if err := eng.Initialize(ctx, b, "engine/user/test/", config); err != nil { t.Fatalf("Initialize: %v", err) } return eng, b } func TestInitializeAndUnseal(t *testing.T) { eng, b := setupEngine(t) ctx := context.Background() // Register a user. resp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "register", CallerInfo: userCaller("alice"), }) if err != nil { t.Fatalf("register: %v", err) } if resp.Data["username"] != "alice" { t.Fatalf("expected username alice, got %v", resp.Data["username"]) } // Seal and unseal. if err := eng.Seal(); err != nil { t.Fatalf("seal: %v", err) } eng2 := NewUserEngine().(*UserEngine) //nolint:errcheck if err := eng2.Unseal(ctx, b, "engine/user/test/"); err != nil { t.Fatalf("unseal: %v", err) } // Verify alice's key is loaded. resp, err = eng2.HandleRequest(ctx, &engine.Request{ Operation: "get-public-key", CallerInfo: userCaller("bob"), Data: map[string]interface{}{"username": "alice"}, }) if err != nil { t.Fatalf("get-public-key after unseal: %v", err) } if resp.Data["username"] != "alice" { t.Fatalf("expected alice, got %v", resp.Data["username"]) } } func TestRegisterCreatesKeypair(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() resp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "register", CallerInfo: userCaller("alice"), }) if err != nil { t.Fatalf("register: %v", err) } pubKey, ok := resp.Data["public_key"].(string) if !ok || pubKey == "" { t.Fatal("expected non-empty public key") } // Decode to verify it's valid base64. raw, err := base64.StdEncoding.DecodeString(pubKey) if err != nil { t.Fatalf("decode public key: %v", err) } if len(raw) != 32 { // X25519 public key is 32 bytes t.Fatalf("expected 32-byte public key, got %d", len(raw)) } } func TestRegisterIdempotent(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() resp1, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "register", CallerInfo: userCaller("alice"), }) if err != nil { t.Fatalf("register 1: %v", err) } resp2, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "register", CallerInfo: userCaller("alice"), }) if err != nil { t.Fatalf("register 2: %v", err) } if resp1.Data["public_key"] != resp2.Data["public_key"] { t.Fatal("register should be idempotent") } } func TestEncryptDecryptSingleRecipient(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() // Register alice and bob. for _, name := range []string{"alice", "bob"} { _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "register", CallerInfo: userCaller(name), }) if err != nil { t.Fatalf("register %s: %v", name, err) } } // Alice encrypts to bob. encResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "encrypt", CallerInfo: userCaller("alice"), Data: map[string]interface{}{ "plaintext": "hello bob", "recipients": []interface{}{"bob"}, }, }) if err != nil { t.Fatalf("encrypt: %v", err) } envelope, ok := encResp.Data["envelope"].(string) if !ok || envelope == "" { t.Fatal("expected non-empty envelope") } // Bob decrypts. decResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "decrypt", CallerInfo: userCaller("bob"), Data: map[string]interface{}{"envelope": envelope}, }) if err != nil { t.Fatalf("decrypt: %v", err) } if decResp.Data["plaintext"] != "hello bob" { t.Fatalf("expected 'hello bob', got %v", decResp.Data["plaintext"]) } if decResp.Data["sender"] != "alice" { t.Fatalf("expected sender alice, got %v", decResp.Data["sender"]) } } func TestEncryptDecryptWithMetadata(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() for _, name := range []string{"alice", "bob"} { _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "register", CallerInfo: userCaller(name), }) if err != nil { t.Fatalf("register %s: %v", name, err) } } encResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "encrypt", CallerInfo: userCaller("alice"), Data: map[string]interface{}{ "plaintext": "secret message", "metadata": "important context", "recipients": []interface{}{"bob"}, }, }) if err != nil { t.Fatalf("encrypt: %v", err) } decResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "decrypt", CallerInfo: userCaller("bob"), Data: map[string]interface{}{"envelope": encResp.Data["envelope"]}, }) if err != nil { t.Fatalf("decrypt: %v", err) } if decResp.Data["plaintext"] != "secret message" { t.Fatalf("plaintext mismatch: %v", decResp.Data["plaintext"]) } if decResp.Data["metadata"] != "important context" { t.Fatalf("metadata mismatch: %v", decResp.Data["metadata"]) } } func TestMultiRecipientEncryptDecrypt(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() users := []string{"alice", "bob", "charlie"} for _, name := range users { _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "register", CallerInfo: userCaller(name), }) if err != nil { t.Fatalf("register %s: %v", name, err) } } // Alice encrypts to bob and charlie. encResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "encrypt", CallerInfo: userCaller("alice"), Data: map[string]interface{}{ "plaintext": "hello everyone", "recipients": []interface{}{"bob", "charlie"}, }, }) if err != nil { t.Fatalf("encrypt: %v", err) } envelope := encResp.Data["envelope"].(string) // Both bob and charlie can decrypt. for _, name := range []string{"bob", "charlie"} { decResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "decrypt", CallerInfo: userCaller(name), Data: map[string]interface{}{"envelope": envelope}, }) if err != nil { t.Fatalf("decrypt by %s: %v", name, err) } if decResp.Data["plaintext"] != "hello everyone" { t.Fatalf("%s: expected 'hello everyone', got %v", name, decResp.Data["plaintext"]) } } // Alice (not a recipient) cannot decrypt. _, err = eng.HandleRequest(ctx, &engine.Request{ Operation: "decrypt", CallerInfo: userCaller("alice"), Data: map[string]interface{}{"envelope": envelope}, }) if err == nil { t.Fatal("expected error when non-recipient decrypts") } } func TestReEncrypt(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() for _, name := range []string{"alice", "bob"} { _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "register", CallerInfo: userCaller(name), }) if err != nil { t.Fatalf("register %s: %v", name, err) } } // Alice encrypts to bob. encResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "encrypt", CallerInfo: userCaller("alice"), Data: map[string]interface{}{ "plaintext": "secret", "recipients": []interface{}{"bob"}, }, }) if err != nil { t.Fatalf("encrypt: %v", err) } // Bob re-encrypts. reEncResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "re-encrypt", CallerInfo: userCaller("bob"), Data: map[string]interface{}{"envelope": encResp.Data["envelope"]}, }) if err != nil { t.Fatalf("re-encrypt: %v", err) } // Bob can decrypt re-encrypted envelope. decResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "decrypt", CallerInfo: userCaller("bob"), Data: map[string]interface{}{"envelope": reEncResp.Data["envelope"]}, }) if err != nil { t.Fatalf("decrypt re-encrypted: %v", err) } if decResp.Data["plaintext"] != "secret" { t.Fatalf("expected 'secret', got %v", decResp.Data["plaintext"]) } if decResp.Data["sender"] != "bob" { t.Fatalf("expected sender bob after re-encrypt, got %v", decResp.Data["sender"]) } } func TestRotateKey(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() for _, name := range []string{"alice", "bob"} { _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "register", CallerInfo: userCaller(name), }) if err != nil { t.Fatalf("register %s: %v", name, err) } } // Alice encrypts to bob. encResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "encrypt", CallerInfo: userCaller("alice"), Data: map[string]interface{}{ "plaintext": "before rotation", "recipients": []interface{}{"bob"}, }, }) if err != nil { t.Fatalf("encrypt: %v", err) } // Bob rotates key. _, err = eng.HandleRequest(ctx, &engine.Request{ Operation: "rotate-key", CallerInfo: userCaller("bob"), }) if err != nil { t.Fatalf("rotate-key: %v", err) } // Old envelope should fail to decrypt (sender's pubkey is used to unwrap, // but the DEK was wrapped with old recipient key). _, err = eng.HandleRequest(ctx, &engine.Request{ Operation: "decrypt", CallerInfo: userCaller("bob"), Data: map[string]interface{}{"envelope": encResp.Data["envelope"]}, }) if err == nil { t.Fatal("expected decrypt to fail after key rotation") } // New encrypt/decrypt should work. encResp2, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "encrypt", CallerInfo: userCaller("alice"), Data: map[string]interface{}{ "plaintext": "after rotation", "recipients": []interface{}{"bob"}, }, }) if err != nil { t.Fatalf("encrypt after rotation: %v", err) } decResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "decrypt", CallerInfo: userCaller("bob"), Data: map[string]interface{}{"envelope": encResp2.Data["envelope"]}, }) if err != nil { t.Fatalf("decrypt after rotation: %v", err) } if decResp.Data["plaintext"] != "after rotation" { t.Fatalf("expected 'after rotation', got %v", decResp.Data["plaintext"]) } } func TestAutoProvisionOnEncrypt(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() // Encrypt without pre-registering anyone. encResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "encrypt", CallerInfo: userCaller("alice"), Data: map[string]interface{}{ "plaintext": "auto-provision test", "recipients": []interface{}{"bob"}, }, }) if err != nil { t.Fatalf("encrypt: %v", err) } // Both alice and bob should be auto-provisioned. bob can decrypt. decResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "decrypt", CallerInfo: userCaller("bob"), Data: map[string]interface{}{"envelope": encResp.Data["envelope"]}, }) if err != nil { t.Fatalf("decrypt: %v", err) } if decResp.Data["plaintext"] != "auto-provision test" { t.Fatalf("expected 'auto-provision test', got %v", decResp.Data["plaintext"]) } } func TestProvisionAdminOnly(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() // Non-admin cannot provision. _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "provision", CallerInfo: userCaller("alice"), Data: map[string]interface{}{"username": "bob"}, }) if err == nil { t.Fatal("expected error for non-admin provision") } // Admin can provision. resp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "provision", CallerInfo: adminCaller(), Data: map[string]interface{}{"username": "bob"}, }) if err != nil { t.Fatalf("admin provision: %v", err) } if resp.Data["username"] != "bob" { t.Fatalf("expected bob, got %v", resp.Data["username"]) } } func TestDecryptSelfOnly(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() for _, name := range []string{"alice", "bob", "charlie"} { _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "register", CallerInfo: userCaller(name), }) if err != nil { t.Fatalf("register %s: %v", name, err) } } // Alice encrypts to bob only. encResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "encrypt", CallerInfo: userCaller("alice"), Data: map[string]interface{}{ "plaintext": "for bob only", "recipients": []interface{}{"bob"}, }, }) if err != nil { t.Fatalf("encrypt: %v", err) } // Charlie cannot decrypt. _, err = eng.HandleRequest(ctx, &engine.Request{ Operation: "decrypt", CallerInfo: userCaller("charlie"), Data: map[string]interface{}{"envelope": encResp.Data["envelope"]}, }) if err == nil { t.Fatal("expected error when charlie tries to decrypt bob's message") } } func TestGuestRejected(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "list-users", CallerInfo: guestCaller(), }) if err == nil { t.Fatal("expected guest to be rejected from list-users") } _, err = eng.HandleRequest(ctx, &engine.Request{ Operation: "get-public-key", CallerInfo: guestCaller(), Data: map[string]interface{}{"username": "alice"}, }) if err == nil { t.Fatal("expected guest to be rejected from get-public-key") } } func TestDeleteUser(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() // Register bob. _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "register", CallerInfo: userCaller("bob"), }) if err != nil { t.Fatalf("register: %v", err) } // Non-admin cannot delete. _, err = eng.HandleRequest(ctx, &engine.Request{ Operation: "delete-user", CallerInfo: userCaller("bob"), Data: map[string]interface{}{"username": "bob"}, }) if err == nil { t.Fatal("expected error for non-admin delete") } // Admin can delete. _, err = eng.HandleRequest(ctx, &engine.Request{ Operation: "delete-user", CallerInfo: adminCaller(), Data: map[string]interface{}{"username": "bob"}, }) if err != nil { t.Fatalf("admin delete: %v", err) } // bob should no longer exist. _, err = eng.HandleRequest(ctx, &engine.Request{ Operation: "get-public-key", CallerInfo: userCaller("alice"), Data: map[string]interface{}{"username": "bob"}, }) if err == nil { t.Fatal("expected user not found after delete") } } func TestMaxRecipientsLimit(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "register", CallerInfo: userCaller("alice"), }) if err != nil { t.Fatalf("register: %v", err) } // Build 101 recipients. recipients := make([]interface{}, 101) for i := range recipients { recipients[i] = "user" + strings.Repeat("x", 5) + string(rune('a'+i%26)) + string(rune('0'+i/26)) } _, err = eng.HandleRequest(ctx, &engine.Request{ Operation: "encrypt", CallerInfo: userCaller("alice"), Data: map[string]interface{}{ "plaintext": "test", "recipients": recipients, }, }) if err == nil { t.Fatal("expected error for too many recipients") } if !strings.Contains(err.Error(), "too many recipients") { t.Fatalf("expected 'too many recipients' error, got: %v", err) } } func TestListUsers(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() for _, name := range []string{"alice", "bob"} { _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "register", CallerInfo: userCaller(name), }) if err != nil { t.Fatalf("register %s: %v", name, err) } } resp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "list-users", CallerInfo: userCaller("alice"), }) if err != nil { t.Fatalf("list-users: %v", err) } users, ok := resp.Data["users"].([]interface{}) if !ok { t.Fatal("expected users list") } if len(users) != 2 { t.Fatalf("expected 2 users, got %d", len(users)) } } func TestP256Algorithm(t *testing.T) { b := newMemBarrier() eng := NewUserEngine().(*UserEngine) //nolint:errcheck ctx := context.Background() config := map[string]interface{}{ "key_algorithm": "ecdh-p256", } if err := eng.Initialize(ctx, b, "engine/user/p256/", config); err != nil { t.Fatalf("Initialize: %v", err) } for _, name := range []string{"alice", "bob"} { _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "register", CallerInfo: userCaller(name), }) if err != nil { t.Fatalf("register %s: %v", name, err) } } encResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "encrypt", CallerInfo: userCaller("alice"), Data: map[string]interface{}{ "plaintext": "p256 test", "recipients": []interface{}{"bob"}, }, }) if err != nil { t.Fatalf("encrypt: %v", err) } decResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "decrypt", CallerInfo: userCaller("bob"), Data: map[string]interface{}{"envelope": encResp.Data["envelope"]}, }) if err != nil { t.Fatalf("decrypt: %v", err) } if decResp.Data["plaintext"] != "p256 test" { t.Fatalf("expected 'p256 test', got %v", decResp.Data["plaintext"]) } }