Skip to main content

bouncycastle_mlkem/
polynomial.rs

1//! Represents a polynomial over the ML-KEM ring.
2
3use core::ops::{Index, IndexMut};
4
5use crate::aux_functions::{
6    ZETAS, ZETAS_INV, barrett_reduce, montgomery_reduce, mul_mont, ntt_base_mult,
7};
8use crate::mlkem::{N, q};
9
10/// A polynomial over the ML-KEM ring.
11///
12/// Dev note: The following structure does not necessarily need to be declared as public. 
13/// There is no real scenario where this function needs to be called directly.
14/// However, in order to test the Debug and Display traits, it is necessary to use STD, so those
15/// can't be tested from inline tests in this file and the real unit tests are in a different crate.
16/// That's the reason why pub is used.
17///
18/// # ๐Ÿšจ Security ๐Ÿšจ
19/// Polynomials themselves are not inherently secret since sometimes they are part of public keys
20/// and sometimes private keys.
21/// It is the responsibility of the caller to wrap sensitive instances in `Secret<Vector>`.
22#[derive(Clone, Copy)]
23pub struct Polynomial {
24    /// Note: this is exposed publicly only for testing purposes and there is no good reason to use it in production code.
25    pub coeffs: [i16; N],
26}
27
28/// Convenience function to avoid ".0" all over the place.
29impl Index<usize> for Polynomial {
30    type Output = i16;
31
32    fn index(&self, index: usize) -> &Self::Output {
33        &self.coeffs[index]
34    }
35}
36/// Convenience function to avoid ".0" all over the place.
37impl IndexMut<usize> for Polynomial {
38    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
39        &mut self.coeffs[index]
40    }
41}
42
43impl Polynomial {
44    /// Create a new polynomial with all coefficients set to zero.
45    pub const fn new() -> Self {
46        Self { coeffs: [0i16; N] }
47    }
48
49    /// Encodes a 32-byte message `m` into a `Polynomial`, implementing the message 
50    /// encoding step of K-PKE.Encrypt `Decompress_1(ByteDecode_1(m))`, 
51    /// (FIPS 203, Alg. 14). Each message bit becomes one coefficient: `Decompress_1`
52    /// (ยง4.2.1) maps bit `1` to `โŒˆq/2โŒ‰ = (q + 1) / 2 = 1665` (for `q = 3329`) and bit
53    /// `0` to `0`, placing a set bit at the point farthest from `0` to maximize the
54    /// decryption noise margin. The mapping is computed branchlessly (constant-time)
55    /// via a bit-derived all-ones / all-zeros mask, and bits are read LSB-first. This
56    /// is the exact inverse of [`to_msg`].
57    pub(crate) fn from_msg(m: [u8; 32]) -> Self {
58        let mut w = Polynomial::new();
59
60        for (i, b) in m.iter().enumerate() {
61            for j in 0..8 {
62                let mask = -(((*b >> j) & 1) as i16);
63                w[8 * i + j] = mask & ((q + 1) / 2);
64            }
65        }
66
67        w
68    }
69
70    /// Decodes a `Polynomial` into its 32-byte message `m`, implementing the message 
71    /// recovery step of K-PKE.Decrypt `ByteEncode_1(Compress_1(self))`,
72    /// (FIPS 203, Alg. 15). Each coefficient yields one message bit: `Compress_1`
73    /// (ยง4.2.1) sets the bit when the coefficient lies nearer `q/2` than `0`, i.e. in
74    /// the central interval `[833, 2496]` for `q = 3329`. The decision is computed
75    /// branchlessly and the bits are packed LSB-first. 
76    /// Coefficients are expected to already be canonical in `[0, q]`: the unsigned 
77    /// interval test is not periodic mod `q`, so the caller reduces beforehand (`poly_reduce()` 
78    /// in `pke_decrypt`) and no reduction is repeated here.
79    pub(crate) fn to_msg(self) -> [u8; 32] {
80        const LOWER: i32 = q as i32 >> 2;     // โŒŠq/4โŒ‹     = 832
81        const UPPER: i32 = q as i32 - LOWER;  // q - โŒŠq/2โŒ‹ = 2497
82
83        let mut msg = [0u8; 32];
84
85        // Using full reduce() might be expected here.
86        // However, this function is only called by pke_decrypt (see mlkem.rs), which performs a 
87        // reduction on every coefficient of the polynomial immediately prior to the call.
88        // For completeness, testing against the bc-test-data set of KATs shows that everything passes 
89        // without modular reduction.
90        // self.cond_sub_q();
91
92        // for (i, item) in msg.iter_mut().enumerate().take(N/8) {
93        for i in 0..N / 8 {
94            for j in 0..8 {
95                let c_j = self[8 * i + j] as i32;
96                let t = (((LOWER - c_j) & (c_j - UPPER)) >> 31) & 0x01;
97                msg[i] |= (t << j) as u8;
98            }
99        }
100
101        msg
102    }
103
104    // Not currently used. It is left here as a reference since it's useful for debugging if it's 
105    // necessary to output values that are normalized to [0,q] to compare against intermediate results 
106    // from other libraries.
107    // pub(crate) fn conditional_add_q(&mut self) {
108    //     for x in self.0.iter_mut() {
109    //         *x = conditional_add_q(*x);
110    //     }
111    // }
112
113    pub(crate) fn add(&mut self, w: &Self) {
114        for i in 0..N {
115            self[i] += w[i];
116        }
117    }
118
119    pub(crate) fn sub(&mut self, w: &Self) {
120        for i in 0..N {
121            self[i] -= w[i];
122        }
123    }
124
125    pub(crate) fn poly_reduce(&mut self) {
126        for i in 0..N {
127            self[i] = barrett_reduce(self[i]);
128        }
129    }
130
131    /// In-place conversion of all coefficients of a polynomial
132    /// from normal domain to Montgomery domain
133    ///
134    /// Borrowed from:
135    /// https://github.com/pq-crystals/kyber/blob/main/ref/poly.c#L307
136    pub(crate) fn convert_to_mont(&mut self) {
137        const F: i16 = ((1u64 << 32) % q as u64) as i16;
138        for i in 0..N {
139            self[i] = montgomery_reduce((self[i] as i32) * (F as i32));
140        }
141    }
142
143    /// This is an optimized version of
144    ///   ByteEncode_๐‘‘๐‘ฃ( Compress_๐‘‘๐‘ฃ(๐‘ฃ) )
145    /// which packs a single polynomial according to the packing coefficient dv
146    pub(crate) fn compress_poly<const dv: i16>(&self, out: &mut [u8]) {
147        // make sure we have received a dv
148        debug_assert!(dv == 4 || dv == 5);
149
150        // make sure the right size output buffer is given
151        // each of the N i16's will take dv bits
152        debug_assert_eq!(out.len(), N * (dv as usize) / 8);
153
154        let mut t = [0u8; 8];
155        let mut idx = 0;
156
157        // bc-java has a cond_sub_q() here, however, it is not needed
158        // The reason for this is because a modular reduction is performed immediately
159        // prior to calling pack_ciphertext in mlkem.rs
160        // This can be corroborated by running the corresponding unit tests 
161        // let mut s = self.clone();
162        // s.cond_sub_q();
163
164        match dv {
165            4 => {
166                // MLKEM512 and MLKEM768
167                for i in 0..N / 8 {
168                    // fill the temp array t
169                    for (j, item) in t.iter_mut().enumerate() {
170                        *item = ((((self[8 * i + j] as i32) << 4) + (q as i32 / 2)) / (q as i32)
171                            & 15) as u8;
172                    }
173
174                    out[idx] = t[0] | (t[1] << 4);
175                    out[idx + 1] = t[2] | (t[3] << 4);
176                    out[idx + 2] = t[4] | (t[5] << 4);
177                    out[idx + 3] = t[6] | (t[7] << 4);
178                    idx += 4;
179                }
180            }
181            5 => {
182                // MLKEM1024
183                for i in 0..N / 8 {
184                    // fill the temp array t
185                    for (j, item) in t.iter_mut().enumerate() {
186                        *item = (((((self[8 * i + j] as i32) << 5) + (q as i32 / 2)) / (q as i32))
187                            & 31) as u8;
188                    }
189
190                    out[idx] = t[0] | (t[1] << 5);
191                    out[idx + 1] = (t[1] >> 3) | (t[2] << 2) | (t[3] << 7);
192                    out[idx + 2] = (t[3] >> 1) | (t[4] << 4);
193                    out[idx + 3] = (t[4] >> 4) | (t[5] << 1) | (t[6] << 6);
194                    out[idx + 4] = (t[6] >> 2) | (t[7] << 3);
195                    idx += 5;
196                }
197            }
198            _ => unreachable!(),
199        };
200    }
201
202    /// This is an optimized version of
203    /// Decompress_๐‘‘๐‘ฃ( ByteDecode_๐‘‘๐‘ฃ(๐‘2) )
204    /// which unpacks a single polynomial according to the packing coefficient dv
205    pub(crate) fn decompress_poly<const dv: i16>(compressed_v: &[u8]) -> Polynomial {
206        // make sure to received a dv
207        debug_assert!(dv == 4 || dv == 5);
208
209        // make sure the right size output buffer is given
210        // each of the N i16's will take dv bits
211        debug_assert_eq!(compressed_v.len(), N * (dv as usize) / 8);
212
213        let mut v = Polynomial::new();
214
215        let mut idx = 0usize;
216
217        // if self.m_engine.poly_compressed_bytes() == 128 {
218        match dv {
219            4 => {
220                // MLKEM512 and MLKEM768
221                for i in 0..N / 2 {
222                    v[2 * i] =
223                        (((((compressed_v[idx] & 15) as i16) as i32 * (q as i32)) + 8) >> 4) as i16;
224                    v[2 * i + 1] =
225                        (((((compressed_v[idx] >> 4) as i16) as i32 * (q as i32)) + 8) >> 4) as i16;
226                    idx += 1;
227                }
228            }
229            5 => {
230                // MLKEM1024
231                let mut t = [0u8; 8];
232                for i in 0..N / 8 {
233                    t[0] = compressed_v[idx];
234                    t[1] = (compressed_v[idx] >> 5) | (compressed_v[idx + 1] << 3);
235                    t[2] = compressed_v[idx + 1] >> 2;
236                    t[3] = (compressed_v[idx + 1] >> 7) | (compressed_v[idx + 2] << 1);
237                    t[4] = (compressed_v[idx + 2] >> 4) | (compressed_v[idx + 3] << 4);
238                    t[5] = compressed_v[idx + 3] >> 1;
239                    t[6] = (compressed_v[idx + 3] >> 6) | (compressed_v[idx + 4] << 2);
240                    t[7] = compressed_v[idx + 4] >> 3;
241                    idx += 5;
242                    for (j, item) in t.iter_mut().enumerate() {
243                        v[8 * i + j] = (((*item & 31) as i32 * (q as i32) + 16) >> 5) as i16;
244                    }
245                }
246            }
247            _ => unreachable!(),
248        }
249
250        v
251    }
252
253    // Not currently used. It is left here as a reference since it's useful for debugging if it's 
254    // necessary to output values that are normalized to [0,q] to compare against intermediate results 
255    // from other libraries.
256    // pub(crate) fn cond_sub_q(&mut self) {
257    //     for i in 0..N {
258    //         self[i] = cond_sub_q(self[i]);
259    //     }
260    // }
261
262    /// Algorithm 9 NTT(๐‘“)
263    /// Computes the NTT representation ๐‘“_hat of the given polynomial ๐‘“ โˆˆ ๐‘…๐‘ž.
264    /// Input: array ๐‘“ โˆˆ โ„ค256  โ–ท the coefficients of the input polynomial
265    /// Output: array ๐‘“_hat โˆˆ โ„ค256  โ–ท the coefficients of the NTT of the input polynomial
266    /// Note: this is exposed publicly only for testing purposes and there is no good reason to use it in production code.
267    pub fn ntt(&mut self) {
268        let mut len = 128;
269        let mut k = 1;
270
271        while len >= 2 {
272            let mut start = 0;
273            while start < 256 {
274                let zeta = ZETAS[k];
275                k += 1;
276                let mut j = start;
277                while j < start + len {
278                    let t = mul_mont(zeta, self[j + len]);
279                    self[j + len] = self[j] - t;
280                    self[j] += t;
281                    j += 1;
282                }
283                start = j + len;
284            }
285            len >>= 1;
286        }
287    }
288
289    /// Algorithm 10 NTT (๐‘“_hat)
290    /// Computes the polynomial ๐‘“ โˆˆ ๐‘…๐‘ž that corresponds to the given NTT representation ๐‘“ โˆˆ ๐‘‡๐‘ž.
291    /// Input: array ๐‘“ โˆˆ โ„ค_{256}  โ–ท the coefficients of input NTT representation
292    /// Output: array ๐‘“ โˆˆ โ„ค_{256}  โ–ท the coefficients of the inverse NTT of the input
293    /// Note: this is exposed publicly only for testing purposes and there is no good reason to use it in production code.
294    pub fn inv_ntt(&mut self) {
295        // FIPS 203 Alg 10 wants you to copy f_hat into f, and then act on f
296        // but here it is performed in-place in order to optimize memory usage.
297
298        let mut len = 2;
299        let mut k = 0;
300
301        while len <= 128 {
302            let mut start = 0;
303            while start < 256 {
304                let zeta = ZETAS_INV[k];
305                k += 1;
306                let mut j = start;
307                while j < start + len {
308                    let t = self[j];
309                    let u = self[j + len];
310
311                    self[j] = barrett_reduce(t + u);
312                    self[j + len] = mul_mont(zeta, t - u);
313                    j += 1;
314                }
315                start = j + len;
316            }
317            len <<= 1;
318        }
319
320        // 14: ๐‘“ โ† ๐‘“ โ‹… 3303 mod ๐‘ž
321        //   โ–ท multiply every entry by 3303 โ‰ก 128โˆ’1 mod ๐‘ž
322        for i in 0..N {
323            self[i] = mul_mont(self[i], ZETAS_INV[127]);
324        }
325    }
326}
327
328/// Multiplication of two polynomials in NTT domain
329///
330/// Borrowed from:
331/// <https://github.com/pq-crystals/kyber/blob/main/ref/poly.c#L290>
332/// Note: this is exposed publicly only for testing purposes and there is no good reason to use it in production code.
333pub fn base_mult_montgomery(a: &Polynomial, b: &Polynomial) -> Polynomial {
334    let mut r = Polynomial::new();
335
336    for i in 0..(N / 4) {
337        ntt_base_mult(
338            r.coeffs.as_mut(),
339            4 * i,
340            a[4 * i],
341            a[4 * i + 1],
342            b[4 * i],
343            b[4 * i + 1],
344            ZETAS[64 + i],
345        );
346        ntt_base_mult(
347            r.coeffs.as_mut(),
348            4 * i + 2,
349            a[4 * i + 2],
350            a[4 * i + 3],
351            b[4 * i + 2],
352            b[4 * i + 3],
353            -ZETAS[64 + i],
354        );
355    }
356
357    r
358}
359
360// Not currently used. It is left here as a reference since it's useful for debugging if it's 
361// necessary to output values that are normalized to [0,q] to compare against intermediate results 
362// from other libraries.
363// /// if a is in \[-q..0], then it shifts it up by q to be in \[0..q]
364// pub(crate) fn conditional_add_q(a: i16) -> i16 {
365//     a + ((a >> 15) & q)
366// }
367//
368// #[test]
369// /// These are the results it's giving; I'm not sure if these are "correct" or not.
370// fn test_conditional_add_q() {
371//     assert_eq!(conditional_add_q(-q -1), -1);
372//     assert_eq!(conditional_add_q(-q), 0);
373//     assert_eq!(conditional_add_q(-q -2), -2);
374//     assert_eq!(conditional_add_q(-q +1), 1);
375//     assert_eq!(conditional_add_q(-1), q -1);
376//     assert_eq!(conditional_add_q(0), 0);
377//     assert_eq!(conditional_add_q(1), 1);
378//     assert_eq!(conditional_add_q(q -1), q -1);
379//     assert_eq!(conditional_add_q(q), q);
380//     assert_eq!(conditional_add_q(q +1), q +1);
381// }