Files
metacrypt/internal/engine/transit/transit_test.go
Kyle Isom bbe382dc10 Migrate module path from kyle/ to mc/ org
All import paths updated to git.wntrmute.dev/mc/. Bumps mcdsl to v1.2.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:05:59 -07:00

1026 lines
27 KiB
Go

package transit
import (
"context"
"encoding/base64"
"strings"
"sync"
"testing"
"git.wntrmute.dev/mc/metacrypt/internal/barrier"
"git.wntrmute.dev/mc/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() *engine.CallerInfo {
return &engine.CallerInfo{Username: "user", Roles: []string{"user"}, IsAdmin: false}
}
func guestCaller() *engine.CallerInfo {
return &engine.CallerInfo{Username: "guest", Roles: []string{"guest"}, IsAdmin: false}
}
func setupEngine(t *testing.T) (*TransitEngine, *memBarrier) {
t.Helper()
b := newMemBarrier()
eng := NewTransitEngine()
ctx := context.Background()
mountPath := "engine/transit/test/"
err := eng.Initialize(ctx, b, mountPath, nil)
if err != nil {
t.Fatalf("Initialize: %v", err)
}
te := eng.(*TransitEngine)
return te, b
}
func setupEngineWithUnseal(t *testing.T) (*TransitEngine, *memBarrier) {
t.Helper()
te, b := setupEngine(t)
// Seal and unseal to test the full lifecycle.
if err := te.Seal(); err != nil {
t.Fatalf("Seal: %v", err)
}
eng2 := NewTransitEngine()
ctx := context.Background()
if err := eng2.Unseal(ctx, b, "engine/transit/test/"); err != nil {
t.Fatalf("Unseal: %v", err)
}
return eng2.(*TransitEngine), b
}
func createKey(t *testing.T, te *TransitEngine, name, keyType string) {
t.Helper()
ctx := context.Background()
_, err := te.HandleRequest(ctx, &engine.Request{
Operation: "create-key",
CallerInfo: adminCaller(),
Data: map[string]interface{}{
"name": name,
"type": keyType,
},
})
if err != nil {
t.Fatalf("create-key %s: %v", name, err)
}
}
func TestInitializeAndUnseal(t *testing.T) {
te, b := setupEngine(t)
// Create a key so unseal has something to load.
createKey(t, te, "mykey", "aes256-gcm")
if err := te.Seal(); err != nil {
t.Fatalf("Seal: %v", err)
}
eng2 := NewTransitEngine()
if err := eng2.Unseal(context.Background(), b, "engine/transit/test/"); err != nil {
t.Fatalf("Unseal: %v", err)
}
te2 := eng2.(*TransitEngine)
if _, ok := te2.keys["mykey"]; !ok {
t.Fatal("expected key 'mykey' after unseal")
}
}
func TestCreateKeyAllTypes(t *testing.T) {
types := []string{"aes256-gcm", "chacha20-poly", "ed25519", "ecdsa-p256", "ecdsa-p384", "hmac-sha256", "hmac-sha512"}
for _, kt := range types {
t.Run(kt, func(t *testing.T) {
te, _ := setupEngine(t)
createKey(t, te, "key-"+kt, kt)
ks, ok := te.keys["key-"+kt]
if !ok {
t.Fatal("key not created")
}
if ks.config.CurrentVersion != 1 {
t.Fatalf("expected version 1, got %d", ks.config.CurrentVersion)
}
if ks.config.MinDecryptionVersion != 1 {
t.Fatalf("expected min_decryption_version 1, got %d", ks.config.MinDecryptionVersion)
}
})
}
}
func TestEncryptDecryptAES(t *testing.T) {
te, _ := setupEngine(t)
createKey(t, te, "aes-key", "aes256-gcm")
ctx := context.Background()
plaintext := base64.StdEncoding.EncodeToString([]byte("hello world"))
// Encrypt.
resp, err := te.HandleRequest(ctx, &engine.Request{
Operation: "encrypt",
CallerInfo: userCaller(),
Data: map[string]interface{}{
"key": "aes-key",
"plaintext": plaintext,
},
})
if err != nil {
t.Fatalf("encrypt: %v", err)
}
ct, _ := resp.Data["ciphertext"].(string)
if !strings.HasPrefix(ct, "metacrypt:v1:") {
t.Fatalf("unexpected ciphertext format: %s", ct)
}
// Decrypt.
resp, err = te.HandleRequest(ctx, &engine.Request{
Operation: "decrypt",
CallerInfo: userCaller(),
Data: map[string]interface{}{
"key": "aes-key",
"ciphertext": ct,
},
})
if err != nil {
t.Fatalf("decrypt: %v", err)
}
ptB64, _ := resp.Data["plaintext"].(string)
decoded, _ := base64.StdEncoding.DecodeString(ptB64)
if string(decoded) != "hello world" {
t.Fatalf("expected 'hello world', got %q", string(decoded))
}
}
func TestEncryptDecryptChaCha(t *testing.T) {
te, _ := setupEngine(t)
createKey(t, te, "chacha-key", "chacha20-poly")
ctx := context.Background()
plaintext := base64.StdEncoding.EncodeToString([]byte("secret data"))
resp, err := te.HandleRequest(ctx, &engine.Request{
Operation: "encrypt",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "chacha-key", "plaintext": plaintext},
})
if err != nil {
t.Fatalf("encrypt: %v", err)
}
ct, _ := resp.Data["ciphertext"].(string)
if !strings.HasPrefix(ct, "metacrypt:v1:") {
t.Fatalf("unexpected ciphertext format: %s", ct)
}
resp, err = te.HandleRequest(ctx, &engine.Request{
Operation: "decrypt",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "chacha-key", "ciphertext": ct},
})
if err != nil {
t.Fatalf("decrypt: %v", err)
}
ptB64, _ := resp.Data["plaintext"].(string)
decoded, _ := base64.StdEncoding.DecodeString(ptB64)
if string(decoded) != "secret data" {
t.Fatalf("expected 'secret data', got %q", string(decoded))
}
}
func TestEncryptWithContext(t *testing.T) {
te, _ := setupEngine(t)
createKey(t, te, "ctx-key", "aes256-gcm")
ctx := context.Background()
plaintext := base64.StdEncoding.EncodeToString([]byte("context test"))
aadB64 := base64.StdEncoding.EncodeToString([]byte("my-context"))
resp, err := te.HandleRequest(ctx, &engine.Request{
Operation: "encrypt",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "ctx-key", "plaintext": plaintext, "context": aadB64},
})
if err != nil {
t.Fatalf("encrypt: %v", err)
}
ct, _ := resp.Data["ciphertext"].(string)
// Decrypt with correct context.
resp, err = te.HandleRequest(ctx, &engine.Request{
Operation: "decrypt",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "ctx-key", "ciphertext": ct, "context": aadB64},
})
if err != nil {
t.Fatalf("decrypt: %v", err)
}
// Decrypt with wrong context should fail.
wrongCtx := base64.StdEncoding.EncodeToString([]byte("wrong-context"))
_, err = te.HandleRequest(ctx, &engine.Request{
Operation: "decrypt",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "ctx-key", "ciphertext": ct, "context": wrongCtx},
})
if err == nil {
t.Fatal("expected decrypt to fail with wrong context")
}
}
func TestKeyRotationAndDecrypt(t *testing.T) {
te, _ := setupEngine(t)
createKey(t, te, "rot-key", "aes256-gcm")
ctx := context.Background()
plaintext := base64.StdEncoding.EncodeToString([]byte("rotate me"))
// Encrypt with v1.
resp, err := te.HandleRequest(ctx, &engine.Request{
Operation: "encrypt",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "rot-key", "plaintext": plaintext},
})
if err != nil {
t.Fatalf("encrypt v1: %v", err)
}
ctV1, _ := resp.Data["ciphertext"].(string)
// Rotate.
_, err = te.HandleRequest(ctx, &engine.Request{
Operation: "rotate-key",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "rot-key"},
})
if err != nil {
t.Fatalf("rotate: %v", err)
}
// Encrypt with v2.
resp, err = te.HandleRequest(ctx, &engine.Request{
Operation: "encrypt",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "rot-key", "plaintext": plaintext},
})
if err != nil {
t.Fatalf("encrypt v2: %v", err)
}
ctV2, _ := resp.Data["ciphertext"].(string)
if !strings.HasPrefix(ctV2, "metacrypt:v2:") {
t.Fatalf("expected v2 ciphertext, got %s", ctV2)
}
// Both ciphertexts should decrypt.
for _, ct := range []string{ctV1, ctV2} {
resp, err = te.HandleRequest(ctx, &engine.Request{
Operation: "decrypt",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "rot-key", "ciphertext": ct},
})
if err != nil {
t.Fatalf("decrypt: %v", err)
}
ptB64, _ := resp.Data["plaintext"].(string)
decoded, _ := base64.StdEncoding.DecodeString(ptB64)
if string(decoded) != "rotate me" {
t.Fatalf("expected 'rotate me', got %q", string(decoded))
}
}
}
func TestUpdateKeyConfig(t *testing.T) {
te, _ := setupEngine(t)
createKey(t, te, "cfg-key", "aes256-gcm")
ctx := context.Background()
plaintext := base64.StdEncoding.EncodeToString([]byte("old data"))
// Encrypt with v1.
resp, err := te.HandleRequest(ctx, &engine.Request{
Operation: "encrypt",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "cfg-key", "plaintext": plaintext},
})
if err != nil {
t.Fatalf("encrypt: %v", err)
}
ctV1, _ := resp.Data["ciphertext"].(string)
// Rotate to v2.
_, err = te.HandleRequest(ctx, &engine.Request{
Operation: "rotate-key",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "cfg-key"},
})
if err != nil {
t.Fatalf("rotate: %v", err)
}
// Advance min_decryption_version to 2.
_, err = te.HandleRequest(ctx, &engine.Request{
Operation: "update-key-config",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "cfg-key", "min_decryption_version": float64(2)},
})
if err != nil {
t.Fatalf("update-key-config: %v", err)
}
// v1 ciphertext should be rejected.
_, err = te.HandleRequest(ctx, &engine.Request{
Operation: "decrypt",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "cfg-key", "ciphertext": ctV1},
})
if err == nil {
t.Fatal("expected decrypt to fail for v1 ciphertext")
}
// Cannot decrease min_decryption_version.
_, err = te.HandleRequest(ctx, &engine.Request{
Operation: "update-key-config",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "cfg-key", "min_decryption_version": float64(1)},
})
if err == nil {
t.Fatal("expected error decreasing min_decryption_version")
}
// Cannot exceed current version.
_, err = te.HandleRequest(ctx, &engine.Request{
Operation: "update-key-config",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "cfg-key", "min_decryption_version": float64(99)},
})
if err == nil {
t.Fatal("expected error exceeding current version")
}
}
func TestTrimKey(t *testing.T) {
te, _ := setupEngine(t)
createKey(t, te, "trim-key", "aes256-gcm")
ctx := context.Background()
// Rotate to v2, v3.
for i := 0; i < 2; i++ {
_, err := te.HandleRequest(ctx, &engine.Request{
Operation: "rotate-key",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "trim-key"},
})
if err != nil {
t.Fatalf("rotate: %v", err)
}
}
// Set min_decryption_version to 2.
_, err := te.HandleRequest(ctx, &engine.Request{
Operation: "update-key-config",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "trim-key", "min_decryption_version": float64(2)},
})
if err != nil {
t.Fatalf("update-key-config: %v", err)
}
// Trim.
resp, err := te.HandleRequest(ctx, &engine.Request{
Operation: "trim-key",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "trim-key"},
})
if err != nil {
t.Fatalf("trim-key: %v", err)
}
trimmed, _ := resp.Data["trimmed"].(int)
if trimmed != 1 {
t.Fatalf("expected 1 trimmed, got %d", trimmed)
}
// Version 1 should be gone.
if _, ok := te.keys["trim-key"].versions[1]; ok {
t.Fatal("version 1 should have been trimmed")
}
if _, ok := te.keys["trim-key"].versions[2]; !ok {
t.Fatal("version 2 should still exist")
}
}
func TestSignVerifyEd25519(t *testing.T) {
te, _ := setupEngine(t)
createKey(t, te, "ed-key", "ed25519")
ctx := context.Background()
input := base64.StdEncoding.EncodeToString([]byte("sign this"))
resp, err := te.HandleRequest(ctx, &engine.Request{
Operation: "sign",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "ed-key", "input": input},
})
if err != nil {
t.Fatalf("sign: %v", err)
}
sig, _ := resp.Data["signature"].(string)
if !strings.HasPrefix(sig, "metacrypt:v1:") {
t.Fatalf("unexpected signature format: %s", sig)
}
// Verify.
resp, err = te.HandleRequest(ctx, &engine.Request{
Operation: "verify",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "ed-key", "input": input, "signature": sig},
})
if err != nil {
t.Fatalf("verify: %v", err)
}
valid, _ := resp.Data["valid"].(bool)
if !valid {
t.Fatal("expected valid signature")
}
// Wrong input should fail verification.
wrongInput := base64.StdEncoding.EncodeToString([]byte("wrong data"))
resp, err = te.HandleRequest(ctx, &engine.Request{
Operation: "verify",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "ed-key", "input": wrongInput, "signature": sig},
})
if err != nil {
t.Fatalf("verify: %v", err)
}
valid, _ = resp.Data["valid"].(bool)
if valid {
t.Fatal("expected invalid signature for wrong input")
}
}
func TestSignVerifyECDSA(t *testing.T) {
for _, keyType := range []string{"ecdsa-p256", "ecdsa-p384"} {
t.Run(keyType, func(t *testing.T) {
te, _ := setupEngine(t)
createKey(t, te, "ec-key", keyType)
ctx := context.Background()
input := base64.StdEncoding.EncodeToString([]byte("ecdsa test"))
resp, err := te.HandleRequest(ctx, &engine.Request{
Operation: "sign",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "ec-key", "input": input},
})
if err != nil {
t.Fatalf("sign: %v", err)
}
sig, _ := resp.Data["signature"].(string)
resp, err = te.HandleRequest(ctx, &engine.Request{
Operation: "verify",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "ec-key", "input": input, "signature": sig},
})
if err != nil {
t.Fatalf("verify: %v", err)
}
valid, _ := resp.Data["valid"].(bool)
if !valid {
t.Fatal("expected valid signature")
}
})
}
}
func TestSignRejectsSymmetricAndHMAC(t *testing.T) {
for _, keyType := range []string{"aes256-gcm", "hmac-sha256"} {
t.Run(keyType, func(t *testing.T) {
te, _ := setupEngine(t)
createKey(t, te, "sym-key", keyType)
ctx := context.Background()
input := base64.StdEncoding.EncodeToString([]byte("test"))
_, err := te.HandleRequest(ctx, &engine.Request{
Operation: "sign",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "sym-key", "input": input},
})
if err == nil {
t.Fatal("expected error signing with non-asymmetric key")
}
})
}
}
func TestHMACComputeAndVerify(t *testing.T) {
for _, keyType := range []string{"hmac-sha256", "hmac-sha512"} {
t.Run(keyType, func(t *testing.T) {
te, _ := setupEngine(t)
createKey(t, te, "hmac-key", keyType)
ctx := context.Background()
input := base64.StdEncoding.EncodeToString([]byte("hmac me"))
// Compute.
resp, err := te.HandleRequest(ctx, &engine.Request{
Operation: "hmac",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "hmac-key", "input": input},
})
if err != nil {
t.Fatalf("hmac compute: %v", err)
}
hmacStr, _ := resp.Data["hmac"].(string)
if !strings.HasPrefix(hmacStr, "metacrypt:v1:") {
t.Fatalf("unexpected hmac format: %s", hmacStr)
}
// Verify.
resp, err = te.HandleRequest(ctx, &engine.Request{
Operation: "hmac",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "hmac-key", "input": input, "hmac": hmacStr},
})
if err != nil {
t.Fatalf("hmac verify: %v", err)
}
valid, _ := resp.Data["valid"].(bool)
if !valid {
t.Fatal("expected valid HMAC")
}
// Wrong input should fail.
wrongInput := base64.StdEncoding.EncodeToString([]byte("wrong data"))
resp, err = te.HandleRequest(ctx, &engine.Request{
Operation: "hmac",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "hmac-key", "input": wrongInput, "hmac": hmacStr},
})
if err != nil {
t.Fatalf("hmac verify wrong: %v", err)
}
valid, _ = resp.Data["valid"].(bool)
if valid {
t.Fatal("expected invalid HMAC for wrong input")
}
})
}
}
func TestBatchEncryptDecrypt(t *testing.T) {
te, _ := setupEngine(t)
createKey(t, te, "batch-key", "aes256-gcm")
ctx := context.Background()
items := []interface{}{
map[string]interface{}{
"plaintext": base64.StdEncoding.EncodeToString([]byte("item1")),
"reference": "ref1",
},
map[string]interface{}{
"plaintext": base64.StdEncoding.EncodeToString([]byte("item2")),
"reference": "ref2",
},
}
// Batch encrypt.
resp, err := te.HandleRequest(ctx, &engine.Request{
Operation: "batch-encrypt",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "batch-key", "items": items},
})
if err != nil {
t.Fatalf("batch-encrypt: %v", err)
}
results, _ := resp.Data["results"].([]interface{})
if len(results) != 2 {
t.Fatalf("expected 2 results, got %d", len(results))
}
// Build decrypt items.
decryptItems := make([]interface{}, len(results))
for i, r := range results {
br, ok := r.(batchResult)
if !ok {
t.Fatalf("expected batchResult, got %T", r)
}
if br.Error != "" {
t.Fatalf("batch encrypt item %d error: %s", i, br.Error)
}
decryptItems[i] = map[string]interface{}{
"ciphertext": br.Ciphertext,
"reference": br.Reference,
}
}
// Batch decrypt.
resp, err = te.HandleRequest(ctx, &engine.Request{
Operation: "batch-decrypt",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "batch-key", "items": decryptItems},
})
if err != nil {
t.Fatalf("batch-decrypt: %v", err)
}
results, _ = resp.Data["results"].([]interface{})
expected := []string{"item1", "item2"}
for i, r := range results {
br, ok := r.(batchResult)
if !ok {
t.Fatalf("expected batchResult, got %T", r)
}
if br.Error != "" {
t.Fatalf("batch decrypt item %d error: %s", i, br.Error)
}
decoded, _ := base64.StdEncoding.DecodeString(br.Plaintext)
if string(decoded) != expected[i] {
t.Fatalf("item %d: expected %q, got %q", i, expected[i], string(decoded))
}
}
}
func TestBatchPartialErrors(t *testing.T) {
te, _ := setupEngine(t)
createKey(t, te, "batch-err-key", "aes256-gcm")
ctx := context.Background()
items := []interface{}{
map[string]interface{}{
"ciphertext": "metacrypt:v1:invalidbase64!!!",
"reference": "bad",
},
map[string]interface{}{
"ciphertext": "metacrypt:v1:" + base64.StdEncoding.EncodeToString([]byte("not-valid-ciphertext")),
"reference": "also-bad",
},
}
resp, err := te.HandleRequest(ctx, &engine.Request{
Operation: "batch-decrypt",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "batch-err-key", "items": items},
})
if err != nil {
t.Fatalf("batch-decrypt: %v", err)
}
results, _ := resp.Data["results"].([]interface{})
if len(results) != 2 {
t.Fatalf("expected 2 results, got %d", len(results))
}
for i, r := range results {
br, ok := r.(batchResult)
if !ok {
t.Fatalf("expected batchResult, got %T", r)
}
if br.Error == "" {
t.Fatalf("item %d: expected error", i)
}
}
}
func TestRewrap(t *testing.T) {
te, _ := setupEngine(t)
createKey(t, te, "rewrap-key", "aes256-gcm")
ctx := context.Background()
plaintext := base64.StdEncoding.EncodeToString([]byte("rewrap me"))
// Encrypt with v1.
resp, err := te.HandleRequest(ctx, &engine.Request{
Operation: "encrypt",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "rewrap-key", "plaintext": plaintext},
})
if err != nil {
t.Fatalf("encrypt: %v", err)
}
ctV1, _ := resp.Data["ciphertext"].(string)
// Rotate.
_, err = te.HandleRequest(ctx, &engine.Request{
Operation: "rotate-key",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "rewrap-key"},
})
if err != nil {
t.Fatalf("rotate: %v", err)
}
// Rewrap.
resp, err = te.HandleRequest(ctx, &engine.Request{
Operation: "rewrap",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "rewrap-key", "ciphertext": ctV1},
})
if err != nil {
t.Fatalf("rewrap: %v", err)
}
ctV2, _ := resp.Data["ciphertext"].(string)
if !strings.HasPrefix(ctV2, "metacrypt:v2:") {
t.Fatalf("expected v2 ciphertext after rewrap, got %s", ctV2)
}
// Decrypt rewrapped ciphertext.
resp, err = te.HandleRequest(ctx, &engine.Request{
Operation: "decrypt",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "rewrap-key", "ciphertext": ctV2},
})
if err != nil {
t.Fatalf("decrypt rewrapped: %v", err)
}
ptB64, _ := resp.Data["plaintext"].(string)
decoded, _ := base64.StdEncoding.DecodeString(ptB64)
if string(decoded) != "rewrap me" {
t.Fatalf("expected 'rewrap me', got %q", string(decoded))
}
}
func TestAuthEnforcement(t *testing.T) {
te, _ := setupEngine(t)
ctx := context.Background()
// Admin-only operations should fail for users.
_, err := te.HandleRequest(ctx, &engine.Request{
Operation: "create-key",
CallerInfo: userCaller(),
Data: map[string]interface{}{"name": "test", "type": "aes256-gcm"},
})
if err == nil {
t.Fatal("expected create-key to fail for user")
}
// Admin-only operations should fail for guests.
_, err = te.HandleRequest(ctx, &engine.Request{
Operation: "create-key",
CallerInfo: guestCaller(),
Data: map[string]interface{}{"name": "test", "type": "aes256-gcm"},
})
if err == nil {
t.Fatal("expected create-key to fail for guest")
}
// User operations should fail for guests.
createKey(t, te, "auth-key", "aes256-gcm")
_, err = te.HandleRequest(ctx, &engine.Request{
Operation: "list-keys",
CallerInfo: guestCaller(),
})
if err == nil {
t.Fatal("expected list-keys to fail for guest")
}
// Encrypt should fail for guest.
_, err = te.HandleRequest(ctx, &engine.Request{
Operation: "encrypt",
CallerInfo: guestCaller(),
Data: map[string]interface{}{"key": "auth-key", "plaintext": base64.StdEncoding.EncodeToString([]byte("test"))},
})
if err == nil {
t.Fatal("expected encrypt to fail for guest")
}
// Unauthenticated should fail.
_, err = te.HandleRequest(ctx, &engine.Request{
Operation: "create-key",
CallerInfo: nil,
Data: map[string]interface{}{"name": "test", "type": "aes256-gcm"},
})
if err == nil {
t.Fatal("expected create-key to fail without auth")
}
}
func TestDeleteKeyWithAndWithoutAllowDeletion(t *testing.T) {
te, _ := setupEngine(t)
ctx := context.Background()
createKey(t, te, "nodelete-key", "aes256-gcm")
// Should fail: allow_deletion is false.
_, err := te.HandleRequest(ctx, &engine.Request{
Operation: "delete-key",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "nodelete-key"},
})
if err == nil {
t.Fatal("expected delete to fail without allow_deletion")
}
// Enable deletion.
_, err = te.HandleRequest(ctx, &engine.Request{
Operation: "update-key-config",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "nodelete-key", "allow_deletion": true},
})
if err != nil {
t.Fatalf("update-key-config: %v", err)
}
// Should succeed now.
_, err = te.HandleRequest(ctx, &engine.Request{
Operation: "delete-key",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "nodelete-key"},
})
if err != nil {
t.Fatalf("delete-key: %v", err)
}
if _, ok := te.keys["nodelete-key"]; ok {
t.Fatal("key should have been deleted")
}
}
func TestGetPublicKey(t *testing.T) {
te, _ := setupEngine(t)
createKey(t, te, "pubkey", "ed25519")
ctx := context.Background()
resp, err := te.HandleRequest(ctx, &engine.Request{
Operation: "get-public-key",
CallerInfo: userCaller(),
Data: map[string]interface{}{"name": "pubkey"},
})
if err != nil {
t.Fatalf("get-public-key: %v", err)
}
pk, _ := resp.Data["public_key"].(string)
if pk == "" {
t.Fatal("expected non-empty public key")
}
ver, _ := resp.Data["version"].(int)
if ver != 1 {
t.Fatalf("expected version 1, got %d", ver)
}
}
func TestGetPublicKeyRejectsSymmetric(t *testing.T) {
te, _ := setupEngine(t)
createKey(t, te, "sym", "aes256-gcm")
ctx := context.Background()
_, err := te.HandleRequest(ctx, &engine.Request{
Operation: "get-public-key",
CallerInfo: userCaller(),
Data: map[string]interface{}{"name": "sym"},
})
if err == nil {
t.Fatal("expected error getting public key for symmetric key")
}
}
func TestBatchRewrap(t *testing.T) {
te, _ := setupEngine(t)
createKey(t, te, "brwrap-key", "aes256-gcm")
ctx := context.Background()
pt1 := base64.StdEncoding.EncodeToString([]byte("item1"))
pt2 := base64.StdEncoding.EncodeToString([]byte("item2"))
// Encrypt two items.
var cts []string
for _, pt := range []string{pt1, pt2} {
resp, _ := te.HandleRequest(ctx, &engine.Request{
Operation: "encrypt",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "brwrap-key", "plaintext": pt},
})
ct, _ := resp.Data["ciphertext"].(string)
cts = append(cts, ct)
}
// Rotate.
te.HandleRequest(ctx, &engine.Request{
Operation: "rotate-key",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "brwrap-key"},
})
// Batch rewrap.
items := []interface{}{
map[string]interface{}{"ciphertext": cts[0], "reference": "r1"},
map[string]interface{}{"ciphertext": cts[1], "reference": "r2"},
}
resp, err := te.HandleRequest(ctx, &engine.Request{
Operation: "batch-rewrap",
CallerInfo: userCaller(),
Data: map[string]interface{}{"key": "brwrap-key", "items": items},
})
if err != nil {
t.Fatalf("batch-rewrap: %v", err)
}
results, _ := resp.Data["results"].([]interface{})
for i, r := range results {
br, ok := r.(batchResult)
if !ok {
t.Fatalf("expected batchResult, got %T", r)
}
if br.Error != "" {
t.Fatalf("item %d error: %s", i, br.Error)
}
if !strings.HasPrefix(br.Ciphertext, "metacrypt:v2:") {
t.Fatalf("item %d: expected v2 ciphertext, got %s", i, br.Ciphertext)
}
}
}
func TestDuplicateKeyCreation(t *testing.T) {
te, _ := setupEngine(t)
createKey(t, te, "dup-key", "aes256-gcm")
ctx := context.Background()
_, err := te.HandleRequest(ctx, &engine.Request{
Operation: "create-key",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "dup-key", "type": "aes256-gcm"},
})
if err == nil {
t.Fatal("expected error creating duplicate key")
}
}
func TestInvalidKeyType(t *testing.T) {
te, _ := setupEngine(t)
ctx := context.Background()
_, err := te.HandleRequest(ctx, &engine.Request{
Operation: "create-key",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "bad", "type": "invalid-type"},
})
if err == nil {
t.Fatal("expected error for invalid key type")
}
}