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
@@ -0,0 +1,878 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Balparda's TransCrypto CLI: Public algorithms commands."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import typer
8
+
9
+ from transcrypto import transcrypto
10
+ from transcrypto.cli import clibase
11
+ from transcrypto.core import dsa, elgamal, rsa
12
+
13
+ # ================================== "RSA" COMMAND =================================================
14
+
15
+
16
+ rsa_app = typer.Typer(
17
+ no_args_is_help=True,
18
+ help=(
19
+ 'RSA (Rivest-Shamir-Adleman) asymmetric cryptography. '
20
+ 'All methods require file key(s) as `-p`/`--key-path` (see provided examples). '
21
+ 'All non-int inputs are raw, or you can use `--input-format <hex|b64|bin>`. '
22
+ 'Attention: if you provide `-a`/`--aad` (associated data, AAD), '
23
+ 'you will need to provide the same AAD when decrypting/verifying and it is NOT included '
24
+ 'in the `ciphertext`/CT or `signature` returned by these methods! '
25
+ 'No measures are taken here to prevent timing attacks.'
26
+ ),
27
+ )
28
+ transcrypto.app.add_typer(rsa_app, name='rsa')
29
+
30
+
31
+ @rsa_app.command(
32
+ 'new',
33
+ help=(
34
+ 'Generate RSA private/public key pair with `bits` modulus size (prime sizes will be `bits`/2).'
35
+ ),
36
+ epilog=(
37
+ 'Example:\n\n\n\n'
38
+ '$ poetry run transcrypto -p rsa-key rsa new --bits 64 '
39
+ '# NEVER use such a small key: example only!\n\n'
40
+ "RSA private/public keys saved to 'rsa-key.priv/.pub'"
41
+ ),
42
+ )
43
+ @clibase.CLIErrorGuard
44
+ def RSANew( # documentation is help/epilog/args # noqa: D103
45
+ *,
46
+ ctx: typer.Context,
47
+ bits: int = typer.Option(
48
+ 3332,
49
+ '-b',
50
+ '--bits',
51
+ min=16,
52
+ help='Modulus size in bits, ≥16; the default (3332) is a safe size',
53
+ ),
54
+ ) -> None:
55
+ config: transcrypto.TransConfig = ctx.obj
56
+ base_path: str = transcrypto.RequireKeyPath(config, 'rsa')
57
+ rsa_priv: rsa.RSAPrivateKey = rsa.RSAPrivateKey.New(bits)
58
+ rsa_pub: rsa.RSAPublicKey = rsa.RSAPublicKey.Copy(rsa_priv)
59
+ transcrypto.SaveObj(rsa_priv, base_path + '.priv', config.protect)
60
+ transcrypto.SaveObj(rsa_pub, base_path + '.pub', config.protect)
61
+ config.console.print(f'RSA private/public keys saved to {base_path + ".priv/.pub"!r}')
62
+
63
+
64
+ @rsa_app.command(
65
+ 'rawencrypt',
66
+ help=(
67
+ 'Raw encrypt *integer* `message` with public key (BEWARE: no OAEP/PSS padding or validation).'
68
+ ),
69
+ epilog=(
70
+ 'Example:\n\n\n\n'
71
+ '$ poetry run transcrypto -p rsa-key.pub rsa rawencrypt 999\n\n'
72
+ '6354905961171348600'
73
+ ),
74
+ )
75
+ @clibase.CLIErrorGuard
76
+ def RSARawEncrypt( # documentation is help/epilog/args # noqa: D103
77
+ *,
78
+ ctx: typer.Context,
79
+ message: str = typer.Argument(..., help='Integer message to encrypt, 1≤`message`<*modulus*'),
80
+ ) -> None:
81
+ config: transcrypto.TransConfig = ctx.obj
82
+ message_i: int = transcrypto.ParseInt(message, min_value=1)
83
+ key_path: str = transcrypto.RequireKeyPath(config, 'rsa')
84
+ rsa_pub: rsa.RSAPublicKey = rsa.RSAPublicKey.Copy(
85
+ transcrypto.LoadObj(key_path, config.protect, rsa.RSAPublicKey)
86
+ )
87
+ config.console.print(rsa_pub.RawEncrypt(message_i))
88
+
89
+
90
+ @rsa_app.command(
91
+ 'rawdecrypt',
92
+ help=(
93
+ 'Raw decrypt *integer* `ciphertext` with private key '
94
+ '(BEWARE: no OAEP/PSS padding or validation).'
95
+ ),
96
+ epilog=(
97
+ 'Example:\n\n\n\n'
98
+ '$ poetry run transcrypto -p rsa-key.priv rsa rawdecrypt 6354905961171348600\n\n'
99
+ '999'
100
+ ),
101
+ )
102
+ @clibase.CLIErrorGuard
103
+ def RSARawDecrypt( # documentation is help/epilog/args # noqa: D103
104
+ *,
105
+ ctx: typer.Context,
106
+ ciphertext: str = typer.Argument(
107
+ ..., help='Integer ciphertext to decrypt, 1≤`ciphertext`<*modulus*'
108
+ ),
109
+ ) -> None:
110
+ config: transcrypto.TransConfig = ctx.obj
111
+ ciphertext_i: int = transcrypto.ParseInt(ciphertext, min_value=1)
112
+ key_path: str = transcrypto.RequireKeyPath(config, 'rsa')
113
+ rsa_priv: rsa.RSAPrivateKey = transcrypto.LoadObj(key_path, config.protect, rsa.RSAPrivateKey)
114
+ config.console.print(rsa_priv.RawDecrypt(ciphertext_i))
115
+
116
+
117
+ @rsa_app.command(
118
+ 'rawsign',
119
+ help='Raw sign *integer* `message` with private key (BEWARE: no OAEP/PSS padding or validation).',
120
+ epilog=(
121
+ 'Example:\n\n\n\n'
122
+ '$ poetry run transcrypto -p rsa-key.priv rsa rawsign 999\n\n'
123
+ '7632909108672871784'
124
+ ),
125
+ )
126
+ @clibase.CLIErrorGuard
127
+ def RSARawSign( # documentation is help/epilog/args # noqa: D103
128
+ *,
129
+ ctx: typer.Context,
130
+ message: str = typer.Argument(..., help='Integer message to sign, 1≤`message`<*modulus*'),
131
+ ) -> None:
132
+ config: transcrypto.TransConfig = ctx.obj
133
+ message_i: int = transcrypto.ParseInt(message, min_value=1)
134
+ key_path: str = transcrypto.RequireKeyPath(config, 'rsa')
135
+ rsa_priv: rsa.RSAPrivateKey = transcrypto.LoadObj(key_path, config.protect, rsa.RSAPrivateKey)
136
+ config.console.print(rsa_priv.RawSign(message_i))
137
+
138
+
139
+ @rsa_app.command(
140
+ 'rawverify',
141
+ help=(
142
+ 'Raw verify *integer* `signature` for *integer* `message` with public key '
143
+ '(BEWARE: no OAEP/PSS padding or validation).'
144
+ ),
145
+ epilog=(
146
+ 'Example:\n\n\n\n'
147
+ '$ poetry run transcrypto -p rsa-key.pub rsa rawverify 999 7632909108672871784\n\n'
148
+ 'RSA signature: OK\n\n'
149
+ '$ poetry run transcrypto -p rsa-key.pub rsa rawverify 999 7632909108672871785\n\n'
150
+ 'RSA signature: INVALID'
151
+ ),
152
+ )
153
+ @clibase.CLIErrorGuard
154
+ def RSARawVerify( # documentation is help/epilog/args # noqa: D103
155
+ *,
156
+ ctx: typer.Context,
157
+ message: str = typer.Argument(
158
+ ..., help='Integer message that was signed earlier, 1≤`message`<*modulus*'
159
+ ),
160
+ signature: str = typer.Argument(
161
+ ..., help='Integer putative signature for `message`, 1≤`signature`<*modulus*'
162
+ ),
163
+ ) -> None:
164
+ config: transcrypto.TransConfig = ctx.obj
165
+ message_i: int = transcrypto.ParseInt(message, min_value=1)
166
+ signature_i: int = transcrypto.ParseInt(signature, min_value=1)
167
+ key_path: str = transcrypto.RequireKeyPath(config, 'rsa')
168
+ rsa_pub: rsa.RSAPublicKey = rsa.RSAPublicKey.Copy(
169
+ transcrypto.LoadObj(key_path, config.protect, rsa.RSAPublicKey)
170
+ )
171
+ config.console.print(
172
+ 'RSA signature: '
173
+ + ('[green]OK[/]' if rsa_pub.RawVerify(message_i, signature_i) else '[red]INVALID[/]')
174
+ )
175
+
176
+
177
+ @rsa_app.command(
178
+ 'encrypt',
179
+ help='Encrypt `message` with public key.',
180
+ epilog=(
181
+ 'Example:\n\n\n\n'
182
+ '$ poetry run transcrypto -i bin -o b64 -p rsa-key.pub rsa encrypt "abcde" -a "xyz"\n\n'
183
+ 'AO6knI6xwq6TGR…Qy22jiFhXi1eQ=='
184
+ ),
185
+ )
186
+ @clibase.CLIErrorGuard
187
+ def RSAEncrypt( # documentation is help/epilog/args # noqa: D103
188
+ *,
189
+ ctx: typer.Context,
190
+ plaintext: str = typer.Argument(..., help='Message to encrypt'),
191
+ aad: str = typer.Option(
192
+ '',
193
+ '-a',
194
+ '--aad',
195
+ help='Associated data (optional; has to be separately sent to receiver/stored)',
196
+ ),
197
+ ) -> None:
198
+ config: transcrypto.TransConfig = ctx.obj
199
+ key_path: str = transcrypto.RequireKeyPath(config, 'rsa')
200
+ rsa_pub: rsa.RSAPublicKey = transcrypto.LoadObj(key_path, config.protect, rsa.RSAPublicKey)
201
+ aad_bytes: bytes | None = transcrypto.BytesFromText(aad, config.input_format) if aad else None
202
+ pt: bytes = transcrypto.BytesFromText(plaintext, config.input_format)
203
+ ct: bytes = rsa_pub.Encrypt(pt, associated_data=aad_bytes)
204
+ config.console.print(transcrypto.BytesToText(ct, config.output_format))
205
+
206
+
207
+ @rsa_app.command(
208
+ 'decrypt',
209
+ help='Decrypt `ciphertext` with private key.',
210
+ epilog=(
211
+ 'Example:\n\n\n\n'
212
+ '$ poetry run transcrypto -i b64 -o bin -p rsa-key.priv rsa decrypt -a eHl6 -- '
213
+ 'AO6knI6xwq6TGR…Qy22jiFhXi1eQ==\n\n'
214
+ 'abcde'
215
+ ),
216
+ )
217
+ @clibase.CLIErrorGuard
218
+ def RSADecrypt( # documentation is help/epilog/args # noqa: D103
219
+ *,
220
+ ctx: typer.Context,
221
+ ciphertext: str = typer.Argument(..., help='Ciphertext to decrypt'),
222
+ aad: str = typer.Option(
223
+ '',
224
+ '-a',
225
+ '--aad',
226
+ help='Associated data (optional; has to be exactly the same as used during encryption)',
227
+ ),
228
+ ) -> None:
229
+ config: transcrypto.TransConfig = ctx.obj
230
+ key_path: str = transcrypto.RequireKeyPath(config, 'rsa')
231
+ rsa_priv: rsa.RSAPrivateKey = transcrypto.LoadObj(key_path, config.protect, rsa.RSAPrivateKey)
232
+ aad_bytes: bytes | None = transcrypto.BytesFromText(aad, config.input_format) if aad else None
233
+ ct: bytes = transcrypto.BytesFromText(ciphertext, config.input_format)
234
+ pt: bytes = rsa_priv.Decrypt(ct, associated_data=aad_bytes)
235
+ config.console.print(transcrypto.BytesToText(pt, config.output_format))
236
+
237
+
238
+ @rsa_app.command(
239
+ 'sign',
240
+ help='Sign `message` with private key.',
241
+ epilog=(
242
+ 'Example:\n\n\n\n'
243
+ '$ poetry run transcrypto -i bin -o b64 -p rsa-key.priv rsa sign "xyz"\n\n'
244
+ '91TS7gC6LORiL…6RD23Aejsfxlw==' # cspell:disable-line
245
+ ),
246
+ )
247
+ @clibase.CLIErrorGuard
248
+ def RSASign( # documentation is help/epilog/args # noqa: D103
249
+ *,
250
+ ctx: typer.Context,
251
+ message: str = typer.Argument(..., help='Message to sign'),
252
+ aad: str = typer.Option(
253
+ '',
254
+ '-a',
255
+ '--aad',
256
+ help='Associated data (optional; has to be separately sent to receiver/stored)',
257
+ ),
258
+ ) -> None:
259
+ config: transcrypto.TransConfig = ctx.obj
260
+ key_path: str = transcrypto.RequireKeyPath(config, 'rsa')
261
+ rsa_priv: rsa.RSAPrivateKey = transcrypto.LoadObj(key_path, config.protect, rsa.RSAPrivateKey)
262
+ aad_bytes: bytes | None = transcrypto.BytesFromText(aad, config.input_format) if aad else None
263
+ pt: bytes = transcrypto.BytesFromText(message, config.input_format)
264
+ sig: bytes = rsa_priv.Sign(pt, associated_data=aad_bytes)
265
+ config.console.print(transcrypto.BytesToText(sig, config.output_format))
266
+
267
+
268
+ @rsa_app.command(
269
+ 'verify',
270
+ help='Verify `signature` for `message` with public key.',
271
+ epilog=(
272
+ 'Example:\n\n\n\n'
273
+ '$ poetry run transcrypto -i b64 -p rsa-key.pub rsa verify -- eHl6 '
274
+ '91TS7gC6LORiL…6RD23Aejsfxlw==\n\n' # cspell:disable-line
275
+ 'RSA signature: OK\n\n'
276
+ '$ poetry run transcrypto -i b64 -p rsa-key.pub rsa verify -- eLl6 '
277
+ '91TS7gC6LORiL…6RD23Aejsfxlw==\n\n' # cspell:disable-line
278
+ 'RSA signature: INVALID'
279
+ ),
280
+ )
281
+ @clibase.CLIErrorGuard
282
+ def RSAVerify( # documentation is help/epilog/args # noqa: D103
283
+ *,
284
+ ctx: typer.Context,
285
+ message: str = typer.Argument(..., help='Message that was signed earlier'),
286
+ signature: str = typer.Argument(..., help='Putative signature for `message`'),
287
+ aad: str = typer.Option(
288
+ '',
289
+ '-a',
290
+ '--aad',
291
+ help='Associated data (optional; has to be exactly the same as used during signing)',
292
+ ),
293
+ ) -> None:
294
+ config: transcrypto.TransConfig = ctx.obj
295
+ key_path: str = transcrypto.RequireKeyPath(config, 'rsa')
296
+ rsa_pub: rsa.RSAPublicKey = transcrypto.LoadObj(key_path, config.protect, rsa.RSAPublicKey)
297
+ aad_bytes: bytes | None = transcrypto.BytesFromText(aad, config.input_format) if aad else None
298
+ pt: bytes = transcrypto.BytesFromText(message, config.input_format)
299
+ sig: bytes = transcrypto.BytesFromText(signature, config.input_format)
300
+ config.console.print(
301
+ 'RSA signature: '
302
+ + ('[green]OK[/]' if rsa_pub.Verify(pt, sig, associated_data=aad_bytes) else '[red]INVALID[/]')
303
+ )
304
+
305
+
306
+ # ================================= "ELGAMAL" COMMAND ==============================================
307
+
308
+
309
+ eg_app = typer.Typer(
310
+ no_args_is_help=True,
311
+ help=(
312
+ 'El-Gamal asymmetric cryptography. '
313
+ 'All methods require file key(s) as `-p`/`--key-path` (see provided examples). '
314
+ 'All non-int inputs are raw, or you can use `--input-format <hex|b64|bin>`. '
315
+ 'Attention: if you provide `-a`/`--aad` (associated data, AAD), '
316
+ 'you will need to provide the same AAD when decrypting/verifying and it is NOT included '
317
+ 'in the `ciphertext`/CT or `signature` returned by these methods! '
318
+ 'No measures are taken here to prevent timing attacks.'
319
+ ),
320
+ )
321
+ transcrypto.app.add_typer(eg_app, name='elgamal')
322
+
323
+
324
+ @eg_app.command(
325
+ 'shared',
326
+ help=(
327
+ 'Generate a shared El-Gamal key with `bits` prime modulus size, which is the '
328
+ 'first step in key generation. '
329
+ 'The shared key can safely be used by any number of users to generate their '
330
+ 'private/public key pairs (with the `new` command). The shared keys are "public".'
331
+ ),
332
+ epilog=(
333
+ 'Example:\n\n\n\n'
334
+ '$ poetry run transcrypto -p eg-key elgamal shared --bits 64 '
335
+ '# NEVER use such a small key: example only!\n\n'
336
+ "El-Gamal shared key saved to 'eg-key.shared'"
337
+ ),
338
+ )
339
+ @clibase.CLIErrorGuard
340
+ def ElGamalShared( # documentation is help/epilog/args # noqa: D103
341
+ *,
342
+ ctx: typer.Context,
343
+ bits: int = typer.Option(
344
+ 3332,
345
+ '-b',
346
+ '--bits',
347
+ min=16,
348
+ help='Prime modulus (`p`) size in bits, ≥16; the default (3332) is a safe size',
349
+ ),
350
+ ) -> None:
351
+ config: transcrypto.TransConfig = ctx.obj
352
+ base_path: str = transcrypto.RequireKeyPath(config, 'elgamal')
353
+ shared_eg: elgamal.ElGamalSharedPublicKey = elgamal.ElGamalSharedPublicKey.NewShared(bits)
354
+ transcrypto.SaveObj(shared_eg, base_path + '.shared', config.protect)
355
+ config.console.print(f'El-Gamal shared key saved to {base_path + ".shared"!r}')
356
+
357
+
358
+ @eg_app.command(
359
+ 'new',
360
+ help='Generate an individual El-Gamal private/public key pair from a shared key.',
361
+ epilog=(
362
+ 'Example:\n\n\n\n'
363
+ '$ poetry run transcrypto -p eg-key elgamal new\n\n'
364
+ "El-Gamal private/public keys saved to 'eg-key.priv/.pub'"
365
+ ),
366
+ )
367
+ @clibase.CLIErrorGuard
368
+ def ElGamalNew(*, ctx: typer.Context) -> None: # documentation is help/epilog/args # noqa: D103
369
+ config: transcrypto.TransConfig = ctx.obj
370
+ base_path: str = transcrypto.RequireKeyPath(config, 'elgamal')
371
+ shared_eg: elgamal.ElGamalSharedPublicKey = transcrypto.LoadObj(
372
+ base_path + '.shared', config.protect, elgamal.ElGamalSharedPublicKey
373
+ )
374
+ eg_priv: elgamal.ElGamalPrivateKey = elgamal.ElGamalPrivateKey.New(shared_eg)
375
+ eg_pub: elgamal.ElGamalPublicKey = elgamal.ElGamalPublicKey.Copy(eg_priv)
376
+ transcrypto.SaveObj(eg_priv, base_path + '.priv', config.protect)
377
+ transcrypto.SaveObj(eg_pub, base_path + '.pub', config.protect)
378
+ config.console.print(f'El-Gamal private/public keys saved to {base_path + ".priv/.pub"!r}')
379
+
380
+
381
+ @eg_app.command(
382
+ 'rawencrypt',
383
+ help=(
384
+ 'Raw encrypt *integer* `message` with public key '
385
+ '(BEWARE: no ECIES-style KEM/DEM padding or validation).'
386
+ ),
387
+ epilog=(
388
+ 'Example:\n\n\n\n'
389
+ '$ poetry run transcrypto -p eg-key.pub elgamal rawencrypt 999\n\n'
390
+ '2948854810728206041:15945988196340032688'
391
+ ),
392
+ )
393
+ @clibase.CLIErrorGuard
394
+ def ElGamalRawEncrypt( # documentation is help/epilog/args # noqa: D103
395
+ *,
396
+ ctx: typer.Context,
397
+ message: str = typer.Argument(..., help='Integer message to encrypt, 1≤`message`<*modulus*'),
398
+ ) -> None:
399
+ config: transcrypto.TransConfig = ctx.obj
400
+ message_i: int = transcrypto.ParseInt(message, min_value=1)
401
+ key_path: str = transcrypto.RequireKeyPath(config, 'elgamal')
402
+ eg_pub: elgamal.ElGamalPublicKey = elgamal.ElGamalPublicKey.Copy(
403
+ transcrypto.LoadObj(key_path, config.protect, elgamal.ElGamalPublicKey)
404
+ )
405
+ c1: int
406
+ c2: int
407
+ c1, c2 = eg_pub.RawEncrypt(message_i)
408
+ config.console.print(f'{c1}:{c2}')
409
+
410
+
411
+ @eg_app.command(
412
+ 'rawdecrypt',
413
+ help=(
414
+ 'Raw decrypt *integer* `ciphertext` with private key '
415
+ '(BEWARE: no ECIES-style KEM/DEM padding or validation).'
416
+ ),
417
+ epilog=(
418
+ 'Example:\n\n\n\n'
419
+ '$ poetry run transcrypto -p eg-key.priv elgamal rawdecrypt '
420
+ '2948854810728206041:15945988196340032688\n\n'
421
+ '999'
422
+ ),
423
+ )
424
+ @clibase.CLIErrorGuard
425
+ def ElGamalRawDecrypt( # documentation is help/epilog/args # noqa: D103
426
+ *,
427
+ ctx: typer.Context,
428
+ ciphertext: str = typer.Argument(
429
+ ...,
430
+ help=(
431
+ 'Integer ciphertext to decrypt; expects `c1:c2` format with 2 integers, `c1`,`c2`<*modulus*'
432
+ ),
433
+ ),
434
+ ) -> None:
435
+ config: transcrypto.TransConfig = ctx.obj
436
+ ciphertext_i: tuple[int, int] = transcrypto.ParseIntPairCLI(ciphertext)
437
+ key_path: str = transcrypto.RequireKeyPath(config, 'elgamal')
438
+ eg_priv: elgamal.ElGamalPrivateKey = transcrypto.LoadObj(
439
+ key_path, config.protect, elgamal.ElGamalPrivateKey
440
+ )
441
+ config.console.print(eg_priv.RawDecrypt(ciphertext_i))
442
+
443
+
444
+ @eg_app.command(
445
+ 'rawsign',
446
+ help=(
447
+ 'Raw sign *integer* message with private key '
448
+ '(BEWARE: no ECIES-style KEM/DEM padding or validation). '
449
+ 'Output will 2 *integers* in a `s1:s2` format.'
450
+ ),
451
+ epilog=(
452
+ 'Example:\n\n\n\n'
453
+ '$ poetry run transcrypto -p eg-key.priv elgamal rawsign 999\n\n'
454
+ '4674885853217269088:14532144906178302633'
455
+ ),
456
+ )
457
+ @clibase.CLIErrorGuard
458
+ def ElGamalRawSign( # documentation is help/epilog/args # noqa: D103
459
+ *,
460
+ ctx: typer.Context,
461
+ message: str = typer.Argument(..., help='Integer message to sign, 1≤`message`<*modulus*'),
462
+ ) -> None:
463
+ config: transcrypto.TransConfig = ctx.obj
464
+ message_i: int = transcrypto.ParseInt(message, min_value=1)
465
+ key_path: str = transcrypto.RequireKeyPath(config, 'elgamal')
466
+ eg_priv: elgamal.ElGamalPrivateKey = transcrypto.LoadObj(
467
+ key_path, config.protect, elgamal.ElGamalPrivateKey
468
+ )
469
+ s1: int
470
+ s2: int
471
+ s1, s2 = eg_priv.RawSign(message_i)
472
+ config.console.print(f'{s1}:{s2}')
473
+
474
+
475
+ @eg_app.command(
476
+ 'rawverify',
477
+ help=(
478
+ 'Raw verify *integer* `signature` for *integer* `message` with public key '
479
+ '(BEWARE: no ECIES-style KEM/DEM padding or validation).'
480
+ ),
481
+ epilog=(
482
+ 'Example:\n\n\n\n'
483
+ '$ poetry run transcrypto -p eg-key.pub elgamal rawverify 999 '
484
+ '4674885853217269088:14532144906178302633\n\n'
485
+ 'El-Gamal signature: OK\n\n'
486
+ '$ poetry run transcrypto -p eg-key.pub elgamal rawverify 999 '
487
+ '4674885853217269088:14532144906178302632\n\n'
488
+ 'El-Gamal signature: INVALID'
489
+ ),
490
+ )
491
+ @clibase.CLIErrorGuard
492
+ def ElGamalRawVerify( # documentation is help/epilog/args # noqa: D103
493
+ *,
494
+ ctx: typer.Context,
495
+ message: str = typer.Argument(
496
+ ..., help='Integer message that was signed earlier, 1≤`message`<*modulus*'
497
+ ),
498
+ signature: str = typer.Argument(
499
+ ...,
500
+ help=(
501
+ 'Integer putative signature for `message`; expects `s1:s2` format with 2 integers, '
502
+ '`s1`,`s2`<*modulus*'
503
+ ),
504
+ ),
505
+ ) -> None:
506
+ config: transcrypto.TransConfig = ctx.obj
507
+ message_i: int = transcrypto.ParseInt(message, min_value=1)
508
+ signature_i: tuple[int, int] = transcrypto.ParseIntPairCLI(signature)
509
+ key_path: str = transcrypto.RequireKeyPath(config, 'elgamal')
510
+ eg_pub: elgamal.ElGamalPublicKey = elgamal.ElGamalPublicKey.Copy(
511
+ transcrypto.LoadObj(key_path, config.protect, elgamal.ElGamalPublicKey)
512
+ )
513
+ config.console.print(
514
+ 'El-Gamal signature: '
515
+ + ('[green]OK[/]' if eg_pub.RawVerify(message_i, signature_i) else '[red]INVALID[/]')
516
+ )
517
+
518
+
519
+ @eg_app.command(
520
+ 'encrypt',
521
+ help='Encrypt `message` with public key.',
522
+ epilog=(
523
+ 'Example:\n\n\n\n'
524
+ '$ poetry run transcrypto -i bin -o b64 -p eg-key.pub elgamal encrypt "abcde" -a "xyz"\n\n'
525
+ 'CdFvoQ_IIPFPZLua…kqjhcUTspISxURg==' # cspell:disable-line
526
+ ),
527
+ )
528
+ @clibase.CLIErrorGuard
529
+ def ElGamalEncrypt( # documentation is help/epilog/args # noqa: D103
530
+ *,
531
+ ctx: typer.Context,
532
+ plaintext: str = typer.Argument(..., help='Message to encrypt'),
533
+ aad: str = typer.Option(
534
+ '',
535
+ '-a',
536
+ '--aad',
537
+ help='Associated data (optional; has to be separately sent to receiver/stored)',
538
+ ),
539
+ ) -> None:
540
+ config: transcrypto.TransConfig = ctx.obj
541
+ key_path: str = transcrypto.RequireKeyPath(config, 'elgamal')
542
+ eg_pub: elgamal.ElGamalPublicKey = transcrypto.LoadObj(
543
+ key_path, config.protect, elgamal.ElGamalPublicKey
544
+ )
545
+ aad_bytes: bytes | None = transcrypto.BytesFromText(aad, config.input_format) if aad else None
546
+ pt: bytes = transcrypto.BytesFromText(plaintext, config.input_format)
547
+ ct: bytes = eg_pub.Encrypt(pt, associated_data=aad_bytes)
548
+ config.console.print(transcrypto.BytesToText(ct, config.output_format))
549
+
550
+
551
+ @eg_app.command(
552
+ 'decrypt',
553
+ help='Decrypt `ciphertext` with private key.',
554
+ epilog=(
555
+ 'Example:\n\n\n\n'
556
+ '$ poetry run transcrypto -i b64 -o bin -p eg-key.priv elgamal decrypt -a eHl6 -- '
557
+ 'CdFvoQ_IIPFPZLua…kqjhcUTspISxURg==\n\n' # cspell:disable-line
558
+ 'abcde'
559
+ ),
560
+ )
561
+ @clibase.CLIErrorGuard
562
+ def ElGamalDecrypt( # documentation is help/epilog/args # noqa: D103
563
+ *,
564
+ ctx: typer.Context,
565
+ ciphertext: str = typer.Argument(..., help='Ciphertext to decrypt'),
566
+ aad: str = typer.Option(
567
+ '',
568
+ '-a',
569
+ '--aad',
570
+ help='Associated data (optional; has to be exactly the same as used during encryption)',
571
+ ),
572
+ ) -> None:
573
+ config: transcrypto.TransConfig = ctx.obj
574
+ key_path: str = transcrypto.RequireKeyPath(config, 'elgamal')
575
+ eg_priv: elgamal.ElGamalPrivateKey = transcrypto.LoadObj(
576
+ key_path, config.protect, elgamal.ElGamalPrivateKey
577
+ )
578
+ aad_bytes: bytes | None = transcrypto.BytesFromText(aad, config.input_format) if aad else None
579
+ ct: bytes = transcrypto.BytesFromText(ciphertext, config.input_format)
580
+ pt: bytes = eg_priv.Decrypt(ct, associated_data=aad_bytes)
581
+ config.console.print(transcrypto.BytesToText(pt, config.output_format))
582
+
583
+
584
+ @eg_app.command(
585
+ 'sign',
586
+ help='Sign message with private key.',
587
+ epilog=(
588
+ 'Example:\n\n\n\n'
589
+ '$ poetry run transcrypto -i bin -o b64 -p eg-key.priv elgamal sign "xyz"\n\n'
590
+ 'Xl4hlYK8SHVGw…0fCKJE1XVzA==' # cspell:disable-line
591
+ ),
592
+ )
593
+ @clibase.CLIErrorGuard
594
+ def ElGamalSign( # documentation is help/epilog/args # noqa: D103
595
+ *,
596
+ ctx: typer.Context,
597
+ message: str = typer.Argument(..., help='Message to sign'),
598
+ aad: str = typer.Option(
599
+ '',
600
+ '-a',
601
+ '--aad',
602
+ help='Associated data (optional; has to be separately sent to receiver/stored)',
603
+ ),
604
+ ) -> None:
605
+ config: transcrypto.TransConfig = ctx.obj
606
+ key_path: str = transcrypto.RequireKeyPath(config, 'elgamal')
607
+ eg_priv: elgamal.ElGamalPrivateKey = transcrypto.LoadObj(
608
+ key_path, config.protect, elgamal.ElGamalPrivateKey
609
+ )
610
+ aad_bytes: bytes | None = transcrypto.BytesFromText(aad, config.input_format) if aad else None
611
+ pt: bytes = transcrypto.BytesFromText(message, config.input_format)
612
+ sig: bytes = eg_priv.Sign(pt, associated_data=aad_bytes)
613
+ config.console.print(transcrypto.BytesToText(sig, config.output_format))
614
+
615
+
616
+ @eg_app.command(
617
+ 'verify',
618
+ help='Verify `signature` for `message` with public key.',
619
+ epilog=(
620
+ 'Example:\n\n\n\n'
621
+ '$ poetry run transcrypto -i b64 -p eg-key.pub elgamal verify -- eHl6 '
622
+ 'Xl4hlYK8SHVGw…0fCKJE1XVzA==\n\n' # cspell:disable-line
623
+ 'El-Gamal signature: OK\n\n'
624
+ '$ poetry run transcrypto -i b64 -p eg-key.pub elgamal verify -- eLl6 '
625
+ 'Xl4hlYK8SHVGw…0fCKJE1XVzA==\n\n' # cspell:disable-line
626
+ 'El-Gamal signature: INVALID'
627
+ ),
628
+ )
629
+ @clibase.CLIErrorGuard
630
+ def ElGamalVerify( # documentation is help/epilog/args # noqa: D103
631
+ *,
632
+ ctx: typer.Context,
633
+ message: str = typer.Argument(..., help='Message that was signed earlier'),
634
+ signature: str = typer.Argument(..., help='Putative signature for `message`'),
635
+ aad: str = typer.Option(
636
+ '',
637
+ '-a',
638
+ '--aad',
639
+ help='Associated data (optional; has to be exactly the same as used during signing)',
640
+ ),
641
+ ) -> None:
642
+ config: transcrypto.TransConfig = ctx.obj
643
+ key_path: str = transcrypto.RequireKeyPath(config, 'elgamal')
644
+ eg_pub: elgamal.ElGamalPublicKey = transcrypto.LoadObj(
645
+ key_path, config.protect, elgamal.ElGamalPublicKey
646
+ )
647
+ aad_bytes: bytes | None = transcrypto.BytesFromText(aad, config.input_format) if aad else None
648
+ pt: bytes = transcrypto.BytesFromText(message, config.input_format)
649
+ sig: bytes = transcrypto.BytesFromText(signature, config.input_format)
650
+ config.console.print(
651
+ 'El-Gamal signature: '
652
+ + ('[green]OK[/]' if eg_pub.Verify(pt, sig, associated_data=aad_bytes) else '[red]INVALID[/]')
653
+ )
654
+
655
+
656
+ # ================================== "DSA" COMMAND =================================================
657
+
658
+
659
+ dsa_app = typer.Typer(
660
+ no_args_is_help=True,
661
+ help=(
662
+ 'DSA (Digital Signature Algorithm) asymmetric signing/verifying. '
663
+ 'All methods require file key(s) as `-p`/`--key-path` (see provided examples). '
664
+ 'All non-int inputs are raw, or you can use `--input-format <hex|b64|bin>`. '
665
+ 'Attention: if you provide `-a`/`--aad` (associated data, AAD), '
666
+ 'you will need to provide the same AAD when decrypting/verifying and it is NOT included '
667
+ 'in the `signature` returned by these methods! '
668
+ 'No measures are taken here to prevent timing attacks.'
669
+ ),
670
+ )
671
+ transcrypto.app.add_typer(dsa_app, name='dsa')
672
+
673
+
674
+ @dsa_app.command(
675
+ 'shared',
676
+ help=(
677
+ 'Generate a shared DSA key with `p-bits`/`q-bits` prime modulus sizes, which is '
678
+ 'the first step in key generation. `q-bits` should be larger than the secrets that '
679
+ 'will be protected and `p-bits` should be much larger than `q-bits` (e.g. 4096/544). '
680
+ 'The shared key can safely be used by any number of users to generate their '
681
+ 'private/public key pairs (with the `new` command). The shared keys are "public".'
682
+ ),
683
+ epilog=(
684
+ 'Example:\n\n\n\n'
685
+ '$ poetry run transcrypto -p dsa-key dsa shared --p-bits 128 --q-bits 32 '
686
+ '# NEVER use such a small key: example only!\n\n'
687
+ "DSA shared key saved to 'dsa-key.shared'"
688
+ ),
689
+ )
690
+ @clibase.CLIErrorGuard
691
+ def DSAShared( # documentation is help/epilog/args # noqa: D103
692
+ *,
693
+ ctx: typer.Context,
694
+ p_bits: int = typer.Option(
695
+ 4096,
696
+ '-b',
697
+ '--p-bits',
698
+ min=16,
699
+ help='Prime modulus (`p`) size in bits, ≥16; the default (4096) is a safe size',
700
+ ),
701
+ q_bits: int = typer.Option(
702
+ 544,
703
+ '-q',
704
+ '--q-bits',
705
+ min=8,
706
+ help=(
707
+ 'Prime modulus (`q`) size in bits, ≥8; the default (544) is a safe size ***IFF*** you '
708
+ 'are protecting symmetric keys or regular hashes'
709
+ ),
710
+ ),
711
+ ) -> None:
712
+ config: transcrypto.TransConfig = ctx.obj
713
+ base_path: str = transcrypto.RequireKeyPath(config, 'dsa')
714
+ dsa_shared: dsa.DSASharedPublicKey = dsa.DSASharedPublicKey.NewShared(p_bits, q_bits)
715
+ transcrypto.SaveObj(dsa_shared, base_path + '.shared', config.protect)
716
+ config.console.print(f'DSA shared key saved to {base_path + ".shared"!r}')
717
+
718
+
719
+ @dsa_app.command(
720
+ 'new',
721
+ help='Generate an individual DSA private/public key pair from a shared key.',
722
+ epilog=(
723
+ 'Example:\n\n\n\n'
724
+ '$ poetry run transcrypto -p dsa-key dsa new\n\n'
725
+ "DSA private/public keys saved to 'dsa-key.priv/.pub'"
726
+ ),
727
+ )
728
+ @clibase.CLIErrorGuard
729
+ def DSANew(*, ctx: typer.Context) -> None: # documentation is help/epilog/args # noqa: D103
730
+ config: transcrypto.TransConfig = ctx.obj
731
+ base_path: str = transcrypto.RequireKeyPath(config, 'dsa')
732
+ dsa_shared: dsa.DSASharedPublicKey = transcrypto.LoadObj(
733
+ base_path + '.shared', config.protect, dsa.DSASharedPublicKey
734
+ )
735
+ dsa_priv: dsa.DSAPrivateKey = dsa.DSAPrivateKey.New(dsa_shared)
736
+ dsa_pub: dsa.DSAPublicKey = dsa.DSAPublicKey.Copy(dsa_priv)
737
+ transcrypto.SaveObj(dsa_priv, base_path + '.priv', config.protect)
738
+ transcrypto.SaveObj(dsa_pub, base_path + '.pub', config.protect)
739
+ config.console.print(f'DSA private/public keys saved to {base_path + ".priv/.pub"!r}')
740
+
741
+
742
+ @dsa_app.command(
743
+ 'rawsign',
744
+ help=(
745
+ 'Raw sign *integer* message with private key (BEWARE: no ECDSA/EdDSA padding or validation). '
746
+ 'Output will 2 *integers* in a `s1:s2` format.'
747
+ ),
748
+ epilog=(
749
+ 'Example:\n\n\n\n'
750
+ '$ poetry run transcrypto -p dsa-key.priv dsa rawsign 999\n\n'
751
+ '2395961484:3435572290'
752
+ ),
753
+ )
754
+ @clibase.CLIErrorGuard
755
+ def DSARawSign( # documentation is help/epilog/args # noqa: D103
756
+ *,
757
+ ctx: typer.Context,
758
+ message: str = typer.Argument(..., help='Integer message to sign, 1≤`message`<`q`'),
759
+ ) -> None:
760
+ config: transcrypto.TransConfig = ctx.obj
761
+ key_path: str = transcrypto.RequireKeyPath(config, 'dsa')
762
+ dsa_priv: dsa.DSAPrivateKey = transcrypto.LoadObj(key_path, config.protect, dsa.DSAPrivateKey)
763
+ message_i: int = transcrypto.ParseInt(message, min_value=1)
764
+ m: int = message_i % dsa_priv.prime_seed
765
+ s1: int
766
+ s2: int
767
+ s1, s2 = dsa_priv.RawSign(m)
768
+ config.console.print(f'{s1}:{s2}')
769
+
770
+
771
+ @dsa_app.command(
772
+ 'rawverify',
773
+ help=(
774
+ 'Raw verify *integer* `signature` for *integer* `message` with public key '
775
+ '(BEWARE: no ECDSA/EdDSA padding or validation).'
776
+ ),
777
+ epilog=(
778
+ 'Example:\n\n\n\n'
779
+ '$ poetry run transcrypto -p dsa-key.pub dsa rawverify 999 2395961484:3435572290\n\n'
780
+ 'DSA signature: OK\n\n'
781
+ '$ poetry run transcrypto -p dsa-key.pub dsa rawverify 999 2395961484:3435572291\n\n'
782
+ 'DSA signature: INVALID'
783
+ ),
784
+ )
785
+ @clibase.CLIErrorGuard
786
+ def DSARawVerify( # documentation is help/epilog/args # noqa: D103
787
+ *,
788
+ ctx: typer.Context,
789
+ message: str = typer.Argument(
790
+ ..., help='Integer message that was signed earlier, 1≤`message`<`q`'
791
+ ),
792
+ signature: str = typer.Argument(
793
+ ...,
794
+ help=(
795
+ 'Integer putative signature for `message`; expects `s1:s2` format with 2 integers, '
796
+ '`s1`,`s2`<`q`'
797
+ ),
798
+ ),
799
+ ) -> None:
800
+ config: transcrypto.TransConfig = ctx.obj
801
+ key_path: str = transcrypto.RequireKeyPath(config, 'dsa')
802
+ dsa_pub: dsa.DSAPublicKey = dsa.DSAPublicKey.Copy(
803
+ transcrypto.LoadObj(key_path, config.protect, dsa.DSAPublicKey)
804
+ )
805
+ message_i: int = transcrypto.ParseInt(message, min_value=1)
806
+ signature_i: tuple[int, int] = transcrypto.ParseIntPairCLI(signature)
807
+ m: int = message_i % dsa_pub.prime_seed
808
+ config.console.print(
809
+ 'DSA signature: ' + ('[green]OK[/]' if dsa_pub.RawVerify(m, signature_i) else '[red]INVALID[/]')
810
+ )
811
+
812
+
813
+ @dsa_app.command(
814
+ 'sign',
815
+ help='Sign message with private key.',
816
+ epilog=(
817
+ 'Example:\n\n\n\n'
818
+ '$ poetry run transcrypto -i bin -o b64 -p dsa-key.priv dsa sign "xyz"\n\n'
819
+ 'yq8InJVpViXh9…BD4par2XuA='
820
+ ),
821
+ )
822
+ @clibase.CLIErrorGuard
823
+ def DSASign( # documentation is help/epilog/args # noqa: D103
824
+ *,
825
+ ctx: typer.Context,
826
+ message: str = typer.Argument(..., help='Message to sign'),
827
+ aad: str = typer.Option(
828
+ '',
829
+ '-a',
830
+ '--aad',
831
+ help='Associated data (optional; has to be separately sent to receiver/stored)',
832
+ ),
833
+ ) -> None:
834
+ config: transcrypto.TransConfig = ctx.obj
835
+ key_path: str = transcrypto.RequireKeyPath(config, 'dsa')
836
+ dsa_priv: dsa.DSAPrivateKey = transcrypto.LoadObj(key_path, config.protect, dsa.DSAPrivateKey)
837
+ aad_bytes: bytes | None = transcrypto.BytesFromText(aad, config.input_format) if aad else None
838
+ pt: bytes = transcrypto.BytesFromText(message, config.input_format)
839
+ sig: bytes = dsa_priv.Sign(pt, associated_data=aad_bytes)
840
+ config.console.print(transcrypto.BytesToText(sig, config.output_format))
841
+
842
+
843
+ @dsa_app.command(
844
+ 'verify',
845
+ help='Verify `signature` for `message` with public key.',
846
+ epilog=(
847
+ 'Example:\n\n\n\n'
848
+ '$ poetry run transcrypto -i b64 -p dsa-key.pub dsa verify -- '
849
+ 'eHl6 yq8InJVpViXh9…BD4par2XuA=\n\n'
850
+ 'DSA signature: OK\n\n'
851
+ '$ poetry run transcrypto -i b64 -p dsa-key.pub dsa verify -- '
852
+ 'eLl6 yq8InJVpViXh9…BD4par2XuA=\n\n'
853
+ 'DSA signature: INVALID'
854
+ ),
855
+ )
856
+ @clibase.CLIErrorGuard
857
+ def DSAVerify( # documentation is help/epilog/args # noqa: D103
858
+ *,
859
+ ctx: typer.Context,
860
+ message: str = typer.Argument(..., help='Message that was signed earlier'),
861
+ signature: str = typer.Argument(..., help='Putative signature for `message`'),
862
+ aad: str = typer.Option(
863
+ '',
864
+ '-a',
865
+ '--aad',
866
+ help='Associated data (optional; has to be exactly the same as used during signing)',
867
+ ),
868
+ ) -> None:
869
+ config: transcrypto.TransConfig = ctx.obj
870
+ key_path: str = transcrypto.RequireKeyPath(config, 'dsa')
871
+ dsa_pub: dsa.DSAPublicKey = transcrypto.LoadObj(key_path, config.protect, dsa.DSAPublicKey)
872
+ aad_bytes: bytes | None = transcrypto.BytesFromText(aad, config.input_format) if aad else None
873
+ pt: bytes = transcrypto.BytesFromText(message, config.input_format)
874
+ sig: bytes = transcrypto.BytesFromText(signature, config.input_format)
875
+ config.console.print(
876
+ 'DSA signature: '
877
+ + ('[green]OK[/]' if dsa_pub.Verify(pt, sig, associated_data=aad_bytes) else '[red]INVALID[/]')
878
+ )