transcrypto 1.1.2__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/aes.py +4 -3
- transcrypto/base.py +84 -30
- transcrypto/dsa.py +225 -48
- transcrypto/elgamal.py +237 -40
- transcrypto/modmath.py +487 -40
- transcrypto/rsa.py +220 -36
- transcrypto/sss.py +160 -23
- transcrypto/transcrypto.py +429 -191
- {transcrypto-1.1.2.dist-info → transcrypto-1.3.0.dist-info}/METADATA +732 -427
- transcrypto-1.3.0.dist-info/RECORD +15 -0
- transcrypto-1.1.2.dist-info/RECORD +0 -15
- {transcrypto-1.1.2.dist-info → transcrypto-1.3.0.dist-info}/WHEEL +0 -0
- {transcrypto-1.1.2.dist-info → transcrypto-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {transcrypto-1.1.2.dist-info → transcrypto-1.3.0.dist-info}/top_level.txt +0 -0
transcrypto/elgamal.py
CHANGED
|
@@ -23,8 +23,9 @@ import logging
|
|
|
23
23
|
# import pdb
|
|
24
24
|
from typing import Self
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
import gmpy2 # type:ignore
|
|
27
|
+
|
|
28
|
+
from . import base, modmath, aes
|
|
28
29
|
|
|
29
30
|
__author__ = 'balparda@github.com'
|
|
30
31
|
__version__: str = base.__version__ # version comes from base!
|
|
@@ -33,14 +34,16 @@ __version_tuple__: tuple[int, ...] = base.__version_tuple__
|
|
|
33
34
|
|
|
34
35
|
_MAX_KEY_GENERATION_FAILURES = 15
|
|
35
36
|
|
|
37
|
+
# fixed prefixes: do NOT ever change! will break all encryption and signature schemes
|
|
38
|
+
_ELGAMAL_ENCRYPTION_AAD_PREFIX = b'transcrypto.ElGamal.Encryption.1.0\x00'
|
|
39
|
+
_ELGAMAL_SIGNATURE_HASH_PREFIX = b'transcrypto.ElGamal.Signature.1.0\x00'
|
|
40
|
+
|
|
36
41
|
|
|
37
42
|
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
38
43
|
class ElGamalSharedPublicKey(base.CryptoKey):
|
|
39
44
|
"""El-Gamal shared public key. This key can be shared by a group.
|
|
40
45
|
|
|
41
|
-
BEWARE: This is
|
|
42
|
-
These are pedagogical/raw primitives; do not use for new protocols.
|
|
43
|
-
No measures are taken here to prevent timing attacks.
|
|
46
|
+
BEWARE: This is **NOT** DSA! No measures are taken here to prevent timing attacks.
|
|
44
47
|
|
|
45
48
|
Attributes:
|
|
46
49
|
prime_modulus (int): prime modulus, ≥ 7
|
|
@@ -69,9 +72,41 @@ class ElGamalSharedPublicKey(base.CryptoKey):
|
|
|
69
72
|
string representation of ElGamalSharedPublicKey
|
|
70
73
|
"""
|
|
71
74
|
return ('ElGamalSharedPublicKey('
|
|
75
|
+
f'bits={self.prime_modulus.bit_length()}, '
|
|
72
76
|
f'prime_modulus={base.IntToEncoded(self.prime_modulus)}, '
|
|
73
77
|
f'group_base={base.IntToEncoded(self.group_base)})')
|
|
74
78
|
|
|
79
|
+
@property
|
|
80
|
+
def modulus_size(self) -> int:
|
|
81
|
+
"""Modulus size in bytes. The number of bytes used in Encrypt/Decrypt/Sign/Verify."""
|
|
82
|
+
return (self.prime_modulus.bit_length() + 7) // 8
|
|
83
|
+
|
|
84
|
+
def _DomainSeparatedHash(
|
|
85
|
+
self, message: bytes, associated_data: bytes | None, salt: bytes, /) -> int:
|
|
86
|
+
"""Compute the domain-separated hash for signing and verifying.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
message (bytes): message to sign/verify
|
|
90
|
+
associated_data (bytes | None): optional associated data
|
|
91
|
+
salt (bytes): salt to use in the hash
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
int: integer representation of the hash output;
|
|
95
|
+
Hash512("prefix" || len(aad) || aad || message || salt)
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
CryptoError: hash output is out of range
|
|
99
|
+
"""
|
|
100
|
+
aad: bytes = b'' if associated_data is None else associated_data
|
|
101
|
+
la: bytes = base.IntToFixedBytes(len(aad), 8)
|
|
102
|
+
assert len(salt) == 64, 'should never happen: salt should be exactly 64 bytes'
|
|
103
|
+
y: int = base.BytesToInt(
|
|
104
|
+
base.Hash512(_ELGAMAL_SIGNATURE_HASH_PREFIX + la + aad + message + salt))
|
|
105
|
+
if not 1 < y < self.prime_modulus:
|
|
106
|
+
# will only reasonably happen if modulus is small
|
|
107
|
+
raise base.CryptoError(f'hash output {y} is out of range/invalid {self.prime_modulus}')
|
|
108
|
+
return y
|
|
109
|
+
|
|
75
110
|
@classmethod
|
|
76
111
|
def NewShared(cls, bit_length: int, /) -> Self:
|
|
77
112
|
"""Make a new shared public key of `bit_length` bits.
|
|
@@ -89,19 +124,18 @@ class ElGamalSharedPublicKey(base.CryptoKey):
|
|
|
89
124
|
if bit_length < 11:
|
|
90
125
|
raise base.InputError(f'invalid bit length: {bit_length=}')
|
|
91
126
|
# generate random prime and number, create object (should never fail)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
127
|
+
p: int = modmath.NBitRandomPrimes(bit_length).pop()
|
|
128
|
+
g: int = 0
|
|
129
|
+
while not 2 < g < p - 1:
|
|
130
|
+
g = base.RandBits(bit_length)
|
|
131
|
+
return cls(prime_modulus=p, group_base=g)
|
|
96
132
|
|
|
97
133
|
|
|
98
134
|
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
99
|
-
class ElGamalPublicKey(ElGamalSharedPublicKey):
|
|
135
|
+
class ElGamalPublicKey(ElGamalSharedPublicKey, base.Encryptor, base.Verifier):
|
|
100
136
|
"""El-Gamal public key. This is an individual public key.
|
|
101
137
|
|
|
102
|
-
BEWARE: This is
|
|
103
|
-
These are pedagogical/raw primitives; do not use for new protocols.
|
|
104
|
-
No measures are taken here to prevent timing attacks.
|
|
138
|
+
BEWARE: This is **NOT** DSA! No measures are taken here to prevent timing attacks.
|
|
105
139
|
|
|
106
140
|
Attributes:
|
|
107
141
|
individual_base (int): individual encryption public base, 3 ≤ i < prime_modulus
|
|
@@ -126,7 +160,8 @@ class ElGamalPublicKey(ElGamalSharedPublicKey):
|
|
|
126
160
|
Returns:
|
|
127
161
|
string representation of ElGamalPublicKey
|
|
128
162
|
"""
|
|
129
|
-
return (
|
|
163
|
+
return ('ElGamalPublicKey('
|
|
164
|
+
f'{super(ElGamalPublicKey, self).__str__()}, ' # pylint: disable=super-with-arguments
|
|
130
165
|
f'individual_base={base.IntToEncoded(self.individual_base)})')
|
|
131
166
|
|
|
132
167
|
def _MakeEphemeralKey(self) -> tuple[int, int]:
|
|
@@ -141,14 +176,16 @@ class ElGamalPublicKey(ElGamalSharedPublicKey):
|
|
|
141
176
|
bit_length: int = self.prime_modulus.bit_length()
|
|
142
177
|
while (not 1 < ephemeral_key < p_1 or
|
|
143
178
|
ephemeral_key in (self.group_base, self.individual_base)):
|
|
144
|
-
ephemeral_key = base.RandBits(bit_length
|
|
179
|
+
ephemeral_key = base.RandBits(bit_length)
|
|
145
180
|
if base.GCD(ephemeral_key, p_1) != 1:
|
|
146
181
|
ephemeral_key = 0 # we have to try again
|
|
147
182
|
return (ephemeral_key, modmath.ModInv(ephemeral_key, p_1))
|
|
148
183
|
|
|
149
|
-
def
|
|
184
|
+
def RawEncrypt(self, message: int, /) -> tuple[int, int]:
|
|
150
185
|
"""Encrypt `message` with this public key.
|
|
151
186
|
|
|
187
|
+
BEWARE: This is raw El-Gamal, no ECIES-style KEM/DEM padding or validation! This is **NOT** DSA!
|
|
188
|
+
These are pedagogical/raw primitives; do not use for new protocols.
|
|
152
189
|
We explicitly disallow `message` to be zero.
|
|
153
190
|
|
|
154
191
|
Args:
|
|
@@ -164,17 +201,66 @@ class ElGamalPublicKey(ElGamalSharedPublicKey):
|
|
|
164
201
|
if not 0 < message < self.prime_modulus:
|
|
165
202
|
raise base.InputError(f'invalid message: {message=}')
|
|
166
203
|
# encrypt
|
|
167
|
-
|
|
168
|
-
|
|
204
|
+
a: int = 0
|
|
205
|
+
b: int = 0
|
|
169
206
|
while a < 2 or b < 2:
|
|
170
|
-
|
|
171
|
-
|
|
207
|
+
ephemeral_key: int = self._MakeEphemeralKey()[0]
|
|
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
|
|
172
210
|
b = (message * s) % self.prime_modulus
|
|
173
211
|
return (a, b)
|
|
174
212
|
|
|
175
|
-
def
|
|
213
|
+
def Encrypt(self, plaintext: bytes, /, *, associated_data: bytes | None = None) -> bytes:
|
|
214
|
+
"""Encrypt `plaintext` and return `ciphertext`.
|
|
215
|
+
|
|
216
|
+
• Let k = ceil(log2(n))/8 be the modulus size in bytes.
|
|
217
|
+
• Pick random r ∈ [2, n-1]
|
|
218
|
+
• ct1, ct2 = ElGamal(r)
|
|
219
|
+
• return Padded(ct1, k) + Padded(ct2, k) +
|
|
220
|
+
AES-256-GCM(key=SHA512(r)[32:], plaintext,
|
|
221
|
+
associated_data="prefix" + len(aad) + aad +
|
|
222
|
+
Padded(ct1, k) + Padded(ct2, k))
|
|
223
|
+
|
|
224
|
+
We pick fresh random r, send ct = ElGamal(r), and derive the DEM key from r,
|
|
225
|
+
then use AES-GCM for the payload. This is the classic El-Gamal-KEM construction.
|
|
226
|
+
With AEAD as the DEM, we get strong confidentiality and ciphertext integrity
|
|
227
|
+
(CCA resistance in the ROM under standard assumptions). There are no
|
|
228
|
+
Bleichenbacher-style issue because we do not expose any padding semantics.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
plaintext (bytes): Data to encrypt.
|
|
232
|
+
associated_data (bytes, optional): Optional AAD; must be provided again on decrypt
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
bytes: Ciphertext; see above:
|
|
236
|
+
Padded(ct1, k) + Padded(ct2, k) + AES-256-GCM(key=SHA512(r)[32:], plaintext,
|
|
237
|
+
associated_data="prefix" + len(aad) + aad +
|
|
238
|
+
Padded(ct1, k) + Padded(ct2, k))
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
InputError: invalid inputs
|
|
242
|
+
CryptoError: internal crypto failures
|
|
243
|
+
"""
|
|
244
|
+
# generate random r and encrypt it
|
|
245
|
+
r: int = 0
|
|
246
|
+
while not 1 < r < self.prime_modulus - 1:
|
|
247
|
+
r = base.RandBits(self.prime_modulus.bit_length())
|
|
248
|
+
k: int = self.modulus_size
|
|
249
|
+
i_ct: tuple[int, int] = self.RawEncrypt(r)
|
|
250
|
+
ct: bytes = base.IntToFixedBytes(i_ct[0], k) + base.IntToFixedBytes(i_ct[1], k)
|
|
251
|
+
assert len(ct) == 2 * k, 'should never happen: c_kem should be exactly 2k bytes'
|
|
252
|
+
# encrypt plaintext with AES-256-GCM using SHA512(r)[32:] as key; return ct || Encrypt(...)
|
|
253
|
+
ss: bytes = base.Hash512(base.IntToFixedBytes(r, k))
|
|
254
|
+
aad: bytes = b'' if associated_data is None else associated_data
|
|
255
|
+
aad_prime: bytes = (
|
|
256
|
+
_ELGAMAL_ENCRYPTION_AAD_PREFIX + base.IntToFixedBytes(len(aad), 8) + aad + ct)
|
|
257
|
+
return ct + aes.AESKey(key256=ss[32:]).Encrypt(plaintext, associated_data=aad_prime)
|
|
258
|
+
|
|
259
|
+
def RawVerify(self, message: int, signature: tuple[int, int], /) -> bool:
|
|
176
260
|
"""Verify a signature. True if OK; False if failed verification.
|
|
177
261
|
|
|
262
|
+
BEWARE: This is raw El-Gamal, no ECIES-style KEM/DEM padding or validation! This is **NOT** DSA!
|
|
263
|
+
These are pedagogical/raw primitives; do not use for new protocols.
|
|
178
264
|
We explicitly disallow `message` to be zero.
|
|
179
265
|
|
|
180
266
|
Args:
|
|
@@ -194,11 +280,47 @@ class ElGamalPublicKey(ElGamalSharedPublicKey):
|
|
|
194
280
|
not 2 <= signature[1] < self.prime_modulus - 1):
|
|
195
281
|
raise base.InputError(f'invalid signature: {signature=}')
|
|
196
282
|
# verify
|
|
197
|
-
a: int =
|
|
198
|
-
b: int =
|
|
199
|
-
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
|
|
200
286
|
return a == (b * c) % self.prime_modulus
|
|
201
287
|
|
|
288
|
+
def Verify(
|
|
289
|
+
self, message: bytes, signature: bytes, /, *, associated_data: bytes | None = None) -> bool:
|
|
290
|
+
"""Verify a `signature` for `message`. True if OK; False if failed verification.
|
|
291
|
+
|
|
292
|
+
• Let k = ceil(log2(n))/8 be the modulus size in bytes.
|
|
293
|
+
• Split signature in 3 parts: the first 64 bytes is salt, the rest is s1 and s2
|
|
294
|
+
• y_check = ElGamal(s1, s2)
|
|
295
|
+
• return y_check == Hash512("prefix" || len(aad) || aad || message || salt)
|
|
296
|
+
• return False for any malformed signature
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
message (bytes): Data that was signed
|
|
300
|
+
signature (bytes): Signature data to verify
|
|
301
|
+
associated_data (bytes, optional): Optional AAD (must match what was used during signing)
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
True if signature is valid, False otherwise
|
|
305
|
+
|
|
306
|
+
Raises:
|
|
307
|
+
InputError: invalid inputs
|
|
308
|
+
CryptoError: internal crypto failures, authentication failure, key mismatch, etc
|
|
309
|
+
"""
|
|
310
|
+
k: int = self.modulus_size
|
|
311
|
+
if k <= 64:
|
|
312
|
+
raise base.InputError(f'modulus too small for signing operations: {k} bytes')
|
|
313
|
+
if len(signature) != (64 + k + k):
|
|
314
|
+
logging.info(f'invalid signature length: {len(signature)} ; expected {64 + k + k}')
|
|
315
|
+
return False
|
|
316
|
+
try:
|
|
317
|
+
return self.RawVerify(
|
|
318
|
+
self._DomainSeparatedHash(message, associated_data, signature[:64]),
|
|
319
|
+
(base.BytesToInt(signature[64:64 + k]), base.BytesToInt(signature[64 + k:])))
|
|
320
|
+
except base.InputError as err:
|
|
321
|
+
logging.info(err)
|
|
322
|
+
return False
|
|
323
|
+
|
|
202
324
|
@classmethod
|
|
203
325
|
def Copy(cls, other: ElGamalPublicKey, /) -> Self:
|
|
204
326
|
"""Initialize a public key by taking the public parts of a public/private key."""
|
|
@@ -209,12 +331,10 @@ class ElGamalPublicKey(ElGamalSharedPublicKey):
|
|
|
209
331
|
|
|
210
332
|
|
|
211
333
|
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
212
|
-
class ElGamalPrivateKey(ElGamalPublicKey):
|
|
334
|
+
class ElGamalPrivateKey(ElGamalPublicKey, base.Decryptor, base.Signer): # pylint: disable=too-many-ancestors
|
|
213
335
|
"""El-Gamal private key.
|
|
214
336
|
|
|
215
|
-
BEWARE: This is
|
|
216
|
-
These are pedagogical/raw primitives; do not use for new protocols.
|
|
217
|
-
No measures are taken here to prevent timing attacks.
|
|
337
|
+
BEWARE: This is **NOT** DSA! No measures are taken here to prevent timing attacks.
|
|
218
338
|
|
|
219
339
|
Attributes:
|
|
220
340
|
decrypt_exp (int): individual decryption exponent, 3 ≤ i < prime_modulus
|
|
@@ -233,8 +353,7 @@ class ElGamalPrivateKey(ElGamalPublicKey):
|
|
|
233
353
|
if (not 2 < self.decrypt_exp < self.prime_modulus - 1 or
|
|
234
354
|
self.decrypt_exp in (self.group_base, self.individual_base)):
|
|
235
355
|
raise base.InputError(f'invalid decrypt_exp: {self}')
|
|
236
|
-
if
|
|
237
|
-
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
|
|
238
357
|
raise base.CryptoError(f'inconsistent g**e % p == i: {self}')
|
|
239
358
|
|
|
240
359
|
def __str__(self) -> str:
|
|
@@ -243,12 +362,16 @@ class ElGamalPrivateKey(ElGamalPublicKey):
|
|
|
243
362
|
Returns:
|
|
244
363
|
string representation of ElGamalPrivateKey without leaking secrets
|
|
245
364
|
"""
|
|
246
|
-
return (
|
|
365
|
+
return ('ElGamalPrivateKey('
|
|
366
|
+
f'{super(ElGamalPrivateKey, self).__str__()}, ' # pylint: disable=super-with-arguments
|
|
247
367
|
f'decrypt_exp={base.ObfuscateSecret(self.decrypt_exp)})')
|
|
248
368
|
|
|
249
|
-
def
|
|
369
|
+
def RawDecrypt(self, ciphertext: tuple[int, int], /) -> int:
|
|
250
370
|
"""Decrypt `ciphertext` tuple with this private key.
|
|
251
371
|
|
|
372
|
+
BEWARE: This is raw El-Gamal, no ECIES-style KEM/DEM padding or validation! This is **NOT** DSA!
|
|
373
|
+
These are pedagogical/raw primitives; do not use for new protocols.
|
|
374
|
+
|
|
252
375
|
Args:
|
|
253
376
|
ciphertext (tuple[int, int]): ciphertext to decrypt, 0 ≤ c1,c2 < modulus
|
|
254
377
|
|
|
@@ -263,13 +386,51 @@ class ElGamalPrivateKey(ElGamalPublicKey):
|
|
|
263
386
|
not 2 <= ciphertext[1] < self.prime_modulus):
|
|
264
387
|
raise base.InputError(f'invalid message: {ciphertext=}')
|
|
265
388
|
# decrypt
|
|
266
|
-
csi: int =
|
|
267
|
-
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
|
|
268
391
|
return (ciphertext[1] * csi) % self.prime_modulus
|
|
269
392
|
|
|
270
|
-
def
|
|
393
|
+
def Decrypt(self, ciphertext: bytes, /, *, associated_data: bytes | None = None) -> bytes:
|
|
394
|
+
"""Decrypt `ciphertext` and return the original `plaintext`.
|
|
395
|
+
|
|
396
|
+
• Let k = ceil(log2(n))/8 be the modulus size in bytes.
|
|
397
|
+
• Split ciphertext in 3 parts: k bytes for ct1, k bytes for ct2, the rest is AES-256-GCM
|
|
398
|
+
• r = ElGamal(ct1, ct2)
|
|
399
|
+
• return AES-256-GCM(key=SHA512(r)[32:], ciphertext,
|
|
400
|
+
associated_data="prefix" + len(aad) + aad +
|
|
401
|
+
Padded(ct1, k) + Padded(ct2, k))
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
ciphertext (bytes): Data to decrypt; see Encrypt() above:
|
|
405
|
+
Padded(ct1, k) + Padded(ct2, k) +
|
|
406
|
+
AES-256-GCM(key=SHA512(r)[32:], plaintext,
|
|
407
|
+
associated_data="prefix" + len(aad) + aad + Padded(ct1, k) + Padded(ct2, k))
|
|
408
|
+
associated_data (bytes, optional): Optional AAD (must match what was used during encrypt)
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
bytes: Decrypted plaintext bytes
|
|
412
|
+
|
|
413
|
+
Raises:
|
|
414
|
+
InputError: invalid inputs
|
|
415
|
+
CryptoError: internal crypto failures, authentication failure, key mismatch, etc
|
|
416
|
+
"""
|
|
417
|
+
k: int = self.modulus_size
|
|
418
|
+
if len(ciphertext) < (k + k + 32):
|
|
419
|
+
raise base.InputError(f'invalid ciphertext length: {len(ciphertext)} ; {k=}')
|
|
420
|
+
# split ciphertext in 3 parts: the first 2k bytes is ct, the rest is AES-256-GCM
|
|
421
|
+
ct1, ct2, aes_ct = ciphertext[:k], ciphertext[k:2 * k], ciphertext[2 * k:]
|
|
422
|
+
r: int = self.RawDecrypt((base.BytesToInt(ct1), base.BytesToInt(ct2)))
|
|
423
|
+
ss: bytes = base.Hash512(base.IntToFixedBytes(r, k))
|
|
424
|
+
aad: bytes = b'' if associated_data is None else associated_data
|
|
425
|
+
aad_prime: bytes = (
|
|
426
|
+
_ELGAMAL_ENCRYPTION_AAD_PREFIX + base.IntToFixedBytes(len(aad), 8) + aad + ct1 + ct2)
|
|
427
|
+
return aes.AESKey(key256=ss[32:]).Decrypt(aes_ct, associated_data=aad_prime)
|
|
428
|
+
|
|
429
|
+
def RawSign(self, message: int, /) -> tuple[int, int]:
|
|
271
430
|
"""Sign `message` with this private key.
|
|
272
431
|
|
|
432
|
+
BEWARE: This is raw El-Gamal, no ECIES-style KEM/DEM padding or validation! This is **NOT** DSA!
|
|
433
|
+
These are pedagogical/raw primitives; do not use for new protocols.
|
|
273
434
|
We explicitly disallow `message` to be zero.
|
|
274
435
|
|
|
275
436
|
Args:
|
|
@@ -285,13 +446,49 @@ class ElGamalPrivateKey(ElGamalPublicKey):
|
|
|
285
446
|
if not 0 < message < self.prime_modulus:
|
|
286
447
|
raise base.InputError(f'invalid message: {message=}')
|
|
287
448
|
# sign
|
|
288
|
-
a
|
|
449
|
+
a: int = 0
|
|
450
|
+
b: int = 0
|
|
451
|
+
p_1: int = self.prime_modulus - 1
|
|
289
452
|
while a < 2 or b < 2:
|
|
290
453
|
ephemeral_key, ephemeral_inv = self._MakeEphemeralKey()
|
|
291
|
-
a =
|
|
454
|
+
a = int(gmpy2.powmod(self.group_base, ephemeral_key, self.prime_modulus)) # type:ignore # pylint:disable=no-member
|
|
292
455
|
b = (ephemeral_inv * ((message - a * self.decrypt_exp) % p_1)) % p_1
|
|
293
456
|
return (a, b)
|
|
294
457
|
|
|
458
|
+
def Sign(self, message: bytes, /, *, associated_data: bytes | None = None) -> bytes:
|
|
459
|
+
"""Sign `message` and return the `signature`.
|
|
460
|
+
|
|
461
|
+
• Let k = ceil(log2(n))/8 be the modulus size in bytes.
|
|
462
|
+
• Pick random salt of 64 bytes
|
|
463
|
+
• s1, s2 = ElGamal(Hash512("prefix" || len(aad) || aad || message || salt))
|
|
464
|
+
• return salt || Padded(s1, k) || Padded(s2, k)
|
|
465
|
+
|
|
466
|
+
This is basically Full-Domain Hash El-Gamal with a 512-bit hash and per-signature salt,
|
|
467
|
+
which is EUF-CMA secure in the ROM. Our domain-separation prefix and explicit AAD
|
|
468
|
+
length prefix are both correct and remove composition/ambiguity pitfalls.
|
|
469
|
+
There are no Bleichenbacher-style issue because we do not expose any padding semantics.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
message (bytes): Data to sign.
|
|
473
|
+
associated_data (bytes, optional): Optional AAD for AEAD modes; must be
|
|
474
|
+
provided again on decrypt
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
bytes: Signature; salt || Padded(s, k) - see above
|
|
478
|
+
|
|
479
|
+
Raises:
|
|
480
|
+
InputError: invalid inputs
|
|
481
|
+
CryptoError: internal crypto failures
|
|
482
|
+
"""
|
|
483
|
+
k: int = self.modulus_size
|
|
484
|
+
if k <= 64:
|
|
485
|
+
raise base.InputError(f'modulus too small for signing operations: {k} bytes')
|
|
486
|
+
salt: bytes = base.RandBytes(64)
|
|
487
|
+
s_int: tuple[int, int] = self.RawSign(self._DomainSeparatedHash(message, associated_data, salt))
|
|
488
|
+
s_bytes: bytes = base.IntToFixedBytes(s_int[0], k) + base.IntToFixedBytes(s_int[1], k)
|
|
489
|
+
assert len(s_bytes) == 2 * k, 'should never happen: s_bytes should be exactly 2k bytes'
|
|
490
|
+
return salt + s_bytes
|
|
491
|
+
|
|
295
492
|
@classmethod
|
|
296
493
|
def New(cls, shared_key: ElGamalSharedPublicKey, /) -> Self:
|
|
297
494
|
"""Make a new private key based on an existing shared public key.
|
|
@@ -318,13 +515,13 @@ class ElGamalPrivateKey(ElGamalPublicKey):
|
|
|
318
515
|
decrypt_exp: int = 0
|
|
319
516
|
while (not 2 < decrypt_exp < shared_key.prime_modulus - 1 or
|
|
320
517
|
decrypt_exp == shared_key.group_base):
|
|
321
|
-
decrypt_exp = base.RandBits(bit_length
|
|
518
|
+
decrypt_exp = base.RandBits(bit_length)
|
|
322
519
|
# make the object
|
|
323
520
|
return cls(
|
|
324
521
|
prime_modulus=shared_key.prime_modulus,
|
|
325
522
|
group_base=shared_key.group_base,
|
|
326
|
-
individual_base=
|
|
327
|
-
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)),
|
|
328
525
|
decrypt_exp=decrypt_exp)
|
|
329
526
|
except base.InputError as err:
|
|
330
527
|
failures += 1
|