IA Cloud Memory

Security model

IA Cloud Memory treats its own backend as untrusted storage. Every byte of user data — file contents, paths, manifests, commit messages — is encrypted in the browser before it ever reaches the server.

Two distinct secrets

We can recover or reset your account password (after the usual proof of email control). We cannot recover your encryption passphrase — losing it means losing your data.

How files are encrypted

Every file is stored as two separate ciphertexts:

Manifests (the path → content-hash mapping) and commits (parent links, messages) are themselves files: encrypted JSON, stored as ordinary objects.

Why two-layer wrapping?

It makes key rotation cheap. To switch master keys we only need to re-wrap the per-object CEKs (66 bytes each), not re-encrypt the content envelopes. Hashes don't change, manifests don't change, commits don't change. The full chain stays valid.

What the server stores

Object   ::= magic||v||flags||nonce||AES-GCM(content, CEK)
Wrap     ::= magic||v||flags||nonce||AES-GCM(CEK, masterKey)
Manifest ::= encrypted JSON { path → contentHash }
Commit   ::= encrypted JSON { parent, manifestHash, message }
Ref      ::= "main" → commitHash (CAS-protected via etag)
User     ::= { id, email, password_hash (argon2id+pepper), crypto_salt (public) }
Session  ::= { id (sha256(token)), user_id, expires_at }
APIKey   ::= { id, user_id, token_hash (sha256), name }

Per-user isolation is enforced by SQL. Every encrypted-data table has a user_id column, and all queries filter on it. There is no API path that can return another user's ciphertexts.

What the server CANNOT do

What the server CAN see (metadata)

Full-text search is client-side only

memory_search (the MCP tool) and its underlying @claude-memory/search package run entirely inside the local MCP server process. Chunking, the keyword index, and the cache that backs it all live in packages/search, packages/client, and packages/mcp-server — never in packages/backend. The backend has no search endpoint, no index, and no code path that could construct one; it continues to see only the same encrypted objects/wraps/refs/commits described above.

The search index itself is cached on disk, encrypted at rest with AES-256-GCM under a dedicated sub-key. That sub-key is derived from your master key via HKDF-SHA256 with a fixed, purpose-specific "info" string (domain separation) — so it is cryptographically distinct from the per-object Content Encryption Keys used for your actual memory files. Losing or corrupting this cache is a non-event: it is rebuilt automatically from the encrypted corpus on next use, the same way the rest of the client does after a fresh Repo.open().

This is keyword (full-text) search v1. Semantic/embedding-based search is intentionally out of scope for now and, when it ships, will follow the same rule: it runs client-side, or it doesn't ship.

The link graph (backlinks) is client-side only, too

Memory files can reference each other with [[wikilinks]]. memory_backlinks and memory_graph (the MCP tools) and the dashboard's "Referenced by" panel are all built on packages/search/src/links.ts and packages/search/src/graph-core.ts — link parsing and resolution over plaintext that only ever exists client-side, reusing the exact same corpus-decrypt pass as memory_search (readCorpus) rather than a second one. This is a design guardrail, not just an implementation detail: no link, backlink, or graph data is ever sent to the backend, and none should ever be added. The MCP server's cache (GraphService) mirrors the search index's: an encrypted on-disk cache under ~/.ia-cloud-memory/graph-index/, its own HKDF sub-key (domain-separated from both the search cache's and your content keys), disposable and rebuilt from the encrypted corpus on any read failure. The browser dashboard, by contrast, uses the package's @claude-memory/search/browser entry point directly — stateless, no cache, no Node built-ins in its bundle — since the already-open Repo in your tab can answer a backlinks query on demand.

Identity is fully local, by design

Signup, login, sessions, password reset and account deletion are all handled by IA Cloud Memory itself — there is no external identity provider in the loop. This is a deliberate departure from the ILYGO-wide convention of delegating human identity to the shared Keycloak SSO (auth.ilygo.ch): that SSO is a single point of failure for every ILYGO app that depends on it (it went down for 9 hours on 2026-07-15), and this product's job — a working memory for an AI agent — should not inherit that risk. See packages/backend/src/auth.ts for the full rationale.

Threat model — out of scope (v1)