Encryption — at rest, in transit, integrity, supply chain.

Four crypto surfaces, every one named, every one with the algorithm and the threat model. SQLCipher AES-256 at rest. mTLS for peer-to-peer federation. HMAC-SHA256 for webhook integrity. Signed git tags + SBOM for supply chain. What's covered, what isn't, and what's coming in v0.7.

SQLCipher at-rest mTLS federation HMAC-SHA256 webhooks signed tags + SBOM v0.7: Ed25519 attested
The four surfaces

At a glance.

surface 1

At rest

SQLCipher · AES-256-CBC · PBKDF2-HMAC-SHA512

The whole SQLite database file is encrypted with the operator's passphrase. No plaintext on disk. Failed key triggers an error at db::open: "PRAGMA key failed (wrong passphrase or unencrypted DB?)".

surface 2

In transit (federation)

rustls · TLS 1.3 · mTLS · SHA-256 fingerprint allowlist

Peer-to-peer federation traffic travels over mutual TLS. Each peer's client cert is pinned by SHA-256 fingerprint in the allowlist file — only allowlisted peers can push or pull, even if they have a valid cert.

surface 3

Integrity (webhooks)

HMAC-SHA256 · per-subscription secret · timestamp-bound

Every outbound webhook POST carries an X-AI-Memory-Signature: sha256=… header. Body + timestamp are HMAC'd with the subscription's secret. Receivers verify before trusting the payload.

surface 4

Supply chain

GPG-signed git tags · cargo-audit · SBOM · reproducible builds

Every release tag is GPG-signed. CI runs cargo audit against the RustSec advisory DB. SBOMs travel with each binary. Build is reproducible from the signed tag — no opaque inputs.

Surface 1 — at rest

SQLCipher database encryption.

PRAGMA keylive since v0.5
When the daemon opens an encrypted DB, it issues PRAGMA key = '…' with the operator's passphrase. SQLCipher's libsqlcipher (drop-in libsqlite3 replacement) handles AES-256-CBC of every page + PBKDF2-HMAC-SHA512 key derivation. A wrong key → loud error at startup.
// from src/db.rs::open conn.execute_batch("PRAGMA key = 'YOUR_PASSPHRASE'") .context("PRAGMA key failed (wrong passphrase or unencrypted DB?)")?; // Operator workflow $ export AI_MEMORY_DB_KEY="$(cat /run/secrets/db-key)" $ ai-memory serve --db /var/lib/ai-memory/data.sqlite // On the wire — encrypted database file is opaque to anyone without the key. // Even page headers are encrypted; the file looks like noise to a forensic // disk dump.
Honest assessment

The passphrase has to live somewhere — usually a secret manager (Vault, AWS Secrets Manager, GCP Secret Manager) or a systemd credential. SQLCipher protects the data file; it doesn't protect the running process's memory or the passphrase environment variable. Pair with disk encryption and process hardening for defense in depth.

Encrypted backupsimplicit
Because SQLCipher encrypts the file on disk, any standard SQLite backup mechanism (file copy, VACUUM INTO, sqlite3 .backup) produces an already-encrypted output. Backup tooling doesn't need to know about encryption — the bytes on disk are already opaque.
Surface 2 — in transit (federation)

Mutual TLS with fingerprint pinning.

