transcrypto 1.0.2__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/aes.py +257 -0
- transcrypto/base.py +1018 -0
- transcrypto/dsa.py +336 -0
- transcrypto/elgamal.py +333 -0
- transcrypto/modmath.py +535 -0
- transcrypto/rsa.py +416 -0
- transcrypto/sss.py +299 -0
- transcrypto/transcrypto.py +1367 -276
- transcrypto-1.1.1.dist-info/METADATA +2257 -0
- transcrypto-1.1.1.dist-info/RECORD +15 -0
- transcrypto-1.0.2.dist-info/METADATA +0 -147
- transcrypto-1.0.2.dist-info/RECORD +0 -8
- {transcrypto-1.0.2.dist-info → transcrypto-1.1.1.dist-info}/WHEEL +0 -0
- {transcrypto-1.0.2.dist-info → transcrypto-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {transcrypto-1.0.2.dist-info → transcrypto-1.1.1.dist-info}/top_level.txt +0 -0
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)
|