cek

package
v1.801.307 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package cek is cloud's ONE encryption-at-rest gate for its per-subsystem SQLite stores. Every store opens its database through cek.Open — the single seam where a plaintext file is transparently migrated to SQLCipher and a keyed *sql.DB is returned — so "encrypted at rest" is a property of the open path, not something each of the ~30 stores must remember to do.

THREAT MODEL. The DO block volume under /var/lib/cloud is provider-encrypted, so the residual exposure is a COPIED PV snapshot/backup or an in-cluster exec/PV read seeing plaintext customer PII (crm), the ledger (treasury), org wallet maps (wallets), the audit log, team/entitlements. cek removes that exposure: each file's pages are SQLCipher-encrypted under a per-database key that never leaves the process in the clear and is itself wrapped by the KMS-injected master key; the pre-migration plaintext copy is shredded once the encrypted store is proven readable. A lifted file is useless without the key.

SCOPE / NON-GOALS. cek provides CONFIDENTIALITY at rest against the read-only exposure above (no master key ⇒ no plaintext). It is NOT integrity, authenticity, or anti-rollback against a PV-WRITE (node-compromise) adversary who can modify the volume: the per-file id lives in the (unauthenticated) .dek sidecar, so such an adversary could still replay an old snapshot of a store in place. Swapping two stores ACROSS tenants no longer works — the owner is in the KEK and the AAD, so a pair carried into another org's directory fails to unwrap (Principal). Rollback of a store onto itself remains out of model; it needs an epoch, which the sidecar does not carry.

ENVELOPE (the primitives live in github.com/hanzoai/sqlite/cek.go and are reused verbatim — one crypto implementation, KAT-gated there):

  • Each database has its OWN random 256-bit DEK (the SQLCipher page key), minted once at first touch and NEVER changed, so ciphertext pages are never rewritten.

  • Each database also gets a random 128-bit FILE ID, stored in the clear at the head of its <db>.dek sidecar. The KEK derives from the OWNER and that id, never from the file path:

    KEK = HKDF-SHA256(masterKey, lp(type) || lp(owner || "/" || hex(fileID)))

    for a tenant store, and lp("global") || lp(hex(fileID)) for the platform partition, which has no owner. Because the path is absent, moving the data dir or changing CLOUD_DATA_DIR can never change a KEK or brick a store; because the owner is present, a {db,.dek} pair carried into another tenant's directory fails to unwrap rather than opening. RFC-5869 HKDF via x/crypto/hkdf — NOT luxfi/crypto/kdf (a QZMQ KeySchedule, not generic HKDF; using it would brick every store).

  • The DEK is wrapped AES-256-GCM under the KEK, bound to the same id as AAD. Sidecar = fileID(16) || wrapped-DEK. The raw DEK is never written.

  • Master-key ROTATION rewraps only the sidecar: the DEK and fileID are unchanged, so no page is rewritten and no file can be bricked.

FAIL-SECURE. On an encryption-CAPABLE build (production is CGO + libsqlcipher) a missing master key is FATAL — the data plane refuses to open unencrypted, the same posture the KMS store takes. A key set on a NON-encrypting build is likewise fatal. An encrypted file whose sidecar is missing is refused. A migration whose encrypted copy does not reproduce the source's schema AND per-table content (a rowid-independent multiset hash, not just a row count) leaves the plaintext untouched and errors — the caller (MountAll) fails closed, so cloud never serves a half-migrated data plane.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotBound = fmt.Errorf("cek: store not bound to the source principal")

ErrNotBound reports that a store did not unwrap under the principal it was asked to move FROM — most often because it is already bound to the target, which makes a re-run a no-op rather than a failure.

View Source
var Global = Principal{/* contains filtered or unexported fields */}

Global is the cross-org platform partition: certs, providers, the audit trail, the gateway's own store. Its id is fixed, so a platform store's key derives from the file id alone exactly as it always has — the platform partition is not a tenant and has no owner to bind to.

Functions

func Encrypting

func Encrypting() bool

Encrypting reports whether cek will encrypt at rest (a valid master key is configured). cloud calls this once at boot for the posture log; a false result means resolveMaster errored (no or invalid key) and the first store Open will fail closed rather than write plaintext.

func EnsureDevKey added in v1.801.231

func EnsureDevKey() bool

EnsureDevKey installs a deterministic development master key when NO key is configured AND the live codec is not linked — i.e. a pure-Go dev/CI build. It lets that build run through the SAME encrypted path as production (per-db DEK, SQLCipher-format envelope) with zero configuration, instead of a divergent plaintext path. It is a NO-OP when a key is already configured (production uses it) or the live codec is linked (a production binary, which MUST supply the real KMS key and fails closed without it). Call once at boot, before the first Open. Returns true when a dev key was installed. Not safe against a concurrent Open — resolveMaster caches on first use, so this must run first.

