Skip to main content

dcrypt_algorithms/poly/
polynomial.rs

1//! polynomial.rs - Enhanced implementation with arithmetic operations
2
3#[cfg(feature = "alloc")]
4extern crate alloc;
5#[cfg(feature = "alloc")]
6use alloc::{boxed::Box, vec};
7
8use super::ntt::montgomery_reduce;
9use super::params::{Modulus, NttModulus}; // FIXED: Import NttModulus from params
10use crate::error::{Error, Result};
11use core::marker::PhantomData;
12use core::ops::{Add, Neg, Sub};
13use dcrypt_internal::zeroing::{Zeroize, ZeroizeOnDrop, Zeroizing};
14
15/// Convert a value from standard domain to Montgomery domain
16#[inline(always)]
17fn to_montgomery<M: NttModulus>(val: u32) -> u32 {
18    ((val as u64 * M::MONT_R as u64) % M::Q as u64) as u32
19}
20
21/// A polynomial in a ring `R_Q = Z_Q[X]/(X^N + 1)`
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct Polynomial<M: Modulus> {
24    /// Coefficients of the polynomial, stored in standard representation
25    #[cfg(feature = "alloc")]
26    pub coeffs: Box<[u32]>,
27    /// Coefficients of the polynomial, stored in standard representation
28    #[cfg(not(feature = "alloc"))]
29    pub coeffs: [u32; 256], // Will need const generics for proper support
30    _marker: PhantomData<M>,
31}
32
33// Custom Zeroize implementation that preserves vector length
34impl<M: Modulus> Zeroize for Polynomial<M> {
35    fn zeroize(&mut self) {
36        // Zero all coefficients without changing the length
37        #[cfg(feature = "alloc")]
38        {
39            self.coeffs.as_mut().zeroize();
40        }
41        #[cfg(not(feature = "alloc"))]
42        {
43            self.coeffs.zeroize();
44        }
45    }
46}
47
48impl<M: Modulus> Drop for Polynomial<M> {
49    fn drop(&mut self) {
50        self.zeroize();
51    }
52}
53
54impl<M: Modulus> ZeroizeOnDrop for Polynomial<M> {}
55
56impl<M: Modulus> Polynomial<M> {
57    /// Creates a new polynomial with all coefficients set to zero
58    pub fn zero() -> Self {
59        Self {
60            #[cfg(feature = "alloc")]
61            coeffs: vec![0; M::N].into_boxed_slice(),
62            #[cfg(not(feature = "alloc"))]
63            coeffs: [0; 256],
64            _marker: PhantomData,
65        }
66    }
67
68    /// Creates a polynomial from a slice of coefficients
69    pub fn from_coeffs(coeffs_slice: &[u32]) -> Result<Self> {
70        if coeffs_slice.len() != M::N {
71            return Err(Error::Parameter {
72                name: "coeffs_slice".into(),
73                reason: "Incorrect number of coefficients for polynomial degree N".into(),
74            });
75        }
76
77        #[cfg(feature = "alloc")]
78        let coeffs = Box::from(coeffs_slice);
79
80        #[cfg(not(feature = "alloc"))]
81        let mut coeffs = [0u32; 256];
82        #[cfg(not(feature = "alloc"))]
83        coeffs[..M::N].copy_from_slice(coeffs_slice);
84
85        Ok(Self {
86            coeffs,
87            _marker: PhantomData,
88        })
89    }
90
91    /// Returns the degree N of the polynomial
92    pub fn degree() -> usize {
93        M::N
94    }
95
96    /// Returns the modulus Q for coefficient arithmetic
97    pub fn modulus_q() -> u32 {
98        M::Q
99    }
100
101    /// Returns a slice view of the coefficients
102    pub fn as_coeffs_slice(&self) -> &[u32] {
103        &self.coeffs[..M::N]
104    }
105
106    /// Returns a mutable slice view of the coefficients
107    pub fn as_mut_coeffs_slice(&mut self) -> &mut [u32] {
108        &mut self.coeffs[..M::N]
109    }
110
111    /// Branch-free modular reduction of a single coefficient
112    #[inline(always)]
113    fn reduce_coefficient(a: u32) -> u32 {
114        // Branch-free reduction: a - Q if a >= Q else a
115        let q = M::Q;
116        let mask = ((a >= q) as u32).wrapping_neg();
117        a.wrapping_sub(q & mask)
118    }
119
120    /// Branch-free conditional subtraction for signed results
121    /// FIXED: Simplified to use rem_euclid for proper modular arithmetic
122    #[inline(always)]
123    fn conditional_sub_q(a: i64) -> u32 {
124        let q = M::Q as i64;
125        // Use rem_euclid for proper modular arithmetic
126        a.rem_euclid(q) as u32
127    }
128
129    /// Polynomial addition modulo Q
130    pub fn add(&self, other: &Self) -> Self {
131        let mut result = Self::zero();
132        for i in 0..M::N {
133            let sum = self.coeffs[i].wrapping_add(other.coeffs[i]);
134            result.coeffs[i] = Self::reduce_coefficient(sum);
135        }
136        result
137    }
138
139    /// Polynomial subtraction modulo Q
140    pub fn sub(&self, other: &Self) -> Self {
141        let mut result = Self::zero();
142        for i in 0..M::N {
143            let diff = (self.coeffs[i] as i64) - (other.coeffs[i] as i64);
144            result.coeffs[i] = Self::conditional_sub_q(diff);
145        }
146        result
147    }
148
149    /// Polynomial negation modulo Q
150    pub fn neg(&self) -> Self {
151        let mut result = Self::zero();
152        for i in 0..M::N {
153            // Mask is 0xFFFF_FFFF when coeff ≠ 0, 0 otherwise
154            let mask = ((self.coeffs[i] != 0) as u32).wrapping_neg();
155            result.coeffs[i] = (M::Q - self.coeffs[i]) & mask;
156        }
157        result
158    }
159
160    /// Scalar multiplication
161    pub fn scalar_mul(&self, scalar: u32) -> Self {
162        let mut result = Self::zero();
163        for i in 0..M::N {
164            let prod = (self.coeffs[i] as u64) * (scalar as u64);
165            result.coeffs[i] = (prod % M::Q as u64) as u32;
166        }
167        result
168    }
169
170    /// Schoolbook polynomial multiplication with NEGACYCLIC reduction for ML-DSA
171    /// In ring `R_q[x]/(x^N + 1)`, when degree >= N, we have `x^N ≡ -1`
172    pub fn schoolbook_mul(&self, other: &Self) -> Self {
173        let mut result = Self::zero();
174        let n = M::N;
175        let q = M::Q as u64;
176
177        // Use a temporary array to accumulate products without modular reduction
178        // This prevents overflow: max value is n * (q-1)^2 < 2^64 for ML-DSA
179        let mut tmp = Zeroizing::new(vec![0u64; 2 * n].into_boxed_slice());
180
181        // Step 1: Compute full convolution without modular reduction
182        // FIXED: Use iterator instead of indexing
183        for (i, &ai_u32) in self.coeffs.iter().enumerate().take(n) {
184            let ai = ai_u32 as u64;
185            for (j, &bj_u32) in other.coeffs.iter().enumerate().take(n) {
186                let bj = bj_u32 as u64;
187                tmp[i + j] = tmp[i + j].wrapping_add(ai * bj);
188            }
189        }
190
191        // Step 2: Apply negacyclic reduction (x^N = -1)
192        // Fold upper half back with negation
193        for k in n..(2 * n) {
194            // When reducing x^k where k >= n, we use x^n = -1
195            // So x^k = -x^(k-n)
196            let upper_val = tmp[k] % q;
197            if upper_val > 0 {
198                // Subtract from lower coefficient (equivalent to adding the negative)
199                tmp[k - n] = (tmp[k - n] + q - upper_val) % q;
200            }
201        }
202
203        // Step 3: Final reduction to [0, q)
204        #[allow(clippy::needless_range_loop)]
205        // We need indexed access here to match tmp and result.coeffs indices
206        for i in 0..n {
207            result.coeffs[i] = (tmp[i] % q) as u32;
208        }
209
210        result
211    }
212
213    /// In-place coefficient reduction to ensure all coefficients are < Q
214    pub fn reduce_coeffs(&mut self) {
215        for i in 0..M::N {
216            self.coeffs[i] = Self::reduce_coefficient(self.coeffs[i]);
217        }
218    }
219}
220
221// NTT operations are implemented in ntt.rs as extension methods
222
223/// Extension trait for polynomials with NTT-enabled modulus
224pub trait PolynomialNttExt<M: NttModulus> {
225    // FIXED: Now uses params::NttModulus
226    /// Fast scalar multiplication using Montgomery reduction
227    fn scalar_mul_montgomery(&self, scalar: u32) -> Polynomial<M>;
228}
229
230impl<M: NttModulus> PolynomialNttExt<M> for Polynomial<M> {
231    // FIXED: Now uses params::NttModulus
232    fn scalar_mul_montgomery(&self, scalar: u32) -> Polynomial<M> {
233        let mut result = Polynomial::<M>::zero();
234        // FIXED: Convert scalar to Montgomery form before multiplication
235        let scalar_mont = to_montgomery::<M>(scalar);
236        for i in 0..M::N {
237            // Now both operands are in Montgomery form, so the result stays in Montgomery form
238            let prod = (self.coeffs[i] as u64) * (scalar_mont as u64);
239            result.coeffs[i] = montgomery_reduce::<M>(prod);
240        }
241        result
242    }
243}
244
245/// Barrett reduction for fast modular arithmetic
246#[inline(always)]
247pub fn barrett_reduce<M: Modulus>(a: u32) -> u32 {
248    // Simplified Barrett reduction
249    // In production, would use precomputed Barrett constant
250    a % M::Q
251}
252
253// Implement standard ops traits for ergonomic usage
254// Define reference implementations first
255impl<M: Modulus> Add for &Polynomial<M> {
256    type Output = Polynomial<M>;
257
258    fn add(self, other: Self) -> Self::Output {
259        self.add(other)
260    }
261}
262
263impl<M: Modulus> Sub for &Polynomial<M> {
264    type Output = Polynomial<M>;
265
266    fn sub(self, other: Self) -> Self::Output {
267        self.sub(other)
268    }
269}
270
271impl<M: Modulus> Neg for &Polynomial<M> {
272    type Output = Polynomial<M>;
273
274    fn neg(self) -> Self::Output {
275        self.neg()
276    }
277}
278
279// Now owned implementations can use the reference implementations
280impl<M: Modulus> Add for Polynomial<M> {
281    type Output = Self;
282
283    fn add(self, other: Self) -> Self::Output {
284        &self + &other
285    }
286}
287
288impl<M: Modulus> Sub for Polynomial<M> {
289    type Output = Self;
290
291    fn sub(self, other: Self) -> Self::Output {
292        &self - &other
293    }
294}
295
296impl<M: Modulus> Neg for Polynomial<M> {
297    type Output = Self;
298
299    fn neg(self) -> Self::Output {
300        -&self
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    // Test modulus for unit tests
309    #[derive(Clone)]
310    struct TestModulus;
311    impl Modulus for TestModulus {
312        const Q: u32 = 3329;
313        const N: usize = 4; // Small for testing
314    }
315
316    #[test]
317    fn test_polynomial_creation() {
318        let poly = Polynomial::<TestModulus>::zero();
319        assert_eq!(poly.as_coeffs_slice(), &[0, 0, 0, 0]);
320
321        let coeffs = vec![1, 2, 3, 4];
322        let poly = Polynomial::<TestModulus>::from_coeffs(&coeffs).unwrap();
323        assert_eq!(poly.as_coeffs_slice(), &[1, 2, 3, 4]);
324    }
325
326    #[test]
327    fn test_polynomial_addition() {
328        let a = Polynomial::<TestModulus>::from_coeffs(&[1, 2, 3, 4]).unwrap();
329        let b = Polynomial::<TestModulus>::from_coeffs(&[5, 6, 7, 8]).unwrap();
330        // Use the + operator directly to avoid explicit borrows
331        let c = a + b;
332        assert_eq!(c.as_coeffs_slice(), &[6, 8, 10, 12]);
333    }
334
335    #[test]
336    fn test_polynomial_subtraction() {
337        let a = Polynomial::<TestModulus>::from_coeffs(&[10, 20, 30, 40]).unwrap();
338        let b = Polynomial::<TestModulus>::from_coeffs(&[5, 6, 7, 8]).unwrap();
339        // Use the - operator directly to avoid explicit borrows
340        let c = a - b;
341        assert_eq!(c.as_coeffs_slice(), &[5, 14, 23, 32]);
342    }
343
344    #[test]
345    fn test_polynomial_negation() {
346        let a = Polynomial::<TestModulus>::from_coeffs(&[1, 2, 0, 4]).unwrap();
347        // Use the - operator directly to avoid explicit borrows
348        let neg_a = -a;
349        assert_eq!(neg_a.as_coeffs_slice(), &[3328, 3327, 0, 3325]);
350    }
351
352    #[test]
353    fn test_modular_reduction() {
354        let a = Polynomial::<TestModulus>::from_coeffs(&[3330, 3331, 3328, 0]).unwrap();
355        let mut b = a.clone();
356        b.reduce_coeffs();
357        assert_eq!(b.as_coeffs_slice(), &[1, 2, 3328, 0]);
358    }
359
360    #[test]
361    fn test_zeroization() {
362        let mut poly = Polynomial::<TestModulus>::from_coeffs(&[1, 2, 3, 4]).unwrap();
363        poly.zeroize();
364        assert_eq!(poly.as_coeffs_slice(), &[0, 0, 0, 0]);
365        assert_eq!(poly.coeffs.len(), 4); // Length preserved
366    }
367
368    #[test]
369    fn test_schoolbook_mul_negacyclic() {
370        // Test negacyclic property: x^N = -1
371        // For N=4, x^4 = -1, so x^3 * x = -1
372        let mut x_cubed = Polynomial::<TestModulus>::zero();
373        x_cubed.coeffs[3] = 1; // x^3
374
375        let mut x = Polynomial::<TestModulus>::zero();
376        x.coeffs[1] = 1; // x
377
378        let result = x_cubed.schoolbook_mul(&x);
379        // x^3 * x = x^4 = -1 mod q = q-1
380        assert_eq!(result.coeffs[0], TestModulus::Q - 1);
381        assert_eq!(result.coeffs[1], 0);
382        assert_eq!(result.coeffs[2], 0);
383        assert_eq!(result.coeffs[3], 0);
384
385        // Test a more complex example
386        let a = Polynomial::<TestModulus>::from_coeffs(&[1, 2, 3, 4]).unwrap();
387        let b = Polynomial::<TestModulus>::from_coeffs(&[5, 6, 7, 8]).unwrap();
388        let c = a.schoolbook_mul(&b);
389
390        // Manually compute expected result with negacyclic reduction
391        // (1 + 2x + 3x^2 + 4x^3)(5 + 6x + 7x^2 + 8x^3)
392        //
393        // Full expansion (before reduction):
394        // 1*5 = 5
395        // 1*6x + 2*5x = 6x + 10x = 16x
396        // 1*7x^2 + 2*6x^2 + 3*5x^2 = 7x^2 + 12x^2 + 15x^2 = 34x^2
397        // 1*8x^3 + 2*7x^3 + 3*6x^3 + 4*5x^3 = 8x^3 + 14x^3 + 18x^3 + 20x^3 = 60x^3
398        // 2*8x^4 + 3*7x^4 + 4*6x^4 = 16x^4 + 21x^4 + 24x^4 = 61x^4
399        // 3*8x^5 + 4*7x^5 = 24x^5 + 28x^5 = 52x^5
400        // 4*8x^6 = 32x^6
401        //
402        // Now apply x^4 = -1:
403        // x^4 = -1
404        // x^5 = -x
405        // x^6 = -x^2
406        //
407        // So:
408        // Constant: 5 - 61 = -56 → 3329 - 56 = 3273
409        // x: 16 - 52 = -36 → 3329 - 36 = 3293
410        // x^2: 34 - 32 = 2
411        // x^3: 60
412
413        let expected_0 = ((5i32 - 61i32).rem_euclid(TestModulus::Q as i32)) as u32;
414        let expected_1 = ((16i32 - 52i32).rem_euclid(TestModulus::Q as i32)) as u32;
415        let expected_2 = ((34i32 - 32i32).rem_euclid(TestModulus::Q as i32)) as u32;
416        let expected_3 = 60u32;
417
418        assert_eq!(c.coeffs[0], expected_0);
419        assert_eq!(c.coeffs[1], expected_1);
420        assert_eq!(c.coeffs[2], expected_2);
421        assert_eq!(c.coeffs[3], expected_3);
422    }
423
424    #[test]
425    fn test_ml_dsa_negacyclic() {
426        // Test with ML-DSA-like parameters
427        #[derive(Clone)]
428        struct MlDsaTestModulus;
429        impl Modulus for MlDsaTestModulus {
430            const Q: u32 = 8380417; // ML-DSA's Q
431            const N: usize = 4; // Small for testing, but same negacyclic property
432        }
433
434        // Test that x^N = -1 in the ring
435        let mut x_to_n_minus_1 = Polynomial::<MlDsaTestModulus>::zero();
436        x_to_n_minus_1.coeffs[3] = 1; // x^3
437
438        let mut x = Polynomial::<MlDsaTestModulus>::zero();
439        x.coeffs[1] = 1; // x
440
441        let result = x_to_n_minus_1.schoolbook_mul(&x);
442        // x^3 * x = x^4 = -1 mod q = q-1
443        assert_eq!(result.coeffs[0], MlDsaTestModulus::Q - 1);
444        assert_eq!(result.coeffs[1], 0);
445        assert_eq!(result.coeffs[2], 0);
446        assert_eq!(result.coeffs[3], 0);
447
448        // Test with sparse polynomial (like challenge polynomial c)
449        let mut sparse = Polynomial::<MlDsaTestModulus>::zero();
450        sparse.coeffs[0] = 1; // +1
451        sparse.coeffs[2] = MlDsaTestModulus::Q - 1; // -1
452
453        let dense = Polynomial::<MlDsaTestModulus>::from_coeffs(&[100, 200, 300, 400]).unwrap();
454        let result = sparse.schoolbook_mul(&dense);
455
456        // (1 - x^2) * (100 + 200x + 300x^2 + 400x^3)
457        // = 100 + 200x + 300x^2 + 400x^3 - 100x^2 - 200x^3 - 300x^4 - 400x^5
458        // With x^4 = -1, x^5 = -x:
459        // = 100 + 200x + (300-100)x^2 + (400-200)x^3 + 300 + 400x
460        // = (100+300) + (200+400)x + 200x^2 + 200x^3
461        // = 400 + 600x + 200x^2 + 200x^3
462
463        assert_eq!(result.coeffs[0], 400);
464        assert_eq!(result.coeffs[1], 600);
465        assert_eq!(result.coeffs[2], 200);
466        assert_eq!(result.coeffs[3], 200);
467    }
468}