transcrypto 1.8.0__py3-none-any.whl → 2.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.
@@ -19,7 +19,8 @@ from typing import Self
19
19
 
20
20
  import gmpy2
21
21
 
22
- from . import base, constants, modmath
22
+ from transcrypto.core import constants, hashes, key, modmath
23
+ from transcrypto.utils import base, saferandom
23
24
 
24
25
  _MAX_KEY_GENERATION_FAILURES = 15
25
26
 
@@ -68,8 +69,8 @@ def NBitRandomDSAPrimes(
68
69
  that p % q == 1 and m == (p - 1) // q
69
70
 
70
71
  Raises:
71
- InputError: invalid inputs
72
- Error: prime search failed
72
+ base.InputError: invalid inputs
73
+ base.Error: prime search failed
73
74
 
74
75
  """
75
76
  # test inputs
@@ -140,7 +141,7 @@ def _PrimePSearchShard(q: int, p_bits: int) -> tuple[int | None, int | None]:
140
141
  return all(m % r != f for r, f in forbidden.items())
141
142
 
142
143
  # try searching starting here
143
- m: int = base.RandInt(min_m, max_m)
144
+ m: int = saferandom.RandInt(min_m, max_m)
144
145
  if m % 2:
145
146
  m += 1 # make even
146
147
  count: int = 0
@@ -158,7 +159,7 @@ def _PrimePSearchShard(q: int, p_bits: int) -> tuple[int | None, int | None]:
158
159
 
159
160
 
160
161
  @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
161
- class DSASharedPublicKey(base.CryptoKey):
162
+ class DSASharedPublicKey(key.CryptoKey):
162
163
  """DSA shared public key. This key can be shared by a group.
163
164
 
164
165
  No measures are taken here to prevent timing attacks.
@@ -178,7 +179,7 @@ class DSASharedPublicKey(base.CryptoKey):
178
179
  """Check data.
179
180
 
180
181
  Raises:
181
- InputError: invalid inputs
182
+ base.InputError: invalid inputs
182
183
 
183
184
  """
184
185
  if self.prime_seed < 7 or not modmath.IsPrime(self.prime_seed): # noqa: PLR2004
@@ -227,16 +228,16 @@ class DSASharedPublicKey(base.CryptoKey):
227
228
  Hash512("prefix" || len(aad) || aad || message || salt)
228
229
 
229
230
  Raises:
230
- CryptoError: hash output is out of range
231
+ key.CryptoError: hash output is out of range
231
232
 
232
233
  """
233
234
  aad: bytes = b'' if associated_data is None else associated_data
234
235
  la: bytes = base.IntToFixedBytes(len(aad), 8)
235
236
  assert len(salt) == 64, 'should never happen: salt should be exactly 64 bytes' # noqa: PLR2004, S101
236
- y: int = base.BytesToInt(base.Hash512(_DSA_SIGNATURE_HASH_PREFIX + la + aad + message + salt))
237
+ y: int = base.BytesToInt(hashes.Hash512(_DSA_SIGNATURE_HASH_PREFIX + la + aad + message + salt))
237
238
  if not 1 < y < self.prime_seed - 1:
238
239
  # will only reasonably happen if prime seed is small
239
- raise base.CryptoError(f'hash output {y} is out of range/invalid {self.prime_seed}')
240
+ raise key.CryptoError(f'hash output {y} is out of range/invalid {self.prime_seed}')
240
241
  return y
241
242
 
242
243
  @classmethod
@@ -257,13 +258,13 @@ class DSASharedPublicKey(base.CryptoKey):
257
258
  # generate random number, create object (should never fail)
258
259
  g: int = 0
259
260
  while g < 3: # noqa: PLR2004
260
- h: int = base.RandBits(p_bits - 1)
261
+ h: int = saferandom.RandBits(p_bits - 1)
261
262
  g = int(gmpy2.powmod(h, m, p))
262
263
  return cls(prime_modulus=p, prime_seed=q, group_base=g)
263
264
 
264
265
 
265
266
  @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
266
- class DSAPublicKey(DSASharedPublicKey, base.Verifier):
267
+ class DSAPublicKey(DSASharedPublicKey, key.Verifier):
267
268
  """DSA public key. This is an individual public key.
268
269
 
269
270
  No measures are taken here to prevent timing attacks.
@@ -279,7 +280,7 @@ class DSAPublicKey(DSASharedPublicKey, base.Verifier):
279
280
  """Check data.
280
281
 
281
282
  Raises:
282
- InputError: invalid inputs
283
+ base.InputError: invalid inputs
283
284
 
284
285
  """
285
286
  super(DSAPublicKey, self).__post_init__()
@@ -315,7 +316,7 @@ class DSAPublicKey(DSASharedPublicKey, base.Verifier):
315
316
  self.group_base,
316
317
  self.individual_base,
317
318
  }:
