Skip to content

Post-quantum hybrid encryption

To defend against “harvest now, decrypt later” adversaries, Zentalk runs its X3DH handshake in hybrid mode by default: the classical X25519 agreement is combined with an ML-KEM-768 (Kyber-768) key encapsulation, so an attacker must break both primitives to recover the session secret.

Implementation: zentalk/src/services/e2ee/pqc/ (hybrid-x3dh.ts, kyber.ts, hybrid-keys.ts), using the mlkem library (NIST FIPS 203 ML-KEM).

Parameter ML-KEM-768 (Kyber-768)
Public key 1,184 bytes
Private key 2,400 bytes
Ciphertext 1,088 bytes
Shared secret 32 bytes
Security level ~192-bit post-quantum (X25519 provides ~128-bit classical)

All sizes are validated before every encapsulation/decapsulation; mismatches abort the handshake.

PQC_HYBRID_ENABLED (NEXT_PUBLIC_PQC_HYBRID_ENABLED) gates Kyber key generation, encapsulation, and decapsulation client-wide. It is on unless explicitly set to false; with the flag off, all PQC operations throw and sessions are established classically.

The published key bundle is extended with two fields:

Field Content
pqcSupported Capability flag advertising hybrid support
kyberIdentityKey ML-KEM-768 public key (1,184 bytes)

The initiator uses the hybrid handshake only when all of the following hold:

  1. PQC_HYBRID_ENABLED is on,
  2. the local identity has a Kyber key pair,
  3. the peer bundle advertises pqcSupported and includes a kyberIdentityKey.

Otherwise the handshake falls back to classical X25519-only X3DH. When hybrid mode is used, the session records pqcEnabled: true, which the call UI surfaces as a “quantum-protected” indicator.

  1. Run standard X3DH, producing the 32-byte X25519 secret.

  2. Encapsulate against the peer’s kyberIdentityKey: (kyberCiphertext, kyberSecret) = ML-KEM-768.Encap(pk).

  3. Combine the two secrets:

    HKDF combiner
    combined = HKDF-SHA256(x25519_secret || kyber_secret,
    salt = HYBRID_X3DH_SALT,
    info = "zentalk.hybrid-x3dh.v1",
    length = 64)

    HYBRID_X3DH_SALT is a dedicated 32-byte protocol constant (derived from the string zentalk-hybrid-x3dh-salt-v1), distinct from the classical X3DH salt — this prevents cross-protocol key reuse between classical and hybrid derivations of the same DH outputs.

  4. Attach the encapsulation to the initial message. The hybrid wire format (v=2) extends the classical initial message with:

    Field Content
    kyberCiphertext Base64 ML-KEM-768 ciphertext (1,088 bytes)
    pqcEnabled true

The responder runs classical X3DH, then inspects the initial message:

  • If it asserts hybrid mode (pqcEnabled and kyberCiphertext present), the responder must decapsulate. If it cannot — missing Kyber private key or PQC disabled locally — the handshake fails rather than silently continuing classically.
  • If the message is classical, the responder completes classically. Compatibility with non-PQC peers is preserved: hybrid is only ever attempted when both sides advertised support.

After decapsulation the responder applies the identical HKDF combiner and obtains the same combined secret, which seeds the Double Ratchet root.

Kyber key material is cleared with multi-pass zeroization (random fill, 0xFF fill, 0x00 fill) when no longer needed, matching the hygiene applied to classical DH outputs.

Hybrid protection covers the session key agreement. Bundle signatures remain Ed25519 (no ML-DSA yet) — recorded as a residual in the threat model in the zentalk-web repository (THREAT_MODEL.md, adversary A1*). Symmetric layers (AES-256-GCM, HMAC-SHA256, HKDF-SHA256) already offer adequate margins against Grover-type quantum attacks.