transcrypto 1.0.2__py3-none-any.whl → 1.0.3__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/transcrypto.py +829 -46
- {transcrypto-1.0.2.dist-info → transcrypto-1.0.3.dist-info}/METADATA +1 -1
- transcrypto-1.0.3.dist-info/RECORD +8 -0
- transcrypto-1.0.2.dist-info/RECORD +0 -8
- {transcrypto-1.0.2.dist-info → transcrypto-1.0.3.dist-info}/WHEEL +0 -0
- {transcrypto-1.0.2.dist-info → transcrypto-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {transcrypto-1.0.2.dist-info → transcrypto-1.0.3.dist-info}/top_level.txt +0 -0
transcrypto/transcrypto.py
CHANGED
|
@@ -4,42 +4,82 @@
|
|
|
4
4
|
#
|
|
5
5
|
"""Balparda's TransCrypto."""
|
|
6
6
|
|
|
7
|
+
import dataclasses
|
|
8
|
+
import datetime
|
|
9
|
+
import logging
|
|
7
10
|
import math
|
|
8
11
|
# import pdb
|
|
9
|
-
import
|
|
10
|
-
from typing import Generator, Optional
|
|
12
|
+
import secrets
|
|
13
|
+
from typing import Collection, Generator, Optional, Reversible, Self
|
|
11
14
|
|
|
12
15
|
__author__ = 'balparda@github.com'
|
|
13
|
-
__version__: tuple[int, int, int] = (1, 0,
|
|
16
|
+
__version__: tuple[int, int, int] = (1, 0, 3) # v1.0.3, 2025-07-30
|
|
14
17
|
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
FIRST_60_PRIMES: set[int] = {
|
|
17
20
|
2, 3, 5, 7, 11, 13, 17, 19, 23, 29,
|
|
18
21
|
31, 37, 41, 43, 47, 53, 59, 61, 67, 71,
|
|
19
22
|
73, 79, 83, 89, 97, 101, 103, 107, 109, 113,
|
|
20
23
|
127, 131, 137, 139, 149, 151, 157, 163, 167, 173,
|
|
21
24
|
179, 181, 191, 193, 197, 199, 211, 223, 227, 229,
|
|
22
25
|
233, 239, 241, 251, 257, 263, 269, 271, 277, 281,
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
}
|
|
27
|
+
FIRST_60_PRIMES_SORTED: list[int] = sorted(FIRST_60_PRIMES)
|
|
25
28
|
COMPOSITE_60: int = math.prod(FIRST_60_PRIMES_SORTED)
|
|
26
|
-
PRIME_60: int = FIRST_60_PRIMES_SORTED[-1]
|
|
29
|
+
PRIME_60: int = FIRST_60_PRIMES_SORTED[-1]
|
|
30
|
+
assert len(FIRST_60_PRIMES) == 60 and PRIME_60 == 281, f'should never happen: {PRIME_60=}'
|
|
31
|
+
FIRST_49_MERSENNE: set[int] = { # <https://oeis.org/A000043>
|
|
32
|
+
2, 3, 5, 7, 13, 17, 19, 31, 61, 89,
|
|
33
|
+
107, 127, 521, 607, 1279, 2203, 2281, 3217, 4253, 4423,
|
|
34
|
+
9689, 9941, 11213, 19937, 21701, 23209, 44497, 86243, 110503, 132049,
|
|
35
|
+
216091, 756839, 859433, 1257787, 1398269, 2976221, 3021377, 6972593, 13466917, 20996011,
|
|
36
|
+
24036583, 25964951, 30402457, 32582657, 37156667, 42643801, 43112609, 57885161, 74207281,
|
|
37
|
+
}
|
|
38
|
+
FIRST_49_MERSENNE_SORTED: list[int] = sorted(FIRST_49_MERSENNE)
|
|
39
|
+
assert len(FIRST_49_MERSENNE) == 49 and FIRST_49_MERSENNE_SORTED[-1] == 74207281, f'should never happen: {FIRST_49_MERSENNE_SORTED[-1]}'
|
|
40
|
+
|
|
41
|
+
_SMALL_ENCRYPTION_EXPONENT = 7
|
|
42
|
+
_BIG_ENCRYPTION_EXPONENT = 2 ** 16 + 1 # 65537
|
|
27
43
|
|
|
28
44
|
_MAX_PRIMALITY_SAFETY = 100 # this is an absurd number, just to have a max
|
|
45
|
+
_MAX_KEY_GENERATION_FAILURES = 15
|
|
46
|
+
|
|
47
|
+
MIN_TM = int( # minimum allowed timestamp
|
|
48
|
+
datetime.datetime(2000, 1, 1, 0, 0, 0).replace(tzinfo=datetime.timezone.utc).timestamp())
|
|
29
49
|
|
|
30
50
|
|
|
31
51
|
class Error(Exception):
|
|
32
52
|
"""TransCrypto exception."""
|
|
33
53
|
|
|
34
54
|
|
|
55
|
+
class InputError(Error):
|
|
56
|
+
"""Input exception (TransCrypto)."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ModularDivideError(Error):
|
|
60
|
+
"""Divide-by-zero-like exception (TransCrypto)."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CryptoError(Error):
|
|
64
|
+
"""Cryptographic exception (TransCrypto)."""
|
|
65
|
+
|
|
66
|
+
|
|
35
67
|
def GCD(a: int, b: int, /) -> int:
|
|
36
|
-
"""Greatest Common Divisor for `a` and `b`, positive integers.
|
|
68
|
+
"""Greatest Common Divisor for `a` and `b`, positive integers. Uses the Euclid method.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
a (int): integer a ≥ 0
|
|
72
|
+
b (int): integer b ≥ 0
|
|
37
73
|
|
|
38
|
-
|
|
74
|
+
Returns:
|
|
75
|
+
gcd(a, b)
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
InputError: invalid inputs
|
|
39
79
|
"""
|
|
40
80
|
# test inputs
|
|
41
81
|
if a < 0 or b < 0:
|
|
42
|
-
raise
|
|
82
|
+
raise InputError(f'negative input: {a=} , {b=}')
|
|
43
83
|
# algo needs to start with a >= b
|
|
44
84
|
if a < b:
|
|
45
85
|
a, b = b, a
|
|
@@ -51,17 +91,22 @@ def GCD(a: int, b: int, /) -> int:
|
|
|
51
91
|
|
|
52
92
|
|
|
53
93
|
def ExtendedGCD(a: int, b: int, /) -> tuple[int, int, int]:
|
|
54
|
-
"""Greatest Common Divisor Extended for `a` and `b`, positive integers.
|
|
94
|
+
"""Greatest Common Divisor Extended for `a` and `b`, positive integers. Uses the Euclid method.
|
|
55
95
|
|
|
56
|
-
|
|
96
|
+
Args:
|
|
97
|
+
a (int): integer a ≥ 0
|
|
98
|
+
b (int): integer b ≥ 0
|
|
57
99
|
|
|
58
100
|
Returns:
|
|
59
101
|
(gcd, x, y) so that a * x + b * y = gcd
|
|
60
102
|
x and y may be negative integers or zero but won't be both zero.
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
InputError: invalid inputs
|
|
61
106
|
"""
|
|
62
107
|
# test inputs
|
|
63
108
|
if a < 0 or b < 0:
|
|
64
|
-
raise
|
|
109
|
+
raise InputError(f'negative input: {a=} , {b=}')
|
|
65
110
|
# algo needs to start with a >= b (but we remember if we did swap)
|
|
66
111
|
swapped = False
|
|
67
112
|
if a < b:
|
|
@@ -79,18 +124,83 @@ def ExtendedGCD(a: int, b: int, /) -> tuple[int, int, int]:
|
|
|
79
124
|
return (a, y2 if swapped else x2, x2 if swapped else y2)
|
|
80
125
|
|
|
81
126
|
|
|
127
|
+
def ModInv(x: int, m: int, /) -> int:
|
|
128
|
+
"""Modular inverse of `x` modulo `m`: a `y` such that (x * y) % m == 1 if GCD(x, m) == 1.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
x (int): integer to invert, x ≥ 0
|
|
132
|
+
m (int): modulo, m ≥ 1
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
positive integer `y` such that (x * y) % m == 1
|
|
136
|
+
this only exists if GCD(x, m) == 1, so to guarantee an inverse `m` must be prime
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
InputError: invalid modulus or x
|
|
140
|
+
ModularDivideError: divide-by-zero, i.e., GCD(x, m) != 1 or x == 0
|
|
141
|
+
"""
|
|
142
|
+
# test inputs
|
|
143
|
+
if m < 1:
|
|
144
|
+
raise InputError(f'invalid modulus: {m=}')
|
|
145
|
+
if not 0 <= x < m:
|
|
146
|
+
raise InputError(f'invalid input: {x=}')
|
|
147
|
+
# easy special cases: 0 and 1
|
|
148
|
+
if not x: # "division by 0"
|
|
149
|
+
gcd = m
|
|
150
|
+
raise ModularDivideError(f'null inverse {x=} mod {m=} with {gcd=}')
|
|
151
|
+
if x == 1: # trivial degenerate case
|
|
152
|
+
return 1
|
|
153
|
+
# compute actual extended GCD and see if we will have an inverse
|
|
154
|
+
gcd, y, w = ExtendedGCD(x, m)
|
|
155
|
+
if gcd != 1:
|
|
156
|
+
raise ModularDivideError(f'invalid inverse {x=} mod {m=} with {gcd=}')
|
|
157
|
+
assert y and w and y >= -m, f'should never happen: {x=} mod {m=} -> {w=} ; {y=}'
|
|
158
|
+
return y if y >= 0 else (y + m)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def ModDiv(x: int, y: int, m: int, /) -> int:
|
|
162
|
+
"""Modular division of `x`/`y` modulo `m`, if GCD(y, m) == 1.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
x (int): integer, x ≥ 0
|
|
166
|
+
y (int): integer, y ≥ 0
|
|
167
|
+
m (int): modulo, m ≥ 1
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
positive integer `z` such that (z * y) % m == x
|
|
171
|
+
this only exists if GCD(y, m) == 1, so to guarantee an inverse `m` must be prime
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
InputError: invalid modulus or x or y
|
|
175
|
+
ModularDivideError: divide-by-zero, i.e., GCD(y, m) != 1 or y == 0
|
|
176
|
+
"""
|
|
177
|
+
return ((x % m) * ModInv(y % m, m)) % m
|
|
178
|
+
|
|
179
|
+
|
|
82
180
|
def ModExp(x: int, y: int, m: int, /) -> int:
|
|
83
|
-
"""Modular exponential: returns (x ** y) % m efficiently (can handle huge values).
|
|
181
|
+
"""Modular exponential: returns (x ** y) % m efficiently (can handle huge values).
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
x (int): integer, x ≥ 0
|
|
185
|
+
y (int): integer, y ≥ 0
|
|
186
|
+
m (int): modulo, m ≥ 1
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
(x ** y) mod m
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
InputError: invalid inputs
|
|
193
|
+
"""
|
|
84
194
|
# test inputs
|
|
85
195
|
if x < 0 or y < 0:
|
|
86
|
-
raise
|
|
196
|
+
raise InputError(f'negative input: {x=} , {y=}')
|
|
87
197
|
if m < 1:
|
|
88
|
-
raise
|
|
198
|
+
raise InputError(f'invalid modulus: {m=}')
|
|
89
199
|
# trivial cases
|
|
90
|
-
if not x:
|
|
91
|
-
return 0
|
|
92
200
|
if not y or x == 1:
|
|
93
201
|
return 1 % m
|
|
202
|
+
if not x:
|
|
203
|
+
return 0 # 0**0==1 was already taken care of by previous condition
|
|
94
204
|
if y == 1:
|
|
95
205
|
return x % m
|
|
96
206
|
# now both x > 1 and y > 1
|
|
@@ -103,6 +213,95 @@ def ModExp(x: int, y: int, m: int, /) -> int:
|
|
|
103
213
|
return z
|
|
104
214
|
|
|
105
215
|
|
|
216
|
+
def ModPolynomial(x: int, polynomial: Reversible[int], m: int, /) -> int:
|
|
217
|
+
"""Evaluates polynomial `poly` (coefficient iterable) at `x` modulus `m`.
|
|
218
|
+
|
|
219
|
+
Evaluate a polynomial at `x` under a modulus `m` using Horner's rule. Horner rewrites:
|
|
220
|
+
a_0 + a_1 x + a_2 x^2 + … + a_n x^n
|
|
221
|
+
= (…((a_n x + a_{n-1}) x + a_{n-2}) … ) x + a_0
|
|
222
|
+
This uses exactly n multiplies and n adds, and lets us take `% m` at each
|
|
223
|
+
step so intermediate numbers never explode.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
x (int) The evaluation point (x ≥ 0)
|
|
227
|
+
polynomial (Reversible[int]): Iterable of coefficients a_0, a_1, …, a_n
|
|
228
|
+
(constant term first); it must be reversible because Horner's rule consumes
|
|
229
|
+
coefficients from highest degree downwards
|
|
230
|
+
m (int): Modulus (m ≥ 1); if you expect multiplicative inverses elsewhere, should be prime
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
f(x) mod m
|
|
234
|
+
|
|
235
|
+
Raises:
|
|
236
|
+
InputError: invalid inputs
|
|
237
|
+
"""
|
|
238
|
+
# test inputs
|
|
239
|
+
if x < 0 or not polynomial:
|
|
240
|
+
raise InputError(f'negative input or no polynomial: {x=} ; {polynomial=}')
|
|
241
|
+
if m < 1:
|
|
242
|
+
raise InputError(f'invalid modulus: {m=}')
|
|
243
|
+
# loop over polynomial coefficients
|
|
244
|
+
total: int = 0
|
|
245
|
+
x %= m
|
|
246
|
+
for coefficient in reversed(polynomial):
|
|
247
|
+
total = (total * x + coefficient) % m
|
|
248
|
+
return total
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def ModLagrangeInterpolate(x: int, points: dict[int, int], m: int, /) -> int:
|
|
252
|
+
"""Find the f(x) solution for the given `x` and {x: y} `points` modulus prime `m`.
|
|
253
|
+
|
|
254
|
+
Given `points` will define a polynomial of up to len(points) order.
|
|
255
|
+
Evaluate (interpolate) the unique polynomial of degree ≤ (n-1) that passes
|
|
256
|
+
through the given points (x_i, y_i), and return f(x) modulo a prime `m`.
|
|
257
|
+
|
|
258
|
+
Lagrange interpolation writes the polynomial as:
|
|
259
|
+
f(X) = Σ_{i=0}^{n-1} y_i * L_i(X)
|
|
260
|
+
where
|
|
261
|
+
L_i(X) = Π_{j≠i} (X - x_j) / (x_i - x_j)
|
|
262
|
+
are the Lagrange basis polynomials. Each L_i(x_i) = 1 and L_i(x_j)=0 for j≠i,
|
|
263
|
+
so f matches every supplied point.
|
|
264
|
+
|
|
265
|
+
In modular arithmetic we replace division by multiplication with modular
|
|
266
|
+
inverses. Because `m` is prime (or at least co-prime with every denominator),
|
|
267
|
+
every (x_i - x_j) has an inverse `mod m`.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
x (int): The x-value at which to evaluate the interpolated polynomial, x ≥ 0
|
|
271
|
+
points (dict[int, int]): A mapping {x_i: y_i}, where all 0 ≤ x_i < m and 0 ≤ y_i < m, minimum of 2
|
|
272
|
+
m (int): Prime modulus (m ≥ 2); we need modular inverses, so gcd(denominator, m) must be 1
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
y-value solution for f(x) mod m given `points` mapping
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
InputError: invalid inputs
|
|
279
|
+
"""
|
|
280
|
+
# test inputs
|
|
281
|
+
if x < 0:
|
|
282
|
+
raise InputError(f'negative input: {x=}')
|
|
283
|
+
if m < 2:
|
|
284
|
+
raise InputError(f'invalid modulus: {m=}')
|
|
285
|
+
if len(points) < 2 or any(not 0 <= k < m or not 0 <= v < m for k, v in points.items()):
|
|
286
|
+
raise InputError(f'invalid points: {points=}')
|
|
287
|
+
# compute everything term-by-term
|
|
288
|
+
x %= m
|
|
289
|
+
result: int = 0
|
|
290
|
+
for xi, yi in points.items():
|
|
291
|
+
# build numerator and denominator of L_i(x)
|
|
292
|
+
num: int = 1 # Π (x - x_j)
|
|
293
|
+
den: int = 1 # Π (xi - x_j)
|
|
294
|
+
for xj in points:
|
|
295
|
+
if xj == xi:
|
|
296
|
+
continue
|
|
297
|
+
num = (num * (x - xj)) % m
|
|
298
|
+
den = (den * (xi - xj)) % m
|
|
299
|
+
# add to the result: (y_i * L_i(x)) = (y_i * num / den)
|
|
300
|
+
result = (result + ModDiv(yi * num, den, m)) % m
|
|
301
|
+
# done
|
|
302
|
+
return result
|
|
303
|
+
|
|
304
|
+
|
|
106
305
|
def FermatIsPrime(
|
|
107
306
|
n: int, /, *,
|
|
108
307
|
safety: int = 10,
|
|
@@ -124,10 +323,13 @@ def FermatIsPrime(
|
|
|
124
323
|
|
|
125
324
|
Returns:
|
|
126
325
|
False if certainly not prime ; True if (probabilistically) prime
|
|
326
|
+
|
|
327
|
+
Raises:
|
|
328
|
+
InputError: invalid inputs
|
|
127
329
|
"""
|
|
128
330
|
# test inputs and test for trivial cases: 1, 2, 3, divisible by 2
|
|
129
331
|
if n < 1:
|
|
130
|
-
raise
|
|
332
|
+
raise InputError(f'invalid number: {n=}')
|
|
131
333
|
if n in (2, 3):
|
|
132
334
|
return True
|
|
133
335
|
if n == 1 or not n % 2:
|
|
@@ -137,15 +339,16 @@ def FermatIsPrime(
|
|
|
137
339
|
if not witnesses:
|
|
138
340
|
max_safety: int = min(n // 2, _MAX_PRIMALITY_SAFETY)
|
|
139
341
|
if safety < 1:
|
|
140
|
-
raise
|
|
342
|
+
raise InputError(f'out of bounds safety: 1 <= {safety=} <= {max_safety}')
|
|
141
343
|
safety = max_safety if safety > max_safety else safety
|
|
142
344
|
witnesses = set()
|
|
345
|
+
rand = secrets.SystemRandom()
|
|
143
346
|
while len(witnesses) < safety:
|
|
144
|
-
witnesses.add(
|
|
347
|
+
witnesses.add(rand.randint(2, n - 2))
|
|
145
348
|
# we have our witnesses: do the actual Fermat algo
|
|
146
349
|
for w in sorted(witnesses):
|
|
147
350
|
if not 2 <= w <= (n - 2):
|
|
148
|
-
raise
|
|
351
|
+
raise InputError(f'out of bounds witness: 2 <= {w=} <= {n - 2}')
|
|
149
352
|
if ModExp(w, n - 1, n) != 1:
|
|
150
353
|
# number is proved to be composite
|
|
151
354
|
return False
|
|
@@ -162,10 +365,19 @@ def _MillerRabinWitnesses(n: int, /) -> set[int]: # pylint: disable=too-many-re
|
|
|
162
365
|
For n >= 3317044064679887385961981 it is probabilistic, but computes an number of witnesses
|
|
163
366
|
that should make the test fail less than once in 2**80 tries (once in 10^25). For all intent and
|
|
164
367
|
purposes it "never" fails.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
n (int): number, n ≥ 5
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
{witness1, witness2, ...} for either "certainty" of primality or error chance < 10**25
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
InputError: invalid inputs
|
|
165
377
|
"""
|
|
166
378
|
# test inputs
|
|
167
379
|
if n < 5:
|
|
168
|
-
raise
|
|
380
|
+
raise InputError(f'invalid number: {n=}')
|
|
169
381
|
# for some "smaller" values there is research that shows these sets are always enough
|
|
170
382
|
if n < 2047:
|
|
171
383
|
return {2} # "safety" 1, but 100% coverage
|
|
@@ -183,9 +395,9 @@ def _MillerRabinWitnesses(n: int, /) -> set[int]: # pylint: disable=too-many-re
|
|
|
183
395
|
return set(FIRST_60_PRIMES_SORTED[:13]) # "safety" 13, but 100% coverage
|
|
184
396
|
# here n should be greater than 2 ** 81, so safety should be 34 or less
|
|
185
397
|
n_bits: int = n.bit_length()
|
|
186
|
-
assert n_bits >= 82
|
|
398
|
+
assert n_bits >= 82, f'should never happen: {n=} -> {n_bits=}'
|
|
187
399
|
safety: int = int(math.ceil(0.375 + 1.59 / (0.000590 * n_bits))) if n_bits <= 1700 else 2
|
|
188
|
-
assert 1 < safety <= 34
|
|
400
|
+
assert 1 < safety <= 34, f'should never happen: {n=} -> {n_bits=} ; {safety=}'
|
|
189
401
|
return set(FIRST_60_PRIMES_SORTED[:safety])
|
|
190
402
|
|
|
191
403
|
|
|
@@ -193,10 +405,19 @@ def _MillerRabinSR(n: int, /) -> tuple[int, int]:
|
|
|
193
405
|
"""Generates (s, r) where (2 ** s) * r == (n - 1) hold true, for odd n > 5.
|
|
194
406
|
|
|
195
407
|
It should be always true that: s >= 1 and r >= 1 and r is odd.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
n (int): odd number, n ≥ 5
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
(s, r) so that (2 ** s) * r == (n - 1)
|
|
414
|
+
|
|
415
|
+
Raises:
|
|
416
|
+
InputError: invalid inputs
|
|
196
417
|
"""
|
|
197
418
|
# test inputs
|
|
198
419
|
if n < 5 or not n % 2:
|
|
199
|
-
raise
|
|
420
|
+
raise InputError(f'invalid odd number: {n=}')
|
|
200
421
|
# divide by 2 until we can't anymore
|
|
201
422
|
s: int = 1
|
|
202
423
|
r: int = (n - 1) // 2
|
|
@@ -204,7 +425,7 @@ def _MillerRabinSR(n: int, /) -> tuple[int, int]:
|
|
|
204
425
|
s += 1
|
|
205
426
|
r //= 2
|
|
206
427
|
# make sure everything checks out and return
|
|
207
|
-
assert 1 <= r <= n and r % 2
|
|
428
|
+
assert 1 <= r <= n and r % 2, f'should never happen: {n=} -> {r=}'
|
|
208
429
|
return (s, r)
|
|
209
430
|
|
|
210
431
|
|
|
@@ -217,15 +438,18 @@ def MillerRabinIsPrime(
|
|
|
217
438
|
<https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test>
|
|
218
439
|
|
|
219
440
|
Args:
|
|
220
|
-
n (int): Number to test primality
|
|
441
|
+
n (int): Number to test primality, n ≥ 1
|
|
221
442
|
witnesses (set[int], optional): If given will use exactly these witnesses, in order
|
|
222
443
|
|
|
223
444
|
Returns:
|
|
224
445
|
False if certainly not prime ; True if (probabilistically) prime
|
|
446
|
+
|
|
447
|
+
Raises:
|
|
448
|
+
InputError: invalid inputs
|
|
225
449
|
"""
|
|
226
450
|
# test inputs and test for trivial cases: 1, 2, 3, divisible by 2
|
|
227
451
|
if n < 1:
|
|
228
|
-
raise
|
|
452
|
+
raise InputError(f'invalid number: {n=}')
|
|
229
453
|
if n in (2, 3):
|
|
230
454
|
return True
|
|
231
455
|
if n == 1 or not n % 2:
|
|
@@ -237,7 +461,7 @@ def MillerRabinIsPrime(
|
|
|
237
461
|
y: int
|
|
238
462
|
for w in sorted(witnesses if witnesses else _MillerRabinWitnesses(n)):
|
|
239
463
|
if not 2 <= w <= (n - 2):
|
|
240
|
-
raise
|
|
464
|
+
raise InputError(f'out of bounds witness: 2 <= {w=} <= {n - 2}')
|
|
241
465
|
x: int = ModExp(w, r, n)
|
|
242
466
|
if x not in n_limits:
|
|
243
467
|
for _ in range(s): # s >= 1 so will execute at least once
|
|
@@ -251,11 +475,40 @@ def MillerRabinIsPrime(
|
|
|
251
475
|
return True
|
|
252
476
|
|
|
253
477
|
|
|
254
|
-
def
|
|
255
|
-
"""
|
|
478
|
+
def IsPrime(n: int, /) -> bool:
|
|
479
|
+
"""Primality test of `n` (n > 0).
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
n (int): Number to test primality, n ≥ 1
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
False if certainly not prime ; True if (probabilistically) prime
|
|
486
|
+
|
|
487
|
+
Raises:
|
|
488
|
+
InputError: invalid inputs
|
|
489
|
+
"""
|
|
490
|
+
# is number divisible by (one of the) first 60 primes? test should eliminate 80%+ of candidates
|
|
491
|
+
if n > PRIME_60 and GCD(n, COMPOSITE_60) != 1:
|
|
492
|
+
return False
|
|
493
|
+
# do the (more expensive) Miller-Rabin primality test
|
|
494
|
+
return MillerRabinIsPrime(n)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def PrimeGenerator(start: int, /) -> Generator[int, None, None]:
|
|
498
|
+
"""Generates all primes from `start` until loop is broken. Tuned for huge numbers.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
start (int): number at which to start generating primes, start ≥ 0
|
|
502
|
+
|
|
503
|
+
Yields:
|
|
504
|
+
prime numbers (int)
|
|
505
|
+
|
|
506
|
+
Raises:
|
|
507
|
+
InputError: invalid inputs
|
|
508
|
+
"""
|
|
256
509
|
# test inputs and make sure we start at an odd number
|
|
257
510
|
if start < 0:
|
|
258
|
-
raise
|
|
511
|
+
raise InputError(f'invalid number: {start=}')
|
|
259
512
|
# handle start of sequence manually if needed... because we have here the only EVEN prime...
|
|
260
513
|
if start <= 2:
|
|
261
514
|
yield 2
|
|
@@ -264,31 +517,561 @@ def PrimeGenerator(start: int) -> Generator[int, None, None]:
|
|
|
264
517
|
n: int = (start if start % 2 else start + 1) - 2 # n >= 1 always
|
|
265
518
|
while True:
|
|
266
519
|
n += 2 # next odd number
|
|
267
|
-
|
|
268
|
-
if n > PRIME_60 and GCD(n, COMPOSITE_60) != 1:
|
|
269
|
-
continue # not prime
|
|
270
|
-
# do the (more expensive) primality test
|
|
271
|
-
if MillerRabinIsPrime(n):
|
|
520
|
+
if IsPrime(n):
|
|
272
521
|
yield n # found a prime
|
|
273
522
|
|
|
274
523
|
|
|
275
|
-
def
|
|
524
|
+
def NBitRandomPrime(n_bits: int, /) -> int:
|
|
525
|
+
"""Generates a random prime with (guaranteed) `n_bits` binary representation length.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
n_bits (int): Number of guaranteed bits in prime representation, n ≥ 4
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
random prime with `n_bits` bits
|
|
532
|
+
|
|
533
|
+
Raises:
|
|
534
|
+
InputError: invalid inputs
|
|
535
|
+
"""
|
|
536
|
+
# test inputs
|
|
537
|
+
if n_bits < 4:
|
|
538
|
+
raise InputError(f'invalid n: {n_bits=}')
|
|
539
|
+
# get a random number with guaranteed bit size
|
|
540
|
+
min_start: int = 2 ** (n_bits - 1)
|
|
541
|
+
prime: int = 0
|
|
542
|
+
while prime.bit_length() != n_bits:
|
|
543
|
+
start_point: int = secrets.randbits(n_bits)
|
|
544
|
+
while start_point < min_start:
|
|
545
|
+
# i know we could just set the bit, but IMO it is better to get another entirely
|
|
546
|
+
start_point = secrets.randbits(n_bits)
|
|
547
|
+
prime = next(PrimeGenerator(start_point))
|
|
548
|
+
return prime
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def MersennePrimesGenerator(start: int, /) -> Generator[tuple[int, int, int], None, None]:
|
|
276
552
|
"""Generates all Mersenne prime (2 ** n - 1) exponents from 2**start until loop is broken.
|
|
277
553
|
|
|
278
554
|
<https://en.wikipedia.org/wiki/List_of_Mersenne_primes_and_perfect_numbers>
|
|
279
555
|
|
|
556
|
+
Args:
|
|
557
|
+
start (int): exponent at which to start generating primes, start ≥ 0
|
|
558
|
+
|
|
280
559
|
Yields:
|
|
281
560
|
(exponent, mersenne_prime, perfect_number), given some exponent `n` that will be exactly:
|
|
282
561
|
(n, 2 ** n - 1, (2 ** (n - 1)) * (2 ** n - 1))
|
|
562
|
+
|
|
563
|
+
Raises:
|
|
564
|
+
InputError: invalid inputs
|
|
283
565
|
"""
|
|
284
566
|
# we now loop forever over prime exponents
|
|
285
567
|
# "The exponents p corresponding to Mersenne primes must themselves be prime."
|
|
286
568
|
for n in PrimeGenerator(start if start >= 1 else 1):
|
|
287
569
|
mersenne: int = 2 ** n - 1
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
570
|
+
if IsPrime(mersenne):
|
|
571
|
+
yield (n, mersenne, (2 ** (n - 1)) * mersenne) # found: also yield perfect number
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
575
|
+
class CryptoKey:
|
|
576
|
+
"""A cryptographic key."""
|
|
577
|
+
|
|
578
|
+
def __post_init__(self) -> None:
|
|
579
|
+
"""Check data."""
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
583
|
+
class RSAPublicKey(CryptoKey):
|
|
584
|
+
"""RSA (Rivest-Shamir-Adleman) key, with the public part of the key.
|
|
585
|
+
|
|
586
|
+
Attributes:
|
|
587
|
+
public_modulus (int): modulus (p * q), ≥ 6
|
|
588
|
+
encrypt_exp (int): encryption exponent, 3 ≤ e < modulus, (e * decrypt) % ((p-1) * (q-1)) == 1
|
|
589
|
+
"""
|
|
590
|
+
|
|
591
|
+
public_modulus: int
|
|
592
|
+
encrypt_exp: int
|
|
593
|
+
|
|
594
|
+
def __post_init__(self) -> None:
|
|
595
|
+
"""Check data.
|
|
596
|
+
|
|
597
|
+
Raises:
|
|
598
|
+
InputError: invalid inputs
|
|
599
|
+
"""
|
|
600
|
+
super(RSAPublicKey, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
|
|
601
|
+
if self.public_modulus < 6 or IsPrime(self.public_modulus):
|
|
602
|
+
# only a full factors check can prove modulus is product of only 2 primes, which is impossible
|
|
603
|
+
# to do for large numbers here; the private key checks the relationship though
|
|
604
|
+
raise InputError(f'invalid public_modulus: {self}')
|
|
605
|
+
if not 2 < self.encrypt_exp < self.public_modulus or not IsPrime(self.encrypt_exp):
|
|
606
|
+
# technically, encrypt_exp < phi, but again the private key tests for this explicitly
|
|
607
|
+
raise InputError(f'invalid encrypt_exp: {self}')
|
|
608
|
+
|
|
609
|
+
def Encrypt(self, message: int, /) -> int:
|
|
610
|
+
"""Encrypt `message` with this public key.
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
message (int): message to encrypt, 1 ≤ m < modulus
|
|
614
|
+
|
|
615
|
+
Returns:
|
|
616
|
+
encrypted message (int, 1 ≤ m < modulus) = (m ** encrypt_exp) mod modulus
|
|
617
|
+
|
|
618
|
+
Raises:
|
|
619
|
+
InputError: invalid inputs
|
|
620
|
+
"""
|
|
621
|
+
# test inputs
|
|
622
|
+
if not 0 < message < self.public_modulus:
|
|
623
|
+
raise InputError(f'invalid message: {message=}')
|
|
624
|
+
# encrypt
|
|
625
|
+
return ModExp(message, self.encrypt_exp, self.public_modulus)
|
|
626
|
+
|
|
627
|
+
def VerifySignature(self, message: int, signature: int, /) -> bool:
|
|
628
|
+
"""Verify a signature. True if OK; False if failed verification.
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
message (int): message that was signed by key owner, 1 ≤ m < modulus
|
|
632
|
+
signature (int): signature, 1 ≤ s < modulus
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
True if signature is valid, False otherwise;
|
|
636
|
+
(signature ** encrypt_exp) mod modulus == message
|
|
637
|
+
|
|
638
|
+
Raises:
|
|
639
|
+
InputError: invalid inputs
|
|
640
|
+
"""
|
|
641
|
+
return self.Encrypt(signature) == message
|
|
642
|
+
|
|
643
|
+
@classmethod
|
|
644
|
+
def Copy(cls, other: 'RSAPublicKey', /) -> Self:
|
|
645
|
+
"""Initialize a public key by taking the public parts of a public/private key."""
|
|
646
|
+
return cls(public_modulus=other.public_modulus, encrypt_exp=other.encrypt_exp)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
650
|
+
class RSAObfuscationPair(RSAPublicKey):
|
|
651
|
+
"""RSA (Rivest-Shamir-Adleman) obfuscation pair for a public key.
|
|
652
|
+
|
|
653
|
+
Attributes:
|
|
654
|
+
random_key (int): random value key, 2 ≤ k < modulus
|
|
655
|
+
key_inverse (int): inverse for `random_key` in relation to the RSA public key, 2 ≤ i < modulus
|
|
656
|
+
"""
|
|
657
|
+
|
|
658
|
+
random_key: int
|
|
659
|
+
key_inverse: int
|
|
660
|
+
|
|
661
|
+
def __post_init__(self) -> None:
|
|
662
|
+
"""Check data.
|
|
663
|
+
|
|
664
|
+
Raises:
|
|
665
|
+
InputError: invalid inputs
|
|
666
|
+
CryptoError: modulus math is inconsistent with values
|
|
667
|
+
"""
|
|
668
|
+
super(RSAObfuscationPair, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
|
|
669
|
+
if (not 1 < self.random_key < self.public_modulus or
|
|
670
|
+
not 1 < self.key_inverse < self.public_modulus or
|
|
671
|
+
self.random_key in (self.key_inverse, self.encrypt_exp, self.public_modulus)):
|
|
672
|
+
raise InputError(f'invalid keys: {self}')
|
|
673
|
+
if (self.random_key * self.key_inverse) % self.public_modulus != 1:
|
|
674
|
+
raise CryptoError(f'inconsistent keys: {self}')
|
|
675
|
+
|
|
676
|
+
def ObfuscateMessage(self, message: int, /) -> int:
|
|
677
|
+
"""Convert message to an obfuscated message to be signed by this key's owner.
|
|
678
|
+
|
|
679
|
+
Args:
|
|
680
|
+
message (int): message to obfuscate before signature, 1 ≤ m < modulus
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
obfuscated message (int, 1 ≤ m < modulus) = (m * (random_key ** encrypt_exp)) mod modulus
|
|
684
|
+
|
|
685
|
+
Raises:
|
|
686
|
+
InputError: invalid inputs
|
|
687
|
+
"""
|
|
688
|
+
# test inputs
|
|
689
|
+
if not 0 < message < self.public_modulus:
|
|
690
|
+
raise InputError(f'invalid message: {message=}')
|
|
691
|
+
# encrypt
|
|
692
|
+
return (message * ModExp(
|
|
693
|
+
self.random_key, self.encrypt_exp, self.public_modulus)) % self.public_modulus
|
|
694
|
+
|
|
695
|
+
def RevealOriginalSignature(self, message: int, signature: int, /) -> int:
|
|
696
|
+
"""Recover original signature for `message` from obfuscated `signature`.
|
|
697
|
+
|
|
698
|
+
Args:
|
|
699
|
+
message (int): original message before obfuscation, 1 ≤ m < modulus
|
|
700
|
+
signature (int): signature for obfuscated message (not `message`!), 1 ≤ s < modulus
|
|
701
|
+
|
|
702
|
+
Returns:
|
|
703
|
+
original signature (int, 1 ≤ s < modulus) to `message`;
|
|
704
|
+
signature * key_inverse mod modulus
|
|
705
|
+
|
|
706
|
+
Raises:
|
|
707
|
+
InputError: invalid inputs
|
|
708
|
+
CryptoError: some signatures were invalid (either plain or obfuscated)
|
|
709
|
+
"""
|
|
710
|
+
# verify that obfuscated signature is valid
|
|
711
|
+
obfuscated: int = self.ObfuscateMessage(message)
|
|
712
|
+
if not self.VerifySignature(obfuscated, signature):
|
|
713
|
+
raise CryptoError(f'obfuscated message was not signed: {message=} ; {signature=}')
|
|
714
|
+
# compute signature for original message and check it
|
|
715
|
+
original: int = (signature * self.key_inverse) % self.public_modulus
|
|
716
|
+
if not self.VerifySignature(message, original):
|
|
717
|
+
raise CryptoError(f'failed signature recovery: {message=} ; {signature=}')
|
|
718
|
+
return original
|
|
719
|
+
|
|
720
|
+
@classmethod
|
|
721
|
+
def New(cls, key: RSAPublicKey, /) -> Self:
|
|
722
|
+
"""New obfuscation pair for this `key`, respecting the size of the public modulus.
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
key (RSAPublicKey): public RSA key to use as base for a new RSAObfuscationPair
|
|
726
|
+
|
|
727
|
+
Returns:
|
|
728
|
+
RSAObfuscationPair object ready for use
|
|
729
|
+
"""
|
|
730
|
+
# find a suitable random key based on the bit_length
|
|
731
|
+
random_key: int = 0
|
|
732
|
+
key_inverse: int = 0
|
|
733
|
+
while (not random_key or not key_inverse or
|
|
734
|
+
random_key == key.encrypt_exp or
|
|
735
|
+
random_key == key_inverse or
|
|
736
|
+
key_inverse == key.encrypt_exp):
|
|
737
|
+
random_key = secrets.randbits(key.public_modulus.bit_length() - 1)
|
|
738
|
+
try:
|
|
739
|
+
key_inverse = ModInv(random_key, key.public_modulus)
|
|
740
|
+
except ModularDivideError:
|
|
741
|
+
key_inverse = 0
|
|
742
|
+
# build object
|
|
743
|
+
return cls(
|
|
744
|
+
public_modulus=key.public_modulus, encrypt_exp=key.encrypt_exp,
|
|
745
|
+
random_key=random_key, key_inverse=key_inverse)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
749
|
+
class RSAPrivateKey(RSAPublicKey):
|
|
750
|
+
"""RSA (Rivest-Shamir-Adleman) private key.
|
|
751
|
+
|
|
752
|
+
Attributes:
|
|
753
|
+
modulus_p (int): prime number p, ≥ 2
|
|
754
|
+
modulus_q (int): prime number q, ≥ 3 and > p
|
|
755
|
+
decrypt_exp (int): decryption exponent, 2 ≤ d < modulus, (encrypt * d) % ((p-1) * (q-1)) == 1
|
|
756
|
+
"""
|
|
757
|
+
|
|
758
|
+
modulus_p: int
|
|
759
|
+
modulus_q: int
|
|
760
|
+
decrypt_exp: int
|
|
761
|
+
|
|
762
|
+
def __post_init__(self) -> None:
|
|
763
|
+
"""Check data.
|
|
764
|
+
|
|
765
|
+
Raises:
|
|
766
|
+
InputError: invalid inputs
|
|
767
|
+
CryptoError: modulus math is inconsistent with values
|
|
768
|
+
"""
|
|
769
|
+
super(RSAPrivateKey, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
|
|
770
|
+
phi: int = (self.modulus_p - 1) * (self.modulus_q - 1)
|
|
771
|
+
min_prime_distance: int = 2 ** (self.public_modulus.bit_length() // 3 + 1)
|
|
772
|
+
if (self.modulus_p < 2 or not IsPrime(self.modulus_p) or # pylint: disable=too-many-boolean-expressions
|
|
773
|
+
self.modulus_q < 3 or not IsPrime(self.modulus_q) or
|
|
774
|
+
self.modulus_q <= self.modulus_p or
|
|
775
|
+
(self.modulus_q - self.modulus_p) < min_prime_distance or
|
|
776
|
+
self.encrypt_exp in (self.modulus_p, self.modulus_q) or
|
|
777
|
+
self.encrypt_exp >= phi or
|
|
778
|
+
self.decrypt_exp in (self.encrypt_exp, self.modulus_p, self.modulus_q, phi)):
|
|
779
|
+
# encrypt_exp has to be less than phi;
|
|
780
|
+
# if p − q < 2*(n**(1/4)) then solving for p and q is trivial
|
|
781
|
+
raise InputError(f'invalid modulus_p or modulus_q: {self}')
|
|
782
|
+
min_decrypt_length: int = self.public_modulus.bit_length() // 2 + 1
|
|
783
|
+
if not (2 ** min_decrypt_length) < self.decrypt_exp < self.public_modulus:
|
|
784
|
+
# if decrypt_exp < public_modulus**(1/4)/3, then decrypt_exp can be computed efficiently
|
|
785
|
+
# from public_modulus and encrypt_exp so we make sure it is larger than public_modulus**(1/2)
|
|
786
|
+
raise InputError(f'invalid decrypt_exp: {self}')
|
|
787
|
+
if self.modulus_p * self.modulus_q != self.public_modulus:
|
|
788
|
+
raise CryptoError(f'inconsistent modulus_p * modulus_q: {self}')
|
|
789
|
+
if (self.encrypt_exp * self.decrypt_exp) % phi != 1:
|
|
790
|
+
raise CryptoError(f'inconsistent exponents: {self}')
|
|
791
|
+
|
|
792
|
+
def Decrypt(self, message: int, /) -> int:
|
|
793
|
+
"""Decrypt `message` with this private key.
|
|
794
|
+
|
|
795
|
+
Args:
|
|
796
|
+
message (int): message to encrypt, 1 ≤ m < modulus
|
|
797
|
+
|
|
798
|
+
Returns:
|
|
799
|
+
decrypted message (int, 1 ≤ m < modulus) = (m ** decrypt_exp) mod modulus
|
|
800
|
+
|
|
801
|
+
Raises:
|
|
802
|
+
InputError: invalid inputs
|
|
803
|
+
"""
|
|
804
|
+
# test inputs
|
|
805
|
+
if not 0 < message < self.public_modulus:
|
|
806
|
+
raise InputError(f'invalid message: {message=}')
|
|
807
|
+
# decrypt
|
|
808
|
+
return ModExp(message, self.decrypt_exp, self.public_modulus)
|
|
809
|
+
|
|
810
|
+
def Sign(self, message: int, /) -> int:
|
|
811
|
+
"""Sign `message` with this private key.
|
|
812
|
+
|
|
813
|
+
Args:
|
|
814
|
+
message (int): message to sign, 1 ≤ m < modulus
|
|
815
|
+
|
|
816
|
+
Returns:
|
|
817
|
+
signed message (int, 1 ≤ m < modulus) = (m ** decrypt_exp) mod modulus;
|
|
818
|
+
identical to Decrypt()
|
|
819
|
+
|
|
820
|
+
Raises:
|
|
821
|
+
InputError: invalid inputs
|
|
822
|
+
"""
|
|
823
|
+
return self.Decrypt(message)
|
|
824
|
+
|
|
825
|
+
@classmethod
|
|
826
|
+
def New(cls, bit_length: int, /) -> Self:
|
|
827
|
+
"""Make a new private key of `bit_length` bits (primes p & q will be half this length).
|
|
828
|
+
|
|
829
|
+
Args:
|
|
830
|
+
bit_length (int): number of bits in the modulus, ≥ 11; primes p & q will be half this length
|
|
831
|
+
|
|
832
|
+
Returns:
|
|
833
|
+
RSAPrivateKey object ready for use
|
|
834
|
+
|
|
835
|
+
Raises:
|
|
836
|
+
InputError: invalid inputs
|
|
837
|
+
"""
|
|
838
|
+
# test inputs
|
|
839
|
+
if bit_length < 11:
|
|
840
|
+
raise InputError(f'invalid bit length: {bit_length=}')
|
|
841
|
+
# generate primes / modulus
|
|
842
|
+
failures: int = 0
|
|
843
|
+
while True:
|
|
844
|
+
try:
|
|
845
|
+
primes: list[int] = [NBitRandomPrime(bit_length // 2), NBitRandomPrime(bit_length // 2)]
|
|
846
|
+
modulus: int = primes[0] * primes[1]
|
|
847
|
+
while modulus.bit_length() != bit_length or primes[0] == primes[1]:
|
|
848
|
+
primes.remove(min(primes))
|
|
849
|
+
primes.append(NBitRandomPrime(
|
|
850
|
+
bit_length // 2 + (bit_length % 2 if modulus.bit_length() < bit_length else 0)))
|
|
851
|
+
modulus = primes[0] * primes[1]
|
|
852
|
+
# build object
|
|
853
|
+
phi: int = (primes[0] - 1) * (primes[1] - 1)
|
|
854
|
+
prime_exp: int = (_SMALL_ENCRYPTION_EXPONENT if phi <= _BIG_ENCRYPTION_EXPONENT else
|
|
855
|
+
_BIG_ENCRYPTION_EXPONENT)
|
|
856
|
+
obj: Self = cls(
|
|
857
|
+
modulus_p=min(primes), # "p" is always the smaller
|
|
858
|
+
modulus_q=max(primes), # "q" is always the larger
|
|
859
|
+
public_modulus=modulus,
|
|
860
|
+
encrypt_exp=prime_exp,
|
|
861
|
+
decrypt_exp=ModInv(prime_exp, phi),
|
|
862
|
+
)
|
|
863
|
+
return obj
|
|
864
|
+
except (InputError, ModularDivideError) as err:
|
|
865
|
+
failures += 1
|
|
866
|
+
if failures >= _MAX_KEY_GENERATION_FAILURES:
|
|
867
|
+
raise CryptoError(f'failed key generation {failures} times') from err
|
|
868
|
+
logging.warning(err)
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
872
|
+
class ShamirSharedSecretPublic(CryptoKey):
|
|
873
|
+
"""Shamir Shared Secret (SSS) public part (<https://en.wikipedia.org/wiki/Shamir's_secret_sharing>).
|
|
874
|
+
|
|
875
|
+
Attributes:
|
|
876
|
+
minimum (int): minimum shares needed for recovery, ≥ 2
|
|
877
|
+
modulus (int): prime modulus used for share generation, prime, ≥ 2
|
|
878
|
+
"""
|
|
879
|
+
|
|
880
|
+
minimum: int
|
|
881
|
+
modulus: int
|
|
882
|
+
|
|
883
|
+
def __post_init__(self) -> None:
|
|
884
|
+
"""Check data.
|
|
885
|
+
|
|
886
|
+
Raises:
|
|
887
|
+
InputError: invalid inputs
|
|
888
|
+
"""
|
|
889
|
+
super(ShamirSharedSecretPublic, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
|
|
890
|
+
if (self.modulus < 2 or
|
|
891
|
+
not IsPrime(self.modulus) or
|
|
892
|
+
self.minimum < 2):
|
|
893
|
+
raise InputError(f'invalid modulus or minimum: {self}')
|
|
894
|
+
|
|
895
|
+
def RecoverSecret(
|
|
896
|
+
self, shares: Collection['ShamirSharePrivate'], /, *, force_recover: bool = False) -> int:
|
|
897
|
+
"""Recover the secret from ShamirSharePrivate objects.
|
|
898
|
+
|
|
899
|
+
Raises:
|
|
900
|
+
InputError: invalid inputs
|
|
901
|
+
CryptoError: secret cannot be recovered
|
|
902
|
+
"""
|
|
903
|
+
# check that we have enough shares
|
|
904
|
+
share_points: dict[int, int] = {s.share_key: s.share_value for s in shares} # de-dup guaranteed
|
|
905
|
+
if (given_shares := len(share_points)) < self.minimum:
|
|
906
|
+
mess: str = f'distinct shares {given_shares} < minimum shares {self.minimum}'
|
|
907
|
+
if force_recover and given_shares > 1:
|
|
908
|
+
logging.error('recovering secret even though: %s', mess)
|
|
909
|
+
else:
|
|
910
|
+
raise CryptoError(f'unrecoverable secret: {mess}')
|
|
911
|
+
# do the math
|
|
912
|
+
return ModLagrangeInterpolate(0, share_points, self.modulus)
|
|
913
|
+
|
|
914
|
+
@classmethod
|
|
915
|
+
def Copy(cls, other: 'ShamirSharedSecretPublic', /) -> Self:
|
|
916
|
+
"""Initialize a public key by taking the public parts of a public/private key."""
|
|
917
|
+
return cls(minimum=other.minimum, modulus=other.modulus)
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
921
|
+
class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
|
|
922
|
+
"""Shamir Shared Secret (SSS) private keys (<https://en.wikipedia.org/wiki/Shamir's_secret_sharing>).
|
|
923
|
+
|
|
924
|
+
Attributes:
|
|
925
|
+
polynomial (list[int]): prime coefficients for generation poly., each modulus.bit_length() size
|
|
926
|
+
"""
|
|
927
|
+
|
|
928
|
+
polynomial: list[int]
|
|
929
|
+
|
|
930
|
+
def __post_init__(self) -> None:
|
|
931
|
+
"""Check data.
|
|
932
|
+
|
|
933
|
+
Raises:
|
|
934
|
+
InputError: invalid inputs
|
|
935
|
+
"""
|
|
936
|
+
super(ShamirSharedSecretPrivate, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
|
|
937
|
+
if (len(self.polynomial) != self.minimum - 1 or # exactly this size
|
|
938
|
+
len(set(self.polynomial)) != self.minimum - 1 or # no duplicate
|
|
939
|
+
self.modulus in self.polynomial or # different from modulus
|
|
940
|
+
any(not IsPrime(p) or p.bit_length() != self.modulus.bit_length()
|
|
941
|
+
for p in self.polynomial)): # all primes and the right size
|
|
942
|
+
raise InputError(f'invalid polynomial: {self}')
|
|
943
|
+
|
|
944
|
+
def Share(self, secret: int, /, *, share_key: int = 0) -> 'ShamirSharePrivate':
|
|
945
|
+
"""Make a new ShamirSharePrivate for the `secret`.
|
|
946
|
+
|
|
947
|
+
Args:
|
|
948
|
+
secret (int): secret message to encrypt and share, 0 ≤ s < modulus
|
|
949
|
+
share_key (int, optional): if given, a random value to use, 1 ≤ r < modulus;
|
|
950
|
+
else will generate randomly
|
|
951
|
+
|
|
952
|
+
Returns:
|
|
953
|
+
ShamirSharePrivate object
|
|
954
|
+
|
|
955
|
+
Raises:
|
|
956
|
+
InputError: invalid inputs
|
|
957
|
+
"""
|
|
958
|
+
# test inputs
|
|
959
|
+
if not 0 <= secret < self.modulus:
|
|
960
|
+
raise InputError(f'invalid secret: {secret=}')
|
|
961
|
+
if not 1 < share_key < self.modulus:
|
|
962
|
+
if not share_key: # default is zero, and that means we generate it here
|
|
963
|
+
sr = secrets.SystemRandom()
|
|
964
|
+
share_key = 0
|
|
965
|
+
while not share_key or share_key in self.polynomial:
|
|
966
|
+
share_key = sr.randint(2, self.modulus - 1)
|
|
967
|
+
else:
|
|
968
|
+
raise InputError(f'invalid share_key: {secret=}')
|
|
969
|
+
# build object
|
|
970
|
+
return ShamirSharePrivate(
|
|
971
|
+
minimum=self.minimum, modulus=self.modulus,
|
|
972
|
+
share_key=share_key,
|
|
973
|
+
share_value=ModPolynomial(share_key, [secret] + self.polynomial, self.modulus))
|
|
974
|
+
|
|
975
|
+
def Shares(
|
|
976
|
+
self, secret: int, /, *, max_shares: int = 0) -> Generator['ShamirSharePrivate', None, None]:
|
|
977
|
+
"""Make any number of ShamirSharePrivate for the `secret`.
|
|
978
|
+
|
|
979
|
+
Args:
|
|
980
|
+
secret (int): secret message to encrypt and share, 0 ≤ s < modulus
|
|
981
|
+
max_shares (int, optional): if given, number (≥ 2) of shares to generate; else infinite
|
|
982
|
+
|
|
983
|
+
Yields:
|
|
984
|
+
ShamirSharePrivate object
|
|
985
|
+
|
|
986
|
+
Raises:
|
|
987
|
+
InputError: invalid inputs
|
|
988
|
+
"""
|
|
989
|
+
# test inputs
|
|
990
|
+
if max_shares and max_shares < self.minimum:
|
|
991
|
+
raise InputError(f'invalid max_shares: {max_shares=} < {self.minimum=}')
|
|
992
|
+
# generate shares
|
|
993
|
+
sr = secrets.SystemRandom()
|
|
994
|
+
count: int = 0
|
|
995
|
+
used_keys: set[int] = set()
|
|
996
|
+
while not max_shares or count < max_shares:
|
|
997
|
+
share_key: int = 0
|
|
998
|
+
while not share_key or share_key in self.polynomial or share_key in used_keys:
|
|
999
|
+
share_key = sr.randint(2, self.modulus - 1)
|
|
1000
|
+
try:
|
|
1001
|
+
yield self.Share(secret, share_key=share_key)
|
|
1002
|
+
used_keys.add(share_key)
|
|
1003
|
+
count += 1
|
|
1004
|
+
except InputError as err:
|
|
1005
|
+
# it could happen, for example, that the share_key will generate a value of 0
|
|
1006
|
+
logging.warning(err)
|
|
1007
|
+
|
|
1008
|
+
def VerifyShare(self, secret: int, share: 'ShamirSharePrivate', /) -> bool:
|
|
1009
|
+
"""Make a new ShamirSharePrivate for the `secret`.
|
|
1010
|
+
|
|
1011
|
+
Args:
|
|
1012
|
+
secret (int): secret message to encrypt and share, 0 ≤ s < modulus
|
|
1013
|
+
share (ShamirSharePrivate): share to verify
|
|
1014
|
+
|
|
1015
|
+
Returns:
|
|
1016
|
+
True if share is valid; False otherwise
|
|
1017
|
+
|
|
1018
|
+
Raises:
|
|
1019
|
+
InputError: invalid inputs
|
|
1020
|
+
"""
|
|
1021
|
+
return share == self.Share(secret, share_key=share.share_key)
|
|
1022
|
+
|
|
1023
|
+
@classmethod
|
|
1024
|
+
def New(cls, minimum_shares: int, bit_length: int, /) -> Self:
|
|
1025
|
+
"""Make a new public sharing prime modulus of `bit_length` bits.
|
|
1026
|
+
|
|
1027
|
+
Args:
|
|
1028
|
+
minimum_shares (int): minimum shares needed for recovery, ≥ 2
|
|
1029
|
+
bit_length (int): number of bits in the primes, ≥ 10
|
|
1030
|
+
|
|
1031
|
+
Returns:
|
|
1032
|
+
ShamirSharedSecretPrivate object ready for use
|
|
1033
|
+
|
|
1034
|
+
Raises:
|
|
1035
|
+
InputError: invalid inputs
|
|
1036
|
+
"""
|
|
1037
|
+
# test inputs
|
|
1038
|
+
if minimum_shares < 2:
|
|
1039
|
+
raise InputError(f'at least 2 shares are needed: {minimum_shares=}')
|
|
1040
|
+
if bit_length < 10:
|
|
1041
|
+
raise InputError(f'invalid bit length: {bit_length=}')
|
|
1042
|
+
# make the primes
|
|
1043
|
+
unique_primes: set[int] = set()
|
|
1044
|
+
while len(unique_primes) < minimum_shares:
|
|
1045
|
+
unique_primes.add(NBitRandomPrime(bit_length))
|
|
1046
|
+
# get the largest prime for the modulus
|
|
1047
|
+
ordered_primes: list[int] = list(unique_primes)
|
|
1048
|
+
modulus: int = max(ordered_primes)
|
|
1049
|
+
ordered_primes.remove(modulus)
|
|
1050
|
+
# make polynomial be a random order
|
|
1051
|
+
secrets.SystemRandom().shuffle(ordered_primes)
|
|
1052
|
+
# build object
|
|
1053
|
+
return cls(minimum=minimum_shares, modulus=modulus, polynomial=ordered_primes)
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
1057
|
+
class ShamirSharePrivate(ShamirSharedSecretPublic):
|
|
1058
|
+
"""Shamir Shared Secret (SSS) one share (<https://en.wikipedia.org/wiki/Shamir's_secret_sharing>).
|
|
1059
|
+
|
|
1060
|
+
Attributes:
|
|
1061
|
+
share_key (int): share secret key; a randomly picked value, 1 ≤ k < modulus
|
|
1062
|
+
share_value (int): share secret value, 1 ≤ v < modulus; (k, v) is a "point" of f(k)=v
|
|
1063
|
+
"""
|
|
1064
|
+
|
|
1065
|
+
share_key: int
|
|
1066
|
+
share_value: int
|
|
1067
|
+
|
|
1068
|
+
def __post_init__(self) -> None:
|
|
1069
|
+
"""Check data.
|
|
1070
|
+
|
|
1071
|
+
Raises:
|
|
1072
|
+
InputError: invalid inputs
|
|
1073
|
+
"""
|
|
1074
|
+
super(ShamirSharePrivate, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
|
|
1075
|
+
if (not 0 < self.share_key < self.modulus or
|
|
1076
|
+
not 0 < self.share_value < self.modulus):
|
|
1077
|
+
raise InputError(f'invalid share: {self}')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: transcrypto
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
4
4
|
Summary: Basic crypto primitives, not intended for actual use, but as a companion to --Criptografia, Métodos e Algoritmos--
|
|
5
5
|
Author-email: Daniel Balparda <balparda@github.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
transcrypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
transcrypto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
transcrypto/transcrypto.py,sha256=NdrvZkPIx6ENWucRfxe2U1TD0eBaf6AJvBIUOAQOU_w,37474
|
|
4
|
+
transcrypto-1.0.3.dist-info/licenses/LICENSE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
|
|
5
|
+
transcrypto-1.0.3.dist-info/METADATA,sha256=ajxCPdDXQz3nP4zHg5ZYw6uePsdzhF_zmy0gSGgmC_4,4372
|
|
6
|
+
transcrypto-1.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
transcrypto-1.0.3.dist-info/top_level.txt,sha256=9IfB0nGtVzQbYok5QIYNOy3coDv2UKX2OZtlFyxFDDQ,12
|
|
8
|
+
transcrypto-1.0.3.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
transcrypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
transcrypto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
transcrypto/transcrypto.py,sha256=CGQ9TfLWVS_1pLMiG4h32Mufp7Ucn4JYgwA-PvCO5Ic,10375
|
|
4
|
-
transcrypto-1.0.2.dist-info/licenses/LICENSE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
|
|
5
|
-
transcrypto-1.0.2.dist-info/METADATA,sha256=oVMr1GxU7lcCMeYVvSf1BN2oA3-MFiuewVJ3DQzH7H4,4372
|
|
6
|
-
transcrypto-1.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
-
transcrypto-1.0.2.dist-info/top_level.txt,sha256=9IfB0nGtVzQbYok5QIYNOy3coDv2UKX2OZtlFyxFDDQ,12
|
|
8
|
-
transcrypto-1.0.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|