Skip to main content

logicaffeine_proof/
factor.rs

1//! Structural factoring: the AIT thesis applied to public-key crypto.
2//!
3//! The compressibility ladder crushes symmetric keystreams by finding their *structure* — a short
4//! feedback register, a correlation, a low-degree annihilator. RSA lives in a different universe: its
5//! security is integer factorization of `N = p·q`, not sequence structure. But the same thesis governs
6//! it. A *weak* RSA modulus is one with exploitable STRUCTURE, and every classical factoring attack is a
7//! structure-detector: a small factor, primes that sit too close, a smooth `p−1`, a shared prime across
8//! moduli, a small private exponent. Each such structure is a compression — a short description of the
9//! secret — and each attack returns a re-checkable WITNESS (the factors themselves, `p·q = N`).
10//!
11//! The point of building the whole arsenal is the *ceiling*: run every structural attack against a
12//! soundly-generated modulus — two large, independent, well-separated strong primes — and it finds
13//! NOTHING within budget. RSA's safety is precisely that its modulus is the number-theoretic
14//! incompressible residue: no structural shortcut exists, and only the general (sub-)exponential
15//! algorithms remain, which real key sizes push out of reach. Crush every structured form; the sound
16//! form stands. That standing IS the proof.
17
18use logicaffeine_base::numeric::{BigInt, Rational};
19
20/// The Miller–Rabin witness bases (the first twelve primes) — deterministic for the modulus sizes used
21/// here, and overwhelmingly reliable beyond them.
22const MR_BASES: &[u64] = &[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37];
23
24fn one() -> BigInt {
25    BigInt::from_i64(1)
26}
27
28fn two() -> BigInt {
29    BigInt::from_i64(2)
30}
31
32/// `a mod m` for non-negative `a`, `m > 0`.
33fn rem(a: &BigInt, m: &BigInt) -> BigInt {
34    a.div_rem(m).expect("modulus is nonzero").1
35}
36
37/// The greatest common divisor of `|a|` and `|b|` (Euclid).
38pub fn gcd(a: &BigInt, b: &BigInt) -> BigInt {
39    let (mut a, mut b) = (a.abs(), b.abs());
40    while !b.is_zero() {
41        let r = rem(&a, &b);
42        a = b;
43        b = r;
44    }
45    a
46}
47
48/// The modular inverse `a⁻¹ mod m` via the extended Euclidean algorithm, or `None` if `gcd(a, m) ≠ 1`.
49pub fn mod_inverse(a: &BigInt, m: &BigInt) -> Option<BigInt> {
50    let (mut old_r, mut r) = (rem(a, m), m.clone());
51    let (mut old_s, mut s) = (one(), BigInt::zero());
52    while !r.is_zero() {
53        let (q, rr) = old_r.div_rem(&r).expect("nonzero");
54        old_r = r;
55        r = rr;
56        let ns = old_s.sub(&q.mul(&s));
57        old_s = s;
58        s = ns;
59    }
60    if old_r != one() {
61        return None;
62    }
63    let mut res = rem(&old_s, m);
64    if res.is_negative() {
65        res = res.add(m);
66    }
67    Some(res)
68}
69
70/// Modular exponentiation `base^exp mod m` with a full `BigInt` exponent (square-and-multiply, the
71/// exponent's bits read off by repeated halving).
72pub fn modpow(base: &BigInt, exp: &BigInt, m: &BigInt) -> BigInt {
73    if *m == one() {
74        return BigInt::zero();
75    }
76    let two = two();
77    let mut result = one();
78    let mut b = rem(base, m);
79    let mut e = exp.clone();
80    while !e.is_zero() {
81        let (q, bit) = e.div_rem(&two).expect("nonzero");
82        if !bit.is_zero() {
83            result = rem(&result.mul(&b), m);
84        }
85        b = rem(&b.mul(&b), m);
86        e = q;
87    }
88    result
89}
90
91/// The integer square root `⌊√n⌋` (Newton's method). The floating-point seed is only ~15 digits precise,
92/// so it is first forced to an OVERESTIMATE (`x² ≥ n`, a handful of doublings); from above, the Newton
93/// iteration `x ← ⌊(x + ⌊n/x⌋)/2⌋` decreases monotonically and halts exactly at `⌊√n⌋`.
94pub fn isqrt(n: &BigInt) -> BigInt {
95    if n.is_zero() || n.is_negative() {
96        return BigInt::zero();
97    }
98    let two = two();
99    let approx = n.to_f64().sqrt();
100    let mut x = if approx.is_finite() && approx >= 1.0 {
101        BigInt::parse_decimal(&format!("{approx:.0}")).unwrap_or_else(one)
102    } else {
103        one()
104    };
105    if x.is_zero() {
106        x = one();
107    }
108    while x.mul(&x) < *n {
109        x = x.mul(&two); // force an overestimate (the f64 seed may undershoot by ~10¹⁵)
110    }
111    loop {
112        let (q, _) = n.div_rem(&x).expect("nonzero");
113        let y = x.add(&q).div_rem(&two).expect("nonzero").0;
114        if y >= x {
115            return x;
116        }
117        x = y;
118    }
119}
120
121/// Miller–Rabin primality over [`MR_BASES`].
122pub fn is_probable_prime(n: &BigInt) -> bool {
123    let (one, two) = (one(), two());
124    if *n < two {
125        return false;
126    }
127    if *n == two {
128        return true;
129    }
130    if !n.is_odd() {
131        return false;
132    }
133    // n − 1 = d·2ˢ.
134    let n1 = n.sub(&one);
135    let mut d = n1.clone();
136    let mut s = 0u32;
137    while !d.is_odd() {
138        d = d.div_rem(&two).expect("nonzero").0;
139        s += 1;
140    }
141    'base: for &a in MR_BASES {
142        let a = BigInt::from_u64(a);
143        if rem(&a, n).is_zero() {
144            continue; // a ≡ 0 mod n (n divides the base) — uninformative
145        }
146        let mut x = modpow(&a, &d, n);
147        if x == one || x == n1 {
148            continue;
149        }
150        for _ in 0..s.saturating_sub(1) {
151            x = rem(&x.mul(&x), n);
152            if x == n1 {
153                continue 'base;
154            }
155        }
156        return false;
157    }
158    true
159}
160
161/// The smallest prime `≥ start`.
162pub fn next_prime(start: &BigInt) -> BigInt {
163    let (one, two) = (one(), two());
164    if *start <= two {
165        return two;
166    }
167    let mut c = if start.is_odd() { start.clone() } else { start.add(&one) };
168    loop {
169        if is_probable_prime(&c) {
170            return c;
171        }
172        c = c.add(&two);
173    }
174}
175
176/// A nontrivial factorization `p·q = n` (`1 < p, q < n`) — the re-checkable witness every attack returns.
177pub fn verify_factorization(n: &BigInt, p: &BigInt, q: &BigInt) -> bool {
178    let one = one();
179    p.mul(q) == *n && *p > one && *q > one && *p < *n && *q < *n
180}
181
182fn split(n: &BigInt, d: &BigInt) -> Option<(BigInt, BigInt)> {
183    let one = one();
184    if *d > one && *d < *n {
185        let (q, r) = n.div_rem(d).expect("nonzero");
186        if r.is_zero() {
187            return Some((d.clone(), q));
188        }
189    }
190    None
191}
192
193/// Trial division up to `limit`: catches a **small factor** — a modulus that leaked a tiny prime.
194pub fn trial_division(n: &BigInt, limit: u64) -> Option<(BigInt, BigInt)> {
195    let mut d = 2u64;
196    while d <= limit {
197        let bd = BigInt::from_u64(d);
198        if bd.mul(&bd) > *n {
199            break;
200        }
201        if let Some(f) = split(n, &bd) {
202            return Some(f);
203        }
204        d += if d == 2 { 1 } else { 2 };
205    }
206    None
207}
208
209/// Fermat's method: catches **primes that sit too close** — `N = a² − b²` with `a` just above `√N`, so a
210/// few steps expose `(a−b, a+b)`. Structurally, close primes are a compressed key (one prime nearly fixes
211/// the other).
212pub fn fermat(n: &BigInt, max_iters: u64) -> Option<(BigInt, BigInt)> {
213    if !n.is_odd() {
214        return split(n, &two());
215    }
216    let one = one();
217    let mut a = isqrt(n);
218    if a.mul(&a) < *n {
219        a = a.add(&one);
220    }
221    for _ in 0..max_iters {
222        let b2 = a.mul(&a).sub(n);
223        let b = isqrt(&b2);
224        if b.mul(&b) == b2 {
225            let p = a.sub(&b);
226            let q = a.add(&b);
227            if verify_factorization(n, &p, &q) {
228                return Some((p, q));
229            }
230        }
231        a = a.add(&one);
232    }
233    None
234}
235
236/// Pollard's rho (`f(x) = x² + 1`, Floyd cycle detection): the general-purpose structural probe. Expected
237/// `O(N^{1/4})`, so it crushes moderate semiprimes but is bounded out on a large sound modulus.
238pub fn pollard_rho(n: &BigInt, max_iters: u64) -> Option<(BigInt, BigInt)> {
239    let (one, two) = (one(), two());
240    if !n.is_odd() {
241        return split(n, &two);
242    }
243    let f = |x: &BigInt| rem(&x.mul(x).add(&one), n);
244    let (mut x, mut y, mut d) = (two.clone(), two.clone(), one.clone());
245    let mut iters = 0u64;
246    while d == one && iters < max_iters {
247        x = f(&x);
248        y = f(&f(&y));
249        d = gcd(&x.sub(&y).abs(), n);
250        iters += 1;
251    }
252    split(n, &d)
253}
254
255/// Pollard's `p − 1`: catches a prime with a **smooth `p − 1`**. Raising a base to `B!` collapses to `1`
256/// modulo any prime whose `p − 1` is `B`-smooth, and `gcd(a − 1, N)` exposes it — the smoothness is the
257/// structure.
258pub fn pollard_p_minus_1(n: &BigInt, bound: u64) -> Option<(BigInt, BigInt)> {
259    let one = one();
260    let mut a = two();
261    for k in 2..=bound {
262        a = modpow(&a, &BigInt::from_u64(k), n);
263        if k % 16 == 0 || k == bound {
264            if let Some(f) = split(n, &gcd(&a.sub(&one), n)) {
265                return Some(f);
266            }
267        }
268    }
269    split(n, &gcd(&a.sub(&one), n))
270}
271
272/// Wiener's attack: catches a **small private exponent `d`** (`d < ⅓ N^{1/4}`). The convergents of the
273/// continued fraction of `e/N` include `k/d`; from `d` we recover `φ(N)`, then `p, q` as roots of
274/// `x² − (N − φ + 1)x + N`. A small `d` is a compressed private key — and this reuses the very
275/// continued-fraction / rational-reconstruction machinery the 2-adic FCSR rung was built on.
276pub fn wiener(e: &BigInt, n: &BigInt) -> Option<(BigInt, BigInt)> {
277    let (one, two, four) = (one(), two(), BigInt::from_i64(4));
278    let (mut num, mut den) = (e.clone(), n.clone());
279    // Convergent recurrence h_i / k_i (numerator = candidate multiplier k, denominator = candidate d).
280    let (mut h2, mut h1) = (BigInt::zero(), one.clone());
281    let (mut k2, mut k1) = (one.clone(), BigInt::zero());
282    for _ in 0..2000 {
283        if den.is_zero() {
284            break;
285        }
286        let (a, r) = num.div_rem(&den).expect("nonzero");
287        let h = a.mul(&h1).add(&h2); // candidate k (the small multiplier)
288        let k = a.mul(&k1).add(&k2); // candidate d (the private exponent)
289        if !h.is_zero() {
290            let (phi, prem) = e.mul(&k).sub(&one).div_rem(&h).expect("nonzero");
291            if prem.is_zero() {
292                // p, q are the roots of x² − (N − φ + 1)x + N.
293                let s = n.sub(&phi).add(&one); // p + q
294                let disc = s.mul(&s).sub(&four.mul(n));
295                if !disc.is_negative() {
296                    let sq = isqrt(&disc);
297                    if sq.mul(&sq) == disc {
298                        let p = s.sub(&sq).div_rem(&two).expect("nonzero").0;
299                        let q = s.add(&sq).div_rem(&two).expect("nonzero").0;
300                        if verify_factorization(n, &p, &q) {
301                            return Some((p, q));
302                        }
303                    }
304                }
305            }
306        }
307        num = den;
308        den = r;
309        (h2, h1) = (h1, h);
310        (k2, k1) = (k1, k);
311    }
312    None
313}
314
315// ---- Håstad broadcast: a MESSAGE-recovery lens (small public exponent + broadcast) ------------------
316//
317// Every attack above recovers the KEY. Håstad's recovers the MESSAGE, from a different structural
318// weakness: the same plaintext `m` sent to `k ≥ e` recipients under a small public exponent `e` and
319// distinct moduli. The ciphertexts are `cᵢ = mᵉ mod Nᵢ`, and since `m < Nᵢ` for all `i`, `mᵉ < ∏Nᵢ`.
320// The Chinese Remainder Theorem reconstructs `mᵉ mod ∏Nᵢ` — which, being smaller than the modulus, IS
321// `mᵉ` over the integers — and an integer `e`-th root pops out `m`. The plaintext is a low-complexity
322// object smeared across the broadcast; CRT gathers it back into a perfect power.
323
324fn pos_rem(a: &BigInt, m: &BigInt) -> BigInt {
325    let r = rem(a, m);
326    if r.is_negative() {
327        r.add(m)
328    } else {
329        r
330    }
331}
332
333/// Chinese Remainder Theorem: the unique `x` in `[0, ∏mᵢ)` with `x ≡ residuesᵢ (mod moduliᵢ)` for
334/// pairwise-coprime `moduli`, or `None` if a modular inverse fails (moduli not coprime).
335pub fn crt(residues: &[BigInt], moduli: &[BigInt]) -> Option<BigInt> {
336    if residues.is_empty() || residues.len() != moduli.len() {
337        return None;
338    }
339    let mut x = pos_rem(&residues[0], &moduli[0]);
340    let mut m = moduli[0].clone();
341    for i in 1..moduli.len() {
342        let mi = &moduli[i];
343        let inv = mod_inverse(&pos_rem(&m, mi), mi)?;
344        let diff = pos_rem(&residues[i].sub(&x), mi);
345        let t = pos_rem(&diff.mul(&inv), mi);
346        x = x.add(&m.mul(&t));
347        m = m.mul(mi);
348        x = pos_rem(&x, &m);
349    }
350    Some(x)
351}
352
353/// The integer `n`-th root `⌊x^{1/n}⌋` (Newton's method, overestimate seed then monotone descent, exact
354/// final adjustment — the `n`-th-root analogue of [`isqrt`]).
355pub fn integer_nth_root(x: &BigInt, n: u32) -> BigInt {
356    if n == 0 || x.is_negative() {
357        return BigInt::zero();
358    }
359    if n == 1 || x.is_zero() {
360        return x.clone();
361    }
362    let (one, two) = (one(), two());
363    let nn = BigInt::from_u64(n as u64);
364    let nm1 = BigInt::from_u64((n - 1) as u64);
365    let approx = x.to_f64().powf(1.0 / n as f64);
366    let mut r = if approx.is_finite() && approx >= 1.0 {
367        BigInt::parse_decimal(&format!("{approx:.0}")).unwrap_or_else(|| one.clone())
368    } else {
369        one.clone()
370    };
371    if r.is_zero() {
372        r = one.clone();
373    }
374    while r.pow(n) < *x {
375        r = r.mul(&two); // force an overestimate
376    }
377    loop {
378        let (q, _) = x.div_rem(&r.pow(n - 1)).expect("nonzero");
379        let next = nm1.mul(&r).add(&q).div_rem(&nn).expect("nonzero").0;
380        if next >= r {
381            break;
382        }
383        r = next;
384    }
385    while r.pow(n) > *x {
386        r = r.sub(&one);
387    }
388    while r.add(&one).pow(n) <= *x {
389        r = r.add(&one);
390    }
391    r
392}
393
394/// Håstad's broadcast attack: recover the plaintext `m` from `k ≥ e` ciphertexts `cᵢ = mᵉ mod Nᵢ` of the
395/// SAME message under a small public exponent `e` and distinct moduli (see the module note above). CRT
396/// reconstructs `mᵉ` exactly, and its integer `e`-th root is `m`. Returns the recovered message, or `None`
397/// if the CRT fails or the result is not a perfect `e`-th power (the precondition `k ≥ e` did not hold).
398pub fn hastad_broadcast_attack(e: u32, ciphertexts: &[BigInt], moduli: &[BigInt]) -> Option<BigInt> {
399    if (ciphertexts.len() as u32) < e {
400        return None;
401    }
402    let c = crt(ciphertexts, moduli)?;
403    let m = integer_nth_root(&c, e);
404    (m.pow(e) == c).then_some(m)
405}
406
407// ---- More message-recovery lenses: common modulus, Franklin–Reiter, low-exponent --------------------
408
409/// Extended Euclid: `(g, x, y)` with `x·a + y·b = g = gcd(a, b)`.
410fn ext_gcd(a: &BigInt, b: &BigInt) -> (BigInt, BigInt, BigInt) {
411    let (mut old_r, mut r) = (a.clone(), b.clone());
412    let (mut old_s, mut s) = (one(), BigInt::zero());
413    let (mut old_t, mut t) = (BigInt::zero(), one());
414    while !r.is_zero() {
415        let (q, _) = old_r.div_rem(&r).expect("nonzero");
416        let nr = old_r.sub(&q.mul(&r));
417        old_r = r;
418        r = nr;
419        let ns = old_s.sub(&q.mul(&s));
420        old_s = s;
421        s = ns;
422        let nt = old_t.sub(&q.mul(&t));
423        old_t = t;
424        t = nt;
425    }
426    (old_r, old_s, old_t)
427}
428
429/// Common-modulus attack: recover `m` when the SAME message is sent to the same modulus `N` under two
430/// coprime public exponents `e₁, e₂` (a key-reuse mistake). Bézout gives `a·e₁ + b·e₂ = 1`, so
431/// `c₁ᵃ · c₂ᵇ = m^{a·e₁ + b·e₂} = m (mod N)` — negative exponents handled by inverting the ciphertext.
432pub fn common_modulus_attack(e1: &BigInt, e2: &BigInt, c1: &BigInt, c2: &BigInt, n: &BigInt) -> Option<BigInt> {
433    let (g, a, b) = ext_gcd(e1, e2);
434    if g != one() {
435        return None;
436    }
437    let t1 = if a.is_negative() {
438        modpow(&mod_inverse(c1, n)?, &a.negated(), n)
439    } else {
440        modpow(c1, &a, n)
441    };
442    let t2 = if b.is_negative() {
443        modpow(&mod_inverse(c2, n)?, &b.negated(), n)
444    } else {
445        modpow(c2, &b, n)
446    };
447    Some(rem(&t1.mul(&t2), n))
448}
449
450/// Low-exponent / no-padding attack: if `mᵉ < N` (a small message under a small public exponent with no
451/// padding), then `c = mᵉ` over the integers, so the plaintext is just the integer `e`-th root of `c`.
452pub fn low_exponent_message(c: &BigInt, e: u32) -> Option<BigInt> {
453    let m = integer_nth_root(c, e);
454    (m.pow(e) == *c).then_some(m)
455}
456
457fn poly_norm_mod(p: &[BigInt], n: &BigInt) -> Vec<BigInt> {
458    let mut v: Vec<BigInt> = p.iter().map(|c| pos_rem(c, n)).collect();
459    while v.len() > 1 && v.last().is_some_and(|c| c.is_zero()) {
460        v.pop();
461    }
462    if v.is_empty() {
463        v.push(BigInt::zero());
464    }
465    v
466}
467
468fn poly_mul_mod(a: &[BigInt], b: &[BigInt], n: &BigInt) -> Vec<BigInt> {
469    let mut out = vec![BigInt::zero(); a.len() + b.len() - 1];
470    for (i, ai) in a.iter().enumerate() {
471        for (j, bj) in b.iter().enumerate() {
472            out[i + j] = pos_rem(&out[i + j].add(&ai.mul(bj)), n);
473        }
474    }
475    poly_norm_mod(&out, n)
476}
477
478/// Remainder of `a` divided by `b` in `(ℤ/N)[x]` (long division, leading coefficient of `b` inverted mod
479/// `N`). `None` if that inverse fails — which would itself expose a factor of `N`.
480fn poly_rem_mod(a: &[BigInt], b: &[BigInt], n: &BigInt) -> Option<Vec<BigInt>> {
481    let b = poly_norm_mod(b, n);
482    let db = b.len() - 1;
483    if b.iter().all(|c| c.is_zero()) {
484        return None;
485    }
486    let lead_inv = mod_inverse(&b[db], n)?;
487    let mut r = poly_norm_mod(a, n);
488    while r.len() > db {
489        let dr = r.len() - 1;
490        let factor = pos_rem(&r[dr].mul(&lead_inv), n);
491        let shift = dr - db;
492        for i in 0..b.len() {
493            r[shift + i] = pos_rem(&r[shift + i].sub(&factor.mul(&b[i])), n);
494        }
495        r = poly_norm_mod(&r, n);
496    }
497    Some(r)
498}
499
500/// Monic GCD of two polynomials over `(ℤ/N)[x]` (Euclidean algorithm).
501fn poly_gcd_mod(a: &[BigInt], b: &[BigInt], n: &BigInt) -> Option<Vec<BigInt>> {
502    let mut a = poly_norm_mod(a, n);
503    let mut b = poly_norm_mod(b, n);
504    while !(b.len() == 1 && b[0].is_zero()) {
505        let r = poly_rem_mod(&a, &b, n)?;
506        a = b;
507        b = r;
508    }
509    Some(a)
510}
511
512/// Franklin–Reiter related-message attack: recover `m` from ciphertexts of two LINEARLY RELATED messages
513/// `m` and `m + r` (known `r`) under the same modulus `N` and a small public exponent `e`. Both
514/// `g₁(x) = xᵉ − c₁` and `g₂(x) = (x + r)ᵉ − c₂` have the root `x = m mod N`, so their GCD over `(ℤ/N)[x]`
515/// is the linear factor `x − m`, and `m` is read straight off it. The lens: two ciphertexts sharing a
516/// hidden root, extracted by polynomial GCD.
517pub fn franklin_reiter_attack(e: u32, r: &BigInt, c1: &BigInt, c2: &BigInt, n: &BigInt) -> Option<BigInt> {
518    let mut g1 = vec![BigInt::zero(); (e + 1) as usize];
519    g1[0] = pos_rem(&c1.negated(), n);
520    g1[e as usize] = one();
521
522    let xr = vec![pos_rem(r, n), one()];
523    let mut g2 = vec![one()];
524    for _ in 0..e {
525        g2 = poly_mul_mod(&g2, &xr, n);
526    }
527    g2[0] = pos_rem(&g2[0].sub(c2), n);
528
529    let g = poly_gcd_mod(&g1, &g2, n)?;
530    if g.len() == 2 {
531        let m = pos_rem(&g[0].negated().mul(&mod_inverse(&g[1], n)?), n);
532        if modpow(&m, &BigInt::from_u64(e as u64), n) == pos_rem(c1, n) {
533            return Some(m);
534        }
535    }
536    None
537}
538
539// ---- Coppersmith's method: the LATTICE lens on RSA (a break with no factoring shortcut) --------------
540//
541// Polynomial arithmetic over the integers (coefficients low-to-high; `p[k]` = coefficient of `xᵏ`).
542
543fn poly_mul(a: &[BigInt], b: &[BigInt]) -> Vec<BigInt> {
544    if a.is_empty() || b.is_empty() {
545        return Vec::new();
546    }
547    let mut out = vec![BigInt::zero(); a.len() + b.len() - 1];
548    for (i, ai) in a.iter().enumerate() {
549        for (j, bj) in b.iter().enumerate() {
550            out[i + j] = out[i + j].add(&ai.mul(bj));
551        }
552    }
553    out
554}
555
556fn poly_pow(base: &[BigInt], e: usize) -> Vec<BigInt> {
557    let mut acc = vec![one()];
558    for _ in 0..e {
559        acc = poly_mul(&acc, base);
560    }
561    acc
562}
563
564fn poly_eval(coeffs: &[BigInt], x: &BigInt) -> BigInt {
565    coeffs.iter().rev().fold(BigInt::zero(), |acc, c| acc.mul(x).add(c))
566}
567
568/// Find an integer root of `coeffs` in `[0, bound]`, if one exists: a floating-point Newton search
569/// (coefficients normalized to keep the doubles finite) proposes candidates, each verified EXACTLY with
570/// `BigInt` evaluation. Root-finding is the easy step — the miracle is that Coppersmith's lattice produced
571/// a polynomial whose small root is an *integer* root at all.
572fn poly_integer_root(coeffs: &[BigInt], bound: &BigInt) -> Option<BigInt> {
573    let maxc = coeffs.iter().map(|c| c.abs()).max()?;
574    if maxc.is_zero() {
575        return None;
576    }
577    // For a small bound, scan exactly — the root of the recovered polynomial is trivial to locate once
578    // the lattice has produced it (this is post-processing, not the attack). Larger bounds fall through
579    // to the Newton search below (as every Coppersmith implementation does).
580    if bound.to_f64() <= 4_200_000.0 {
581        let mut x = BigInt::zero();
582        let one = one();
583        while x <= *bound {
584            if poly_eval(coeffs, &x).is_zero() {
585                return Some(x);
586            }
587            x = x.add(&one);
588        }
589        return None;
590    }
591    let cf: Vec<f64> =
592        coeffs.iter().map(|c| Rational::new(c.clone(), maxc.clone()).map(|r| r.to_f64()).unwrap_or(0.0)).collect();
593    let eval = |x: f64| cf.iter().rev().fold(0.0, |acc, &c| acc * x + c);
594    let deriv = |x: f64| (1..cf.len()).rev().fold(0.0, |acc, k| acc * x + cf[k] * k as f64);
595    let bound_f = bound.to_f64();
596
597    let mut seeds: Vec<f64> = Vec::new();
598    let mut p = 1.0;
599    while p <= bound_f {
600        seeds.push(p);
601        p *= 1.5;
602    }
603    for i in 0..=64 {
604        seeds.push(bound_f * i as f64 / 64.0);
605    }
606
607    let mut seen: Vec<BigInt> = Vec::new();
608    for &s in &seeds {
609        let mut x = s;
610        for _ in 0..200 {
611            let d = deriv(x);
612            if d.abs() < 1e-15 {
613                break;
614            }
615            let nx = x - eval(x) / d;
616            if !nx.is_finite() {
617                break;
618            }
619            if (nx - x).abs() < 0.4 {
620                x = nx;
621                break;
622            }
623            x = nx;
624        }
625        if x.is_finite() && x >= -1.0 && x <= bound_f + 1.0 {
626            if let Some(cand) = BigInt::parse_decimal(&format!("{:.0}", x.max(0.0))) {
627                if !seen.contains(&cand) {
628                    if cand <= *bound && poly_eval(coeffs, &cand).is_zero() {
629                        return Some(cand);
630                    }
631                    seen.push(cand);
632                }
633            }
634        }
635    }
636    None
637}
638
639fn poly_derivative(p: &[BigInt]) -> Vec<BigInt> {
640    (1..p.len()).map(|k| p[k].mul(&BigInt::from_i64(k as i64))).collect()
641}
642
643/// Nearest integer to `a/b` (ties away from zero).
644fn round_div_factor(a: &BigInt, b: &BigInt) -> BigInt {
645    let (q, r) = a.div_rem(b).expect("nonzero");
646    if r.is_zero() {
647        return q;
648    }
649    if BigInt::from_i64(2).mul(&r.abs()) > b.abs() {
650        if a.is_negative() ^ b.is_negative() {
651            q.sub(&one())
652        } else {
653            q.add(&one())
654        }
655    } else {
656        q
657    }
658}
659
660/// Exact integer roots of `res` in `[−bound, bound]`: a floating-point Newton pass proposes seeds, and
661/// EXACT integer Newton (`x ← x − round(res(x)/res'(x))`, evaluated in `BigInt`) refines each to a true
662/// integer root — necessary because the resultant's coefficients are far too large for f64 to locate the
663/// root directly.
664fn integer_roots_of(res: &[BigInt], bound: &BigInt) -> Vec<BigInt> {
665    let deriv = poly_derivative(res);
666    let mut out: Vec<BigInt> = Vec::new();
667    for seed in real_root_candidates(res, bound) {
668        let mut x = seed;
669        for _ in 0..80 {
670            let fx = poly_eval(res, &x);
671            if fx.is_zero() {
672                break;
673            }
674            let dfx = poly_eval(&deriv, &x);
675            if dfx.is_zero() {
676                break;
677            }
678            let step = round_div_factor(&fx, &dfx);
679            if step.is_zero() {
680                break;
681            }
682            x = x.sub(&step);
683        }
684        if poly_eval(res, &x).is_zero() && x.abs() <= *bound && !out.contains(&x) {
685            out.push(x);
686        }
687    }
688    out
689}
690
691/// Factor `N` from the **high bits of one prime** (Coppersmith's method). We know `p = p_high + x₀` with
692/// `0 ≤ x₀ < 2^unknown_bits`; the monic linear polynomial `f(x) = x + p_high` has the small root `x₀`
693/// modulo the unknown factor `p`. Coppersmith builds a lattice from the `N`-power and `x`-shift multiples
694/// of `f` (all vanishing modulo `pᵐ` at `x₀`), LLL-reduces it, and reads a short vector whose small root
695/// is an INTEGER root of a real polynomial — recovering `x₀`, hence `p`. This is the LATTICE lens: a
696/// factorization from PARTIAL knowledge that no factoring shortcut (Fermat, rho, `p−1`) can provide.
697pub fn coppersmith_factor_high_bits(n: &BigInt, p_high: &BigInt, unknown_bits: u32) -> Option<(BigInt, BigInt)> {
698    let two = two();
699    let mut x_bound = one();
700    for _ in 0..unknown_bits {
701        x_bound = x_bound.mul(&two);
702    }
703    // A larger lattice pushes `unknown_bits` toward the N^{1/4} limit. The float-Gram-Schmidt LLL
704    // (`lll_reduce_bigint`) handles this dimension in milliseconds — the exact-rational version could not.
705    let (m, t) = (4usize, 4usize);
706    let deg = m + t;
707    let f = vec![p_high.clone(), one()]; // f(x) = p_high + x
708
709    // Polynomials that vanish modulo pᵐ at x₀: gᵢ = N^{m-i}·fⁱ (i=0..m) and hⱼ = xʲ·fᵐ (j=1..t).
710    let mut polys: Vec<Vec<BigInt>> = Vec::new();
711    for i in 0..=m {
712        let scale = n.pow((m - i) as u32);
713        polys.push(poly_pow(&f, i).iter().map(|c| c.mul(&scale)).collect());
714    }
715    let fm = poly_pow(&f, m);
716    for j in 1..=t {
717        let mut p = vec![BigInt::zero(); j];
718        p.extend_from_slice(&fm);
719        polys.push(p);
720    }
721
722    // Lattice rows: coefficient k scaled by Xᵏ (so a short row ⇒ a small-normed polynomial in x·X).
723    let mut xpow = vec![one()];
724    for k in 1..=deg {
725        xpow.push(xpow[k - 1].mul(&x_bound));
726    }
727    let rows: Vec<Vec<BigInt>> = polys
728        .iter()
729        .map(|p| (0..=deg).map(|k| p.get(k).cloned().unwrap_or_else(BigInt::zero).mul(&xpow[k])).collect())
730        .collect();
731
732    let reduced = crate::lattice::lll_reduce_bigint_fp(&rows);
733    for row in &reduced {
734        // Unscale: hₖ = rowₖ / Xᵏ (exact — every lattice entry is an integer multiple of Xᵏ).
735        let mut h = Vec::with_capacity(deg + 1);
736        let mut ok = true;
737        for k in 0..=deg {
738            let (q, r) = row[k].div_rem(&xpow[k]).expect("nonzero");
739            if !r.is_zero() {
740                ok = false;
741                break;
742            }
743            h.push(q);
744        }
745        if !ok || h.iter().all(|c| c.is_zero()) {
746            continue;
747        }
748        if let Some(x0) = poly_integer_root(&h, &x_bound) {
749            if let Some(f) = split(n, &p_high.add(&x0)) {
750                return Some(f);
751            }
752        }
753    }
754    None
755}
756
757/// Derive the RSA private exponent `d = e⁻¹ mod φ(N)` from the public exponent and the two primes
758/// (`φ = (p−1)(q−1)`), or `None` if `e` is not coprime to `φ`. The "if you can factor, you can break RSA"
759/// direction: the factorization hands over the private key.
760pub fn rsa_private_exponent(e: &BigInt, p: &BigInt, q: &BigInt) -> Option<BigInt> {
761    let one = one();
762    mod_inverse(e, &p.sub(&one).mul(&q.sub(&one)))
763}
764
765/// Factor `N` from the RSA private exponent `d` (Miller's deterministic reduction). Since `e·d − 1` is a
766/// multiple of `λ(N)`, writing it as `t·2ˢ` and raising a base `g` to `t·2ⁱ` walks a chain that ends at
767/// `1`; a step where the value squares to `1` while itself being `≠ ±1` is a NONTRIVIAL square root of
768/// unity, and `gcd(x − 1, N)` splits `N`. This is the converse of [`rsa_private_exponent`]: it proves
769/// that recovering the private key is **computationally equivalent to factoring the modulus** — the two
770/// are one problem, so RSA breaks exactly when `N` factors. Returns `(p, q)` or `None` (no base worked).
771pub fn factor_via_private_exponent(n: &BigInt, e: &BigInt, d: &BigInt) -> Option<(BigInt, BigInt)> {
772    let (one, two) = (one(), two());
773    let n1 = n.sub(&one);
774    let k = e.mul(d).sub(&one);
775    if k.is_zero() || k.is_negative() {
776        return None;
777    }
778    // k = t·2ˢ, t odd.
779    let mut t = k;
780    let mut s = 0u32;
781    while !t.is_odd() {
782        t = t.div_rem(&two).expect("nonzero").0;
783        s += 1;
784    }
785    for &gb in MR_BASES {
786        let g = BigInt::from_u64(gb);
787        let shared = gcd(&g, n);
788        if shared != one {
789            if let Some(f) = split(n, &shared) {
790                return Some(f);
791            }
792            continue;
793        }
794        let mut x = modpow(&g, &t, n);
795        if x == one || x == n1 {
796            continue;
797        }
798        for _ in 0..s {
799            let y = modpow(&x, &two, n);
800            if y == one {
801                // x is a square root of 1 with x ∉ {1, N−1}: a nontrivial root splits N.
802                if let Some(f) = split(n, &gcd(&x.sub(&one), n)) {
803                    return Some(f);
804                }
805                break;
806            }
807            if y == n1 {
808                break; // x² ≡ −1: this base gives no nontrivial root
809            }
810            x = y;
811        }
812    }
813    None
814}
815
816/// Batch GCD: catches a **shared prime** reused across moduli — the classic failure of low-entropy key
817/// generation. Any pair with `gcd(Nᵢ, Nⱼ) > 1` hands over the common factor for free.
818pub fn batch_gcd(moduli: &[BigInt]) -> Vec<(usize, usize, BigInt)> {
819    let one = one();
820    let mut hits = Vec::new();
821    for i in 0..moduli.len() {
822        for j in (i + 1)..moduli.len() {
823            let g = gcd(&moduli[i], &moduli[j]);
824            if g > one {
825                hits.push((i, j, g));
826            }
827        }
828    }
829    hits
830}
831
832// ---- Dixon's method: RSA's ring structure broken by OUR GF(2) symmetry breaking -------------------
833//
834// The algebraic rungs (D, G, J) all turn on one primitive: finding a GF(2) linear DEPENDENCY — a subset
835// of vectors that XORs to zero. That same symmetry break factors integers. Dixon's method (the heart of
836// the quadratic sieve) collects numbers `r` whose square `r² mod N` is smooth over a small prime base,
837// records each as an exponent vector, and finds a subset whose product is a PERFECT SQUARE — which is
838// exactly a GF(2) kernel vector of the parity matrix. That yields `x² ≡ y² (mod N)`, and `gcd(x−y, N)`
839// splits `N`. It uses RSA's own ring structure (congruences of squares) and our own symmetry breaking
840// (the dependency search), so it breaks small and medium moduli; on a sound modulus it degrades to the
841// sub-exponential regime (the real quadratic-sieve / GNFS frontier) — the honest boundary.
842
843/// Every GF(2) linear dependency among `rows` (subsets of row indices that XOR to the zero vector), found
844/// by Gaussian elimination with identity tracking. This is the symmetry-break at the core of Dixon's
845/// method, shared with the algebraic rungs.
846fn gf2_dependencies(rows: &[Vec<bool>], ncols: usize) -> Vec<Vec<usize>> {
847    let m = rows.len();
848    let mut mat: Vec<(Vec<bool>, Vec<bool>)> = rows
849        .iter()
850        .enumerate()
851        .map(|(i, r)| {
852            let mut tag = vec![false; m];
853            tag[i] = true;
854            (r.clone(), tag)
855        })
856        .collect();
857    let mut pivot = 0;
858    for col in 0..ncols {
859        let Some(pr) = (pivot..m).find(|&i| mat[i].0[col]) else {
860            continue;
861        };
862        mat.swap(pivot, pr);
863        let (prow, ptag) = (mat[pivot].0.clone(), mat[pivot].1.clone());
864        for i in 0..m {
865            if i != pivot && mat[i].0[col] {
866                for c in 0..ncols {
867                    mat[i].0[c] ^= prow[c];
868                }
869                for c in 0..m {
870                    mat[i].1[c] ^= ptag[c];
871                }
872            }
873        }
874        pivot += 1;
875    }
876    mat.iter()
877        .filter(|(prim, _)| prim.iter().all(|&b| !b))
878        .map(|(_, tag)| (0..m).filter(|&i| tag[i]).collect())
879        .filter(|s: &Vec<usize>| !s.is_empty())
880        .collect()
881}
882
883/// Dixon's method: factor `N` via a congruence of squares found by a GF(2) dependency among smooth
884/// relations (see the module note above). Searches `r = ⌈√N⌉, ⌈√N⌉+1, …` for up to `tries` steps to
885/// gather relations whose `r² mod N` is smooth over `base`, then combines them. Returns `(p, q)` or
886/// `None` (not enough smooth relations found, or only trivial congruences). Reuses our GF(2)
887/// symmetry-breaking on RSA's ring structure.
888pub fn dixon_factor(n: &BigInt, base: &[u64], tries: usize) -> Option<(BigInt, BigInt)> {
889    let one = one();
890    let need = base.len() + 5;
891    let mut rels: Vec<(BigInt, Vec<u64>)> = Vec::new();
892    let mut r = isqrt(n).add(&one);
893    let mut steps = 0;
894    while rels.len() < need && steps < tries {
895        let mut s = rem(&r.mul(&r), n);
896        let mut exps = vec![0u64; base.len()];
897        for (bi, &b) in base.iter().enumerate() {
898            let bb = BigInt::from_u64(b);
899            while let Some((q, rr)) = s.div_rem(&bb) {
900                if rr.is_zero() {
901                    s = q;
902                    exps[bi] += 1;
903                } else {
904                    break;
905                }
906            }
907        }
908        if s == one {
909            rels.push((r.clone(), exps));
910        }
911        r = r.add(&one);
912        steps += 1;
913    }
914    if rels.len() < 2 {
915        return None;
916    }
917    let rows: Vec<Vec<bool>> = rels.iter().map(|(_, e)| e.iter().map(|&x| x & 1 == 1).collect()).collect();
918    for dep in gf2_dependencies(&rows, base.len()) {
919        let mut x = one.clone();
920        let mut total = vec![0u64; base.len()];
921        for &i in &dep {
922            x = rem(&x.mul(&rels[i].0), n);
923            for (k, &e) in rels[i].1.iter().enumerate() {
924                total[k] += e;
925            }
926        }
927        let mut y = one.clone();
928        for (k, &e) in total.iter().enumerate() {
929            y = rem(&y.mul(&modpow(&BigInt::from_u64(base[k]), &BigInt::from_u64(e / 2), n)), n);
930        }
931        let diff = if x >= y { x.sub(&y) } else { x.add(n).sub(&y) };
932        for cand in [gcd(&diff, n), gcd(&x.add(&y), n)] {
933            if let Some(f) = split(n, &cand) {
934                return Some(f);
935            }
936        }
937    }
938    None
939}
940
941// ---- Quadratic sieve: real sieving (the L[½] algorithm, GNFS's predecessor) ------------------------
942//
943// Dixon trial-divides `r² mod N` at every step; the quadratic sieve replaces that with a SIEVE. It uses
944// `Q(x) = (⌈√N⌉ + x)² − N`, small for small `x`, and for each factor-base prime `p` marks the interval
945// positions where `p | Q(x)` (the two roots of `Q ≡ 0 mod p`) by adding `ln p` to a running total. Where
946// that total reaches `ln|Q(x)|`, `Q(x)` is smooth — found without dividing. The smooth `Q(x)` become
947// relations, and the SAME GF(2) dependency search factors `N`. This is sub-exponential (`L[½]`); its far
948// deeper cousin GNFS is `L[⅓]` — still sub-exponential, so both confirm the wall rather than breaching it.
949
950fn is_small_prime(p: u64) -> bool {
951    if p < 2 {
952        return false;
953    }
954    let mut d = 2u64;
955    while d * d <= p {
956        if p % d == 0 {
957            return false;
958        }
959        d += 1;
960    }
961    true
962}
963
964fn bigint_mod_u64(n: &BigInt, p: u64) -> u64 {
965    n.div_rem(&BigInt::from_u64(p)).expect("nonzero").1.to_i64().unwrap_or(0) as u64
966}
967
968/// A square root of `a` modulo the small prime `p` (brute force — the factor-base primes are tiny), or
969/// `None` if `a` is a non-residue.
970fn modular_sqrt_u64(a: u64, p: u64) -> Option<u64> {
971    let a = a % p;
972    (0..p).find(|&r| (r as u128 * r as u128 % p as u128) as u64 == a)
973}
974
975/// The quadratic sieve (see the module note above): factor `N` by log-sieving `Q(x) = (⌈√N⌉+x)² − N`
976/// over `[0, m_interval)` against the factor base of primes `≤ b` for which `N` is a quadratic residue,
977/// then combining smooth relations through a GF(2) dependency. Returns `(p, q)` or `None`.
978pub fn quadratic_sieve(n: &BigInt, b: u64, m_interval: usize) -> Option<(BigInt, BigInt)> {
979    let one = one();
980    let a = isqrt(n).add(&one);
981
982    // Factor base: primes p ≤ b with N a quadratic residue mod p, and their two roots of Q ≡ 0 mod p.
983    let mut base: Vec<u64> = Vec::new();
984    let mut roots: Vec<(u64, u64)> = Vec::new();
985    for p in 2..=b {
986        if !is_small_prime(p) {
987            continue;
988        }
989        let np = bigint_mod_u64(n, p);
990        if p == 2 {
991            base.push(2);
992            roots.push((np % 2, np % 2));
993        } else if let Some(r) = modular_sqrt_u64(np, p) {
994            base.push(p);
995            roots.push((r, p - r));
996        }
997    }
998    if base.len() < 3 {
999        return None;
1000    }
1001
1002    // Log-sieve the interval.
1003    let mut sieve = vec![0f64; m_interval];
1004    for (bi, &p) in base.iter().enumerate() {
1005        let am = bigint_mod_u64(&a, p) as i64;
1006        let (r1, r2) = roots[bi];
1007        let both = [r1, r2];
1008        let candidates: &[u64] = if p == 2 { &both[..1] } else { &both[..] };
1009        for &root in candidates {
1010            let mut x = ((root as i64 - am).rem_euclid(p as i64)) as usize;
1011            while x < m_interval {
1012                sieve[x] += (p as f64).ln();
1013                x += p as usize;
1014            }
1015        }
1016    }
1017
1018    // Collect smooth relations: sieve pinpoints candidates, exact trial division confirms them.
1019    let mut rels: Vec<(BigInt, Vec<u64>)> = Vec::new();
1020    for x in 0..m_interval {
1021        let ax = a.add(&BigInt::from_u64(x as u64));
1022        let q = ax.mul(&ax).sub(n);
1023        if q.is_zero() {
1024            continue;
1025        }
1026        // The sieve accounts for one ln(p) per distinct factor; require it to cover at least half the
1027        // log-mass of Q(x) (generous, so no smooth value is missed), then confirm by exact division.
1028        if sieve[x] < 0.5 * q.to_f64().ln() {
1029            continue;
1030        }
1031        let mut qq = q;
1032        let mut exps = vec![0u64; base.len()];
1033        for (bi, &p) in base.iter().enumerate() {
1034            let bp = BigInt::from_u64(p);
1035            while let Some((quo, rr)) = qq.div_rem(&bp) {
1036                if rr.is_zero() {
1037                    qq = quo;
1038                    exps[bi] += 1;
1039                } else {
1040                    break;
1041                }
1042            }
1043        }
1044        if qq == one {
1045            rels.push((ax, exps));
1046            if rels.len() > base.len() + 5 {
1047                break;
1048            }
1049        }
1050    }
1051    if rels.len() < 2 {
1052        return None;
1053    }
1054
1055    let rows: Vec<Vec<bool>> = rels.iter().map(|(_, e)| e.iter().map(|&x| x & 1 == 1).collect()).collect();
1056    for dep in gf2_dependencies(&rows, base.len()) {
1057        let mut x = one.clone();
1058        let mut total = vec![0u64; base.len()];
1059        for &i in &dep {
1060            x = rem(&x.mul(&rels[i].0), n);
1061            for (k, &e) in rels[i].1.iter().enumerate() {
1062                total[k] += e;
1063            }
1064        }
1065        let mut y = one.clone();
1066        for (k, &e) in total.iter().enumerate() {
1067            y = rem(&y.mul(&modpow(&BigInt::from_u64(base[k]), &BigInt::from_u64(e / 2), n)), n);
1068        }
1069        let diff = if x >= y { x.sub(&y) } else { x.add(n).sub(&y) };
1070        for cand in [gcd(&diff, n), gcd(&x.add(&y), n)] {
1071            if let Some(f) = split(n, &cand) {
1072                return Some(f);
1073            }
1074        }
1075    }
1076    None
1077}
1078
1079// ---- Resultants: eliminate a variable from two bivariate polynomials (Boneh–Durfee's core step) ------
1080//
1081// A bivariate polynomial is a map from monomial `(i, j)` (meaning `xⁱyʲ`) to its integer coefficient.
1082type BPoly = std::collections::BTreeMap<(u32, u32), BigInt>;
1083
1084/// The degree of a dense univariate polynomial (coefficients low-to-high), ignoring trailing zeros.
1085fn poly_deg(p: &[BigInt]) -> usize {
1086    let mut d = p.len();
1087    while d > 0 && p[d - 1].is_zero() {
1088        d -= 1;
1089    }
1090    d.saturating_sub(1)
1091}
1092
1093/// The determinant of an integer matrix by the fraction-free Bareiss algorithm — exact, no rationals.
1094fn det_bareiss(mut a: Vec<Vec<BigInt>>) -> BigInt {
1095    let n = a.len();
1096    if n == 0 {
1097        return one();
1098    }
1099    let mut prev = one();
1100    let mut neg = false;
1101    for k in 0..n - 1 {
1102        if a[k][k].is_zero() {
1103            match (k + 1..n).find(|&i| !a[i][k].is_zero()) {
1104                Some(p) => {
1105                    a.swap(k, p);
1106                    neg = !neg;
1107                }
1108                None => return BigInt::zero(),
1109            }
1110        }
1111        for i in k + 1..n {
1112            for j in k + 1..n {
1113                let num = a[i][j].mul(&a[k][k]).sub(&a[i][k].mul(&a[k][j]));
1114                a[i][j] = num.div_rem(&prev).expect("Bareiss division is exact").0;
1115            }
1116        }
1117        prev = a[k][k].clone();
1118    }
1119    if neg {
1120        a[n - 1][n - 1].negated()
1121    } else {
1122        a[n - 1][n - 1].clone()
1123    }
1124}
1125
1126/// The resultant of two univariate polynomials (coefficients low-to-high) — the Sylvester determinant.
1127fn univariate_resultant(a: &[BigInt], b: &[BigInt]) -> BigInt {
1128    let (da, db) = (poly_deg(a), poly_deg(b));
1129    if a.iter().all(|c| c.is_zero()) || b.iter().all(|c| c.is_zero()) {
1130        return BigInt::zero();
1131    }
1132    if da == 0 {
1133        return a[0].pow(db as u32);
1134    }
1135    if db == 0 {
1136        return b[0].pow(da as u32);
1137    }
1138    let n = da + db;
1139    let mut syl = vec![vec![BigInt::zero(); n]; n];
1140    for r in 0..db {
1141        for c in 0..=da {
1142            syl[r][r + c] = a[da - c].clone();
1143        }
1144    }
1145    for r in 0..da {
1146        for c in 0..=db {
1147            syl[db + r][r + c] = b[db - c].clone();
1148        }
1149    }
1150    det_bareiss(syl)
1151}
1152
1153/// Evaluate a bivariate polynomial at `y = t`, returning the univariate polynomial in `x` (low-to-high).
1154fn eval_bivar_at_y(g: &BPoly, t: &BigInt) -> Vec<BigInt> {
1155    let max_i = g.keys().map(|&(i, _)| i).max().unwrap_or(0) as usize;
1156    let mut uni = vec![BigInt::zero(); max_i + 1];
1157    for (&(i, j), c) in g {
1158        uni[i as usize] = uni[i as usize].add(&c.mul(&t.pow(j)));
1159    }
1160    uni
1161}
1162
1163/// Lagrange-interpolate the unique polynomial (coefficients low-to-high) through `points`.
1164fn lagrange_interpolate(points: &[(BigInt, BigInt)]) -> Vec<BigInt> {
1165    let n = points.len();
1166    let mut acc = vec![Rational::zero(); n];
1167    for (i, (xi, yi)) in points.iter().enumerate() {
1168        // Basis polynomial Lᵢ(x) = Π_{m≠i} (x − xₘ)/(xᵢ − xₘ), scaled by yᵢ.
1169        let mut basis = vec![Rational::zero(); n];
1170        basis[0] = Rational::from_bigint(yi.clone());
1171        let mut denom = Rational::from_i64(1);
1172        let mut deg = 0;
1173        for (m, (xm, _)) in points.iter().enumerate() {
1174            if m == i {
1175                continue;
1176            }
1177            // Multiply basis by (x − xₘ).
1178            for d in (0..=deg).rev() {
1179                let shifted = basis[d].clone();
1180                basis[d + 1] = basis[d + 1].add(&shifted);
1181                basis[d] = basis[d].mul(&Rational::from_bigint(xm.negated()));
1182            }
1183            deg += 1;
1184            denom = denom.mul(&Rational::from_bigint(xi.sub(xm)));
1185        }
1186        let inv = denom.recip().expect("distinct nodes");
1187        for d in 0..n {
1188            acc[d] = acc[d].add(&basis[d].mul(&inv));
1189        }
1190    }
1191    acc.iter().map(|r| r.round()).collect()
1192}
1193
1194/// Eliminate `x` from two bivariate polynomials: `Resₓ(g1, g2)`, a univariate polynomial in `y`
1195/// (coefficients low-to-high) whose roots include every shared `y`-coordinate. Computed by evaluating the
1196/// resultant at enough integer `y`-points and interpolating — avoiding a symbolic polynomial determinant.
1197fn bivariate_resultant_x(g1: &BPoly, g2: &BPoly) -> Vec<BigInt> {
1198    let dx = |g: &BPoly| g.keys().map(|&(i, _)| i).max().unwrap_or(0) as usize;
1199    let dy = |g: &BPoly| g.keys().map(|&(_, j)| j).max().unwrap_or(0) as usize;
1200    let degree = dx(g1) * dy(g2) + dx(g2) * dy(g1);
1201    let points: Vec<(BigInt, BigInt)> = (0..=degree as i64)
1202        .map(|t| {
1203            let ty = BigInt::from_i64(t);
1204            let r = univariate_resultant(&eval_bivar_at_y(g1, &ty), &eval_bivar_at_y(g2, &ty));
1205            (ty, r)
1206        })
1207        .collect();
1208    lagrange_interpolate(&points)
1209}
1210
1211fn bpoly_mul(a: &BPoly, b: &BPoly) -> BPoly {
1212    let mut out = BPoly::new();
1213    for (&(i1, j1), c1) in a {
1214        for (&(i2, j2), c2) in b {
1215            let e = out.entry((i1 + i2, j1 + j2)).or_insert_with(BigInt::zero);
1216            *e = e.add(&c1.mul(c2));
1217        }
1218    }
1219    out.retain(|_, c| !c.is_zero());
1220    out
1221}
1222
1223fn bpoly_pow(base: &BPoly, e: u32) -> BPoly {
1224    let mut acc = BPoly::new();
1225    acc.insert((0, 0), one());
1226    for _ in 0..e {
1227        acc = bpoly_mul(&acc, base);
1228    }
1229    acc
1230}
1231
1232fn bpoly_eval(g: &BPoly, x: &BigInt, y: &BigInt) -> BigInt {
1233    g.iter().fold(BigInt::zero(), |acc, (&(i, j), c)| acc.add(&c.mul(&x.pow(i)).mul(&y.pow(j))))
1234}
1235
1236/// Substitute `x = xv` into a bivariate polynomial, returning the univariate polynomial in `y`
1237/// (coefficients low-to-high).
1238fn bpoly_subst_x(g: &BPoly, xv: &BigInt) -> Vec<BigInt> {
1239    let max_j = g.keys().map(|&(_, j)| j).max().unwrap_or(0) as usize;
1240    let mut uni = vec![BigInt::zero(); max_j + 1];
1241    for (&(i, j), c) in g {
1242        uni[j as usize] = uni[j as usize].add(&c.mul(&xv.pow(i)));
1243    }
1244    uni
1245}
1246
1247/// Build the Boneh–Durfee lattice, LLL-reduce it (fast float-Gram-Schmidt), and return the short
1248/// bivariate polynomials (unscaled) that should share the root `(k, −(p+q))`, plus the `y`-bound.
1249fn bd_reduced_polys(n: &BigInt, e: &BigInt, m: usize, t: usize, x_bound: &BigInt) -> (Vec<BPoly>, BigInt) {
1250    let one = one();
1251    let y_bound = isqrt(n).mul(&BigInt::from_i64(3));
1252
1253    let mut f = BPoly::new();
1254    f.insert((1, 1), one.clone());
1255    f.insert((1, 0), n.add(&one));
1256    f.insert((0, 0), one.clone());
1257
1258    let mut shifts: Vec<BPoly> = Vec::new();
1259    for k in 0..=m {
1260        let fke: BPoly = {
1261            let scale = e.pow((m - k) as u32);
1262            bpoly_pow(&f, k as u32).into_iter().map(|(key, c)| (key, c.mul(&scale))).collect()
1263        };
1264        for i in 0..=(m - k) {
1265            shifts.push(fke.iter().map(|(&(a, b), c)| ((a + i as u32, b), c.clone())).collect());
1266        }
1267        for j in 1..=t {
1268            shifts.push(fke.iter().map(|(&(a, b), c)| ((a, b + j as u32), c.clone())).collect());
1269        }
1270    }
1271
1272    let mut monos: std::collections::BTreeSet<(u32, u32)> = std::collections::BTreeSet::new();
1273    for s in &shifts {
1274        monos.extend(s.keys());
1275    }
1276    let monos: Vec<(u32, u32)> = monos.into_iter().collect();
1277    let scale_of: Vec<BigInt> = monos.iter().map(|&(i, j)| x_bound.pow(i).mul(&y_bound.pow(j))).collect();
1278    let col: std::collections::HashMap<(u32, u32), usize> =
1279        monos.iter().enumerate().map(|(i, &k)| (k, i)).collect();
1280    let rows: Vec<Vec<BigInt>> = shifts
1281        .iter()
1282        .map(|s| {
1283            let mut row = vec![BigInt::zero(); monos.len()];
1284            for (&key, c) in s {
1285                let ci = col[&key];
1286                row[ci] = c.mul(&scale_of[ci]);
1287            }
1288            row
1289        })
1290        .collect();
1291
1292    let reduced = crate::lattice::lll_reduce_bigint_fp(&rows);
1293    let polys: Vec<BPoly> = reduced
1294        .iter()
1295        .map(|row| {
1296            let mut g = BPoly::new();
1297            for (ci, &key) in monos.iter().enumerate() {
1298                if !row[ci].is_zero() {
1299                    let (q, r) = row[ci].div_rem(&scale_of[ci]).expect("nonzero");
1300                    if r.is_zero() {
1301                        g.insert(key, q);
1302                    }
1303                }
1304            }
1305            g
1306        })
1307        .filter(|g| !g.is_empty())
1308        .collect();
1309    (polys, y_bound)
1310}
1311
1312/// Round-tripping real root candidates of `res` (a univariate polynomial, low-to-high) in `[−bound,
1313/// bound]`, from a floating-point Newton search — proposals, verified exactly by the caller.
1314fn real_root_candidates(res: &[BigInt], bound: &BigInt) -> Vec<BigInt> {
1315    let maxc = res.iter().map(|c| c.abs()).max().filter(|c| !c.is_zero());
1316    let Some(maxc) = maxc else {
1317        return Vec::new();
1318    };
1319    let cf: Vec<f64> =
1320        res.iter().map(|c| Rational::new(c.clone(), maxc.clone()).map(|r| r.to_f64()).unwrap_or(0.0)).collect();
1321    let eval = |x: f64| cf.iter().rev().fold(0.0, |acc, &c| acc * x + c);
1322    let deriv = |x: f64| (1..cf.len()).rev().fold(0.0, |acc, k| acc * x + cf[k] * k as f64);
1323    let bf = bound.to_f64();
1324    let mut seeds: Vec<f64> = Vec::new();
1325    let mut p = 1.0;
1326    while p <= bf {
1327        seeds.push(p);
1328        seeds.push(-p);
1329        p *= 1.3;
1330    }
1331    let mut out: Vec<BigInt> = Vec::new();
1332    for &s in &seeds {
1333        let mut x = s;
1334        for _ in 0..200 {
1335            let d = deriv(x);
1336            if d.abs() < 1e-18 || !x.is_finite() {
1337                break;
1338            }
1339            let nx = x - eval(x) / d;
1340            if !nx.is_finite() {
1341                break;
1342            }
1343            if (nx - x).abs() < 0.4 {
1344                x = nx;
1345                break;
1346            }
1347            x = nx;
1348        }
1349        if x.is_finite() && x.abs() <= bf * 1.01 {
1350            if let Some(c) = BigInt::parse_decimal(&format!("{:.0}", x.abs())) {
1351                let cand = if x < 0.0 { c.negated() } else { c };
1352                if !out.contains(&cand) {
1353                    out.push(cand);
1354                }
1355            }
1356        }
1357    }
1358    out
1359}
1360
1361/// Boneh–Durfee: recover the factorization from a **small private exponent** `d < N^{0.284}` — beyond
1362/// Wiener's `N^{0.25}` — by bivariate Coppersmith. Since `e·d − 1 = k·φ(N)` and `φ(N) = N + 1 − (p+q)`,
1363/// the polynomial `f(x, y) = x·y + (N+1)·x + 1` has the small root `(x₀, y₀) = (k, −(p+q))` modulo `e`.
1364/// The lattice of `x`- and `y`-shifts of `f`, LLL-reduced (fast float-Gram-Schmidt), yields short bivariate
1365/// polynomials sharing that root; a resultant eliminates `x`, its root gives `s = p+q`, and `z² − s·z + N`
1366/// splits `N`. `m`, `t` size the lattice; `x_bound = N^δ` bounds `k`. Returns `(p, q)` or `None`.
1367pub fn boneh_durfee(n: &BigInt, e: &BigInt, m: usize, t: usize, x_bound: &BigInt) -> Option<(BigInt, BigInt)> {
1368    let (polys, y_bound) = bd_reduced_polys(n, e, m, t, x_bound);
1369
1370    // Read the shared root (k, −s) off the reduced polynomials: k is small (< X), so scan it; substituting
1371    // x = k into a vanishing polynomial gives a low-degree polynomial in y whose root is −s, found
1372    // reliably. `s = p+q` then splits N via z² − s·z + N.
1373    let x_lim = x_bound.to_i64().unwrap_or(0).max(0);
1374    let four_n = BigInt::from_i64(4).mul(n);
1375    let two = two();
1376    for g in polys.iter().take(6) {
1377        for ki in 1..=x_lim {
1378            let k = BigInt::from_i64(ki);
1379            let gy = bpoly_subst_x(g, &k);
1380            if gy.iter().all(|c| c.is_zero()) {
1381                continue;
1382            }
1383            for y0 in integer_roots_of(&gy, &y_bound) {
1384                let s = y0.negated();
1385                if s.is_negative() || s.is_zero() {
1386                    continue;
1387                }
1388                let disc = s.mul(&s).sub(&four_n);
1389                if disc.is_negative() {
1390                    continue;
1391                }
1392                let sq = isqrt(&disc);
1393                if sq.mul(&sq) == disc {
1394                    let p = s.sub(&sq).div_rem(&two).expect("nonzero").0;
1395                    let q = s.add(&sq).div_rem(&two).expect("nonzero").0;
1396                    if verify_factorization(n, &p, &q) {
1397                        return Some((p, q));
1398                    }
1399                }
1400            }
1401        }
1402    }
1403    None
1404}
1405
1406/// The effort budget for [`structural_factor`]: each structural attack runs to its own bound, then
1407/// declines. A soundly-generated modulus exhausts every bound and yields no witness.
1408#[derive(Clone, Copy, Debug)]
1409pub struct StructuralBudget {
1410    pub trial_limit: u64,
1411    pub fermat_iters: u64,
1412    pub pminus1_bound: u64,
1413    pub rho_iters: u64,
1414}
1415
1416impl Default for StructuralBudget {
1417    // A quick structural triage: enough to expose any real structural weakness, but far short of the
1418    // (sub-)exponential effort a sound modulus would demand — so the ceiling is proven cheaply. A
1419    // 200-bit sound modulus needs ~2⁵⁰ rho steps; 5 000 is not remotely close, yet a *structured* key
1420    // falls in a handful of steps regardless.
1421    fn default() -> Self {
1422        Self { trial_limit: 1_000, fermat_iters: 3_000, pminus1_bound: 500, rho_iters: 5_000 }
1423    }
1424}
1425
1426/// A certified structural weakness: the factors and the attack that found them.
1427#[derive(Clone, Debug, PartialEq, Eq)]
1428pub struct StructuralWitness {
1429    pub p: BigInt,
1430    pub q: BigInt,
1431    pub method: &'static str,
1432}
1433
1434/// Run the whole structural arsenal against `n` within `budget`, returning the first certified
1435/// factorization found, or `None` — the number-theoretic incompressible residue (a sound modulus has no
1436/// structural shortcut, so only the general sub-exponential algorithms, out of scope here, remain).
1437pub fn structural_factor(n: &BigInt, budget: StructuralBudget) -> Option<StructuralWitness> {
1438    let mk = |(p, q): (BigInt, BigInt), method: &'static str| StructuralWitness { p, q, method };
1439    if let Some(f) = trial_division(n, budget.trial_limit) {
1440        return Some(mk(f, "trial division (small factor)"));
1441    }
1442    if let Some(f) = fermat(n, budget.fermat_iters) {
1443        return Some(mk(f, "Fermat (close primes)"));
1444    }
1445    if let Some(f) = pollard_p_minus_1(n, budget.pminus1_bound) {
1446        return Some(mk(f, "Pollard p−1 (smooth p−1)"));
1447    }
1448    if let Some(f) = pollard_rho(n, budget.rho_iters) {
1449        return Some(mk(f, "Pollard rho"));
1450    }
1451    None
1452}
1453
1454#[cfg(test)]
1455mod tests {
1456    use super::*;
1457
1458    fn big(s: &str) -> BigInt {
1459        BigInt::parse_decimal(s).expect("valid decimal")
1460    }
1461
1462    #[test]
1463    fn primality_and_next_prime_agree_with_known_values() {
1464        assert!(is_probable_prime(&BigInt::from_i64(2)));
1465        assert!(is_probable_prime(&BigInt::from_i64(1_000_003)));
1466        assert!(!is_probable_prime(&BigInt::from_i64(1_000_000)));
1467        assert!(!is_probable_prime(&big("1000000000000000000000000000000"))); // even
1468        assert_eq!(next_prime(&BigInt::from_i64(1_000_000)), BigInt::from_i64(1_000_003));
1469    }
1470
1471    #[test]
1472    fn isqrt_is_exact() {
1473        assert_eq!(isqrt(&BigInt::from_i64(0)), BigInt::from_i64(0));
1474        assert_eq!(isqrt(&BigInt::from_i64(15)), BigInt::from_i64(3));
1475        assert_eq!(isqrt(&BigInt::from_i64(16)), BigInt::from_i64(4));
1476        let big_sq = big("1000000000000000000000000000000"); // 10³⁰ = (10¹⁵)²
1477        assert_eq!(isqrt(&big_sq), big("1000000000000000"));
1478    }
1479
1480    #[test]
1481    fn trial_division_catches_a_small_factor() {
1482        let p = next_prime(&big("1000000000000000000000000000000"));
1483        let n = BigInt::from_i64(3).mul(&p);
1484        let (a, b) = trial_division(&n, 100).expect("the small factor 3 is found");
1485        assert!(verify_factorization(&n, &a, &b));
1486        assert!(a == BigInt::from_i64(3) || b == BigInt::from_i64(3));
1487    }
1488
1489    #[test]
1490    fn fermat_crushes_close_primes() {
1491        let p = next_prime(&big("1000000000000000000000000000000"));
1492        let q = next_prime(&p.add(&BigInt::from_i64(2)));
1493        let n = p.mul(&q);
1494        let (a, b) = fermat(&n, 100_000).expect("adjacent primes fall to Fermat");
1495        assert!(verify_factorization(&n, &a, &b));
1496    }
1497
1498    #[test]
1499    fn pollard_rho_factors_a_moderate_semiprime() {
1500        let p = next_prime(&big("1000000007"));
1501        let q = next_prime(&big("2000000011"));
1502        let n = p.mul(&q);
1503        let (a, b) = pollard_rho(&n, 500_000).expect("rho factors a ~60-bit semiprime");
1504        assert!(verify_factorization(&n, &a, &b));
1505    }
1506
1507    #[test]
1508    fn pollard_p_minus_1_crushes_a_smooth_prime() {
1509        // Build p with p−1 = 37-smooth: p = (∏ primes ≤ 37)·k + 1, prime, k small.
1510        let mut smooth = one();
1511        for &pr in &[2u64, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37] {
1512            smooth = smooth.mul(&BigInt::from_u64(pr));
1513        }
1514        let mut p = smooth.add(&one());
1515        let mut k = 1u64;
1516        while !is_probable_prime(&p) {
1517            k += 1;
1518            assert!(k <= 40, "a smooth prime is found with a small (≤40-smooth) multiplier");
1519            p = smooth.mul(&BigInt::from_u64(k)).add(&one());
1520        }
1521        let q = next_prime(&big("999999999999999999999999999999"));
1522        let n = p.mul(&q);
1523        let (a, b) = pollard_p_minus_1(&n, 40).expect("a smooth p−1 falls to Pollard p−1");
1524        assert!(verify_factorization(&n, &a, &b));
1525    }
1526
1527    #[test]
1528    fn wiener_crushes_a_small_private_exponent() {
1529        let p = next_prime(&big("1000000007"));
1530        let q = next_prime(&big("3000000019"));
1531        let n = p.mul(&q);
1532        let phi = p.sub(&one()).mul(&q.sub(&one()));
1533        let d = BigInt::from_i64(7919); // small: 7919 < ⅓·N^{1/4}
1534        let e = mod_inverse(&d, &phi).expect("d is coprime to φ");
1535        let (a, b) = wiener(&e, &n).expect("a small private exponent falls to Wiener");
1536        assert!(verify_factorization(&n, &a, &b));
1537    }
1538
1539    #[test]
1540    fn batch_gcd_crushes_a_shared_prime() {
1541        let p = next_prime(&big("1000000000000000000000000000000"));
1542        let q1 = next_prime(&big("3000000000000000000000000000000"));
1543        let q2 = next_prime(&big("7000000000000000000000000000000"));
1544        let (n1, n2) = (p.mul(&q1), p.mul(&q2));
1545        let hits = batch_gcd(&[n1, n2]);
1546        assert_eq!(hits.len(), 1, "the shared prime is detected");
1547        assert_eq!(hits[0].2, p, "and it is exactly p");
1548    }
1549
1550    #[test]
1551    fn structural_factor_orchestrates_the_arsenal() {
1552        // A close-primes modulus is caught, and the method is reported.
1553        let p = next_prime(&big("1000000000000000000000000000000"));
1554        let q = next_prime(&p.add(&BigInt::from_i64(2)));
1555        let n = p.mul(&q);
1556        let w = structural_factor(&n, StructuralBudget::default()).expect("weakness found");
1557        assert!(verify_factorization(&n, &w.p, &w.q));
1558        assert_eq!(w.method, "Fermat (close primes)");
1559    }
1560
1561    #[test]
1562    fn a_soundly_generated_modulus_resists_the_entire_structural_arsenal() {
1563        // Two large, independent, well-separated strong primes: not close (Fermat fails), p−1/q−1 are
1564        // not smooth (Pollard p−1 fails), no shared prime, and N is far too large for bounded rho. Every
1565        // STRUCTURAL attack, run to its budget, finds NOTHING — RSA's safety IS this ceiling, the
1566        // number-theoretic incompressible residue. Only general sub-exponential factoring remains, which
1567        // real key sizes push out of reach.
1568        let p = next_prime(&big("1000000000000000000000000000057"));
1569        let q = next_prime(&big("9000000000000000000000000000000"));
1570        let n = p.mul(&q);
1571        assert!(
1572            structural_factor(&n, StructuralBudget::default()).is_none(),
1573            "a sound modulus has no structural shortcut — the ceiling stands"
1574        );
1575    }
1576
1577    #[test]
1578    fn resultant_machinery_eliminates_a_variable() {
1579        // Fraction-free determinant: |[[1,2],[3,4]]| = −2.
1580        let det = det_bareiss(vec![
1581            vec![BigInt::from_i64(1), BigInt::from_i64(2)],
1582            vec![BigInt::from_i64(3), BigInt::from_i64(4)],
1583        ]);
1584        assert_eq!(det, BigInt::from_i64(-2));
1585
1586        // Eliminate x from g1 = x − y and g2 = x + y − 2 (common root (1, 1)). The resultant is a
1587        // polynomial in y that must vanish exactly at the shared coordinate y = 1.
1588        let mut g1 = BPoly::new();
1589        g1.insert((1, 0), BigInt::from_i64(1));
1590        g1.insert((0, 1), BigInt::from_i64(-1));
1591        let mut g2 = BPoly::new();
1592        g2.insert((1, 0), BigInt::from_i64(1));
1593        g2.insert((0, 1), BigInt::from_i64(1));
1594        g2.insert((0, 0), BigInt::from_i64(-2));
1595        let res = bivariate_resultant_x(&g1, &g2);
1596        assert!(poly_eval(&res, &BigInt::from_i64(1)).is_zero(), "resultant vanishes at the shared y = 1");
1597        assert!(!poly_eval(&res, &BigInt::from_i64(0)).is_zero(), "and is nonzero away from it");
1598    }
1599
1600    #[test]
1601    #[ignore] // heavy (dim ~72 fpLLL) — run explicitly in release
1602    fn boneh_durfee_breaks_small_d() {
1603        let p = next_prime(&big("1000003"));
1604        let q = next_prime(&big("2000003"));
1605        let n = p.mul(&q);
1606        let phi = p.sub(&one()).mul(&q.sub(&one()));
1607        let d = BigInt::from_i64(1423); // δ ≈ 0.255 — past Wiener's 0.25
1608        let e = mod_inverse(&d, &phi).unwrap();
1609        assert!(wiener(&e, &n).is_none(), "beyond Wiener (control)");
1610        let x_bound = BigInt::from_i64(1 << 11);
1611        let (a, b) = boneh_durfee(&n, &e, 8, 3, &x_bound).expect("Boneh-Durfee breaks small-d");
1612        eprintln!("SMALL-d BROKEN: {a:?} · {b:?}");
1613        assert!(verify_factorization(&n, &a, &b), "the recovered factors check out");
1614    }
1615
1616    #[test]
1617    fn boneh_durfee_lattice_lifts_the_modular_root_to_the_integers() {
1618        // The verified core of Boneh-Durfee: for a small-d key, build the bivariate lattice of x/y-shifts
1619        // of f(x,y) = x·y + (N+1)·x + 1 and LLL-reduce it (fast float-Gram-Schmidt). The short polynomials
1620        // then vanish at the true root (k, −(p+q)) OVER THE INTEGERS — Howgrave-Graham — which is the deep
1621        // step that lifts "root mod e" to a solvable integer system. Full factor recovery additionally
1622        // needs two ALGEBRAICALLY-INDEPENDENT vanishing vectors for the resultant elimination; producing
1623        // them reliably is Boneh-Durfee's own geometrically-progressive sublattice (the documented
1624        // remaining refinement — in the low-δ regime here the vanishing sublattice collapses to a single
1625        // dependent family, so the resultant degenerates).
1626        let p = next_prime(&big("100003"));
1627        let q = next_prime(&big("200003"));
1628        let n = p.mul(&q);
1629        let phi = p.sub(&one()).mul(&q.sub(&one()));
1630        let d = BigInt::from_i64(43);
1631        let e = mod_inverse(&d, &phi).unwrap();
1632        let k = e.mul(&d).sub(&one()).div_rem(&phi).unwrap().0;
1633        let s = p.add(&q);
1634        let (polys, _) = bd_reduced_polys(&n, &e, 4, 2, &BigInt::from_i64(1 << 8));
1635        let vanishing = polys.iter().filter(|g| g.len() > 1 && bpoly_eval(g, &k, &s.negated()).is_zero()).count();
1636        assert!(vanishing >= 5, "the reduced lattice lifts the modular root to integer roots, got {vanishing}");
1637    }
1638
1639    #[test]
1640    fn quadratic_sieve_factors_a_larger_semiprime_by_sieving() {
1641        // A ~34-bit semiprime — beyond comfortable Dixon range — factored by log-sieving Q(x) and the
1642        // GF(2) dependency search.
1643        let p = next_prime(&big("100000"));
1644        let q = next_prime(&big("100050"));
1645        let n = p.mul(&q);
1646        let (a, b) = quadratic_sieve(&n, 500, 50_000).expect("the quadratic sieve factors N");
1647        assert!(verify_factorization(&n, &a, &b), "sieving + a GF(2) dependency split N");
1648    }
1649
1650    #[test]
1651    fn dixon_breaks_a_semiprime_with_our_gf2_symmetry_breaking() {
1652        // N = 179 · 257. Dixon collects r with r² mod N smooth over a small prime base, then OUR GF(2)
1653        // dependency finder picks a subset whose product is a perfect square → x² ≡ y² (mod N) → factor.
1654        let n = BigInt::from_i64(179).mul(&BigInt::from_i64(257));
1655        let base = [2u64, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31];
1656        let (a, b) = dixon_factor(&n, &base, 50_000).expect("Dixon factors via a GF(2) dependency");
1657        assert!(verify_factorization(&n, &a, &b), "the congruence of squares splits N");
1658    }
1659
1660    #[test]
1661    fn crt_and_nth_root_are_correct() {
1662        // Sunzi's classic: x ≡ 2 (mod 3), 3 (mod 5), 2 (mod 7) → 23.
1663        let x = crt(
1664            &[BigInt::from_i64(2), BigInt::from_i64(3), BigInt::from_i64(2)],
1665            &[BigInt::from_i64(3), BigInt::from_i64(5), BigInt::from_i64(7)],
1666        )
1667        .unwrap();
1668        assert_eq!(x, BigInt::from_i64(23));
1669        assert_eq!(integer_nth_root(&BigInt::from_i64(27), 3), BigInt::from_i64(3));
1670        assert_eq!(integer_nth_root(&big("1000000000000000000"), 3), BigInt::from_i64(1000000));
1671        assert_eq!(integer_nth_root(&BigInt::from_i64(3125), 5), BigInt::from_i64(5));
1672        assert_eq!(integer_nth_root(&BigInt::from_i64(26), 3), BigInt::from_i64(2), "floor cbrt(26) = 2");
1673    }
1674
1675    #[test]
1676    fn hastad_broadcast_recovers_the_message() {
1677        // The same plaintext, encrypted under e=3 to three recipients with distinct moduli, all > m.
1678        let e = 3u32;
1679        let m = big("123456789");
1680        let moduli: Vec<BigInt> = vec![
1681            next_prime(&big("100000007")).mul(&next_prime(&big("200000033"))),
1682            next_prime(&big("300000007")).mul(&next_prime(&big("400000009"))),
1683            next_prime(&big("500000003")).mul(&next_prime(&big("600000011"))),
1684        ];
1685        let e_big = BigInt::from_u64(e as u64);
1686        let ciphertexts: Vec<BigInt> = moduli.iter().map(|n| modpow(&m, &e_big, n)).collect();
1687        let recovered = hastad_broadcast_attack(e, &ciphertexts, &moduli).expect("Håstad recovers m");
1688        assert_eq!(recovered, m, "the broadcast plaintext falls out of the ciphertexts alone");
1689    }
1690
1691    #[test]
1692    fn common_modulus_recovers_the_message() {
1693        // The same message encrypted under the SAME modulus with two coprime public exponents.
1694        let n = next_prime(&big("1000000007")).mul(&next_prime(&big("2000000011")));
1695        let m = big("31415926535");
1696        let (e1, e2) = (BigInt::from_i64(65537), BigInt::from_i64(3));
1697        let c1 = modpow(&m, &e1, &n);
1698        let c2 = modpow(&m, &e2, &n);
1699        let recovered = common_modulus_attack(&e1, &e2, &c1, &c2, &n).expect("common modulus recovers m");
1700        assert_eq!(recovered, m, "a shared modulus + coprime exponents leaks the message via Bézout");
1701    }
1702
1703    #[test]
1704    fn franklin_reiter_recovers_related_messages() {
1705        // Two linearly related messages m and m+r under the same N and e=3.
1706        let n = next_prime(&big("1000000007")).mul(&next_prime(&big("2000000011")));
1707        let e = 3u32;
1708        let e_big = BigInt::from_u64(e as u64);
1709        let m = big("424242424242");
1710        let r = big("31337");
1711        let c1 = modpow(&m, &e_big, &n);
1712        let c2 = modpow(&m.add(&r), &e_big, &n);
1713        let recovered = franklin_reiter_attack(e, &r, &c1, &c2, &n).expect("Franklin-Reiter recovers m");
1714        assert_eq!(recovered, m, "the related pair leaks the plaintext via polynomial GCD");
1715    }
1716
1717    #[test]
1718    fn low_exponent_no_padding_recovers_small_message() {
1719        // A small message under e=3 with no padding: c = m³ over the integers (no reduction).
1720        let n = next_prime(&big("100000000000000003")).mul(&next_prime(&big("200000000000000003")));
1721        let m = big("123456");
1722        let c = modpow(&m, &BigInt::from_i64(3), &n);
1723        let recovered = low_exponent_message(&c, 3).expect("cube root recovers the small message");
1724        assert_eq!(recovered, m, "no padding under a small exponent is just an integer root");
1725    }
1726
1727    #[test]
1728    fn coppersmith_factors_from_known_high_bits() {
1729        // A ~129-bit modulus. Reveal all but the low 20 bits of p — a partial-key-exposure leak that no
1730        // factoring shortcut (Fermat/rho/p−1) exploits. The dim-9 Coppersmith lattice (fast now via
1731        // float-Gram-Schmidt LLL) recovers the missing bits and factors N.
1732        let p = next_prime(&big("18446744073709551629"));
1733        let q = next_prime(&big("36893488147419103237"));
1734        let n = p.mul(&q);
1735        let unknown_bits = 20u32;
1736        let mut mask = one();
1737        for _ in 0..unknown_bits {
1738            mask = mask.mul(&two());
1739        }
1740        let (p_div, _) = p.div_rem(&mask).expect("nonzero");
1741        let p_high = p_div.mul(&mask); // p with its low 18 bits zeroed
1742        let (a, b) = coppersmith_factor_high_bits(&n, &p_high, unknown_bits).expect("Coppersmith recovers p");
1743        assert!(verify_factorization(&n, &a, &b), "the recovered factorization checks out");
1744    }
1745
1746    #[test]
1747    fn rsa_breaks_end_to_end_when_the_modulus_factors() {
1748        // A weak RSA key: two primes chosen close together.
1749        let p = next_prime(&big("1000000000000000000000000000057"));
1750        let q = next_prime(&p.add(&BigInt::from_i64(100)));
1751        let n = p.mul(&q);
1752        let e = BigInt::from_i64(65537);
1753        let d = rsa_private_exponent(&e, &p, &q).expect("e is coprime to φ");
1754        let m = big("123456789987654321");
1755        let c = modpow(&m, &e, &n);
1756        assert_eq!(modpow(&c, &d, &n), m, "RSA encryption round-trips");
1757
1758        // Factor the modulus (Fermat crushes the close primes), recover d from the factors, decrypt: a
1759        // complete break with no access to the private key.
1760        let w = structural_factor(&n, StructuralBudget::default()).expect("close primes factor");
1761        let d_broken = rsa_private_exponent(&e, &w.p, &w.q).expect("recover d from the factors");
1762        assert_eq!(modpow(&c, &d_broken, &n), m, "the recovered key decrypts — RSA broken end to end");
1763    }
1764
1765    #[test]
1766    fn recovering_the_private_key_is_equivalent_to_factoring() {
1767        // A sound RSA key: two large, well-separated primes.
1768        let p = next_prime(&big("1000000000000000000000000000057"));
1769        let q = next_prime(&big("9000000000000000000000000000000"));
1770        let n = p.mul(&q);
1771        let e = BigInt::from_i64(65537);
1772        let d = rsa_private_exponent(&e, &p, &q).expect("e is coprime to φ");
1773
1774        // The attacker's side: the structural arsenal cannot factor it — no shortcut to the key.
1775        assert!(
1776            structural_factor(&n, StructuralBudget::default()).is_none(),
1777            "the sound modulus resists every structural attack"
1778        );
1779
1780        // The equivalence: yet the private exponent d, if known, DETERMINISTICALLY factors N. So
1781        // recovering the key and factoring the modulus are one and the same problem — RSA's security
1782        // reduces exactly to factoring, which the arsenal cannot shortcut and which (by Chaitin) we
1783        // cannot prove hard. It neither provably breaks nor is provably unbreakable.
1784        let (fp, fq) = factor_via_private_exponent(&n, &e, &d).expect("the private exponent factors N");
1785        assert!(verify_factorization(&n, &fp, &fq), "recovering d recovers the factorization");
1786    }
1787}