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
- Account password. Authenticates with the server. Hashed with scrypt (N=32768, r=8, p=1) before storage. The server checks it on login but never derives encryption keys from it.
- Encryption passphrase. Stays in your browser. We derive a 256-bit master key from it via PBKDF2-SHA256 with 600 000 iterations, salted by a random per-user 16-byte salt that's stored on the server (the salt is not a secret).
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:
- A content envelope: AES-256-GCM ciphertext of the file bytes, encrypted with a random per-object 256-bit Content Encryption Key (CEK). Hashed with SHA-256; this hash is the file's server-side address.
- A wrap: a 66-byte AES-256-GCM ciphertext of the CEK, encrypted with the master key. Stored separately, keyed by the content hash.
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 (scrypt), 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
- Read the contents of any file.
- List the names of your files.
- Read commit messages.
- Re-derive your master key — it never sees your encryption passphrase.
What the server CAN see (metadata)
- Your email and account creation time.
- The number, size, and timestamps of objects you store.
- The DAG of commits (parent links) — connected by hash, no payload.
- API key names and last-used timestamps.
Threat model — out of scope (v1)
- Compromised browser / malicious extensions: game over for any zero-knowledge web app.
- Server-side rate limiting and abuse protection: minimal in v1.
- Hardware-backed key storage / passkey unlocking: planned.
- Forward secrecy on rotation: the old master key remains usable for previously-rotated wraps unless explicitly purged.