Skip to main content

quantica/ml_kem/
kpke.rs

1#[cfg(feature = "sca-protected")]
2use super::MlKemError;
3use super::encode;
4#[cfg(feature = "sca-protected")]
5use super::masked::{self, MaskedPoly};
6use super::ntt;
7/// K-PKE lattice-based public-key encryption component (FIPS 203 Section 5).
8///
9/// Provides the three core algorithms that underlie ML-KEM:
10///
11/// - [`keygen`] -- Algorithm 13: K-PKE.KeyGen
12/// - [`encrypt`] -- Algorithm 14: K-PKE.Encrypt
13/// - [`decrypt`] -- Algorithm 15: K-PKE.Decrypt
14///
15/// All operations are constant-time on secret data. Secret intermediates
16/// (private key polynomials, error vectors, randomness vectors) are zeroized
17/// via volatile writes after use.
18///
19/// When the `sca-protected` feature is enabled, additional side-channel
20/// countermeasures are applied:
21/// - **Masked decryption**: the secret key is split into additive shares
22///   and the inner product is computed share-wise (DPA protection).
23/// - **Shuffled NTT**: butterfly operations on secret data use randomized
24///   ordering (SPA protection).
25use super::params::{N, Params};
26#[cfg(feature = "sca-protected")]
27use super::rng::CryptoRng;
28use super::sample;
29use super::sha3;
30#[cfg(feature = "sca-protected")]
31use super::shuffle;
32
33/// Maximum module rank across all ML-KEM parameter sets.
34const MAX_K: usize = 4;
35/// Maximum PRF output size: 64 * max(eta) = 64 * 3 = 192 bytes.
36const MAX_PRF_LEN: usize = 192;
37
38/// Generate a K-PKE key pair (Algorithm 13).
39///
40/// From a 32-byte seed `d`, derives the public matrix A (in NTT domain),
41/// secret vector s, and error vector e using SHA3-512 and SHAKE256-based
42/// CBD sampling. Writes the encoded encapsulation key `ek_pke = ByteEncode_12(t_hat) || rho`
43/// into `ek_out` and the encoded decapsulation key `dk_pke = ByteEncode_12(s_hat)`
44/// into `dk_out`.
45///
46/// Secret polynomials `s` and `e` are zeroized after use.
47///
48/// # Arguments
49///
50/// * `d` - 32-byte seed for deterministic key generation.
51/// * `ek_out` - Output slice for ek_pke, must be at least `384*k + 32` bytes.
52/// * `dk_out` - Output slice for dk_pke, must be at least `384*k` bytes.
53///
54/// # Returns
55///
56/// A tuple `(ek_len, dk_len)` with the actual lengths written.
57pub fn keygen<P: Params>(d: &[u8; 32], ek_out: &mut [u8], dk_out: &mut [u8]) -> (usize, usize) {
58    let k = P::K;
59    let ek_len = 384 * k + 32;
60    let dk_len = 384 * k;
61
62    // (ρ, σ) ← G(d || k)
63    let mut g_input = [0u8; 33];
64    g_input[..32].copy_from_slice(d);
65    g_input[32] = k as u8;
66    let (rho, sigma) = sha3::g(&g_input);
67    ntt::zeroize_bytes(&mut g_input);
68
69    // Generate matrix  (in NTT domain) — public data
70    let mut a_hat = [[0i16; N]; MAX_K * MAX_K];
71    for i in 0..k {
72        for j in 0..k {
73            let mut seed = [0u8; 34];
74            seed[..32].copy_from_slice(&rho);
75            seed[32] = j as u8;
76            seed[33] = i as u8;
77            a_hat[i * k + j] = sample::sample_ntt(&seed);
78        }
79    }
80
81    // Generate s (secret) and e (secret) from CBD
82    let mut n_counter = 0u8;
83    let mut s_hat = [[0i16; N]; MAX_K];
84    let mut prf_buf = [0u8; MAX_PRF_LEN];
85    for i in 0..k {
86        sha3::prf(P::ETA1, &sigma, n_counter, &mut prf_buf);
87        s_hat[i] = sample::sample_poly_cbd(P::ETA1, &prf_buf[..64 * P::ETA1]);
88        ntt::zeroize_bytes(&mut prf_buf[..64 * P::ETA1]);
89        ntt::ntt(&mut s_hat[i]);
90        n_counter += 1;
91    }
92
93    let mut e_hat = [[0i16; N]; MAX_K];
94    for i in 0..k {
95        sha3::prf(P::ETA1, &sigma, n_counter, &mut prf_buf);
96        e_hat[i] = sample::sample_poly_cbd(P::ETA1, &prf_buf[..64 * P::ETA1]);
97        ntt::zeroize_bytes(&mut prf_buf[..64 * P::ETA1]);
98        ntt::ntt(&mut e_hat[i]);
99        n_counter += 1;
100    }
101
102    // t̂ = Â ∘ ŝ + ê (all in NTT domain)
103    // multiply_ntts output is in /R domain (Montgomery basemul).
104    // to_mont_poly converts /R → normal before adding ê (normal domain from NTT).
105    let mut t_hat = [[0i16; N]; MAX_K];
106    for i in 0..k {
107        for j in 0..k {
108            let mut tmp = [0i16; N];
109            ntt::multiply_ntts(&a_hat[i * k + j], &s_hat[j], &mut tmp);
110            for l in 0..N {
111                t_hat[i][l] = t_hat[i][l] + tmp[l];
112            }
113        }
114        ntt::to_mont_poly(&mut t_hat[i]); // /R → normal
115        for l in 0..N {
116            t_hat[i][l] = t_hat[i][l] + e_hat[i][l];
117        }
118    }
119
120    // ek_pke = ByteEncode_12(t̂) || ρ
121    for i in 0..k {
122        let mut t_u16 = [0u16; N];
123        for l in 0..N {
124            t_u16[l] = ntt::barrett_reduce(t_hat[i][l]) as u16;
125        }
126        encode::byte_encode(12, &t_u16, &mut ek_out[384 * i..384 * (i + 1)]);
127    }
128    ek_out[384 * k..384 * k + 32].copy_from_slice(&rho);
129
130    // dk_pke = ByteEncode_12(ŝ)
131    for i in 0..k {
132        let mut s_u16 = [0u16; N];
133        for l in 0..N {
134            s_u16[l] = ntt::barrett_reduce(s_hat[i][l]) as u16;
135        }
136        encode::byte_encode(12, &s_u16, &mut dk_out[384 * i..384 * (i + 1)]);
137    }
138
139    // Zeroize secrets
140    for poly in s_hat[..k].iter_mut() {
141        ntt::zeroize_poly(poly);
142    }
143    for poly in e_hat[..k].iter_mut() {
144        ntt::zeroize_poly(poly);
145    }
146
147    (ek_len, dk_len)
148}
149
150/// SCA-protected K-PKE key generation with shuffled NTT on secret polynomials.
151///
152/// Functionally identical to [`keygen`] but uses [`shuffle::ntt_shuffled`] for
153/// the forward NTT on secret polynomials `s` and `e`, randomizing the butterfly
154/// execution order to defeat Simple Power Analysis.
155///
156/// The NTT on public data (matrix A via `SampleNTT`) is unaffected.
157///
158/// # Arguments
159///
160/// * `d` - 32-byte seed for deterministic key generation.
161/// * `ek_out` - Output slice for ek_pke, must be at least `384*k + 32` bytes.
162/// * `dk_out` - Output slice for dk_pke, must be at least `384*k` bytes.
163/// * `rng` - A cryptographic RNG for shuffle permutation randomness.
164///
165/// # Errors
166///
167/// Returns [`MlKemError::RngFailure`] if the RNG fails.
168#[cfg(feature = "sca-protected")]
169pub fn keygen_sca<P: Params>(
170    d: &[u8; 32],
171    ek_out: &mut [u8],
172    dk_out: &mut [u8],
173    rng: &mut impl CryptoRng,
174) -> Result<(usize, usize), MlKemError> {
175    let k = P::K;
176    let ek_len = 384 * k + 32;
177    let dk_len = 384 * k;
178
179    // (ρ, σ) ← G(d || k)
180    let mut g_input = [0u8; 33];
181    g_input[..32].copy_from_slice(d);
182    g_input[32] = k as u8;
183    let (rho, sigma) = sha3::g(&g_input);
184    ntt::zeroize_bytes(&mut g_input);
185
186    // Generate matrix  (in NTT domain) — public data, no shuffling needed
187    let mut a_hat = [[0i16; N]; MAX_K * MAX_K];
188    for i in 0..k {
189        for j in 0..k {
190            let mut seed = [0u8; 34];
191            seed[..32].copy_from_slice(&rho);
192            seed[32] = j as u8;
193            seed[33] = i as u8;
194            a_hat[i * k + j] = sample::sample_ntt(&seed);
195        }
196    }
197
198    // Generate s (secret) and e (secret) from CBD — shuffled NTT for SPA protection
199    let mut n_counter = 0u8;
200    let mut s_hat = [[0i16; N]; MAX_K];
201    let mut prf_buf = [0u8; MAX_PRF_LEN];
202    for i in 0..k {
203        sha3::prf(P::ETA1, &sigma, n_counter, &mut prf_buf);
204        s_hat[i] = sample::sample_poly_cbd(P::ETA1, &prf_buf[..64 * P::ETA1]);
205        ntt::zeroize_bytes(&mut prf_buf[..64 * P::ETA1]);
206        shuffle::ntt_shuffled(&mut s_hat[i], rng)?;
207        n_counter += 1;
208    }
209
210    let mut e_hat = [[0i16; N]; MAX_K];
211    for i in 0..k {
212        sha3::prf(P::ETA1, &sigma, n_counter, &mut prf_buf);
213        e_hat[i] = sample::sample_poly_cbd(P::ETA1, &prf_buf[..64 * P::ETA1]);
214        ntt::zeroize_bytes(&mut prf_buf[..64 * P::ETA1]);
215        shuffle::ntt_shuffled(&mut e_hat[i], rng)?;
216        n_counter += 1;
217    }
218
219    // t̂ = Â ∘ ŝ + ê (all in NTT domain)
220    let mut t_hat = [[0i16; N]; MAX_K];
221    for i in 0..k {
222        for j in 0..k {
223            let mut tmp = [0i16; N];
224            ntt::multiply_ntts(&a_hat[i * k + j], &s_hat[j], &mut tmp);
225            for l in 0..N {
226                t_hat[i][l] = t_hat[i][l] + tmp[l];
227            }
228        }
229        ntt::to_mont_poly(&mut t_hat[i]);
230        for l in 0..N {
231            t_hat[i][l] = t_hat[i][l] + e_hat[i][l];
232        }
233    }
234
235    // ek_pke = ByteEncode_12(t̂) || ρ
236    for i in 0..k {
237        let mut t_u16 = [0u16; N];
238        for l in 0..N {
239            t_u16[l] = ntt::barrett_reduce(t_hat[i][l]) as u16;
240        }
241        encode::byte_encode(12, &t_u16, &mut ek_out[384 * i..384 * (i + 1)]);
242    }
243    ek_out[384 * k..384 * k + 32].copy_from_slice(&rho);
244
245    // dk_pke = ByteEncode_12(ŝ)
246    for i in 0..k {
247        let mut s_u16 = [0u16; N];
248        for l in 0..N {
249            s_u16[l] = ntt::barrett_reduce(s_hat[i][l]) as u16;
250        }
251        encode::byte_encode(12, &s_u16, &mut dk_out[384 * i..384 * (i + 1)]);
252    }
253
254    // Zeroize secrets
255    for poly in s_hat[..k].iter_mut() {
256        ntt::zeroize_poly(poly);
257    }
258    for poly in e_hat[..k].iter_mut() {
259        ntt::zeroize_poly(poly);
260    }
261
262    Ok((ek_len, dk_len))
263}
264
265/// Encrypt a 32-byte message under a K-PKE public key (Algorithm 14).
266///
267/// Computes `u = NTT_inv(A^T * y_hat) + e1` and
268/// `v = NTT_inv(t_hat^T * y_hat) + e2 + Decompress_1(m)`, then compresses
269/// and encodes both into a ciphertext of [`Params::CT_LEN`] bytes.
270///
271/// The randomness vectors `y`, `e1`, and `e2` are derived deterministically
272/// from the 32-byte seed `r` via SHAKE256-based CBD sampling, and are
273/// zeroized after use.
274///
275/// # Arguments
276///
277/// * `ek_pke` - The K-PKE encapsulation key (`ByteEncode_12(t_hat) || rho`).
278/// * `m` - The 32-byte message to encrypt (one bit per coefficient).
279/// * `r` - 32-byte encryption randomness seed.
280/// * `ct_out` - Output slice for the ciphertext, must be at least `P::CT_LEN` bytes.
281///
282/// # Returns
283///
284/// The actual ciphertext length written.
285pub fn encrypt<P: Params>(ek_pke: &[u8], m: &[u8; 32], r: &[u8; 32], ct_out: &mut [u8]) -> usize {
286    let k = P::K;
287    let du = P::DU;
288    let dv = P::DV;
289
290    // Decode t̂
291    let mut t_hat = [[0i16; N]; MAX_K];
292    for i in 0..k {
293        let mut t_decoded = [0u16; N];
294        encode::byte_decode(12, &ek_pke[384 * i..384 * (i + 1)], &mut t_decoded);
295        for l in 0..N {
296            t_hat[i][l] = t_decoded[l] as i16;
297        }
298    }
299
300    // Extract ρ and re-generate Â
301    let rho = &ek_pke[384 * k..384 * k + 32];
302    let mut a_hat = [[0i16; N]; MAX_K * MAX_K];
303    for i in 0..k {
304        for j in 0..k {
305            let mut seed = [0u8; 34];
306            seed[..32].copy_from_slice(rho);
307            seed[32] = j as u8;
308            seed[33] = i as u8;
309            a_hat[i * k + j] = sample::sample_ntt(&seed);
310        }
311    }
312
313    // Generate y, e1, e2 from CBD
314    let mut n_counter = 0u8;
315    let mut y_hat = [[0i16; N]; MAX_K];
316    let mut prf_buf = [0u8; MAX_PRF_LEN];
317    for i in 0..k {
318        sha3::prf(P::ETA1, r, n_counter, &mut prf_buf);
319        y_hat[i] = sample::sample_poly_cbd(P::ETA1, &prf_buf[..64 * P::ETA1]);
320        ntt::zeroize_bytes(&mut prf_buf[..64 * P::ETA1]);
321        ntt::ntt(&mut y_hat[i]);
322        n_counter += 1;
323    }
324
325    let mut e1 = [[0i16; N]; MAX_K];
326    for i in 0..k {
327        sha3::prf(P::ETA2, r, n_counter, &mut prf_buf);
328        e1[i] = sample::sample_poly_cbd(P::ETA2, &prf_buf[..64 * P::ETA2]);
329        ntt::zeroize_bytes(&mut prf_buf[..64 * P::ETA2]);
330        n_counter += 1;
331    }
332
333    sha3::prf(P::ETA2, r, n_counter, &mut prf_buf);
334    let mut e2 = sample::sample_poly_cbd(P::ETA2, &prf_buf[..64 * P::ETA2]);
335    ntt::zeroize_bytes(&mut prf_buf[..64 * P::ETA2]);
336
337    // u = NTT⁻¹(Â^T ∘ ŷ) + e1
338    let mut u = [[0i16; N]; MAX_K];
339    for i in 0..k {
340        let mut acc = [0i16; N];
341        for j in 0..k {
342            let mut tmp = [0i16; N];
343            ntt::multiply_ntts(&a_hat[j * k + i], &y_hat[j], &mut tmp);
344            for l in 0..N {
345                acc[l] = acc[l].wrapping_add(tmp[l]);
346            }
347        }
348        ntt::ntt_inv(&mut acc);
349        ntt::poly_add(&acc, &e1[i], &mut u[i]);
350    }
351
352    // Decode message
353    let mut mu = [0u16; N];
354    encode::byte_decode(1, m, &mut mu);
355
356    // v = NTT⁻¹(t̂^T ∘ ŷ) + e2 + Decompress_1(mu)
357    let mut v = [0i16; N];
358    {
359        let mut acc = [0i16; N];
360        for j in 0..k {
361            let mut tmp = [0i16; N];
362            ntt::multiply_ntts(&t_hat[j], &y_hat[j], &mut tmp);
363            for l in 0..N {
364                acc[l] = acc[l].wrapping_add(tmp[l]);
365            }
366        }
367        ntt::ntt_inv(&mut acc);
368        for l in 0..N {
369            let mu_dec = encode::decompress(1, mu[l]) as i16;
370            v[l] = acc[l].wrapping_add(e2[l]).wrapping_add(mu_dec);
371        }
372    }
373
374    // Compress and encode
375    let ct_len = 32 * (du * k + dv);
376
377    for i in 0..k {
378        let mut u_comp = [0u16; N];
379        for l in 0..N {
380            u_comp[l] = encode::compress(du as u32, ntt::barrett_reduce(u[i][l]) as u16);
381        }
382        encode::byte_encode(du, &u_comp, &mut ct_out[32 * du * i..32 * du * (i + 1)]);
383    }
384
385    let c2_off = 32 * du * k;
386    let mut v_comp = [0u16; N];
387    for l in 0..N {
388        v_comp[l] = encode::compress(dv as u32, ntt::barrett_reduce(v[l]) as u16);
389    }
390    encode::byte_encode(dv, &v_comp, &mut ct_out[c2_off..c2_off + 32 * dv]);
391
392    // Zeroize secrets
393    for poly in y_hat[..k].iter_mut() {
394        ntt::zeroize_poly(poly);
395    }
396    for poly in e1[..k].iter_mut() {
397        ntt::zeroize_poly(poly);
398    }
399    ntt::zeroize_poly(&mut e2);
400
401    ct_len
402}
403
404/// Decrypt a K-PKE ciphertext to recover the 32-byte message (Algorithm 15).
405///
406/// Decompresses `u` and `v` from the ciphertext, decodes the secret key `s`,
407/// then computes `w = v - NTT_inv(s_hat^T * NTT(u))` and compresses `w`
408/// to recover the original one-bit-per-coefficient message.
409///
410/// The secret key polynomial `s_hat` and the accumulator are zeroized
411/// after use.
412///
413/// # Arguments
414///
415/// * `dk_pke` - The K-PKE decapsulation key (`ByteEncode_12(s_hat)`).
416/// * `c` - The ciphertext.
417///
418/// # Returns
419///
420/// The recovered 32-byte message.
421pub fn decrypt<P: Params>(dk_pke: &[u8], c: &[u8]) -> [u8; 32] {
422    let k = P::K;
423    let du = P::DU;
424    let dv = P::DV;
425
426    // Decompress u
427    let mut u = [[0i16; N]; MAX_K];
428    for i in 0..k {
429        let mut u_comp = [0u16; N];
430        encode::byte_decode(du, &c[32 * du * i..32 * du * (i + 1)], &mut u_comp);
431        for l in 0..N {
432            u[i][l] = encode::decompress(du as u32, u_comp[l]) as i16;
433        }
434    }
435
436    // Decompress v
437    let c2_off = 32 * du * k;
438    let mut v_comp = [0u16; N];
439    encode::byte_decode(dv, &c[c2_off..c2_off + 32 * dv], &mut v_comp);
440    let mut v = [0i16; N];
441    for l in 0..N {
442        v[l] = encode::decompress(dv as u32, v_comp[l]) as i16;
443    }
444
445    // Decode ŝ
446    let mut s_hat = [[0i16; N]; MAX_K];
447    for i in 0..k {
448        let mut s_dec = [0u16; N];
449        encode::byte_decode(12, &dk_pke[384 * i..384 * (i + 1)], &mut s_dec);
450        for l in 0..N {
451            s_hat[i][l] = s_dec[l] as i16;
452        }
453    }
454
455    // NTT(u)
456    for poly in u[..k].iter_mut() {
457        ntt::ntt(poly);
458    }
459
460    // ŝ^T ∘ NTT(u)
461    let mut acc = [0i16; N];
462    for j in 0..k {
463        let mut tmp = [0i16; N];
464        ntt::multiply_ntts(&s_hat[j], &u[j], &mut tmp);
465        for l in 0..N {
466            acc[l] = acc[l].wrapping_add(tmp[l]);
467        }
468    }
469    ntt::reduce(&mut acc);
470    ntt::ntt_inv(&mut acc);
471
472    // w = v - acc
473    let mut w = [0i16; N];
474    ntt::poly_sub(&v, &acc, &mut w);
475
476    // m = ByteEncode_1(Compress_1(w))
477    let mut w_comp = [0u16; N];
478    for l in 0..N {
479        w_comp[l] = encode::compress(1, ntt::barrett_reduce(w[l]) as u16);
480    }
481    let mut m = [0u8; 32];
482    encode::byte_encode(1, &w_comp, &mut m);
483
484    // Zeroize secrets
485    for poly in s_hat[..k].iter_mut() {
486        ntt::zeroize_poly(poly);
487    }
488    ntt::zeroize_poly(&mut acc);
489
490    m
491}
492
493/// SCA-protected K-PKE decryption with masked secret key and shuffled NTT.
494///
495/// Functionally identical to [`decrypt`] but applies two side-channel
496/// countermeasures on the critical `ŝ^T · NTT(u)` inner product:
497///
498/// 1. **Shuffled NTT**: the forward NTT on each ciphertext polynomial `u[i]`
499///    uses randomized butterfly ordering to defeat Simple Power Analysis.
500/// 2. **Masked multiplication**: the secret key `ŝ` is split into two additive
501///    shares `(s0, s1)` and the inner product is computed as
502///    `s0·NTT(u) + s1·NTT(u)`, so no single intermediate reveals the secret
503///    (Differential Power Analysis protection).
504///
505/// The RNG is needed for both generating mask shares and shuffle permutations.
506///
507/// # Arguments
508///
509/// * `dk_pke` - The K-PKE decapsulation key (`ByteEncode_12(s_hat)`).
510/// * `c` - The ciphertext.
511/// * `rng` - A cryptographic RNG for masking and shuffle randomness.
512///
513/// # Errors
514///
515/// Returns [`MlKemError::RngFailure`] if the RNG fails.
516#[cfg(feature = "sca-protected")]
517pub fn decrypt_sca<P: Params>(dk_pke: &[u8], c: &[u8], rng: &mut impl CryptoRng) -> Result<[u8; 32], MlKemError> {
518    let k = P::K;
519    let du = P::DU;
520    let dv = P::DV;
521
522    // Decompress u
523    let mut u = [[0i16; N]; MAX_K];
524    for i in 0..k {
525        let mut u_comp = [0u16; N];
526        encode::byte_decode(du, &c[32 * du * i..32 * du * (i + 1)], &mut u_comp);
527        for l in 0..N {
528            u[i][l] = encode::decompress(du as u32, u_comp[l]) as i16;
529        }
530    }
531
532    // Decompress v
533    let c2_off = 32 * du * k;
534    let mut v_comp = [0u16; N];
535    encode::byte_decode(dv, &c[c2_off..c2_off + 32 * dv], &mut v_comp);
536    let mut v = [0i16; N];
537    for l in 0..N {
538        v[l] = encode::decompress(dv as u32, v_comp[l]) as i16;
539    }
540
541    // Decode ŝ
542    let mut s_hat = [[0i16; N]; MAX_K];
543    for i in 0..k {
544        let mut s_dec = [0u16; N];
545        encode::byte_decode(12, &dk_pke[384 * i..384 * (i + 1)], &mut s_dec);
546        for l in 0..N {
547            s_hat[i][l] = s_dec[l] as i16;
548        }
549    }
550
551    // NTT(u) — shuffled NTT for SPA protection (u is derived from ciphertext
552    // but the access pattern during NTT can leak info about the secret when
553    // combined with the subsequent multiplication)
554    for poly in u[..k].iter_mut() {
555        shuffle::ntt_shuffled(poly, rng)?;
556    }
557
558    // Masked inner product: split ŝ into shares, compute each share's
559    // contribution independently.
560    // s_hat is in NTT domain (Barrett-reduced coefficients in [0, q-1]),
561    // so MaskedPoly::mask works directly.
562    let mut acc_masked = MaskedPoly {
563        share0: [0i16; N],
564        share1: [0i16; N],
565    };
566    for j in 0..k {
567        let s_masked = MaskedPoly::mask(&s_hat[j], rng)?;
568        masked::masked_multiply_accumulate(&mut acc_masked, &s_masked, &u[j]);
569    }
570
571    // Unmask and reduce
572    let mut acc = acc_masked.unmask();
573    acc_masked.zeroize();
574    ntt::reduce(&mut acc);
575    ntt::ntt_inv(&mut acc);
576
577    // w = v - acc
578    let mut w = [0i16; N];
579    ntt::poly_sub(&v, &acc, &mut w);
580
581    // m = ByteEncode_1(Compress_1(w))
582    let mut w_comp = [0u16; N];
583    for l in 0..N {
584        w_comp[l] = encode::compress(1, ntt::barrett_reduce(w[l]) as u16);
585    }
586    let mut m = [0u8; 32];
587    encode::byte_encode(1, &w_comp, &mut m);
588
589    // Zeroize secrets
590    for poly in s_hat[..k].iter_mut() {
591        ntt::zeroize_poly(poly);
592    }
593    ntt::zeroize_poly(&mut acc);
594
595    Ok(m)
596}