Encryption & Security
SyncBins is end-to-end encrypted. The server stores only ciphertext and can never read your content — not even bin names. This page describes the full cryptographic model.
Primitives
Section titled “Primitives”All cryptography uses libsodium (NaCl). No custom crypto, no home-grown algorithms.
| Primitive | Algorithm | Purpose |
|---|---|---|
| Symmetric encryption | XChaCha20-Poly1305 (crypto_aead_xchacha20poly1305_ietf) | Encrypting item payloads |
| Asymmetric key exchange | Curve25519 + XSalsa20-Poly1305 (crypto_box_seal) | Wrapping item keys per device |
| Key derivation | Argon2id (crypto_pwhash) | Master key from recovery phrase; PIN/passphrase wrapping |
| Pair attestation | crypto_auth (HMAC-SHA-512-256) | Proving the new device read the pair code |
| Item authentication | crypto_auth (HMAC-SHA-512-256), master-key-derived key | The per-item sig tag (forgery/relabel resistance) |
Master key
Section titled “Master key”The master key is a 32-byte secret derived from your 12-word recovery phrase:
phrase → BIP39 seed → Argon2id(seed, fixed-salt, t=3, m=64MB, p=1) → 32-byte master keyThe master key never leaves the device. It is used for:
- Device pairing — wrapping each device’s private key for recovery.
- Recovery — bootstrapping a new device when no existing paired device is available.
It is not used directly to encrypt items (see per-item keys below).
Per-device keypairs
Section titled “Per-device keypairs”Each device generates a Curve25519 keypair on first setup:
- The public key is registered with the server and visible to all other paired devices.
- The private key is encrypted to the master key and stored locally. It never leaves the device in plaintext.
Item encryption
Section titled “Item encryption”Every item is encrypted with its own random key:
- Generate a random 32-byte item key
K_itemand a random 24-byte nonce. - Encrypt the plaintext payload with XChaCha20-Poly1305:
AAD (additional authenticated data) =ciphertext = AEAD_Encrypt(K_item, nonce, plaintext, AAD)
type || ts.binIdis deliberately not in the AAD (so an item can move bins without re-encrypting its body) — instead it is covered by the authentication tag below. - Wrap
K_itemfor each non-revoked device usingcrypto_box_seal, and also to the master key (secretbox) so any device holding MK can decrypt:wrappedKey[deviceId] = crypto_box_seal(K_item, device.pubkey)master = secretbox(K_item, masterKey) - Authenticate the item with a MAC the server cannot compute:
K_mac = BLAKE2b(masterKey, "syncbins-item-mac-v1", 32)sig = crypto_auth(K_mac, canonical(id, binId, type, ts, ciphertext, nonce[, blobRef, blobNonce, blobKeys]))
- Send
{ ciphertext, nonce, wrappedKeys, master, sig }to the server aspayloadEnc.
The server stores everything opaquely; it has no private keys and no master key, so it can neither unwrap K_item nor forge a valid sig.
Decryption
Section titled “Decryption”On the receiving device:
- Verify
sigwithK_macover the same canonical fields. Reject the item on mismatch — this is how a forged or relabelled item (a malicious server moving it between bins, or changing its id/type/timestamp, or swapping its attachment) is caught. - Recover
K_itemvia themasterwrap (secretbox_openwith MK) or this device’s entry inwrappedKeys(crypto_box_seal_open). - Decrypt:
plaintext = AEAD_Decrypt(K_item, nonce, ciphertext, AAD).
Because sig binds binId, moving an item between bins recomputes it on the moving device (which holds the master key); a server cannot move items itself.
Large items (blobs)
Section titled “Large items (blobs)”For payloads larger than 64 KB (images, files, audio, video):
- Encrypt the bytes locally with the same XChaCha20-Poly1305 scheme.
- Request a short-lived upload URL from
POST /api/blobs/upload-url. - Upload the encrypted ciphertext directly to blob storage (Azure Blob / S3 / R2).
- Write the item to the server with
blobRef,blobNonce, andblobKeysinstead ofpayloadEnc.
The server and storage provider see only ciphertext blobs. Blob names are opaque ULIDs — {itemId}.bin — with no PII in the path.
Device pairing handshake
Section titled “Device pairing handshake”The 6-word pair code is a secure out-of-band channel:
- Existing device generates an ephemeral X25519 keypair
(epk, esk)and derives 6 words fromBIP39(SHA256(epk)[0:4]). Registersepkwith the server under the code. - New device resolves the code to
epk. Generates its own keypair(npk, nsk). Computes:SendssharedSecret = X25519(nsk, epk)attestation = HMAC-SHA256(sharedSecret, npk || epk){ code, npk, name, glyph, attestation }to the server. - Both devices display a 6-digit verification code =
BLAKE2b(epk || npk) mod 1e6. The user confirms they match and approves on the existing device. Because the code is bound to the actual joining public key, an attacker who only photographed the QR (and so knows the pair code) produces a different code and is rejected. - Existing device (after approval) verifies the attestation and wraps the master key to
npk:Sends the wrapped key back through the server to the new device.wrappedMasterKey = crypto_box_seal(masterKey, npk) - New device unseals:
masterKey = crypto_box_seal_open(wrappedMasterKey, npk, nsk).
The server never sees esk, nsk, or the master key. The 6 words are valid for 5 minutes.
What the server can see
Section titled “What the server can see”| Data | Visible to server? |
|---|---|
| Item ciphertext | Yes (but unreadable) |
Item type (link, password, etc.) | Yes — stored in the clear |
| Timestamp | Yes — stored in the clear |
| Bin emoji, hue, capacity | Yes — stored in the clear for fast rendering |
| Bin name | No — encrypted |
| Item payload | No — encrypted |
| Device public keys | Yes — necessary for key wrapping |
| Master key | No — never sent to the server |
| Recovery phrase | No — never leaves the device |
Leaking type and timestamp is an intentional trade-off: hiding them would make the sync protocol significantly more complex, and for a single-user app the risk model is acceptable.
Recovery
Section titled “Recovery”If all your devices are lost but you have your recovery phrase:
- Install SyncBins on a new device.
- Choose “I have a recovery phrase” during onboarding.
- Enter the 12 words. The master key is re-derived on-device.
- Your device private key is regenerated. A new pairing is completed against the server (using the server password as the second factor, if configured).
Because every item’s K_item is wrapped to each device’s public key individually, recovering the master key alone does not decrypt historical items. However, from the recovered device you can:
- Write new items (fully encrypted to your new keypair)
- Re-download and re-wrap historical items on any future paired device
Key rotation
Section titled “Key rotation”There is currently no automated key rotation. Manual re-encryption of all items with a new keypair is planned as a future feature.
Threat model
Section titled “Threat model”SyncBins protects against:
- Compromised server — the server database contains only ciphertext. It also cannot forge or relabel items: every item carries a master-key-derived
sigit can’t compute, verified on receipt. - Compromised storage — blobs are ciphertext blobs with opaque names; the item
sigbinds the blob reference and keys, so a swapped blob is rejected. - Network interception — all traffic is TLS; payloads are additionally E2E encrypted.
- Revoked device — revocation invalidates the session token; the revoked device cannot pull new items.
SyncBins does not protect against:
- Compromised device — if an attacker has shell access to your device, they can read the decrypted items in memory or on disk.
- Lost recovery phrase — there is no server-side recovery path.
- Metadata analysis — item type, timestamps, and bin metadata (emoji/hue/capacity) are stored in the clear.