transcrypto 1.0.2__py3-none-any.whl → 1.1.1__py3-none-any.whl
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.
- transcrypto/aes.py +257 -0
- transcrypto/base.py +1018 -0
- transcrypto/dsa.py +336 -0
- transcrypto/elgamal.py +333 -0
- transcrypto/modmath.py +535 -0
- transcrypto/rsa.py +416 -0
- transcrypto/sss.py +299 -0
- transcrypto/transcrypto.py +1367 -276
- transcrypto-1.1.1.dist-info/METADATA +2257 -0
- transcrypto-1.1.1.dist-info/RECORD +15 -0
- transcrypto-1.0.2.dist-info/METADATA +0 -147
- transcrypto-1.0.2.dist-info/RECORD +0 -8
- {transcrypto-1.0.2.dist-info → transcrypto-1.1.1.dist-info}/WHEEL +0 -0
- {transcrypto-1.0.2.dist-info → transcrypto-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {transcrypto-1.0.2.dist-info → transcrypto-1.1.1.dist-info}/top_level.txt +0 -0
transcrypto/modmath.py
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025 Daniel Balparda (balparda@github.com) - Apache-2.0 license
|
|
4
|
+
#
|
|
5
|
+
"""Balparda's TransCrypto modular math library."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
# import pdb
|
|
11
|
+
from typing import Generator, Reversible
|
|
12
|
+
|
|
13
|
+
from . import base
|
|
14
|
+
|
|
15
|
+
__author__ = 'balparda@github.com'
|
|
16
|
+
__version__: str = base.__version__ # version comes from base!
|
|
17
|
+
__version_tuple__: tuple[int, ...] = base.__version_tuple__
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_FIRST_60_PRIMES: set[int] = {
|
|
21
|
+
2, 3, 5, 7, 11, 13, 17, 19, 23, 29,
|
|
22
|
+
31, 37, 41, 43, 47, 53, 59, 61, 67, 71,
|
|
23
|
+
73, 79, 83, 89, 97, 101, 103, 107, 109, 113,
|
|
24
|
+
127, 131, 137, 139, 149, 151, 157, 163, 167, 173,
|
|
25
|
+
179, 181, 191, 193, 197, 199, 211, 223, 227, 229,
|
|
26
|
+
233, 239, 241, 251, 257, 263, 269, 271, 277, 281,
|
|
27
|
+
}
|
|
28
|
+
_FIRST_60_PRIMES_SORTED: list[int] = sorted(_FIRST_60_PRIMES)
|
|
29
|
+
_COMPOSITE_60: int = math.prod(_FIRST_60_PRIMES_SORTED)
|
|
30
|
+
_PRIME_60: int = _FIRST_60_PRIMES_SORTED[-1]
|
|
31
|
+
assert len(_FIRST_60_PRIMES) == 60 and _PRIME_60 == 281, f'should never happen: {_PRIME_60=}'
|
|
32
|
+
_FIRST_49_MERSENNE: set[int] = { # <https://oeis.org/A000043>
|
|
33
|
+
2, 3, 5, 7, 13, 17, 19, 31, 61, 89,
|
|
34
|
+
107, 127, 521, 607, 1279, 2203, 2281, 3217, 4253, 4423,
|
|
35
|
+
9689, 9941, 11213, 19937, 21701, 23209, 44497, 86243, 110503, 132049,
|
|
36
|
+
216091, 756839, 859433, 1257787, 1398269, 2976221, 3021377, 6972593, 13466917, 20996011,
|
|
37
|
+
24036583, 25964951, 30402457, 32582657, 37156667, 42643801, 43112609, 57885161, 74207281,
|
|
38
|
+
}
|
|
39
|
+
_FIRST_49_MERSENNE_SORTED: list[int] = sorted(_FIRST_49_MERSENNE)
|
|
40
|
+
assert len(_FIRST_49_MERSENNE) == 49 and _FIRST_49_MERSENNE_SORTED[-1] == 74207281, f'should never happen: {_FIRST_49_MERSENNE_SORTED[-1]}'
|
|
41
|
+
|
|
42
|
+
_MAX_PRIMALITY_SAFETY = 100 # this is an absurd number, just to have a max
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ModularDivideError(base.Error):
|
|
46
|
+
"""Divide-by-zero-like exception (TransCrypto)."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def ModInv(x: int, m: int, /) -> int:
|
|
50
|
+
"""Modular inverse of `x` mod `m`: a `y` such that (x * y) % m == 1 if GCD(x, m) == 1.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
x (int): integer to invert
|
|
54
|
+
m (int): modulus, m ≥ 2
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
positive integer `y` such that (x * y) % m == 1
|
|
58
|
+
this only exists if GCD(x, m) == 1, so to guarantee an inverse `m` must be prime
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
InputError: invalid modulus or x
|
|
62
|
+
ModularDivideError: divide-by-zero, i.e., GCD(x, m) != 1 or x == 0
|
|
63
|
+
"""
|
|
64
|
+
# test inputs
|
|
65
|
+
if m < 2:
|
|
66
|
+
raise base.InputError(f'invalid modulus: {m=}')
|
|
67
|
+
# easy special cases: 0 and 1
|
|
68
|
+
reduced_x: int = x % m
|
|
69
|
+
if not reduced_x: # "division by 0"
|
|
70
|
+
raise ModularDivideError(f'null inverse {x=} mod {m=}')
|
|
71
|
+
if reduced_x == 1: # trivial degenerate case
|
|
72
|
+
return 1
|
|
73
|
+
# compute actual extended GCD and see if we will have an inverse
|
|
74
|
+
gcd, y, w = base.ExtendedGCD(reduced_x, m)
|
|
75
|
+
if gcd != 1:
|
|
76
|
+
raise ModularDivideError(f'invalid inverse {x=} mod {m=} with {gcd=}')
|
|
77
|
+
assert y and w and y >= -m, f'should never happen: {x=} mod {m=} -> {w=} ; {y=}'
|
|
78
|
+
return y if y >= 0 else (y + m)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def ModDiv(x: int, y: int, m: int, /) -> int:
|
|
82
|
+
"""Modular division of `x`/`y` mod `m`, if GCD(y, m) == 1.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
x (int): integer
|
|
86
|
+
y (int): integer
|
|
87
|
+
m (int): modulus, m ≥ 2
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
positive integer `z` such that (z * y) % m == x
|
|
91
|
+
this only exists if GCD(y, m) == 1, so to guarantee an inverse `m` must be prime
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
InputError: invalid modulus or x or y
|
|
95
|
+
ModularDivideError: divide-by-zero, i.e., GCD(y, m) != 1 or y == 0
|
|
96
|
+
"""
|
|
97
|
+
# test inputs
|
|
98
|
+
if m < 2:
|
|
99
|
+
raise base.InputError(f'invalid modulus: {m=}')
|
|
100
|
+
if not y: # "division by 0"
|
|
101
|
+
raise ModularDivideError(f'divide by zero {x=} / {y=} mod {m=}')
|
|
102
|
+
# do the math
|
|
103
|
+
if not x:
|
|
104
|
+
return 0
|
|
105
|
+
return ((x % m) * ModInv(y % m, m)) % m
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def CRTPair(a1: int, m1: int, a2: int, m2: int) -> int:
|
|
109
|
+
"""Chinese Remainder Theorem Pair: given co-prime `m1`/`m2`, solve a1 = x % m1 and a2 = x % m2.
|
|
110
|
+
|
|
111
|
+
<https://en.wikipedia.org/wiki/Chinese_remainder_theorem>
|
|
112
|
+
|
|
113
|
+
Finds the unique integer x in [0, m1 * m2) satisfying
|
|
114
|
+
|
|
115
|
+
x ≡ a1 (mod m1)
|
|
116
|
+
x ≡ a2 (mod m2)
|
|
117
|
+
|
|
118
|
+
The solution is guaranteed to exist and be unique because the moduli are assumed to
|
|
119
|
+
be positive, ≥ 2, and pairwise co-prime, gcd(m1, m2) == 1.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
a1 (int): residue for the first congruence
|
|
123
|
+
m1 (int): modulus 1, m ≥ 2 and co-prime with m2, i.e. gcd(m1, m2) == 1
|
|
124
|
+
a2 (int): residue for the second congruence
|
|
125
|
+
m2 (int): modulus 2, m ≥ 2 and co-prime with m1, i.e. gcd(m1, m2) == 1
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
the least non-negative solution `x` such that a1 = x % m1 and a2 = x % m2 and 0 ≤ x < m1 * m2
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
InputError: invalid inputs
|
|
132
|
+
ModularDivideError: moduli are not co-prime, i.e. gcd(m1, m2) != 1
|
|
133
|
+
"""
|
|
134
|
+
# test inputs
|
|
135
|
+
if m1 < 2 or m2 < 2 or m1 == m2:
|
|
136
|
+
raise base.InputError(f'invalid moduli: {m1=} / {m2=}')
|
|
137
|
+
# compute
|
|
138
|
+
a1 %= m1
|
|
139
|
+
a2 %= m2
|
|
140
|
+
try:
|
|
141
|
+
n1: int = ModInv(m1, m2)
|
|
142
|
+
n2: int = ModInv(m2, m1)
|
|
143
|
+
except ModularDivideError as err:
|
|
144
|
+
raise ModularDivideError(f'moduli not co-prime: {m1=} / {m2=}') from err
|
|
145
|
+
return (a1 * m2 * n2 + a2 * m1 * n1) % (m1 * m2)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def ModExp(x: int, y: int, m: int, /) -> int:
|
|
149
|
+
"""Modular exponential: returns (x ** y) % m efficiently (can handle huge values).
|
|
150
|
+
|
|
151
|
+
0 ** 0 mod m = 1 (by convention)
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
x (int): integer
|
|
155
|
+
y (int): integer, y ≥ 0
|
|
156
|
+
m (int): modulus, m ≥ 2
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
(x ** y) mod m
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
InputError: invalid inputs
|
|
163
|
+
"""
|
|
164
|
+
# test inputs
|
|
165
|
+
if m < 2:
|
|
166
|
+
raise base.InputError(f'invalid modulus: {m=}')
|
|
167
|
+
if y < 0:
|
|
168
|
+
raise base.InputError(f'negative exponent: {y=}')
|
|
169
|
+
# trivial cases
|
|
170
|
+
x %= m
|
|
171
|
+
if not y or x == 1:
|
|
172
|
+
return 1 % m
|
|
173
|
+
if not x:
|
|
174
|
+
return 0 # 0**0==1 was already taken care of by previous condition
|
|
175
|
+
if y == 1:
|
|
176
|
+
return x
|
|
177
|
+
# now both x > 1 and y > 1
|
|
178
|
+
z: int = 1
|
|
179
|
+
while y:
|
|
180
|
+
y, odd = divmod(y, 2)
|
|
181
|
+
if odd:
|
|
182
|
+
z = (z * x) % m
|
|
183
|
+
x = (x * x) % m
|
|
184
|
+
return z
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def ModPolynomial(x: int, polynomial: Reversible[int], m: int, /) -> int:
|
|
188
|
+
"""Evaluates `polynomial` (coefficients iterable) at `x` modulus `m`.
|
|
189
|
+
|
|
190
|
+
Evaluate a polynomial at `x` under a modulus `m` using Horner's rule. Horner rewrites:
|
|
191
|
+
a_0 + a_1 x + a_2 x^2 + … + a_n x^n
|
|
192
|
+
= (…((a_n x + a_{n-1}) x + a_{n-2}) … ) x + a_0
|
|
193
|
+
This uses exactly n multiplies and n adds, and lets us take `% m` at each
|
|
194
|
+
step so intermediate numbers never explode.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
x (int): The evaluation point
|
|
198
|
+
polynomial (Reversible[int]): Iterable of coefficients a_0, a_1, …, a_n
|
|
199
|
+
(constant term first); it must be reversible because Horner's rule consumes
|
|
200
|
+
coefficients from highest degree downwards
|
|
201
|
+
m (int): modulus, m ≥ 2; if you expect multiplicative inverses elsewhere it should be prime
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
f(x) mod m
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
InputError: invalid inputs
|
|
208
|
+
"""
|
|
209
|
+
# test inputs
|
|
210
|
+
if not polynomial:
|
|
211
|
+
raise base.InputError(f'no polynomial: {polynomial=}')
|
|
212
|
+
if m < 2:
|
|
213
|
+
raise base.InputError(f'invalid modulus: {m=}')
|
|
214
|
+
# loop over polynomial coefficients
|
|
215
|
+
total: int = 0
|
|
216
|
+
x %= m # takes care of negative numbers and also x >= m
|
|
217
|
+
for coefficient in reversed(polynomial):
|
|
218
|
+
total = (total * x + coefficient) % m
|
|
219
|
+
return total
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def ModLagrangeInterpolate(x: int, points: dict[int, int], m: int, /) -> int:
|
|
223
|
+
"""Find the f(x) solution for the given `x` and {x: y} `points` modulus prime `m`.
|
|
224
|
+
|
|
225
|
+
Given `points` will define a polynomial of up to len(points) order.
|
|
226
|
+
Evaluate (interpolate) the unique polynomial of degree ≤ (n-1) that passes
|
|
227
|
+
through the given points (x_i, y_i), and return f(x) mod a prime `m`.
|
|
228
|
+
|
|
229
|
+
Lagrange interpolation writes the polynomial as:
|
|
230
|
+
f(X) = Σ_{i=0}^{n-1} y_i * L_i(X)
|
|
231
|
+
where
|
|
232
|
+
L_i(X) = Π_{j≠i} (X - x_j) / (x_i - x_j)
|
|
233
|
+
are the Lagrange basis polynomials. Each L_i(x_i) = 1 and L_i(x_j)=0 for j≠i,
|
|
234
|
+
so f matches every supplied point.
|
|
235
|
+
|
|
236
|
+
In modular arithmetic we replace division by multiplication with modular
|
|
237
|
+
inverses. Because `m` is prime (or at least co-prime with every denominator),
|
|
238
|
+
every (x_i - x_j) has an inverse `mod m`.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
x (int): The x-value at which to evaluate the interpolated polynomial
|
|
242
|
+
points (dict[int, int]): A mapping {x_i: y_i}, with at least 2 points/entries;
|
|
243
|
+
dict keeps x_i distinct, as they should be; also, `x` cannot be a key to `points`
|
|
244
|
+
m (int): prime modulus, m ≥ 2; we need modular inverses, so gcd(denominator, m) must be 1
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
y-value solution for f(x) mod m given `points` mapping
|
|
248
|
+
|
|
249
|
+
Raises:
|
|
250
|
+
InputError: invalid inputs
|
|
251
|
+
"""
|
|
252
|
+
# test inputs
|
|
253
|
+
if m < 2:
|
|
254
|
+
raise base.InputError(f'invalid modulus: {m=}')
|
|
255
|
+
x %= m # takes care of negative numbers and also x >= m
|
|
256
|
+
reduced_points: dict[int, int] = {k % m: v % m for k, v in points.items()}
|
|
257
|
+
if len(points) < 2 or len(reduced_points) != len(points) or x in reduced_points:
|
|
258
|
+
raise base.InputError(f'invalid points or duplicate x/x_i found: {x=} / {points=}')
|
|
259
|
+
# compute everything term-by-term
|
|
260
|
+
result: int = 0
|
|
261
|
+
for xi, yi in reduced_points.items():
|
|
262
|
+
# build numerator and denominator of L_i(x)
|
|
263
|
+
num: int = 1 # Π (x - x_j)
|
|
264
|
+
den: int = 1 # Π (xi - x_j)
|
|
265
|
+
for xj in reduced_points:
|
|
266
|
+
if xj == xi:
|
|
267
|
+
continue
|
|
268
|
+
num = (num * (x - xj)) % m
|
|
269
|
+
den = (den * (xi - xj)) % m
|
|
270
|
+
# add to the result: (y_i * L_i(x)) = (y_i * num / den)
|
|
271
|
+
result = (result + ModDiv(yi * num, den, m)) % m
|
|
272
|
+
# done
|
|
273
|
+
return result
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def FermatIsPrime(n: int, /, *, safety: int = 10, witnesses: set[int] | None = None) -> bool:
|
|
277
|
+
"""Primality test of `n` by Fermat's algo (n > 0). DO NOT RELY!
|
|
278
|
+
|
|
279
|
+
Will execute Fermat's algo for non-trivial `n` (n > 3 and odd).
|
|
280
|
+
<https://en.wikipedia.org/wiki/Fermat_primality_test>
|
|
281
|
+
|
|
282
|
+
This is for didactical uses only, as it is reasonably easy for this algo to fail
|
|
283
|
+
on simple cases. For example, 8911 will fail for many sets of 10 random witnesses.
|
|
284
|
+
(See <https://en.wikipedia.org/wiki/Carmichael_number> to understand better.)
|
|
285
|
+
Miller-Rabin below (MillerRabinIsPrime) has been tuned to be VERY reliable by default.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
n (int): Number to test primality
|
|
289
|
+
safety (int, optional): Maximum witnesses to use (only if witnesses is not given)
|
|
290
|
+
witnesses (set[int], optional): If given will use exactly these witnesses, in order
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
False if certainly not prime ; True if (probabilistically) prime
|
|
294
|
+
|
|
295
|
+
Raises:
|
|
296
|
+
InputError: invalid inputs
|
|
297
|
+
"""
|
|
298
|
+
# test inputs and test for trivial cases: 1, 2, 3, divisible by 2
|
|
299
|
+
if n < 1:
|
|
300
|
+
raise base.InputError(f'invalid number: {n=}')
|
|
301
|
+
if n in (2, 3):
|
|
302
|
+
return True
|
|
303
|
+
if n == 1 or not n % 2:
|
|
304
|
+
return False
|
|
305
|
+
# n is odd and >= 5 so now we generate witnesses (if needed)
|
|
306
|
+
# degenerate case is: n==5, max_safety==2 => randint(2, 3) => {2, 3}
|
|
307
|
+
if not witnesses:
|
|
308
|
+
max_safety: int = min(n // 2, _MAX_PRIMALITY_SAFETY)
|
|
309
|
+
if safety < 1:
|
|
310
|
+
raise base.InputError(f'out of bounds safety: 1 <= {safety=} <= {max_safety}')
|
|
311
|
+
safety = max_safety if safety > max_safety else safety
|
|
312
|
+
witnesses = set()
|
|
313
|
+
while len(witnesses) < safety:
|
|
314
|
+
witnesses.add(base.RandInt(2, n - 2))
|
|
315
|
+
# we have our witnesses: do the actual Fermat algo
|
|
316
|
+
for w in sorted(witnesses):
|
|
317
|
+
if not 2 <= w <= (n - 2):
|
|
318
|
+
raise base.InputError(f'out of bounds witness: 2 ≤ {w=} ≤ {n - 2}')
|
|
319
|
+
if ModExp(w, n - 1, n) != 1:
|
|
320
|
+
# number is proved to be composite
|
|
321
|
+
return False
|
|
322
|
+
# we declare the number PROBABLY a prime to the limits of this test
|
|
323
|
+
return True
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _MillerRabinWitnesses(n: int, /) -> set[int]: # pylint: disable=too-many-return-statements
|
|
327
|
+
"""Generates a reasonable set of Miller-Rabin witnesses for testing primality of `n`.
|
|
328
|
+
|
|
329
|
+
For n < 3317044064679887385961981 it is precise. That is more than 2**81. See:
|
|
330
|
+
<https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test#Testing_against_small_sets_of_bases>
|
|
331
|
+
|
|
332
|
+
For n >= 3317044064679887385961981 it is probabilistic, but computes an number of witnesses
|
|
333
|
+
that should make the test fail less than once in 2**80 tries (once in 10^25). For all intent and
|
|
334
|
+
purposes it "never" fails.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
n (int): number, n ≥ 5
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
{witness1, witness2, ...} for either "certainty" of primality or error chance < 10**25
|
|
341
|
+
|
|
342
|
+
Raises:
|
|
343
|
+
InputError: invalid inputs
|
|
344
|
+
"""
|
|
345
|
+
# test inputs
|
|
346
|
+
if n < 5:
|
|
347
|
+
raise base.InputError(f'invalid number: {n=}')
|
|
348
|
+
# for some "smaller" values there is research that shows these sets are always enough
|
|
349
|
+
if n < 2047:
|
|
350
|
+
return {2} # "safety" 1, but 100% coverage
|
|
351
|
+
if n < 9080191:
|
|
352
|
+
return {31, 73} # "safety" 2, but 100% coverage
|
|
353
|
+
if n < 4759123141:
|
|
354
|
+
return {2, 7, 61} # "safety" 3, but 100% coverage
|
|
355
|
+
if n < 2152302898747:
|
|
356
|
+
return set(_FIRST_60_PRIMES_SORTED[:5]) # "safety" 5, but 100% coverage
|
|
357
|
+
if n < 341550071728321:
|
|
358
|
+
return set(_FIRST_60_PRIMES_SORTED[:7]) # "safety" 7, but 100% coverage
|
|
359
|
+
if n < 18446744073709551616: # 2 ** 64
|
|
360
|
+
return set(_FIRST_60_PRIMES_SORTED[:12]) # "safety" 12, but 100% coverage
|
|
361
|
+
if n < 3317044064679887385961981: # > 2 ** 81
|
|
362
|
+
return set(_FIRST_60_PRIMES_SORTED[:13]) # "safety" 13, but 100% coverage
|
|
363
|
+
# here n should be greater than 2 ** 81, so safety should be 34 or less
|
|
364
|
+
n_bits: int = n.bit_length()
|
|
365
|
+
assert n_bits >= 82, f'should never happen: {n=} -> {n_bits=}'
|
|
366
|
+
safety: int = int(math.ceil(0.375 + 1.59 / (0.000590 * n_bits))) if n_bits <= 1700 else 2
|
|
367
|
+
assert 1 < safety <= 34, f'should never happen: {n=} -> {n_bits=} ; {safety=}'
|
|
368
|
+
return set(_FIRST_60_PRIMES_SORTED[:safety])
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _MillerRabinSR(n: int, /) -> tuple[int, int]:
|
|
372
|
+
"""Generates (s, r) where (2 ** s) * r == (n - 1) hold true, for odd n > 5.
|
|
373
|
+
|
|
374
|
+
It should be always true that: s ≥ 1 and r ≥ 1 and r is odd.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
n (int): odd number, n ≥ 5
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
(s, r) so that (2 ** s) * r == (n - 1)
|
|
381
|
+
|
|
382
|
+
Raises:
|
|
383
|
+
InputError: invalid inputs
|
|
384
|
+
"""
|
|
385
|
+
# test inputs
|
|
386
|
+
if n < 5 or not n % 2:
|
|
387
|
+
raise base.InputError(f'invalid odd number: {n=}')
|
|
388
|
+
# divide by 2 until we can't anymore
|
|
389
|
+
s: int = 1
|
|
390
|
+
r: int = (n - 1) // 2
|
|
391
|
+
while not r % 2:
|
|
392
|
+
s += 1
|
|
393
|
+
r //= 2
|
|
394
|
+
# make sure everything checks out and return
|
|
395
|
+
assert 1 <= r <= n and r % 2, f'should never happen: {n=} -> {r=}'
|
|
396
|
+
return (s, r)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def MillerRabinIsPrime(n: int, /, *, witnesses: set[int] | None = None) -> bool:
|
|
400
|
+
"""Primality test of `n` by Miller-Rabin's algo (n > 0).
|
|
401
|
+
|
|
402
|
+
Will execute Miller-Rabin's algo for non-trivial `n` (n > 3 and odd).
|
|
403
|
+
<https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test>
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
n (int): Number to test primality, n ≥ 1
|
|
407
|
+
witnesses (set[int], optional): If given will use exactly these witnesses, in order
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
False if certainly not prime ; True if (probabilistically) prime
|
|
411
|
+
|
|
412
|
+
Raises:
|
|
413
|
+
InputError: invalid inputs
|
|
414
|
+
"""
|
|
415
|
+
# test inputs and test for trivial cases: 1, 2, 3, divisible by 2
|
|
416
|
+
if n < 1:
|
|
417
|
+
raise base.InputError(f'invalid number: {n=}')
|
|
418
|
+
if n in (2, 3):
|
|
419
|
+
return True
|
|
420
|
+
if n == 1 or not n % 2:
|
|
421
|
+
return False
|
|
422
|
+
# n is odd and >= 5; find s and r so that (2 ** s) * r == (n - 1)
|
|
423
|
+
s, r = _MillerRabinSR(n)
|
|
424
|
+
# do the Miller-Rabin algo
|
|
425
|
+
n_limits: tuple[int, int] = (1, n - 1)
|
|
426
|
+
y: int
|
|
427
|
+
for w in sorted(witnesses if witnesses else _MillerRabinWitnesses(n)):
|
|
428
|
+
if not 2 <= w <= (n - 2):
|
|
429
|
+
raise base.InputError(f'out of bounds witness: 2 ≤ {w=} ≤ {n - 2}')
|
|
430
|
+
x: int = ModExp(w, r, n)
|
|
431
|
+
if x not in n_limits:
|
|
432
|
+
for _ in range(s): # s >= 1 so will execute at least once
|
|
433
|
+
y = (x * x) % n
|
|
434
|
+
if y == 1 and x not in n_limits:
|
|
435
|
+
return False # number is proved to be composite
|
|
436
|
+
x = y
|
|
437
|
+
if x != 1:
|
|
438
|
+
return False # number is proved to be composite
|
|
439
|
+
# we declare the number PROBABLY a prime to the limits of this test
|
|
440
|
+
return True
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def IsPrime(n: int, /) -> bool:
|
|
444
|
+
"""Primality test of `n` (n > 0).
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
n (int): Number to test primality, n ≥ 1
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
False if certainly not prime ; True if (probabilistically) prime
|
|
451
|
+
|
|
452
|
+
Raises:
|
|
453
|
+
InputError: invalid inputs
|
|
454
|
+
"""
|
|
455
|
+
# is number divisible by (one of the) first 60 primes? test should eliminate 80%+ of candidates
|
|
456
|
+
if n > _PRIME_60 and base.GCD(n, _COMPOSITE_60) != 1:
|
|
457
|
+
return False
|
|
458
|
+
# do the (more expensive) Miller-Rabin primality test
|
|
459
|
+
return MillerRabinIsPrime(n)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def PrimeGenerator(start: int, /) -> Generator[int, None, None]:
|
|
463
|
+
"""Generates all primes from `start` until loop is broken. Tuned for huge numbers.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
start (int): number at which to start generating primes, start ≥ 0
|
|
467
|
+
|
|
468
|
+
Yields:
|
|
469
|
+
prime numbers (int)
|
|
470
|
+
|
|
471
|
+
Raises:
|
|
472
|
+
InputError: invalid inputs
|
|
473
|
+
"""
|
|
474
|
+
# test inputs and make sure we start at an odd number
|
|
475
|
+
if start < 0:
|
|
476
|
+
raise base.InputError(f'negative number: {start=}')
|
|
477
|
+
# handle start of sequence manually if needed... because we have here the only EVEN prime...
|
|
478
|
+
if start <= 2:
|
|
479
|
+
yield 2
|
|
480
|
+
start = 3
|
|
481
|
+
# we now focus on odd numbers only and loop forever
|
|
482
|
+
n: int = (start if start % 2 else start + 1) - 2 # n >= 1 always
|
|
483
|
+
while True:
|
|
484
|
+
n += 2 # next odd number
|
|
485
|
+
if IsPrime(n):
|
|
486
|
+
yield n # found a prime
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def NBitRandomPrime(n_bits: int, /) -> int:
|
|
490
|
+
"""Generates a random prime with (guaranteed) `n_bits` size (i.e., first bit == 1).
|
|
491
|
+
|
|
492
|
+
The fact that the first bit will be 1 means the entropy is ~ (n_bits-1) and
|
|
493
|
+
because of this we only allow for a byte or more prime bits generated. This drawback
|
|
494
|
+
is negligible for the large primes a crypto library will work with, in practice.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
n_bits (int): Number of guaranteed bits in prime representation, n ≥ 8
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
random prime with `n_bits` bits
|
|
501
|
+
|
|
502
|
+
Raises:
|
|
503
|
+
InputError: invalid inputs
|
|
504
|
+
"""
|
|
505
|
+
# test inputs
|
|
506
|
+
if n_bits < 8:
|
|
507
|
+
raise base.InputError(f'invalid n: {n_bits=}')
|
|
508
|
+
# get a random number with guaranteed bit size
|
|
509
|
+
prime: int = 0
|
|
510
|
+
while prime.bit_length() != n_bits:
|
|
511
|
+
prime = next(PrimeGenerator(base.RandBits(n_bits)))
|
|
512
|
+
return prime
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def MersennePrimesGenerator(start: int, /) -> Generator[tuple[int, int, int], None, None]:
|
|
516
|
+
"""Generates all Mersenne prime (2 ** n - 1) exponents from 2**start until loop is broken.
|
|
517
|
+
|
|
518
|
+
<https://en.wikipedia.org/wiki/List_of_Mersenne_primes_and_perfect_numbers>
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
start (int): exponent at which to start generating primes, start ≥ 0
|
|
522
|
+
|
|
523
|
+
Yields:
|
|
524
|
+
(exponent, mersenne_prime, perfect_number), given some exponent `n` that will be exactly:
|
|
525
|
+
(n, 2 ** n - 1, (2 ** (n - 1)) * (2 ** n - 1))
|
|
526
|
+
|
|
527
|
+
Raises:
|
|
528
|
+
InputError: invalid inputs
|
|
529
|
+
"""
|
|
530
|
+
# we now loop forever over prime exponents
|
|
531
|
+
# "The exponents p corresponding to Mersenne primes must themselves be prime."
|
|
532
|
+
for n in PrimeGenerator(start if start >= 1 else 1):
|
|
533
|
+
mersenne: int = 2 ** n - 1
|
|
534
|
+
if IsPrime(mersenne):
|
|
535
|
+
yield (n, mersenne, (2 ** (n - 1)) * mersenne) # found: also yield perfect number
|