transcrypto 1.7.0__py3-none-any.whl → 2.0.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.
Files changed (34) hide show
  1. transcrypto/__init__.py +1 -1
  2. transcrypto/cli/__init__.py +3 -0
  3. transcrypto/cli/aeshash.py +370 -0
  4. transcrypto/cli/bidsecret.py +336 -0
  5. transcrypto/cli/clibase.py +183 -0
  6. transcrypto/cli/intmath.py +429 -0
  7. transcrypto/cli/publicalgos.py +878 -0
  8. transcrypto/core/__init__.py +3 -0
  9. transcrypto/{aes.py → core/aes.py} +17 -29
  10. transcrypto/core/bid.py +161 -0
  11. transcrypto/{dsa.py → core/dsa.py} +28 -27
  12. transcrypto/{elgamal.py → core/elgamal.py} +33 -32
  13. transcrypto/core/hashes.py +96 -0
  14. transcrypto/core/key.py +735 -0
  15. transcrypto/{modmath.py → core/modmath.py} +91 -17
  16. transcrypto/{rsa.py → core/rsa.py} +51 -50
  17. transcrypto/{sss.py → core/sss.py} +27 -26
  18. transcrypto/profiler.py +29 -13
  19. transcrypto/transcrypto.py +60 -1996
  20. transcrypto/utils/__init__.py +3 -0
  21. transcrypto/utils/base.py +72 -0
  22. transcrypto/utils/human.py +278 -0
  23. transcrypto/utils/logging.py +139 -0
  24. transcrypto/utils/saferandom.py +102 -0
  25. transcrypto/utils/stats.py +360 -0
  26. transcrypto/utils/timer.py +175 -0
  27. {transcrypto-1.7.0.dist-info → transcrypto-2.0.0.dist-info}/METADATA +111 -109
  28. transcrypto-2.0.0.dist-info/RECORD +33 -0
  29. transcrypto/base.py +0 -1918
  30. transcrypto-1.7.0.dist-info/RECORD +0 -17
  31. /transcrypto/{constants.py → core/constants.py} +0 -0
  32. {transcrypto-1.7.0.dist-info → transcrypto-2.0.0.dist-info}/WHEEL +0 -0
  33. {transcrypto-1.7.0.dist-info → transcrypto-2.0.0.dist-info}/entry_points.txt +0 -0
  34. {transcrypto-1.7.0.dist-info → transcrypto-2.0.0.dist-info}/licenses/LICENSE +0 -0
transcrypto/__init__.py CHANGED
@@ -3,5 +3,5 @@
3
3
  """Basic cryptography primitives implementation."""
4
4
 
5
5
  __all__: list[str] = ['__author__', '__version__']
