The full equations behind LWE, Kyber key generation, encryption, decryption, and the correctness proof. Click any section to expand.
| Symbol | Meaning | Value (Demo) |
|---|---|---|
| $n$ | Lattice dimension (LWE security parameter) | 4 (real Kyber: 256) |
| $q$ | Modulus (prime integer) | 97 (real Kyber: 3329) |
| $\mathbb{Z}_q$ | Integers modulo q: $\{0, 1, \ldots, q-1\}$ | $\{0, \ldots, 96\}$ |
| $\mathbf{A}$ | Public random matrix $\in \mathbb{Z}_q^{n \times n}$ | 4×4 matrix |
| $\mathbf{s}$ | Secret key vector $\in \mathbb{Z}_q^n$ (small) | $s_i \in \{0,1,2\}$ |
| $\mathbf{e}$ | Error vector sampled from $\chi^n$ | $e_i \in \{-1,0,1\}$ |
| $\mathbf{b}$ | Public LWE sample: $\mathbf{b} = \mathbf{As} + \mathbf{e}$ | Public key component |
| $\chi$ | Error distribution (discrete Gaussian) | Centered binomial $B(2,0.5)-1$ |
| $\eta$ | Error distribution parameter | 1 (standard deviation ≈ 0.7) |
| $\lfloor \cdot \rceil$ | Round to nearest integer | — |
Two problems define LWE security:
Given samples $(\mathbf{A}, \mathbf{b}) = (\mathbf{A}, \mathbf{As} + \mathbf{e})$ where $\mathbf{A} \xleftarrow{R} \mathbb{Z}_q^{n \times n}$, $\mathbf{s} \xleftarrow{R} \mathbb{Z}_q^n$, $\mathbf{e} \xleftarrow{} \chi^n$, find $\mathbf{s}$.
This is believed to be computationally infeasible for appropriate $(n, q, \chi)$.
Distinguish between $(\mathbf{A}, \mathbf{As+e})$ (LWE samples) and $(\mathbf{A}, \mathbf{u})$ where $\mathbf{u} \xleftarrow{R} \mathbb{Z}_q^n$ (uniformly random). If this is hard, LWE is semantically secure.
Regev (2005) showed that Decision-LWE ≤ Search-LWE via a reduction, and that LWE is as hard as worst-case lattice problems (GapSVP, SIVP).
Security: Given $(pk = (\mathbf{A}, \mathbf{b}))$, recovering $sk = \mathbf{s}$ requires solving LWE. The error $\mathbf{e}$ is essential — without it, you'd have $\mathbf{b} = \mathbf{As}$ and solving for $\mathbf{s}$ is trivial (Gaussian elimination).
Remove the error $\mathbf{e}$, and you have $\mathbf{b} = \mathbf{As}$: a linear system solvable in $O(n^3)$ with Gaussian elimination. The error is what makes it a lattice problem instead of linear algebra.
// LWE Key Generation (from our lwe.js)
function keyGen(n = 4, q = 97) {
// Public matrix A: random entries mod q
const A = Array.from({ length: n }, () =>
Array.from({ length: n }, () => randMod(q))
);
// Secret vector s: small values (centered binomial)
const s = Array.from({ length: n }, () => mod(sampleError(2), q));
// Error vector e from narrow distribution χ
const e = Array.from({ length: n }, () => sampleError(1)); // |e_i| ≤ 1
// b = A·s + e mod q ← This is the LWE hard problem
const As = matVecMulMod(A, s, q); // matrix-vector multiply
const b = As.map((v, i) => mod(v + e[i], q));
return {
publicKey: { A, b }, // (A, b) is public
secretKey: s // s is private
};
}
The sender uses the receiver's public key to encapsulate a shared secret.
In our demo, we simplify to just $\mathbf{u} = \mathbf{A}^T\mathbf{r} + \mathbf{e}_1$ and derive the shared key directly from $K_A = \mathbf{b}^T\mathbf{r} \pmod{q}$, which captures the core LWE exchange logic.
// LWE Encapsulation (Sender side)
function encapsulate(publicKey, noiseBound = 1, n = 4, q = 97) {
const { A, b } = publicKey;
// Choose random r (small, like a one-time secret)
const r = generateSecret(n, q); // small values
// Error vectors for encapsulation (adds security)
const e1 = generateErrorVec(n, noiseBound);
// Compute u = A^T · r + e1 mod q (ciphertext sent to Bob)
const AT = transpose(A);
const ATr = matVecMulMod(AT, r, q);
const u = ATr.map((v, i) => mod(v + e1[i], q));
// Alice's raw shared key: K_A = b^T · r (inner product)
const kAlice = dotMod(b, r, q);
return { ciphertext: u, rawSecret: kAlice };
}
So $K_B = K_A - \delta$. If the noise $|\delta| < q/4$, then rounding both $K_A$ and $K_B$ to the nearest multiple of $\lfloor q/4 \rfloor$ gives the same result. This is the reconciliation step.
// LWE Decapsulation (Receiver side)
function decapsulate(secretKey, ciphertext, q = 97) {
// K_B = s^T · u mod q
// When noise is small: K_B ≈ K_A ← the same key!
const kBob = dotMod(secretKey, ciphertext, q);
return kBob;
}
// Check correctness and derive key bytes
async function deriveKeyBytes(kAlice, kBob, q = 97) {
const diff = Math.min(
Math.abs(kAlice - kBob),
q - Math.abs(kAlice - kBob)
);
const match = diff < Math.floor(q / 4); // correctness bound!
// Hash to get 32 uniform bytes (key derivation)
const rounded = match ? round(kAlice) : kAlice;
const buf = new TextEncoder().encode(`LWE_SECRET_${rounded}_q${q}`);
const hashBuf = await crypto.subtle.digest('SHA-256', buf);
return { match, difference: diff, keyBytes: new Uint8Array(hashBuf) };
}
For our demo parameters ($n=4, q=97$, errors in $\{-1,0,1\}$, secret in $\{0,1,2\}$):
$|\mathbf{e}^T\mathbf{r}| \leq n \cdot \eta_e \cdot \eta_r = 4 \cdot 1 \cdot 2 = 8$
$|\mathbf{s}^T\mathbf{e}_1| \leq n \cdot \eta_s \cdot \eta_{e_1} = 4 \cdot 2 \cdot 1 = 8$
$|\delta|_{\max} \leq 16 < \lfloor 97/4 \rfloor = 24$ ✓
For our demo: failure ≈ 0 (the deterministic worst-case is within bound). Real Kyber-512's decryption failure probability is $\approx 2^{-139}$ — essentially zero.
In the interactive demo, you can increase the noise bound beyond the correctness threshold. When $|\delta| \geq q/4$, decryption fails — this visualizes why parameter selection matters and why real Kyber uses careful parameter engineering.
| Parameter | Our Demo | Kyber-512 | Kyber-768 | Kyber-1024 |
|---|---|---|---|---|
| $n$ (dimension) | 4 | 256 | 256 | 256 |
| $q$ (modulus) | 97 | 3329 | 3329 | 3329 |
| $k$ (module rank) | — | 2 | 3 | 4 |
| $\eta_1$ (secret noise) | 2 | 3 | 2 | 2 |
| $\eta_2$ (enc noise) | 1 | 2 | 2 | 2 |
| Classical security | Demo only | ~100 bits | ~161 bits | ~218 bits |
| Quantum security | Demo only | ~100 bits | ~161 bits | ~218 bits |
| Public key size | ~90 B | 800 B | 1,184 B | 1,568 B |
| Dec. failure prob. | ~0 | $2^{-139}$ | $2^{-164}$ | $2^{-174}$ |
The complete LWE implementation is in js/lwe.js. Key primitives:
// Correct modular reduction (handles negative numbers)
function mod(x, q) { return ((x % q) + q) % q; }
// Matrix-vector multiply mod q
function matVecMulMod(A, v, q) {
return A.map(row =>
mod(row.reduce((sum, a, j) => sum + a * v[j], 0), q)
);
}
// Inner product mod q
function dotMod(a, b, q) {
return mod(a.reduce((sum, x, i) => sum + x * b[i], 0), q);
}
We use a centered binomial distribution $B(2\eta, 1/2) - \eta$, which approximates a discrete Gaussian and is easy to implement securely:
// Sample from centered binomial distribution
// Models discrete Gaussian χ with parameter η
function sampleError(eta = 1) {
let s = 0;
for (let i = 0; i < 2 * eta; i++) {
const arr = new Uint8Array(1);
crypto.getRandomValues(arr);
s += arr[0] & 1; // uniform bit
}
return s - eta; // center at 0: values in {-η, ..., η}
}
// E[sampleError] = 0, Var[sampleError] = η/2
// Hybrid key: SHA-256(ECDH_secret || LWE_secret || label)
async function deriveHybridKey(ecdhSecret, lweSecret) {
const label = new TextEncoder().encode('HybridPQC-v1:ECDH+LWE');
const material = concat(concat(ecdhSecret, lweSecret), label);
const hashBuf = await crypto.subtle.digest('SHA-256', material);
const hybridKeyBytes = new Uint8Array(hashBuf);
// Import as AES-256-GCM key for direct use
const aesKey = await crypto.subtle.importKey(
'raw', hybridKeyBytes,
{ name: 'AES-GCM' }, false, ['encrypt', 'decrypt']
);
return { hybridKeyBytes, hybridKeyHex: toHex(hybridKeyBytes), aesKey };
}