Files
mpc/pkg/kvstore/encrypted_test.go
zeekay db0d44f3c7 feat: per-user shard encryption + wallet isolation
EncryptedKVStore: AES-256-GCM wrapper over ZapDB for per-user shard encryption.
Each user's key shards encrypted with a unique DEK:

  user DEK = HMAC-SHA256(masterKey, "mpc:shard:" + orgID + ":" + userID)
  org DEK  = HMAC-SHA256(masterKey, "mpc:org:" + orgID)

Three layers of MPC data protection:
  1. ZapDB ChaCha20-Poly1305 (node-level, existing)
  2. EncryptedKVStore AES-256-GCM (per-user, NEW)
  3. API filter on createdBy (access control, NEW)

Even if ZapDB is fully dumped, individual users' shards are
independently encrypted. Wrong user key → decryption fails.

5 new tests: roundtrip, key uniqueness, wrong-key rejection,
empty values, org vs user key separation.
2025-12-28 00:13:50 -08:00

110 lines
3.1 KiB
Go

package kvstore
import (
"bytes"
"testing"
)
type memKV struct {
data map[string][]byte
}
func newMemKV() *memKV { return &memKV{data: make(map[string][]byte)} }
func (m *memKV) Put(k string, v []byte) error { m.data[k] = v; return nil }
func (m *memKV) Get(k string) ([]byte, error) { return m.data[k], nil }
func (m *memKV) Delete(k string) error { delete(m.data, k); return nil }
func (m *memKV) Close() error { return nil }
func (m *memKV) Backup() error { return nil }
func TestEncryptedKVStoreRoundtrip(t *testing.T) {
inner := newMemKV()
key := DeriveUserKey([]byte("master-secret-key-for-testing!!"), "acme", "user001")
store, err := NewEncryptedKVStore(inner, key)
if err != nil {
t.Fatal(err)
}
plaintext := []byte("this is a secret MPC key share shard data")
if err := store.Put("org:acme:wallet_001", plaintext); err != nil {
t.Fatal(err)
}
// Verify the underlying store has encrypted (different) data
raw := inner.data["org:acme:wallet_001"]
if bytes.Equal(raw, plaintext) {
t.Error("underlying store has plaintext — encryption not working")
}
if len(raw) == 0 {
t.Error("underlying store is empty")
}
// Decrypt via the encrypted store
decrypted, err := store.Get("org:acme:wallet_001")
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(decrypted, plaintext) {
t.Errorf("decrypted = %q, want %q", decrypted, plaintext)
}
}
func TestDifferentUsersGetDifferentKeys(t *testing.T) {
master := []byte("master-secret-key-for-testing!!")
key1 := DeriveUserKey(master, "acme", "user001")
key2 := DeriveUserKey(master, "acme", "user002")
key3 := DeriveUserKey(master, "beta", "user001")
if bytes.Equal(key1, key2) {
t.Error("same org, different users should get different keys")
}
if bytes.Equal(key1, key3) {
t.Error("different orgs, same user ID should get different keys")
}
if bytes.Equal(key2, key3) {
t.Error("different org+user combos should get different keys")
}
}
func TestWrongKeyCannotDecrypt(t *testing.T) {
inner := newMemKV()
master := []byte("master-secret-key-for-testing!!")
store1, _ := NewEncryptedKVStore(inner, DeriveUserKey(master, "acme", "user001"))
store2, _ := NewEncryptedKVStore(inner, DeriveUserKey(master, "acme", "user002"))
plaintext := []byte("user001's secret shard")
store1.Put("shard_001", plaintext)
// user002's key cannot decrypt user001's data
_, err := store2.Get("shard_001")
if err == nil {
t.Error("expected decryption error with wrong key")
}
}
func TestEmptyValueRoundtrip(t *testing.T) {
inner := newMemKV()
key := DeriveUserKey([]byte("master-secret-key-for-testing!!"), "acme", "user001")
store, _ := NewEncryptedKVStore(inner, key)
store.Put("empty", []byte{})
got, err := store.Get("empty")
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Errorf("expected empty, got %d bytes", len(got))
}
}
func TestOrgKeyDiffersFromUserKey(t *testing.T) {
master := []byte("master-secret-key-for-testing!!")
orgKey := DeriveOrgKey(master, "acme")
userKey := DeriveUserKey(master, "acme", "user001")
if bytes.Equal(orgKey, userKey) {
t.Error("org key and user key should differ")
}
}