PassQ Cryptographic Design Details
1. Scope
PassQ is designed as a local-first password manager with optional synchronization to a remote server. The cryptographic trust boundary is the client. The server stores encrypted blobs, public keys, device records, and authorization metadata. It does not derive wrapping keys, decrypt entries, perform entry re-encryption, or hold plaintext vault contents.
Current asymmetric primitives are post-quantum:
ML-KEM-768 for recipient encryption and ML-DSA-65 for signatures. Symmetric
protection uses ChaCha20-Poly1305. Passphrase hardening uses Argon2id.
Serialization relevant to authenticated encryption and signatures uses deterministic CBOR.
2. Cryptographic Objects
Each vault and device maintains the following cryptographic state.
- Master KEM keypair. Used to unwrap per-entry symmetric material for vault recipients.
- Master signing keypair. Used to sign entry blobs and other vault-authored cryptographic state.
- Recovery signing keypair. Used to authorize recovery-based device enrollment.
- Device signing keypair. Bound to a specific device and used to authenticate to the server.
- Device secret. A high-entropy local secret used together with the passphrase to derive the wrapped-key unlocking key.
- Wrapped master-key blob. The encrypted container holding master private key material and recovery private key material.
- Entry blobs. Per-password ciphertext objects containing recipient wraps, ciphertext, and an author signature.
3. Master Key Formation
The primary long-term secret is not the passphrase itself. The passphrase is used only to derive a symmetric wrapping key for the wrapped master-key blob.
input = passphrase || device_secret
wrap_key = Argon2id(input, salt, memory_kib=262144, iterations=3, parallelism=4)
wrapped_master_key = ChaCha20-Poly1305-Encrypt(
key = wrap_key,
nonce = wrapped_key_nonce,
aad = "PQPW" || version || blob_type || canonical(aead) || canonical(kdf) || salt,
pt = canonical_cbor({
master_kem_sk,
master_kem_pk,
master_sig_sk,
master_sig_pk,
recovery_signing_sk,
recovery_signing_pk,
vault_id
})
)
The wrapped master-key plaintext contains the vault’s private asymmetric material. Unlocking therefore means: derive the wrapping key locally, decrypt the wrapped blob locally, and reconstruct the master key material in memory locally. The server is not involved in this derivation step beyond serving the already-encrypted blob.
4. Entry Encryption and Decryption
Each password entry is encrypted under a fresh per-entry data-encryption key (DEK). The DEK is not reused across entries.
dek = random(32 bytes)
nonce = random(12 bytes)
for each recipient:
(kem_ct, shared) = ML-KEM-768-Encap(recipient.master_kem_pk)
wrap_key = HKDF-SHA256(shared, info="passq-entry-v1-recipient")
wrapped_dek = dek XOR wrap_key
recipient_wrap = kem_ct || wrapped_dek
aad = "PQPW" || version || entry_blob_type
|| canonical(recipients)
|| canonical(signer_fingerprint)
|| canonical(signer_sig_algorithm)
entry_ciphertext = ChaCha20-Poly1305-Encrypt(
key = dek,
nonce = nonce,
aad = aad,
pt = pad_to_1KiB_multiple(canonical_cbor(entry_plaintext))
)
Decryption reverses this process on the client. The client selects the recipient record for its own
user_id, decapsulates the ML-KEM-768 ciphertext using its master KEM secret key,
reconstructs the DEK, and then opens the entry ciphertext using the authenticated header and recipient data.
The server never sees the DEK, never derives it, and cannot transform an entry from one recipient set to another without client participation.
5. Signing and Authenticated Writes
After the entry ciphertext is formed, the client signs the entry blob using the master signing keypair. The signer fingerprint and signature algorithm are embedded in the blob and bound into the authenticated data for the ciphertext.
unsigned_entry_blob = encode_blob(entry without signature)
signature = ML-DSA-65-Sign(master_sig_sk, unsigned_entry_blob)
On read, the client verifies the signature using the trusted signer public key before accepting the entry as a valid shared or authored object. On write, the server can reject malformed or unauthorized updates, but it is not relied upon as the sole verifier of entry provenance.
Device-to-server authentication is separate from entry signing. The server issues a challenge, the device signs it with its device signing key, and the server returns a short-lived bearer token. This token authorizes upload, delete, and metadata operations but does not confer the ability to decrypt vault contents.
6. Recovery and New Devices
Recovery is designed so a user can rebuild access using:
server URL + user ID + master passphrase + recovery code.
The recovery code corresponds to the local device secret used in wrapped-key unwrapping. A new device fetches
the wrapped master-key blob and the published recovery signing public key, derives the local wrapping key from
passphrase || device_secret, unwraps the vault locally, and then proves recovery authorization by
signing the server’s recovery challenge with the recovery signing secret key recovered from the wrapped blob.
1. fetch wrapped_master_key, recovery_signing_pk
2. derive wrap_key from passphrase and recovery-derived device_secret
3. decrypt wrapped_master_key locally
4. sign server recovery challenge using recovery_signing_sk
5. register a new device signing key with the server
This model avoids making an already-enrolled device a mandatory online dependency for account recovery.
7. Passphrase Change and Rotation
Passphrase change and key rotation are separate operations.
- Passphrase change. Re-wrap the existing master private key material under a new Argon2id-derived wrapping key. Entry ciphertexts do not change.
- Master key rotation. Generate a new master KEM keypair, decrypt entries locally, and re-encrypt them to the new recipient key material. This is an actual vault-wide re-encryption event.
Both operations are client-side cryptographic transforms. The server’s role is to accept the resulting updated wrapped-key blob and entry blobs.
8. Server Role
The server is expected to handle the following classes of data and operations.
- Store wrapped master-key blobs.
- Store encrypted entry blobs and deletion state.
- Store published public keys and device records.
- Enforce request authentication and object-level authorization.
- Issue authentication and recovery challenges.
The server is not expected to do any of the following.
- Derive passphrase-based wrapping keys.
- Decrypt entry ciphertext.
- Rotate vault keys on behalf of the client.
- Translate recipient sets by re-encrypting entries server-side.
Under compromise, the server should expose encrypted blobs, public metadata, public keys, and authorization records, but not plaintext secrets or private asymmetric material.
9. Security Properties and Boundaries
- Client-side trust boundary. Sensitive operations are intended to terminate on the client, not the server.
- Per-entry compromise containment. A fresh DEK per entry avoids vault-wide ciphertext reuse.
- Recipient-explicit encryption. Access to an entry depends on possession of a corresponding master KEM secret key.
- Author binding. Entry signatures bind each blob to a specific signing identity.
- Passphrase non-sufficiency. The passphrase alone is not enough; wrapped-key unwrapping also requires the device secret / recovery-derived secret.
- Offline operation. Local read and write operations can proceed without network access once the vault is unlocked and local blobs are available.
- Network minimization. Synchronization transmits ciphertext, wrapped keys, public keys, and authorization metadata; not plaintext entries.