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 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.2.0' # 2025-09-05, Fri
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(p_bits: int, q_bits: int, /) -> tuple[int, int, int]:
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.NBitRandomPrime(q_bits)
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
- forbidden: dict[int, int] = {} # (modulus: forbidden residue)
66
- for r in modmath.PrimeGenerator(3):
67
- forbidden[r] = (-modmath.ModInv(q % r, r)) % r
68
- if r > 100000 or r > approx_q_root:
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
- # find range of multiples to use
78
- min_p, max_p = 2 ** (p_bits - 1), 2 ** p_bits - 1
79
- min_m, max_m = min_p // q + 2, max_p // q - 2
80
- assert max_m - min_m > 1000 # make sure we'll have options!
81
- # start searching from a random multiple
82
- failures: int = 0
83
- window: int = max(_PRIME_MULTIPLE_SEARCH, 2 * p_bits)
84
- while True:
85
- # try searching starting here
86
- m: int = base.RandInt(min_m, max_m)
87
- if m % 2:
88
- m += 1 # make even
89
- for _ in range(window):
90
- p: int = q * m + 1
91
- if p >= max_p:
92
- break
93
- # first do a quick sieve test
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 = modmath.ModExp(h, m, p)
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 = modmath.ModExp(
282
- self.group_base, (message * inv) % self.prime_seed, self.prime_modulus)
283
- b: int = modmath.ModExp(
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 modmath.ModExp(
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, b = 0, 0
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 = modmath.ModExp(self.group_base, ephemeral_key, self.prime_modulus) % self.prime_seed
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=modmath.ModExp(
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.NBitRandomPrime(bit_length)
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 = modmath.ModExp(self.group_base, ephemeral_key, self.prime_modulus)
207
- s: int = modmath.ModExp(self.individual_base, ephemeral_key, self.prime_modulus)
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 = modmath.ModExp(self.group_base, message, self.prime_modulus)
282
- b: int = modmath.ModExp(signature[0], signature[1], self.prime_modulus)
283
- c: int = modmath.ModExp(self.individual_base, signature[0], self.prime_modulus)
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 modmath.ModExp(
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 = modmath.ModExp(
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 = modmath.ModExp(self.group_base, ephemeral_key, self.prime_modulus)
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=modmath.ModExp(
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