Skip to main content

quantica/ml_kem/
encode.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Cédric Mesnil <cslashm@pm.me>
3
4// Index-based loops in this module deliberately mirror the reference
5// algorithm (parallel-array access `a[i]`/`b[i]`, or a position-indexed
6// packing/reduction state) and keep the constant-time data layout explicit;
7// the `iter().enumerate()` rewrite would obscure it. Documented, single-lint
8// allow per CLAUDE.md §6 (scoped to this module, not a blanket suppression).
9#![allow(clippy::needless_range_loop)]
10// Complete FIPS-algorithm / countermeasure family: with the module now
11// crate-internal (API re-tiering, v0.2), not every entry point has an
12// in-crate caller in every feature combination. All of them light up under
13// --all-features and are exercised by the KAT suites; keeping the full,
14// audited family beats pruning it per-combo. Documented allow (CLAUDE.md §6).
15#![allow(dead_code)]
16
17//! # Stability (hazmat tier)
18//!
19//! This module is part of the `hazmat` expert building-block layer: it is
20//! public only with the `hazmat` Cargo feature, for KAT/test-vector tooling
21//! and research. Read each function's invariants before use. **No stability,
22//! misuse-resistance, or side-channel promise** applies to this tier — its
23//! API may change in any release. The typed facade is the guaranteed API.
24
25/// Encoding, decoding, compression, and decompression algorithms.
26///
27/// Implements FIPS 203 Section 4.2.1, Algorithms 3-6:
28///
29/// - [`byte_encode`] / [`byte_decode`] -- bit-pack/unpack 256 integers into/from bytes
30/// - [`compress`] / [`decompress`] -- lossy compression between Z_q and Z_{2^d}
31/// - [`compress_poly`] / [`decompress_poly`] -- vectorized compression over full polynomials
32use super::params::{N, Q};
33
34/// Bit-pack 256 integers into `32*d` bytes (Algorithm 5: ByteEncode_d).
35///
36/// Each of the 256 coefficients in `f` is encoded using `d` bits.
37/// For `d < 12`, coefficients are treated modulo `2^d`.
38/// For `d = 12`, coefficients are treated modulo q = 3329.
39///
40/// # Arguments
41///
42/// * `d` - The bit-width per coefficient (1..=12).
43/// * `f` - Array of 256 unsigned 16-bit coefficients.
44/// * `out` - Output buffer of exactly `32 * d` bytes.
45///
46/// # Panics
47///
48/// Debug-asserts that `out.len() == 32 * d`.
49pub fn byte_encode(d: usize, f: &[u16; N], out: &mut [u8]) {
50    debug_assert_eq!(out.len(), 32 * d);
51
52    if d < 12 {
53        // Bit-packing: each coefficient is d bits
54        let mut bit_idx = 0usize;
55        for i in 0..N {
56            let mut val = f[i] as u32;
57            for _ in 0..d {
58                let byte_pos = bit_idx >> 3;
59                let bit_pos = bit_idx & 7;
60                if bit_pos == 0 && byte_pos < out.len() {
61                    out[byte_pos] = 0;
62                }
63                if byte_pos < out.len() {
64                    out[byte_pos] |= ((val & 1) as u8) << bit_pos;
65                }
66                val >>= 1;
67                bit_idx += 1;
68            }
69        }
70    } else {
71        // d = 12: same logic but coefficients are mod q
72        let mut bit_idx = 0usize;
73        for b in out.iter_mut() {
74            *b = 0;
75        }
76        for i in 0..N {
77            let mut val = f[i] as u32;
78            for _ in 0..12 {
79                let byte_pos = bit_idx >> 3;
80                let bit_pos = bit_idx & 7;
81                if byte_pos < out.len() {
82                    out[byte_pos] |= ((val & 1) as u8) << bit_pos;
83                }
84                val >>= 1;
85                bit_idx += 1;
86            }
87        }
88    }
89}
90
91/// Unpack `32*d` bytes into 256 integers (Algorithm 6: ByteDecode_d).
92///
93/// The inverse of [`byte_encode`]. Each coefficient is extracted from `d`
94/// consecutive bits and reduced modulo `2^d` (for `d < 12`) or modulo
95/// q = 3329 (for `d = 12`).
96///
97/// # Arguments
98///
99/// * `d` - The bit-width per coefficient (1..=12).
100/// * `input` - Input buffer of exactly `32 * d` bytes.
101/// * `f` - Output array of 256 unsigned 16-bit coefficients.
102///
103/// # Panics
104///
105/// Debug-asserts that `input.len() == 32 * d`.
106pub fn byte_decode(d: usize, input: &[u8], f: &mut [u16; N]) {
107    debug_assert_eq!(input.len(), 32 * d);
108
109    let m = if d < 12 { 1u32 << d } else { Q as u32 };
110
111    let mut bit_idx = 0usize;
112    for i in 0..N {
113        let mut val = 0u32;
114        for j in 0..d {
115            let byte_pos = bit_idx >> 3;
116            let bit_pos = bit_idx & 7;
117            val |= (((input[byte_pos] >> bit_pos) & 1) as u32) << j;
118            bit_idx += 1;
119        }
120        f[i] = (val % m) as u16;
121    }
122}
123
124/// Lossy compression from Z_q to Z_{2^d} (FIPS 203 eq. 4.7).
125///
126/// Computes `x -> round(2^d / q * x) mod 2^d` using integer-only
127/// arithmetic: `floor((2^d * x + q/2) / q) mod 2^d`.
128///
129/// # Arguments
130///
131/// * `d` - Target bit-width (1..=12).
132/// * `x` - A coefficient in `[0, q-1]`.
133///
134/// # Returns
135///
136/// The compressed value in `[0, 2^d - 1]`.
137#[inline(always)]
138pub fn compress(d: u32, x: u16) -> u16 {
139    // ⌈(2^d / q) · x⌋ = ⌊(2^d · x + q/2) / q⌋ mod 2^d
140    let shifted = ((x as u64) << d) + (Q as u64 / 2);
141    ((shifted / Q as u64) & ((1u64 << d) - 1)) as u16
142}
143
144/// Decompression from Z_{2^d} back to Z_q (FIPS 203 eq. 4.8).
145///
146/// Computes `y -> round(q / 2^d * y)` using integer-only arithmetic:
147/// `floor((q * y + 2^{d-1}) / 2^d)`.
148///
149/// This is the approximate inverse of [`compress`]. The round-trip
150/// introduces a bounded quantization error.
151///
152/// # Arguments
153///
154/// * `d` - Source bit-width (1..=12).
155/// * `y` - A compressed value in `[0, 2^d - 1]`.
156///
157/// # Returns
158///
159/// The decompressed value in `[0, q-1]`.
160#[inline(always)]
161pub fn decompress(d: u32, y: u16) -> u16 {
162    // ⌈(q / 2^d) · y⌋ = ⌊(q · y + 2^(d-1)) / 2^d⌋
163    let val = (Q as u32 * y as u32 + (1u32 << (d - 1))) >> d;
164    val as u16
165}
166
167/// Compress all 256 coefficients of a polynomial from Z_q to Z_{2^d}.
168///
169/// Applies [`compress`] element-wise.
170///
171/// # Arguments
172///
173/// * `d` - Target bit-width.
174/// * `f` - Input polynomial with coefficients in `[0, q-1]`.
175/// * `out` - Output polynomial with coefficients in `[0, 2^d - 1]`.
176pub fn compress_poly(d: u32, f: &[u16; N], out: &mut [u16; N]) {
177    for i in 0..N {
178        out[i] = compress(d, f[i]);
179    }
180}
181
182/// Decompress all 256 coefficients of a polynomial from Z_{2^d} to Z_q.
183///
184/// Applies [`decompress`] element-wise.
185///
186/// # Arguments
187///
188/// * `d` - Source bit-width.
189/// * `f` - Input polynomial with coefficients in `[0, 2^d - 1]`.
190/// * `out` - Output polynomial with coefficients in `[0, q-1]`.
191pub fn decompress_poly(d: u32, f: &[u16; N], out: &mut [u16; N]) {
192    for i in 0..N {
193        out[i] = decompress(d, f[i]);
194    }
195}