Skip to content
SafePass

SafePass OPAQUE profile v1

SafePass uses the RFC 9807 OPAQUE implementation in opaque-ke 4.0.1. The suite is fixed for this profile:

  • OPRF: Ristretto255.
  • Key exchange: TripleDH over Ristretto255 with SHA-512.
  • Password KSF: Argon2id v1.3, 65,536 KiB, 3 passes, 4 lanes.
  • OPAQUE context: SafePass authenticated API login v1.
  • Server identifier: SafePass OPAQUE server v1.
  • Account identifier: 1–128 ASCII letters, digits, @, ., _, or -.
  • Password input: 1–4096 bytes. Clients should send the same UTF-8 byte encoding on registration and login; normalization is not performed here.

The C ABI functions in packages/crypto_core/include/safepass_crypto_core.h wrap the library's serialized OPAQUE types. The client keeps its serialized registration or login state only while that exchange is active. The server setup contains long-lived private OPRF and key-exchange material; generate it once and store it in a secret manager or an equivalently protected file. The OPAQUE password file is per-account verifier material, not plaintext password or vault key. Login server state is short-lived authentication material and must be encrypted with a key separate from the PostgreSQL data key if it is persisted between requests.

The client records the server public key returned at registration and checks that the login result matches that pin before sending the finalization message. The server uses the same generic first response for an absent account through opaque-ke's dummy credential path; registration policy and account existence rate limits still belong to the API layer.

The versioned API now exposes registration and login routes. Registration start assigns a random UUIDv4 account ID; the client must save this ID because there is no email identity or account-name lookup. Registration finish stores the OPAQUE password file. Login start gives the server a 3-minute state, which is wrapped in an SPCR AES-GCM envelope bound to the random session ID and account ID, using a separate 256-bit server key, before being persisted in PostgreSQL; login finish atomically consumes it and issues a 15-minute access JWT only after the OPAQUE proof verifies. Unknown accounts use the dummy credential path and receive the same generic login-failure response. The API limits authentication routes to 20 requests per minute per source IP.

Messages in the HTTPS API use canonical unpadded Base64URL around the opaque-ke serialized values, with a 16 KiB decoded cap. They are carried only on /v1/auth/... routes over TLS, not on sync routes. Rust tests and a native C smoke test verify registration, successful login, server-key pinning, wrong-password and unknown-account failures, state serialization, and the dummy response size. The Flutter Dart FFI and HTTPS client perform registration and login, reject unpinned server keys before finalization, and retain the 15-minute access token only in memory. A random 256-bit refresh token is returned with it. PostgreSQL stores the token's SHA-256 hash, rotates it in one transaction and revokes the device-session family if a consumed token is replayed. Refresh tokens expire after 30 days and the family has a 90-day maximum lifetime. Logout revokes one family; an authenticated endpoint can revoke all families for the account. Access JWTs with sid stop working when their family is revoked. Legacy JWTs without sid remain valid until expiry. The account ID, API URL, public-key pin and refresh token are stored in the local SQLCipher database; the refresh token is cleared from memory on vault lock. .NET HTTP tests cover token issuance and rotation; PostgreSQL integration checks the stored hash and replay revocation.

Signed account recovery can reset the OPAQUE credential and revokes every refresh-token family. Its challenge transcript and limits are specified in account-recovery-v1.md. The web account portal uses TypeScript/WASM bindings for authentication. It does not pin the server key, as documented in the threat model. After a device family expires, the client requires a new OPAQUE login.

Changing a password from an authenticated session requires a new OPAQUE proof of the current password, then a short-lived single-use ticket for registration of the replacement credential. See password-change-v1.md for the endpoint sequence, ticket storage, and client key-rewrapping duties.

Secret lifetime

  • Never log passwords, OPAQUE messages, serialized protocol states, password files, server setup bytes, recovery material, or vault keys.
  • Keep client protocol state in memory and discard it after the exchange.
  • Keep the server setup outside the database. Provision and rotate it through a secret manager; changing it invalidates existing OPAQUE password files.
  • The API encrypts state before writing it to PostgreSQL, binds the envelope authentication data to the random session ID and account, expires it after three minutes, and consumes it in one database operation.
  • Issue the sync access token only after the final OPAQUE proof succeeds. A client that pins the server key must verify it before sending that proof.

Dependency

opaque-ke 4.0.1 is distributed under Apache-2.0 OR MIT. Its transitive cryptographic dependencies are recorded in Cargo.lock; the release SBOM and license scan must cover those dependencies as well.

Documentation