The Database Falls, and Nothing Spills
For most of this project's life, at-rest encryption was all-or-nothing: one passphrase locked the entire database, and once it was open, everything inside was legible. This week we shipped the opposite โ every kind of data sealed in its own cryptographic compartment, so a fully copied database, or even a compromised process holding the master vault open, reveals nothing it was never granted.
The unit of secrecy matters as much as the strength of the lock.
For most of this project's life, NAOMS encrypted its local store the obvious way: the whole database, as one block. A passphrase derived a key, the key unlocked the file, and from that moment everything inside was legible. It's the standard move, and it has a standard weakness. The unit of secrecy is the entire database. Open it for one purpose and you've opened it for all of them. A piece of code that needed to read a calendar reminder could, in principle, read a saved password sitting in the same file โ not because anyone granted it that, but because there was only ever one lock, and it was already open.
That's fine right up until it isn't. If an attacker copies the database file at rest, they get one thing to crack. If a bug or a hostile dependency runs inside the process while the vault is unlocked, it inherits the whole thing. The blast radius of any single compromise is everything.
This week (item 1688, celebrated June 30th) we changed the unit. Not the strength of the lock โ the granularity of it.
Compartments, not one big room
The new model gives each kind of data its own cryptographic compartment. A
partition names a compartment, and they form a small escalate-only lattice
(classify-partition.ts):
general (0, plaintext) < personal (1) < secret (2)generalis today's behaviour, byte-for-byte: no field encryption. Timestamps, type tags, structural metadata โ the boring stuff that has to stay legible for the database to be a database โ lives here.personalis an at-rest compartment for sensitive being data: the contents of memories, notes, the things you'd mind a stranger reading.secretis maximum sensitivity: passwords, cards, credentials, recovery material.
The classification is declared, per data kind, in the feature manifest โ a kind
says "encrypt these fields, under at least this partition." The resolver
(classifyPartition) then returns the higher of the declared floor and
whatever a writer requested at runtime. It never returns lower. A writer can
escalate a record into a stronger compartment; it can never quietly downgrade one
out of the compartment its kind demands. Undeclared kinds fall to general, so
nothing changes for data that was never sensitive to begin with.
The crucial design choice is that this is field-level, not
whole-payload. Inside a single memory, the content field gets sealed while the
type, the ids, and the timestamps stay in plaintext. That's not a compromise โ
it's what lets the graph keep indexing and querying structurally while the
sensitive bytes stay dark. You can find and order your memories without any code
being able to read what's in them.
flowchart TD
A[Write a classified record] --> B[classifyPartition ยท pick the compartment]
B --> C[Fresh per-record DEK ยท ChaCha20-Poly1305 seals only the declared fields]
C --> D[Wrap the DEK for the partition domain via PRE ยท envelope magic 0x04]
D --> E[Embed key-blind __enc envelope ยท sign the ciphertext we store]
E --> F[(At-rest DB ยท sealed fields dark ยท structural fields legible)]
F --> G[Read at the outbound projection boundary]
G --> H{Caller holds a grant for this domain?}
H -- yes --> I[Recover DEK via live PRE path ยท decrypt in place]
H -- no --> J[Typed encrypted_no_key placeholder ยท never ciphertext]
The write seam: seal it, or refuse โ never plaintext
Every classified write funnels through one chokepoint (encrypt-on-append.ts).
For each record, it mints a fresh per-record data-encryption key, seals only
the declared fields with ChaCha20-Poly1305, and then wraps that DEK for the
partition's domain using the proxy-re-encryption key stack that already runs in
the system. The wrapped key and the list of sealed fields ride along as a
key-blind __enc envelope embedded in the record โ "key-blind" because the
component that carries it onto the graph node never holds a DEK and never
decrypts anything. It just copies the envelope through.
One invariant governs the whole seam, and it's the honest one: we sign the bytes we store. The signature covers the ciphertext, not the cleartext that briefly existed before it. There's no window in which what's signed and what's on disk disagree.
The part worth dwelling on is what happens when the seam can't seal. If the
vault is locked, the partition key can't be wrapped โ so the write is refused,
loudly, with a PartitionLockedError. It does not fall back to writing your
password in plaintext because encryption was momentarily inconvenient. There's a
second, sharper refusal for the compartments that require a two-device unlock: if
that quorum hasn't happened, the write raises CompartmentLockedError rather than
silently sealing the record in a weaker, single-device-readable way. And a third
guards a subtle incompatibility โ a classified record emitted in a format that
would relocate its fields to a place the read seam can't find them is rejected up
front, because a write that succeeds and can never be read back is just silent
data loss wearing a success message.
Fail-closed, every time. The Honesty axiom in this system is not a slogan on a wall; it's a set of exceptions that would rather stop your write than lie about what they did with your data.
The read seam: "no key" is an answer, not an error
Reading is where compartmentalization earns its keep. The decrypt path
(decrypt-node-fields.ts) runs at exactly one place โ the outbound projection
boundary, where data leaves for a client โ and deliberately not on the
internal query hot path that reducers and enrichers use. Those internal readers
work fine against the structural plaintext fields and simply tolerate ciphertext
in the sealed ones, because the two field sets are disjoint by construction.
At that outbound boundary, for each sealed record, the seam asks one question: does this caller hold an active grant for the record's domain? If yes, it recovers the data-encryption key through the live re-encryption path โ the same mechanism the rest of the system's payload decryption uses, verified by the envelope's magic bytes, not a bespoke shortcut โ and decrypts the fields in place.
If no, it does something more interesting than throwing an error. It replaces
each sealed field with a typed placeholder: { __encrypted: true, reason: "encrypted_no_key" }. Not the ciphertext. Not a fabricated blank. An explicit,
honest marker that says "there is something here, and you don't have the key for
it." There's even a distinct second marker, quorum_unmet, so a client can tell
apart "you lack the grant entirely" from "this compartment needs a two-device
unlock you haven't done" โ two different situations that deserve two different
answers, and two different remedies.
Three hard rules bind this path, and they're the whole point: it never returns ciphertext as if it were content, it never leaks plaintext to a caller without a grant, and it never throws โ any failure degrades to the placeholder. A decrypt path that crashes is a decrypt path that can be used to probe what exists. A decrypt path that returns raw ciphertext is one that leaks under the wrong client. This one does neither.
The catch an audit caught
The most instructive part of this build was a leak that the design's first cut didn't close, and a security pass did. The naive version redacts the fields it sees in the outbound projection. But the projection is, in the threat model, attacker-writable โ and an attacker who could omit a sensitive field from the projection could get its ciphertext served through a different, unredacted path. Redacting what you see isn't enough when what you see is controlled by the adversary.
The fix (M7, closing the audit's CRITICAL-1 / HIGH-1 / MED-1c) is to take the set of sealed fields from the FROST-signed source commit, never from the projection. The signed commit is the ground truth an attacker can't forge. So the seam redacts every field the verified envelope names โ even ones the projection tried to leave out โ and the omit-a-field trick simply stops working. It's a small change with a large moral: the redaction list has to come from the thing that's signed, not the thing that's convenient.
What is live, and what is honestly not
The core star is live on the main line and end-to-end proven: memories, credentials, and governance records can each be sealed in their own compartment; a stolen database reveals nothing it was never granted; a caller without a grant gets the typed marker; and the vault's own secrets now flow through this same seam rather than a bespoke crypto path of their own. Twelve real-crypto tests carry it โ genuine reconstruct, real wrap-to-recipient, anti-downgrade read threading, fail-closed negatives armed by the actual production code โ and they were held to a first-hand bar. When an early attempt to celebrate rode on a relayed "12 of 12" that ground truth showed was really 1 of 12, it was reverted; the celebrate stands only on a genuine, first-hand-verified pass. We'd rather tell you that than let it pass quietly.
And two things are explicitly not finished, because saying so is part of the contract. The two-operating-system-process cross-device compartment-key unlock โ the flow where a second physical device participates in opening a maximum-sensitivity compartment โ is named follow-on work (tracked under item 1681), not something live today; in the current build that cross-device hop is modelled within a single process, and the sign-off says so plainly rather than implying the multi-device story is done. Separately, folding the compartment identifier into the signed commit tuple for full tamper-evidence is a fleet-wide cutover being staged on its own, under direct watch, precisely because it touches every existing record.
The shape of the win is easy to state and was hard to earn. The database used to be one room with one lock. Now it's a building of sealed compartments, each with its own key, and the walls are the ciphertext itself. Copy the whole building and you hold a stack of locked doors. Stand inside one unlocked room and you still can't see into the others. The database can fall โ and nothing it was never granted spills out.
Written by AI agents from real project logs; owned and edited by Mujo.