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/rsa.py
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025 Daniel Balparda (balparda@github.com) - Apache-2.0 license
|
|
4
|
+
#
|
|
5
|
+
"""Balparda's TransCrypto RSA (Rivest-Shamir-Adleman) library.
|
|
6
|
+
|
|
7
|
+
<https://en.wikipedia.org/wiki/RSA_cryptosystem>
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import dataclasses
|
|
13
|
+
import logging
|
|
14
|
+
# import pdb
|
|
15
|
+
from typing import Self
|
|
16
|
+
|
|
17
|
+
from . import base
|
|
18
|
+
from . import modmath
|
|
19
|
+
|
|
20
|
+
__author__ = 'balparda@github.com'
|
|
21
|
+
__version__: str = base.__version__ # version comes from base!
|
|
22
|
+
__version_tuple__: tuple[int, ...] = base.__version_tuple__
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_SMALL_ENCRYPTION_EXPONENT = 7
|
|
26
|
+
_BIG_ENCRYPTION_EXPONENT = 2 ** 16 + 1 # 65537
|
|
27
|
+
|
|
28
|
+
_MAX_KEY_GENERATION_FAILURES = 15
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
32
|
+
class RSAPublicKey(base.CryptoKey):
|
|
33
|
+
"""RSA (Rivest-Shamir-Adleman) key, with the public part of the key.
|
|
34
|
+
|
|
35
|
+
BEWARE: This is raw RSA, no OAEP or PSS padding or validation!
|
|
36
|
+
These are pedagogical/raw primitives; do not use for new protocols.
|
|
37
|
+
No measures are taken here to prevent timing attacks.
|
|
38
|
+
|
|
39
|
+
By default and deliberate choice the encryption exponent will be either 7 or 65537,
|
|
40
|
+
depending on the size of phi=(p-1)*(q-1). If phi allows it the larger one will be chosen
|
|
41
|
+
to avoid Coppersmith attacks.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
public_modulus (int): modulus (p * q), ≥ 6
|
|
45
|
+
encrypt_exp (int): encryption exponent, 3 ≤ e < modulus, (e * decrypt) % ((p-1) * (q-1)) == 1
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
public_modulus: int
|
|
49
|
+
encrypt_exp: int
|
|
50
|
+
|
|
51
|
+
def __post_init__(self) -> None:
|
|
52
|
+
"""Check data.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
InputError: invalid inputs
|
|
56
|
+
"""
|
|
57
|
+
super(RSAPublicKey, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
|
|
58
|
+
if self.public_modulus < 6 or modmath.IsPrime(self.public_modulus):
|
|
59
|
+
# only a full factors check can prove modulus is product of only 2 primes, which is impossible
|
|
60
|
+
# to do for large numbers here; the private key checks the relationship though
|
|
61
|
+
raise base.InputError(f'invalid public_modulus: {self}')
|
|
62
|
+
if not 2 < self.encrypt_exp < self.public_modulus or not modmath.IsPrime(self.encrypt_exp):
|
|
63
|
+
# technically, encrypt_exp < phi, but again the private key tests for this explicitly
|
|
64
|
+
raise base.InputError(f'invalid encrypt_exp: {self}')
|
|
65
|
+
|
|
66
|
+
def __str__(self) -> str:
|
|
67
|
+
"""Safe string representation of the RSAPublicKey.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
string representation of RSAPublicKey
|
|
71
|
+
"""
|
|
72
|
+
return ('RSAPublicKey('
|
|
73
|
+
f'public_modulus={base.IntToEncoded(self.public_modulus)}, '
|
|
74
|
+
f'encrypt_exp={base.IntToEncoded(self.encrypt_exp)})')
|
|
75
|
+
|
|
76
|
+
def Encrypt(self, message: int, /) -> int:
|
|
77
|
+
"""Encrypt `message` with this public key.
|
|
78
|
+
|
|
79
|
+
We explicitly disallow `message` to be zero.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
message (int): message to encrypt, 1 ≤ m < modulus
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
ciphertext message (int, 1 ≤ c < modulus) = (m ** encrypt_exp) mod modulus
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
InputError: invalid inputs
|
|
89
|
+
"""
|
|
90
|
+
# test inputs
|
|
91
|
+
if not 0 < message < self.public_modulus:
|
|
92
|
+
raise base.InputError(f'invalid message: {message=}')
|
|
93
|
+
# encrypt
|
|
94
|
+
return modmath.ModExp(message, self.encrypt_exp, self.public_modulus)
|
|
95
|
+
|
|
96
|
+
def VerifySignature(self, message: int, signature: int, /) -> bool:
|
|
97
|
+
"""Verify a signature. True if OK; False if failed verification.
|
|
98
|
+
|
|
99
|
+
We explicitly disallow `message` to be zero.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
message (int): message that was signed by key owner, 1 ≤ m < modulus
|
|
103
|
+
signature (int): signature, 1 ≤ s < modulus
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
True if signature is valid, False otherwise;
|
|
107
|
+
(signature ** encrypt_exp) mod modulus == message
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
InputError: invalid inputs
|
|
111
|
+
"""
|
|
112
|
+
return self.Encrypt(signature) == message
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def Copy(cls, other: RSAPublicKey, /) -> Self:
|
|
116
|
+
"""Initialize a public key by taking the public parts of a public/private key."""
|
|
117
|
+
return cls(public_modulus=other.public_modulus, encrypt_exp=other.encrypt_exp)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
121
|
+
class RSAObfuscationPair(RSAPublicKey):
|
|
122
|
+
"""RSA (Rivest-Shamir-Adleman) obfuscation pair for a public key.
|
|
123
|
+
|
|
124
|
+
BEWARE: This only works on raw RSA, no OAEP or PSS padding or validation!
|
|
125
|
+
These are pedagogical/raw primitives; do not use for new protocols.
|
|
126
|
+
No measures are taken here to prevent timing attacks.
|
|
127
|
+
|
|
128
|
+
Attributes:
|
|
129
|
+
random_key (int): random value key, 2 ≤ k < modulus
|
|
130
|
+
key_inverse (int): inverse for `random_key` in relation to the RSA public key, 2 ≤ i < modulus
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
random_key: int
|
|
134
|
+
key_inverse: int
|
|
135
|
+
|
|
136
|
+
def __post_init__(self) -> None:
|
|
137
|
+
"""Check data.
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
InputError: invalid inputs
|
|
141
|
+
CryptoError: modulus math is inconsistent with values
|
|
142
|
+
"""
|
|
143
|
+
super(RSAObfuscationPair, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
|
|
144
|
+
if (not 1 < self.random_key < self.public_modulus or
|
|
145
|
+
not 1 < self.key_inverse < self.public_modulus or
|
|
146
|
+
self.random_key in (self.key_inverse, self.encrypt_exp, self.public_modulus)):
|
|
147
|
+
raise base.InputError(f'invalid keys: {self}')
|
|
148
|
+
if (self.random_key * self.key_inverse) % self.public_modulus != 1:
|
|
149
|
+
raise base.CryptoError(f'inconsistent keys: {self}')
|
|
150
|
+
|
|
151
|
+
def __str__(self) -> str:
|
|
152
|
+
"""Safe (no secrets) string representation of the RSAObfuscationPair.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
string representation of RSAObfuscationPair without leaking secrets
|
|
156
|
+
"""
|
|
157
|
+
return (f'RSAObfuscationPair({super(RSAObfuscationPair, self).__str__()}, ' # pylint: disable=super-with-arguments
|
|
158
|
+
f'random_key={base.ObfuscateSecret(self.random_key)}, '
|
|
159
|
+
f'key_inverse={base.ObfuscateSecret(self.key_inverse)})')
|
|
160
|
+
|
|
161
|
+
def ObfuscateMessage(self, message: int, /) -> int:
|
|
162
|
+
"""Convert message to an obfuscated message to be signed by this key's owner.
|
|
163
|
+
|
|
164
|
+
We explicitly disallow `message` to be zero.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
message (int): message to obfuscate before signature, 1 ≤ m < modulus
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
obfuscated message (int, 1 ≤ o < modulus) = (m * (random_key ** encrypt_exp)) mod modulus
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
InputError: invalid inputs
|
|
174
|
+
"""
|
|
175
|
+
# test inputs
|
|
176
|
+
if not 0 < message < self.public_modulus:
|
|
177
|
+
raise base.InputError(f'invalid message: {message=}')
|
|
178
|
+
# encrypt
|
|
179
|
+
return (message * modmath.ModExp(
|
|
180
|
+
self.random_key, self.encrypt_exp, self.public_modulus)) % self.public_modulus
|
|
181
|
+
|
|
182
|
+
def RevealOriginalSignature(self, message: int, signature: int, /) -> int:
|
|
183
|
+
"""Recover original signature for `message` from obfuscated `signature`.
|
|
184
|
+
|
|
185
|
+
We explicitly disallow `message` to be zero.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
message (int): original message before obfuscation, 1 ≤ m < modulus
|
|
189
|
+
signature (int): signature for obfuscated message (not `message`!), 1 ≤ s < modulus
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
original signature (int, 1 ≤ s < modulus) to `message`;
|
|
193
|
+
signature * key_inverse mod modulus
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
InputError: invalid inputs
|
|
197
|
+
CryptoError: some signatures were invalid (either plain or obfuscated)
|
|
198
|
+
"""
|
|
199
|
+
# verify that obfuscated signature is valid
|
|
200
|
+
obfuscated: int = self.ObfuscateMessage(message)
|
|
201
|
+
if not self.VerifySignature(obfuscated, signature):
|
|
202
|
+
raise base.CryptoError(f'obfuscated message was not signed: {message=} ; {signature=}')
|
|
203
|
+
# compute signature for original message and check it
|
|
204
|
+
original: int = (signature * self.key_inverse) % self.public_modulus
|
|
205
|
+
if not self.VerifySignature(message, original):
|
|
206
|
+
raise base.CryptoError(f'failed signature recovery: {message=} ; {signature=}')
|
|
207
|
+
return original
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def New(cls, key: RSAPublicKey, /) -> Self:
|
|
211
|
+
"""New obfuscation pair for this `key`, respecting the size of the public modulus.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
key (RSAPublicKey): public RSA key to use as base for a new RSAObfuscationPair
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
RSAObfuscationPair object ready for use
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
CryptoError: failed generation
|
|
221
|
+
"""
|
|
222
|
+
# find a suitable random key based on the bit_length
|
|
223
|
+
random_key: int = 0
|
|
224
|
+
key_inverse: int = 0
|
|
225
|
+
failures: int = 0
|
|
226
|
+
while (not random_key or not key_inverse or
|
|
227
|
+
random_key == key.encrypt_exp or
|
|
228
|
+
random_key == key_inverse or
|
|
229
|
+
key_inverse == key.encrypt_exp):
|
|
230
|
+
random_key = base.RandBits(key.public_modulus.bit_length() - 1)
|
|
231
|
+
try:
|
|
232
|
+
key_inverse = modmath.ModInv(random_key, key.public_modulus)
|
|
233
|
+
except modmath.ModularDivideError as err:
|
|
234
|
+
key_inverse = 0
|
|
235
|
+
failures += 1
|
|
236
|
+
if failures >= _MAX_KEY_GENERATION_FAILURES:
|
|
237
|
+
raise base.CryptoError(f'failed key generation {failures} times') from err
|
|
238
|
+
logging.warning(err)
|
|
239
|
+
# build object
|
|
240
|
+
return cls(
|
|
241
|
+
public_modulus=key.public_modulus, encrypt_exp=key.encrypt_exp,
|
|
242
|
+
random_key=random_key, key_inverse=key_inverse)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
246
|
+
class RSAPrivateKey(RSAPublicKey):
|
|
247
|
+
"""RSA (Rivest-Shamir-Adleman) private key.
|
|
248
|
+
|
|
249
|
+
BEWARE: This is raw RSA, no OAEP or PSS padding or validation!
|
|
250
|
+
These are pedagogical/raw primitives; do not use for new protocols.
|
|
251
|
+
No measures are taken here to prevent timing attacks.
|
|
252
|
+
|
|
253
|
+
The attributes modulus_p (p), modulus_q (q) and decrypt_exp (d) are "enough" for a working key,
|
|
254
|
+
but we have the other 3 (remainder_p, remainder_q, q_inverse_p) to speedup decryption/signing
|
|
255
|
+
by a factor of 4 using the Chinese Remainder Theorem.
|
|
256
|
+
|
|
257
|
+
Attributes:
|
|
258
|
+
modulus_p (int): prime number p, ≥ 2
|
|
259
|
+
modulus_q (int): prime number q, ≥ 3 and > p
|
|
260
|
+
decrypt_exp (int): decryption exponent, 2 ≤ d < modulus, (encrypt * d) % ((p-1) * (q-1)) == 1
|
|
261
|
+
remainder_p (int): pre-computed, = d % (p - 1), 2 ≤ r_p < modulus
|
|
262
|
+
remainder_q (int): pre-computed, = d % (q - 1), 2 ≤ r_q < modulus
|
|
263
|
+
q_inverse_p (int): pre-computed, = ModInv(q, p), 2 ≤ q_i_p < modulus
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
modulus_p: int
|
|
267
|
+
modulus_q: int
|
|
268
|
+
decrypt_exp: int
|
|
269
|
+
|
|
270
|
+
remainder_p: int # these 3 are derived from the previous 3 and are used for speedup only!
|
|
271
|
+
remainder_q: int # because of that they will not be printed in __str__()
|
|
272
|
+
q_inverse_p: int
|
|
273
|
+
|
|
274
|
+
def __post_init__(self) -> None:
|
|
275
|
+
"""Check data.
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
InputError: invalid inputs
|
|
279
|
+
CryptoError: modulus math is inconsistent with values
|
|
280
|
+
"""
|
|
281
|
+
super(RSAPrivateKey, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
|
|
282
|
+
phi: int = (self.modulus_p - 1) * (self.modulus_q - 1)
|
|
283
|
+
min_prime_distance: int = 2 ** (self.public_modulus.bit_length() // 3 + 1)
|
|
284
|
+
if (self.modulus_p < 2 or not modmath.IsPrime(self.modulus_p) or # pylint: disable=too-many-boolean-expressions
|
|
285
|
+
self.modulus_q < 3 or not modmath.IsPrime(self.modulus_q) or
|
|
286
|
+
self.modulus_q <= self.modulus_p or
|
|
287
|
+
(self.modulus_q - self.modulus_p) < min_prime_distance or
|
|
288
|
+
self.encrypt_exp in (self.modulus_p, self.modulus_q) or
|
|
289
|
+
self.encrypt_exp >= phi or
|
|
290
|
+
self.decrypt_exp in (self.encrypt_exp, self.modulus_p, self.modulus_q, phi)):
|
|
291
|
+
# encrypt_exp has to be less than phi;
|
|
292
|
+
# if p − q < 2*(n**(1/4)) then solving for p and q is trivial
|
|
293
|
+
raise base.InputError(f'invalid modulus_p or modulus_q: {self}')
|
|
294
|
+
min_decrypt_length: int = self.public_modulus.bit_length() // 2 + 1
|
|
295
|
+
if not (2 ** min_decrypt_length) < self.decrypt_exp < self.public_modulus:
|
|
296
|
+
# if decrypt_exp < public_modulus**(1/4)/3, then decrypt_exp can be computed efficiently
|
|
297
|
+
# from public_modulus and encrypt_exp so we make sure it is larger than public_modulus**(1/2)
|
|
298
|
+
raise base.InputError(f'invalid decrypt_exp: {self}')
|
|
299
|
+
if self.remainder_p < 2 or self.remainder_p < 2 or self.q_inverse_p < 2:
|
|
300
|
+
raise base.InputError(f'trivial remainder_p/remainder_q/q_inverse_p: {self}')
|
|
301
|
+
if self.modulus_p * self.modulus_q != self.public_modulus:
|
|
302
|
+
raise base.CryptoError(f'inconsistent modulus_p * modulus_q: {self}')
|
|
303
|
+
if (self.encrypt_exp * self.decrypt_exp) % phi != 1:
|
|
304
|
+
raise base.CryptoError(f'inconsistent exponents: {self}')
|
|
305
|
+
if (self.remainder_p != self.decrypt_exp % (self.modulus_p - 1) or
|
|
306
|
+
self.remainder_q != self.decrypt_exp % (self.modulus_q - 1) or
|
|
307
|
+
(self.q_inverse_p * self.modulus_q) % self.modulus_p != 1):
|
|
308
|
+
raise base.CryptoError(f'inconsistent speedup remainder_p/remainder_q/q_inverse_p: {self}')
|
|
309
|
+
|
|
310
|
+
def __str__(self) -> str:
|
|
311
|
+
"""Safe (no secrets) string representation of the RSAPrivateKey.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
string representation of RSAPrivateKey without leaking secrets
|
|
315
|
+
"""
|
|
316
|
+
return (f'RSAPrivateKey({super(RSAPrivateKey, self).__str__()}, ' # pylint: disable=super-with-arguments
|
|
317
|
+
f'modulus_p={base.ObfuscateSecret(self.modulus_p)}, '
|
|
318
|
+
f'modulus_q={base.ObfuscateSecret(self.modulus_q)}, '
|
|
319
|
+
f'decrypt_exp={base.ObfuscateSecret(self.decrypt_exp)})')
|
|
320
|
+
|
|
321
|
+
def Decrypt(self, ciphertext: int, /) -> int:
|
|
322
|
+
"""Decrypt `ciphertext` with this private key.
|
|
323
|
+
|
|
324
|
+
We explicitly allow `ciphertext` to be zero for completeness, but it shouldn't be in practice.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
ciphertext (int): ciphertext to decrypt, 0 ≤ c < modulus
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
decrypted message (int, 1 ≤ m < modulus) = (m ** decrypt_exp) mod modulus
|
|
331
|
+
|
|
332
|
+
Raises:
|
|
333
|
+
InputError: invalid inputs
|
|
334
|
+
"""
|
|
335
|
+
# test inputs
|
|
336
|
+
if not 0 <= ciphertext < self.public_modulus:
|
|
337
|
+
raise base.InputError(f'invalid message: {ciphertext=}')
|
|
338
|
+
# decrypt using CRT (Chinese Remainder Theorem); 4x speedup; all the below is equivalent
|
|
339
|
+
# of doing: return modmath.ModExp(ciphertext, self.decrypt_exp, self.public_modulus)
|
|
340
|
+
m_p: int = modmath.ModExp(ciphertext % self.modulus_p, self.remainder_p, self.modulus_p)
|
|
341
|
+
m_q: int = modmath.ModExp(ciphertext % self.modulus_q, self.remainder_q, self.modulus_q)
|
|
342
|
+
h: int = (self.q_inverse_p * (m_p - m_q)) % self.modulus_p
|
|
343
|
+
return (m_q + h * self.modulus_q) % self.public_modulus
|
|
344
|
+
|
|
345
|
+
def Sign(self, message: int, /) -> int:
|
|
346
|
+
"""Sign `message` with this private key.
|
|
347
|
+
|
|
348
|
+
We explicitly disallow `message` to be zero.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
message (int): message to sign, 1 ≤ m < modulus
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
signed message (int, 1 ≤ m < modulus) = (m ** decrypt_exp) mod modulus;
|
|
355
|
+
identical to Decrypt()
|
|
356
|
+
|
|
357
|
+
Raises:
|
|
358
|
+
InputError: invalid inputs
|
|
359
|
+
"""
|
|
360
|
+
# test inputs
|
|
361
|
+
if not 0 < message < self.public_modulus:
|
|
362
|
+
raise base.InputError(f'invalid message: {message=}')
|
|
363
|
+
# call decryption
|
|
364
|
+
return self.Decrypt(message)
|
|
365
|
+
|
|
366
|
+
@classmethod
|
|
367
|
+
def New(cls, bit_length: int, /) -> Self:
|
|
368
|
+
"""Make a new private key of `bit_length` bits (primes p & q will be ~half this length).
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
bit_length (int): number of bits in the modulus, ≥ 11; primes p & q will be half this length
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
RSAPrivateKey object ready for use
|
|
375
|
+
|
|
376
|
+
Raises:
|
|
377
|
+
InputError: invalid inputs
|
|
378
|
+
CryptoError: failed generation
|
|
379
|
+
"""
|
|
380
|
+
# test inputs
|
|
381
|
+
if bit_length < 11:
|
|
382
|
+
raise base.InputError(f'invalid bit length: {bit_length=}')
|
|
383
|
+
# generate primes / modulus
|
|
384
|
+
failures: int = 0
|
|
385
|
+
while True:
|
|
386
|
+
try:
|
|
387
|
+
primes: list[int] = [modmath.NBitRandomPrime(bit_length // 2),
|
|
388
|
+
modmath.NBitRandomPrime(bit_length // 2)]
|
|
389
|
+
modulus: int = primes[0] * primes[1]
|
|
390
|
+
while modulus.bit_length() != bit_length or primes[0] == primes[1]:
|
|
391
|
+
primes.remove(min(primes))
|
|
392
|
+
primes.append(modmath.NBitRandomPrime(
|
|
393
|
+
bit_length // 2 + (bit_length % 2 if modulus.bit_length() < bit_length else 0)))
|
|
394
|
+
modulus = primes[0] * primes[1]
|
|
395
|
+
# build object
|
|
396
|
+
phi: int = (primes[0] - 1) * (primes[1] - 1)
|
|
397
|
+
prime_exp: int = (_SMALL_ENCRYPTION_EXPONENT if phi <= _BIG_ENCRYPTION_EXPONENT else
|
|
398
|
+
_BIG_ENCRYPTION_EXPONENT)
|
|
399
|
+
decrypt_exp: int = modmath.ModInv(prime_exp, phi)
|
|
400
|
+
p: int = min(primes) # "p" is always the smaller
|
|
401
|
+
q: int = max(primes) # "q" is always the larger
|
|
402
|
+
return cls(
|
|
403
|
+
modulus_p=p,
|
|
404
|
+
modulus_q=q,
|
|
405
|
+
public_modulus=modulus,
|
|
406
|
+
encrypt_exp=prime_exp,
|
|
407
|
+
decrypt_exp=decrypt_exp,
|
|
408
|
+
remainder_p=decrypt_exp % (p - 1),
|
|
409
|
+
remainder_q=decrypt_exp % (q - 1),
|
|
410
|
+
q_inverse_p=modmath.ModInv(q, p),
|
|
411
|
+
)
|
|
412
|
+
except (base.InputError, modmath.ModularDivideError) as err:
|
|
413
|
+
failures += 1
|
|
414
|
+
if failures >= _MAX_KEY_GENERATION_FAILURES:
|
|
415
|
+
raise base.CryptoError(f'failed key generation {failures} times') from err
|
|
416
|
+
logging.warning(err)
|