foxmath 0.7.0__tar.gz → 0.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: foxmath
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: Minimalist CLI for math & cryptography exploration 🦊
5
5
  Author-email: Abhrankan Chakrabarti <abhrankan@gmail.com>
6
6
  License: MIT
@@ -61,7 +61,7 @@ Check which version you have installed:
61
61
  foxmath --version
62
62
  ```
63
63
  ```
64
- foxmath 0.7.0
64
+ foxmath 0.7.1
65
65
  ```
66
66
 
67
67
  ## Usage Examples
@@ -42,7 +42,7 @@ Check which version you have installed:
42
42
  foxmath --version
43
43
  ```
44
44
  ```
45
- foxmath 0.7.0
45
+ foxmath 0.7.1
46
46
  ```
47
47
 
48
48
  ## Usage Examples
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "foxmath"
7
- version = "0.7.0"
7
+ version = "0.7.1"
8
8
  description = "Minimalist CLI for math & cryptography exploration 🦊"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -22,20 +22,66 @@ def _package_version() -> str:
22
22
 
23
23
  # ------------------- Math Functions -------------------
24
24
 
25
- def legendre_symbol(a: int, p: int) -> int:
26
- """Compute Legendre symbol (a/p)"""
27
- if p < 2 or p % 2 == 0:
25
+ # First 13 primes. As Miller-Rabin bases they make the test *deterministic*
26
+ # for every n below 3,317,044,064,679,887,385,961,981 (~3.3e24); 12 bases
27
+ # would not be enough (399165290221 * 798330580441, ~3.2e23, fools them).
28
+ _PRIME_TEST_BASES = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41)
29
+
30
+ def _is_probable_prime(n: int) -> bool:
31
+ """Miller-Rabin primality test.
32
+
33
+ Exact for n < ~3.3e24. Above that it is a strong probable-prime test:
34
+ ordinary composites essentially never pass, but a composite built
35
+ specifically to fool these 13 bases could.
36
+ """
37
+ if n < 2:
38
+ return False
39
+ for sp in _PRIME_TEST_BASES:
40
+ if n == sp:
41
+ return True
42
+ if n % sp == 0:
43
+ return False
44
+ d, r = n - 1, 0
45
+ while d % 2 == 0:
46
+ d //= 2
47
+ r += 1
48
+ for a in _PRIME_TEST_BASES:
49
+ x = pow(a, d, n)
50
+ if x == 1 or x == n - 1:
51
+ continue
52
+ for _ in range(r - 1):
53
+ x = x * x % n
54
+ if x == n - 1:
55
+ break
56
+ else:
57
+ return False
58
+ return True
59
+
60
+ def _require_odd_prime(p: int) -> None:
61
+ """Enforce the contract the Legendre symbol and Tonelli-Shanks rely on.
62
+
63
+ Euler's criterion is only meaningful for a prime modulus. On a composite
64
+ it returns garbage, and Tonelli-Shanks' search for a non-residue can loop
65
+ forever (mod 9 or mod 561, for example, there is none to find).
66
+ """
67
+ if p < 2 or p % 2 == 0 or not _is_probable_prime(p):
28
68
  raise ValueError("p must be an odd prime")
