PassQ Security Analysis

1. Server Zero-Knowledge Analysis

If a passq server handed to an attacker — full disk, full database, ability to run the binary in their lab — what do they actually have, and what can they do with it?

1.1 What the server stores

Table / object Contents Secret?
users.master_kem_pk ML-KEM-768 public key (1184 B) no
users.master_sig_pk ML-DSA-65 public key (1952 B) no
users.recovery_sig_pk ML-DSA-65 public key no
devices.device_sig_pk ML-DSA-65 public key per device no
wrapped_keys.data PQPW-framed WrappedMasterKey blob encrypted under Argon2id(passphrase ∥ device_secret)
blobs.data PQPW-framed Entry blob encrypted under per-entry DEK
blob_recipients (blob_id, user_id) ACL pairs metadata (cleartext)
auth_challenges.nonce 32 random bytes per challenge random, short-lived
sessions.token 32 random bytes per session bearer credential, ≤15 min TTL

1.2 What the server provably never holds

This is structural rather than promised: the protocol has no code path that uploads any of these.

1.3 What a fully compromised server can actually do

The attacker walks off with the SQLite file and runs the binary in their lab. They get:

Verdict

The server is zero-knowledge with respect to keys and plaintext. All cleartext on the server is either public-key material that has no value when stolen, opaque ciphertext, or routing metadata. The two residual attack surfaces against a stolen server are offline brute force of the wrapped-key blob — bounded in §3 — and pubkey substitution against not-yet-pinned clients, which is the standard TOFU caveat.

2. Client Compromise Analysis

The outcome depends on the device state at the moment of seizure.

2.1 Device locked since boot, no passcode entered

Everything PassQ touches sits behind iOS's class-key encryption. device_secret uses kSecAttrAccessibleWhenUnlockedThisDeviceOnly, the wrapped-key blob and entry cache live in app sandbox files protected by NSFileProtectionComplete, the cached master key uses .biometryCurrentSet, and the device signing secret uses .userPresence. On a powered-off or freshly rebooted device, none of these keys are accessible without the user passcode.

Attacker yield: nothing usable without breaking iOS itself. A modern iPhone with a strong passcode is the limiting factor. This is the same threat model that protects the iOS Keychain across the OS.

2.2 iOS unlocked, PassQ locked

kSecAttrAccessibleWhenUnlockedThisDeviceOnly items become accessible to any process running as the app. Two sub-cases:

Combined yield in the second sub-case: wrapped-key ciphertext + device_secret. From this they mount an offline passphrase-only attack. The second factor has fallen; passphrase quality is the only remaining barrier (quantified in §3).

Mitigation available: binding the device_secret Keychain item to .userPresence blocks this attack — the attacker cannot read it without a biometric or passcode prompt. Cost: a Face ID / passcode prompt on every passphrase unlock.

2.3 PassQ unlocked

In-memory: the master KEM secret key, the master signing secret key, the recovery signing secret key, and the full decrypted entries array. Any attacker with code execution in this process state has full read access to the vault on this device.

iOS sandbox protections make this difficult but not impossible. Process memory can be dumped via debugger on a development-signed build or via OS-level exploit. The 30-second background auto-lock shortens the window but does not eliminate it. Swift-managed buffers are not actively zeroized on lock; ARC will deallocate but bytes can linger in physical pages until reuse.

2.4 What a stolen device cannot do to other devices

The keys on this device do not yield access to other devices' device signing keys or independent device sessions. However, device_secret doubles as the recovery code for the whole vault — if an attacker exfiltrates it from one device and also obtains the passphrase, they can recover-enroll a new device they control. The user's only detection path is the device list, which they must check.

2.5 Summary

State Attacker yield Practical risk
Locked since boot Nothing without an iOS zero-day low
Unlocked, PassQ locked, sandboxed device_secret (today) — one of two factors, offline attack possible medium until device_secret is biometry-bound
Unlocked, PassQ locked, after biometric binding Wrapped-key ciphertext only — zero of two factors low
Unlocked, PassQ unlocked Full vault content high — depends on 30 s auto-lock and OS sandbox
Verdict

