Add user-to-user encryption engine with ECDH key exchange and AES-256-GCM
Implements the complete user engine for multi-recipient envelope encryption: - ECDH key agreement (X25519, P-256, P-384) with HKDF-derived wrapping keys - Per-message random DEK wrapped individually for each recipient - 9 operations: register, provision, get-public-key, list-users, encrypt, decrypt, re-encrypt, rotate-key, delete-user - Auto-provisioning of sender and recipients on encrypt - Role-based authorization (admin-only provision/delete, user-only decrypt) - gRPC UserService with proto definitions and REST API routes - 16 comprehensive tests covering lifecycle, crypto roundtrips, multi-recipient, key rotation, auth enforcement, and algorithm variants Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
739
internal/engine/user/user_test.go
Normal file
739
internal/engine/user/user_test.go
Normal file
@@ -0,0 +1,739 @@
|
||||
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"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user