How encryption works
Everything you save in SyncBins is encrypted on your device before it ever touches the server. The server stores scrambled bytes — it can’t read your passwords, notes, or files. Even if the server were compromised, your data stays private.
This page explains how that works, step by step. You don’t need to read it to use SyncBins — but if you’re the kind of person who wants to know what’s under the hood, this is for you.
Primitives
Section titled “Primitives”SyncBins only uses well-vetted, NaCl-family primitives — there’s no custom crypto. Every primitive in the table below ships in libsodium and is exposed through libsodium-wrappers-sumo on the web client and server.
| Use | Primitive | Library call |
|---|---|---|
| Symmetric encryption (items, blobs) | XChaCha20-Poly1305 | crypto_aead_xchacha20poly1305_ietf_* |
| Key wrapping (K_item → device) | Curve25519 sealed box | crypto_box_seal / _open |
| Master-key derivation (recovery phrase) | Argon2id (t=3, m=64 MiB) | crypto_pwhash |
| Passphrase → KEK (wraps master key) | Argon2id (t=3, m=256 MiB) | crypto_pwhash |
| PIN key stretching | Argon2id (t=4, m=64 MiB) | crypto_pwhash |
| Recovery phrase | BIP39 128-bit entropy | @scure/bip39 |
| Pairing shared secret | BLAKE2b | crypto_generichash |
| Pairing attestation | HMAC-SHA512-256 | crypto_auth / _verify |
Item authentication (sig) | HMAC-SHA512-256 (MK-derived key) | crypto_auth / _verify |
| Device reauth challenge | Curve25519 sealed box | crypto_box_seal / _open |
| Session tokens | SHA-256 (server-side hash) | Node crypto.createHash |
| Blob signed URLs | HMAC-SHA256 | Node crypto.createHmac |
From 12 words to a master key
Section titled “From 12 words to a master key”During onboarding we generate 128 bits of entropy and encode it as a BIP39 12-word phrase. The phrase is the only thing you have to back up.
// 12-word phrase → master key — never sent to serverconst seed = bip39ToSeed(phrase); // 64 bytes (PBKDF2-HMAC-SHA512)const salt = blake2b("SyncBins/v1/master-salt", 16); // fixed 16-byte saltconst master = await argon2id(seed, salt, { t: 3, m: 64 * 1024, p: 1, len: 32, // 64 MiB, ~700 ms on laptop});The master key is used for device key wrapping and recovery only — it does not directly encrypt items. Each device generates its own Curve25519 keypair on first run; the device’s secret key is wrapped (encrypted) to the master key using XChaCha20-Poly1305 and stored in the browser’s IndexedDB.
The item envelope
Section titled “The item envelope”When you send an item, four things happen on the client. The server never sees the plaintext or K_item — only the ciphertext and the wrapped-key bundle.
// Per-item encryption (TypeScript-ish pseudocode)const K_item = randomBytes(32);const nonce = randomBytes(24);const aad = utf8(`${type}|${ts}`); // binId is bound by sig, not AADconst ct = xchacha20poly1305.encrypt(K_item, nonce, plaintext, aad);const wrapped = devices.map(d => sodium.crypto_box_seal(K_item, d.pubkey));const master = secretbox(K_item, masterKey); // any device with MK can decrypt// Authentication tag — keyed by a master-key-derived subkey the server never hasconst K_mac = blake2b(masterKey, "syncbins-item-mac-v1", 32);const sig = crypto_auth(K_mac, canonical(id, bin_id, type, ts, ct, nonce));postItem({ id, bin_id, type, ts, nonce, ct, wrapped, master, sig });Item authentication
Section titled “Item authentication”The sig tag is what stops a malicious server from forging or relabelling
items. It’s an HMAC over the item’s id, bin, type, timestamp, and ciphertext,
keyed by a subkey derived from the master key — which the server never holds, so
it can’t produce a valid tag. Recipients verify sig before decrypting and
reject anything that fails, so the server can neither inject content that looks
like it came from one of your devices nor silently move an item to another bin or
change its id/type/timestamp.
Because sig binds binId, moving an item between bins re-signs it (the moving
device holds the master key). For items with a file attachment, sig also binds
the blob reference and wrapped blob keys, so the attachment can’t be swapped.
Big items: blobs
Section titled “Big items: blobs”Items larger than 64 KB (screenshots, audio, files) take a different path: the body is encrypted locally just like above, then the ciphertext is uploaded directly to your storage backend via a short-lived SAS URL. Only the metadata + a blob_ref go through the JSON API.
What the server can actually see
Section titled “What the server can actually see”This is the part worth dwelling on. Three columns: what’s fully readable to the server, what’s structural metadata, and what’s fully opaque.
- Item ID (ULID)
- Bin ID
- Type (
password,link, …) - Timestamp
- Sender device ID
- Ciphertext size
- Bin emoji + hue + capacity
- Device name + glyph
- Sync version counter
- Pubkey of each device
- Storage backend URL
- Item payload (any field)
- Bin name
- Master key / recovery phrase
- Device private keys
- Item search index
- Your PIN
Pairing a new device
Section titled “Pairing a new device”The 6-word pair code you see in the UI is a hash of an ephemeral public key. The full handshake is:
The attestation in step 3 proves the new device knows the shared secret — derived from BLAKE2b(ephemeralPubkey || code) — meaning it physically read the 6 words (or scanned the QR) from the existing device, not just intercepted them on the wire.
Before the master key is sealed, both devices display a 6-digit verification code = BLAKE2b(ephemeralPubkey || newDevicePubkey) mod 1e6, and the user must approve on the existing device. Because the code is bound to the actual joining public key, an attacker who photographed the QR (and thus knows the code) produces a different verification code — the mismatch is visible and the user rejects. Only on approval does the existing device crypto_box_seal the master key to the new device’s pubkey and pair-wrap it.
Returning to your device
Section titled “Returning to your device”When you come back after closing the browser, your session token may have expired (tokens last 30 days). SyncBins handles this without making you pair again:
- PIN unlock — your PIN decrypts the master key from localStorage, which in turn unlocks your device’s Curve25519 keypair from IndexedDB.
- Token refresh — if the bearer token is still valid, it’s refreshed automatically.
- Challenge-response reauth — if the token has expired, the client proves it still holds the device’s secret key. The server generates a random challenge, seals it with
crypto_box_sealto your device’s public key, and the client opens it with the secret key. This proves key possession without sending any secret over the wire — and the server issues a fresh token.
If even the reauth fails (e.g. the device was revoked), you’ll be guided to re-pair from another active device.
Threat model summary
Section titled “Threat model summary”| Adversary | Can read items? | Notes |
|---|---|---|
| Network observer (TLS-on-the-wire) | No | HTTPS + items already encrypted at the app layer. |
| Compromised SyncBins host | No | Sees ciphertext + metadata only. Cannot forge items, relabel them, or move them between bins — the master-key-derived sig (which it can’t compute) is verified on every item. |
| Stolen unlocked device | Yes | Set a PIN; revoke the device remotely from another paired device. |
| Recovery phrase + stolen device | Yes | Full access. Keep the phrase offline; rotate after exposure. |
| Malicious paired device | Yes | It’s paired — it has the master key. Revoke + rotate to evict. |
| Quantum adversary, future | No (for now) | Curve25519 is breakable post-Q. Re-encrypt rolling out with PQ KEM in 2027. |
Key rotation & revocation
Section titled “Key rotation & revocation”Revoking a device is a metadata flag. The device’s pubkey is marked revoked; new items skip it in the wrapped-key bundle; the server refuses its bearer token. Existing items the revoked device downloaded before revocation are obviously still on that device — revocation prevents future access, not past.
For full rotation (e.g. you think the phrase leaked), generate a new phrase from the Encryption settings pane. Every item is re-wrapped to fresh device keys; old wrapped-key bundles are dropped. This is a background job — expect a few minutes on a large bin.