zeekay e6133f4be9 Release the plaintext on a proof, not on an absence of evidence
The shred was keyed on "this file does not have a plaintext SQLite header",
read as "it is the encrypted store, so the copy beside it can go". Four
different files answer that question the same way: an empty one, a truncated
one, one encrypted under another master, and the real store. Three of them
would have destroyed the last readable copy of the identity graph and returned
success — reachable by a commit that finished both renames and then failed to
open, where the next boot shreds the backup without opening anything at all.

So a file has four states now, not two, and the plaintext is released only by
settle, which OPENS the store under its key and READS its schema first. Opening
a handle would not do: database/sql connects lazily, so a wrong key surfaces at
the first read and not before. recoverInterrupted no longer shreds anything —
whether the backup may go is settle's question, and it cannot be answered from
file names.

A store too small to be a database is now an error rather than "already
encrypted". A zero-length file where the store was is what an unclean stop
leaves behind, and treating it as done let Open create a fresh one: a fully
mounted, completely empty identity service.

Three more, each one a real window:

  - fsync the copy's CONTENTS before the plaintext stops being the source of
    truth. syncDir made the RENAME durable, which is a different promise; the
    pages could still be in the page cache when the original was released.
  - read wal_checkpoint's RESULT. It reports a refusal in its first column and
    still returns success, so an unchecked call cannot be told from one that did
    nothing — and the plaintext WAL is deleted a few lines later on the strength
    of it having worked.
  - bind the key as a PARAMETER. Written into the statement, a parse error
    quotes it back, and SQLite would carry the whole key into a log the moment a
    build turns off the double-quoted-string fallback that makes KEY "x'...'"
    parse at all. Also: an attached target journals in DELETE mode, so -journal
    is the file that actually turns up beside it — clean it up and refuse to
    commit while it is there.
2026-08-11 10:51:01 -07:00

cek

Opens a namespace's SQLite database encrypted at rest.

cek.SetMaster(k)                            // once, at boot, from KMS
db, err := cek.Open(ns, "treasury", dataDir) // everywhere else

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. That is the property you want from encryption at rest, and the reason the master lives in KMS.

Where it sits

hanzoai/namespace names the entity, and where its file lives
hanzoai/cek turns the master + that name into the file's key, and opens it
hanzoai/sqlite opens a file under a raw key; knows nothing about who owns it
hanzoai/kms holds the master

Nothing here knows about orgs, users, billing or plugins. It knows a namespace, a subsystem, and a master key.

S
Description
No description provided
Readme MIT
128 KiB
Languages
Go 100%