Share a Memory by Wrapping a Key β Not Re-Encrypting a Single Byte
The two-key (DEK/KEK) pattern that makes sharing instant and a stolen database useless
Picture a memory as a locked box, with one key that opens it. To let a friend read that box, you don't unlock it and copy the contents out into the open. You take a spare copy of the box's key, seal it inside a tiny envelope only your friend can open, and hand them the envelope. The box never moves and is never opened in public; your friend just ends up holding a key that fits it. To take the access back, you destroy that little envelope. That is the whole trick of this article.
Here is the problem it solves, which sounds simple until you try to build it: you have an encrypted memory, and you want to let one specific other person read it. Not everyone. Not a copy in the clear. Just them, just this one memory, and you'd like to be able to take that access back later.
The naive answer is to unlock the box, copy out the contents, and re-lock a fresh copy under a key the recipient holds. It works. It's also a trap. Memories can be large, you'd be moving plaintext through memory on every share, and revocation would mean doing the whole dance again in reverse. Worse, you'd now have the same content encrypted two different ways, which is two different things to keep in sync.
NAOMS doesn't do any of that. When you share a memory, not a single byte of content is re-encrypted. We wrap a key. This is the DEK/KEK pattern, and once it clicks, a lot of the system's behavior β instant sharing, cheap revocation, the self-access guarantee β falls out of it for free. Let's build the intuition, then look at the real code.
Two keys, two jobs
The core move is to stop using one key for two jobs.
The first key is the DEK β the Data Encryption Key. It's a random 256-bit key, generated once per memory, and it's the only key that ever touches content. The memory's content is sealed with the DEK using AES-256-GCM: authenticated encryption, so any tampering with the ciphertext, the IV, or the auth tag is detected on the way out. The DEK is the key to the box itself.
The second key is the KEK β the Key Encryption Key. Its only job is to lock the little envelope around a copy of the DEK. It never sees content. Each being has their own KEK material, derived from their own key tree, so no shared secret exists anywhere outside the specific wrapping relationship between an owner and a recipient.
So the layers stack like this:
content --sealed by--> DEK --wrapped by--> KEK (per recipient)The content is encrypted exactly once, under one DEK, for its entire life. What changes when you share is the outer layer: a small sealed envelope holding a copy of the box-key, one per person who's allowed in.
That's the whole idea. Sharing a memory is sealing an envelope around its DEK for someone new. Revoking is destroying that envelope. The box never moves.
Why "wrap a key" beats "re-encrypt the data"
Think about the sizes involved. A DEK is 256 bits β 32 bytes. A memory can be a sentence or a long transcript or a document. The design says it plainly: re-wrapping a 256-bit key is trivially fast compared to re-encrypting potentially large content. You're paying a fixed, tiny cost per recipient instead of a content-sized cost per share.
There's a structural payoff too. Because the content has exactly one representation β one ciphertext under one DEK β there's nothing to keep in sync. Three people with access don't mean three encryptions of the memory; they mean one encryption and three small wrapped DEKs sitting next to it. Add a fourth person, add a fourth wrap. Remove one, remove one wrap. The data plane is untouched.
How a wrap actually happens
This isn't hand-waving β it's what the vault's key-wrapping routine actually does. To wrap a data key for a recipient, NAOMS needs a shared secret that only the owner (doing the wrapping) and the recipient (doing the unwrapping) can compute. That's an Elliptic-Curve DiffieβHellman agreement over X25519.
The implementation does something worth calling out: it generates a fresh ephemeral X25519 keypair for every single wrapping operation. Here's the sequence:
- Generate an ephemeral X25519 keypair, properly clamped for the curve.
- Run ECDH between the ephemeral private key and the recipient's public key to get a shared secret.
- Run that shared secret through HKDF-SHA256 (with the domain string
naoms-dek-wrap) to derive the actual wrapping key. - Encrypt the DEK under that wrapping key with a fresh random 12-byte nonce.
- Pack the result as
ephemeral_pubkey (32) || nonce (12) || ciphertext (DEK + 16-byte auth tag)and store it.
The ephemeral public key travels with the wrapped DEK precisely so the recipient can reconstruct the same shared secret from their own private key. Using an ephemeral keypair per wrap means two wraps of the same DEK for the same recipient don't produce the same bytes, and the owner's long-term key never has to participate in the agreement.
Crucially, the recipient's private key is required to unwrap. That's the asymmetry that makes the whole thing safe: anyone can be wrapped for (you only need their public key), but only the holder of the matching private key can ever unwrap. Wrap for Being B and B can decrypt; Being C, with no wrapped DEK and no key, cannot β that's the exact test the design holds itself to.
The wrapped DEKs are stored in a table keyed by memory and recipient, so "list every memory I can read" is just "list the wrapped DEKs addressed to me." Access becomes a query, not a guess.
The self-access guarantee, falling out for free
There's a quieter benefit hiding in this design, and it's one of the parts of NAOMS we care most about.
Because KEK material is derived from a being's own HD key tree, and because each encrypted memory's envelope carries the full derivation path it needs, a being who holds their BIP-39 mnemonic can decrypt all of their own memories with no running service at all:
mnemonic -> seed -> HD key tree -> derive key -> unwrap DEK -> decrypt contentNo vault process, no network, no NAOMS instance. The seed phrase is sufficient. The design calls this the Self-Access Guarantee, and it's a direct consequence of deriving keys deterministically rather than handing them out from a server. Even a fully compromised database is opaque: it holds ciphertext and wrapped keys, and nothing in it can decrypt anything. The seal only opens with keys that were never stored there.
Revocation: changing the locks, honestly
Now the hard part, and the part where we want to be precise rather than flattering, because the honest story is more useful than a tidy one.
The wrapping model gives you an obvious lever for revocation: stop honoring a wrapped DEK. In the shipped code, revocation does exactly this as a soft delete β it stamps the row as revoked, and the unwrap path refuses any wrapped DEK that's been marked that way. From that moment, that wrapped copy is dead to the system.
But there's a subtlety worth stating plainly. Soft-revoking a wrap means the system won't unwrap it anymore. The DEK itself, and the content under it, haven't changed. The stronger guarantee β the one where the old key is made useless forever, the way changing the locks makes an old physical key useless β requires rotating the DEK: generate a new DEK, re-encrypt the content under it, re-wrap the new DEK for everyone who still has access, and destroy the old DEK and all its wrapped copies. That's the design behind the planned revocable- access work.
As of this writing, the full cryptographic key-rotation-on-revoke ceremony is not shipped β that piece of work was cancelled. The wrapping-and-soft-revoke machinery is implemented and shipped; the rotate-and-re-encrypt revocation was designed but deliberately not built.
We're telling you this because the difference matters and pretending otherwise would be a lie of the convenient kind. That design's own values section already names the limit you can never escape: a party who has already decrypted and cached content cannot be made to un-see it. Cryptographic revocation protects future access to the source of truth β it does not reach into someone else's memory and erase what they've already read. That's not a NAOMS shortcoming; it's physics. Any honest access-control system lives with the same boundary.
Why it's built this way
Step back and the shape of the decision is clear. By separating the key that touches data (DEK) from the keys that grant access (KEKs), NAOMS turns three expensive data operations into cheap key operations:
- Sharing is wrapping a 32-byte key, not copying or re-encrypting content.
- Listing access is a query over the wrapped keys, not a scan.
- Revoking is, at minimum, killing a wrapped key β and, once full rotation lands, a bounded key-rotation rather than an unbounded data migration.
And the property that anchors all of it: the database can be fully compromised and still reveal nothing, because the keys that open the seals are derived from seeds that were never stored in it. Sovereignty isn't a policy here. It's a consequence of where the keys live.
What's next is closing the gap between the wrapping model that ships today and the full rotate-on-revoke ceremony the design describes β so that "I take it back" is as cryptographically final as "changing the locks" promises. The hard part was never the crypto. It's making sure every wrapped copy of the old key is found and destroyed. We'll get there honestly, or not at all.
Written by AI agents from real project logs; owned and edited by Mujo.