Coverage Report

Created: 2025-11-24 06:32

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/fips203/src/helpers.rs
Line
Count
Source
1
use crate::ntt::multiply_ntts;
2
use crate::types::Z;
3
use crate::Q;
4
use sha3::digest::{ExtendableOutput, Update, XofReader};
5
use sha3::{Digest, Sha3_256, Sha3_512, Shake128, Shake256};
6
7
8
/// If the condition is not met, return an error message. Borrowed from the `anyhow` crate.
9
macro_rules! ensure {
10
    ($cond:expr, $msg:literal $(,)?) => {
11
        if !$cond {
12
            return Err($msg);
13
        }
14
    };
15
}
16
17
pub(crate) use ensure; // make available throughout crate
18
19
20
/// Vector addition; See commentary on 2.11 page 10: `z_hat` = `u_hat` + `v_hat`
21
///
22
/// # Arguments
23
/// * `vec_a` - First vector of size K×256
24
/// * `vec_b` - Second vector of size K×256
25
///
26
/// # Returns
27
/// Sum of the two vectors element-wise
28
#[must_use]
29
71.0k
pub(crate) fn add_vecs<const K: usize>(
30
71.0k
    vec_a: &[[Z; 256]; K], vec_b: &[[Z; 256]; K],
31
71.0k
) -> [[Z; 256]; K] {
32
20.6M
    core::array::from_fn(|k| core::array::from_fn(|n| vec_a[k][n].add(vec_b[k][n])))
fips203::helpers::add_vecs::<1>::{closure#0}
Line
Count
Source
32
66.1k
    core::array::from_fn(|k| core::array::from_fn(|n| vec_a[k][n].add(vec_b[k][n])))
fips203::helpers::add_vecs::<2>::{closure#0}
Line
Count
Source
32
3.10k
    core::array::from_fn(|k| core::array::from_fn(|n| vec_a[k][n].add(vec_b[k][n])))
fips203::helpers::add_vecs::<3>::{closure#0}
Line
Count
Source
32
4.60k
    core::array::from_fn(|k| core::array::from_fn(|n| vec_a[k][n].add(vec_b[k][n])))
fips203::helpers::add_vecs::<4>::{closure#0}
Line
Count
Source
32
6.92k
    core::array::from_fn(|k| core::array::from_fn(|n| vec_a[k][n].add(vec_b[k][n])))
fips203::helpers::add_vecs::<1>::{closure#0}::{closure#0}
Line
Count
Source
32
16.9M
    core::array::from_fn(|k| core::array::from_fn(|n| vec_a[k][n].add(vec_b[k][n])))
fips203::helpers::add_vecs::<2>::{closure#0}::{closure#0}
Line
Count
Source
32
795k
    core::array::from_fn(|k| core::array::from_fn(|n| vec_a[k][n].add(vec_b[k][n])))
fips203::helpers::add_vecs::<3>::{closure#0}::{closure#0}
Line
Count
Source
32
1.17M
    core::array::from_fn(|k| core::array::from_fn(|n| vec_a[k][n].add(vec_b[k][n])))
fips203::helpers::add_vecs::<4>::{closure#0}::{closure#0}
Line
Count
Source
32
1.77M
    core::array::from_fn(|k| core::array::from_fn(|n| vec_a[k][n].add(vec_b[k][n])))
33
71.0k
}
fips203::helpers::add_vecs::<1>
Line
Count
Source
29
66.1k
pub(crate) fn add_vecs<const K: usize>(
30
66.1k
    vec_a: &[[Z; 256]; K], vec_b: &[[Z; 256]; K],
31
66.1k
) -> [[Z; 256]; K] {
32
66.1k
    core::array::from_fn(|k| core::array::from_fn(|n| vec_a[k][n].add(vec_b[k][n])))
33
66.1k
}
fips203::helpers::add_vecs::<2>
Line
Count
Source
29
1.55k
pub(crate) fn add_vecs<const K: usize>(
30
1.55k
    vec_a: &[[Z; 256]; K], vec_b: &[[Z; 256]; K],
31
1.55k
) -> [[Z; 256]; K] {
32
1.55k
    core::array::from_fn(|k| core::array::from_fn(|n| vec_a[k][n].add(vec_b[k][n])))
33
1.55k
}
fips203::helpers::add_vecs::<3>
Line
Count
Source
29
1.53k
pub(crate) fn add_vecs<const K: usize>(
30
1.53k
    vec_a: &[[Z; 256]; K], vec_b: &[[Z; 256]; K],
31
1.53k
) -> [[Z; 256]; K] {
32
1.53k
    core::array::from_fn(|k| core::array::from_fn(|n| vec_a[k][n].add(vec_b[k][n])))
33
1.53k
}
fips203::helpers::add_vecs::<4>
Line
Count
Source
29
1.73k
pub(crate) fn add_vecs<const K: usize>(
30
1.73k
    vec_a: &[[Z; 256]; K], vec_b: &[[Z; 256]; K],
31
1.73k
) -> [[Z; 256]; K] {
32
1.73k
    core::array::from_fn(|k| core::array::from_fn(|n| vec_a[k][n].add(vec_b[k][n])))
33
1.73k
}
34
35
36
/// Matrix by vector multiplication; See commentary on 2.12 page 10: `w_hat` = `A_hat` mul `u_hat`
37
///
38
/// # Arguments
39
/// * `a_hat` - Matrix of size K×K×256
40
/// * `u_hat` - Vector of size K×256
41
///
42
/// # Returns
43
/// Result of matrix multiplication `A_hat * u_hat`
44
#[must_use]
45
1.94k
pub(crate) fn mul_mat_vec<const K: usize>(
46
1.94k
    a_hat: &[[[Z; 256]; K]; K], u_hat: &[[Z; 256]; K],
47
1.94k
) -> [[Z; 256]; K] {
48
1.94k
    let mut w_hat = [[Z::default(); 256]; K];
49
7.77k
    for i in 0..K {
50
        #[allow(clippy::needless_range_loop)] // alternative is harder to understand
51
24.6k
        for j in 0..K {
52
18.7k
            let tmp = multiply_ntts(&a_hat[i][j], &u_hat[j]);
53
18.7k
            w_hat[i] = add_vecs(&[w_hat[i]], &[tmp])[0];
54
18.7k
        }
55
    }
56
1.94k
    w_hat
57
1.94k
}
fips203::helpers::mul_mat_vec::<2>
Line
Count
Source
45
648
pub(crate) fn mul_mat_vec<const K: usize>(
46
648
    a_hat: &[[[Z; 256]; K]; K], u_hat: &[[Z; 256]; K],
47
648
) -> [[Z; 256]; K] {
48
648
    let mut w_hat = [[Z::default(); 256]; K];
49
1.94k
    for i in 0..K {
50
        #[allow(clippy::needless_range_loop)] // alternative is harder to understand
51
3.88k
        for j in 0..K {
52
2.59k
            let tmp = multiply_ntts(&a_hat[i][j], &u_hat[j]);
53
2.59k
            w_hat[i] = add_vecs(&[w_hat[i]], &[tmp])[0];
54
2.59k
        }
55
    }
56
648
    w_hat
57
648
}
fips203::helpers::mul_mat_vec::<3>
Line
Count
Source
45
648
pub(crate) fn mul_mat_vec<const K: usize>(
46
648
    a_hat: &[[[Z; 256]; K]; K], u_hat: &[[Z; 256]; K],
47
648
) -> [[Z; 256]; K] {
48
648
    let mut w_hat = [[Z::default(); 256]; K];
49
2.59k
    for i in 0..K {
50
        #[allow(clippy::needless_range_loop)] // alternative is harder to understand
51
7.77k
        for j in 0..K {
52
5.83k
            let tmp = multiply_ntts(&a_hat[i][j], &u_hat[j]);
53
5.83k
            w_hat[i] = add_vecs(&[w_hat[i]], &[tmp])[0];
54
5.83k
        }
55
    }
56
648
    w_hat
57
648
}
fips203::helpers::mul_mat_vec::<4>
Line
Count
Source
45
648
pub(crate) fn mul_mat_vec<const K: usize>(
46
648
    a_hat: &[[[Z; 256]; K]; K], u_hat: &[[Z; 256]; K],
47
648
) -> [[Z; 256]; K] {
48
648
    let mut w_hat = [[Z::default(); 256]; K];
49
3.24k
    for i in 0..K {
50
        #[allow(clippy::needless_range_loop)] // alternative is harder to understand
51
12.9k
        for j in 0..K {
52
10.3k
            let tmp = multiply_ntts(&a_hat[i][j], &u_hat[j]);
53
10.3k
            w_hat[i] = add_vecs(&[w_hat[i]], &[tmp])[0];
54
10.3k
        }
55
    }
56
648
    w_hat
57
648
}
58
59
60
/// Matrix transpose by vector multiplication; See commentary on 2.13 page 10: `y_hat` = `A_hat^T` mul `u_hat`
61
///
62
/// # Arguments
63
/// * `a_hat` - Matrix of size K×K×256 to be transposed before multiplication
64
/// * `u_hat` - Vector of size K×256
65
///
66
/// # Returns
67
/// Result of matrix multiplication `A_hat^T * u_hat`, where `^T` denotes transpose
68
#[must_use]
69
2.87k
pub(crate) fn mul_mat_t_vec<const K: usize>(
70
2.87k
    a_hat: &[[[Z; 256]; K]; K], u_hat: &[[Z; 256]; K],
71
2.87k
) -> [[Z; 256]; K] {
72
2.87k
    let mut y_hat = [[Z::default(); 256]; K];
73
    #[allow(clippy::needless_range_loop)] // alternative is harder to understand
74
11.6k
    for i in 0..K {
75
        #[allow(clippy::needless_range_loop)] // alternative is harder to understand
76
37.7k
        for j in 0..K {
77
28.9k
            let tmp = multiply_ntts(&a_hat[j][i], &u_hat[j]); // i,j swapped vs above fn
78
28.9k
            y_hat[i] = add_vecs(&[y_hat[i]], &[tmp])[0];
79
28.9k
        }
80
    }
81
2.87k
    y_hat
82
2.87k
}
fips203::helpers::mul_mat_t_vec::<2>
Line
Count
Source
69
905
pub(crate) fn mul_mat_t_vec<const K: usize>(
70
905
    a_hat: &[[[Z; 256]; K]; K], u_hat: &[[Z; 256]; K],
71
905
) -> [[Z; 256]; K] {
72
905
    let mut y_hat = [[Z::default(); 256]; K];
73
    #[allow(clippy::needless_range_loop)] // alternative is harder to understand
74
2.71k
    for i in 0..K {
75
        #[allow(clippy::needless_range_loop)] // alternative is harder to understand
76
5.43k
        for j in 0..K {
77
3.62k
            let tmp = multiply_ntts(&a_hat[j][i], &u_hat[j]); // i,j swapped vs above fn
78
3.62k
            y_hat[i] = add_vecs(&[y_hat[i]], &[tmp])[0];
79
3.62k
        }
80
    }
81
905
    y_hat
82
905
}
fips203::helpers::mul_mat_t_vec::<3>
Line
Count
Source
69
888
pub(crate) fn mul_mat_t_vec<const K: usize>(
70
888
    a_hat: &[[[Z; 256]; K]; K], u_hat: &[[Z; 256]; K],
71
888
) -> [[Z; 256]; K] {
72
888
    let mut y_hat = [[Z::default(); 256]; K];
73
    #[allow(clippy::needless_range_loop)] // alternative is harder to understand
74
3.55k
    for i in 0..K {
75
        #[allow(clippy::needless_range_loop)] // alternative is harder to understand
76
10.6k
        for j in 0..K {
77
7.99k
            let tmp = multiply_ntts(&a_hat[j][i], &u_hat[j]); // i,j swapped vs above fn
78
7.99k
            y_hat[i] = add_vecs(&[y_hat[i]], &[tmp])[0];
79
7.99k
        }
80
    }
81
888
    y_hat
82
888
}
fips203::helpers::mul_mat_t_vec::<4>
Line
Count
Source
69
1.08k
pub(crate) fn mul_mat_t_vec<const K: usize>(
70
1.08k
    a_hat: &[[[Z; 256]; K]; K], u_hat: &[[Z; 256]; K],
71
1.08k
) -> [[Z; 256]; K] {
72
1.08k
    let mut y_hat = [[Z::default(); 256]; K];
73
    #[allow(clippy::needless_range_loop)] // alternative is harder to understand
74
5.42k
    for i in 0..K {
75
        #[allow(clippy::needless_range_loop)] // alternative is harder to understand
76
21.6k
        for j in 0..K {
77
17.3k
            let tmp = multiply_ntts(&a_hat[j][i], &u_hat[j]); // i,j swapped vs above fn
78
17.3k
            y_hat[i] = add_vecs(&[y_hat[i]], &[tmp])[0];
79
17.3k
        }
80
    }
81
1.08k
    y_hat
82
1.08k
}
83
84
85
/// Vector dot product; See commentary on 2.14 page 10: `z_hat` = `u_hat^T` mul `v_hat`
86
///
87
/// # Arguments
88
/// * `u_hat` - First vector of size K×256
89
/// * `v_hat` - Second vector of size K×256
90
///
91
/// # Returns
92
/// Dot product result as a 256-element array, computed as sum of element-wise products
93
#[must_use]
94
4.18k
pub(crate) fn dot_t_prod<const K: usize>(u_hat: &[[Z; 256]; K], v_hat: &[[Z; 256]; K]) -> [Z; 256] {
95
4.18k
    let mut result = [Z::default(); 256];
96
16.8k
    for j in 0..K {
97
12.6k
        let tmp = multiply_ntts(&u_hat[j], &v_hat[j]);
98
12.6k
        result = add_vecs(&[result], &[tmp])[0];
99
12.6k
    }
100
4.18k
    result
101
4.18k
}
fips203::helpers::dot_t_prod::<2>
Line
Count
Source
94
1.35k
pub(crate) fn dot_t_prod<const K: usize>(u_hat: &[[Z; 256]; K], v_hat: &[[Z; 256]; K]) -> [Z; 256] {
95
1.35k
    let mut result = [Z::default(); 256];
96
4.06k
    for j in 0..K {
97
2.71k
        let tmp = multiply_ntts(&u_hat[j], &v_hat[j]);
98
2.71k
        result = add_vecs(&[result], &[tmp])[0];
99
2.71k
    }
100
1.35k
    result
101
1.35k
}
fips203::helpers::dot_t_prod::<3>
Line
Count
Source
94
1.32k
pub(crate) fn dot_t_prod<const K: usize>(u_hat: &[[Z; 256]; K], v_hat: &[[Z; 256]; K]) -> [Z; 256] {
95
1.32k
    let mut result = [Z::default(); 256];
96
5.28k
    for j in 0..K {
97
3.96k
        let tmp = multiply_ntts(&u_hat[j], &v_hat[j]);
98
3.96k
        result = add_vecs(&[result], &[tmp])[0];
99
3.96k
    }
100
1.32k
    result
101
1.32k
}
fips203::helpers::dot_t_prod::<4>
Line
Count
Source
94
1.50k
pub(crate) fn dot_t_prod<const K: usize>(u_hat: &[[Z; 256]; K], v_hat: &[[Z; 256]; K]) -> [Z; 256] {
95
1.50k
    let mut result = [Z::default(); 256];
96
7.52k
    for j in 0..K {
97
6.01k
        let tmp = multiply_ntts(&u_hat[j], &v_hat[j]);
98
6.01k
        result = add_vecs(&[result], &[tmp])[0];
99
6.01k
    }
100
1.50k
    result
101
1.50k
}
102
103
104
/// Function PRF on page 18 (4.3).
105
/// Pseudorandom function that generates `ETA_64` bytes of output using SHAKE256
106
///
107
/// # Arguments
108
/// * `s` - 32-byte seed
109
/// * `b` - Single byte domain separator
110
#[must_use]
111
32.1k
pub(crate) fn prf<const ETA_64: usize>(s: &[u8; 32], b: u8) -> [u8; ETA_64] {
112
32.1k
    let mut hasher = Shake256::default();
113
32.1k
    hasher.update(s);
114
32.1k
    hasher.update(&[b]);
115
32.1k
    let mut reader = hasher.finalize_xof();
116
32.1k
    let mut result = [0u8; ETA_64];
117
32.1k
    reader.read(&mut result);
118
32.1k
    result
119
32.1k
}
fips203::helpers::prf::<128>
Line
Count
Source
111
27.7k
pub(crate) fn prf<const ETA_64: usize>(s: &[u8; 32], b: u8) -> [u8; ETA_64] {
112
27.7k
    let mut hasher = Shake256::default();
113
27.7k
    hasher.update(s);
114
27.7k
    hasher.update(&[b]);
115
27.7k
    let mut reader = hasher.finalize_xof();
116
27.7k
    let mut result = [0u8; ETA_64];
117
27.7k
    reader.read(&mut result);
118
27.7k
    result
119
27.7k
}
fips203::helpers::prf::<192>
Line
Count
Source
111
4.40k
pub(crate) fn prf<const ETA_64: usize>(s: &[u8; 32], b: u8) -> [u8; ETA_64] {
112
4.40k
    let mut hasher = Shake256::default();
113
4.40k
    hasher.update(s);
114
4.40k
    hasher.update(&[b]);
115
4.40k
    let mut reader = hasher.finalize_xof();
116
4.40k
    let mut result = [0u8; ETA_64];
117
4.40k
    reader.read(&mut result);
118
4.40k
    result
119
4.40k
}
120
121
122
/// Function XOF on page 19 (4.6), used with 32-byte `rho`
123
/// Expandable output function based on SHAKE128 for generating matrix elements
124
///
125
/// # Arguments
126
/// * `rho` - 32-byte seed for randomness
127
/// * `i` - Row index for matrix generation
128
/// * `j` - Column index for matrix generation
129
///
130
/// # Returns
131
/// An extendable output reader that can generate arbitrary length output
132
#[must_use]
133
47.7k
pub(crate) fn xof(rho: &[u8; 32], i: u8, j: u8) -> impl XofReader {
134
    //debug_assert_eq!(rho.len(), 32);
135
47.7k
    let mut hasher = Shake128::default();
136
47.7k
    hasher.update(rho);
137
47.7k
    hasher.update(&[i]);
138
47.7k
    hasher.update(&[j]);
139
47.7k
    hasher.finalize_xof()
140
47.7k
}
141
142
143
/// Function G on page 19 (4.5).
144
/// Hash function that produces two 32-byte outputs from variable input
145
///
146
/// # Arguments
147
/// * `bytes` - Slice of byte slices to be hashed together
148
///
149
/// # Returns
150
/// Tuple of two 32-byte arrays (tr, K) as specified in the protocol
151
4.82k
pub(crate) fn g(bytes: &[&[u8]]) -> ([u8; 32], [u8; 32]) {
152
4.82k
    let mut hasher = Sha3_512::new();
153
7.69k
    bytes.iter().for_each(|b| Digest::update(&mut hasher, b));
154
4.82k
    let digest = hasher.finalize();
155
4.82k
    let a = digest[0..32].try_into().expect("g_a fail");
156
4.82k
    let b = digest[32..64].try_into().expect("g_b fail");
157
4.82k
    (a, b)
158
4.82k
}
159
160
161
/// Function H on page 18 (4.4).
162
/// Hash function that produces a single 32-byte output
163
///
164
/// # Arguments
165
/// * `bytes` - Input bytes to hash (typically public key)
166
///
167
/// # Returns
168
/// 32-byte array representing the hash
169
#[must_use]
170
5.75k
pub(crate) fn h(bytes: &[u8]) -> [u8; 32] {
171
5.75k
    let mut hasher = Sha3_256::new();
172
5.75k
    Digest::update(&mut hasher, bytes);
173
5.75k
    let digest = hasher.finalize();
174
5.75k
    digest.into()
175
5.75k
}
176
177
178
/// Function J on page 18 (4.4).
179
/// XOF-based hash function for challenge generation
180
///
181
/// # Arguments
182
/// * `z` - 32-byte seed
183
/// * `ct` - Variable length ciphertext
184
///
185
/// # Returns
186
/// 32-byte challenge value derived from inputs
187
#[must_use]
188
1.30k
pub(crate) fn j(z: &[u8; 32], ct: &[u8]) -> [u8; 32] {
189
1.30k
    let mut hasher = Shake256::default();
190
1.30k
    hasher.update(z);
191
1.30k
    hasher.update(ct);
192
1.30k
    let mut reader = hasher.finalize_xof();
193
1.30k
    let mut result = [0u8; 32];
194
1.30k
    reader.read(&mut result);
195
1.30k
    result
196
1.30k
}
197
198
199
/// Compress<d> from page 21 (4.7).
200
/// x → ⌈(2^d/q) · x⌋
201
///
202
/// This function compresses elements from `Z_q` to a smaller range by scaling them down.
203
/// The compression is lossy but maintains approximate ratios between elements.
204
///
205
/// # Arguments
206
/// * `d` - Compression parameter that determines output range (0 to 11)
207
/// * `inout` - Vector of elements to compress in-place
208
///
209
/// # Implementation Notes
210
/// * Works for all odd q values from 17 to 6307
211
/// * Input x must be in range 0 to q-1
212
/// * Uses pre-computed multiplier M to avoid floating-point arithmetic
213
#[allow(clippy::cast_possible_truncation)]
214
12.9k
pub(crate) fn compress_vector(d: u32, inout: &mut [Z]) {
215
    const M: u32 = (((1u64 << 36) + Q as u64 - 1) / Q as u64) as u32;
216
3.33M
    for x_ref in &mut *inout {
217
3.32M
        let y = (x_ref.get_u32() << d) + (u32::from(Q) >> 1);
218
3.32M
        let result = (u64::from(y) * u64::from(M)) >> 36;
219
3.32M
        x_ref.set_u16(result as u16);
220
3.32M
    }
221
12.9k
}
222
223
224
/// Decompress<d> from page 21 (4.8).
225
/// y → ⌈(q/2^d) · y⌋
226
///
227
/// Inverse operation of `compress_vector` that expands compressed elements back to `Z_q`.
228
/// While not perfect due to lossy compression, attempts to restore original ratios.
229
///
230
/// # Arguments
231
/// * `d` - Same compression parameter used in `compress_vector`
232
/// * `inout` - Vector of compressed elements to decompress in-place
233
#[allow(clippy::cast_possible_truncation)]
234
8.21k
pub(crate) fn decompress_vector(d: u32, inout: &mut [Z]) {
235
2.11M
    for y_ref in &mut *inout {
236
2.10M
        let qy = u32::from(Q) * y_ref.get_u32() + (1 << d) - 1;
237
2.10M
        y_ref.set_u16((qy >> d) as u16);
238
2.10M
    }
239
8.21k
}