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