Encryption
How does Areev encrypt data at rest?
Areev encrypts every grain blob with AES-256-GCM using a random 96-bit nonce before writing to the Fjall LSM store. This context database encrypts AI memory at the grain level, giving each user a unique 256-bit Data Encryption Key (DEK) wrapped by a master key through envelope encryption.
The encryption module generates a fresh nonce from the OS CSPRNG (Aes256Gcm::generate_nonce(&mut OsRng)) for every encrypt call. The GCM authentication tag provides tamper detection — flipping a single byte in the ciphertext or tag causes decryption to fail. Each encrypted grain carries 28 bytes of overhead (12-byte nonce + 16-byte authentication tag), stored in the envelope format [nonce:12B][ciphertext][GCM-tag:16B].
All key material uses the DerivedKey type which implements zeroize::Zeroize on drop, clearing secrets from memory when no longer needed. Intermediate HKDF output buffers use Zeroizing<[u8; 32]> to prevent key material leaks on error paths.
import requests
# Store an encrypted grain via HTTP
resp = requests.post("http://localhost:4009/api/memories/default/add", json={
"subject": "john",
"relation": "likes",
"object": "coffee",
"content": "John mentioned he likes coffee"
})
# Grain is AES-256-GCM encrypted before hitting disk
POST /api/memories/default/add HTTP/1.1
Host: localhost:4009
Content-Type: application/json
{"subject":"john","relation":"likes","object":"coffee"}
# Encryption is always active when a master key is set
areev serve --http 0.0.0.0:4009 --master-key $AREEV_MASTER_KEY
How does Areev manage encryption keys?
Areev uses envelope encryption with random per-user DEKs. Each user gets a unique 256-bit Data Encryption Key generated from the OS CSPRNG (OsRng), then wrapped (encrypted) by the master key using AES-256-GCM. The wrapped DEK is stored in a dedicated Fjall partition keyed by user ID. This autonomous memory system isolates every user’s AI memory data under a distinct cryptographic boundary.
Scope-level encryption works the same way — each scope path gets a random 256-bit DEK, wrapped by the master key and stored keyed by scope path. Blind index keys for subject, relation, and object fields are derived from the user’s DEK via HKDF-SHA256, and a settings encryption key is derived via HKDF("areev-settings-key:llm"). This AI agent memory architecture prevents a compromise of one user’s key from affecting any other user.
Master Key (256-bit)
|
+-- wraps random per-user DEK (AES-256-GCM)
| |
| +-- HKDF("areev-blind-index:subject") --> subject blind key
| +-- HKDF("areev-blind-index:relation") --> relation blind key
| +-- HKDF("areev-blind-index:object") --> object blind key
|
+-- wraps random per-scope DEK (AES-256-GCM)
+-- HKDF("areev-settings-key:llm") --> settings encryption key
How does encrypted search work?
Areev uses HMAC-SHA256 blind index tokens to enable equality searches on encrypted fields without decrypting the data. Each searchable field (subject, relation, object) gets a dedicated key derived from the user’s DEK, so this context database supports full SPO queries over ciphertext.
Blind tokens are case-insensitive and whitespace-insensitive by design. The system normalizes input (lowercase, trim, Unicode NFC), computes HMAC-SHA256(field_key, value), and truncates to 128 bits (16 bytes), yielding a deterministic 32-character hex token. The same value always produces the same token with the same key, enabling exact-match lookups in the hexastore index. Different field keys produce different tokens for the same value, preventing cross-field correlation attacks.
# Searching encrypted grains — the API handles blind indexing transparently
resp = requests.post("http://localhost:4009/api/memories/default/recall", json={
"subject": "john", # Converted to blind token server-side
"relation": "likes"
})
# CLI search also uses blind indexes under the hood
areev recall --subject john --relation likes
How does tiered storage encryption work?
Grains in the hot tier (local Fjall) are encrypted with the user’s DEK. When grains compact to warm (S3/Azure/GCS) or cold (Glacier/Archive) storage, they are AES-256-GCM encrypted before upload. This AI memory architecture maintains encryption across all storage tiers without any manual re-encryption step.
The EncryptedBlob newtype provides compile-time enforcement that only encrypted data can be passed to ObjectBackend::put(). There is no public constructor — the only way to create an EncryptedBlob is through the encrypt_for_tier() function, ensuring unencrypted data can never accidentally leave the hot tier. Each tier uses the same AES-256-GCM algorithm but wraps blobs independently, so tier migration does not require access to the original plaintext.
Hot tier: encrypted with user DEK (local SSD)
Warm tier: AES-256-GCM encrypted, uploaded to object storage
Cold tier: AES-256-GCM encrypted, uploaded to archive storage
Related
- Key Management: Key lifecycle, rotation, and backend options
- Crypto-Erasure: GDPR Art. 17 right-to-erasure via key destruction
- Audit Trail: Hash-chained audit entries for every operation