Skip to content

Add hybrid post-quantum handshake for passive capture resistance #217

@danmarg

Description

@danmarg

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

  1. Alice generates: X25519 ephemeral keypair + PQ KEM keypair
  2. Alice puts SHA-256(kemEncKey) in QrPayload; publishes full kemEncKey via discovery/mailbox
  3. Bob scans QR → fetches kemEncKey → verifies commitment → encapsulates → sends KeyExchangeInitMessage with Bob's X25519 ephemeral pub + PQ ciphertext + hybrid key confirmation
  4. 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

  • CryptoPrimitives.kt exposes PQ KEM primitives via expect/actual
  • QrPayload includes a commitment to the PQ public key (not the full key)
  • Discovery/bootstrap path can publish and retrieve the full PQ encapsulation key
  • Bob verifies PQ key commitment before encapsulation
  • KeyExchangeInitMessage carries PQ ciphertext
  • Bootstrap secret derived from X25519_sk || PQ_KEM_sk via HKDF
  • Key confirmation and initial routing token derived from hybrid bootstrap secret
  • Protocol version bumped; v1↔v2 interop behavior defined
  • Tests cover: successful hybrid bootstrap, commitment mismatch, decapsulation failure, version mismatch

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions