Command Palette
Search for a command to run...

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)

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 holds K2 and the wrapped keypair blob; factor1 lives in the IdP (or is derived from the user's passphrase) and never reaches the server. A stolen server DB is K2 + ciphertext, insufficient to decrypt without factor1.
  • "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.