318
- ephemeral_key = base.RandBits(bit_length - 1)
319
+ ephemeral_key = saferandom.RandBits(bit_length - 1)
319
320
  return (ephemeral_key, modmath.ModInv(ephemeral_key, self.prime_seed))
320
321
 
321
322
  def RawVerify(self, message: int, signature: tuple[int, int], /) -> bool:
@@ -333,7 +334,7 @@ class DSAPublicKey(DSASharedPublicKey, base.Verifier):
333
334
  True if signature is valid, False otherwise
334
335
 
335
336
  Raises:
336
- InputError: invalid inputs
337
+ base.InputError: invalid inputs
337
338
 
338
339
  """
339
340
  # test inputs
@@ -371,7 +372,7 @@ class DSAPublicKey(DSASharedPublicKey, base.Verifier):
371
372
  True if signature is valid, False otherwise
372
373
 
373
374
  Raises:
374
- InputError: invalid inputs
375
+ base.InputError: invalid inputs
375
376
 
376
377
  """
377
378
  k: int = self.modulus_size[1] # use prime_seed size
@@ -409,7 +410,7 @@ class DSAPublicKey(DSASharedPublicKey, base.Verifier):
409
410
 
410
411
 
411
412
  @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
412
- class DSAPrivateKey(DSAPublicKey, base.Signer):
413
+ class DSAPrivateKey(DSAPublicKey, key.Signer):
413
414
  """DSA private key.
414
415
 
415
416
  No measures are taken here to prevent timing attacks.
@@ -425,8 +426,8 @@ class DSAPrivateKey(DSAPublicKey, base.Signer):
425
426
  """Check data.
426
427
 
427
428
  Raises:
428
- InputError: invalid inputs
429
- CryptoError: modulus math is inconsistent with values
429
+ base.InputError: invalid inputs
430
+ key.CryptoError: modulus math is inconsistent with values
430
431
 
431
432
  """
432
433
  super(DSAPrivateKey, self).__post_init__()
@@ -436,7 +437,7 @@ class DSAPrivateKey(DSAPublicKey, base.Signer):
436
437
  }:
437
438
  raise base.InputError(f'invalid decrypt_exp: {self}')
438
439
  if gmpy2.powmod(self.group_base, self.decrypt_exp, self.prime_modulus) != self.individual_base:
439
- raise base.CryptoError(f'inconsistent g**d % p == i: {self}')
440
+ raise key.CryptoError(f'inconsistent g**d % p == i: {self}')
440
441
 
441
442
  def __str__(self) -> str:
442
443
  """Safe (no secrets) string representation of the DSAPrivateKey.
@@ -448,7 +449,7 @@ class DSAPrivateKey(DSAPublicKey, base.Signer):
448
449
  return (
449
450
  'DSAPrivateKey('
450
451
  f'{super(DSAPrivateKey, self).__str__()}, '
451
- f'decrypt_exp={base.ObfuscateSecret(self.decrypt_exp)})'
452
+ f'decrypt_exp={hashes.ObfuscateSecret(self.decrypt_exp)})'
452
453
  )
453
454
 
454
455
  def RawSign(self, message: int, /) -> tuple[int, int]:
