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