Skip to content
SafePass

SafePass account recovery v1

Account recovery changes the server OPAQUE password credential. It does not decrypt or rewrite vault records. The 24-word recovery phrase remains on the client and has two separate roles: it derives the Ed25519 key registered with the API, and it unwraps the account-bound SPRE vault-key envelope locally.

Enrolling and rotating the recovery key

An authenticated client requests POST /v1/account/recovery-envelope/challenge. The API returns a random 32-byte challenge ID, a random 32-byte nonce, and the currently registered public key, if one exists. The challenge expires after five minutes and is single-use.

The client derives the new Ed25519 public key from the recovery phrase, then signs this byte sequence:

"SafePass account recovery envelope v1\0"
|| account_id_ascii[36]
|| challenge_id[32]
|| challenge_nonce[32]
|| new_recovery_public_key[32]
|| SHA-256(recovery_envelope[81])

The signature uses the phrase-derived Ed25519 key. On initial enrollment, the new key signs the request, proving possession of the phrase. On key rotation, the previously registered key signs the new key and envelope, so a stolen access token cannot replace an established recovery key. PUT /v1/account/recovery-envelope verifies the proof and atomically stores the ciphertext and public key while consuming the challenge. The API never receives the phrase or vault key.

Resetting the OPAQUE password

The client starts a fresh OPAQUE registration for the existing account and sends its registration request to POST /v1/auth/recovery/start. The API returns the OPAQUE registration response, the registered recovery public key, and a fresh, five-minute, single-use challenge. Unknown accounts and accounts without an enrolled recovery key get the same response shape with a random decoy public key. Recovery start and finish share the anonymous-auth rate limit of 20 requests per IP per minute.

The client derives the recovery public key from the phrase and compares it to the response before continuing. It then creates the OPAQUE registration upload for the requested account and signs:

"SafePass account recovery reset v1\0"
|| account_id_ascii[36]
|| challenge_id[32]
|| challenge_nonce[32]
|| SHA-256(opaque_registration_upload)

crypto_core applies its recovery-signature domain separator around this payload. Including the upload hash binds the proof to the exact replacement credential and prevents a captured signature from authorizing another password registration.

POST /v1/auth/recovery/finish verifies the signature with the registered public key, derives the server-side OPAQUE password file, then uses one PostgreSQL transaction to replace that file, consume all outstanding recovery challenges, and revoke every refresh-token family for the account. Only after the transaction commits does the API issue the recovering device a new access and refresh token pair. Invalid proofs, expired challenges, unknown accounts, and replays return the same generic recovery failure.

Recovery immediately invalidates access JWTs with sid for the revoked sessions. Legacy JWTs without sid can remain usable until their 15-minute lifetime ends. Recovery does not reset an existing local device master password. The user recovers the vault key from the SPRE envelope on the client, then chooses a new local master password as part of the local restore flow. On an already linked device, the reset leaves the open local vault and records intact. On a new empty device, the client fetches the envelope after reset, verifies and unwraps it with the account phrase, wraps the shared key under that device's chosen local master password, then syncs the encrypted records. A non-empty unlinked local vault cannot be overwritten by this flow.

Persistent data and compatibility

Migration 006_account_recovery_challenges.sql adds a 32-byte public-key column and a table containing challenge IDs, public nonces, purpose, expiry, single-use state, and a snapshot of the key that must verify the request. A challenge row is removed after expiry cleanup. These values contain no password, mnemonic, private key, or vault plaintext.

Recovery envelopes written before v1 may have no registered public key. Such accounts must enroll the key from an authenticated device before password recovery is available. The existing encrypted envelope remains unreadable to the API.

Documentation