transcrypto 1.2.0__py3-none-any.whl → 1.3.0__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/base.py +1 -1
- transcrypto/dsa.py +91 -48
- transcrypto/elgamal.py +14 -13
- transcrypto/modmath.py +487 -40
- transcrypto/rsa.py +17 -17
- transcrypto/sss.py +1 -3
- transcrypto/transcrypto.py +42 -27
- {transcrypto-1.2.0.dist-info → transcrypto-1.3.0.dist-info}/METADATA +72 -25
- transcrypto-1.3.0.dist-info/RECORD +15 -0
- transcrypto-1.2.0.dist-info/RECORD +0 -15
- {transcrypto-1.2.0.dist-info → transcrypto-1.3.0.dist-info}/WHEEL +0 -0
- {transcrypto-1.2.0.dist-info → transcrypto-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {transcrypto-1.2.0.dist-info → transcrypto-1.3.0.dist-info}/top_level.txt +0 -0
transcrypto/base.py
CHANGED
|
@@ -23,7 +23,7 @@ from typing import Any, Callable, final, MutableSequence, Protocol, runtime_chec
|
|
|
23
23
|
import zstandard
|
|
24
24
|
|
|
25
25
|
__author__ = 'balparda@github.com'
|
|
26
|
-
__version__ = '1.
|
|
26
|
+
__version__ = '1.3.0' # 2025-09-07, Sun
|
|
27
27
|
__version_tuple__: tuple[int, ...] = tuple(int(v) for v in __version__.split('.'))
|
|
28
28
|
|
|
29
29
|
# MIN_TM = int( # minimum allowed timestamp
|
transcrypto/dsa.py
CHANGED
|
@@ -12,11 +12,16 @@ In the future we will design a proper DSA+Hash implementation.
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
+
import concurrent.futures
|
|
15
16
|
import dataclasses
|
|
16
17
|
import logging
|
|
18
|
+
import multiprocessing
|
|
19
|
+
import os
|
|
17
20
|
# import pdb
|
|
18
21
|
from typing import Self
|
|
19
22
|
|
|
23
|
+
import gmpy2 # type:ignore
|
|
24
|
+
|
|
20
25
|
from . import base, modmath
|
|
21
26
|
|
|
22
27
|
__author__ = 'balparda@github.com'
|
|
@@ -24,14 +29,14 @@ __version__: str = base.__version__ # version comes from base!
|
|
|
24
29
|
__version_tuple__: tuple[int, ...] = base.__version_tuple__
|
|
25
30
|
|
|
26
31
|
|
|
27
|
-
_PRIME_MULTIPLE_SEARCH = 4096 # how many multiples of q to try before restarting
|
|
28
32
|
_MAX_KEY_GENERATION_FAILURES = 15
|
|
29
33
|
|
|
30
34
|
# fixed prefixes: do NOT ever change! will break all encryption and signature schemes
|
|
31
35
|
_DSA_SIGNATURE_HASH_PREFIX = b'transcrypto.DSA.Signature.1.0\x00'
|
|
32
36
|
|
|
33
37
|
|
|
34
|
-
def NBitRandomDSAPrimes(
|
|
38
|
+
def NBitRandomDSAPrimes(
|
|
39
|
+
p_bits: int, q_bits: int, /, *, serial: bool = True) -> tuple[int, int, int]:
|
|
35
40
|
"""Generates 2 random DSA primes p & q with `x_bits` size and (p-1)%q==0.
|
|
36
41
|
|
|
37
42
|
Uses an aggressive small-prime wheel sieve:
|
|
@@ -41,10 +46,15 @@ def NBitRandomDSAPrimes(p_bits: int, q_bits: int, /) -> tuple[int, int, int]:
|
|
|
41
46
|
m_forbidden ≡ -q⁻¹ (mod r) (because (m·q + 1) % r == 0 ⇔ m ≡ -q⁻¹ (mod r))
|
|
42
47
|
• When we iterate m, we skip values that hit any forbidden residue class.
|
|
43
48
|
|
|
49
|
+
Method will decide if executes on one thread or many.
|
|
50
|
+
|
|
44
51
|
Args:
|
|
45
52
|
p_bits (int): Number of guaranteed bits in `p` prime representation,
|
|
46
53
|
p_bits ≥ q_bits + 11
|
|
47
54
|
q_bits (int): Number of guaranteed bits in `q` prime representation, ≥ 11
|
|
55
|
+
serial (bool, optional): True (default) will force one thread; False will allow parallelism;
|
|
56
|
+
we have temporarily disabled parallelism with a default of True because it is not making
|
|
57
|
+
things faster...
|
|
48
58
|
|
|
49
59
|
Returns:
|
|
50
60
|
random primes tuple (p, q, m), with p-1 a random multiple m of q, such
|
|
@@ -59,14 +69,59 @@ def NBitRandomDSAPrimes(p_bits: int, q_bits: int, /) -> tuple[int, int, int]:
|
|
|
59
69
|
if p_bits < q_bits + 11:
|
|
60
70
|
raise base.InputError(f'invalid p_bits length: {p_bits=}')
|
|
61
71
|
# make q
|
|
62
|
-
q: int = modmath.
|
|
72
|
+
q: int = modmath.NBitRandomPrimes(q_bits).pop()
|
|
73
|
+
# get number of CPUs and decide if we do parallel or not
|
|
74
|
+
n_workers: int = min(4, os.cpu_count() or 1)
|
|
75
|
+
pr: int | None = None
|
|
76
|
+
m: int | None = None
|
|
77
|
+
if serial or n_workers <= 1 or p_bits < 200:
|
|
78
|
+
# do one worker
|
|
79
|
+
while pr is None or m is None or pr.bit_length() != p_bits:
|
|
80
|
+
pr, m = _PrimePSearchShard(q, p_bits)
|
|
81
|
+
return (pr, q, m)
|
|
82
|
+
# parallel: keep a small pool of bounded shards; stop on first hit
|
|
83
|
+
multiprocessing.set_start_method('fork', force=True)
|
|
84
|
+
with concurrent.futures.ProcessPoolExecutor(max_workers=n_workers) as pool:
|
|
85
|
+
workers: set[concurrent.futures.Future[tuple[int | None, int | None]]] = {
|
|
86
|
+
pool.submit(_PrimePSearchShard, q, p_bits) for _ in range(n_workers)}
|
|
87
|
+
while workers:
|
|
88
|
+
done: set[concurrent.futures.Future[tuple[int | None, int | None]]] = concurrent.futures.wait(
|
|
89
|
+
workers, return_when=concurrent.futures.FIRST_COMPLETED)[0]
|
|
90
|
+
for worker in done:
|
|
91
|
+
workers.remove(worker)
|
|
92
|
+
pr, m = worker.result()
|
|
93
|
+
if pr is not None and m is not None and pr.bit_length() == p_bits:
|
|
94
|
+
return (pr, q, m)
|
|
95
|
+
# no hit in that shard: keep the pool full with a fresh shard
|
|
96
|
+
workers.add(pool.submit(_PrimePSearchShard, q, p_bits)) # pragma: no cover
|
|
97
|
+
# can never reach this point, but leave this here; remove line from coverage
|
|
98
|
+
raise base.Error(f'could not find prime with {p_bits=}/{q_bits=} bits') # pragma: no cover
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _PrimePSearchShard(q: int, p_bits: int) -> tuple[int | None, int | None]:
|
|
102
|
+
"""Search for a `p_bits` random prime, starting from a random point, for ~6× expected prime gap.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
q (int): Prime `q` for DSA
|
|
106
|
+
p_bits (int): Number of guaranteed bits in prime `p` representation
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
tuple[int | None, int | None]: either the prime `p` and multiple `m` or None if no prime found
|
|
110
|
+
"""
|
|
111
|
+
q_bits: int = q.bit_length()
|
|
112
|
+
shard_len: int = max(2000, 6 * int(0.693 * p_bits)) # ~6× expected prime gap ~2^k (≈ 0.693*k)
|
|
113
|
+
# find range of multiples to use
|
|
114
|
+
min_p: int = 2 ** (p_bits - 1)
|
|
115
|
+
max_p: int = 2 ** p_bits - 1
|
|
116
|
+
min_m: int = min_p // q + 2
|
|
117
|
+
max_m: int = max_p // q - 2
|
|
118
|
+
assert max_m - min_m > 1000 # make sure we'll have options!
|
|
63
119
|
# make list of small primes to use for sieving
|
|
64
120
|
approx_q_root: int = 1 << (q_bits // 2)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
break
|
|
121
|
+
pr: int
|
|
122
|
+
forbidden: dict[int, int] = { # (modulus: forbidden residue)
|
|
123
|
+
pr: ((-modmath.ModInv(q % pr, pr)) % pr)
|
|
124
|
+
for pr in modmath.FIRST_5K_PRIMES_SORTED[1:min(5000, approx_q_root)]} # skip pr==2
|
|
70
125
|
|
|
71
126
|
def _PassesSieve(m: int) -> bool:
|
|
72
127
|
for r, f in forbidden.items():
|
|
@@ -74,35 +129,23 @@ def NBitRandomDSAPrimes(p_bits: int, q_bits: int, /) -> tuple[int, int, int]:
|
|
|
74
129
|
return False
|
|
75
130
|
return True
|
|
76
131
|
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if not _PassesSieve(m):
|
|
95
|
-
m += 2
|
|
96
|
-
continue
|
|
97
|
-
# passed sieve, do full test
|
|
98
|
-
if modmath.IsPrime(p):
|
|
99
|
-
return (p, q, m) # found a suitable prime set!
|
|
100
|
-
m += 2 # next multiple
|
|
101
|
-
# after _PRIME_MULTIPLE_SEARCH we declare this range failed
|
|
102
|
-
failures += 1
|
|
103
|
-
if failures >= _MAX_KEY_GENERATION_FAILURES:
|
|
104
|
-
raise base.CryptoError(f'failed primes generation {failures} times')
|
|
105
|
-
logging.warning(f'failed primes search: {failures}')
|
|
132
|
+
# try searching starting here
|
|
133
|
+
m: int = base.RandInt(min_m, max_m)
|
|
134
|
+
if m % 2:
|
|
135
|
+
m += 1 # make even
|
|
136
|
+
count: int = 0
|
|
137
|
+
pr = 0
|
|
138
|
+
while count < shard_len:
|
|
139
|
+
pr = q * m + 1
|
|
140
|
+
if pr > max_p:
|
|
141
|
+
break
|
|
142
|
+
# first do a quick sieve test
|
|
143
|
+
if _PassesSieve(m):
|
|
144
|
+
if modmath.IsPrime(pr): # passed sieve, do full test
|
|
145
|
+
return (pr, m) # found a suitable prime set!
|
|
146
|
+
count += 1
|
|
147
|
+
m += 2 # next even number
|
|
148
|
+
return (None, None)
|
|
106
149
|
|
|
107
150
|
|
|
108
151
|
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
@@ -203,7 +246,7 @@ class DSASharedPublicKey(base.CryptoKey):
|
|
|
203
246
|
g: int = 0
|
|
204
247
|
while g < 2:
|
|
205
248
|
h: int = base.RandBits(p_bits - 1)
|
|
206
|
-
g =
|
|
249
|
+
g = int(gmpy2.powmod(h, m, p)) # type:ignore # pylint:disable=no-member
|
|
207
250
|
return cls(prime_modulus=p, prime_seed=q, group_base=g)
|
|
208
251
|
|
|
209
252
|
|
|
@@ -278,10 +321,10 @@ class DSAPublicKey(DSASharedPublicKey, base.Verifier):
|
|
|
278
321
|
raise base.InputError(f'invalid signature: {signature=}')
|
|
279
322
|
# verify
|
|
280
323
|
inv: int = modmath.ModInv(signature[1], self.prime_seed)
|
|
281
|
-
a: int =
|
|
282
|
-
self.group_base, (message * inv) % self.prime_seed, self.prime_modulus)
|
|
283
|
-
b: int =
|
|
284
|
-
self.individual_base, (signature[0] * inv) % self.prime_seed, self.prime_modulus)
|
|
324
|
+
a: int = int(gmpy2.powmod( # type:ignore # pylint:disable=no-member
|
|
325
|
+
self.group_base, (message * inv) % self.prime_seed, self.prime_modulus))
|
|
326
|
+
b: int = int(gmpy2.powmod( # type:ignore # pylint:disable=no-member
|
|
327
|
+
self.individual_base, (signature[0] * inv) % self.prime_seed, self.prime_modulus))
|
|
285
328
|
return ((a * b) % self.prime_modulus) % self.prime_seed == signature[0]
|
|
286
329
|
|
|
287
330
|
def Verify(
|
|
@@ -353,8 +396,7 @@ class DSAPrivateKey(DSAPublicKey, base.Signer): # pylint: disable=too-many-ance
|
|
|
353
396
|
if (not 2 < self.decrypt_exp < self.prime_seed or
|
|
354
397
|
self.decrypt_exp in (self.group_base, self.individual_base)):
|
|
355
398
|
raise base.InputError(f'invalid decrypt_exp: {self}')
|
|
356
|
-
if
|
|
357
|
-
self.group_base, self.decrypt_exp, self.prime_modulus) != self.individual_base:
|
|
399
|
+
if gmpy2.powmod(self.group_base, self.decrypt_exp, self.prime_modulus) != self.individual_base: # type:ignore # pylint:disable=no-member
|
|
358
400
|
raise base.CryptoError(f'inconsistent g**d % p == i: {self}')
|
|
359
401
|
|
|
360
402
|
def __str__(self) -> str:
|
|
@@ -387,10 +429,11 @@ class DSAPrivateKey(DSAPublicKey, base.Signer): # pylint: disable=too-many-ance
|
|
|
387
429
|
if not 0 < message < self.prime_seed:
|
|
388
430
|
raise base.InputError(f'invalid message: {message=}')
|
|
389
431
|
# sign
|
|
390
|
-
a
|
|
432
|
+
a: int = 0
|
|
433
|
+
b: int = 0
|
|
391
434
|
while a < 2 or b < 2:
|
|
392
435
|
ephemeral_key, ephemeral_inv = self._MakeEphemeralKey()
|
|
393
|
-
a =
|
|
436
|
+
a = int(gmpy2.powmod(self.group_base, ephemeral_key, self.prime_modulus) % self.prime_seed) # type:ignore # pylint:disable=no-member
|
|
394
437
|
b = (ephemeral_inv * ((message + a * self.decrypt_exp) % self.prime_seed)) % self.prime_seed
|
|
395
438
|
return (a, b)
|
|
396
439
|
|
|
@@ -460,8 +503,8 @@ class DSAPrivateKey(DSAPublicKey, base.Signer): # pylint: disable=too-many-ance
|
|
|
460
503
|
prime_modulus=shared_key.prime_modulus,
|
|
461
504
|
prime_seed=shared_key.prime_seed,
|
|
462
505
|
group_base=shared_key.group_base,
|
|
463
|
-
individual_base=
|
|
464
|
-
shared_key.group_base, decrypt_exp, shared_key.prime_modulus),
|
|
506
|
+
individual_base=int(gmpy2.powmod( # type:ignore # pylint:disable=no-member
|
|
507
|
+
shared_key.group_base, decrypt_exp, shared_key.prime_modulus)),
|
|
465
508
|
decrypt_exp=decrypt_exp)
|
|
466
509
|
except base.InputError as err:
|
|
467
510
|
failures += 1
|
transcrypto/elgamal.py
CHANGED
|
@@ -23,6 +23,8 @@ import logging
|
|
|
23
23
|
# import pdb
|
|
24
24
|
from typing import Self
|
|
25
25
|
|
|
26
|
+
import gmpy2 # type:ignore
|
|
27
|
+
|
|
26
28
|
from . import base, modmath, aes
|
|
27
29
|
|
|
28
30
|
__author__ = 'balparda@github.com'
|
|
@@ -122,7 +124,7 @@ class ElGamalSharedPublicKey(base.CryptoKey):
|
|
|
122
124
|
if bit_length < 11:
|
|
123
125
|
raise base.InputError(f'invalid bit length: {bit_length=}')
|
|
124
126
|
# generate random prime and number, create object (should never fail)
|
|
125
|
-
p: int = modmath.
|
|
127
|
+
p: int = modmath.NBitRandomPrimes(bit_length).pop()
|
|
126
128
|
g: int = 0
|
|
127
129
|
while not 2 < g < p - 1:
|
|
128
130
|
g = base.RandBits(bit_length)
|
|
@@ -203,8 +205,8 @@ class ElGamalPublicKey(ElGamalSharedPublicKey, base.Encryptor, base.Verifier):
|
|
|
203
205
|
b: int = 0
|
|
204
206
|
while a < 2 or b < 2:
|
|
205
207
|
ephemeral_key: int = self._MakeEphemeralKey()[0]
|
|
206
|
-
a =
|
|
207
|
-
s: int =
|
|
208
|
+
a = int(gmpy2.powmod(self.group_base, ephemeral_key, self.prime_modulus)) # type:ignore # pylint:disable=no-member
|
|
209
|
+
s: int = int(gmpy2.powmod(self.individual_base, ephemeral_key, self.prime_modulus)) # type:ignore # pylint:disable=no-member
|
|
208
210
|
b = (message * s) % self.prime_modulus
|
|
209
211
|
return (a, b)
|
|
210
212
|
|
|
@@ -278,9 +280,9 @@ class ElGamalPublicKey(ElGamalSharedPublicKey, base.Encryptor, base.Verifier):
|
|
|
278
280
|
not 2 <= signature[1] < self.prime_modulus - 1):
|
|
279
281
|
raise base.InputError(f'invalid signature: {signature=}')
|
|
280
282
|
# verify
|
|
281
|
-
a: int =
|
|
282
|
-
b: int =
|
|
283
|
-
c: int =
|
|
283
|
+
a: int = int(gmpy2.powmod(self.group_base, message, self.prime_modulus)) # type:ignore # pylint:disable=no-member
|
|
284
|
+
b: int = int(gmpy2.powmod(signature[0], signature[1], self.prime_modulus)) # type:ignore # pylint:disable=no-member
|
|
285
|
+
c: int = int(gmpy2.powmod(self.individual_base, signature[0], self.prime_modulus)) # type:ignore # pylint:disable=no-member
|
|
284
286
|
return a == (b * c) % self.prime_modulus
|
|
285
287
|
|
|
286
288
|
def Verify(
|
|
@@ -351,8 +353,7 @@ class ElGamalPrivateKey(ElGamalPublicKey, base.Decryptor, base.Signer): # pylin
|
|
|
351
353
|
if (not 2 < self.decrypt_exp < self.prime_modulus - 1 or
|
|
352
354
|
self.decrypt_exp in (self.group_base, self.individual_base)):
|
|
353
355
|
raise base.InputError(f'invalid decrypt_exp: {self}')
|
|
354
|
-
if
|
|
355
|
-
self.group_base, self.decrypt_exp, self.prime_modulus) != self.individual_base:
|
|
356
|
+
if gmpy2.powmod(self.group_base, self.decrypt_exp, self.prime_modulus) != self.individual_base: # type:ignore # pylint:disable=no-member
|
|
356
357
|
raise base.CryptoError(f'inconsistent g**e % p == i: {self}')
|
|
357
358
|
|
|
358
359
|
def __str__(self) -> str:
|
|
@@ -385,8 +386,8 @@ class ElGamalPrivateKey(ElGamalPublicKey, base.Decryptor, base.Signer): # pylin
|
|
|
385
386
|
not 2 <= ciphertext[1] < self.prime_modulus):
|
|
386
387
|
raise base.InputError(f'invalid message: {ciphertext=}')
|
|
387
388
|
# decrypt
|
|
388
|
-
csi: int =
|
|
389
|
-
ciphertext[0], self.prime_modulus - 1 - self.decrypt_exp, self.prime_modulus)
|
|
389
|
+
csi: int = int(
|
|
390
|
+
gmpy2.powmod(ciphertext[0], self.prime_modulus - 1 - self.decrypt_exp, self.prime_modulus)) # type:ignore # pylint:disable=no-member
|
|
390
391
|
return (ciphertext[1] * csi) % self.prime_modulus
|
|
391
392
|
|
|
392
393
|
def Decrypt(self, ciphertext: bytes, /, *, associated_data: bytes | None = None) -> bytes:
|
|
@@ -450,7 +451,7 @@ class ElGamalPrivateKey(ElGamalPublicKey, base.Decryptor, base.Signer): # pylin
|
|
|
450
451
|
p_1: int = self.prime_modulus - 1
|
|
451
452
|
while a < 2 or b < 2:
|
|
452
453
|
ephemeral_key, ephemeral_inv = self._MakeEphemeralKey()
|
|
453
|
-
a =
|
|
454
|
+
a = int(gmpy2.powmod(self.group_base, ephemeral_key, self.prime_modulus)) # type:ignore # pylint:disable=no-member
|
|
454
455
|
b = (ephemeral_inv * ((message - a * self.decrypt_exp) % p_1)) % p_1
|
|
455
456
|
return (a, b)
|
|
456
457
|
|
|
@@ -519,8 +520,8 @@ class ElGamalPrivateKey(ElGamalPublicKey, base.Decryptor, base.Signer): # pylin
|
|
|
519
520
|
return cls(
|
|
520
521
|
prime_modulus=shared_key.prime_modulus,
|
|
521
522
|
group_base=shared_key.group_base,
|
|
522
|
-
individual_base=
|
|
523
|
-
shared_key.group_base, decrypt_exp, shared_key.prime_modulus),
|
|
523
|
+
individual_base=int(gmpy2.powmod( # type:ignore # pylint:disable=no-member
|
|
524
|
+
shared_key.group_base, decrypt_exp, shared_key.prime_modulus)),
|
|
524
525
|
decrypt_exp=decrypt_exp)
|
|
525
526
|
except base.InputError as err:
|
|
526
527
|
failures += 1
|