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.

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.

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.

The server is not expected to do any of the following.

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