Skip to main content

dcrypt_algorithms/poly/
serialize.rs

1//! serialize.rs - Polynomial coefficient packing and unpacking
2
3#[cfg(feature = "alloc")]
4extern crate alloc;
5#[cfg(feature = "alloc")]
6use alloc::vec::Vec;
7
8use super::params::Modulus;
9use super::polynomial::Polynomial;
10use crate::error::{Error, Result};
11
12/// Trait for packing polynomial coefficients into a byte array
13pub trait CoefficientPacker<M: Modulus> {
14    /// Packs the polynomial's coefficients into a byte vector
15    fn pack_coeffs(poly: &Polynomial<M>, bits_per_coeff: usize) -> Result<Vec<u8>>;
16}
17
18/// Trait for unpacking polynomial coefficients from a byte array
19pub trait CoefficientUnpacker<M: Modulus> {
20    /// Unpacks coefficients from a byte vector into a new polynomial
21    fn unpack_coeffs(bytes: &[u8], bits_per_coeff: usize) -> Result<Polynomial<M>>;
22}
23
24/// Default implementation for coefficient serialization
25pub struct DefaultCoefficientSerde;
26
27impl<M: Modulus> CoefficientPacker<M> for DefaultCoefficientSerde {
28    fn pack_coeffs(poly: &Polynomial<M>, bits_per_coeff: usize) -> Result<Vec<u8>> {
29        let num_bytes = packed_length::<M>(bits_per_coeff)?;
30        let mut packed = vec![0u8; num_bytes];
31        DefaultCoefficientSerde::pack_coeffs_into(poly, bits_per_coeff, &mut packed)?;
32        Ok(packed)
33    }
34}
35
36fn packed_length<M: Modulus>(bits_per_coeff: usize) -> Result<usize> {
37    if bits_per_coeff == 0 || bits_per_coeff > 32 {
38        return Err(Error::Parameter {
39            name: "coefficient packing".into(),
40            reason: format!(
41                "bits_per_coeff must be in range [1, 32], got {}",
42                bits_per_coeff
43            )
44            .into(),
45        });
46    }
47    Ok((M::N * bits_per_coeff).div_ceil(8))
48}
49
50impl<M: Modulus> CoefficientUnpacker<M> for DefaultCoefficientSerde {
51    fn unpack_coeffs(bytes: &[u8], bits_per_coeff: usize) -> Result<Polynomial<M>> {
52        if bits_per_coeff == 0 || bits_per_coeff > 32 {
53            return Err(Error::Parameter {
54                name: "coefficient unpacking".into(),
55                reason: format!(
56                    "bits_per_coeff must be in range [1, 32], got {}",
57                    bits_per_coeff
58                )
59                .into(),
60            });
61        }
62
63        let n = M::N;
64        let total_bits = n * bits_per_coeff;
65        let required_bytes = total_bits.div_ceil(8); // FIXED: Use div_ceil
66
67        if bytes.len() < required_bytes {
68            return Err(Error::Parameter {
69                name: "coefficient unpacking".into(),
70                reason: format!(
71                    "insufficient bytes: expected {}, got {}",
72                    required_bytes,
73                    bytes.len()
74                )
75                .into(),
76            });
77        }
78
79        let mut poly = Polynomial::<M>::zero();
80        let coeffs = poly.as_mut_coeffs_slice();
81        let mask = (1u32 << bits_per_coeff) - 1;
82
83        let mut bit_pos = 0;
84        // FIXED: Use iterator instead of indexing
85        for coeff in coeffs.iter_mut().take(n) {
86            let mut coeff_value = 0u32;
87
88            // Unpack coefficient from byte array
89            for bit in 0..bits_per_coeff {
90                let byte_idx = bit_pos / 8;
91                let bit_idx = bit_pos % 8;
92                coeff_value |= (((bytes[byte_idx] >> bit_idx) & 1) as u32) << bit;
93                bit_pos += 1;
94            }
95
96            *coeff = coeff_value & mask;
97        }
98
99        Ok(poly)
100    }
101}
102
103/// Helper function to calculate the number of bytes required for packing
104#[allow(clippy::manual_div_ceil)]
105pub const fn bytes_required(bits_per_coeff: usize, n: usize) -> usize {
106    // Note: div_ceil is not const-stable yet, so we use manual implementation
107    // This is required for const functions
108    (n * bits_per_coeff + 7) / 8
109}
110
111/// Optimized packing for common bit widths
112impl DefaultCoefficientSerde {
113    /// Pack directly into caller-owned exact-size storage.
114    ///
115    /// Secret-key encoders use this form so no growable byte allocation ever
116    /// temporarily owns encoded secret coefficients.
117    pub fn pack_coeffs_into<M: Modulus>(
118        poly: &Polynomial<M>,
119        bits_per_coeff: usize,
120        packed: &mut [u8],
121    ) -> Result<()> {
122        let expected = packed_length::<M>(bits_per_coeff)?;
123        if packed.len() != expected {
124            return Err(Error::Parameter {
125                name: "coefficient packing output".into(),
126                reason: format!("expected {expected} bytes, got {}", packed.len()).into(),
127            });
128        }
129        packed.fill(0);
130        let mask = if bits_per_coeff == 32 {
131            u32::MAX
132        } else {
133            (1u32 << bits_per_coeff) - 1
134        };
135        let mut bit_pos = 0;
136        for &coeff in poly.as_coeffs_slice().iter().take(M::N) {
137            let masked_coeff = coeff & mask;
138            for bit in 0..bits_per_coeff {
139                packed[bit_pos / 8] |= (((masked_coeff >> bit) & 1) as u8) << (bit_pos % 8);
140                bit_pos += 1;
141            }
142        }
143        Ok(())
144    }
145
146    /// Optimized packing for 10-bit coefficients.
147    pub fn pack_10bit<M: Modulus>(poly: &Polynomial<M>) -> Result<Vec<u8>> {
148        let n = M::N;
149        let mut packed = vec![0u8; (n * 10) / 8];
150        let coeffs = poly.as_coeffs_slice();
151
152        for i in (0..n).step_by(4) {
153            let c0 = coeffs[i] & 0x3FF;
154            let c1 = coeffs[i + 1] & 0x3FF;
155            let c2 = coeffs[i + 2] & 0x3FF;
156            let c3 = coeffs[i + 3] & 0x3FF;
157
158            let idx = (i * 10) / 8;
159            packed[idx] = c0 as u8;
160            packed[idx + 1] = ((c0 >> 8) | (c1 << 2)) as u8;
161            packed[idx + 2] = ((c1 >> 6) | (c2 << 4)) as u8;
162            packed[idx + 3] = ((c2 >> 4) | (c3 << 6)) as u8;
163            packed[idx + 4] = (c3 >> 2) as u8;
164        }
165
166        Ok(packed)
167    }
168
169    /// Optimized unpacking for 10-bit coefficients
170    pub fn unpack_10bit<M: Modulus>(bytes: &[u8]) -> Result<Polynomial<M>> {
171        let n = M::N;
172        if bytes.len() < (n * 10) / 8 {
173            return Err(Error::Parameter {
174                name: "10-bit unpacking".into(),
175                reason: format!(
176                    "insufficient bytes: expected {}, got {}",
177                    (n * 10) / 8,
178                    bytes.len()
179                )
180                .into(),
181            });
182        }
183
184        let mut poly = Polynomial::<M>::zero();
185        let coeffs = poly.as_mut_coeffs_slice();
186
187        for i in (0..n).step_by(4) {
188            let idx = (i * 10) / 8;
189            coeffs[i] = (bytes[idx] as u32) | ((bytes[idx + 1] as u32 & 0x03) << 8);
190            coeffs[i + 1] = ((bytes[idx + 1] as u32) >> 2) | ((bytes[idx + 2] as u32 & 0x0F) << 6);
191            coeffs[i + 2] = ((bytes[idx + 2] as u32) >> 4) | ((bytes[idx + 3] as u32 & 0x3F) << 4);
192            coeffs[i + 3] = ((bytes[idx + 3] as u32) >> 6) | ((bytes[idx + 4] as u32) << 2);
193        }
194
195        Ok(poly)
196    }
197
198    /// Optimized packing for 13-bit coefficients (ML-DSA)
199    pub fn pack_13bit<M: Modulus>(poly: &Polynomial<M>) -> Result<Vec<u8>> {
200        let n = M::N;
201        let mut packed = vec![0u8; (n * 13) / 8];
202        let coeffs = poly.as_coeffs_slice();
203
204        for i in (0..n).step_by(8) {
205            let idx = (i * 13) / 8;
206
207            // Pack 8 coefficients (13 bits each) into 13 bytes
208            packed[idx] = coeffs[i] as u8;
209            packed[idx + 1] = ((coeffs[i] >> 8) | (coeffs[i + 1] << 5)) as u8;
210            packed[idx + 2] = (coeffs[i + 1] >> 3) as u8;
211            packed[idx + 3] = ((coeffs[i + 1] >> 11) | (coeffs[i + 2] << 2)) as u8;
212            packed[idx + 4] = ((coeffs[i + 2] >> 6) | (coeffs[i + 3] << 7)) as u8;
213            packed[idx + 5] = (coeffs[i + 3] >> 1) as u8;
214            packed[idx + 6] = ((coeffs[i + 3] >> 9) | (coeffs[i + 4] << 4)) as u8;
215            packed[idx + 7] = (coeffs[i + 4] >> 4) as u8;
216            packed[idx + 8] = ((coeffs[i + 4] >> 12) | (coeffs[i + 5] << 1)) as u8;
217            packed[idx + 9] = ((coeffs[i + 5] >> 7) | (coeffs[i + 6] << 6)) as u8;
218            packed[idx + 10] = (coeffs[i + 6] >> 2) as u8;
219            packed[idx + 11] = ((coeffs[i + 6] >> 10) | (coeffs[i + 7] << 3)) as u8;
220            packed[idx + 12] = (coeffs[i + 7] >> 5) as u8;
221        }
222
223        Ok(packed)
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use dcrypt_internal::random::{ChaCha20Rng, RngCore};
231
232    #[derive(Clone)]
233    struct TestModulus;
234    impl Modulus for TestModulus {
235        const Q: u32 = 3329;
236        const N: usize = 256;
237    }
238
239    #[test]
240    fn test_pack_unpack_roundtrip() {
241        let mut rng = ChaCha20Rng::from_seed([42u8; 32]);
242
243        // Test various bit widths
244        for bits in [10, 12, 13, 23] {
245            let mask = (1u32 << bits) - 1;
246
247            // Create random polynomial with coefficients fitting in `bits` bits
248            let mut poly = Polynomial::<TestModulus>::zero();
249            for i in 0..TestModulus::N {
250                poly.coeffs[i] = rng.next_u32() & mask;
251            }
252
253            // Pack and unpack
254            let packed = DefaultCoefficientSerde::pack_coeffs(&poly, bits).unwrap();
255            let unpacked =
256                <DefaultCoefficientSerde as CoefficientUnpacker<TestModulus>>::unpack_coeffs(
257                    &packed, bits,
258                )
259                .unwrap();
260
261            // Verify roundtrip
262            for i in 0..TestModulus::N {
263                assert_eq!(
264                    poly.coeffs[i], unpacked.coeffs[i],
265                    "Mismatch at index {} for {} bits",
266                    i, bits
267                );
268            }
269        }
270    }
271
272    #[test]
273    fn test_bytes_required() {
274        assert_eq!(bytes_required(10, 256), 320);
275        assert_eq!(bytes_required(12, 256), 384);
276        assert_eq!(bytes_required(13, 256), 416); // ML-DSA
277        assert_eq!(bytes_required(23, 256), 736); // ML-DSA signature
278    }
279
280    #[test]
281    fn test_optimized_10bit() {
282        let mut rng = ChaCha20Rng::from_seed([42u8; 32]);
283
284        // Create random polynomial with 10-bit coefficients
285        let mut poly = Polynomial::<TestModulus>::zero();
286        for i in 0..TestModulus::N {
287            poly.coeffs[i] = rng.next_u32() & 0x3FF;
288        }
289
290        // Test optimized packing
291        let packed_opt = DefaultCoefficientSerde::pack_10bit(&poly).unwrap();
292        let packed_gen = DefaultCoefficientSerde::pack_coeffs(&poly, 10).unwrap();
293        assert_eq!(packed_opt, packed_gen);
294
295        // Test optimized unpacking
296        let unpacked_opt =
297            DefaultCoefficientSerde::unpack_10bit::<TestModulus>(&packed_opt).unwrap();
298        let unpacked_gen =
299            <DefaultCoefficientSerde as CoefficientUnpacker<TestModulus>>::unpack_coeffs(
300                &packed_gen,
301                10,
302            )
303            .unwrap();
304
305        for i in 0..TestModulus::N {
306            assert_eq!(unpacked_opt.coeffs[i], unpacked_gen.coeffs[i]);
307            assert_eq!(unpacked_opt.coeffs[i], poly.coeffs[i]);
308        }
309    }
310
311    #[test]
312    fn test_invalid_parameters() {
313        let poly = Polynomial::<TestModulus>::zero();
314
315        // Test invalid bits_per_coeff
316        assert!(DefaultCoefficientSerde::pack_coeffs(&poly, 0).is_err());
317        assert!(DefaultCoefficientSerde::pack_coeffs(&poly, 33).is_err());
318
319        // Test invalid unpacking length
320        let short_bytes = vec![0u8; 10];
321        assert!(
322            <DefaultCoefficientSerde as CoefficientUnpacker<TestModulus>>::unpack_coeffs(
323                &short_bytes,
324                10
325            )
326            .is_err()
327        );
328    }
329}