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.
@@ -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 random
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, 2) # v1.0.2, 2025-07-22
16
+ __version__: tuple[int, int, int] = (1, 0, 3) # v1.0.3, 2025-07-30
14
17
 
15
18
 
16
- FIRST_60_PRIMES_SORTED: list[int] = [
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
- FIRST_60_PRIMES: set[int] = set(FIRST_60_PRIMES_SORTED)
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] # 281
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
- Uses the Euclid method.
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 Error(f'negative input: {a=} , {b=}')
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
- Uses the Euclid method.
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 Error(f'negative input: {a=} , {b=}')
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 Error(f'negative input: {x=} , {y=}')
196
+ raise InputError(f'negative input: {x=} , {y=}')
87
197
  if m < 1:
88
- raise Error(f'invalid module: {m=}')
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 Error(f'invalid number: {n=}')
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 Error(f'out of bounds safety: 1 <= {safety=} <= {max_safety}')
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(random.randint(2, n - 2))
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 Error(f'out of bounds witness: 2 <= {w=} <= {n - 2}')
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 Error(f'invalid number: {n=}')
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 # "should never happen"
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 # "should never happen"
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 Error(f'invalid odd number: {n=}')
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 # "should never happen"
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 Error(f'invalid number: {n=}')
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 Error(f'out of bounds witness: 2 <= {w=} <= {n - 2}')
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 PrimeGenerator(start: int) -> Generator[int, None, None]:
255
- """Generates all primes from `start` until loop is broken. Tuned for huge numbers."""
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 Error(f'invalid number: {start=}')
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
- # is number divisible by (one of the) first 60 primes? test should eliminate 80%+ of candidates
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 MersennePrimesGenerator(start: int) -> Generator[tuple[int, int, int], None, None]:
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
- # is number divisible by (one of the) first 60 primes? test should eliminate 80%+ of candidates
289
- if mersenne > PRIME_60 and GCD(mersenne, COMPOSITE_60) != 1:
290
- continue # not prime
291
- # do the (more expensive) primality test
292
- if MillerRabinIsPrime(mersenne):
293
- # found a prime, yield it plus the perfect number associated with it
294
- yield (n, mersenne, (2 ** (n - 1)) * mersenne)
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.2
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,,