transcrypto 1.6.0__py3-none-any.whl → 1.8.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 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.8.0' # remember to also update pyproject.toml
7
+ __author__ = 'Daniel Balparda <balparda@github.com>'
transcrypto/aes.py CHANGED
@@ -1,7 +1,5 @@
1
- #!/usr/bin/env python3
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
- # import datetime
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
- '63b56fe9260ed3ff752a86a3414e4358e4d8e3e31b9dbc16e11ec19809e2f3c0') # fixed random salt: do NOT ever change!
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=', 'should never happen: constant'
46
- assert _PASSWORD_ITERATIONS == (6075308 + 1) // 3, 'should never happen: constant'
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
- super(AESKey, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
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
- Error: empty password
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
- algorithm=hazmat_hashes.SHA256(), length=32,
112
- salt=_PASSWORD_SALT_256, iterations=_PASSWORD_ITERATIONS)
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
- """Constructor.
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
- 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
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
- algorithms.AES256(self.key256), modes.GCM(iv))
225
- assert cipher.algorithm.key_size == 256, 'should never happen: AES256+GCM should have 256 bits key'
226
- assert cipher.algorithm.block_size == 128, 'should never happen: AES256+GCM should have 128 bits block' # type:ignore
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 = encryptor.update(plaintext) + encryptor.finalize() # GCM doesn't need padding
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
- algorithms.AES256(self.key256), modes.GCM(iv, tag)).decryptor()
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