Four crypto surfaces, every one named, every one with the algorithm and the threat model. SQLCipher AES-256 full-database at rest, plus opt-in ChaCha20-Poly1305 per-memory content encryption (#228). mTLS for peer-to-peer federation. HMAC-SHA256 for webhook integrity. GPG-signed git tags + cargo-audit (RustSec) for supply chain; a CycloneDX SBOM ships in v1.0. What's covered, what isn't, and what v0.8.0 added (opt-in per-memory content envelopes; Ed25519 attested identity since v0.7.0).
The whole SQLite database file is encrypted with the operator's passphrase (SQLCipher). No plaintext on disk. Failed key triggers an error at db::open: "PRAGMA key failed (wrong passphrase or unencrypted DB?)". v0.8.0 adds an orthogonal, opt-in per-memory content envelope (#228) keyed to the writing agent's X25519 key — works on plain SQLite and Postgres.
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.
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.
Every release tag is GPG-signed. CI runs cargo audit against the RustSec advisory DB. A CycloneDX SBOM ships with each release binary in v1.0. Build-provenance attestation (SLSA-style) is tracked post-v1.0; bit-for-bit reproducible builds are NOT claimed (they need an independent rebuilder).
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.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.
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.content field is sealed into a self-describing AEAD envelope keyed to the writing agent's X25519 keypair. The plaintext never lands in the content column — the ciphertext envelope is persisted in encrypted_envelope and the column carries an empty placeholder. Content-only: title, tags, and metadata stay plaintext. Wired on both backends — embedded SQLite (src/storage/mod.rs) and PostgreSQL (src/store/postgres.rs, schema v68) — and round-trips losslessly through archive → restore.This protects the most sensitive column (the memory body) at the application layer, independent of disk/SQLCipher encryption, and keys each row to the NHI that authored it. It does not encrypt title/tags/metadata, and the per-agent X25519 secret lives on disk under the key directory — pair with filesystem permissions and process hardening. SQLCipher (whole-file) and the #228 envelope (per-memory content) compose: use both for defense in depth.
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.9eeb453 for v0.6.3 final.mTLS authenticates the peer node, not the memory author. Once a peer is allowlisted, every memory it pushes is trusted at the node level. v0.7.0 (#791/#922) adds envelope-level Ed25519 push signatures + nonce anti-replay (AI_MEMORY_FED_REQUIRE_SIG=1 / AI_MEMORY_FED_REQUIRE_NONCE=1 secure defaults), and #626 Layer-3 verifies caller-presented per-memory signatures on the store surfaces; v0.8.0 (#1464) adds receiver-side per-write content attestation across federation relay — a relayed memory carrying a valid metadata.write_signature over the canonical SignableWrite envelope is verified against the author's enrolled key and upgraded to agent_attested (opt-in strict mode via AI_MEMORY_FED_REQUIRE_WRITE_SIG=1).
secret_hash in the subscriptions table). Every dispatch is signed with the secret using HMAC-SHA256.{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.git tag -v v0.6.3 verifies. CI workflow refuses to publish releases from unsigned tags.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.v0.6.3 shipped a placeholder signature column on memory_links + a deferred observed_by column. v0.7.0 fills that contract.
SignableWrite envelope on every store surface; the daemon verifies it against the agent's bound public key and stamps attest_level = "agent_attested" (forged → 403). Links carry per-link signatures + attest_level + signed_at. As of v0.9.0 (#1751) store-path attestation is required by default: an unsigned direct CLI/MCP/HTTP write is rejected (403 ATTESTATION_FAILED) rather than landing claimed, unless the operator sets the explicit opt-out AI_MEMORY_REQUIRE_AGENT_ATTESTATION=0.signed_events audit chain with the V-4 cross-row hash chain — independent of the daemon log, queryable (memory_replay), and tamper-evident (each row chains the previous row's hash).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.