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/aes.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025 Daniel Balparda (balparda@github.com) - Apache-2.0 license
|
|
4
|
+
#
|
|
5
|
+
"""Balparda's TransCrypto Advanced Encryption Standard (AES) library.
|
|
6
|
+
|
|
7
|
+
<https://en.wikipedia.org/wiki/Advanced_Encryption_Standard>
|
|
8
|
+
|
|
9
|
+
<https://cryptography.io/en/latest/>
|
|
10
|
+
|
|
11
|
+
The Advanced Encryption Standard (AES), also known by its original name Rijndael
|
|
12
|
+
is a specification for the encryption of electronic data established by the
|
|
13
|
+
U.S. National Institute of Standards and Technology (NIST) in 2001.
|
|
14
|
+
|
|
15
|
+
We don't want to re-implement AES here, we will provide for good crypto
|
|
16
|
+
wrappers, consistent with the transcrypto style.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import dataclasses
|
|
22
|
+
# import datetime
|
|
23
|
+
# import pdb
|
|
24
|
+
from typing import Self
|
|
25
|
+
|
|
26
|
+
from cryptography.hazmat.primitives import ciphers
|
|
27
|
+
from cryptography.hazmat.primitives.ciphers import algorithms, modes
|
|
28
|
+
from cryptography.hazmat.primitives import hashes as hazmat_hashes
|
|
29
|
+
from cryptography.hazmat.primitives.kdf import pbkdf2 as hazmat_pbkdf2
|
|
30
|
+
from cryptography import exceptions as crypt_exceptions
|
|
31
|
+
|
|
32
|
+
from . import base
|
|
33
|
+
|
|
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
|
+
# these fixed salt/iterations are for password->key generation only; NEVER use them to
|
|
40
|
+
# build a database of passwords because it would not be safe; NEVER change them or the
|
|
41
|
+
# keys will change and previous databases/encryptions will become inconsistent/unreadable!
|
|
42
|
+
_PASSWORD_SALT_256: bytes = base.HexToBytes(
|
|
43
|
+
'63b56fe9260ed3ff752a86a3414e4358e4d8e3e31b9dbc16e11ec19809e2f3c0') # fixed random salt: do NOT ever change!
|
|
44
|
+
_PASSWORD_ITERATIONS = 2025103 # fixed iterations, purposefully huge: do NOT ever change!
|
|
45
|
+
assert base.BytesToEncoded(_PASSWORD_SALT_256) == 'Y7Vv6SYO0_91KoajQU5DWOTY4-MbnbwW4R7BmAni88A=', 'should never happen: constant'
|
|
46
|
+
assert _PASSWORD_ITERATIONS == (6075308 + 1) // 3, 'should never happen: constant'
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
50
|
+
class AESKey(base.CryptoKey, base.SymmetricCrypto):
|
|
51
|
+
"""Advanced Encryption Standard (AES) 256 bits key (32 bytes).
|
|
52
|
+
|
|
53
|
+
No measures are taken here to prevent timing attacks.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
key256 (bytes): AES 256 bits key (32 bytes), so length is always 32
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
key256: bytes
|
|
60
|
+
|
|
61
|
+
def __post_init__(self) -> None:
|
|
62
|
+
"""Check data.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
InputError: invalid inputs
|
|
66
|
+
"""
|
|
67
|
+
super(AESKey, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
|
|
68
|
+
if len(self.key256) != 32:
|
|
69
|
+
raise base.InputError(f'invalid key256: {self}')
|
|
70
|
+
|
|
71
|
+
def __str__(self) -> str:
|
|
72
|
+
"""Safe (no secrets) string representation of the AESKey.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
string representation of AESKey without leaking secrets
|
|
76
|
+
"""
|
|
77
|
+
return f'AESKey(key256={base.ObfuscateSecret(self.key256)})'
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def FromStaticPassword(cls, str_password: str, /) -> Self:
|
|
81
|
+
"""Derive crypto key using string password.
|
|
82
|
+
|
|
83
|
+
This is, purposefully, a very costly operation that should be cheap to execute once
|
|
84
|
+
after the user typed a password, but costly for an attacker to run a dictionary campaign on.
|
|
85
|
+
We do not use salt (or, more precisely, we use a fixed salt), as this is meant for direct use,
|
|
86
|
+
not to store the key in a DB. To compensate, the number o iterations is set especially high:
|
|
87
|
+
on the computer this was developed it takes ~1 sec to execute and is almost triple the
|
|
88
|
+
recommended amount of 600,000 (see https://en.wikipedia.org/wiki/PBKDF2).
|
|
89
|
+
|
|
90
|
+
The salt and the iteration number were randomly generated when this method was written
|
|
91
|
+
so as to be unique to this implementation and not a standard one that can have a standard
|
|
92
|
+
dictionary (i.e. attacks would have to generate a dictionary specific to this implementation).
|
|
93
|
+
ON THE OTHER HAND, this only serves the purpose of generating keys from static passwords.
|
|
94
|
+
NEVER use this method to save a database of keys. ONLY use it for direct user input.
|
|
95
|
+
|
|
96
|
+
Docs: https://cryptography.io/en/latest/
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
str_password (str): Non-empty string password; empty spaces at start/end are IGNORED
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
AESKey crypto key to use (URL-safe base64-encoded 32-byte key)
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
Error: empty password
|
|
106
|
+
"""
|
|
107
|
+
str_password = str_password.strip()
|
|
108
|
+
if not str_password:
|
|
109
|
+
raise base.InputError('empty passwords not allowed, for safety reasons')
|
|
110
|
+
kdf = hazmat_pbkdf2.PBKDF2HMAC(
|
|
111
|
+
algorithm=hazmat_hashes.SHA256(), length=32,
|
|
112
|
+
salt=_PASSWORD_SALT_256, iterations=_PASSWORD_ITERATIONS)
|
|
113
|
+
return cls(key256=kdf.derive(str_password.encode('utf-8')))
|
|
114
|
+
|
|
115
|
+
class ECBEncoderClass(base.SymmetricCrypto):
|
|
116
|
+
"""The simplest encryption possible (UNSAFE if misused): 128 bit block AES-ECB, 256 bit key.
|
|
117
|
+
|
|
118
|
+
Please DO **NOT** use this for regular cryptography. For regular crypto use Encrypt()/Decrypt().
|
|
119
|
+
This class was specifically built to encode/decode 128 bit / 16 bytes blocks using a
|
|
120
|
+
pre-existing key. No measures are taken here to prevent timing attacks.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(self, key256: AESKey, /) -> None:
|
|
124
|
+
"""Constructor.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
key256 (AESKey): key
|
|
128
|
+
"""
|
|
129
|
+
self._cipher: ciphers.Cipher[modes.ECB] = ciphers.Cipher(
|
|
130
|
+
algorithms.AES256(key256.key256), modes.ECB())
|
|
131
|
+
assert self._cipher.algorithm.key_size == 256, 'should never happen: AES256+ECB should have 256 bits key'
|
|
132
|
+
assert self._cipher.algorithm.block_size == 128, 'should never happen: AES256+ECB should have 128 bits block' # type:ignore
|
|
133
|
+
|
|
134
|
+
def Encrypt(self, plaintext: bytes, /, *, associated_data: bytes | None = None) -> bytes:
|
|
135
|
+
"""Encrypt a 128 bits block (16 bytes) `plaintext` and return `ciphertext` of 128 bits.
|
|
136
|
+
|
|
137
|
+
Please DO **NOT** use this for regular cryptography.
|
|
138
|
+
No measures are taken here to prevent timing attacks.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
plaintext (bytes): Data to encrypt.
|
|
142
|
+
associated_data (bytes, optional): DO NOT USE - not supported in ECB mode
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
bytes: Ciphertext, a block of 128 bits (16 bytes)
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
InputError: invalid inputs
|
|
149
|
+
"""
|
|
150
|
+
if associated_data is not None:
|
|
151
|
+
raise base.InputError('AES/ECB does not support associated_data')
|
|
152
|
+
if len(plaintext) != 16:
|
|
153
|
+
raise base.InputError(f'plaintext must be 16 bytes long, got {len(plaintext)}')
|
|
154
|
+
encryptor: ciphers.CipherContext = self._cipher.encryptor()
|
|
155
|
+
return encryptor.update(plaintext) + encryptor.finalize()
|
|
156
|
+
|
|
157
|
+
def Decrypt(self, ciphertext: bytes, /, *, associated_data: bytes | None = None) -> bytes:
|
|
158
|
+
"""Decrypt a 128 bits block (16 bytes) `ciphertext` and return original 128 bits `plaintext`.
|
|
159
|
+
|
|
160
|
+
Please DO **NOT** use this for regular cryptography.
|
|
161
|
+
No measures are taken here to prevent timing attacks.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
ciphertext (bytes): Data to decrypt (including any embedded nonce/tag if applicable)
|
|
165
|
+
associated_data (bytes, optional): DO NOT USE - not supported in ECB mode
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
bytes: Decrypted plaintext, a block of 128 bits (16 bytes)
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
InputError: invalid inputs
|
|
172
|
+
"""
|
|
173
|
+
if associated_data is not None:
|
|
174
|
+
raise base.InputError('AES/ECB does not support associated_data')
|
|
175
|
+
if len(ciphertext) != 16:
|
|
176
|
+
raise base.InputError(f'ciphertext must be 16 bytes long, got {len(ciphertext)}')
|
|
177
|
+
decryptor: ciphers.CipherContext = self._cipher.decryptor()
|
|
178
|
+
return decryptor.update(ciphertext) + decryptor.finalize()
|
|
179
|
+
|
|
180
|
+
def EncryptHex(self, plaintext_hex: str, /) -> str:
|
|
181
|
+
"""Encrypt a 256 bits hexadecimal block, outputting also a 256 bits hexadecimal block."""
|
|
182
|
+
return base.BytesToHex(self.Encrypt(base.HexToBytes(plaintext_hex)))
|
|
183
|
+
|
|
184
|
+
def DecryptHex(self, ciphertext_hex: str, /) -> str:
|
|
185
|
+
"""Decrypt a 256 bits hexadecimal block, outputting also a 256 bits hexadecimal block."""
|
|
186
|
+
return base.BytesToHex(self.Decrypt(base.HexToBytes(ciphertext_hex)))
|
|
187
|
+
|
|
188
|
+
def ECBEncoder(self) -> AESKey.ECBEncoderClass:
|
|
189
|
+
"""Return a AESKey.ECBEncoderClass object using this key."""
|
|
190
|
+
return AESKey.ECBEncoderClass(self)
|
|
191
|
+
|
|
192
|
+
def Encrypt(self, plaintext: bytes, /, *, associated_data: bytes | None = None) -> bytes:
|
|
193
|
+
"""Encrypt `plaintext` and return `ciphertext` with AES-256 + GCM algorithm.
|
|
194
|
+
|
|
195
|
+
<https://en.wikipedia.org/wiki/Galois/Counter_Mode>
|
|
196
|
+
<https://cryptography.io/en/latest/hazmat/primitives/symmetric-encryption/#cryptography.hazmat.primitives.ciphers.modes.GCM>
|
|
197
|
+
|
|
198
|
+
No measures are taken here to prevent timing attacks.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
plaintext (bytes): Data to encrypt.
|
|
202
|
+
associated_data (bytes, optional): Optional AAD (Authenticated Associated Data),
|
|
203
|
+
AEAD mode (authenticated encryption with associated data); must be provided
|
|
204
|
+
again on decrypt
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
bytes: Ciphertext; if a nonce/tag is needed for decryption, the implementation
|
|
208
|
+
must encode it within the returned bytes (or document how to retrieve it)
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
InputError: invalid inputs
|
|
212
|
+
CryptoError: internal crypto failures
|
|
213
|
+
"""
|
|
214
|
+
iv: bytes = base.RandBytes(16)
|
|
215
|
+
cipher: ciphers.Cipher[modes.GCM] = ciphers.Cipher(
|
|
216
|
+
algorithms.AES256(self.key256), modes.GCM(iv))
|
|
217
|
+
assert cipher.algorithm.key_size == 256, 'should never happen: AES256+GCM should have 256 bits key'
|
|
218
|
+
assert cipher.algorithm.block_size == 128, 'should never happen: AES256+GCM should have 128 bits block' # type:ignore
|
|
219
|
+
encryptor: ciphers.CipherContext = cipher.encryptor()
|
|
220
|
+
if associated_data:
|
|
221
|
+
encryptor.authenticate_additional_data(associated_data) # type:ignore
|
|
222
|
+
ciphertext: bytes = encryptor.update(plaintext) + encryptor.finalize() # GCM doesn't need padding
|
|
223
|
+
tag: bytes = encryptor.tag # type:ignore
|
|
224
|
+
assert len(iv) == 16, 'should never happen: AES256+GCM should have 128 bits IV/nonce'
|
|
225
|
+
assert len(tag) == 16, 'should never happen: AES256+GCM should have 128 bits tag'
|
|
226
|
+
return iv + ciphertext + tag
|
|
227
|
+
|
|
228
|
+
def Decrypt(self, ciphertext: bytes, /, *, associated_data: bytes | None = None) -> bytes:
|
|
229
|
+
"""Decrypt `ciphertext` and return the original `plaintext` with AES-256 + GCM algorithm.
|
|
230
|
+
|
|
231
|
+
<https://en.wikipedia.org/wiki/Galois/Counter_Mode>
|
|
232
|
+
<https://cryptography.io/en/latest/hazmat/primitives/symmetric-encryption/#cryptography.hazmat.primitives.ciphers.modes.GCM>
|
|
233
|
+
|
|
234
|
+
No measures are taken here to prevent timing attacks.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
ciphertext (bytes): Data to decrypt (including any embedded nonce/tag if applicable)
|
|
238
|
+
associated_data (bytes, optional): Optional AAD (Authenticated Associated Data);
|
|
239
|
+
must match what was used during encrypt
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
bytes: Decrypted plaintext bytes
|
|
243
|
+
|
|
244
|
+
Raises:
|
|
245
|
+
InputError: invalid inputs
|
|
246
|
+
CryptoError: internal crypto failures, authentication failure, key mismatch, etc
|
|
247
|
+
"""
|
|
248
|
+
assert len(ciphertext) >= 32, 'should never happen: AES256+GCM should have ≥32 bytes IV/CT/tag'
|
|
249
|
+
iv, tag = ciphertext[:16], ciphertext[-16:]
|
|
250
|
+
decryptor: ciphers.CipherContext = ciphers.Cipher(
|
|
251
|
+
algorithms.AES256(self.key256), modes.GCM(iv, tag)).decryptor()
|
|
252
|
+
if associated_data:
|
|
253
|
+
decryptor.authenticate_additional_data(associated_data) # type:ignore
|
|
254
|
+
try:
|
|
255
|
+
return decryptor.update(ciphertext[16:-16]) + decryptor.finalize()
|
|
256
|
+
except crypt_exceptions.InvalidTag as err:
|
|
257
|
+
raise base.CryptoError('failed decryption') from err
|