Post-Quantum Extended Diffie-Hellman Key Exchange
PQXDH is Signal's hybrid post-quantum key agreement protocol that combines classical X25519 elliptic curve cryptography with ML-KEM-1024 (formerly CRYSTALS-Kyber), a NIST-standardized post-quantum key encapsulation mechanism. This provides protection against both classical and quantum computer attacks.
Key Features: Post-quantum forward secrecy • Quantum-resistant • NIST FIPS 203 compliant • Production-ready (Signal Messenger)
ML-KEM (Module-Lattice Key Encapsulation Mechanism) is based on the Learning With Errors (LWE) problem, a mathematical problem that is believed to be hard for both classical and quantum computers to solve.
The security of ML-KEM is based on this mathematical challenge:
The small error e "hides" the secret key s. Without e, this would be easy to solve (simple linear algebra). With e, it becomes extremely difficult—even for quantum computers!
To share a secret with someone, we use their public key (a, b):
The recipient uses their secret key s to recover the shared secret:
| Lattice Dimension (k × n) | 4 × 256 = 1024 coefficients |
| Modulus (q) | 3329 (prime) |
| Error Distribution (σ) | Centered binomial distribution, η = 2 |
| Public Key Size | 1568 bytes |
| Ciphertext Size | 1568 bytes |
| Security Level | 256-bit quantum security (NIST Level 5) |
📚 Further Reading: The actual ML-KEM uses polynomial rings and module lattices for efficiency, but the core security principle remains the same: adding small errors to linear equations makes them exponentially hard to solve!
This PQXDH simulation uses JavaScript to demonstrate ML-KEM (Kyber), while the Slow Lynx Cryptography Discovery project includes a Rust implementation of educational LWE encryption. Both are based on the same lattice cryptography principles!
| Aspect | JavaScript (This Page) | Rust (Slow Lynx) |
|---|---|---|
| Implementation | EducationalMLKEM class | LatticeKey struct |
| Purpose | ML-KEM-1024 demonstration in PQXDH protocol | Educational LWE encryption with multiple security levels |
| Security Levels | NIST Level 5 (256-bit quantum) | Toy (80-bit), Standard (128-bit), High (192-bit), Military (256-bit) |
| Dimension | 4 × 256 = 1024 | 128, 256, 512, or 1024 (configurable) |
| Modulus | q = 3329 (ML-KEM standard) | q = 7681, 12289, 16381, or 32749 |
| Error Sampling | Centered binomial (η=2) | Discrete Gaussian approximation (σ=3.2) |
| Environment | Browser (HTML/JavaScript) | Command-line (Rust CLI) |
| Encryption Capability | KEM (Key Encapsulation Mechanism) | Bit-by-bit, 4-bit (nibble), and 8-bit (byte) encryption |
Both implementations follow the same core algorithm: b = a·s + e (mod q)
// Key Generation: b = a·s + e (mod q)
generateKeyPair() {
// Generate public lattice vector 'a' (uniformly random)
const a = new Uint8Array(this.publicKeySize);
crypto.getRandomValues(a);
// Generate secret key 's' (small binary values)
const s = new Uint8Array(this.publicKeySize);
for (let i = 0; i < s.length; i++) {
s[i] = Math.random() < 0.5 ? 0 : 1;
}
// Sample small error 'e' (centered binomial)
const e = this.sampleSmallError(this.publicKeySize);
// Compute b = a·s + e (mod q)
const b = new Uint8Array(this.publicKeySize);
for (let i = 0; i < this.publicKeySize; i++) {
let sum = 0;
for (let j = 0; j < Math.min(8, this.publicKeySize - i); j++) {
sum += a[i + j] * s[j];
}
b[i] = (sum + e[i]) % 256;
}
return { publicKey: a, secretKey: s };
}
// Key Generation: b = a·s + e (mod q)
pub fn generate(rng: &CspRng, params: &LatticeParams)
-> Result<LatticeKeyPair, &'static str> {
let n = params.dimension;
let q = params.modulus;
// Generate secret key s (binary values {0, 1})
let mut s = vec![0u32; n];
for i in 0..n {
s[i] = rng.next_u32()? % 2;
}
// Generate random lattice vector a
let mut a = vec![0u32; n];
for i in 0..n {
a[i] = rng.next_u32()? % q;
}
// Sample small error e (Gaussian approximation)
let mut e = vec![0u32; n];
for i in 0..n {
e[i] = sample_discrete_gaussian(rng, params.error_stddev, q)?;
}
// Compute b = a*s + e (mod q) - element-wise
let mut b = vec![0u32; n];
for i in 0..n {
let mut sum = 0u64;
sum = (a[i] as u64 * s[i] as u64) % q as u64;
b[i] = ((sum + e[i] as u64) % q as u64) as u32;
}
Ok(LatticeKeyPair { public_key: (a, b), secret_key: s })
}
The Rust implementation offers configurable security levels through parameter presets:
| Security Level | Dimension (n) | Modulus (q) | Error (σ) | Security |
|---|---|---|---|---|
| Toy | 128 | 7681 | 3.2 | ~80-bit |
| Standard | 256 | 12289 | 3.2 | ~128-bit |
| High | 512 | 16381 | 3.2 | ~192-bit |
| Military | 1024 | 32749 | 3.2 | ~256-bit |
| Slow Butterfly Simulation | Browser-based cryptography simulations (this page!) - DH, ECDH, PQXDH, ML-KEM |
| Slow Lynx Cryptography Discovery | Rust CLI: CSPRNG analysis, LWE implementation, crypto library scanner |
| Slow Owl Packet Observer | Real-time network analyzer: observe hybrid PQ (X25519Kyber768) in live traffic |
💡 Design Philosophy: The JavaScript version prioritizes browser accessibility and protocol demonstration (ML-KEM in PQXDH). The Rust version emphasizes configurable parameters, comprehensive testing, and integration with a CSPRNG for cryptographic research. Both teach the same core LWE concepts from different angles!
| Feature | X3DH (Classical) | PQXDH (Hybrid) |
|---|---|---|
| Primary Key Exchange | X25519 (Curve25519) | X25519 + ML-KEM-1024 |
| Quantum Resistance | ❌ Vulnerable to Shor's Algorithm | ✅ Quantum-Resistant |
| Forward Secrecy | ✅ Yes (classical) | ✅ Yes (post-quantum) |
| Protection Timeline | Until ~2035 (quantum threat) | 2025+ (current protection) |
| NIST Standardized | NIST SP 800-186 | NIST FIPS 203 (Aug 2024) |
| Key Size | 32 bytes (256-bit) | 32 bytes + 1568 bytes (KEM) |
| Performance | ~1ms | ~10-20ms |
| Production Use | Signal (2016-2023) | Signal (2023+) |