Chapter 10: Regev encryption from scratch
Regev encryption hides a message bit inside LWE noise. The public key is an LWE sample . A ciphertext is a random -linear combination of the rows of this sample. The sender adds to the second coordinate to carry the bit . Decryption computes modulo , which cancels the secret and leaves . Rounding the result to the nearer of or recovers whenever the noise stays inside the window the decoder tolerates.
For correctness, the noise budget is the entire story. Security adds two more pieces: the public-key indistinguishability from uniform under decisional LWE, and the leftover hash lemma applied to the joint hash .
A bit hidden in LWE noise
Section titled “A bit hidden in LWE noise”The toy parameters are , matching Chapter 8. Key generation draws a uniform secret and a uniform matrix . It also draws an error vector and sets in . The public key is the LWE sample and the secret key is (Regev, 2009).
Seeding numpy’s default generator at fixes a specific instance. The secret is . The error in symmetric representatives is . Every coordinate of is at most in absolute value.
To encrypt the bit , the sender draws a random . The ciphertext is . The shift is the encoding of the bit . At the same seed the draw is , and the ciphertext is with .
Decryption uses the secret to compute . The result is . In symmetric representatives this is distance from , so it rounds to and decodes to the bit . The noise did not flip the decoded bit because , and the decoder tolerates any up to at these parameters.
The same with the bit gives instead of , because the encoding shift drops from to . The decrypted value is , which in symmetric representatives is . That rounds to and decodes to . The distance from to and the distance from to are both the same quantity .
A numpy block reproduces the walkthrough end to end.
import numpy as np
n, q, m, B = 4, 97, 8, 1rng = np.random.default_rng(seed=0)s = rng.integers(0, q, size=n, dtype=np.int64)A = rng.integers(0, q, size=(m, n), dtype=np.int64)e = rng.integers(-B, B + 1, size=m, dtype=np.int64)b = (A @ s + e) % qr = rng.integers(0, 2, size=m, dtype=np.int64)half_q = q // 2
c1 = (A.T @ r) % qc2_one = (int(b @ r) + half_q * 1) % qc2_zero = (int(b @ r) + half_q * 0) % qv_one = (c2_one - int(c1 @ s)) % qv_zero = (c2_zero - int(c1 @ s)) % q
print("s =", s.tolist())print("r =", r.tolist())print("c1 =", c1.tolist())print("c2 for mu=1:", c2_one, "c2 for mu=0:", c2_zero)print("v for mu=1:", v_one, "v for mu=0:", v_zero)# ==> s = [82, 61, 49, 26]# ==> r = [0, 1, 1, 1, 0, 1, 1, 0]# ==> c1 = [44, 50, 54, 74]# ==> c2 for mu=1: 21 c2 for mu=0: 70# ==> v for mu=1: 45 v for mu=0: 94Every Python block this chapter prints is also a standalone file in the companion repository, under chapter-code/ch10/, one file per block. Appendix C covers the clone and the environment they run on.
Noise budget and symmetric representatives
Section titled “Noise budget and symmetric representatives”Two notational conventions carry through the rest of the chapter. The first is symmetric representatives on : every residue is mapped to the signed integer in the half-open interval that shares its residue class. For odd , this is the integer closest to . For , the values map to themselves, and the values map to . The symmetric representative of is ; the symmetric representative of is .
The second convention is the nearest-codeword decoder for the two encodings and . Given , the decoder asks which of and is closer to on the cycle . The integer expression gives the same answer without any floating-point arithmetic. For the decoder returns on the residues and on the residues . The two regions have different widths because is odd, and the decoder breaks the midpoint tie toward the bit . A ciphertext decodes correctly if and only if the decrypted value lands inside the region that contains its encoded value.
The elementary noise bound is
for and . The worst case is achieved when every coordinate of is and every coordinate of is .
Regev’s 2009 theorem uses the rounded error distribution over , derived from a Gaussian on the torus, rather than the bounded uniform error used in the toy code. For bounded uniform errors with , the typical scales as by sub-Gaussian concentration; for Regev’s Gaussian-shaped error distribution, the analogous scale is .
The bounded-uniform typical size sits a factor of below the deterministic worst case . The Gaussian error has no deterministic bound at all, which is why Regev’s correctness statement is a probability rather than an inequality. The Gaussian form is what the worst-case-to-average-case reduction from Chapter 8 requires. The bounded uniform form is useful for toy correctness experiments but is not the distribution covered directly by Regev’s original theorem (Regev, 2009). The uniform worst-case bound is what the walkthrough code uses, and it suffices for decryption correctness at the toy parameters.
Key generation, encryption, decryption
Section titled “Key generation, encryption, decryption”Key generation, encryption, and decryption all live inside . They use only matrix-vector multiplication, modular reduction, and the nearest-codeword decoder for the two encodings and . The blocks below pass the parameters around as loose integers so that each one runs on its own. The regev_pke package under solutions/ch10-regev-pke carries them in a RegevParams dataclass instead, with the noise budget exposed as a method on it.
Key generation. KeyGen takes the parameters and returns a public key together with a secret key . The secret is drawn uniformly from . The matrix is drawn uniformly from . The error is drawn uniformly from . The public vector is in . The public key is the LWE sample , and for the same error distribution it is computationally indistinguishable from uniform under decisional LWE from Chapter 8 (Regev, 2009).
import numpy as np
def keygen(n, q, m, B, rng): s = rng.integers(0, q, size=n, dtype=np.int64) A = rng.integers(0, q, size=(m, n), dtype=np.int64) e = rng.integers(-B, B + 1, size=m, dtype=np.int64) b = (A @ s + e) % q return (A, b), s
rng = np.random.default_rng(seed=0)(A, b), s = keygen(4, 97, 8, 1, rng)
# Recompute A @ s with an explicit Python loop and verify that# b - A s lives in the symmetric interval [-1, 1], which is the# noise bound B = 1 set by the parameters.As_loop = [sum(int(A[i, j]) * int(s[j]) for j in range(4)) % 97 for i in range(8)]residual = [(int(b[i]) - As_loop[i]) % 97 for i in range(8)]e_sym = [v - 97 if v > 48 else v for v in residual]print("s =", s.tolist())print("b - A s (symmetric) =", e_sym)# ==> s = [82, 61, 49, 26]# ==> b - A s (symmetric) = [-1, -1, 0, 0, 0, -1, -1, -1]Encryption. Encrypt takes the public key and a message bit and returns a ciphertext . The sender draws a random . The first coordinate is . The second coordinate is . The ciphertext lives in .
import numpy as np
def encrypt(A, b, bit, q, rng): m = b.shape[0] r = rng.integers(0, 2, size=m, dtype=np.int64) half_q = q // 2 c1 = (A.T @ r) % q c2 = (int(b @ r) + half_q * bit) % q return c1, int(c2)
# Replay the seeded keygen so this block is self-contained.rng = np.random.default_rng(seed=0)n, q, m, B = 4, 97, 8, 1s = rng.integers(0, q, size=n, dtype=np.int64)A = rng.integers(0, q, size=(m, n), dtype=np.int64)e = rng.integers(-B, B + 1, size=m, dtype=np.int64)b = (A @ s + e) % q
c1_one, c2_one = encrypt(A, b, 1, q, rng)print("mu = 1: c1 =", c1_one.tolist(), "c2 =", c2_one)# ==> mu = 1: c1 = [44, 50, 54, 74] c2 = 21Decryption. Decrypt takes the secret key and a ciphertext and returns the decoded bit. It computes and returns . The expression is the nearest-codeword decoder for applied to .
import numpy as np
def decrypt(s, c1, c2, q): v = (int(c2) - int(c1 @ s)) % q half_q = q // 2 return ((2 * v + half_q) // q) % 2
# Replay keygen and produce ciphertexts for both message bits with# the same random r, so the only difference is the encoding shift.rng = np.random.default_rng(seed=0)n, q, m, B = 4, 97, 8, 1s = rng.integers(0, q, size=n, dtype=np.int64)A = rng.integers(0, q, size=(m, n), dtype=np.int64)e = rng.integers(-B, B + 1, size=m, dtype=np.int64)b = (A @ s + e) % qr = rng.integers(0, 2, size=m, dtype=np.int64)c1 = (A.T @ r) % qc2_one = (int(b @ r) + (q // 2) * 1) % qc2_zero = (int(b @ r) + (q // 2) * 0) % q
print("decrypt mu = 1 ->", decrypt(s, c1, c2_one, q))print("decrypt mu = 0 ->", decrypt(s, c1, c2_zero, q))# ==> decrypt mu = 1 -> 1# ==> decrypt mu = 0 -> 0The secret-cancellation identity. Start from the definitions and . Substituting and expanding gives
The expansion uses distributivity and the matrix transpose identity. The subtracted term equals by the same identity. Cancelling the two summands leaves
The decryption input is therefore the encoded bit plus a small noise term, viewed modulo . The multiple of vanishes in before the decoder runs.
The noise budget. The decoder returns correctly whenever lies inside the decoding region of the intended codeword. The two regions are separated by on the cycle, and because is odd they are not perfectly symmetric: the bit- region contains residues and the bit- region contains residues. A clean symmetric sufficient condition that works for both message bits and both signs of the noise is
For large the right side is essentially , and the condition is often written in the asymptotic form . For the symmetric form gives . The actual decoder boundary differs by at most one residue depending on the sign of the noise and the message bit. The symmetric form is the worst-case correctness condition used in the rest of this chapter.
The elementary bound gives for and . Combining the two inequalities produces the bounded-uniform correctness budget
which is sufficient for every honest encryption to decrypt correctly. Regev’s original paper states correctness probabilistically through the distribution of sums of errors rather than through a deterministic worst-case bound (Regev, 2009). The deterministic form above is the bounded-uniform analogue used by the toy code in this chapter.
At the toy parameters the left side is and the right side is , so the budget has a factor-of-three margin. At the parameters the left side is still but the right side is , so the budget is violated. The exact distribution of at those parameters predicts a failure rate of about . The code block below shows an empirical rate of across seeds with both message bits encrypted. A sample of seeds is small for a -rate event, so the empirical and analytical rates differ within typical sample-size variance.
import numpy as np
def keygen(n, q, m, B, rng): s = rng.integers(0, q, size=n, dtype=np.int64) A = rng.integers(0, q, size=(m, n), dtype=np.int64) e = rng.integers(-B, B + 1, size=m, dtype=np.int64) return (A, (A @ s + e) % q), s
def encrypt(A, b, bit, q, rng): m = b.shape[0] r = rng.integers(0, 2, size=m, dtype=np.int64) half_q = q // 2 return (A.T @ r) % q, (int(b @ r) + half_q * bit) % q
def decrypt(s, c1, c2, q): v = (int(c2) - int(c1 @ s)) % q half_q = q // 2 return ((2 * v + half_q) // q) % 2
def failure_rate(n, q, m, B, num_seeds): failures = 0 for seed in range(num_seeds): rng = np.random.default_rng(seed=seed) (A, b), s = keygen(n, q, m, B, rng) for bit in (0, 1): c1, c2 = encrypt(A, b, bit, q, rng) if decrypt(s, c1, c2, q) != bit: failures += 1 return failures / (2 * num_seeds)
print("feasible (n=4, q=97, m=8, B=1):", failure_rate(4, 97, 8, 1, 200))print("infeasible (n=4, q=13, m=8, B=1):", failure_rate(4, 13, 8, 1, 200))# ==> feasible (n=4, q=97, m=8, B=1): 0.0# ==> infeasible (n=4, q=13, m=8, B=1): 0.0675The Ring-LWE / LPR-style descendant. Replacing the flat LWE sample with a Ring-LWE sample in keeps the same cancellation idea but changes the shape of the ciphertext. The construction is no longer literally Regev. Instead of a random -subset sum of many flat LWE samples, it works over with short polynomial secrets and adds fresh encryption errors .
The public key is a single polynomial together with in . Encryption draws a random short polynomial and the two fresh short errors . The ciphertext is . The message polynomial encodes up to bits as coefficients in . Decryption computes , and the noise term is now two small polynomial products plus the fresh error . This two-element scheme is the one the journal version of the Ring-LWE paper presents. The conference version’s example cryptosystem is the dual-style scheme with about ring elements per ciphertext (Lyubashevsky et al., 2013, sec. 1). Chapter 11 walks the Module-LWE refinement that ML-KEM uses and the Fujisaki-Okamoto transform that FIPS 203 applies to its component PKE, yielding a KEM that §3.2 says is believed to satisfy IND-CCA2 security (National Institute of Standards and Technology, 2024).
IND-CPA from decisional LWE
Section titled “IND-CPA from decisional LWE”In the IND-CPA game, the adversary receives the public key, submits two equal-length messages and of its choice, receives an encryption of for a uniformly random bit , and must guess . The scheme is IND-CPA secure if every polynomial-time adversary guesses correctly with probability at most , where is the security parameter (Boneh & Shoup, 2023). For Regev PKE the two messages are the bit and the bit , so the game reduces to: given and an encryption of a random bit , guess .
Security reduces to decisional LWE (DLWE) through a hybrid argument over three games, each changing one thing in the adversary’s view. Write for the adversary’s advantage in Hybrid .
| Hybrid | Public key | Challenge ciphertext | Reached by |
|---|---|---|---|
| 0 | the real scheme | ||
| 1 | uniform in | decisional LWE | |
| 2 | uniform in | uniform in | the leftover hash lemma |
Hybrid 0 to Hybrid 1. A distinguisher for the two hybrids gives a DLWE distinguisher. On input with either or uniform in , installs as the public key. It then samples the IND-CPA challenge bit itself, forms the challenge ciphertext using (encryption does not need the secret), gives the ciphertext to the adversary, and outputs iff the adversary’s final guess equals . If the simulation is exactly Hybrid 0; if is uniform it is Hybrid 1. Any non-negligible gap in the adversary’s success probability between the two hybrids therefore gives a DLWE distinguisher with the same advantage, which Chapter 8 assumes is negligible (Regev, 2009).
Hybrid 1 to Hybrid 2. The adversary’s advantage in Hybrid 2 is exactly zero, because a challenge ciphertext drawn uniformly and independently of carries no information about . The adversary’s view in Hybrid 1 is , with uniform in . Apply the leftover hash lemma to the joint hash . For uniform and uniform , the joint output is -close to uniform on when (Boneh & Shoup, 2023; Regev, 2009). The shift by preserves uniformity, so the Hybrid 1 view is -close to the Hybrid 2 view and the advantage gap is at most .
Chaining the Hybrid 0 to Hybrid 1 to Hybrid 2 transitions bounds the real-world advantage: , where is the distinguishing advantage against decisional LWE and is the leftover hash lemma statistical distance. Both are negligible in under the standard Regev parameterization, so the scheme is IND-CPA secure (Boneh & Shoup, 2023; Regev, 2009).
The proof establishes IND-CPA security only. An IND-CCA2 adversary gets an additional decryption oracle that it can query on any ciphertext except the challenge. Such an adversary can learn partial information about the secret through malformed ciphertexts, and the raw Regev scheme is not IND-CCA2 secure. Chapter 11 moves from PKE to KEMs and adds the Fujisaki-Okamoto-style transform used by ML-KEM: decapsulation decrypts the ciphertext, derives the encryption randomness again from the decrypted value, re-encrypts, and compares the recomputed ciphertext to the received one. On mismatch, ML-KEM performs implicit rejection and returns a pseudorandom fallback key derived from a stored secret rather than an explicit failure symbol. The resulting KEM is believed to satisfy IND-CCA2 under the stated module-lattice assumptions and the FO-style random-oracle / quantum-random-oracle modeling assumptions. Chapter 11 walks that construction and states the Hofheinz-Hovelmanns-Kiltz theorem’s assumptions. Their random-oracle reduction for the implicit-rejection transform is the one FIPS 203 cites, and it builds on the original Fujisaki-Okamoto transform (Fujisaki & Okamoto, 1999; Hofheinz et al., 2017, sec. 3; National Institute of Standards and Technology, 2024, sec. 3.2). Their quantum-random-oracle theorems are proved for a variant that appends a confirmation hash to the ciphertext, which ML-KEM does not carry, so the QROM analysis of ML-KEM’s own transform is not in their paper (Hofheinz et al., 2017, sec. 4).
Tradeoffs inside Part II
Section titled “Tradeoffs inside Part II”Regev’s original flat construction uses only matrix-vector arithmetic over , which is why the noise budget derivation and the IND-CPA proof in this chapter both fit in a page (Regev, 2009). The Ring-LWE framing from Chapter 9 improves public-key size and amortized bandwidth by replacing unstructured flat-LWE linear algebra with structured multiplication by ring elements in . A single ring element acts as a negacyclic convolution operator, so an entire structured linear map is carried by one polynomial.
A strict single-bit comparison is closer to field elements (flat Regev for one bit) versus field elements (the two-element Ring-LWE ciphertext) (Lyubashevsky et al., 2013, sec. 1). The structural gain comes from public-key compression, faster polynomial arithmetic, and message packing of up to bits per ciphertext. The cost is a security assumption that restricts to ideal lattices (Lyubashevsky et al., 2010).
Chapter 11 uses Module-LWE, where the secret is a vector of ring elements in . At this is Ring-LWE; at ring dimension this is flat LWE. ML-KEM chooses and to match a target bandwidth and security level (National Institute of Standards and Technology, 2024). Chapter 13 walks primal and dual lattice attacks against the public LWE samples such a scheme’s key material exposes, and estimates their cost under a stated attack and cost model. It reads those estimates against the published category assessments rather than deriving a security level for arbitrary decisional-LWE parameters.
Chapter 11 is the immediate next step: it rebuilds key generation, encryption, and decryption over Module-LWE, then adds the Fujisaki-Okamoto transform that turns an IND-CPA scheme like this one into a KEM believed to satisfy IND-CCA2 (Hofheinz et al., 2017), standardized as ML-KEM (National Institute of Standards and Technology, 2024).
Exercises
Section titled “Exercises”-
Modify
RegevParamsto accept and measure the decryption failure rate across 1000 seeds with both message bits. Compare the observed rate against the analytical failure rate computed from the exact distribution of at those parameters. -
Extend the single-bit scheme to encrypt a -bit plaintext by running independent ciphertexts with the same public key. Report the ciphertext expansion factor in entries per plaintext bit.
-
Re-derive the noise budget for drawn uniformly from instead of . Show that the worst-case bound remains , while the typical size increases by a constant factor because ternary is nonzero with probability rather than . The signs make the sum symmetric but do not by themselves increase the variance when the error distribution is already symmetric.
-
Explain in two sentences why replacing the encoding shift with breaks the existing nearest-codeword decoder for . State the new noise bound that would make a redesigned nearest-codeword decoder for work. Show that it tightens to roughly .
Worked solutions and editorial notes for these exercises are in Appendix D, Chapter 10. A separate track, for rebuilding rather than reading: the package exercises/ch10-regev-pke has every function the chapter teaches replaced by a stub. Run PQC_IMPL=exercises pytest tests/ch10 to grade your version against the suite that proves the reference one.
References
Section titled “References”Last updated: