Skip to main content

Module encryption

Module encryption 

Source
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 = true config field (operator opt-in via config.toml), OR
  • The AI_MEMORY_ENCRYPT_AT_REST=1 environment 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_bytes handle the substrate-stable wire shape; storage callers persist the bytes verbatim into the encrypted_envelope column.
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. 0x02 introduced the H3 HKDF-SHA256 key derivation + AAD header binding (the 0x01 MVP 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 Envelope using the recipient’s static X25519 secret key (my_sk). Returns the original UTF-8 plaintext.
encrypt
Encrypt content to the given recipient X25519 public key, returning a self-describing Envelope.
encryption_enabled
Consult the [encryption].at_rest config flag OR the AI_MEMORY_ENCRYPT_AT_REST=1 env 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 same agent_id return clones of the cached entry, so plaintext encrypt + recall + decrypt within a single process always round-trips through the same recipient secret.