Problem
The current E2EE protocol uses an ephemeral-only X25519 bootstrap handshake and X25519 DH ratchet steps. This is vulnerable to a future "harvest now, decrypt later" attacker who passively captures traffic today and later gains a cryptographically relevant quantum computer.
The main concern is passive capture with future decryption, not active nearby compromise of the QR pairing flow.
Current protocol points affected
- Initial session bootstrap in
KeyExchange.kt:
aliceCreateQrPayload()
bobProcessQr()
aliceProcessInit()
- Ongoing asymmetric ratchet in
Ratchet.kt / session evolution
- Protocol metadata and serialization in
Types.kt and ProtocolConstants.kt
- Platform crypto abstraction in
CryptoPrimitives.kt
The current bootstrap is explicitly documented as "ephemeral-only single X25519" and derives the initial session state from that shared secret. The crypto primitive layer currently exposes X25519, SHA-256/512, HMAC-SHA-256, ChaCha20-Poly1305, RNG, and zeroization, but no PQ KEM operations. PROTOCOL_VERSION and SUPPORTED_MAX_VERSION are both currently 1.
Goal
Add post-quantum resistance against a current passive capture adversary, while preserving the existing QR/link format and avoiding oversized QR payloads.
Proposed approach
Implement a hybrid classical + post-quantum handshake:
- Keep the existing X25519 bootstrap.
- Add a PQ KEM to the bootstrap and combine the classical and PQ shared secrets with HKDF.
- Treat QR as a commitment/binding channel, not the transport for the full PQ key.
- Preserve existing QR/link format by putting only a commitment to the PQ public key in the QR payload.
This follows Signal's PQXDH direction: combine classical ECDH with an ML-KEM so that an attacker must break both primitives.
Requirements
1. Hybrid bootstrap secret
In KeyExchange.kt, replace the effective bootstrap secret with a hybrid secret derived from both classical and PQ shared secrets:
hybrid_sk = HKDF(
ikm = x25519_sk || pq_kem_sk,
salt = null,
info = "Where-v2-HybridKEX",
length = 32
)
Use hybrid_sk everywhere the current code uses sk for initial session derivation, key confirmation, and initial routing token derivation.
2. Add PQ KEM primitives
Extend CryptoPrimitives.kt with KEM operations (ML-KEM-1024 or ML-KEM-512):
generateKemKeyPair() → KemKeyPair
kemEncapsulate(recipientEncapKey) → (ciphertext, sharedSecret)
kemDecapsulate(ciphertext, decapKey) → sharedSecret
The existing expect/actual abstraction is the right integration point for platform-specific implementations (BouncyCastle/JDK 21 on Android/JVM; CryptoKit on iOS 18+).
3. Keep QR format compact via commitment
Do not place the full PQ public key (1568 bytes for ML-KEM-1024) in the QrPayload — this would produce unusably dense QR codes. Instead, add a commitment field to QrPayload:
kemEncKeyHash: SHA-256(kemEncKey) (32 bytes)
The QR continues to carry existing bootstrap material plus this commitment. Bob fetches the full PQ encapsulation key from the existing discovery/mailbox path, verifies it matches the commitment, then encapsulates. This keeps QR and invite-link formats aligned.
4. Extended bootstrap flow
- Alice generates: X25519 ephemeral keypair + PQ KEM keypair
- Alice puts
SHA-256(kemEncKey) in QrPayload; publishes full kemEncKey via discovery/mailbox
- Bob scans QR → fetches
kemEncKey → verifies commitment → encapsulates → sends KeyExchangeInitMessage with Bob's X25519 ephemeral pub + PQ ciphertext + hybrid key confirmation
- Alice decapsulates and derives the same
hybrid_sk
5. Schema changes (Types.kt)
QrPayload: add kem_enc_key_hash field
KeyExchangeInitMessage: add kem_ciphertext field
- Add
KemKeyPair and KemEncapsulateResult data classes
Maintain serialization compatibility carefully (existing ignoreUnknownKeys = true helps for forward compat).
6. Versioning / compatibility (ProtocolConstants.kt)
Bump to PROTOCOL_VERSION = 2. Define explicit behavior:
- v2↔v2: use hybrid PQ bootstrap
- v1↔v2: reject (do not silently lose PQ protection)
The existing version guard in bobProcessQr / aliceProcessInit already handles protocolVersion > SUPPORTED_MAX_VERSION.
7. Follow-up: ongoing ratchet
This issue is scoped to bootstrap hardening. A follow-up issue should evaluate whether each X25519 DH ratchet step also needs a hybrid upgrade. The symmetric ratchet itself is much less urgent — HKDF/HMAC over 256-bit keys are not broken by Shor's algorithm, only weakened by Grover's to ~128-bit PQ security.
Acceptance criteria
Notes
This is a confidentiality-hardening change against passive traffic capture today with decryption later by a future quantum-capable adversary. The QR channel itself is not the primary concern — it remains an authentication/binding channel whose out-of-band nature already limits the practical attacker surface.
Problem
The current E2EE protocol uses an ephemeral-only X25519 bootstrap handshake and X25519 DH ratchet steps. This is vulnerable to a future "harvest now, decrypt later" attacker who passively captures traffic today and later gains a cryptographically relevant quantum computer.
The main concern is passive capture with future decryption, not active nearby compromise of the QR pairing flow.
Current protocol points affected
KeyExchange.kt:aliceCreateQrPayload()bobProcessQr()aliceProcessInit()Ratchet.kt/ session evolutionTypes.ktandProtocolConstants.ktCryptoPrimitives.ktThe current bootstrap is explicitly documented as "ephemeral-only single X25519" and derives the initial session state from that shared secret. The crypto primitive layer currently exposes X25519, SHA-256/512, HMAC-SHA-256, ChaCha20-Poly1305, RNG, and zeroization, but no PQ KEM operations.
PROTOCOL_VERSIONandSUPPORTED_MAX_VERSIONare both currently1.Goal
Add post-quantum resistance against a current passive capture adversary, while preserving the existing QR/link format and avoiding oversized QR payloads.
Proposed approach
Implement a hybrid classical + post-quantum handshake:
This follows Signal's PQXDH direction: combine classical ECDH with an ML-KEM so that an attacker must break both primitives.
Requirements
1. Hybrid bootstrap secret
In
KeyExchange.kt, replace the effective bootstrap secret with a hybrid secret derived from both classical and PQ shared secrets:Use
hybrid_skeverywhere the current code usesskfor initial session derivation, key confirmation, and initial routing token derivation.2. Add PQ KEM primitives
Extend
CryptoPrimitives.ktwith KEM operations (ML-KEM-1024 or ML-KEM-512):generateKemKeyPair()→KemKeyPairkemEncapsulate(recipientEncapKey)→(ciphertext, sharedSecret)kemDecapsulate(ciphertext, decapKey)→sharedSecretThe existing
expect/actualabstraction is the right integration point for platform-specific implementations (BouncyCastle/JDK 21 on Android/JVM; CryptoKit on iOS 18+).3. Keep QR format compact via commitment
Do not place the full PQ public key (1568 bytes for ML-KEM-1024) in the
QrPayload— this would produce unusably dense QR codes. Instead, add a commitment field toQrPayload:kemEncKeyHash:SHA-256(kemEncKey)(32 bytes)The QR continues to carry existing bootstrap material plus this commitment. Bob fetches the full PQ encapsulation key from the existing discovery/mailbox path, verifies it matches the commitment, then encapsulates. This keeps QR and invite-link formats aligned.
4. Extended bootstrap flow
SHA-256(kemEncKey)inQrPayload; publishes fullkemEncKeyvia discovery/mailboxkemEncKey→ verifies commitment → encapsulates → sendsKeyExchangeInitMessagewith Bob's X25519 ephemeral pub + PQ ciphertext + hybrid key confirmationhybrid_sk5. Schema changes (
Types.kt)QrPayload: addkem_enc_key_hashfieldKeyExchangeInitMessage: addkem_ciphertextfieldKemKeyPairandKemEncapsulateResultdata classesMaintain serialization compatibility carefully (existing
ignoreUnknownKeys = truehelps for forward compat).6. Versioning / compatibility (
ProtocolConstants.kt)Bump to
PROTOCOL_VERSION = 2. Define explicit behavior:The existing version guard in
bobProcessQr/aliceProcessInitalready handlesprotocolVersion > SUPPORTED_MAX_VERSION.7. Follow-up: ongoing ratchet
This issue is scoped to bootstrap hardening. A follow-up issue should evaluate whether each X25519 DH ratchet step also needs a hybrid upgrade. The symmetric ratchet itself is much less urgent — HKDF/HMAC over 256-bit keys are not broken by Shor's algorithm, only weakened by Grover's to ~128-bit PQ security.
Acceptance criteria
CryptoPrimitives.ktexposes PQ KEM primitives viaexpect/actualQrPayloadincludes a commitment to the PQ public key (not the full key)KeyExchangeInitMessagecarries PQ ciphertextX25519_sk || PQ_KEM_skvia HKDFNotes
This is a confidentiality-hardening change against passive traffic capture today with decryption later by a future quantum-capable adversary. The QR channel itself is not the primary concern — it remains an authentication/binding channel whose out-of-band nature already limits the practical attacker surface.