Expand description
v0.7.0 (issue #228) — E2E memory content encryption at rest.
This module is the substrate primitive for end-to-end encryption of
memory content columns at rest. It pairs a per-agent X25519 ECDH
keypair with ChaCha20-Poly1305 AEAD encryption so a single recipient
(an agent identified by agent_id) can decrypt content encrypted to
its public key.
§Wire shape
Each encrypted payload is serialised as a self-describing Envelope
and persisted into the new memories.encrypted_envelope BLOB column
(schema v44). The envelope layout is the byte concatenation of:
version (1 byte = 0x02)
ephemeral_pub (32 bytes — X25519 sender ephemeral pubkey)
nonce (12 bytes — ChaCha20-Poly1305 nonce, random)
ciphertext_with_tag (variable — AEAD ciphertext + 16-byte tag)The recipient’s static X25519 secret key (per-agent, generated and
cached via get_or_create_keypair) plus the envelope’s ephemeral
pubkey produce the shared secret. That secret is not used directly
as the symmetric key — H3 runs it through HKDF-SHA256 (domain-separated
by [HKDF_INFO]) to derive the ChaCha20-Poly1305 key, and binds the
envelope version + ephemeral pubkey into the AEAD associated data (AAD)
so the header cannot be swapped without failing authentication. Derived
key material is zeroized immediately after the cipher is constructed.
§Key lifecycle
Keypairs live in-memory only by default (per-process cache). A
follow-up issue will add on-disk persistence under
[crate::identity::keypair]-style files; today the in-memory
cache is sufficient for the encrypt → store → recall → decrypt round
trip exercised by tests/encryption_at_rest.rs.
§Activation
Callers gate at-rest encryption behind either:
- The
[encryption].at_rest = trueconfig field (operator opt-in viaconfig.toml), OR - The
AI_MEMORY_ENCRYPT_AT_REST=1environment variable (CLI / container-runtime opt-in).
Both surfaces feed the same encryption_enabled gate, which the
storage write path consults before invoking encrypt / decrypt.
Structs§
- Envelope
- Decrypt-able envelope produced by
encrypt. Carries the sender’s ephemeral X25519 pubkey + the AEAD nonce + the ciphertext-with-tag.Envelope::to_bytes/Envelope::from_byteshandle the substrate-stable wire shape; storage callers persist the bytes verbatim into theencrypted_envelopecolumn. - Keypair
- Per-agent X25519 keypair. The static-secret variant supports cloning so the per-process cache can hand out copies without re-deriving from the random generator.
Constants§
- AEAD_
KEY_ LEN - ChaCha20-Poly1305 key length in bytes, and therefore the HKDF output
length the [
derive_aead_key] expand step produces. - ENVELOPE_
VERSION - Envelope wire-version. Bumped when the byte layout OR the
cryptographic scheme (KDF / AAD construction) changes; readers refuse
unknown versions with a typed error so a bump doesn’t silently
mis-parse or mis-decrypt legacy rows.
0x02introduced the H3 HKDF-SHA256 key derivation + AAD header binding (the0x01MVP used the raw X25519 output directly with empty AAD). - NONCE_
LEN - ChaCha20-Poly1305 nonce length in bytes.
- PUBKEY_
LEN - X25519 pubkey length in bytes.
- TAG_LEN
- ChaCha20-Poly1305 AEAD tag length in bytes (appended to ciphertext
by
Aead::encrypt).
Functions§
- decrypt
- Decrypt an
Envelopeusing the recipient’s static X25519 secret key (my_sk). Returns the original UTF-8 plaintext. - encrypt
- Encrypt
contentto the given recipient X25519 public key, returning a self-describingEnvelope. - encryption_
enabled - Consult the [encryption].at_rest config flag OR the
AI_MEMORY_ENCRYPT_AT_REST=1env var. Truthy env values:1/true/yes/on(case-insensitive). Used by the storage write path to gate the encrypt-on-insert / decrypt-on-read branches. - get_
or_ create_ keypair - Look up the per-agent X25519
Keypair, generating + caching it on first call. Subsequent calls for the sameagent_idreturn clones of the cached entry, so plaintext encrypt + recall + decrypt within a single process always round-trips through the same recipient secret.