Client compromise is bounded but not zero. The single largest improvement available is binding device_secret to biometric presence. That change collapses the unlocked-phone, locked-app attack from "passphrase brute force" to "no actionable material."

3. Post-Quantum and Nation-State Threat Model

The threat is an actor with state-level compute resources and possibly a cryptographically relevant quantum computer (CRQC), attempting to decrypt one user's vault.

3.1 Algorithm-by-algorithm work factor

Primitive Role Best classical attack Best quantum attack
ML-KEM-768 Recipient encryption ≈2200 (lattice sieving) ≈2180 (Grover-accelerated sieving). Shor's algorithm does not apply.
ML-DSA-65 Signatures ≈2192 (SIS / MLWE forging) ≈2180. Same MLWE foundation as ML-KEM.
ChaCha20-Poly1305 AEAD for wrapped key and entries 2256 key search ≈2128 (Grover). Still beyond plausible CRQC.
Argon2id (256 MiB / 3 / 4) Passphrase hardening Memory-bandwidth bound; ~0.5 s/guess commodity CPU Grover gives sqrt speedup on guess space; per-guess cost is bandwidth-bound and not improved.
HKDF-SHA-256 Per-recipient DEK wrap key derivation 2256 output ≈2128 (Grover). Not the weakest link.
SHA-256 Fingerprints, AAD construction 2128 collision ≈285 with memory tradeoffs. Fingerprints (16 bytes) inherit 264 quantum collision — adequate for human comparison, not for cryptographic commitment.

3.2 Concrete attacker walkthroughs

A. Server compromise, nothing else

To decrypt one entry the attacker needs the ML-KEM-768 decapsulation key, which lives inside the wrapped-key blob. To open the wrapped-key blob they need the passphrase and the 256-bit device_secret. Brute force of device_secret alone is 2256 guesses classical, 2128 quantum with Grover — both infeasible.

Pure-PQ with a 256-bit second factor against a server-only attacker is effectively unbreakable.

B. Server compromise plus a stolen device_secret

The attacker also seized a phone or extracted device_secret via the unlocked-phone path in §2.2. Only the passphrase stands between them and the master keypair. Argon2id at the current parameters takes roughly 500 ms per guess on a commodity CPU; specialized hardware can drive that to ~10 ms per guess on a high-bandwidth rig.

For a passphrase with N bits of entropy, work factor = 2N−1 × 0.01 s per guess:

Passphrase entropy Single rig 100,000-way parallel (nation-state) Verdict
40 bits (4 dictionary words) ~170 years ~15 hours breakable
50 bits (5 diceware words) ~175,000 years ~64 days hard
60 bits (6 diceware words) ~180 million years ~1,800 years infeasible
70 bits (7+ words) — — unbreakable
80 bits — quantum (Grover) ~350 years unbreakable

Argon2id is the load-bearing element. Without it, even a 60-bit passphrase falls in hours on a GPU farm. Anchor any future passphrase-strength UI at ≥60 bits assuming device_secret is also compromised, and consider raising iterations to 4 or memory_kib to 512 MiB to widen the wall.

C. Adversary with a working CRQC

Such a CRQC does not directly attack ML-KEM-768 or ML-DSA-65. Shor's algorithm is irrelevant to lattice schemes. The adversary would need novel lattice cryptanalysis — possible but not currently known. Best public progress on BKZ blocksize and sieving has been steady but not breakthrough.

If lattice cryptanalysis advances to break ML-KEM-768 in practice, the response is algorithm agility (move to ML-KEM-1024 or another family) rather than parameter tweaks. The wire format supports this: every blob carries explicit algorithm identifiers and the AEAD AAD binds them, so a v2 wire format can ship new algorithms alongside the old.

D. Harvest now, decrypt later

Pure-PQ resists this completely under any known quantum algorithm. The hybrid X25519+ML-KEM alternative would resist a single-algorithm break of either component but not both. The current design chose pure-PQ, trading algorithm-flaw risk for cleaner harvest-now-decrypt-later resistance.

3.3 Where nation-state attackers actually win

