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/elgamal.py CHANGED
@@ -23,8 +23,9 @@ import logging
23
23
  # import pdb
24
24
  from typing import Self
25
25
 
26
- from . import base
27
- from . import modmath
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 raw El-Gamal, no ECIES-style KEM/DEM padding or validation! This is **NOT** DSA!
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
- return cls(
93
- prime_modulus=modmath.NBitRandomPrime(bit_length),
94
- group_base=base.RandBits(bit_length - 1),
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 raw El-Gamal, no ECIES-style KEM/DEM padding or validation! This is **NOT** DSA!
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 (f'ElGamalPublicKey({super(ElGamalPublicKey, self).__str__()}, ' # pylint: disable=super-with-arguments
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 - 1)
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 Encrypt(self, message: int, /) -> tuple[int, int]:
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
- ephemeral_key: int = self._MakeEphemeralKey()[0]
168
- a, b = 0, 0
204
+ a: int = 0
205
+ b: int = 0
169
206
  while a < 2 or b < 2:
170
- a = modmath.ModExp(self.group_base, ephemeral_key, self.prime_modulus)
171
- s: int = modmath.ModExp(self.individual_base, ephemeral_key, self.prime_modulus)
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 VerifySignature(self, message: int, signature: tuple[int, int], /) -> bool:
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 = modmath.ModExp(self.group_base, message, self.prime_modulus)
198
- b: int = modmath.ModExp(signature[0], signature[1], self.prime_modulus)
199
- 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
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 raw El-Gamal, no ECIES-style KEM/DEM padding or validation! This is **NOT** DSA!
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 modmath.ModExp(
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 (f'ElGamalPrivateKey({super(ElGamalPrivateKey, self).__str__()}, ' # pylint: disable=super-with-arguments
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 Decrypt(self, ciphertext: tuple[int, int], /) -> int:
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 = modmath.ModExp(
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 Sign(self, message: int, /) -> tuple[int, int]:
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, b, p_1 = 0, 0, self.prime_modulus - 1
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 = 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
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 - 1)
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=modmath.ModExp(
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