How our end-to-end encryption works
Last updated: June 2026
In short
- Your data is encrypted on your device and only ever leaves it as unreadable ciphertext. Our server stores nothing but encrypted blobs and has no access to the plaintext.
- All keys are derived from a 128-bit recovery code that never leaves your device. We don't know it and cannot recover it.
- Every single record is encrypted with its own key and checked for authenticity before it is decrypted (verify-before-decrypt using Ed25519).
- E2EE does not protect everything: certain metadata (data volume, access times) remains visible, and a compromised device or a lost recovery code cannot be rescued by cryptography.
What "end-to-end" means here
End-to-end encryption means the plaintext of your data only ever exists on your own devices. Encryption and decryption happen exclusively on the client. Whatever is synchronized between devices and stored on our server is already encrypted before it leaves the device.
Concretely: a data row is canonically encoded to JSON on the client and then encrypted with an AEAD scheme. The server receives only an opaque byte blob plus a few management fields. It holds none of the keys needed to decrypt. This is an architectural property, not a promise: because the keys never reach the server, it technically cannot read the content.
Click on components for details:
Device A (Sender)
Data is encrypted locally on the device with AES-256-GCM. The recovery code and derived keys never leave your device.
How your data is encrypted
We use proven, standardized building blocks from the @noble/ciphers, @noble/hashes and @noble/curves libraries:
- AES-256-GCM encrypts your records. AES-256-GCM is an AEAD scheme that simultaneously ensures confidentiality (nobody can read along) and integrity (tampering is detected). Each record uses a random 12-byte nonce and a 16-byte authentication code (MAC). Format: nonce(12) ‖ ciphertext ‖ mac(16).
- XChaCha20-Poly1305 encrypts the key material during device pairing (key wrapping). It is also an AEAD scheme but uses a longer 24-byte nonce, which makes random nonces non-critical.
- AAD (Associated Data) binds each ciphertext to its metadata — header, bucket, record UUID, revision, key epoch, schema version and padded length. These fields are authenticated alongside the ciphertext: changing even one of them makes decryption fail. Empty AAD is rejected outright (confused-deputy protection).
- Padding obscures the exact size: each record is padded up to a bucket size (256, 1024, 4096, 16384 or 65536 bytes; above that, multiples of 65536). So the blob size only reveals a coarse class, not the exact amount of data.
- Ed25519 signature (blob_sig) signs every blob. Before any decryption, the client verifies this signature against the authorized device keys (verify-before-decrypt). The check runs RFC-8032-strict (zip215:false), which rules out forgeries via low-order points. Tampered data produces an immediate error, not a silent defect.
Your keys stay on your device
The root of everything is a 128-bit recovery entropy — the recovery code. From these 16 random bytes, HKDF-SHA256 (a key-derivation function that produces many domain-separated keys from one secret) deterministically derives a master secret, and from it further keys: an account ID, a key-encryption key, the auth key (authSeed) and the routing key. Each derivation uses its own versioned label (e.g. mypep/master/v1), so the keys never overlap.
Each collection gets its own collection key, and each record and revision gets its own record key (rec/<uuid>/<rev>). This per-(record, revision) key provides complete key separation: no AES-GCM key is ever used twice — a strong protection against nonce-reuse attacks.
The master secret is deliberately not stored. Only the derived values and the device-specific signing/wrap keys live locally — in the browser, in IndexedDB.
Click on a key for details:
Recovery Code
128-bit random entropy. The absolute root key. Generated during account creation and shown to the user. Never leaves your device.
Recovery — feature and responsibility: the entire key tree hangs on the recovery code. We do not store it; a back door, key escrow or server-side reset are architecturally excluded — not merely a matter of policy, but because the keys never reach the server in the first place. This is the essence of true E2EE: nobody but you — not even us — can recover your data without the recovery code or a paired device. If the code is lost and no paired device remains, the data becomes permanently inaccessible.
What our server sees — and what it doesn't
The server stores only encrypted blobs. It does not see: the content of your records, the names of your collections (they appear only as an opaque HMAC-SHA256 bucket), or your keys.
To be honest, it does see some metadata: the opaque bucket values, the record revisions, the coarse size class via padding, the synchronization frequency, device IDs (as a SHA-256 hash of the device keys), and the cursor position during sync. From this one can infer that an account is active and roughly how much or how large the data moved is, and when — but not what it is about.
Conflict resolution runs purely on revisions (higher revision wins, "last-write-wins"), because the server doesn't know the content and cannot merge it by meaning. Authentication uses a challenge-response with Ed25519 followed by a JWT token; the device list is signed with your auth key and verified exclusively on the client.
Multiple devices (pairing)
A new device, such as the browser, is paired with the phone via QR code. The QR transmits the browser's public device keys directly (out-of-band), not via the server — this protects the key exchange against man-in-the-middle attacks. The phone wraps the identity bundle (account data key, auth key, account ID, routing key) using X25519-ECDH and XChaCha20-Poly1305 exclusively for the browser and transmits it encrypted.
After decrypting, the browser displays a fingerprint (SHA-256 of the account ID, as hex groups). If it matches the display on the phone, you can be sure you received the correct bundle. Web clients have no recovery code of their own: if the browser storage is lost, you simply pair with the phone again.
Device Pairing Flow
The browser generates temporary device keys and shows them as a QR code. The smartphone scans it directly. Since this is done out-of-band via camera, the key exchange is immune to Man-in-the-Middle attacks.
What this does NOT protect
E2EE is strong, but not a cure-all. Honestly about the limits:
- Compromised device: malware in the browser context or direct access to IndexedDB can read the locally stored keys and decrypt everything. The local database is stored in plaintext.
- Lost recovery code: without the code and without a paired device, there is no way back. There is no key rotation and no backup mechanism.
- Metadata: data volumes, size classes, access patterns and timing remain visible to the server.
- Photo blobs: photos are content-addressed (
blob_id = sha256(blob)). Anyone who already holds a plaintext can test whether exactly that photo was uploaded. - Malicious server: it cannot read or forge the content (verify-before-decrypt protects against this), but it could withhold data or re-serve old signed device lists (replay). The damage of such a replay is limited: device registration is idempotent, so a replayed old list does not overwrite the current state. It cannot invent new valid data.
- Auth key: anyone who obtains the auth key can change the device list and re-authenticate. It must stay secret.
The cryptographic building blocks at a glance
- AES-256-GCM — record encryption (AEAD); 12-byte nonce, 16-byte MAC.
- XChaCha20-Poly1305 — key wrapping during pairing (AEAD); 24-byte nonce, 16-byte MAC.
- Argon2id v1.3 — password-based, memory-hard derivation (defaults: 64 MiB, 3 iterations, 4 lanes); available for future password-based features, but NOT used for the recovery code — that is already 128 bits of random entropy and needs no memory hardness.
- HKDF-SHA256 — derivation of all keys from the recovery code, domain-separated via versioned labels; 32-byte output.
- HMAC-SHA256 — opaque routing of collections (bucket computation).
- SHA-256 — hashing for fingerprints, device IDs and photo addressing.
- Ed25519 (RFC-8032-strict, zip215:false) — signatures for records, device list and authentication.
- X25519-ECDH — key exchange during device pairing.
- Padding buckets (256 B to 65536 B, above that multiples of 65536) — size obfuscation.
- Key length consistently 32 bytes (256 bit).