69
+
70
+ def _legendre_unchecked(a: int, p: int) -> int:
29
71
  res = pow(a, (p - 1) // 2, p)
30
72
  return 1 if res == 1 else (-1 if res == p - 1 else 0)
31
73
 
74
+ def legendre_symbol(a: int, p: int) -> int:
75
+ """Compute Legendre symbol (a/p). p must be an odd prime."""
76
+ _require_odd_prime(p)
77
+ return _legendre_unchecked(a, p)
78
+
32
79
  def sqrt_mod(a: int, p: int) -> tuple:
33
80
  """Return the square root(s) of a modulo an odd prime p."""
34
- if p < 2 or p % 2 == 0:
35
- raise ValueError("p must be an odd prime")
81
+ _require_odd_prime(p)
36
82
 
37
83
  a %= p
38
- symbol = legendre_symbol(a, p)
84
+ symbol = _legendre_unchecked(a, p)
39
85
  if symbol == 0:
40
86
  return (0,)
41
87
  if symbol == -1:
@@ -52,7 +98,7 @@ def sqrt_mod(a: int, p: int) -> tuple:
52
98
  s += 1
53
99
 
54
100
  non_residue = 2
55
- while legendre_symbol(non_residue, p) != -1:
101
+ while _legendre_unchecked(non_residue, p) != -1:
56
102
  non_residue += 1
57
103
 
58
104
  c = pow(non_residue, q, p)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: foxmath
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: Minimalist CLI for math & cryptography exploration 🦊
5
5
  Author-email: Abhrankan Chakrabarti <abhrankan@gmail.com>
6
6
  License: MIT
@@ -61,7 +61,7 @@ Check which version you have installed:
61
61
  foxmath --version
62
62
  ```
63
63
  ```
64
- foxmath 0.7.0
64
+ foxmath 0.7.1
65
65
  ```
66
66
 
67
67
  ## Usage Examples
@@ -301,6 +301,16 @@ class TestNormalUsage(unittest.TestCase):
301
301
  self.assertEqual(code, 0)
302
302
  self.assertIn("sqrt(0) = 0 (mod 17)", out)
303
303
 
304
+ def test_legendre_composite_p_errors(self):
305
+ code, out, err = run("legendre", "2", "9")
306
+ self.assertEqual(code, 1)
307
+ self.assertIn("p must be an odd prime", err)
308
+
309
+ def test_sqrtmod_composite_p_errors(self):
310
+ code, out, err = run("sqrtmod", "2", "9")
311
+ self.assertEqual(code, 1)
312
+ self.assertIn("p must be an odd prime", err)
313
+
304
314
 
305
315
  if __name__ == "__main__":
306
316
  unittest.main()
@@ -15,7 +15,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..",
15
15
  from foxmath.foxmath import (
16
16
  legendre_symbol, sqrt_mod, euler_hermann_pi_approx, ec_point_add, ec_mul, crt_solve,
17
17
  continued_fraction, cf_convergents, CURVES, is_on_curve,
18
- generate_problem,
18
+ generate_problem, _is_probable_prime,
19
19
  )
20
20
  import random
21
21
 
@@ -51,6 +51,12 @@ class TestLegendreSymbol(unittest.TestCase):
51
51
  with self.assertRaises(ValueError):
52
52
  legendre_symbol(3, 1)
53
53
 
54
+ def test_rejects_composite_odd_p(self):
55
+ for comp in (9, 15, 21, 25, 27, 33, 35, 561):
56
+ with self.subTest(p=comp):
57
+ with self.assertRaises(ValueError):
58
+ legendre_symbol(2, comp)
59
+
54
60
 
55
61
  class TestSqrtMod(unittest.TestCase):
56
62
  def test_tonelli_shanks_returns_both_roots(self):
@@ -93,6 +99,32 @@ class TestSqrtMod(unittest.TestCase):
93
99
  self.assertEqual(roots_97, (14, 83))
94
100
  self.assertTrue(all((r * r - 2) % 97 == 0 for r in roots_97))
95
101
 
102
+ def test_rejects_composite_odd_p(self):
103
+ for comp in (9, 15, 25, 561):
104
+ with self.subTest(p=comp):
105
+ with self.assertRaises(ValueError):
106
+ sqrt_mod(4, comp)
107
+
108
+
109
+ class TestPrimality(unittest.TestCase):
110
+ def test_small_primes(self):
111
+ for p in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 97, 193):
112
+ with self.subTest(p=p):
113
+ self.assertTrue(_is_probable_prime(p))
114
+
115
+ def test_small_composites(self):
116
+ for c in (0, 1, 4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20, 21, 25, 27):
117
+ with self.subTest(c=c):
118
+ self.assertFalse(_is_probable_prime(c))
119
+
120
+ def test_carmichael_numbers_rejected(self):
121
+ for cn in (561, 1105, 1729):
122
+ with self.subTest(cn=cn):
123
+ self.assertFalse(_is_probable_prime(cn))
124
+
125
+ def test_mersenne_prime(self):
126
+ self.assertTrue(_is_probable_prime(2147483647))
127
+
96
128
 
97
129
  class TestEulerHermannPiApprox(unittest.TestCase):
98
130
  def test_converges_to_pi(self):
File without changes
File without changes
File without changes