Files
zeekay b4a26d51a8 fix: use published kms SDK module path, remove local replace
Replace the local filesystem replace directive with the properly
published module at github.com/hanzoai/kms/sdk/go v1.0.0. The
local path /Users/z/work/hanzo/kms/sdk/go doesn't exist in CI.
2026-03-02 03:16:33 -07:00

370 lines
8.9 KiB
Go

// Copyright (C) 2020-2026, Hanzo AI Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package kms
import (
"encoding/base64"
"strings"
"testing"
sdk "github.com/hanzoai/kms/sdk/go"
)
func TestConfigValidate(t *testing.T) {
tests := []struct {
name string
config Config
wantErr bool
}{
{
name: "valid config",
config: Config{
Nodes: []string{"https://node1:9651"},
OrgSlug: "test-org",
Threshold: 1,
EncryptedCollections: map[string][]string{
"secrets": {"value"},
},
},
wantErr: false,
},
{
name: "no nodes",
config: Config{
OrgSlug: "test-org",
Threshold: 1,
},
wantErr: true,
},
{
name: "no org slug",
config: Config{
Nodes: []string{"n1"},
Threshold: 1,
},
wantErr: true,
},
{
name: "zero threshold",
config: Config{
Nodes: []string{"n1"},
OrgSlug: "test-org",
Threshold: 0,
},
wantErr: true,
},
{
name: "threshold exceeds nodes",
config: Config{
Nodes: []string{"n1"},
OrgSlug: "test-org",
Threshold: 2,
},
wantErr: true,
},
{
name: "fhe field not in encrypted",
config: Config{
Nodes: []string{"n1"},
OrgSlug: "test-org",
Threshold: 1,
EncryptedCollections: map[string][]string{
"secrets": {"value"},
},
FHESearchable: map[string][]string{
"secrets": {"name"}, // "name" not in EncryptedCollections
},
},
wantErr: true,
},
{
name: "fhe collection not in encrypted",
config: Config{
Nodes: []string{"n1"},
OrgSlug: "test-org",
Threshold: 1,
EncryptedCollections: map[string][]string{
"secrets": {"value"},
},
FHESearchable: map[string][]string{
"other": {"name"}, // "other" not in EncryptedCollections
},
},
wantErr: true,
},
{
name: "valid fhe config",
config: Config{
Nodes: []string{"n1"},
OrgSlug: "test-org",
Threshold: 1,
EncryptedCollections: map[string][]string{
"secrets": {"name", "value"},
},
FHESearchable: map[string][]string{
"secrets": {"name"},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.validate()
if (err != nil) != tt.wantErr {
t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestDefaultConfig(t *testing.T) {
cfg := DefaultConfig()
if !cfg.Enabled {
t.Error("expected Enabled to be true")
}
if cfg.EncryptedCollections == nil {
t.Error("expected EncryptedCollections to be non-nil")
}
if cfg.FHESearchable == nil {
t.Error("expected FHESearchable to be non-nil")
}
}
func TestIsEncryptedValue(t *testing.T) {
tests := []struct {
val string
want bool
}{
{"enc:v1:abc123", true},
{"enc:v1:", false}, // too short — no data after prefix
{"plaintext", false}, // no prefix
{"", false}, // empty
{"enc:v1:a", true}, // minimal valid
{"ENC:V1:abc", false}, // case sensitive
{"enc:v2:abc", false}, // wrong version
}
for _, tt := range tests {
got := isEncryptedValue(tt.val)
if got != tt.want {
t.Errorf("isEncryptedValue(%q) = %v, want %v", tt.val, got, tt.want)
}
}
}
func TestSealOpenField_RoundTrip(t *testing.T) {
// Use the SDK's key derivation to get a valid 32-byte key.
masterKey, err := sdk.DeriveMasterKey("test-passphrase", "test-org")
if err != nil {
t.Fatalf("derive master key: %v", err)
}
defer clear(masterKey)
cek, err := sdk.DeriveCEK(masterKey, "test-org")
if err != nil {
t.Fatalf("derive CEK: %v", err)
}
defer clear(cek)
plaintext := []byte("super-secret-api-key-12345")
aad := []byte("test-org:secrets:record-1")
// Seal
encrypted, err := sealField(cek, plaintext, aad)
if err != nil {
t.Fatalf("sealField: %v", err)
}
// Verify ciphertext is different from plaintext.
if string(encrypted) == string(plaintext) {
t.Fatal("encrypted should differ from plaintext")
}
// Open
decrypted, err := openField(cek, encrypted, aad)
if err != nil {
t.Fatalf("openField: %v", err)
}
if string(decrypted) != string(plaintext) {
t.Errorf("decrypted = %q, want %q", string(decrypted), string(plaintext))
}
}
func TestSealOpenField_WrongKey(t *testing.T) {
mk1, _ := sdk.DeriveMasterKey("pass1", "org1")
cek1, _ := sdk.DeriveCEK(mk1, "org1")
defer clear(mk1)
defer clear(cek1)
mk2, _ := sdk.DeriveMasterKey("pass2", "org2")
cek2, _ := sdk.DeriveCEK(mk2, "org2")
defer clear(mk2)
defer clear(cek2)
plaintext := []byte("secret")
aad := []byte("aad")
encrypted, err := sealField(cek1, plaintext, aad)
if err != nil {
t.Fatalf("sealField: %v", err)
}
_, err = openField(cek2, encrypted, aad)
if err == nil {
t.Fatal("expected error decrypting with wrong key")
}
}
func TestSealOpenField_WrongAAD(t *testing.T) {
mk, _ := sdk.DeriveMasterKey("pass", "org")
cek, _ := sdk.DeriveCEK(mk, "org")
defer clear(mk)
defer clear(cek)
encrypted, err := sealField(cek, []byte("secret"), []byte("aad1"))
if err != nil {
t.Fatalf("sealField: %v", err)
}
_, err = openField(cek, encrypted, []byte("aad2"))
if err == nil {
t.Fatal("expected error decrypting with wrong AAD")
}
}
func TestComputeFHEIndex_Deterministic(t *testing.T) {
mk, _ := sdk.DeriveMasterKey("pass", "org")
cek, _ := sdk.DeriveCEK(mk, "org")
defer clear(mk)
defer clear(cek)
idx1, err := computeFHEIndex(cek, "org", "secrets", "name", "my-key")
if err != nil {
t.Fatalf("computeFHEIndex: %v", err)
}
idx2, err := computeFHEIndex(cek, "org", "secrets", "name", "my-key")
if err != nil {
t.Fatalf("computeFHEIndex: %v", err)
}
if base64.RawURLEncoding.EncodeToString(idx1) != base64.RawURLEncoding.EncodeToString(idx2) {
t.Error("FHE index should be deterministic for same input")
}
}
func TestComputeFHEIndex_DifferentValues(t *testing.T) {
mk, _ := sdk.DeriveMasterKey("pass", "org")
cek, _ := sdk.DeriveCEK(mk, "org")
defer clear(mk)
defer clear(cek)
idx1, _ := computeFHEIndex(cek, "org", "secrets", "name", "key-a")
idx2, _ := computeFHEIndex(cek, "org", "secrets", "name", "key-b")
if base64.RawURLEncoding.EncodeToString(idx1) == base64.RawURLEncoding.EncodeToString(idx2) {
t.Error("FHE index should differ for different values")
}
}
func TestComputeFHEIndex_DifferentFields(t *testing.T) {
mk, _ := sdk.DeriveMasterKey("pass", "org")
cek, _ := sdk.DeriveCEK(mk, "org")
defer clear(mk)
defer clear(cek)
idx1, _ := computeFHEIndex(cek, "org", "secrets", "name", "same-val")
idx2, _ := computeFHEIndex(cek, "org", "secrets", "label", "same-val")
if base64.RawURLEncoding.EncodeToString(idx1) == base64.RawURLEncoding.EncodeToString(idx2) {
t.Error("FHE index should differ for different fields even with same value")
}
}
func TestComputeFHEIndex_DifferentOrgs(t *testing.T) {
mk1, _ := sdk.DeriveMasterKey("pass", "org-a")
cek1, _ := sdk.DeriveCEK(mk1, "org-a")
defer clear(mk1)
defer clear(cek1)
mk2, _ := sdk.DeriveMasterKey("pass", "org-b")
cek2, _ := sdk.DeriveCEK(mk2, "org-b")
defer clear(mk2)
defer clear(cek2)
idx1, _ := computeFHEIndex(cek1, "org-a", "secrets", "name", "same-val")
idx2, _ := computeFHEIndex(cek2, "org-b", "secrets", "name", "same-val")
if base64.RawURLEncoding.EncodeToString(idx1) == base64.RawURLEncoding.EncodeToString(idx2) {
t.Error("FHE index should differ for different orgs")
}
}
func TestEncryptedPrefix(t *testing.T) {
if encryptedPrefix != "enc:v1:" {
t.Errorf("unexpected prefix: %q", encryptedPrefix)
}
}
func TestFHEIndexPrefix(t *testing.T) {
if fheIndexPrefix != "fhe:v1:" {
t.Errorf("unexpected prefix: %q", fheIndexPrefix)
}
}
func TestResolvePassphrase(t *testing.T) {
// Config passphrase takes priority.
c := Config{Passphrase: "from-config"}
if got := c.resolvePassphrase(); got != "from-config" {
t.Errorf("expected 'from-config', got %q", got)
}
// Empty config falls back to env.
c2 := Config{}
t.Setenv("KMS_PASSPHRASE", "from-env")
if got := c2.resolvePassphrase(); got != "from-env" {
t.Errorf("expected 'from-env', got %q", got)
}
}
func TestFieldEncoding_RoundTrip(t *testing.T) {
mk, _ := sdk.DeriveMasterKey("test", "org")
cek, _ := sdk.DeriveCEK(mk, "org")
defer clear(mk)
defer clear(cek)
plaintext := "my-secret-value"
aad := []byte("org:col:id")
// Simulate what hooks.go does.
encrypted, err := sealField(cek, []byte(plaintext), aad)
if err != nil {
t.Fatalf("sealField: %v", err)
}
encoded := encryptedPrefix + base64.RawURLEncoding.EncodeToString(encrypted)
if !isEncryptedValue(encoded) {
t.Fatal("encoded value should be detected as encrypted")
}
// Decode.
ciphertext, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(encoded, encryptedPrefix))
if err != nil {
t.Fatalf("base64 decode: %v", err)
}
decrypted, err := openField(cek, ciphertext, aad)
if err != nil {
t.Fatalf("openField: %v", err)
}
if string(decrypted) != plaintext {
t.Errorf("round-trip failed: got %q, want %q", string(decrypted), plaintext)
}
}