Threat model
The design target is simple to state: a full compromise of the server must not yield anyone's plaintext, and the server must hold no secret that lets it impersonate a user across devices.
What the server stores
- Per-(version, device) ciphertext of every entry.
- Device public keys (signing and encryption).
- Device attestations (opaque signed blobs the server can't forge).
- A server-only symmetric key for minting session tokens.
- Email, display name, and non-secret metadata (timestamps, ULIDs, content hashes).
What the server never has
- Any entry plaintext.
- Any device private key, master password, or seed.
- Your unlock password (it never leaves the device).
- The ability to read a vault, reset a login, or add a device you didn't authorize.
What a server compromise yields
An attacker who fully owns the server gets the ciphertext-and-public-keys database. They can:
- see that you have entries, how many, when they changed, and who they're shared with (metadata);
- deny service, or serve stale/tampered data, which clients detect, because every version is signed and verified against the attestation-rooted trusted-device set;
- attempt to inject a rogue device, which fails, because a device only enters your trusted set via a parent attestation the server can't forge.
They cannot decrypt your secrets, the keys that unwrap them never leave your devices, and they cannot forge a trusted device's signature, so they can neither inject a rogue device nor issue a signed command (delete, rename, share) in your name. A fully compromised server can mint its own session tokens, but those only ever reach the same ciphertext-and-public-keys it already holds.
Trust boundaries
- Server, untrusted for confidentiality and for device trust; trusted only to store and serve opaque blobs and to enforce coarse authorization (you can only fetch your own rows).
- Device, trusted. A device holds private keys; full control of an unlocked device is full control of that device's access. At rest, keys are wrapped under the unlock password.
- The human moving a pairing code, trusted. Pairing relies on you transferring public keys between your own devices; that's the channel, and there's no server-issued code to phish.
Known limits (by design)
- Recovery is your responsibility, not the server's. The server can't help, it has no keys. You recover from any one of: another paired device, your downloaded backup file
- passphrase, or operator backup. The vault is unrecoverable only if you lose all of those at once.
- The operator backup key, if configured, can decrypt opted-in vaults. It's an explicit, offline-held recovery factor that shifts the threat model for users who opt in. See Backup & recovery.
- Same-origin script execution (XSS) on a frontend is game over for that session, as it is for any in-browser crypto app, the keys are reachable to code running as the app. CSP and XSS hygiene are load-bearing controls.
- A revoked device keeps whatever it already cached. Revocation is immediate going forward, the interceptor's per-request device-active check rejects the revoked device's next request, reads included, but it cannot retract ciphertext the device already decrypted and cached. Rotate any secret a lost device saw. See Authentication & sessions.
- Metadata is not hidden. Entry counts, timestamps, sharing graph, and email are visible to the server.
Enterprise IDP mode (opt-in)
A deployment can run in IDP mode, where users onboard through your OIDC provider instead of a per-device password. This relaxes one clause of the invariant above, "the server holds no cross-device secret", and binds the relaxation:
- The vault key is
HKDF(factor1 ‖ K2). The server holdsK2and the wrapped keypair blob;factor1lives in the IdP (or is derived from the user's passphrase) and never reaches the server. A stolen server DB isK2+ ciphertext, insufficient to decrypt withoutfactor1. - "The server never sees plaintext" is preserved, that clause is not relaxed. The backend still only ever holds opaque blobs.
- The two halves are concatenated as independent HKDF input (not XORed), so neither the server nor the IdP can steer the derived key alone, and a distinct KDF domain tag separates IDP-wrapped blobs from password-wrapped ones.
What changes for the worse:
- Revocation becomes identity-level, via IdP deprovisioning + identity- device revoke, there is no per-physical-device cryptographic revocation (one escrowed keypair per user).
- With the generic/passphrase provider, a weak passphrase plus a stolen server DB is offline-brute-forceable (Argon2-protected), the same exposure as a standalone unlock password. Attribute providers (Entra, Authentik) avoid this by storing a high-entropy key in the IdP.
The mode is deployment-wide and immutable, so this trade-off is a deliberate operator decision made once, not something individual users mix into a standalone deployment.
The canonical, longer-form threat model lives in the repository's password-manager-design.md; this page is the operator/user-facing summary.