SafePass threat model
Scope
This model covers client-side encryption, local caches, account authentication and recovery, sync API, browser extension, optional Native Messaging and deployment. It assumes an attacker may control the network or obtain a copy of server storage. It does not claim protection from a compromised unlocked endpoint.
Assets
- Master password and OPAQUE client state.
- Vault key and plaintext login records, notes and generated passwords.
- Recovery phrase, recovery signing key and recovery-wrapped vault key.
- Refresh tokens, access sessions and device session state.
- Encrypted revision envelopes, branch relationships and local pending operations.
- OPAQUE server setup keys and service signing/deployment secrets.
- Encrypted PostgreSQL backups and the separate age identity required to restore them.
Trust boundaries
- The Flutter and browser processes are trusted while locked and become sensitive execution environments while unlocked.
crypto_coreowns ciphertext formats and key derivation. Its C ABI and platform linkage are part of the trusted computing base.- The API and PostgreSQL are not trusted with vault plaintext or vault keys. The API enforces OPAQUE login, signed recovery-key proofs and JWT authorization. Refresh secrets are stored as hashes in PostgreSQL and as ciphertext inside the SQLCipher client database.
- Browser content scripts are untrusted page-adjacent code. They receive only the fields necessary for an explicit user-approved fill operation.
- Native Messaging is an optional local boundary. A valid browser origin, bounded message size, allowlisted commands and a one-time handshake are required.
- TLS, operating-system secure storage and platform code signing are required deployment assumptions.
Threats and controls
| Threat | Control | Remaining exposure |
|---|---|---|
| Network interception or modification | TLS; client-side AEAD; authenticated OPAQUE transcript | Traffic timing, account identifier and ciphertext size remain visible |
| Server or database disclosure | Client-encrypted record revisions; separate recovery envelope; server has no vault key | Account metadata, ciphertext relationships, and offline password-guessing material may be exposed |
| Backup media disclosure | PostgreSQL dumps are age-encrypted to an offline recipient; backup directories are permission-restricted | The backup recipient's private identity and deployment secret bundle must remain available for recovery and protected separately |
| Offline guessing of master password | OPAQUE password file; Argon2id-wrapped vault key with versioned parameters | A weak master password can be guessed against the password envelope; Argon2id raises cost but cannot eliminate it |
| Ciphertext, record ID or revision substitution | AES-GCM tag and AAD bind record ID/revision; immutable revisions | A malicious server can withhold or replay an old valid revision; clients need rollback detection policy |
| Recovery phrase theft | Phrase is shown with confirmation; the envelope is client-encrypted; the API stores only a public key and ciphertext | Anyone with the phrase can reset the server password; with the matching envelope they can also unwrap the vault key |
| Recovery envelope replacement | Enrollment proves possession of the new phrase-derived key; later changes require a signature from the registered key and bind the envelope hash | A malicious API can delete or roll back ciphertext and deny access; clients have no remote rollback witness |
| Stolen access JWT during recovery-key rotation | Once a recovery key is enrolled, changing it requires the previous phrase-derived signing key | First enrollment uses an authenticated API session; protect the short-lived access token and enroll promptly |
| Stolen refresh token or replay after rotation | 256-bit random token, hash-at-rest, one-time rotation and family revocation on replay; client storage is SQLCipher protected; access JWTs name their session (sid) and are rejected as soon as it is revoked |
A stolen current token can be used until rotated, revoked or expired; legacy access JWTs without sid remain valid for up to 15 minutes |
| Stolen access token used to delete the account or sign out other devices | Account deletion needs a fresh single-use OPAQUE re-authentication ticket; per-session revocation only reaches the caller's own sessions and answers 404 otherwise | A stolen access token can list the account's session timestamps and revoke its other sessions until it expires |
| Cross-origin calls from a malicious site | CORS is off by default; SAFEPASS_WEB_ORIGINS allows exact HTTPS origins without credentials; tokens are never sent as cookies |
Code served from an allowed website origin is trusted to handle the account password and tokens; the website account portal cannot pin the server key and never receives the vault key |
| Extension compromise or malicious page | Explicit fill action; exact origin matching; no whole-vault content-script access; strict CSP | Browser/OS compromise and malicious extension updates can expose unlocked records |
| Stolen device while locked | SQLCipher cache, OS-backed database key, lock-on-idle and no plaintext database columns | Memory dumps, keyloggers, accessibility abuse and a compromised OS are out of scope |
| Native host spoofing or local port abuse | Native Messaging only; manifest origin allowlist; bounded framed JSON; command allowlist | An attacker with local user privileges may replace binaries or manifests |
| Logs, crash dumps or telemetry leak | Structured redaction; no payload/password logging; no secret-bearing exception text | Platform crash reporters and third-party diagnostics need release-time audit |
Required implementation invariants
- No plaintext record payload or master password is sent to the API.
- The API never receives the vault key or recovery phrase.
- PostgreSQL stores only refresh-token hashes. Clients persist refresh tokens only inside encrypted local storage.
- Every record revision uses a fresh salt and nonce and is encrypted once under a derived key.
- A sync conflict preserves both immutable branches until the user chooses a resolution.
- Local recovery replaces the device's password envelope and recovery envelope without changing record ciphertext.
- Server recovery replaces the OPAQUE credential, consumes outstanding challenges and revokes all refresh sessions in one PostgreSQL transaction. Access JWTs with
sidstop working immediately. Legacy JWTs withoutsidremain valid until expiry. - The browser extension must be able to unlock its own encrypted local cache and call the API without a running desktop process.
- SQLCipher is mandatory for the local database. Startup must fail closed when the cipher provider or key is unavailable.
- Production API secrets are mounted as files; database administrator credentials are not used by the API runtime role.
- PostgreSQL backups are encrypted before storage, and restore verification uses a temporary database without replacing production data.
Out of scope and release gates
Protection against a compromised unlocked device, coercion, shoulder surfing, clipboard capture, malicious OS accessibility tools and availability attacks is out of scope. Before production use, the OPAQUE suite and KSF parameters require device benchmarks, all native crypto bindings require cross-platform review, recovery and distributed one-time challenge logic require independent review, and the complete product needs an external security audit and dependency/SBOM review.