6
- __version__ = '1.7.0' # remember to also update pyproject.toml
6
+ __version__ = '2.0.0' # remember to also update pyproject.toml
7
7
  __author__ = 'Daniel Balparda <balparda@github.com>'
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """CLI logic."""
@@ -0,0 +1,370 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Balparda's TransCrypto CLI: AES and Hash commands."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import pathlib
8
+ import re
9
+
10
+ import click
11
+ import typer
12
+
13
+ from transcrypto import transcrypto
14
+ from transcrypto.cli import clibase
15
+ from transcrypto.core import aes, hashes
16
+ from transcrypto.utils import base
17
+
18
+ _HEX_RE = re.compile(r'^[0-9a-fA-F]+$')
19
+
20
+ # =================================== "HASH" COMMAND ===============================================
21
+
22
+
23
+ hash_app = typer.Typer(
24
+ no_args_is_help=True,
25
+ help='Cryptographic Hashing (SHA-256 / SHA-512 / file).',
26
+ )
27
+ transcrypto.app.add_typer(hash_app, name='hash')
28
+
29
+
30
+ @hash_app.command(
31
+ 'sha256',
32
+ help='SHA-256 of input `data`.',
33
+ epilog=(
34
+ 'Example:\n\n\n\n'
35
+ '$ poetry run transcrypto -i bin hash sha256 xyz\n\n'
36
+ '3608bca1e44ea6c4d268eb6db02260269892c0b42b86bbf1e77a6fa16c3c9282\n\n'
37
+ '$ poetry run transcrypto -i b64 hash sha256 -- eHl6 # "xyz" in base-64\n\n'
38
+ '3608bca1e44ea6c4d268eb6db02260269892c0b42b86bbf1e77a6fa16c3c9282'
39
+ ),
40
+ )
41
+ @clibase.CLIErrorGuard
42
+ def Hash256( # documentation is help/epilog/args # noqa: D103
43
+ *,
44
+ ctx: typer.Context,
45
+ data: str = typer.Argument(..., help='Input data (raw text; or `--input-format <hex|b64|bin>`)'),
46
+ ) -> None:
47
+ config: transcrypto.TransConfig = ctx.obj
48
+ bt: bytes = transcrypto.BytesFromText(data, config.input_format)
49
+ config.console.print(transcrypto.BytesToText(hashes.Hash256(bt), config.output_format))
50
+
51
+
52
+ @hash_app.command(
53
+ 'sha512',
54
+ help='SHA-512 of input `data`.',
55
+ epilog=(
56
+ 'Example:\n\n\n\n'
57
+ '$ poetry run transcrypto -i bin hash sha512 xyz\n\n'
58
+ '4a3ed8147e37876adc8f76328e5abcc1b470e6acfc18efea0135f983604953a5'
59
+ '8e183c1a6086e91ba3e821d926f5fdeb37761c7ca0328a963f5e92870675b728\n\n'
60
+ '$ poetry run transcrypto -i b64 hash sha512 -- eHl6 # "xyz" in base-64\n\n'
61
+ '4a3ed8147e37876adc8f76328e5abcc1b470e6acfc18efea0135f983604953a5'
62
+ '8e183c1a6086e91ba3e821d926f5fdeb37761c7ca0328a963f5e92870675b728'
63
+ ),
64
+ )
65
+ @clibase.CLIErrorGuard
66
+ def Hash512( # documentation is help/epilog/args # noqa: D103
67
+ *,
68
+ ctx: typer.Context,
69
+ data: str = typer.Argument(..., help='Input data (raw text; or `--input-format <hex|b64|bin>`)'),
70
+ ) -> None:
71
+ config: transcrypto.TransConfig = ctx.obj
72
+ bt: bytes = transcrypto.BytesFromText(data, config.input_format)
73
+ config.console.print(transcrypto.BytesToText(hashes.Hash512(bt), config.output_format))
74
+
75
+
76
+ @hash_app.command(
77
+ 'file',
78
+ help='SHA-256/512 hash of file contents, defaulting to SHA-256.',
79
+ epilog=(
80
+ 'Example:\n\n\n\n'
81
+ '$ poetry run transcrypto hash file /etc/passwd --digest sha512\n\n'
82
+ '8966f5953e79f55dfe34d3dc5b160ac4a4a3f9cbd1c36695a54e28d77c7874df'
83
+ 'f8595502f8a420608911b87d336d9e83c890f0e7ec11a76cb10b03e757f78aea'
84
+ ),
85
+ )
86
+ @clibase.CLIErrorGuard
87
+ def HashFile( # documentation is help/epilog/args # noqa: D103
88
+ *,
89
+ ctx: typer.Context,
90
+ path: pathlib.Path = typer.Argument( # noqa: B008
91
+ ...,
92
+ exists=True,
93
+ file_okay=True,
94
+ dir_okay=False,
95
+ readable=True,
96
+ resolve_path=True,
97
+ help='Path to existing file',
98
+ ),
99
+ digest: str = typer.Option(
100
+ 'sha256',
101
+ '-d',
102
+ '--digest',
103
+ click_type=click.Choice(['sha256', 'sha512'], case_sensitive=False),
104
+ help='Digest type, SHA-256 ("sha256") or SHA-512 ("sha512")',
105
+ ),
106
+ ) -> None:
107
+ config: transcrypto.TransConfig = ctx.obj
108
+ config.console.print(
109
+ transcrypto.BytesToText(hashes.FileHash(str(path), digest=digest), config.output_format)
110
+ )
111
+
112
+
113
+ # =================================== "AES" COMMAND ================================================
114
+
115
+
116
+ aes_app = typer.Typer(
117
+ no_args_is_help=True,
118
+ help=(
119
+ 'AES-256 operations (GCM/ECB) and key derivation. '
120
+ 'No measures are taken here to prevent timing attacks.'
121
+ ),
122
+ )
123
+ transcrypto.app.add_typer(aes_app, name='aes')
124
+
125
+
126
+ @aes_app.command(
127
+ 'key',
128
+ help=(
129
+ 'Derive key from a password (PBKDF2-HMAC-SHA256) with custom expensive '
130
+ 'salt and iterations. Very good/safe for simple password-to-key but not for '
131
+ 'passwords databases (because of constant salt).'
132
+ ),
133
+ epilog=(
134
+ 'Example:\n\n\n\n'
135
+ '$ poetry run transcrypto -o b64 aes key "correct horse battery staple"\n\n'
136
+ 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es=\n\n' # cspell:disable-line
137
+ '$ poetry run transcrypto -p keyfile.out --protect hunter aes key '
138
+ '"correct horse battery staple"\n\n'
139
+ "AES key saved to 'keyfile.out'"
140
+ ),
141
+ )
142
+ @clibase.CLIErrorGuard
143
+ def AESKeyFromPass( # documentation is help/epilog/args # noqa: D103
144
+ *,
145
+ ctx: typer.Context,
146
+ password: str = typer.Argument(..., help='Password (leading/trailing spaces ignored)'),
147
+ ) -> None:
148
+ config: transcrypto.TransConfig = ctx.obj
149
+ aes_key: aes.AESKey = aes.AESKey.FromStaticPassword(password)
150
+ if config.key_path is not None:
151
+ transcrypto.SaveObj(aes_key, str(config.key_path), config.protect)
152
+ config.console.print(f'AES key saved to {str(config.key_path)!r}')
153
+ else:
154
+ config.console.print(transcrypto.BytesToText(aes_key.key256, config.output_format))
155
+
156
+
157
+ @aes_app.command(
158
+ 'encrypt',
159
+ help=(
160
+ 'AES-256-GCM: safely encrypt `plaintext` with `-k`/`--key` or with '
161
+ '`-p`/`--key-path` keyfile. All inputs are raw, or you '
162
+ 'can use `--input-format <hex|b64|bin>`. Attention: if you provide `-a`/`--aad` '
163
+ '(associated data, AAD), you will need to provide the same AAD when decrypting '
164
+ 'and it is NOT included in the `ciphertext`/CT returned by this method!'
165
+ ),
166
+ epilog=(
167
+ 'Example:\n\n\n\n'
168
+ '$ poetry run transcrypto -i b64 -o b64 aes encrypt -k '
169
+ 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= -- AAAAAAB4eXo=\n\n' # cspell:disable-line
170
+ 'F2_ZLrUw5Y8oDnbTP5t5xCUWX8WtVILLD0teyUi_37_4KHeV-YowVA==\n\n' # cspell:disable-line
171
+ '$ poetry run transcrypto -i b64 -o b64 aes encrypt -k '
172
+ 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= -a eHl6 -- AAAAAAB4eXo=\n\n' # cspell:disable-line
173
+ 'xOlAHPUPpeyZHId-f3VQ_QKKMxjIW0_FBo9WOfIBrzjn0VkVV6xTRA==' # cspell:disable-line
174
+ ),
175
+ )
176
+ @clibase.CLIErrorGuard
177
+ def AESEncrypt( # documentation is help/epilog/args # noqa: D103
178
+ *,
179
+ ctx: typer.Context,
180
+ plaintext: str = typer.Argument(..., help='Input data to encrypt (PT)'),
181
+ key: str | None = typer.Option(
182
+ None, '-k', '--key', help="Key if `-p`/`--key-path` wasn't used (32 bytes)"
183
+ ),
184
+ aad: str = typer.Option(
185
+ '',
186
+ '-a',
187
+ '--aad',
188
+ help='Associated data (optional; has to be separately sent to receiver/stored)',
189
+ ),
190
+ ) -> None:
191
+ config: transcrypto.TransConfig = ctx.obj
192
+ aes_key: aes.AESKey
193
+ if key:
194
+ key_bytes: bytes = transcrypto.BytesFromText(key, config.input_format)
195
+ if len(key_bytes) != 32: # noqa: PLR2004
196
+ raise base.InputError(f'invalid AES key size: {len(key_bytes)} bytes (expected 32)')
197
+ aes_key = aes.AESKey(key256=key_bytes)
198
+ elif config.key_path is not None:
199
+ aes_key = transcrypto.LoadObj(str(config.key_path), config.protect, aes.AESKey)
200
+ else:
201
+ raise base.InputError('provide -k/--key or -p/--key-path')
202
+ aad_bytes: bytes | None = transcrypto.BytesFromText(aad, config.input_format) if aad else None
203
+ pt: bytes = transcrypto.BytesFromText(plaintext, config.input_format)
204
+ ct: bytes = aes_key.Encrypt(pt, associated_data=aad_bytes)
205
+ config.console.print(transcrypto.BytesToText(ct, config.output_format))
206
+
207
+
208
+ @aes_app.command(
209
+ 'decrypt',
210
+ help=(
211
+ 'AES-256-GCM: safely decrypt `ciphertext` with `-k`/`--key` or with '
212
+ '`-p`/`--key-path` keyfile. All inputs are raw, or you '
213
+ 'can use `--input-format <hex|b64|bin>`. Attention: if you provided `-a`/`--aad` '
214
+ '(associated data, AAD) during encryption, you will need to provide the same AAD now!'
215
+ ),
216
+ epilog=(
217
+ 'Example:\n\n\n\n'
218
+ '$ poetry run transcrypto -i b64 -o b64 aes decrypt -k '
219
+ 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= -- ' # cspell:disable-line
220
+ 'F2_ZLrUw5Y8oDnbTP5t5xCUWX8WtVILLD0teyUi_37_4KHeV-YowVA==\n\n' # cspell:disable-line
221
+ 'AAAAAAB4eXo=\n\n' # cspell:disable-line
222
+ '$ poetry run transcrypto -i b64 -o b64 aes decrypt -k '
223
+ 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= -a eHl6 -- ' # cspell:disable-line
224
+ 'xOlAHPUPpeyZHId-f3VQ_QKKMxjIW0_FBo9WOfIBrzjn0VkVV6xTRA==\n\n' # cspell:disable-line
225
+ 'AAAAAAB4eXo=' # cspell:disable-line
226
+ ),
227
+ )
228
+ @clibase.CLIErrorGuard
229
+ def AESDecrypt( # documentation is help/epilog/args # noqa: D103
230
+ *,
231
+ ctx: typer.Context,
232
+ ciphertext: str = typer.Argument(..., help='Input data to decrypt (CT)'),
233
+ key: str | None = typer.Option(
234
+ None, '-k', '--key', help="Key if `-p`/`--key-path` wasn't used (32 bytes)"
235
+ ),
236
+ aad: str = typer.Option(
237
+ '',
238
+ '-a',
239
+ '--aad',
240
+ help='Associated data (optional; has to be exactly the same as used during encryption)',
241
+ ),
242
+ ) -> None:
243
+ config: transcrypto.TransConfig = ctx.obj
244
+ aes_key: aes.AESKey
245
+ if key:
246
+ key_bytes: bytes = transcrypto.BytesFromText(key, config.input_format)
247
+ if len(key_bytes) != 32: # noqa: PLR2004
248
+ raise base.InputError(f'invalid AES key size: {len(key_bytes)} bytes (expected 32)')
249
+ aes_key = aes.AESKey(key256=key_bytes)
250
+ elif config.key_path is not None:
251
+ aes_key = transcrypto.LoadObj(str(config.key_path), config.protect, aes.AESKey)
252
+ else:
253
+ raise base.InputError('provide -k/--key or -p/--key-path')
254
+ # associated data, if any
255
+ aad_bytes: bytes | None = transcrypto.BytesFromText(aad, config.input_format) if aad else None
256
+ ct: bytes = transcrypto.BytesFromText(ciphertext, config.input_format)
257
+ pt: bytes = aes_key.Decrypt(ct, associated_data=aad_bytes)
258
+ config.console.print(transcrypto.BytesToText(pt, config.output_format))
259
+
260
+
261
+ # ================================ "AES ECB" SUB-COMMAND ===========================================
262
+
263
+
264
+ aes_ecb_app = typer.Typer(
265
+ no_args_is_help=True,
266
+ help=(
267
+ 'AES-256-ECB: encrypt/decrypt 128 bit (16 bytes) hexadecimal blocks. UNSAFE, except '
268
+ 'for specifically encrypting hash blocks which are very much expected to look random. '
269
+ 'ECB mode will have the same output for the same input (no IV/nonce is used).'
270
+ ),
271
+ )
272
+ aes_app.add_typer(aes_ecb_app, name='ecb')
273
+
274
+
275
+ @aes_ecb_app.command(
276
+ 'encrypt',
277
+ help=(
278
+ 'AES-256-ECB: encrypt 16-bytes hex `plaintext` with `-k`/`--key` or with '
279
+ '`-p`/`--key-path` keyfile. UNSAFE, except for specifically encrypting hash blocks.'
280
+ ),
281
+ epilog=(
282
+ 'Example:\n\n\n\n'
283
+ '$ poetry run transcrypto -i b64 aes ecb -k '
284
+ 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= encrypt ' # cspell:disable-line
285
+ '00112233445566778899aabbccddeeff\n\n' # cspell:disable-line
286
+ '54ec742ca3da7b752e527b74e3a798d7'
287
+ ),
288
+ )
289
+ @clibase.CLIErrorGuard
290
+ def AESECBEncrypt( # documentation is help/epilog/args # noqa: D103
291
+ *,
292
+ ctx: typer.Context,
293
+ plaintext: str = typer.Argument(..., help='Plaintext block as 32 hex chars (16-bytes)'),
294
+ key: str | None = typer.Option(
295
+ None,
296
+ '-k',
297
+ '--key',
298
+ help=(
299
+ "Key if `-p`/`--key-path` wasn't used (32 bytes; raw, or you "
300
+ 'can use `--input-format <hex|b64|bin>`)'
301
+ ),
302
+ ),
303
+ ) -> None:
304
+ config: transcrypto.TransConfig = ctx.obj
305
+ plaintext = plaintext.strip()
306
+ if len(plaintext) != 32: # noqa: PLR2004
307
+ raise base.InputError('hexadecimal string must be exactly 32 hex chars')
308
+ if not _HEX_RE.match(plaintext):
309
+ raise base.InputError(f'invalid hexadecimal string: {plaintext!r}')
310
+ aes_key: aes.AESKey
311
+ if key:
312
+ key_bytes: bytes = transcrypto.BytesFromText(key, config.input_format)
313
+ if len(key_bytes) != 32: # noqa: PLR2004
314
+ raise base.InputError(f'invalid AES key size: {len(key_bytes)} bytes (expected 32)')
315
+ aes_key = aes.AESKey(key256=key_bytes)
316
+ elif config.key_path is not None:
317
+ aes_key = transcrypto.LoadObj(str(config.key_path), config.protect, aes.AESKey)
318
+ else:
319
+ raise base.InputError('provide -k/--key or -p/--key-path')
320
+ ecb: aes.AESKey.ECBEncoderClass = aes_key.ECBEncoder()
321
+ config.console.print(ecb.EncryptHex(plaintext))
322
+
323
+
324
+ @aes_ecb_app.command(
325
+ 'decrypt',
326
+ help=(
327
+ 'AES-256-ECB: decrypt 16-bytes hex `ciphertext` with `-k`/`--key` or with '
328
+ '`-p`/`--key-path` keyfile. UNSAFE, except for specifically encrypting hash blocks.'
329
+ ),
330
+ epilog=(
331
+ 'Example:\n\n\n\n'
332
+ '$ poetry run transcrypto -i b64 aes ecb -k '
333
+ 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= decrypt ' # cspell:disable-line
334
+ '54ec742ca3da7b752e527b74e3a798d7\n\n' # cspell:disable-line
335
+ '00112233445566778899aabbccddeeff' # cspell:disable-line
336
+ ),
337
+ )
338
+ @clibase.CLIErrorGuard
339
+ def AESECBDecrypt( # documentation is help/epilog/args # noqa: D103
340
+ *,
341
+ ctx: typer.Context,
342
+ ciphertext: str = typer.Argument(..., help='Ciphertext block as 32 hex chars (16-bytes)'),
343
+ key: str | None = typer.Option(
344
+ None,
345
+ '-k',
346
+ '--key',
347
+ help=(
348
+ "Key if `-p`/`--key-path` wasn't used (32 bytes; raw, or you "
349
+ 'can use `--input-format <hex|b64|bin>`)'
350
+ ),
351
+ ),
352
+ ) -> None:
353
+ config: transcrypto.TransConfig = ctx.obj
354
+ ciphertext = ciphertext.strip()
355
+ if len(ciphertext) != 32: # noqa: PLR2004
356
+ raise base.InputError('hexadecimal string must be exactly 32 hex chars')
357
+ if not _HEX_RE.match(ciphertext):
358
+ raise base.InputError(f'invalid hexadecimal string: {ciphertext!r}')
359
+ aes_key: aes.AESKey
360
+ if key:
361
+ key_bytes: bytes = transcrypto.BytesFromText(key, config.input_format)
362
+ if len(key_bytes) != 32: # noqa: PLR2004
363
+ raise base.InputError(f'invalid AES key size: {len(key_bytes)} bytes (expected 32)')
364
+ aes_key = aes.AESKey(key256=key_bytes)
365
+ elif config.key_path is not None:
366
+ aes_key = transcrypto.LoadObj(str(config.key_path), config.protect, aes.AESKey)
367
+ else:
368
+ raise base.InputError('provide -k/--key or -p/--key-path')
369
+ ecb: aes.AESKey.ECBEncoderClass = aes_key.ECBEncoder()
370
+ config.console.print(ecb.DecryptHex(ciphertext))