Crypto is rarely the breaking point. In rough order of practical likelihood:

  1. Endpoint compromise. An implant on the user's iOS device captures the master key from memory after unlock. The crypto is fine; the OS is the perimeter.
  2. Passphrase capture. Keylogger, shoulder surf, coerced disclosure.
  3. Implementation flaws. A memory-safety bug, a timing leak in Argon2id, a state-machine bug in OpenSSL. OpenSSL 3.5's PQ paths are newer code and will be heavily scrutinized.
  4. Side channels. Power, EM, cache timing — relevant when the attacker has physical or process co-location.
  5. Supply chain. Malicious dependency update adding a backdoor before signing.
  6. Lattice cryptanalysis advance. Possible but the field has not had a "Shor moment" for lattices in 25 years.

Direct algorithmic break of ML-KEM-768 or ML-DSA-65 by a nation state is, today, less likely than any of the above. The crypto choices are not where a state-level adversary should be expected to attack.

4. OpenSSL 3.5 Component Map

Where each cryptographic primitive in PassQ gets serviced by OpenSSL 3.5.

Operation OpenSSL algorithm name API path
ML-KEM-768 keypair generation "ML-KEM-768" EVP_PKEY_CTX_new_from_name + EVP_PKEY_keygen
ML-KEM-768 encapsulation (entry sharing) "ML-KEM-768" EVP_PKEY_encapsulate_init + EVP_PKEY_encapsulate
ML-KEM-768 decapsulation (entry decrypt) "ML-KEM-768" EVP_PKEY_decapsulate_init + EVP_PKEY_decapsulate
ML-DSA-65 keypair generation "ML-DSA-65" EVP_PKEY_CTX_new_from_name + EVP_PKEY_keygen
ML-DSA-65 signing "ML-DSA-65" EVP_DigestSignInit_ex (NULL md) + EVP_DigestSign
ML-DSA-65 verification "ML-DSA-65" EVP_DigestVerifyInit_ex (NULL md) + EVP_DigestVerify
ChaCha20-Poly1305 AEAD "ChaCha20-Poly1305" EVP_CIPHER_fetch + EVP_EncryptInit_ex2 + AAD via EVP_EncryptUpdate (NULL out) + plaintext via EVP_EncryptUpdate + EVP_EncryptFinal_ex + EVP_CIPHER_CTX_ctrl(GET_TAG)
Argon2id (passphrase → wrapping key) "ARGON2ID" EVP_KDF_fetch + EVP_KDF_CTX_new + OSSL_PARAM (memory_cost, iter, threads, password, salt) + EVP_KDF_derive
HKDF-SHA-256 (per-recipient DEK wrap key) "HKDF" with "digest"="SHA2-256" EVP_KDF_fetch + EVP_KDF_CTX_new + OSSL_PARAM (mode, digest, key, salt, info) + EVP_KDF_derive
SHA-256 (fingerprints, AAD) "SHA2-256" EVP_MD_fetch + EVP_DigestInit_ex2 + EVP_DigestUpdate + EVP_DigestFinal_ex, or one-shot EVP_Q_digest
Random bytes DRBG (AES-256-CTR-DRBG, NIST SP 800-90A) RAND_bytes

Notes

Stack mapping

Layer Integration
Rust server The openssl crate (≥ 0.10.66) provides idiomatic PKey<Private>, Cipher, Kdf, and MessageDigest wrappers around the EVP API. ML-KEM and ML-DSA are exposed via pkey::Id::ML_KEM_768 and pkey::Id::ML_DSA_65. openssl-sys is available for direct FFI if needed.
iOS (Swift) Build OpenSSL 3.5 as a static libcrypto.a xcframework. Replace OQSKit with a thin Swift wrapper around the EVP API, matching the existing MLKEM768.swift / MLDSA65.swift signatures so call sites in CryptoCore are unchanged.

After the migration the only PQ-aware code in the tree is the thin Swift wrapper and the matching Rust wrappers — both pointing at the same audited C codebase. Add a cross-stack roundtrip test that signs a known message on iOS and verifies on the server, and encaps to a known pubkey on iOS and decaps the same ciphertext on the server. That single test would have caught the earlier Dilithium-3 vs ML-DSA-65 mismatch instantly.

5. Summary