func Exists added in v1.801.231

func Exists(path string) bool

Exists reports whether a store lives at path — the question "has this org/ subsystem been created yet?", asked by every caller that discovers stores by walking the data directory.

os.Stat on the database file alone does NOT answer it. The pure-Go codec holds the database in its envelope and materializes the file on close, so a store that is OPEN right now has only its sidecar on disk; the live codec writes the database in place and has both. A caller that stats only the database therefore sees a store on one build and not the other, and on the pure-Go build skips precisely the stores that are in use. cek mints the sidecar eagerly, before the first byte of database is written, on both codecs — so "database or sidecar" is the marker that holds on every build and at every point in a store's life.

The layout stays cek's own: callers ask this rather than knowing the suffix.

func Master added in v1.801.299

func Master() []byte

Master returns the resolved data-plane key, or nil when none is configured.

It exists for ONE caller — the credential broker, which hands the same key to every child process in the deployment. That is not a convenience: the children open the SAME encrypted files, so a child that resolved a key of its own would write a store no sibling could read. The broker therefore has to be able to state which key this deployment settled on, including the dev key a keyless build installs for itself.

It resolves (and so memoizes) exactly like Encrypting, and must be called after any SetMasterKey/EnsureDevKey — the same ordering every caller here has.

func Open

func Open(p Principal, path string) (*sql.DB, error)

Open returns a *sql.DB for the SQLite database at path, encrypted at rest and bound to p. It is the single way a cloud store opens its file.

The principal comes first because it is the question a caller must answer, not one it may forget: pass cek.Global for a platform store, cek.Org(slug) for a tenant's. Passing the wrong one is not a silent mistake — the store will not unwrap.

func Rebind added in v1.801.307

func Rebind(from, to Principal, path string) error

Rebind moves one store from one owner's key to another's. It exists because the principal became part of the derivation AFTER stores were already on disk: a tenant store written when every file keyed under the platform tag cannot be opened by its owner until its sidecar is rewrapped.

It is an OPERATION, deliberately not a fallback inside Open. A store has one key and Open derives it one way; a second derivation tried on failure would mean every open silently accepts two answers forever, which is the thing that made the old binding unenforceable in the first place. Running this once is a migration. Leaving it in the open path would be a permanent ambiguity.

Only the sidecar changes. The DEK and the fileID are read out under `from` and written straight back under `to`, so not one database page is rewritten and the file itself is never opened — which is why this is safe on a store too large to copy and why a failure cannot corrupt data. The write is atomic (temp + rename), so an interrupted rebind leaves either the old sidecar or the new one, never a torn file.

It is idempotent in the way that matters: a store already bound to `to` fails to unwrap under `from` and returns ErrNotBound, so re-running a completed migration reports "already done" rather than damaging anything.

func RebindOrgs added in v1.801.307

func RebindOrgs(dataDir, platformSlug string) (bound, skipped int, errs []error)

RebindOrgs binds every per-org store under dataDir to the org that owns it, which is the one migration the principal change requires. It walks {dataDir}/orgs/<slug>/, including the nested projects/<slug>/ level, and rebinds each store from Global to Org(slug).

The reserved platform partition is skipped: it is not a tenant and keys as Global both before and after, so there is nothing to move.

It never stops on one store's failure. A run over a live volume will meet stores that are already bound (a re-run, or a store created after the change) and those are counted as skipped, not errors — the point of the walk is to leave every store bound, and that is a state to converge on rather than a transaction.

func SetMasterKey

func SetMasterKey(k []byte)

SetMasterKey injects the 32-byte master key explicitly (cloud's boot resolves it once from cfg and hands it here), taking precedence over the environment. Call before the first Open. A wrong-length key is ignored so the env path can still apply.

Types

type Principal added in v1.801.307

type Principal struct {
	// contains filtered or unexported fields
}

Principal is WHOSE store this is. It is the first argument to Open because it is part of a store's identity, not a property of its location: the same bytes at the same path belong to exactly one org, and the key says so.

It binds the derivation. Before, every store — platform and per-org alike — derived under the single tag "global", so a store's key knew nothing about its owner and a {db,.dek} pair lifted into another org's directory opened there perfectly well. Confidentiality between orgs still held (each file has its own random DEK under its own KEK), but nothing tied a file to the tenant it belonged to. Now the owner is in the HKDF info and in the GCM AAD, so a store carried across a tenant boundary fails to unwrap instead of opening.

func Org added in v1.801.307

func Org(slug string) Principal

Org binds a store to one tenant. slug MUST be the value SanitizeOrg produced (the injective slugger OrgDB already folds every org through), so two distinct orgs can never derive the same key.

func User added in v1.801.307

func User(id string) Principal

User binds a store to one person, for the per-user partition.

func (Principal) String added in v1.801.307

func (p Principal) String() string

String renders the principal for errors and logs. It never carries key material.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL