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).
Primitive parameters
Section titled “Primitive parameters”| 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.
Feature flag
Section titled “Feature flag”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.
Hybrid key bundle
Section titled “Hybrid key bundle”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) |
Negotiation
Section titled “Negotiation”The initiator uses the hybrid handshake only when all of the following hold:
PQC_HYBRID_ENABLEDis on,- the local identity has a Kyber key pair,
- the peer bundle advertises
pqcSupportedand includes akyberIdentityKey.
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.
Initiator flow
Section titled “Initiator flow”-
Run standard X3DH, producing the 32-byte X25519 secret.
-
Encapsulate against the peer’s
kyberIdentityKey:(kyberCiphertext, kyberSecret) = ML-KEM-768.Encap(pk). -
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_SALTis a dedicated 32-byte protocol constant (derived from the stringzentalk-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. -
Attach the encapsulation to the initial message. The hybrid wire format (v=2) extends the classical initial message with:
Field Content kyberCiphertextBase64 ML-KEM-768 ciphertext (1,088 bytes) pqcEnabledtrue
Responder flow and downgrade protection
Section titled “Responder flow and downgrade protection”The responder runs classical X3DH, then inspects the initial message:
- If it asserts hybrid mode (
pqcEnabledandkyberCiphertextpresent), 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.
Key hygiene
Section titled “Key hygiene”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.
Scope and residual risk
Section titled “Scope and residual risk”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.
