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]>
115 lines
3.7 KiB
Go
115 lines
3.7 KiB
Go
// Package cek opens a namespace's SQLite database encrypted at rest.
|
|
//
|
|
// There is one way to do it:
|
|
//
|
|
// db, err := cek.Open(master, ns, "treasury", dataDir)
|
|
//
|
|
// The key is derived from the master and the namespace. It is not generated,
|
|
// not wrapped, not stored, and not rotated in place — so there is no unwrap
|
|
// step, no rewrap step, no per-file key material to lose, and no migration
|
|
// path to maintain. A database is born encrypted or it does not exist. Losing
|
|
// the master loses the data, which is the property you want from encryption at
|
|
// rest and the reason the master lives in KMS.
|
|
//
|
|
// The split of responsibilities is deliberate:
|
|
//
|
|
// namespace names the entity and where its file lives
|
|
// cek turns the master + that name into the file's key, and opens it
|
|
// sqlite opens a file under a raw key and knows nothing about who owns it
|
|
// kms holds the master
|
|
//
|
|
// Nothing here knows about orgs, users, billing or plugins. It knows a
|
|
// namespace, a subsystem, and a master key.
|
|
package cek
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/hanzoai/namespace"
|
|
sqlitedrv "github.com/hanzoai/sqlite"
|
|
"golang.org/x/crypto/hkdf"
|
|
)
|
|
|
|
// KeyLen is the length of both the master key and every derived key.
|
|
const KeyLen = 32
|
|
|
|
// info binds a derived key to this scheme, this namespace and this subsystem.
|
|
// Changing any part of it changes every key, which is why the version is in
|
|
// it: a future scheme is a new prefix, not a silent reinterpretation of the
|
|
// same bytes.
|
|
const infoPrefix = "hanzo/cek/v1/"
|
|
|
|
// ErrNoMaster reports a master key that is missing or the wrong length. It is
|
|
// deliberately fatal to Open rather than falling back to plaintext: a service
|
|
// that starts unencrypted because a key was absent has failed silently at the
|
|
// only job this package has.
|
|
var ErrNoMaster = errors.New("cek: master key must be " + itoa(KeyLen) + " bytes")
|
|
|
|
// DeriveKey returns the key for one database: the master, bound to the
|
|
// namespace that owns it and the subsystem it holds.
|
|
//
|
|
// It is a pure function of its inputs. The same namespace and subsystem always
|
|
// produce the same key, so a file can be opened again after a restart with
|
|
// nothing persisted alongside it, and two different databases never share a
|
|
// key because the subsystem is part of the binding.
|
|
func DeriveKey(master []byte, ns namespace.Namespace, subsystem string) ([]byte, error) {
|
|
if len(master) != KeyLen {
|
|
return nil, ErrNoMaster
|
|
}
|
|
if ns.IsZero() {
|
|
return nil, errors.New("cek: the zero namespace owns no database")
|
|
}
|
|
if subsystem == "" {
|
|
return nil, errors.New("cek: empty subsystem")
|
|
}
|
|
|
|
key := make([]byte, KeyLen)
|
|
info := []byte(infoPrefix + ns.String() + "/" + subsystem)
|
|
if _, err := io.ReadFull(hkdf.New(sha256.New, master, nil, info), key); err != nil {
|
|
return nil, fmt.Errorf("cek: derive key: %w", err)
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
// Open opens the encrypted database holding subsystem for ns, under dir,
|
|
// creating it if it does not exist. The returned handle is already keyed;
|
|
// callers use it as an ordinary *sql.DB.
|
|
//
|
|
// The error never contains the key or a DSN that holds it.
|
|
func Open(master []byte, ns namespace.Namespace, subsystem, dir string) (*sql.DB, error) {
|
|
key, err := DeriveKey(master, ns, subsystem)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
path, err := namespace.Path(dir, ns, subsystem)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
db, err := sqlitedrv.OpenDB(path, key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cek: open %s database for %s: %w", subsystem, ns, err)
|
|
}
|
|
return db, nil
|
|
}
|
|
|
|
// itoa avoids importing strconv for one constant in an error string.
|
|
func itoa(n int) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
var b [4]byte
|
|
i := len(b)
|
|
for n > 0 {
|
|
i--
|
|
b[i] = byte('0' + n%10)
|
|
n /= 10
|
|
}
|
|
return string(b[i:])
|
|
}
|