@@ -465,7 +466,7 @@ class DSAPrivateKey(DSAPublicKey, base.Signer):
465
466
  signed message tuple ((int, int), 2 ≤ s1,s2 < prime_seed
466
467
 
467
468
  Raises:
468
- InputError: invalid inputs
469
+ base.InputError: invalid inputs
469
470
 
470
471
  """
471
472
  # test inputs
@@ -502,13 +503,13 @@ class DSAPrivateKey(DSAPublicKey, base.Signer):
502
503
  bytes: Signature; salt || Padded(s, k) - see above
503
504
 
504
505
  Raises:
505
- InputError: invalid inputs
506
+ base.InputError: invalid inputs
506
507
 
507
508
  """
508
509
  k: int = self.modulus_size[1] # use prime_seed size
509
510
  if k <= 64: # noqa: PLR2004
510
511
  raise base.InputError(f'modulus/seed too small for signing operations: {k} bytes')
511
- salt: bytes = base.RandBytes(64)
512
+ salt: bytes = saferandom.RandBytes(64)
512
513
  s_int: tuple[int, int] = self.RawSign(self._DomainSeparatedHash(message, associated_data, salt))
513
514
  s_bytes: bytes = base.IntToFixedBytes(s_int[0], k) + base.IntToFixedBytes(s_int[1], k)
514
515
  assert len(s_bytes) == 2 * k, 'should never happen: s_bytes should be exactly 2k bytes' # noqa: S101
@@ -525,8 +526,8 @@ class DSAPrivateKey(DSAPublicKey, base.Signer):
525
526
  DSAPrivateKey object ready for use
526
527
 
527
528
  Raises:
528
- InputError: invalid inputs
529
- CryptoError: failed generation
529
+ base.InputError: invalid inputs
530
+ key.CryptoError: failed generation
530
531
 
531
532
  """
532
533
  # test inputs
@@ -542,7 +543,7 @@ class DSAPrivateKey(DSAPublicKey, base.Signer):
542
543
  while (
543
544
  not 2 < decrypt_exp < shared_key.prime_seed or decrypt_exp == shared_key.group_base # noqa: PLR2004
544
545
  ):
545
- decrypt_exp = base.RandBits(bit_length - 1)
546
+ decrypt_exp = saferandom.RandBits(bit_length - 1)
546
547
  # make the object
547
548
  return cls(
548
549
  prime_modulus=shared_key.prime_modulus,
@@ -556,5 +557,5 @@ class DSAPrivateKey(DSAPublicKey, base.Signer):
556
557
  except base.InputError as err:
557
558
  failures += 1
558
559
  if failures >= _MAX_KEY_GENERATION_FAILURES:
559
- raise base.CryptoError(f'failed key generation {failures} times') from err
560
+ raise key.CryptoError(f'failed key generation {failures} times') from err
560
561
  logging.warning(err)
@@ -22,7 +22,8 @@ from typing import Self
22
22
 
23
23
  import gmpy2
24
24
 
25
- from . import aes, base, modmath
25
+ from transcrypto.core import aes, hashes, key, modmath
26
+ from transcrypto.utils import base, saferandom
26
27
 
27
28
  _MAX_KEY_GENERATION_FAILURES = 15
28
29
 
@@ -32,7 +33,7 @@ _ELGAMAL_SIGNATURE_HASH_PREFIX = b'transcrypto.ElGamal.Signature.1.0\x00'
32
33
 
33
34
 
34
35
  @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
35
- class ElGamalSharedPublicKey(base.CryptoKey):
36
+ class ElGamalSharedPublicKey(key.CryptoKey):
36
37
  """El-Gamal shared public key. This key can be shared by a group.
37
38
 
38
39
  BEWARE: This is **NOT** DSA! No measures are taken here to prevent timing attacks.
@@ -50,7 +51,7 @@ class ElGamalSharedPublicKey(base.CryptoKey):
50
51
  """Check data.
51
52
 
52
53
  Raises:
53
- InputError: invalid inputs
54
+ base.InputError: invalid inputs
54
55
 
55
56
  """
56
57
  if self.prime_modulus < 7 or not modmath.IsPrime(self.prime_modulus): # noqa: PLR2004
@@ -92,18 +93,18 @@ class ElGamalSharedPublicKey(base.CryptoKey):
92
93
  Hash512("prefix" || len(aad) || aad || message || salt)
93
94
 
94
95
  Raises:
95
- CryptoError: hash output is out of range
96
+ key.CryptoError: hash output is out of range
96
97
 
97
98
  """
98
99
  aad: bytes = b'' if associated_data is None else associated_data
99
100
  la: bytes = base.IntToFixedBytes(len(aad), 8)
100
101
  assert len(salt) == 64, 'should never happen: salt should be exactly 64 bytes' # noqa: PLR2004, S101
101
102
  y: int = base.BytesToInt(
102
- base.Hash512(_ELGAMAL_SIGNATURE_HASH_PREFIX + la + aad + message + salt)
103
+ hashes.Hash512(_ELGAMAL_SIGNATURE_HASH_PREFIX + la + aad + message + salt)
103
104
  )
104
105
  if not 1 < y < self.prime_modulus:
105
106
  # will only reasonably happen if modulus is small
106
- raise base.CryptoError(f'hash output {y} is out of range/invalid {self.prime_modulus}')
107
+ raise key.CryptoError(f'hash output {y} is out of range/invalid {self.prime_modulus}')
107
108
  return y
108
109
 
109
110
  @classmethod
@@ -117,7 +118,7 @@ class ElGamalSharedPublicKey(base.CryptoKey):
117
118
  ElGamalSharedPublicKey object ready for use
118
119
 
119
120
  Raises:
120
- InputError: invalid inputs
121
+ base.InputError: invalid inputs
121
122
 
122
123
  """
123
124
  # test inputs
@@ -127,12 +128,12 @@ class ElGamalSharedPublicKey(base.CryptoKey):
127
128
  p: int = modmath.NBitRandomPrimes(bit_length).pop()
128
129
  g: int = 0
129
130
  while not 2 < g < p: # noqa: PLR2004
130
- g = base.RandBits(bit_length)
131
+ g = saferandom.RandBits(bit_length)
131
132
  return cls(prime_modulus=p, group_base=g)
132
133
 
133
134
 
134
135
  @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
135
- class ElGamalPublicKey(ElGamalSharedPublicKey, base.Encryptor, base.Verifier):
136
+ class ElGamalPublicKey(ElGamalSharedPublicKey, key.Encryptor, key.Verifier):
136
137
  """El-Gamal public key. This is an individual public key.
137
138
 
138
139
  BEWARE: This is **NOT** DSA! No measures are taken here to prevent timing attacks.
@@ -148,7 +149,7 @@ class ElGamalPublicKey(ElGamalSharedPublicKey, base.Encryptor, base.Verifier):
148
149
  """Check data.
149
150
 
150
151
  Raises:
151
- InputError: invalid inputs
152
+ base.InputError: invalid inputs
152
153
 
153
154
  """
154
155
  super(ElGamalPublicKey, self).__post_init__()
@@ -186,8 +187,8 @@ class ElGamalPublicKey(ElGamalSharedPublicKey, base.Encryptor, base.Verifier):
186
187
  self.group_base,
187
188
  self.individual_base,
188
189
  }:
189
- ephemeral_key = base.RandBits(bit_length)
190
- if base.GCD(ephemeral_key, p_1) != 1:
190
+ ephemeral_key = saferandom.RandBits(bit_length)
191
+ if modmath.GCD(ephemeral_key, p_1) != 1:
191
192
  ephemeral_key = 0 # we have to try again
192
193
  return (ephemeral_key, modmath.ModInv(ephemeral_key, p_1))
193
194
 
@@ -205,7 +206,7 @@ class ElGamalPublicKey(ElGamalSharedPublicKey, base.Encryptor, base.Verifier):
205
206
  ciphertext message tuple ((int, int), 2 ≤ c1,c2 < modulus)
206
207
 
207
208
  Raises:
208
- InputError: invalid inputs
209
+ base.InputError: invalid inputs
209
210
 
210
211
  """
211
212
  # test inputs
@@ -252,13 +253,13 @@ class ElGamalPublicKey(ElGamalSharedPublicKey, base.Encryptor, base.Verifier):
252
253
  # generate random r and encrypt it
253
254
  r: int = 0
254
255
  while not 1 < r < self.prime_modulus:
255
- r = base.RandBits(self.prime_modulus.bit_length())
256
+ r = saferandom.RandBits(self.prime_modulus.bit_length())
256
257
  k: int = self.modulus_size
257
258
  i_ct: tuple[int, int] = self.RawEncrypt(r)
258
259
  ct: bytes = base.IntToFixedBytes(i_ct[0], k) + base.IntToFixedBytes(i_ct[1], k)
259
260
  assert len(ct) == 2 * k, 'should never happen: c_kem should be exactly 2k bytes' # noqa: S101
260
261
  # encrypt plaintext with AES-256-GCM using SHA512(r)[32:] as key; return ct || Encrypt(...)
261
- ss: bytes = base.Hash512(base.IntToFixedBytes(r, k))
262
+ ss: bytes = hashes.Hash512(base.IntToFixedBytes(r, k))
262
263
  aad: bytes = b'' if associated_data is None else associated_data
263
264
  aad_prime: bytes = _ELGAMAL_ENCRYPTION_AAD_PREFIX + base.IntToFixedBytes(len(aad), 8) + aad + ct
264
265
  return ct + aes.AESKey(key256=ss[32:]).Encrypt(plaintext, associated_data=aad_prime)
@@ -278,7 +279,7 @@ class ElGamalPublicKey(ElGamalSharedPublicKey, base.Encryptor, base.Verifier):
278
279
  True if signature is valid, False otherwise
279
280
 
280
281
  Raises:
281
- InputError: invalid inputs
282
+ base.InputError: invalid inputs
282
283
 
283
284
  """
284
285
  # test inputs
@@ -312,7 +313,7 @@ class ElGamalPublicKey(ElGamalSharedPublicKey, base.Encryptor, base.Verifier):
312
313
  True if signature is valid, False otherwise
313
314
 
314
315
  Raises:
315
- InputError: invalid inputs
316
+ base.InputError: invalid inputs
316
317
 
317
318
  """
318
319
  k: int = self.modulus_size
@@ -349,7 +350,7 @@ class ElGamalPublicKey(ElGamalSharedPublicKey, base.Encryptor, base.Verifier):
349
350
 
350
351
 
351
352
  @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
352
- class ElGamalPrivateKey(ElGamalPublicKey, base.Decryptor, base.Signer):
353
+ class ElGamalPrivateKey(ElGamalPublicKey, key.Decryptor, key.Signer):
353
354
  """El-Gamal private key.
354
355
 
355
356
  BEWARE: This is **NOT** DSA! No measures are taken here to prevent timing attacks.
@@ -365,8 +366,8 @@ class ElGamalPrivateKey(ElGamalPublicKey, base.Decryptor, base.Signer):
365
366
  """Check data.
366
367
 
367
368
  Raises:
368
- InputError: invalid inputs
369
- CryptoError: modulus math is inconsistent with values
369
+ base.InputError: invalid inputs
370
+ key.CryptoError: modulus math is inconsistent with values
370
371
 
371
372
  """
372
373
  super(ElGamalPrivateKey, self).__post_init__()
@@ -376,7 +377,7 @@ class ElGamalPrivateKey(ElGamalPublicKey, base.Decryptor, base.Signer):
376
377
  }:
377
378
  raise base.InputError(f'invalid decrypt_exp: {self}')
378
379
  if gmpy2.powmod(self.group_base, self.decrypt_exp, self.prime_modulus) != self.individual_base:
379
- raise base.CryptoError(f'inconsistent g**e % p == i: {self}')
380
+ raise key.CryptoError(f'inconsistent g**e % p == i: {self}')
380
381
 
381
382
  def __str__(self) -> str:
382
383
  """Safe (no secrets) string representation of the ElGamalPrivateKey.
@@ -388,7 +389,7 @@ class ElGamalPrivateKey(ElGamalPublicKey, base.Decryptor, base.Signer):
388
389
  return (
389
390
  'ElGamalPrivateKey('
390
391
  f'{super(ElGamalPrivateKey, self).__str__()}, '
391
- f'decrypt_exp={base.ObfuscateSecret(self.decrypt_exp)})'
392
+ f'decrypt_exp={hashes.ObfuscateSecret(self.decrypt_exp)})'
392
393
  )
393
394
 
394
395
  def RawDecrypt(self, ciphertext: tuple[int, int], /) -> int:
@@ -404,7 +405,7 @@ class ElGamalPrivateKey(ElGamalPublicKey, base.Decryptor, base.Signer):
404
405
  decrypted message (int, 1 ≤ m < modulus)
405
406
 
406
407
  Raises:
407
- InputError: invalid inputs
408
+ base.InputError: invalid inputs
408
409
 
409
410
  """
410
411
  # test inputs
@@ -437,7 +438,7 @@ class ElGamalPrivateKey(ElGamalPublicKey, base.Decryptor, base.Signer):
437
438
  bytes: Decrypted plaintext bytes
438
439
 
439
440
  Raises:
440
- InputError: invalid inputs
441
+ base.InputError: invalid inputs
441
442
 
442
443
  """
443
444
  k: int = self.modulus_size
@@ -446,7 +447,7 @@ class ElGamalPrivateKey(ElGamalPublicKey, base.Decryptor, base.Signer):
446
447
  # split ciphertext in 3 parts: the first 2k bytes is ct, the rest is AES-256-GCM
447
448
  ct1, ct2, aes_ct = ciphertext[:k], ciphertext[k : 2 * k], ciphertext[2 * k :]
448
449
  r: int = self.RawDecrypt((base.BytesToInt(ct1), base.BytesToInt(ct2)))
449
- ss: bytes = base.Hash512(base.IntToFixedBytes(r, k))
450
+ ss: bytes = hashes.Hash512(base.IntToFixedBytes(r, k))
450
451
  aad: bytes = b'' if associated_data is None else associated_data
451
452
  aad_prime: bytes = (
452
453
  _ELGAMAL_ENCRYPTION_AAD_PREFIX + base.IntToFixedBytes(len(aad), 8) + aad + ct1 + ct2
@@ -467,7 +468,7 @@ class ElGamalPrivateKey(ElGamalPublicKey, base.Decryptor, base.Signer):
467
468
  signed message tuple ((int, int), 2 ≤ s1 < modulus, 2 ≤ s2 < modulus-1)
468
469
 
469
470
  Raises:
470
- InputError: invalid inputs
471
+ base.InputError: invalid inputs
471
472
 
472
473
  """
473
474
  # test inputs
@@ -505,13 +506,13 @@ class ElGamalPrivateKey(ElGamalPublicKey, base.Decryptor, base.Signer):
505
506
  bytes: Signature; salt || Padded(s, k) - see above
506
507
 
507
508
  Raises:
508
- InputError: invalid inputs
509
+ base.InputError: invalid inputs
509
510
 
510
511
  """
511
512
  k: int = self.modulus_size
512
513
  if k <= 64: # noqa: PLR2004
513
514
  raise base.InputError(f'modulus too small for signing operations: {k} bytes')
514
- salt: bytes = base.RandBytes(64)
515
+ salt: bytes = saferandom.RandBytes(64)
515
516
  s_int: tuple[int, int] = self.RawSign(self._DomainSeparatedHash(message, associated_data, salt))
516
517
  s_bytes: bytes = base.IntToFixedBytes(s_int[0], k) + base.IntToFixedBytes(s_int[1], k)
517
518
  assert len(s_bytes) == 2 * k, 'should never happen: s_bytes should be exactly 2k bytes' # noqa: S101
@@ -528,8 +529,8 @@ class ElGamalPrivateKey(ElGamalPublicKey, base.Decryptor, base.Signer):
528
529
  ElGamalPrivateKey object ready for use
529
530
 
530
531
  Raises:
531
- InputError: invalid inputs
532
- CryptoError: failed generation
532
+ base.InputError: invalid inputs
533
+ key.CryptoError: failed generation
533
534
 
534
535
  """
535
536
  # test inputs
@@ -545,7 +546,7 @@ class ElGamalPrivateKey(ElGamalPublicKey, base.Decryptor, base.Signer):
545
546
  while (
546
547
  not 2 < decrypt_exp < shared_key.prime_modulus or decrypt_exp == shared_key.group_base # noqa: PLR2004
547
548
  ):
548
- decrypt_exp = base.RandBits(bit_length)
549
+ decrypt_exp = saferandom.RandBits(bit_length)
549
550
  # make the object
550
551
  return cls(
551
552
  prime_modulus=shared_key.prime_modulus,
@@ -558,5 +559,5 @@ class ElGamalPrivateKey(ElGamalPublicKey, base.Decryptor, base.Signer):
558
559
  except base.InputError as err:
559
560
  failures += 1
560
561
  if failures >= _MAX_KEY_GENERATION_FAILURES:
561
- raise base.CryptoError(f'failed key generation {failures} times') from err
562
+ raise key.CryptoError(f'failed key generation {failures} times') from err
562
563
  logging.warning(err)
@@ -0,0 +1,96 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Balparda's TransCrypto hash utilities library."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import hashlib
8
+ import logging
9
+ import pathlib
10
+
11
+ from transcrypto.utils import base
12
+
13
+
14
+ def Hash256(data: bytes, /) -> bytes:
15
+ """SHA-256 hash of bytes data. Always a length of 32 bytes.
16
+
17
+ Args:
18
+ data (bytes): Data to compute hash for
19
+
20
+ Returns:
21
+ 32 bytes (256 bits) of SHA-256 hash;
22
+ if converted to hexadecimal (with BytesToHex() or hex()) will be 64 chars of string;
23
+ if converted to int (big-endian, unsigned, with BytesToInt()) will be 0 ≤ i < 2**256
24
+
25
+ """
26
+ return hashlib.sha256(data).digest()
27
+
28
+
29
+ def Hash512(data: bytes, /) -> bytes:
30
+ """SHA-512 hash of bytes data. Always a length of 64 bytes.
31
+
32
+ Args:
33
+ data (bytes): Data to compute hash for
34
+
35
+ Returns:
36
+ 64 bytes (512 bits) of SHA-512 hash;
37
+ if converted to hexadecimal (with BytesToHex() or hex()) will be 128 chars of string;
38
+ if converted to int (big-endian, unsigned, with BytesToInt()) will be 0 ≤ i < 2**512
39
+
40
+ """
41
+ return hashlib.sha512(data).digest()
42
+
43
+
44
+ def FileHash(full_path: str, /, *, digest: str = 'sha256') -> bytes:
45
+ """SHA-256 hex hash of file on disk. Always a length of 32 bytes (if default digest=='sha256').
46
+
47
+ Args:
48
+ full_path (str): Path to existing file on disk
49
+ digest (str, optional): Hash method to use, accepts 'sha256' (default) or 'sha512'
50
+
51
+ Returns:
52
+ 32 bytes (256 bits) of SHA-256 hash (if default digest=='sha256');
53
+ if converted to hexadecimal (with BytesToHex() or hex()) will be 64 chars of string;
54
+ if converted to int (big-endian, unsigned, with BytesToInt()) will be 0 ≤ i < 2**256
55
+
56
+ Raises:
57
+ base.InputError: file could not be found
58
+
59
+ """
60
+ # test inputs
61
+ digest = digest.lower().strip().replace('-', '') # normalize so we can accept e.g. "SHA-256"
62
+ if digest not in {'sha256', 'sha512'}:
63
+ raise base.InputError(f'unrecognized digest: {digest!r}')
64
+ full_path = full_path.strip()
65
+ if not full_path or not pathlib.Path(full_path).exists():
66
+ raise base.InputError(f'file {full_path!r} not found for hashing')
67
+ # compute hash
68
+ logging.info(f'Hashing file {full_path!r}')
69
+ with pathlib.Path(full_path).open('rb') as file_obj:
70
+ return hashlib.file_digest(file_obj, digest).digest()
71
+
72
+
73
+ def ObfuscateSecret(data: str | bytes | int, /) -> str:
74
+ """Obfuscate a secret string/key/bytes/int by hashing SHA-512 and only showing the first 4 bytes.
75
+
76
+ Always a length of 9 chars, e.g. "aabbccdd…" (always adds '…' at the end).
77
+ Known vulnerability: If the secret is small, can be brute-forced!
78
+ Use only on large (~>64bits) secrets.
79
+
80
+ Args:
81
+ data (str | bytes | int): Data to obfuscate
82
+
83
+ Raises:
84
+ base.InputError: _description_
85
+
86
+ Returns:
87
+ str: obfuscated string, e.g. "aabbccdd…"
88
+
89
+ """
90
+ if isinstance(data, str):
91
+ data = data.encode('utf-8')
92
+ elif isinstance(data, int):
93
+ data = base.IntToBytes(data)
94
+ if not isinstance(data, bytes): # pyright: ignore[reportUnnecessaryIsInstance]
95
+ raise base.InputError(f'invalid type for data: {type(data)}')
96
+ return base.BytesToHex(Hash512(data))[:8] + '…'