use super::MlDsaError;
use super::ntt::{self, mod_q};
use super::params::{N, Q};
use super::rng::CryptoRng;
pub struct MaskedPoly {
pub share0: [i32; N],
pub share1: [i32; N],
}
impl MaskedPoly {
pub const fn zero() -> Self {
Self {
share0: [0i32; N],
share1: [0i32; N],
}
}
pub fn sample_expand_mask(
rho_double_prime: &[u8; 64],
nonce: u16,
gamma1: i32,
bitlen_gamma1_minus1: usize,
) -> Self {
use super::sha3;
let c = bitlen_gamma1_minus1 + 1; let poly_bytes = 32 * c; let mask_bytes = N * 4;
let mut state = sha3::shake256();
state.absorb(rho_double_prime);
state.absorb(&nonce.to_le_bytes());
let mut y_buf = [0u8; 640]; state.squeeze(&mut y_buf[..poly_bytes]);
let mut mask_buf = [0u8; N * 4];
state.squeeze(&mut mask_buf);
let mut mp = Self::zero();
let mut bit_pos = 0usize;
let b = gamma1 as u32;
let range = (gamma1 as u32 - 1) + gamma1 as u32; let bits = 32 - range.leading_zeros() as usize;
debug_assert_eq!(bits, c);
for i in 0..N {
let mut val = 0u32;
for bit in 0..bits {
if (y_buf[bit_pos / 8] >> (bit_pos % 8)) & 1 == 1 {
val |= 1 << bit;
}
bit_pos += 1;
}
let y_i = b as i32 - val as i32;
let r = u32::from_le_bytes([
mask_buf[4 * i],
mask_buf[4 * i + 1],
mask_buf[4 * i + 2],
mask_buf[4 * i + 3],
]);
let r_i = (r % (Q as u32)) as i32;
mp.share1[i] = r_i;
mp.share0[i] = mod_q(y_i - r_i);
}
zeroize_bytes(&mut y_buf);
zeroize_bytes(&mut mask_buf);
mp
}
pub fn mask(poly: &[i32; N], rng: &mut dyn CryptoRng) -> Result<Self, MlDsaError> {
let mut share0 = [0i32; N];
let mut share1 = [0i32; N];
let mut rand_bytes = [0u8; N * 4];
rng.fill_bytes(&mut rand_bytes)?;
for i in 0..N {
let r = u32::from_le_bytes([
rand_bytes[4 * i],
rand_bytes[4 * i + 1],
rand_bytes[4 * i + 2],
rand_bytes[4 * i + 3],
]);
share1[i] = (r % (Q as u32)) as i32;
}
zeroize_bytes(&mut rand_bytes);
for i in 0..N {
share0[i] = mod_q(poly[i] - share1[i]);
}
Ok(MaskedPoly { share0, share1 })
}
pub fn unmask(&self) -> [i32; N] {
let mut out = [0i32; N];
for i in 0..N {
out[i] = mod_q(self.share0[i] + self.share1[i]);
}
out
}
pub fn zeroize(&mut self) {
zeroize_poly(&mut self.share0);
zeroize_poly(&mut self.share1);
}
pub fn refresh(&mut self, rng: &mut dyn CryptoRng) -> Result<(), MlDsaError> {
let mut rand_bytes = [0u8; N * 4];
rng.fill_bytes(&mut rand_bytes)?;
for i in 0..N {
let r = (u32::from_le_bytes([
rand_bytes[4 * i],
rand_bytes[4 * i + 1],
rand_bytes[4 * i + 2],
rand_bytes[4 * i + 3],
]) % (Q as u32)) as i32;
self.share0[i] = mod_q(self.share0[i] - r);
self.share1[i] = mod_q(self.share1[i] + r);
}
zeroize_bytes(&mut rand_bytes);
Ok(())
}
}
pub fn masked_ntt(m: &mut MaskedPoly) {
ntt::ntt(&mut m.share0);
ntt::ntt(&mut m.share1);
}
pub fn masked_ntt_inv(m: &mut MaskedPoly) {
ntt::ntt_inv(&mut m.share0);
ntt::ntt_inv(&mut m.share1);
}
pub fn masked_pointwise_mul_public(masked: &MaskedPoly, c_hat: &[i32; N]) -> MaskedPoly {
MaskedPoly {
share0: ntt::pointwise_mul(&masked.share0, c_hat),
share1: ntt::pointwise_mul(&masked.share1, c_hat),
}
}
pub fn masked_mat_vec_mul(
a_hat: &[[[i32; N]; super::params::MAX_L]; super::params::MAX_K],
y_hat_m: &[MaskedPoly],
k: usize,
l: usize,
out: &mut [MaskedPoly],
) {
for i in 0..k {
out[i].share0 = [0i32; N];
out[i].share1 = [0i32; N];
for j in 0..l {
let prod0 = ntt::pointwise_mul(&a_hat[i][j], &y_hat_m[j].share0);
let prod1 = ntt::pointwise_mul(&a_hat[i][j], &y_hat_m[j].share1);
for n in 0..N {
out[i].share0[n] = mod_q(out[i].share0[n] + prod0[n]);
out[i].share1[n] = mod_q(out[i].share1[n] + prod1[n]);
}
}
}
}
pub fn masked_mat_vec_mul_lazy(rho: &[u8; 32], y_hat_m: &[MaskedPoly], k: usize, l: usize, out: &mut [MaskedPoly]) {
use super::sample;
for i in 0..k {
out[i].share0 = [0i32; N];
out[i].share1 = [0i32; N];
for j in 0..l {
let a_ij = sample::rej_ntt_poly(rho, j as u8, i as u8);
let prod0 = ntt::pointwise_mul(&a_ij, &y_hat_m[j].share0);
let prod1 = ntt::pointwise_mul(&a_ij, &y_hat_m[j].share1);
for n in 0..N {
out[i].share0[n] = mod_q(out[i].share0[n] + prod0[n]);
out[i].share1[n] = mod_q(out[i].share1[n] + prod1[n]);
}
}
}
}
fn zeroize_poly(p: &mut [i32; N]) {
for c in p.iter_mut() {
unsafe { core::ptr::write_volatile(c, 0) };
}
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
}
fn zeroize_bytes(b: &mut [u8]) {
for byte in b.iter_mut() {
unsafe { core::ptr::write_volatile(byte, 0) };
}
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
}
#[cfg(test)]
mod tests {
use super::super::ntt;
use super::*;
struct TestRng(u64);
impl CryptoRng for TestRng {
fn fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), MlDsaError> {
for chunk in dest.chunks_mut(8) {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
let bytes = x.to_le_bytes();
for (i, b) in chunk.iter_mut().enumerate() {
*b = bytes[i];
}
}
Ok(())
}
}
fn fixture_poly() -> [i32; N] {
let mut p = [0i32; N];
for i in 0..N {
p[i] = ((i as i32 * 12345 + 7).rem_euclid(Q)) as i32;
}
p
}
#[test]
fn masked_mat_vec_mul_matches_unmasked() {
use super::super::ntt as dsa_ntt;
use super::super::params::{MAX_K, MAX_L, MlDsa65, Params as ParamsT};
use super::super::sample;
let k = MlDsa65::K;
let l = MlDsa65::L;
let rho = [0x17u8; 32];
let a_hat = sample::expand_a::<MlDsa65>(&rho);
let mut y = [[0i32; N]; MAX_L];
for j in 0..l {
for n in 0..N {
y[j][n] = ((j as i32 * 100 + n as i32) % Q).abs();
}
dsa_ntt::ntt(&mut y[j]);
}
let mut w_ref = [[0i32; N]; MAX_K];
for i in 0..k {
for j in 0..l {
let prod = dsa_ntt::pointwise_mul(&a_hat[i][j], &y[j]);
for n in 0..N {
w_ref[i][n] = mod_q(w_ref[i][n] + prod[n]);
}
}
}
let mut rng = TestRng(0xFEEDC0DEBADCAFEu64);
let mut y_m: [MaskedPoly; MAX_L] = core::array::from_fn(|_| MaskedPoly::zero());
for j in 0..l {
y_m[j] = MaskedPoly::mask(&y[j], &mut rng).unwrap();
}
let mut w_m: [MaskedPoly; MAX_K] = core::array::from_fn(|_| MaskedPoly::zero());
masked_mat_vec_mul(&a_hat, &y_m, k, l, &mut w_m);
for i in 0..k {
let w_got = w_m[i].unmask();
assert_eq!(w_got, w_ref[i], "mismatch at row {}", i);
}
}
#[test]
fn masked_expand_mask_matches_unmasked_expand_mask() {
use super::super::params::MlDsa65;
use super::super::params::Params as _;
use super::super::sample;
let rho = [0x42u8; 64];
let kappa = 0u16;
let gamma1 = MlDsa65::GAMMA1;
let bitlen = MlDsa65::BITLEN_GAMMA1_MINUS1;
let y_ref = sample::expand_mask::<MlDsa65>(&rho, kappa);
let mp = MaskedPoly::sample_expand_mask(&rho, kappa, gamma1, bitlen);
let unmasked = mp.unmask();
for i in 0..N {
let r = y_ref[0][i];
let u = {
let mut v = unmasked[i];
if v > Q / 2 {
v -= Q;
}
v
};
assert_eq!(r, u, "mismatch at i={}: ref={}, masked={}", i, r, u);
}
}
#[test]
fn mask_unmask_roundtrip() {
let p = fixture_poly();
let mut rng = TestRng(0xCAFEF00DDEADBEEF);
let mp = MaskedPoly::mask(&p, &mut rng).unwrap();
let recovered = mp.unmask();
assert_eq!(recovered, p);
}
#[test]
fn refresh_preserves_unmasked_value() {
let p = fixture_poly();
let mut rng = TestRng(0x1234567890ABCDEF);
let mut mp = MaskedPoly::mask(&p, &mut rng).unwrap();
mp.refresh(&mut rng).unwrap();
mp.refresh(&mut rng).unwrap();
assert_eq!(mp.unmask(), p);
}
#[test]
fn masked_ntt_matches_regular_ntt() {
let p = fixture_poly();
let mut expected = p;
ntt::ntt(&mut expected);
let mut rng = TestRng(0x0123456789ABCDEF);
let mut mp = MaskedPoly::mask(&p, &mut rng).unwrap();
masked_ntt(&mut mp);
for i in 0..N {
let got = mod_q(mp.share0[i] + mp.share1[i]);
assert_eq!(got, expected[i], "mismatch at i={}", i);
}
}
#[test]
fn masked_pointwise_mul_public_matches_unmasked() {
let secret = fixture_poly();
let mut public = [0i32; N];
for i in 0..N {
public[i] = ((i as i32 * 991 + 13).rem_euclid(Q)) as i32;
}
let mut secret_ntt = secret;
ntt::ntt(&mut secret_ntt);
let mut public_ntt = public;
ntt::ntt(&mut public_ntt);
let mut expected = ntt::pointwise_mul(&secret_ntt, &public_ntt);
for c in expected.iter_mut() {
*c = mod_q(*c);
}
let mut rng = TestRng(0xFEEDFACE12345678);
let mp_secret = MaskedPoly::mask(&secret_ntt, &mut rng).unwrap();
let mp_product = masked_pointwise_mul_public(&mp_secret, &public_ntt);
let got = mp_product.unmask();
assert_eq!(got, expected);
}
}