Security
Encryption model
Everything in zcrypt is encrypted on your device before it touches the network. This page documents exactly which algorithms run, in what order, and where each key lives, no hand-waving.
The primitives
zcrypt uses a small, deliberately boring set of well-understood primitives. There is no custom cipher and no novel construction: just standard authenticated encryption and a standard key-derivation function, run client-side via the Web Crypto API (in the browser) or native Rust (in the desktop and Android apps). The exact parameters below are shared by every client, so a file encrypted in one decrypts in any other.
| Purpose | Primitive | Parameters |
|---|---|---|
| Content encryption | AES-256-GCM | 256-bit key, 12-byte random nonce, 16-byte auth tag |
| Key derivation | PBKDF2-HMAC-SHA256 | 600,000 iterations, 32-byte random salt, 256-bit output |
| Integrity | SHA-256 | Per-chunk and whole-file digests |
| Sharing keys to other users (client-side) | X25519 + AES-256-GCM | ECIES sealed-box: ephemeral ECDH, SHA-256-derived wrap key |
| Token wrapping (server-side) | HKDF-SHA256 + AES-256-GCM | Per-user KEK derived from a server master key |
Why AES-GCM specifically
GCM is authenticated encryption: every ciphertext carries a 16-byte tag bound to the key and nonce. Flip a single bit of a stored chunk and decryption fails loudly rather than returning garbage. You get confidentiality and tamper-detection from one pass.The last row (token wrapping) is the one place zcrypt holds a key server-side, and it never touches your files. It is covered honestly in the zero-knowledge architecture.
Envelope encryption
Your passphrase does not encrypt your files directly. Instead, every file gets its own random 256-bit Content Encryption Key (CEK). The file's chunks are encrypted with that CEK, and the CEK itself is then wrapped (encrypted) with a Key Encryption Key (KEK) derived from your passphrase. This is called envelope encryption.
- •The CEK is generated fresh, at random, per file, never derived from anything you type.
- •The KEK is derived from your passphrase plus the file's salt and is used only to wrap and unwrap the CEK.
- •The server stores the wrapped CEK (a small base64 ciphertext) and the salt, but holds no key that can unwrap it.
This indirection is what makes secure sharing and per-folder passwords possible without ever exposing your passphrase: zcrypt can re-wrap a file's CEK under a different key: a link's one-time share key, a folder-password key, or a key sealed to another user's public key , so a recipient (or a different password) can decrypt that one file while your master passphrase never moves. The underlying chunks are untouched; only the small wrapped CEK changes. See passphrase & key management for how keys flow end to end.
Legacy files
A small number of files predate envelope encryption and were encrypted directly with the passphrase-derived key (no wrapped CEK). zcrypt detects this automatically on download, a file with no stored wrapped CEK uses the passphrase-derived key as the content key directly, so either path just works and you never have to think about it.Key derivation
Keys are derived with PBKDF2-HMAC-SHA256 at 600,000 iterations against a cryptographically random 32-byte salt. The high iteration count makes each guess expensive, so brute-forcing a strong passphrase is computationally impractical. The salt is unique per file, so identical passphrases never produce identical keys and a precomputed-table attack buys an adversary nothing.
salt = random(32 bytes) // unique per file
KEK = PBKDF2-HMAC-SHA256(passphrase, salt, // 600,000 iterations
iter = 600000,
dkLen = 32)
CEK = random(32 bytes) // unique per file
wrappedCEK = AES-256-GCM(key = KEK, plaintext = CEK)The salt is not secret. It is stored alongside the file's metadata. The passphrase is. It is never transmitted, never written to our database, and exists only in your device's memory for the duration of an unlocked session.
The upload pipeline
When you add a file, all of the heavy lifting happens locally: in a Web Worker in the browser, or on-device in the desktop and Android apps. The server only ever receives finished, encrypted chunks.
- Compress. Each chunk is compressed with zstd on your device, before encryption, unless that would not make it smaller. Encrypted data is effectively random and will not compress, so this order matters.
- Encrypt. Each compressed chunk is sealed with AES-256-GCM under the file's CEK, producing
[12B nonce][ciphertext][16B tag]. - Hash. A SHA-256 digest is computed per encrypted chunk for integrity. The whole-file digest is a keyed HMAC-SHA256, so the server holds no plain hash of your content.
- Store. Chunks are pushed to your configured storage backend as opaque binary blobs.
Compression is client-side only
The backend does not compress anything. It merely carries a per-chunkcompressed flag so the client knows whether to run zstd in reverse on the way back down. Older documentation implied server-side zstd. That was never how it worked.Chunks & integrity
Files are split into chunks so uploads can run in parallel, resume after interruption, and stay within storage-provider size limits. The plaintext chunk size is chosen for your device: 4 MiB on constrained hardware, 10 MiB for most computers, up to 16 MiB on the strongest machines, and each encrypted chunk adds a 12-byte nonce plus a 16-byte tag of overhead. Encrypted chunks therefore top out around 16 MiB.
- •Every chunk is independently authenticated by its GCM tag, so tampering with one chunk cannot silently corrupt the file.
- •Per-chunk and whole-file SHA-256 digests let the client verify that what it downloaded is exactly what it uploaded.
- •Because each chunk is encrypted on its own, large files stream through memory in bounded windows rather than being held whole.
- •On the way back down, decryption and zstd decompression run across a device-aware pool of Web Workers, off the main thread: the same client-side pipeline as upload, just in reverse. Plaintext never leaves your device.
On chunk sizes
A nonce and a tag are appended per chunk, not per file, so overhead is a flat ~28 bytes per chunk regardless of file size. The exact plaintext window is picked from your device's reported memory at upload time.Where to go next
Dig into the guarantees and the honest edges of this design.