mirror of
https://github.com/hanzoai/cek.git
synced 2026-10-09 02:37:18 +00:00
A key derived from the master and the namespace: not generated, not wrapped, not stored, not rotated in place. That removes the unwrap step, the rewrap step, the per-file key material, and the plaintext-to-encrypted migration — every one of which existed to carry old data forward, and none of which is a property anyone wanted. A database is born encrypted or it does not exist. The same namespace and subsystem always derive the same key, so a file reopens after a restart with nothing persisted beside it, and two databases never share a key because the subsystem is part of the binding. Losing the master loses the data. That is what encryption at rest means, and why the master lives in KMS. Co-authored-by: Hanzo Dev <[email protected]>
121 lines
3.4 KiB
Go
121 lines
3.4 KiB
Go
package cek
|
|
|
|
import (
|
|
"bytes"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/hanzoai/namespace"
|
|
)
|
|
|
|
func master(b byte) []byte { return bytes.Repeat([]byte{b}, KeyLen) }
|
|
|
|
func mustNS(t *testing.T, org, project string) namespace.Namespace {
|
|
t.Helper()
|
|
ns, err := namespace.OrgProject(org, project)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return ns
|
|
}
|
|
|
|
// The same inputs must always give the same key, or a database cannot be
|
|
// reopened after a restart.
|
|
func TestDeriveIsDeterministic(t *testing.T) {
|
|
ns := mustNS(t, "acme", "")
|
|
a, err := DeriveKey(master(1), ns, "treasury")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, _ := DeriveKey(master(1), ns, "treasury")
|
|
if !bytes.Equal(a, b) {
|
|
t.Fatal("same inputs produced different keys")
|
|
}
|
|
if len(a) != KeyLen {
|
|
t.Fatalf("key is %d bytes, want %d", len(a), KeyLen)
|
|
}
|
|
}
|
|
|
|
// Every axis must change the key. If any of these collide, two databases share
|
|
// a key and one account's file opens another's.
|
|
func TestDeriveSeparatesEveryAxis(t *testing.T) {
|
|
base, _ := DeriveKey(master(1), mustNS(t, "acme", ""), "treasury")
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
key func() ([]byte, error)
|
|
}{
|
|
{"different master", func() ([]byte, error) { return DeriveKey(master(2), mustNS(t, "acme", ""), "treasury") }},
|
|
{"different org", func() ([]byte, error) { return DeriveKey(master(1), mustNS(t, "globex", ""), "treasury") }},
|
|
{"different subsystem", func() ([]byte, error) { return DeriveKey(master(1), mustNS(t, "acme", ""), "audit") }},
|
|
{"a project of that org", func() ([]byte, error) { return DeriveKey(master(1), mustNS(t, "acme", "web"), "treasury") }},
|
|
{"the system namespace", func() ([]byte, error) { return DeriveKey(master(1), namespace.System(), "treasury") }},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := tc.key()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if bytes.Equal(base, got) {
|
|
t.Fatalf("%s produced the SAME key — two databases would share it", tc.name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A missing or short master must fail, never fall back to opening in plaintext.
|
|
func TestDeriveRefusesBadInput(t *testing.T) {
|
|
ns := mustNS(t, "acme", "")
|
|
for _, tc := range []struct {
|
|
name string
|
|
master []byte
|
|
ns namespace.Namespace
|
|
sub string
|
|
}{
|
|
{"no master", nil, ns, "treasury"},
|
|
{"short master", bytes.Repeat([]byte{1}, 16), ns, "treasury"},
|
|
{"zero namespace", master(1), namespace.Namespace{}, "treasury"},
|
|
{"empty subsystem", master(1), ns, ""},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if _, err := DeriveKey(tc.master, tc.ns, tc.sub); err == nil {
|
|
t.Fatal("expected an error, got a key")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Open must write where namespace says, so the file and its durable slot agree.
|
|
func TestOpenUsesTheNamespaceLocation(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ns := mustNS(t, "acme", "")
|
|
|
|
db, err := Open(master(1), ns, "treasury", dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
if _, err := db.Exec(`CREATE TABLE t (v TEXT)`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
want, err := namespace.Path(dir, ns, "treasury")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := filepath.Abs(want); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// An error must never carry the key or a DSN holding it.
|
|
func TestOpenErrorHoldsNoKey(t *testing.T) {
|
|
_, err := Open(master(1), namespace.Namespace{}, "treasury", t.TempDir())
|
|
if err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
if bytes.Contains([]byte(err.Error()), master(1)) {
|
|
t.Fatal("error leaked the key")
|
|
}
|
|
}
|