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.