Skip to content
SafePass

SafePass encrypted formats, version 1

All integers in binary envelopes are big-endian. Parsers reject unknown versions, truncated headers, oversized payloads and invalid authentication tags.

Password key envelope (SPWK)

magic[4] = "SPWK" | version[1] = 1 | salt[16] | nonce[12] | ciphertext[32] | tag[16]

The master password is UTF-8 encoded. Argon2id v1.3 uses m=65536 KiB, t=3, p=4 and produces a 32-byte KEK. AES-256-GCM wraps a randomly generated 32-byte vault key. AAD is "SPWK" || version || salt. A new salt and nonce are generated for every password envelope.

Record revision envelope (SPCR)

magic[4] = "SPCR" | version[1] = 1 | hkdf_salt[16] | nonce[12] | ciphertext[n] | tag[16]

For each immutable revision, HKDF-SHA-256 derives a 32-byte encryption key from the vault key, the random hkdf_salt, and length-prefixed record/revision identifiers under the context SafePass record key v1. AES-256-GCM encrypts the serialized entry. AAD contains the format tag and length-prefixed record/revision identifiers. A revision is encrypted once; edits receive a new revision ID and fresh salt/nonce.

The maximum record plaintext is 1 MiB. This limit is enforced by the core before calling native crypto APIs.

Local cache

Flutter's SQLCipher database stores the same encrypted record envelopes used for sync, plus encrypted-at-rest local metadata and a durable pending-operation queue. The SQLCipher key is a random device-specific secret held in the platform secure-storage service. The browser uses IndexedDB for envelopes and sync metadata; it never persists a decrypted vault key.

Recovery format

The client generates 256 bits from the OS CSPRNG and encodes them as a 24-word BIP-39 English mnemonic with the standard 8-bit checksum. Phrase input is accepted only when all words and the checksum validate. The checked-in word list is from the Bitcoin BIPs repository and is covered by the BIP-39 MIT license.

HKDF-SHA-256 derives separate keys from the mnemonic entropy: an Ed25519 signing seed and a recovery wrapping key. crypto_core signs and verifies domain-separated challenges. The API binds enrollment or rotation proofs to the account ID, a random five-minute challenge and a hash of the exact recovery update or OPAQUE registration. The payload formats and transactional server behavior are specified in account recovery v1.

The recovery envelope is account-bound and uses a fresh 128-bit salt and 96-bit nonce:

magic[4] = "SPRE" | version[1] = 1 | salt[16] | nonce[12] | ciphertext[32] | tag[16]

HKDF info is "SafePass recovery wrapping key v1\0" || u16be(account_id_length) || account_id. AES-GCM AAD is "SPRE" || version || salt || u16be(account_id_length) || account_id. The API stores this 81-byte ciphertext with the recovery public key and never unwraps it. Another device fetches the ciphertext and unwraps the vault key locally. Recovery-key proof is required to change an enrolled recovery key, and a signed OPAQUE reset revokes all refresh sessions in the same database transaction.

Restoring an existing local vault unwraps its key, wraps it under a new local master password and creates a new local recovery phrase. For a linked account the user must upload a new account-bound envelope after confirming that phrase. The previous recovery phrase signs the key transition; it is kept in memory for that restore session, and can be re-entered if the app is restarted before the update.

The recovery phrase is a bearer secret: anyone who obtains both it and the matching envelope can decrypt the vault key. It must never be logged, synchronized as plaintext or saved automatically to cloud storage.

The deterministic cross-target Ed25519 recovery derivation and signature vector is stored in recovery-ed25519-v1.txt and checked by Rust, Dart, WASM/TypeScript and .NET tests. Its published BIP-39 phrase is intentionally compromised test data and must never be used for an account.

Documentation