rustls TLS 1.3live since v0.6
Federation HTTP traffic uses rustls — Rust-native TLS, no OpenSSL dependency. TLS 1.3 only (no fallback to 1.2 or below). Forward secrecy via X25519 ECDHE. AEAD ciphers only.
mTLS — mutual authlive
Both ends present client certificates. The receiving daemon validates the peer's cert chain (using the configured CA bundle) AND checks its SHA-256 fingerprint against the allowlist. A peer with a valid cert but unallowlisted fingerprint is rejected at handshake time.
# Fingerprint allowlist — one per line, # comments tolerated (incl. trailing). # Allowlist applies to mTLS client certs presented during sync_push/since. sha256:abc123…def456 # node-1 production sha256:fed987…321cba # node-2 production sha256:111222…aaa888 # node-3 backup # Bug fix [#358] (v0.6.3 Patch 3) — trailing # comments now tolerated; # previously a line like "sha256:abc...def # node-1" failed the 64-char # hex-length check and aborted ai-memory serve.
SSRF guards on webhook URLslive since v0.6.3 W10/W11
Outbound webhook URLs pass through two SSRF guards before any HTTP attempt: a URL guard (rejects non-http/https schemes, file://, data:) and a DNS guard (resolves the host, rejects loopback / RFC1918 / link-local addresses). Two SSRF defects were closed in commit 9eeb453 for v0.6.3 final.
Honest assessment — what mTLS does and doesn't do

mTLS authenticates the peer node, not the memory author. Once a peer is allowlisted, every memory it pushes is trusted. v0.7 Bucket 1 (Ed25519 attested identity) closes that gap by attaching a per-memory signature so individual memories' provenance is verifiable independent of which peer relayed them.

Surface 3 — integrity (webhooks)

HMAC-SHA256 signed dispatch.

memory_subscribe — secret-boundlive since v0.6.0
When a webhook subscription is created, the operator provides a secret. The daemon stores the secret as a hex-encoded hash (secret_hash in the subscriptions table). Every dispatch is signed with the secret using HMAC-SHA256.
// MCP — memory_subscribe { "url": "https://hooks.example.com/ai-memory", "secret": "…32+ random bytes…", // HMAC key (operator-supplied) "events": ["memory_store", "memory_promote"] } // Outbound POST to the webhook URL POST /ai-memory HTTP/1.1 Host: hooks.example.com Content-Type: application/json X-AI-Memory-Timestamp: 2026-04-27T05:00:00Z X-AI-Memory-Signature: sha256=a1b2c3d4… {"event":"memory_store", "memory_id":"…", "namespace":"…", …}
Canonical signing stringstable
The signature covers {timestamp}.{body}. Receivers reconstruct the canonical string and HMAC-SHA256 it with their stored secret; X-AI-Memory-Signature: sha256=… must match. Timestamp is included so receivers can reject replays older than a window of their choice.
// Receiver pseudo-code (Node.js) const sig = req.headers["x-ai-memory-signature"].replace("sha256=", ""); const ts = req.headers["x-ai-memory-timestamp"]; const canonical = `${ts}.${rawBody}`; const expected = crypto.createHmac("sha256", MY_SECRET).update(canonical).digest("hex"); if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) throw "bad signature";
Surface 4 — supply chain

Signed tags, SBOM, reproducible builds.

GPG-signed git tagssince v0.6.3
Every release tag is signed with the alphaonedev maintainer key. git tag -v v0.6.3 verifies. CI workflow refuses to publish releases from unsigned tags.
cargo audit on every CI runsince v0.5
CI installs cargo-audit and runs it against the RustSec advisory DB on the Ubuntu job. A new CVE in any transitive dep fails CI before merge. Adversarial dep updates can't slip in.
SBOM with every releasesince v0.6
Each release artifact ships with a Software Bill of Materials (CycloneDX format). Every transitive dep, version, and SHA-256 is enumerated. Procurement teams can ingest the SBOM into their compliance tooling without bespoke work.
Reproducible buildsopt-in
The release pipeline pins the Rust toolchain version + the git tag SHA. Building the same tag twice on a clean runner produces byte-identical artifacts. Operators can verify they're running the same code that was audited.
v0.7 roadmap

Per-memory cryptographic identity.

v0.6.3 ships a placeholder signature column on memory_links + a deferred observed_by column. v0.7 Bucket 1 fills that contract.

Ed25519 attested identityv0.7 Bucket 1
Each agent gets an Ed25519 keypair. Every memory write attaches a signature over the canonical row (id + content hash + timestamp). Receivers verify the signature before accepting the memory. Cross-peer provenance becomes verifiable end-to-end — not "this peer says X authored this" but "we have a cryptographic proof X authored this."
Sidechain transcriptsv0.7 Bucket 1.7
Every governed action lands a row in a transcript table with the full payload + verdict + signature. Independent of the daemon log, queryable, and tamper-evident (each transcript row signs the previous row's hash, forming a chain).
CRDT merge tiebreak via attested identityv0.8 Pillar 3
Concurrent edits across federation peers get merged via CRDT semantics; when two edits tie on vector clock + timestamp, the Ed25519 signature determines precedence. Deterministic conflict resolution with cryptographic provenance.
What's NOT encrypted

Honest scope.

Process memory. Live memories sit in RAM after the daemon opens the encrypted DB. A core dump or memory-scraping malware on the host can read them. Mitigation: pair with OS-level process hardening + secure-boot + memory encryption (Intel TME, AMD SME).
Daemon ↔ MCP/HTTP clients on localhost. Default deployment binds to 127.0.0.1 — no encryption needed because traffic never leaves the loopback. Operators exposing the daemon on a network interface should put it behind a TLS-terminating reverse proxy.
Embeddings sent to the LLM provider. Smart / Autonomous tier embedders run locally via Ollama — content stays on-host. Operators configuring a remote embedder URL are sending content over the wire to that URL; that channel's encryption is the operator's responsibility.
Backup files (without operator wrapping). SQLCipher protects the live DB. sqlite3 .backup produces an encrypted file. But if you export to JSON via memory_export, the export is plaintext — wrap it in your own encryption before storing.