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/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)