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.
- transcrypto/__init__.py +1 -1
- transcrypto/cli/aeshash.py +14 -12
- transcrypto/cli/bidsecret.py +19 -16
- transcrypto/cli/clibase.py +22 -142
- transcrypto/cli/intmath.py +24 -21
- transcrypto/cli/publicalgos.py +28 -26
- transcrypto/core/__init__.py +3 -0
- transcrypto/{aes.py → core/aes.py} +17 -29
- transcrypto/core/bid.py +161 -0
- transcrypto/{dsa.py → core/dsa.py} +28 -27
- transcrypto/{elgamal.py → core/elgamal.py} +33 -32
- transcrypto/core/hashes.py +96 -0
- transcrypto/core/key.py +735 -0
- transcrypto/{modmath.py → core/modmath.py} +91 -17
- transcrypto/{rsa.py → core/rsa.py} +51 -50
- transcrypto/{sss.py → core/sss.py} +27 -26
- transcrypto/profiler.py +25 -11
- transcrypto/transcrypto.py +25 -15
- transcrypto/utils/__init__.py +3 -0
- transcrypto/utils/base.py +72 -0
- transcrypto/utils/human.py +278 -0
- transcrypto/utils/logging.py +139 -0
- transcrypto/utils/saferandom.py +102 -0
- transcrypto/utils/stats.py +360 -0
- transcrypto/utils/timer.py +175 -0
- {transcrypto-1.8.0.dist-info → transcrypto-2.0.3.dist-info}/METADATA +101 -101
- transcrypto-2.0.3.dist-info/RECORD +33 -0
- {transcrypto-1.8.0.dist-info → transcrypto-2.0.3.dist-info}/WHEEL +1 -1
- transcrypto/base.py +0 -1637
- transcrypto-1.8.0.dist-info/RECORD +0 -23
- /transcrypto/{constants.py → core/constants.py} +0 -0
- {transcrypto-1.8.0.dist-info → transcrypto-2.0.3.dist-info}/entry_points.txt +0 -0
- {transcrypto-1.8.0.dist-info → transcrypto-2.0.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -19,7 +19,8 @@ from typing import Self
|
|
|
19
19
|
|
|
20
20
|
import gmpy2
|
|
21
21
|
|
|
22
|
-
from . import
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
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,
|
|
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 =
|
|
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,
|
|
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
|
|
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={
|
|
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 =
|
|
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 =
|
|
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
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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,
|
|
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 =
|
|
190
|
-
if
|
|
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 =
|
|
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 =
|
|
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,
|
|
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
|
|
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={
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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] + '…'
|