Files
zeekayandHanzo Dev 6ca9ce9a19
deploy / build (push) Waiting to run
keys: derive every key with cek, and delete the ones that did not
base had its own key derivations and no dependency on hanzoai/cek, the
primitive that is meant to be the only one. Two HKDFs in the platform
plugin, two HMACs beside them for S3, and a two-level HMAC hierarchy in
the vault. None agreed with cek, and the comments describing them had
drifted off the code: org_db and platform.go both said HMAC where the
code ran HKDF, and the vault drew a KEK hierarchy in its package doc.

All of them are now cek.DeriveKey(master, namespace, subsystem):

	org database   org/{org}          "org"
	user database  org/{org}/{user}   "user"
	org objects    org/{org}          "s3"
	user objects   org/{org}/{user}   "s3"
	vault shard    org/{org}/{user}   "vault"
	shared vault   org/{org}/{vault}  "vault-shared"

A per-org and a per-user key are one derivation over two namespaces,
which is cek's argument: the key is bound to the name that renders the
path. namespace.Key refuses a user namespace, but that is about LAYOUT
and DeriveKey does not use it — a person's data is named org/{org}/{user}
here, which is what namespace.Of already builds, and it is what keeps
acme/alice and globex/alice on different keys. The vault reaches the same
namespace through OrgProject, because its ids are DIDs and a colon is not
legal in a segment; Sanitize is injective, so two DIDs cannot fold onto
one vault.

The vault's org KEK is gone. It was there to be rotatable without
re-encrypting user data, nothing ever rotated it, and the org is in the
namespace now — the property it was actually providing.

store/keyring.go keeps its own HKDF, and now says why. It WRAPS a random
age identity rather than deriving a key, so Rotate re-wraps one sidecar
instead of rewriting every byte of ciphertext, and its secret is a
per-org KMS KEK rather than one master. cek has no wrap by design. A
different shape, left alone rather than bent into one.

Tests now compare each derivation against cek.DeriveKey directly. A
comment claiming a derivation cannot fail when the derivation drifts,
which is exactly how the one deleted last time survived.

A master key of the wrong length is now an error instead of silently no
key, and the resolver's encKey, computed and never read, is gone.

deps: cek v0.2.3 and namespace v1.2.0, plus tasks v1.52.4 -> v1.52.9,
which is what moves hanzoai/sqlite to v0.5.0 — tasks below v1.52.9 still
calls the wrapped-DEK API that v0.5.0 removed.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-02 19:56:10 -07:00

100 lines
2.8 KiB
Go

package vault
import (
"bytes"
"testing"
"github.com/hanzoai/cek"
"github.com/hanzoai/namespace"
)
// TestVaultKey_IsCEKDerivation states the derivation as an assertion rather
// than as a comment. This plugin's package doc has claimed a key hierarchy in
// prose before while the code did something else; prose cannot go red.
func TestVaultKey_IsCEKDerivation(t *testing.T) {
master := testMasterKey()
const org, user = "did:lux:org:acme", "did:lux:user:alice"
ns, err := namespace.OrgProject(org, user)
if err != nil {
t.Fatalf("namespace.OrgProject: %v", err)
}
want, err := cek.DeriveKey(master, ns, userSubsystem)
if err != nil {
t.Fatalf("cek.DeriveKey: %v", err)
}
got, err := vaultKey(master, org, user, userSubsystem)
if err != nil {
t.Fatalf("vaultKey: %v", err)
}
if !bytes.Equal(got, want) {
t.Fatalf("user DEK is not the cek derivation:\n got %x\nwant %x", got, want)
}
if len(got) != cek.KeyLen {
t.Fatalf("expected a %d-byte key, got %d", cek.KeyLen, len(got))
}
}
// The org is in the namespace, which is what replaced the org KEK. Without it,
// two orgs that use the same user id would open each other's vaults.
func TestVaultKey_BoundToOrg(t *testing.T) {
master := testMasterKey()
const user = "did:lux:user:alice"
a, err := vaultKey(master, "did:lux:org:acme", user, userSubsystem)
if err != nil {
t.Fatalf("vaultKey: %v", err)
}
b, err := vaultKey(master, "did:lux:org:globex", user, userSubsystem)
if err != nil {
t.Fatalf("vaultKey: %v", err)
}
if bytes.Equal(a, b) {
t.Fatal("the same user id in two orgs must not derive the same key")
}
}
// A user vault and a shared vault of the same name are different files and must
// be different keys; the subsystem is what separates them.
func TestVaultKey_UserVsShared(t *testing.T) {
master := testMasterKey()
const org, name = "did:lux:org:acme", "widgets"
user, err := vaultKey(master, org, name, userSubsystem)
if err != nil {
t.Fatalf("vaultKey: %v", err)
}
shared, err := vaultKey(master, org, name, sharedSubsystem)
if err != nil {
t.Fatalf("vaultKey: %v", err)
}
if bytes.Equal(user, shared) {
t.Fatal("a user vault and a shared vault of the same name must not share a key")
}
}
// Sanitize is injective, and this is the property that matters for a key: two
// DIDs that differ only in punctuation must not fold onto one vault.
func TestVaultKey_DistinctIDsDistinctKeys(t *testing.T) {
master := testMasterKey()
const org = "did:lux:org:acme"
seen := map[string]string{}
for _, id := range []string{
"did:lux:user:alice",
"did-lux-user-alice",
"did:lux:user:Alice",
"did:lux:user:alice2",
} {
k, err := vaultKey(master, org, id, userSubsystem)
if err != nil {
t.Fatalf("vaultKey(%q): %v", id, err)
}
if prev, dup := seen[string(k)]; dup {
t.Fatalf("%q and %q derived the same key", prev, id)
}
seen[string(k)] = id
}
}