transcrypto 1.6.0__py3-none-any.whl → 1.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- transcrypto/__init__.py +7 -0
- transcrypto/aes.py +150 -44
- transcrypto/base.py +587 -442
- transcrypto/constants.py +20070 -1906
- transcrypto/dsa.py +132 -99
- transcrypto/elgamal.py +116 -84
- transcrypto/modmath.py +88 -78
- transcrypto/profiler.py +225 -175
- transcrypto/rsa.py +126 -90
- transcrypto/sss.py +122 -70
- transcrypto/transcrypto.py +2361 -1419
- {transcrypto-1.6.0.dist-info → transcrypto-1.7.0.dist-info}/METADATA +78 -58
- transcrypto-1.7.0.dist-info/RECORD +17 -0
- {transcrypto-1.6.0.dist-info → transcrypto-1.7.0.dist-info}/WHEEL +1 -2
- transcrypto-1.7.0.dist-info/entry_points.txt +4 -0
- transcrypto/safetrans.py +0 -1228
- transcrypto-1.6.0.dist-info/RECORD +0 -18
- transcrypto-1.6.0.dist-info/top_level.txt +0 -1
- {transcrypto-1.6.0.dist-info → transcrypto-1.7.0.dist-info}/licenses/LICENSE +0 -0
transcrypto/__init__.py
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Basic cryptography primitives implementation."""
|
|
4
|
+
|
|
5
|
+
__all__: list[str] = ['__author__', '__version__']
|
|
6
|
+
__version__ = '1.7.0' # remember to also update pyproject.toml
|
|
7
|
+
__author__ = 'Daniel Balparda <balparda@github.com>'
|
transcrypto/aes.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
#
|
|
3
|
-
# Copyright 2025 Daniel Balparda (balparda@github.com) - Apache-2.0 license
|
|
4
|
-
#
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
3
|
"""Balparda's TransCrypto Advanced Encryption Standard (AES) library.
|
|
6
4
|
|
|
7
5
|
<https://en.wikipedia.org/wiki/Advanced_Encryption_Standard>
|
|
@@ -19,31 +17,27 @@ wrappers, consistent with the transcrypto style.
|
|
|
19
17
|
from __future__ import annotations
|
|
20
18
|
|
|
21
19
|
import dataclasses
|
|
22
|
-
|
|
23
|
-
# import pdb
|
|
24
|
-
from typing import Self
|
|
20
|
+
from typing import Self, cast
|
|
25
21
|
|
|
22
|
+
from cryptography import exceptions as crypt_exceptions
|
|
26
23
|
from cryptography.hazmat.primitives import ciphers
|
|
27
|
-
from cryptography.hazmat.primitives.ciphers import algorithms, modes
|
|
28
24
|
from cryptography.hazmat.primitives import hashes as hazmat_hashes
|
|
25
|
+
from cryptography.hazmat.primitives.ciphers import algorithms, modes
|
|
29
26
|
from cryptography.hazmat.primitives.kdf import pbkdf2 as hazmat_pbkdf2
|
|
30
|
-
from cryptography import exceptions as crypt_exceptions
|
|
31
27
|
|
|
32
28
|
from . import base
|
|
33
29
|
|
|
34
|
-
__author__ = 'balparda@github.com'
|
|
35
|
-
__version__: str = base.__version__ # version comes from base!
|
|
36
|
-
__version_tuple__: tuple[int, ...] = base.__version_tuple__
|
|
37
|
-
|
|
38
|
-
|
|
39
30
|
# these fixed salt/iterations are for password->key generation only; NEVER use them to
|
|
40
31
|
# build a database of passwords because it would not be safe; NEVER change them or the
|
|
41
32
|
# keys will change and previous databases/encryptions will become inconsistent/unreadable!
|
|
42
33
|
_PASSWORD_SALT_256: bytes = base.HexToBytes(
|
|
43
|
-
|
|
34
|
+
'63b56fe9260ed3ff752a86a3414e4358e4d8e3e31b9dbc16e11ec19809e2f3c0'
|
|
35
|
+
) # fixed random salt: do NOT ever change!
|
|
44
36
|
_PASSWORD_ITERATIONS = 2025103 # fixed iterations, purposefully huge: do NOT ever change!
|
|
45
|
-
assert base.BytesToEncoded(_PASSWORD_SALT_256) == 'Y7Vv6SYO0_91KoajQU5DWOTY4-MbnbwW4R7BmAni88A=',
|
|
46
|
-
|
|
37
|
+
assert base.BytesToEncoded(_PASSWORD_SALT_256) == 'Y7Vv6SYO0_91KoajQU5DWOTY4-MbnbwW4R7BmAni88A=', ( # noqa: S101
|
|
38
|
+
'should never happen: constant'
|
|
39
|
+
)
|
|
40
|
+
assert _PASSWORD_ITERATIONS == (6075308 + 1) // 3, 'should never happen: constant' # noqa: S101
|
|
47
41
|
|
|
48
42
|
|
|
49
43
|
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
@@ -54,6 +48,7 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
54
48
|
|
|
55
49
|
Attributes:
|
|
56
50
|
key256 (bytes): AES 256 bits key (32 bytes), so length is always 32
|
|
51
|
+
|
|
57
52
|
"""
|
|
58
53
|
|
|
59
54
|
key256: bytes
|
|
@@ -63,9 +58,9 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
63
58
|
|
|
64
59
|
Raises:
|
|
65
60
|
InputError: invalid inputs
|
|
61
|
+
|
|
66
62
|
"""
|
|
67
|
-
|
|
68
|
-
if len(self.key256) != 32:
|
|
63
|
+
if len(self.key256) != 32: # noqa: PLR2004
|
|
69
64
|
raise base.InputError(f'invalid key256: {self}')
|
|
70
65
|
|
|
71
66
|
def __str__(self) -> str:
|
|
@@ -73,6 +68,7 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
73
68
|
|
|
74
69
|
Returns:
|
|
75
70
|
string representation of AESKey without leaking secrets
|
|
71
|
+
|
|
76
72
|
"""
|
|
77
73
|
return f'AESKey(key256={base.ObfuscateSecret(self.key256)})'
|
|
78
74
|
|
|
@@ -102,38 +98,59 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
102
98
|
AESKey crypto key to use (URL-safe base64-encoded 32-byte key)
|
|
103
99
|
|
|
104
100
|
Raises:
|
|
105
|
-
|
|
101
|
+
InputError: empty password
|
|
102
|
+
|
|
106
103
|
"""
|
|
107
104
|
str_password = str_password.strip()
|
|
108
105
|
if not str_password:
|
|
109
106
|
raise base.InputError('empty passwords not allowed, for safety reasons')
|
|
110
107
|
kdf = hazmat_pbkdf2.PBKDF2HMAC(
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
algorithm=hazmat_hashes.SHA256(),
|
|
109
|
+
length=32,
|
|
110
|
+
salt=_PASSWORD_SALT_256,
|
|
111
|
+
iterations=_PASSWORD_ITERATIONS,
|
|
112
|
+
)
|
|
113
113
|
return cls(key256=kdf.derive(str_password.encode('utf-8')))
|
|
114
114
|
|
|
115
115
|
class ECBEncoderClass(base.Encryptor, base.Decryptor):
|
|
116
116
|
"""The simplest encryption possible (UNSAFE if misused): 128 bit block AES-ECB, 256 bit key.
|
|
117
117
|
|
|
118
|
+
Note: Due to ECB encoding, this class is only safe-ish for blocks of random-looking data,
|
|
119
|
+
like hashes for example.
|
|
120
|
+
|
|
118
121
|
Please DO **NOT** use this for regular cryptography. For regular crypto use Encrypt()/Decrypt().
|
|
119
122
|
This class was specifically built to encode/decode 128 bit / 16 bytes blocks using a
|
|
120
123
|
pre-existing key. No measures are taken here to prevent timing attacks.
|
|
121
124
|
"""
|
|
122
125
|
|
|
123
126
|
def __init__(self, key256: AESKey, /) -> None:
|
|
124
|
-
"""
|
|
127
|
+
"""Construct.
|
|
125
128
|
|
|
126
129
|
Args:
|
|
127
130
|
key256 (AESKey): key
|
|
131
|
+
|
|
128
132
|
"""
|
|
129
133
|
self._cipher: ciphers.Cipher[modes.ECB] = ciphers.Cipher(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
algorithms.AES256(key256.key256),
|
|
135
|
+
modes.ECB(), # noqa: S305
|
|
136
|
+
)
|
|
137
|
+
alg: ciphers.BlockCipherAlgorithm = cast(
|
|
138
|
+
'algorithms.BlockCipherAlgorithm', # type: ignore
|
|
139
|
+
self._cipher.algorithm,
|
|
140
|
+
)
|
|
141
|
+
assert alg.key_size == 256, ( # noqa: PLR2004, S101
|
|
142
|
+
'should never happen: AES256+ECB should have 256 bits key'
|
|
143
|
+
)
|
|
144
|
+
assert alg.block_size == 128, ( # noqa: PLR2004, S101
|
|
145
|
+
'should never happen: AES256+ECB should have 128 bits block'
|
|
146
|
+
)
|
|
133
147
|
|
|
134
148
|
def Encrypt(self, plaintext: bytes, /, *, associated_data: bytes | None = None) -> bytes:
|
|
135
149
|
"""Encrypt a 128 bits block (16 bytes) `plaintext` and return `ciphertext` of 128 bits.
|
|
136
150
|
|
|
151
|
+
Note: Due to ECB encoding, this method is only safe-ish for blocks of random-looking data,
|
|
152
|
+
like hashes for example.
|
|
153
|
+
|
|
137
154
|
Please DO **NOT** use this for regular cryptography.
|
|
138
155
|
No measures are taken here to prevent timing attacks.
|
|
139
156
|
|
|
@@ -146,10 +163,11 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
146
163
|
|
|
147
164
|
Raises:
|
|
148
165
|
InputError: invalid inputs
|
|
166
|
+
|
|
149
167
|
"""
|
|
150
168
|
if associated_data is not None:
|
|
151
169
|
raise base.InputError('AES/ECB does not support associated_data')
|
|
152
|
-
if len(plaintext) != 16:
|
|
170
|
+
if len(plaintext) != 16: # noqa: PLR2004
|
|
153
171
|
raise base.InputError(f'plaintext must be 16 bytes long, got {len(plaintext)}')
|
|
154
172
|
encryptor: ciphers.CipherContext = self._cipher.encryptor()
|
|
155
173
|
return encryptor.update(plaintext) + encryptor.finalize()
|
|
@@ -157,6 +175,9 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
157
175
|
def Decrypt(self, ciphertext: bytes, /, *, associated_data: bytes | None = None) -> bytes:
|
|
158
176
|
"""Decrypt a 128 bits block (16 bytes) `ciphertext` and return original 128 bits `plaintext`.
|
|
159
177
|
|
|
178
|
+
Note: Due to ECB encoding, this method is only safe-ish for blocks of random-looking data,
|
|
179
|
+
like hashes for example.
|
|
180
|
+
|
|
160
181
|
Please DO **NOT** use this for regular cryptography.
|
|
161
182
|
No measures are taken here to prevent timing attacks.
|
|
162
183
|
|
|
@@ -169,32 +190,92 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
169
190
|
|
|
170
191
|
Raises:
|
|
171
192
|
InputError: invalid inputs
|
|
193
|
+
|
|
172
194
|
"""
|
|
173
195
|
if associated_data is not None:
|
|
174
196
|
raise base.InputError('AES/ECB does not support associated_data')
|
|
175
|
-
if len(ciphertext) != 16:
|
|
197
|
+
if len(ciphertext) != 16: # noqa: PLR2004
|
|
176
198
|
raise base.InputError(f'ciphertext must be 16 bytes long, got {len(ciphertext)}')
|
|
177
199
|
decryptor: ciphers.CipherContext = self._cipher.decryptor()
|
|
178
200
|
return decryptor.update(ciphertext) + decryptor.finalize()
|
|
179
201
|
|
|
180
202
|
def EncryptHex(self, plaintext_hex: str, /) -> str:
|
|
181
|
-
"""Encrypt a 128 bits hexadecimal block, outputting also a 128 bits hexadecimal block.
|
|
203
|
+
"""Encrypt a 128 bits hexadecimal block, outputting also a 128 bits hexadecimal block.
|
|
204
|
+
|
|
205
|
+
Note: Due to ECB encoding, this method is only safe-ish for blocks of random-looking data,
|
|
206
|
+
like hashes for example.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
plaintext_hex (str): plaintext hexadecimal block (length==32)
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
str: encrypted hexadecimal block (length==32)
|
|
213
|
+
|
|
214
|
+
"""
|
|
182
215
|
return base.BytesToHex(self.Encrypt(base.HexToBytes(plaintext_hex)))
|
|
183
216
|
|
|
184
217
|
def EncryptHex256(self, plaintext_hex: str, /) -> str:
|
|
185
|
-
"""Encrypt a 256 bits hexadecimal block, outputting also a 256 bits hexadecimal block.
|
|
218
|
+
"""Encrypt a 256 bits hexadecimal block, outputting also a 256 bits hexadecimal block.
|
|
219
|
+
|
|
220
|
+
Note: Due to ECB encoding, this method is only safe-ish for blocks of random-looking data,
|
|
221
|
+
like hashes for example.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
plaintext_hex (str): plaintext hexadecimal block (length==64)
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
str: encrypted hexadecimal block (length==64)
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
InputError: invalid inputs
|
|
231
|
+
|
|
232
|
+
"""
|
|
233
|
+
if len(plaintext_hex) != 64: # noqa: PLR2004
|
|
234
|
+
raise base.InputError(f'plaintext_hex must be 64 chars long, got {len(plaintext_hex)}')
|
|
186
235
|
return self.EncryptHex(plaintext_hex[:32]) + self.EncryptHex(plaintext_hex[32:])
|
|
187
236
|
|
|
188
237
|
def DecryptHex(self, ciphertext_hex: str, /) -> str:
|
|
189
|
-
"""Decrypt a 128 bits hexadecimal block, outputting also a 128 bits hexadecimal block.
|
|
238
|
+
"""Decrypt a 128 bits hexadecimal block, outputting also a 128 bits hexadecimal block.
|
|
239
|
+
|
|
240
|
+
Note: Due to ECB encoding, this method is only safe-ish for blocks of random-looking data,
|
|
241
|
+
like hashes for example.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
ciphertext_hex (str): encrypted hexadecimal block (length==32)
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
str: plaintext hexadecimal block (length==32)
|
|
248
|
+
|
|
249
|
+
"""
|
|
190
250
|
return base.BytesToHex(self.Decrypt(base.HexToBytes(ciphertext_hex)))
|
|
191
251
|
|
|
192
252
|
def DecryptHex256(self, ciphertext_hex: str, /) -> str:
|
|
193
|
-
"""Decrypt a 256 bits hexadecimal block, outputting also a 256 bits hexadecimal block.
|
|
253
|
+
"""Decrypt a 256 bits hexadecimal block, outputting also a 256 bits hexadecimal block.
|
|
254
|
+
|
|
255
|
+
Note: Due to ECB encoding, this method is only safe-ish for blocks of random-looking data,
|
|
256
|
+
like hashes for example.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
ciphertext_hex (str): encrypted hexadecimal block (length==64)
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
str: plaintext hexadecimal block (length==64)
|
|
263
|
+
|
|
264
|
+
Raises:
|
|
265
|
+
InputError: invalid inputs
|
|
266
|
+
|
|
267
|
+
"""
|
|
268
|
+
if len(ciphertext_hex) != 64: # noqa: PLR2004
|
|
269
|
+
raise base.InputError(f'ciphertext_hex must be 64 chars long, got {len(ciphertext_hex)}')
|
|
194
270
|
return self.DecryptHex(ciphertext_hex[:32]) + self.DecryptHex(ciphertext_hex[32:])
|
|
195
271
|
|
|
196
272
|
def ECBEncoder(self) -> AESKey.ECBEncoderClass:
|
|
197
|
-
"""Return a AESKey.ECBEncoderClass object using this key.
|
|
273
|
+
"""Return a AESKey.ECBEncoderClass object using this key.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
AESKey.ECBEncoderClass: ECB encoder with same key as self
|
|
277
|
+
|
|
278
|
+
"""
|
|
198
279
|
return AESKey.ECBEncoderClass(self)
|
|
199
280
|
|
|
200
281
|
def Encrypt(self, plaintext: bytes, /, *, associated_data: bytes | None = None) -> bytes:
|
|
@@ -215,22 +296,30 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
215
296
|
bytes: Ciphertext; if a nonce/tag is needed for decryption, the implementation
|
|
216
297
|
must encode it within the returned bytes (or document how to retrieve it)
|
|
217
298
|
|
|
218
|
-
Raises:
|
|
219
|
-
InputError: invalid inputs
|
|
220
|
-
CryptoError: internal crypto failures
|
|
221
299
|
"""
|
|
222
300
|
iv: bytes = base.RandBytes(16)
|
|
223
301
|
cipher: ciphers.Cipher[modes.GCM] = ciphers.Cipher(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
302
|
+
algorithms.AES256(self.key256), modes.GCM(iv)
|
|
303
|
+
)
|
|
304
|
+
alg: ciphers.BlockCipherAlgorithm = cast(
|
|
305
|
+
'algorithms.BlockCipherAlgorithm', # type: ignore
|
|
306
|
+
cipher.algorithm,
|
|
307
|
+
)
|
|
308
|
+
assert alg.key_size == 256, ( # noqa: PLR2004, S101
|
|
309
|
+
'should never happen: AES256+GCM should have 256 bits key'
|
|
310
|
+
)
|
|
311
|
+
assert alg.block_size == 128, ( # noqa: PLR2004, S101
|
|
312
|
+
'should never happen: AES256+GCM should have 128 bits block'
|
|
313
|
+
)
|
|
227
314
|
encryptor: ciphers.CipherContext = cipher.encryptor()
|
|
228
315
|
if associated_data:
|
|
229
316
|
encryptor.authenticate_additional_data(associated_data) # type:ignore
|
|
230
|
-
ciphertext: bytes =
|
|
317
|
+
ciphertext: bytes = (
|
|
318
|
+
encryptor.update(plaintext) + encryptor.finalize()
|
|
319
|
+
) # GCM doesn't need padding
|
|
231
320
|
tag: bytes = encryptor.tag # type:ignore
|
|
232
|
-
assert len(iv) == 16, 'should never happen: AES256+GCM should have 128 bits IV/nonce'
|
|
233
|
-
assert len(tag) == 16, 'should never happen: AES256+GCM should have 128 bits tag'
|
|
321
|
+
assert len(iv) == 16, 'should never happen: AES256+GCM should have 128 bits IV/nonce' # noqa: PLR2004, S101
|
|
322
|
+
assert len(tag) == 16, 'should never happen: AES256+GCM should have 128 bits tag' # noqa: PLR2004, S101
|
|
234
323
|
return iv + ciphertext + tag
|
|
235
324
|
|
|
236
325
|
def Decrypt(self, ciphertext: bytes, /, *, associated_data: bytes | None = None) -> bytes:
|
|
@@ -252,15 +341,32 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
252
341
|
Raises:
|
|
253
342
|
InputError: invalid inputs
|
|
254
343
|
CryptoError: internal crypto failures, authentication failure, key mismatch, etc
|
|
344
|
+
|
|
255
345
|
"""
|
|
256
|
-
if len(ciphertext) < 32:
|
|
346
|
+
if len(ciphertext) < 32: # noqa: PLR2004
|
|
257
347
|
raise base.InputError(f'AES256+GCM should have ≥32 bytes IV/CT/tag: {len(ciphertext)}')
|
|
258
348
|
iv, tag = ciphertext[:16], ciphertext[-16:]
|
|
259
349
|
decryptor: ciphers.CipherContext = ciphers.Cipher(
|
|
260
|
-
|
|
350
|
+
algorithms.AES256(self.key256), modes.GCM(iv, tag)
|
|
351
|
+
).decryptor()
|
|
261
352
|
if associated_data:
|
|
262
353
|
decryptor.authenticate_additional_data(associated_data) # type:ignore
|
|
263
354
|
try:
|
|
264
355
|
return decryptor.update(ciphertext[16:-16]) + decryptor.finalize()
|
|
265
356
|
except crypt_exceptions.InvalidTag as err:
|
|
266
357
|
raise base.CryptoError('failed decryption') from err
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _TestCryptoKeyEncoding(obj: base.CryptoKey, tp: type[base.CryptoKey]) -> None: # pyright: ignore[reportUnusedFunction]
|
|
361
|
+
"""Test encoding for a CryptoKey instance. Only for use from test modules."""
|
|
362
|
+
assert tp.FromJSON(obj.json) == obj # noqa: S101
|
|
363
|
+
assert tp.FromJSON(obj.formatted_json) == obj # noqa: S101
|
|
364
|
+
assert tp.Load(obj.blob) == obj # noqa: S101
|
|
365
|
+
assert tp.Load(obj.encoded) == obj # noqa: S101
|
|
366
|
+
assert tp.Load(obj.hex) == obj # noqa: S101
|
|
367
|
+
assert tp.Load(obj.raw) == obj # noqa: S101
|
|
368
|
+
key = AESKey(key256=b'x' * 32)
|
|
369
|
+
assert tp.Load(obj.Blob(key=key), key=key) == obj # noqa: S101
|
|
370
|
+
assert tp.Load(obj.Encoded(key=key), key=key) == obj # noqa: S101
|
|
371
|
+
assert tp.Load(obj.Hex(key=key), key=key) == obj # noqa: S101
|
|
372
|
+
assert tp.Load(obj.Raw(key=key), key=key) == obj # noqa: S101
|