#![allow(clippy::clone_on_copy, clippy::op_ref)]
use const_num_traits::{CtIsZero, CtParity, One, WrappingAdd, WrappingSub, Zero};
use modmath_cios::CiosRowOps;
use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, ConstantTimeLess, CtOption};
pub(crate) const fn divsteps_total(modulus_bits: usize) -> usize {
if modulus_bits < 46 {
(49 * modulus_bits + 80) / 17
} else {
(49 * modulus_bits + 57) / 17
}
}
#[inline]
fn ct_low_bit<T>(value: &T) -> Choice
where
T: CiosRowOps,
T::Word: CtParity,
{
value.word(0).ct_is_odd()
}
#[inline]
fn ct_i64_positive(delta: i64) -> Choice {
let sign_clear = (!(delta.wrapping_sub(1) as u64)) >> 63;
Choice::from(core::hint::black_box(sign_clear) as u8)
}
#[derive(Clone)]
struct SignedExt<T> {
lo: T,
hi: u64,
}
#[inline]
fn se_select<T: ConditionallySelectable>(
a: &SignedExt<T>,
b: &SignedExt<T>,
choice: Choice,
) -> SignedExt<T> {
SignedExt {
lo: T::conditional_select(&a.lo, &b.lo, choice),
hi: u64::conditional_select(&a.hi, &b.hi, choice),
}
}
#[inline]
fn se_add<T>(a: &SignedExt<T>, b: &SignedExt<T>) -> SignedExt<T>
where
T: Clone + WrappingAdd<Output = T> + ConstantTimeLess,
{
let lo = a.lo.clone().wrapping_add(b.lo.clone());
let carry = lo.ct_lt(&a.lo);
let hi =
a.hi.wrapping_add(b.hi)
.wrapping_add(carry.unwrap_u8() as u64);
SignedExt { lo, hi }
}
#[inline]
fn se_neg<T>(a: &SignedExt<T>) -> SignedExt<T>
where
T: Clone + Zero + WrappingSub<Output = T> + CtIsZero,
{
let lo = T::zero().wrapping_sub(a.lo.clone());
let borrow = !a.lo.ct_is_zero();
let hi = 0u64
.wrapping_sub(a.hi)
.wrapping_sub(borrow.unwrap_u8() as u64);
SignedExt { lo, hi }
}
#[inline]
fn se_shr1<T>(a: &SignedExt<T>, top_bit_mask: &T) -> SignedExt<T>
where
T: Clone
+ ConditionallySelectable
+ core::ops::Shr<usize, Output = T>
+ core::ops::BitOr<Output = T>,
{
let logical = a.lo.clone() >> 1;
let with_hi_bit = logical.clone() | top_bit_mask.clone();
let hi_low_bit = Choice::from((a.hi & 1) as u8);
SignedExt {
lo: T::conditional_select(&logical, &with_hi_bit, hi_low_bit),
hi: ((a.hi as i64) >> 1) as u64,
}
}
#[inline]
fn add_mod_ct<T>(a: &T, b: &T, m: &T) -> T
where
T: Clone
+ ConditionallySelectable
+ WrappingAdd<Output = T>
+ WrappingSub<Output = T>
+ ConstantTimeLess,
{
let m_minus_b = m.clone().wrapping_sub(b.clone());
let need_sub = !a.ct_lt(&m_minus_b);
let sum = a.clone().wrapping_add(b.clone());
let reduced = a.clone().wrapping_sub(m_minus_b);
T::conditional_select(&sum, &reduced, need_sub)
}
#[inline]
fn neg_mod_ct<T>(x: &T, m: &T) -> T
where
T: Clone + Zero + ConditionallySelectable + WrappingSub<Output = T> + CtIsZero,
{
let zero = T::zero();
let x_is_zero = x.ct_is_zero();
let neg = m.clone().wrapping_sub(x.clone());
T::conditional_select(&neg, &zero, x_is_zero)
}
#[inline]
fn half_mod_ct<T>(x: &T, m_half: &T) -> T
where
T: CiosRowOps
+ Clone
+ ConditionallySelectable
+ One
+ WrappingAdd<Output = T>
+ core::ops::Shr<usize, Output = T>,
T::Word: CtParity,
{
let x_odd = ct_low_bit(x);
let candidate_even = x.clone() >> 1;
let candidate_odd = candidate_even
.clone()
.wrapping_add(m_half.clone())
.wrapping_add(T::one());
T::conditional_select(&candidate_even, &candidate_odd, x_odd)
}
pub fn safegcd_inv_ct<T>(value: &T, modulus: &T) -> CtOption<T>
where
T: CiosRowOps
+ Clone
+ ConditionallySelectable
+ ConstantTimeEq
+ ConstantTimeLess
+ CtIsZero
+ Zero
+ One
+ WrappingAdd<Output = T>
+ WrappingSub<Output = T>
+ core::ops::Shr<usize, Output = T>
+ core::ops::Shl<usize, Output = T>
+ core::ops::BitOr<Output = T>,
T::Word: CtParity,
{
let n_bits = value.word_count() * core::mem::size_of::<T::Word>() * 8;
let total_steps = divsteps_total(n_bits);
let max = T::zero().wrapping_sub(T::one());
let top_bit_mask = max.wrapping_sub(max.clone() >> 1);
let m_half = modulus.clone() >> 1;
let mut f = SignedExt {
lo: modulus.clone(),
hi: 0,
};
let mut g = SignedExt {
lo: value.clone(),
hi: 0,
};
let mut d = T::zero();
let mut e = T::one();
let mut delta: i64 = 1;
for _ in 0..total_steps {
let delta_pos = ct_i64_positive(delta);
let g_odd = ct_low_bit(&g.lo);
let swap = delta_pos & g_odd;
let new_f = se_select(&f, &g, swap);
let neg_f = se_neg(&f);
g = se_select(&g, &neg_f, swap);
f = new_f;
let new_d = T::conditional_select(&d, &e, swap);
let neg_d_mod = neg_mod_ct(&d, modulus);
e = T::conditional_select(&e, &neg_d_mod, swap);
d = new_d;
let neg_delta = (delta as u64).wrapping_neg() as i64;
delta = i64::conditional_select(&delta, &neg_delta, swap);
delta = delta.wrapping_add(1);
let g_odd_now = g_odd;
let zero_ext = SignedExt {
lo: T::zero(),
hi: 0,
};
let to_add_to_g = se_select(&zero_ext, &f, g_odd_now);
g = se_add(&g, &to_add_to_g);
let add_to_e = add_mod_ct(&e, &d, modulus);
e = T::conditional_select(&e, &add_to_e, g_odd_now);
g = se_shr1(&g, &top_bit_mask);
e = half_mod_ct(&e, &m_half);
}
let one = T::one();
let neg_one_lo = T::zero().wrapping_sub(T::one());
let f_is_one = f.lo.ct_eq(&one) & f.hi.ct_eq(&0u64);
let f_is_neg_one = f.lo.ct_eq(&neg_one_lo) & f.hi.ct_eq(&u64::MAX);
let has_inverse = f_is_one | f_is_neg_one;
let neg_d = neg_mod_ct(&d, modulus);
let result = T::conditional_select(&d, &neg_d, f_is_neg_one);
let modulus_is_odd = ct_low_bit(modulus);
let modulus_is_one = modulus.ct_eq(&one);
let modulus_ok = modulus_is_odd & !modulus_is_one;
CtOption::new(result, has_inverse & modulus_ok)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::inv::basic_mod_inv;
#[test]
fn divsteps_total_matches_paper_bound() {
assert!(divsteps_total(256) >= (45usize * 256 + 64).div_ceil(19));
assert!(divsteps_total(2048) >= (45usize * 2048 + 64).div_ceil(19));
assert!(divsteps_total(4096) >= (45usize * 4096 + 64).div_ceil(19));
}
#[test]
fn matches_basic_mod_inv_mod_small_primes_u32() {
for &m in &[3u32, 5, 7, 11, 13, 17, 19, 23, 29, 31, 97, 251] {
for v in 1..m {
let got = safegcd_inv_ct::<u32>(&v, &m);
let want = basic_mod_inv(v, m);
match (got.into_option(), want) {
(Some(g), Some(w)) => {
assert_eq!(
g, w,
"value={v} modulus={m}: safegcd gave {g}, basic_mod_inv gave {w}"
);
assert_eq!((g as u64 * v as u64) % m as u64, 1, "inv check failed");
}
(Some(_), None) | (None, Some(_)) => {
panic!("disagreement on value={v} modulus={m}");
}
(None, None) => {}
}
}
}
}
#[test]
fn exhaustive_u8_all_odd_moduli() {
for m in (3u16..=255).step_by(2) {
for v in 1..m {
let got = safegcd_inv_ct::<u8>(&(v as u8), &(m as u8)).into_option();
let want = basic_mod_inv(v as u32, m as u32);
match (got, want) {
(Some(g), Some(w)) => {
assert_eq!(g as u32, w, "value={v} modulus={m}: mismatch");
}
(None, None) => {}
_ => panic!("value={v} modulus={m}: Some/None disagreement"),
}
}
}
}
#[test]
fn handles_composite_modulus_u32() {
let m: u32 = 15;
let coprime_values = [1u32, 2, 4, 7, 8, 11, 13, 14];
for &v in &coprime_values {
let got = safegcd_inv_ct::<u32>(&v, &m).into_option();
let want = basic_mod_inv(v, m);
assert_eq!(got, want, "value={v} modulus=15: composite case mismatch");
if let Some(inv) = got {
assert_eq!((inv as u64 * v as u64) % 15, 1);
}
}
for &v in &[3u32, 5, 6, 9, 10, 12] {
assert!(
safegcd_inv_ct::<u32>(&v, &m).into_option().is_none(),
"value={v} modulus=15: expected None (not coprime)"
);
}
}
#[test]
fn fixed_bigint_smoke_test() {
use const_num_traits::Ct;
use fixed_bigint::FixedUInt;
type U64Ct = FixedUInt<u32, 2, Ct>;
type U64Nct = FixedUInt<u32, 2>;
let m_raw: u64 = 0x7FFF_FFFF_FFFF_FFE7; let m = U64Ct::from(m_raw);
let m_nct = U64Nct::from(m_raw);
let test_vals = [1u64, 2, 7, 42, 0xDEAD_BEEF];
for &v_raw in &test_vals {
let v = U64Ct::from(v_raw);
let v_nct = U64Nct::from(v_raw);
let got = safegcd_inv_ct(&v, &m).into_option();
assert!(got.is_some(), "expected inverse to exist for v={v_raw}");
let inv = got.unwrap();
let baseline = basic_mod_inv(v_nct, m_nct).expect("baseline inv");
let inv_nct: U64Nct = inv.forget_ct();
assert_eq!(
inv_nct, baseline,
"FixedUInt v={v_raw}: mismatch with basic_mod_inv"
);
}
}
#[test]
fn u64_composite_coprime() {
let n: u64 = 0x1_0000_0007 * 0x100_0007;
for v in [1u64, 2, 3, 0xCAFE_BABE, 0xFEED_FACE] {
let got = safegcd_inv_ct::<u64>(&v, &n).into_option();
assert!(got.is_some(), "u64 case: v={v} mod {n:#x} expected Some");
let inv = got.unwrap();
let prod = (inv as u128 * v as u128) % n as u128;
assert_eq!(prod, 1, "u64 v={v}: inv*v mod n != 1");
}
assert!(
safegcd_inv_ct::<u64>(&0xDEAD_BEEF, &n)
.into_option()
.is_none()
);
}
#[test]
fn u64_smoke_test() {
let m: u64 = 0x7FFF_FFFF_FFFF_FFE7; let test_vals = [1u64, 2, 7, 0xDEAD_BEEF, 0xCAFE_BABE];
for &v in &test_vals {
let got = safegcd_inv_ct::<u64>(&v, &m).into_option();
let want = basic_mod_inv(v, m);
assert_eq!(got, want, "value={v} modulus={m}: u64 case mismatch");
if let Some(inv) = got {
let prod = (inv as u128 * v as u128) % m as u128;
assert_eq!(prod, 1, "u64 inv * value mod m != 1");
}
}
}
#[test]
fn u32_full_width_modulus() {
let m: u32 = 0xFFFF_FFFB; for v in [1u32, 2, 7, 0xDEAD_BEEF, 0xFFFF_FFFA] {
let got = safegcd_inv_ct::<u32>(&v, &m).into_option();
let inv = got.unwrap_or_else(|| panic!("expected inverse for v={v:#x}"));
assert_eq!(
(inv as u64 * v as u64) % m as u64,
1,
"full-width u32 v={v:#x}: inv*v mod m != 1"
);
}
assert_eq!(
safegcd_inv_ct::<u32>(&(m - 1), &m).into_option(),
Some(m - 1)
);
}
#[test]
fn u64_full_width_prime() {
let m: u64 = 0xFFFF_FFFF_FFFF_FFC5; for v in [1u64, 2, 7, 0xDEAD_BEEF, 0xCAFE_BABE_F00D_D00D, m - 1] {
let got = safegcd_inv_ct::<u64>(&v, &m).into_option();
let inv = got.unwrap_or_else(|| panic!("expected inverse for v={v:#x}"));
let prod = (inv as u128 * v as u128) % m as u128;
assert_eq!(prod, 1, "full-width u64 v={v:#x}: inv*v mod m != 1");
}
}
#[test]
fn u64_full_width_composite() {
let p: u64 = 0xFFFF_FFFB; let q: u64 = 0xFFFF_FFEF; let n = p * q;
assert!(n >> 63 == 1, "test setup: n must have MSB set");
for v in [1u64, 2, 3, 0xCAFE_BABE, 0xFEED_FACE_DEAD_BEEF % n] {
let got = safegcd_inv_ct::<u64>(&v, &n).into_option();
let inv = got.unwrap_or_else(|| panic!("expected inverse for v={v:#x}"));
let prod = (inv as u128 * v as u128) % n as u128;
assert_eq!(prod, 1, "full-width composite v={v:#x}: inv*v mod n != 1");
}
assert!(
safegcd_inv_ct::<u64>(&p, &n).into_option().is_none(),
"factor p must not be invertible mod p*q"
);
}
#[test]
fn fixed_bigint_full_width_modulus() {
use const_num_traits::Ct;
use fixed_bigint::FixedUInt;
type U64Ct = FixedUInt<u32, 2, Ct>;
type U64Nct = FixedUInt<u32, 2>;
let m_raw: u64 = 0xFFFF_FFFF_FFFF_FFC5; let m = U64Ct::from(m_raw);
for v_raw in [1u64, 2, 7, 42, 0xDEAD_BEEF, m_raw - 1] {
let v = U64Ct::from(v_raw);
let got = safegcd_inv_ct(&v, &m).into_option();
let inv = got.unwrap_or_else(|| panic!("expected inverse for v={v_raw:#x}"));
let expected = safegcd_inv_ct::<u64>(&v_raw, &m_raw)
.into_option()
.expect("u64 baseline");
let inv_nct: U64Nct = inv.forget_ct();
assert_eq!(
inv_nct,
U64Nct::from(expected),
"FixedUInt full-width v={v_raw:#x}: mismatch with u64 baseline"
);
}
}
#[test]
fn modulus_precondition_masks() {
for m in [2u32, 4, 6, 100, 0xFFFF_FFFE] {
for v in [1u32, 3, 7, 0xCAFE_BABE] {
assert!(
safegcd_inv_ct::<u32>(&v, &m).into_option().is_none(),
"even modulus {m:#x} with value {v:#x}: expected None"
);
}
}
for v in [0u32, 1, 7] {
assert!(
safegcd_inv_ct::<u32>(&v, &1u32).into_option().is_none(),
"modulus = 1 with value {v}: expected None"
);
}
}
}