transcrypto 1.0.3__py3-none-any.whl → 1.1.1__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/dsa.py ADDED
@@ -0,0 +1,336 @@
1
+ #!/usr/bin/env python3
2
+ #
3
+ # Copyright 2025 Daniel Balparda (balparda@github.com) - Apache-2.0 license
4
+ #
5
+ """Balparda's TransCrypto DSA (Digital Signature Algorithm) library.
6
+
7
+ <https://en.wikipedia.org/wiki/Digital_Signature_Algorithm>
8
+
9
+ BEWARE: For now, this implementation is raw DSA, no padding, no hash!
10
+ In the future we will design a proper DSA+Hash implementation.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import dataclasses
16
+ import logging
17
+ # import pdb
18
+ from typing import Self
19
+
20
+ from . import base
21
+ from . import modmath
22
+
23
+ __author__ = 'balparda@github.com'
24
+ __version__: str = base.__version__ # version comes from base!
25
+ __version_tuple__: tuple[int, ...] = base.__version_tuple__
26
+
27
+
28
+ _PRIME_MULTIPLE_SEARCH = 30
29
+ _MAX_KEY_GENERATION_FAILURES = 15
30
+
31
+
32
+ def NBitRandomDSAPrimes(p_bits: int, q_bits: int, /) -> tuple[int, int, int]:
33
+ """Generates 2 random DSA primes p & q with `x_bits` size and (p-1)%q==0.
34
+
35
+ Args:
36
+ p_bits (int): Number of guaranteed bits in `p` prime representation,
37
+ p_bits ≥ q_bits + 11
38
+ q_bits (int): Number of guaranteed bits in `q` prime representation, ≥ 11
39
+
40
+ Returns:
41
+ random primes tuple (p, q, m), with p-1 a random multiple m of q, such
42
+ that p % q == 1 and m == (p - 1) // q
43
+
44
+ Raises:
45
+ InputError: invalid inputs
46
+ """
47
+ # test inputs
48
+ if q_bits < 11:
49
+ raise base.InputError(f'invalid q_bits length: {q_bits=}')
50
+ if p_bits < q_bits + 11:
51
+ raise base.InputError(f'invalid p_bits length: {p_bits=}')
52
+ # make q
53
+ q = modmath.NBitRandomPrime(q_bits)
54
+ # find range of multiples to use
55
+ min_p, max_p = 2 ** (p_bits - 1), 2 ** p_bits - 1
56
+ min_m, max_m = min_p // q + 2, max_p // q - 2
57
+ assert max_m - min_m > 1000 # make sure we'll have options!
58
+ # start searching from a random multiple
59
+ failures: int = 0
60
+ while True:
61
+ # try searching starting here
62
+ m: int = base.RandInt(min_m, max_m)
63
+ for _ in range(_PRIME_MULTIPLE_SEARCH):
64
+ p: int = q * m + 1
65
+ if p >= max_p:
66
+ break
67
+ if modmath.IsPrime(p):
68
+ return (p, q, m) # found a suitable prime set!
69
+ m += 1 # next multiple
70
+ # after _PRIME_MULTIPLE_SEARCH we declare this range failed
71
+ failures += 1
72
+ if failures >= _MAX_KEY_GENERATION_FAILURES:
73
+ raise base.CryptoError(f'failed primes generation {failures} times')
74
+ logging.warning(f'failed primes search: {failures}')
75
+
76
+
77
+ @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
78
+ class DSASharedPublicKey(base.CryptoKey):
79
+ """DSA shared public key. This key can be shared by a group.
80
+
81
+ BEWARE: This is raw DSA, no ECDSA/EdDSA padding, no hash, no validation!
82
+ These are pedagogical/raw primitives; do not use for new protocols.
83
+ No measures are taken here to prevent timing attacks.
84
+
85
+ Attributes:
86
+ prime_modulus (int): prime modulus (p), > prime_seed
87
+ prime_seed (int): prime seed (q), ≥ 7
88
+ group_base (int): shared encryption group public base, 3 ≤ g < prime_modulus
89
+ """
90
+
91
+ prime_modulus: int
92
+ prime_seed: int
93
+ group_base: int
94
+
95
+ def __post_init__(self) -> None:
96
+ """Check data.
97
+
98
+ Raises:
99
+ InputError: invalid inputs
100
+ """
101
+ super(DSASharedPublicKey, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
102
+ if self.prime_seed < 7 or not modmath.IsPrime(self.prime_seed):
103
+ raise base.InputError(f'invalid prime_seed: {self}')
104
+ if (self.prime_modulus <= self.prime_seed or
105
+ self.prime_modulus % self.prime_seed != 1 or
106
+ not modmath.IsPrime(self.prime_modulus)):
107
+ raise base.InputError(f'invalid prime_modulus: {self}')
108
+ if (not 2 < self.group_base < self.prime_modulus or
109
+ self.group_base == self.prime_seed):
110
+ raise base.InputError(f'invalid group_base: {self}')
111
+
112
+ def __str__(self) -> str:
113
+ """Safe string representation of the DSASharedPublicKey.
114
+
115
+ Returns:
116
+ string representation of DSASharedPublicKey
117
+ """
118
+ return ('DSASharedPublicKey('
119
+ f'prime_modulus={base.IntToEncoded(self.prime_modulus)}, '
120
+ f'prime_seed={base.IntToEncoded(self.prime_seed)}, '
121
+ f'group_base={base.IntToEncoded(self.group_base)})')
122
+
123
+ @classmethod
124
+ def NewShared(cls, p_bits: int, q_bits: int, /) -> Self:
125
+ """Make a new shared public key of `bit_length` bits.
126
+
127
+ Args:
128
+ p_bits (int): Number of guaranteed bits in `p` prime representation,
129
+ p_bits ≥ q_bits + 11
130
+ q_bits (int): Number of guaranteed bits in `q` prime representation, ≥ 11
131
+
132
+ Returns:
133
+ DSASharedPublicKey object ready for use
134
+
135
+ Raises:
136
+ InputError: invalid inputs
137
+ """
138
+ # test inputs and generate primes
139
+ p, q, m = NBitRandomDSAPrimes(p_bits, q_bits)
140
+ # generate random number, create object (should never fail)
141
+ g: int = 0
142
+ while g < 2:
143
+ h: int = base.RandBits(p_bits - 1)
144
+ g = modmath.ModExp(h, m, p)
145
+ return cls(prime_modulus=p, prime_seed=q, group_base=g)
146
+
147
+
148
+ @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
149
+ class DSAPublicKey(DSASharedPublicKey):
150
+ """DSA public key. This is an individual public key.
151
+
152
+ BEWARE: This is raw DSA, no ECDSA/EdDSA padding, no hash, no validation!
153
+ These are pedagogical/raw primitives; do not use for new protocols.
154
+ No measures are taken here to prevent timing attacks.
155
+
156
+ Attributes:
157
+ individual_base (int): individual encryption public base, 3 ≤ i < prime_modulus
158
+ """
159
+
160
+ individual_base: int
161
+
162
+ def __post_init__(self) -> None:
163
+ """Check data.
164
+
165
+ Raises:
166
+ InputError: invalid inputs
167
+ """
168
+ super(DSAPublicKey, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
169
+ if (not 2 < self.individual_base < self.prime_modulus or
170
+ self.individual_base in (self.group_base, self.prime_seed)):
171
+ raise base.InputError(f'invalid individual_base: {self}')
172
+
173
+ def __str__(self) -> str:
174
+ """Safe string representation of the DSAPublicKey.
175
+
176
+ Returns:
177
+ string representation of DSAPublicKey
178
+ """
179
+ return (f'DSAPublicKey({super(DSAPublicKey, self).__str__()}, ' # pylint: disable=super-with-arguments
180
+ f'individual_base={base.IntToEncoded(self.individual_base)})')
181
+
182
+ def _MakeEphemeralKey(self) -> tuple[int, int]:
183
+ """Make an ephemeral key adequate to be used with El-Gamal.
184
+
185
+ Returns:
186
+ (key, key_inverse), where 3 ≤ k < p_seed and (k*i) % p_seed == 1
187
+ """
188
+ ephemeral_key: int = 0
189
+ bit_length: int = self.prime_seed.bit_length()
190
+ while (not 2 < ephemeral_key < self.prime_seed or
191
+ ephemeral_key in (self.group_base, self.individual_base)):
192
+ ephemeral_key = base.RandBits(bit_length - 1)
193
+ return (ephemeral_key, modmath.ModInv(ephemeral_key, self.prime_seed))
194
+
195
+ def VerifySignature(self, message: int, signature: tuple[int, int], /) -> bool:
196
+ """Verify a signature. True if OK; False if failed verification.
197
+
198
+ We explicitly disallow `message` to be zero.
199
+
200
+ Args:
201
+ message (int): message that was signed by key owner, 0 < m < prime_seed
202
+ signature (tuple[int, int]): signature, 2 ≤ s1,s2 < prime_seed
203
+
204
+ Returns:
205
+ True if signature is valid, False otherwise
206
+
207
+ Raises:
208
+ InputError: invalid inputs
209
+ """
210
+ # test inputs
211
+ if not 0 < message < self.prime_seed:
212
+ raise base.InputError(f'invalid message: {message=}')
213
+ if (not 2 <= signature[0] < self.prime_seed or
214
+ not 2 <= signature[1] < self.prime_seed):
215
+ raise base.InputError(f'invalid signature: {signature=}')
216
+ # verify
217
+ inv: int = modmath.ModInv(signature[1], self.prime_seed)
218
+ a: int = modmath.ModExp(
219
+ self.group_base, (message * inv) % self.prime_seed, self.prime_modulus)
220
+ b: int = modmath.ModExp(
221
+ self.individual_base, (signature[0] * inv) % self.prime_seed, self.prime_modulus)
222
+ return ((a * b) % self.prime_modulus) % self.prime_seed == signature[0]
223
+
224
+ @classmethod
225
+ def Copy(cls, other: DSAPublicKey, /) -> Self:
226
+ """Initialize a public key by taking the public parts of a public/private key."""
227
+ return cls(
228
+ prime_modulus=other.prime_modulus,
229
+ prime_seed=other.prime_seed,
230
+ group_base=other.group_base,
231
+ individual_base=other.individual_base)
232
+
233
+
234
+ @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
235
+ class DSAPrivateKey(DSAPublicKey):
236
+ """DSA private key.
237
+
238
+ BEWARE: This is raw DSA, no ECDSA/EdDSA padding, no hash, no validation!
239
+ These are pedagogical/raw primitives; do not use for new protocols.
240
+ No measures are taken here to prevent timing attacks.
241
+
242
+ Attributes:
243
+ decrypt_exp (int): individual decryption exponent, 3 ≤ i < prime_modulus
244
+ """
245
+
246
+ decrypt_exp: int
247
+
248
+ def __post_init__(self) -> None:
249
+ """Check data.
250
+
251
+ Raises:
252
+ InputError: invalid inputs
253
+ CryptoError: modulus math is inconsistent with values
254
+ """
255
+ super(DSAPrivateKey, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
256
+ if (not 2 < self.decrypt_exp < self.prime_seed or
257
+ self.decrypt_exp in (self.group_base, self.individual_base)):
258
+ raise base.InputError(f'invalid decrypt_exp: {self}')
259
+ if modmath.ModExp(
260
+ self.group_base, self.decrypt_exp, self.prime_modulus) != self.individual_base:
261
+ raise base.CryptoError(f'inconsistent g**d % p == i: {self}')
262
+
263
+ def __str__(self) -> str:
264
+ """Safe (no secrets) string representation of the DSAPrivateKey.
265
+
266
+ Returns:
267
+ string representation of DSAPrivateKey without leaking secrets
268
+ """
269
+ return (f'DSAPrivateKey({super(DSAPrivateKey, self).__str__()}, ' # pylint: disable=super-with-arguments
270
+ f'decrypt_exp={base.ObfuscateSecret(self.decrypt_exp)})')
271
+
272
+ def Sign(self, message: int, /) -> tuple[int, int]:
273
+ """Sign `message` with this private key.
274
+
275
+ We explicitly disallow `message` to be zero.
276
+
277
+ Args:
278
+ message (int): message to sign, 1 ≤ m < prime_seed
279
+
280
+ Returns:
281
+ signed message tuple ((int, int), 2 ≤ s1,s2 < prime_seed
282
+
283
+ Raises:
284
+ InputError: invalid inputs
285
+ """
286
+ # test inputs
287
+ if not 0 < message < self.prime_seed:
288
+ raise base.InputError(f'invalid message: {message=}')
289
+ # sign
290
+ a, b = 0, 0
291
+ while a < 2 or b < 2:
292
+ ephemeral_key, ephemeral_inv = self._MakeEphemeralKey()
293
+ a = modmath.ModExp(self.group_base, ephemeral_key, self.prime_modulus) % self.prime_seed
294
+ b = (ephemeral_inv * ((message + a * self.decrypt_exp) % self.prime_seed)) % self.prime_seed
295
+ return (a, b)
296
+
297
+ @classmethod
298
+ def New(cls, shared_key: DSASharedPublicKey, /) -> Self:
299
+ """Make a new private key based on an existing shared public key.
300
+
301
+ Args:
302
+ shared_key (DSASharedPublicKey): shared public key
303
+
304
+ Returns:
305
+ DSAPrivateKey object ready for use
306
+
307
+ Raises:
308
+ InputError: invalid inputs
309
+ CryptoError: failed generation
310
+ """
311
+ # test inputs
312
+ bit_length: int = shared_key.prime_seed.bit_length()
313
+ if bit_length < 11:
314
+ raise base.InputError(f'invalid q_bit length: {bit_length=}')
315
+ # loop until we have an object
316
+ failures: int = 0
317
+ while True:
318
+ try:
319
+ # generate private key differing from group_base
320
+ decrypt_exp: int = 0
321
+ while (not 2 < decrypt_exp < shared_key.prime_seed - 1 or
322
+ decrypt_exp == shared_key.group_base):
323
+ decrypt_exp = base.RandBits(bit_length - 1)
324
+ # make the object
325
+ return cls(
326
+ prime_modulus=shared_key.prime_modulus,
327
+ prime_seed=shared_key.prime_seed,
328
+ group_base=shared_key.group_base,
329
+ individual_base=modmath.ModExp(
330
+ shared_key.group_base, decrypt_exp, shared_key.prime_modulus),
331
+ decrypt_exp=decrypt_exp)
332
+ except base.InputError as err:
333
+ failures += 1
334
+ if failures >= _MAX_KEY_GENERATION_FAILURES:
335
+ raise base.CryptoError(f'failed key generation {failures} times') from err
336
+ logging.warning(err)
transcrypto/elgamal.py ADDED
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env python3
2
+ #
3
+ # Copyright 2025 Daniel Balparda (balparda@github.com) - Apache-2.0 license
4
+ #
5
+ """Balparda's TransCrypto El-Gamal library.
6
+
7
+ <https://en.wikipedia.org/wiki/ElGamal_encryption>
8
+ <https://en.wikipedia.org/wiki/ElGamal_signature_scheme>
9
+
10
+ ATTENTION: This is pure El-Gamal, **NOT** DSA (Digital Signature Algorithm).
11
+ For DSA, see the dsa.py library.
12
+
13
+ ALSO: ElGamal encryption is unconditionally malleable, and therefore is
14
+ not secure under chosen ciphertext attack. For example, given an encryption
15
+ `(c1,c2)` of some (possibly unknown) message `m`, one can easily construct
16
+ a valid encryption `(c1,2*c2)` of the message `2*m`.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import dataclasses
22
+ import logging
23
+ # import pdb
24
+ from typing import Self
25
+
26
+ from . import base
27
+ from . import modmath
28
+
29
+ __author__ = 'balparda@github.com'
30
+ __version__: str = base.__version__ # version comes from base!
31
+ __version_tuple__: tuple[int, ...] = base.__version_tuple__
32
+
33
+
34
+ _MAX_KEY_GENERATION_FAILURES = 15
35
+
36
+
37
+ @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
38
+ class ElGamalSharedPublicKey(base.CryptoKey):
39
+ """El-Gamal shared public key. This key can be shared by a group.
40
+
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.
44
+
45
+ Attributes:
46
+ prime_modulus (int): prime modulus, ≥ 7
47
+ group_base (int): shared encryption group public base, 3 ≤ g < prime_modulus
48
+ """
49
+
50
+ prime_modulus: int
51
+ group_base: int
52
+
53
+ def __post_init__(self) -> None:
54
+ """Check data.
55
+
56
+ Raises:
57
+ InputError: invalid inputs
58
+ """
59
+ super(ElGamalSharedPublicKey, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
60
+ if self.prime_modulus < 7 or not modmath.IsPrime(self.prime_modulus):
61
+ raise base.InputError(f'invalid prime_modulus: {self}')
62
+ if not 2 < self.group_base < self.prime_modulus - 1:
63
+ raise base.InputError(f'invalid group_base: {self}')
64
+
65
+ def __str__(self) -> str:
66
+ """Safe string representation of the ElGamalSharedPublicKey.
67
+
68
+ Returns:
69
+ string representation of ElGamalSharedPublicKey
70
+ """
71
+ return ('ElGamalSharedPublicKey('
72
+ f'prime_modulus={base.IntToEncoded(self.prime_modulus)}, '
73
+ f'group_base={base.IntToEncoded(self.group_base)})')
74
+
75
+ @classmethod
76
+ def NewShared(cls, bit_length: int, /) -> Self:
77
+ """Make a new shared public key of `bit_length` bits.
78
+
79
+ Args:
80
+ bit_length (int): number of bits in the prime modulus, ≥ 11
81
+
82
+ Returns:
83
+ ElGamalSharedPublicKey object ready for use
84
+
85
+ Raises:
86
+ InputError: invalid inputs
87
+ """
88
+ # test inputs
89
+ if bit_length < 11:
90
+ raise base.InputError(f'invalid bit length: {bit_length=}')
91
+ # 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
+ )
96
+
97
+
98
+ @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
99
+ class ElGamalPublicKey(ElGamalSharedPublicKey):
100
+ """El-Gamal public key. This is an individual public key.
101
+
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.
105
+
106
+ Attributes:
107
+ individual_base (int): individual encryption public base, 3 ≤ i < prime_modulus
108
+ """
109
+
110
+ individual_base: int
111
+
112
+ def __post_init__(self) -> None:
113
+ """Check data.
114
+
115
+ Raises:
116
+ InputError: invalid inputs
117
+ """
118
+ super(ElGamalPublicKey, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
119
+ if (not 2 < self.individual_base < self.prime_modulus - 1 or
120
+ self.individual_base == self.group_base):
121
+ raise base.InputError(f'invalid individual_base: {self}')
122
+
123
+ def __str__(self) -> str:
124
+ """Safe string representation of the ElGamalPublicKey.
125
+
126
+ Returns:
127
+ string representation of ElGamalPublicKey
128
+ """
129
+ return (f'ElGamalPublicKey({super(ElGamalPublicKey, self).__str__()}, ' # pylint: disable=super-with-arguments
130
+ f'individual_base={base.IntToEncoded(self.individual_base)})')
131
+
132
+ def _MakeEphemeralKey(self) -> tuple[int, int]:
133
+ """Make an ephemeral key adequate to be used with El-Gamal.
134
+
135
+ Returns:
136
+ (key, key_inverse), where 2 ≤ k < modulus - 1 and
137
+ GCD(k, modulus - 1) == 1 and (k*i) % (p-1) == 1
138
+ """
139
+ ephemeral_key: int = 0
140
+ p_1: int = self.prime_modulus - 1
141
+ bit_length: int = self.prime_modulus.bit_length()
142
+ while (not 1 < ephemeral_key < p_1 or
143
+ ephemeral_key in (self.group_base, self.individual_base)):
144
+ ephemeral_key = base.RandBits(bit_length - 1)
145
+ if base.GCD(ephemeral_key, p_1) != 1:
146
+ ephemeral_key = 0 # we have to try again
147
+ return (ephemeral_key, modmath.ModInv(ephemeral_key, p_1))
148
+
149
+ def Encrypt(self, message: int, /) -> tuple[int, int]:
150
+ """Encrypt `message` with this public key.
151
+
152
+ We explicitly disallow `message` to be zero.
153
+
154
+ Args:
155
+ message (int): message to encrypt, 1 ≤ m < modulus
156
+
157
+ Returns:
158
+ ciphertext message tuple ((int, int), 2 ≤ c1,c2 < modulus)
159
+
160
+ Raises:
161
+ InputError: invalid inputs
162
+ """
163
+ # test inputs
164
+ if not 0 < message < self.prime_modulus:
165
+ raise base.InputError(f'invalid message: {message=}')
166
+ # encrypt
167
+ ephemeral_key: int = self._MakeEphemeralKey()[0]
168
+ a, b = 0, 0
169
+ 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)
172
+ b = (message * s) % self.prime_modulus
173
+ return (a, b)
174
+
175
+ def VerifySignature(self, message: int, signature: tuple[int, int], /) -> bool:
176
+ """Verify a signature. True if OK; False if failed verification.
177
+
178
+ We explicitly disallow `message` to be zero.
179
+
180
+ Args:
181
+ message (int): message that was signed by key owner, 0 < m < modulus
182
+ signature (tuple[int, int]): signature, 2 ≤ s1 < modulus, 2 ≤ s2 < modulus-1
183
+
184
+ Returns:
185
+ True if signature is valid, False otherwise
186
+
187
+ Raises:
188
+ InputError: invalid inputs
189
+ """
190
+ # test inputs
191
+ if not 0 < message < self.prime_modulus:
192
+ raise base.InputError(f'invalid message: {message=}')
193
+ if (not 2 <= signature[0] < self.prime_modulus or
194
+ not 2 <= signature[1] < self.prime_modulus - 1):
195
+ raise base.InputError(f'invalid signature: {signature=}')
196
+ # 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)
200
+ return a == (b * c) % self.prime_modulus
201
+
202
+ @classmethod
203
+ def Copy(cls, other: ElGamalPublicKey, /) -> Self:
204
+ """Initialize a public key by taking the public parts of a public/private key."""
205
+ return cls(
206
+ prime_modulus=other.prime_modulus,
207
+ group_base=other.group_base,
208
+ individual_base=other.individual_base)
209
+
210
+
211
+ @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
212
+ class ElGamalPrivateKey(ElGamalPublicKey):
213
+ """El-Gamal private key.
214
+
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.
218
+
219
+ Attributes:
220
+ decrypt_exp (int): individual decryption exponent, 3 ≤ i < prime_modulus
221
+ """
222
+
223
+ decrypt_exp: int
224
+
225
+ def __post_init__(self) -> None:
226
+ """Check data.
227
+
228
+ Raises:
229
+ InputError: invalid inputs
230
+ CryptoError: modulus math is inconsistent with values
231
+ """
232
+ super(ElGamalPrivateKey, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
233
+ if (not 2 < self.decrypt_exp < self.prime_modulus - 1 or
234
+ self.decrypt_exp in (self.group_base, self.individual_base)):
235
+ 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:
238
+ raise base.CryptoError(f'inconsistent g**e % p == i: {self}')
239
+
240
+ def __str__(self) -> str:
241
+ """Safe (no secrets) string representation of the ElGamalPrivateKey.
242
+
243
+ Returns:
244
+ string representation of ElGamalPrivateKey without leaking secrets
245
+ """
246
+ return (f'ElGamalPrivateKey({super(ElGamalPrivateKey, self).__str__()}, ' # pylint: disable=super-with-arguments
247
+ f'decrypt_exp={base.ObfuscateSecret(self.decrypt_exp)})')
248
+
249
+ def Decrypt(self, ciphertext: tuple[int, int], /) -> int:
250
+ """Decrypt `ciphertext` tuple with this private key.
251
+
252
+ Args:
253
+ ciphertext (tuple[int, int]): ciphertext to decrypt, 0 ≤ c1,c2 < modulus
254
+
255
+ Returns:
256
+ decrypted message (int, 1 ≤ m < modulus)
257
+
258
+ Raises:
259
+ InputError: invalid inputs
260
+ """
261
+ # test inputs
262
+ if (not 2 <= ciphertext[0] < self.prime_modulus or
263
+ not 2 <= ciphertext[1] < self.prime_modulus):
264
+ raise base.InputError(f'invalid message: {ciphertext=}')
265
+ # decrypt
266
+ csi: int = modmath.ModExp(
267
+ ciphertext[0], self.prime_modulus - 1 - self.decrypt_exp, self.prime_modulus)
268
+ return (ciphertext[1] * csi) % self.prime_modulus
269
+
270
+ def Sign(self, message: int, /) -> tuple[int, int]:
271
+ """Sign `message` with this private key.
272
+
273
+ We explicitly disallow `message` to be zero.
274
+
275
+ Args:
276
+ message (int): message to sign, 1 ≤ m < modulus
277
+
278
+ Returns:
279
+ signed message tuple ((int, int), 2 ≤ s1 < modulus, 2 ≤ s2 < modulus-1)
280
+
281
+ Raises:
282
+ InputError: invalid inputs
283
+ """
284
+ # test inputs
285
+ if not 0 < message < self.prime_modulus:
286
+ raise base.InputError(f'invalid message: {message=}')
287
+ # sign
288
+ a, b, p_1 = 0, 0, self.prime_modulus - 1
289
+ while a < 2 or b < 2:
290
+ ephemeral_key, ephemeral_inv = self._MakeEphemeralKey()
291
+ a = modmath.ModExp(self.group_base, ephemeral_key, self.prime_modulus)
292
+ b = (ephemeral_inv * ((message - a * self.decrypt_exp) % p_1)) % p_1
293
+ return (a, b)
294
+
295
+ @classmethod
296
+ def New(cls, shared_key: ElGamalSharedPublicKey, /) -> Self:
297
+ """Make a new private key based on an existing shared public key.
298
+
299
+ Args:
300
+ shared_key (ElGamalSharedPublicKey): shared public key
301
+
302
+ Returns:
303
+ ElGamalPrivateKey object ready for use
304
+
305
+ Raises:
306
+ InputError: invalid inputs
307
+ CryptoError: failed generation
308
+ """
309
+ # test inputs
310
+ bit_length: int = shared_key.prime_modulus.bit_length()
311
+ if bit_length < 11:
312
+ raise base.InputError(f'invalid bit length: {bit_length=}')
313
+ # loop until we have an object
314
+ failures: int = 0
315
+ while True:
316
+ try:
317
+ # generate private key differing from group_base
318
+ decrypt_exp: int = 0
319
+ while (not 2 < decrypt_exp < shared_key.prime_modulus - 1 or
320
+ decrypt_exp == shared_key.group_base):
321
+ decrypt_exp = base.RandBits(bit_length - 1)
322
+ # make the object
323
+ return cls(
324
+ prime_modulus=shared_key.prime_modulus,
325
+ group_base=shared_key.group_base,
326
+ individual_base=modmath.ModExp(
327
+ shared_key.group_base, decrypt_exp, shared_key.prime_modulus),
328
+ decrypt_exp=decrypt_exp)
329
+ except base.InputError as err:
330
+ failures += 1
331
+ if failures >= _MAX_KEY_GENERATION_FAILURES:
332
+ raise base.CryptoError(f'failed key generation {failures} times') from err
333
+ logging.warning(err)