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.
@@ -1,6 +1,6 @@
1
1
  # SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
2
2
  # SPDX-License-Identifier: Apache-2.0
3
- """Balparda's TransCrypto command line interface.
3
+ """Balparda's TransCrypto command line interface (CLI).
4
4
 
5
5
  See <transcrypto.md> for documentation on how to use. Quick examples:
6
6
 
@@ -92,30 +92,40 @@ from __future__ import annotations
92
92
 
93
93
  import dataclasses
94
94
  import enum
95
- import glob
96
95
  import logging
97
96
  import pathlib
98
- import re
99
- from typing import Any
100
97
 
101
98
  import click
102
99
  import typer
103
100
 
101
+ from transcrypto.cli import clibase
102
+
104
103
  from . import (
105
104
  __version__,
106
105
  aes,
107
106
  base,
108
- dsa,
109
- elgamal,
110
- modmath,
111
- rsa,
112
- sss,
113
107
  )
114
108
 
115
- _HEX_RE = re.compile(r'^[0-9a-fA-F]+$')
109
+
110
+ class IOFormat(enum.Enum):
111
+ """Input/output data format for CLI commands."""
112
+
113
+ hex = 'hex'
114
+ b64 = 'b64'
115
+ bin = 'bin'
116
+
117
+
118
+ @dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
119
+ class TransConfig(clibase.CLIConfig):
120
+ """CLI global context, storing the configuration."""
121
+
122
+ input_format: IOFormat
123
+ output_format: IOFormat
124
+ key_path: pathlib.Path | None
125
+ protect: str | None
116
126
 
117
127
 
118
- def _RequireKeyPath(config: TransConfig, command: str, /) -> str:
128
+ def RequireKeyPath(config: TransConfig, command: str, /) -> str:
119
129
  """Ensure key path is provided and valid.
120
130
 
121
131
  Args:
@@ -136,7 +146,7 @@ def _RequireKeyPath(config: TransConfig, command: str, /) -> str:
136
146
  return str(config.key_path)
137
147
 
138
148
 
139
- def _ParseInt(s: str, /, *, min_value: int | None = None) -> int:
149
+ def ParseInt(s: str, /, *, min_value: int | None = None) -> int:
140
150
  """Parse int, try to determine if binary, octal, decimal, or hexadecimal.
141
151
 
142
152
  Args:
@@ -171,7 +181,7 @@ def _ParseInt(s: str, /, *, min_value: int | None = None) -> int:
171
181
  raise base.InputError(f'invalid int: {s!r}') from err
172
182
 
173
183
 
174
- def _ParseIntPairCLI(s: str, /) -> tuple[int, int]:
184
+ def ParseIntPairCLI(s: str, /) -> tuple[int, int]:
175
185
  """Parse a CLI int pair of the form `a:b`.
176
186
 
177
187
  Args:
@@ -187,10 +197,10 @@ def _ParseIntPairCLI(s: str, /) -> tuple[int, int]:
187
197
  parts: list[str] = s.split(':')
188
198
  if len(parts) != 2: # noqa: PLR2004
189
199
  raise base.InputError(f'invalid int(s): {s!r} (expected a:b)')
190
- return (_ParseInt(parts[0]), _ParseInt(parts[1]))
200
+ return (ParseInt(parts[0]), ParseInt(parts[1]))
191
201
 
192
202
 
193
- def _BytesFromText(text: str, fmt: IOFormat, /) -> bytes:
203
+ def BytesFromText(text: str, fmt: IOFormat, /) -> bytes:
194
204
  """Parse bytes according to `fmt` (IOFormat.hex|b64|bin).
195
205
 
196
206
  Args:
@@ -210,7 +220,7 @@ def _BytesFromText(text: str, fmt: IOFormat, /) -> bytes:
210
220
  return base.EncodedToBytes(text)
211
221
 
212
222
 
213
- def _BytesToText(b: bytes, fmt: IOFormat, /) -> str:
223
+ def BytesToText(b: bytes, fmt: IOFormat, /) -> str:
214
224
  """Format bytes according to `fmt` (IOFormat.hex|b64|bin).
215
225
 
216
226
  Args:
@@ -230,11 +240,11 @@ def _BytesToText(b: bytes, fmt: IOFormat, /) -> str:
230
240
  return base.BytesToEncoded(b)
231
241
 
232
242
 
233
- def _SaveObj(obj: Any, path: str, password: str | None, /) -> None: # noqa: ANN401
243
+ def SaveObj(obj: base.CryptoKey, path: str, password: str | None, /) -> None:
234
244
  """Save object.
235
245
 
236
246
  Args:
237
- obj (Any): object
247
+ obj (base.CryptoKey): object
238
248
  path (str): path
239
249
  password (str | None): password
240
250
 
@@ -244,7 +254,7 @@ def _SaveObj(obj: Any, path: str, password: str | None, /) -> None: # noqa: ANN
244
254
  logging.info('saved object: %s (%s)', path, base.HumanizedBytes(len(blob)))
245
255
 
246
256
 
247
- def _LoadObj[T](path: str, password: str | None, expect: type[T], /) -> T:
257
+ def LoadObj[T](path: str, password: str | None, expect: type[T], /) -> T:
248
258
  """Load object.
249
259
 
250
260
  Args:
@@ -268,24 +278,6 @@ def _LoadObj[T](path: str, password: str | None, expect: type[T], /) -> T:
268
278
  return obj
269
279
 
270
280
 
271
- class IOFormat(enum.Enum):
272
- """Input/output data format for CLI commands."""
273
-
274
- hex = 'hex'
275
- b64 = 'b64'
276
- bin = 'bin'
277
-
278
-
279
- @dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
280
- class TransConfig(base.CLIConfig):
281
- """CLI global context, storing the configuration."""
282
-
283
- input_format: IOFormat
284
- output_format: IOFormat
285
- key_path: pathlib.Path | None
286
- protect: str | None
287
-
288
-
289
281
  # ============================= "TRANSCRYPTO"/ROOT COMMAND =========================================
290
282
 
291
283
 
@@ -379,6 +371,7 @@ def Run() -> None:
379
371
  invoke_without_command=True, # have only one; this is the "constructor"
380
372
  help='transcrypto: CLI for number theory, hash, AES, RSA, El-Gamal, DSA, bidding, SSS, and more.',
381
373
  ) # keep message in sync with app.help
374
+ @clibase.CLIErrorGuard
382
375
  def Main( # documentation is help/epilog/args # noqa: D103
383
376
  *,
384
377
  ctx: click.Context, # global context
@@ -434,10 +427,10 @@ def Main( # documentation is help/epilog/args # noqa: D103
434
427
  if version:
435
428
  typer.echo(__version__)
436
429
  raise typer.Exit(0)
437
- console, verbose, color = base.InitLogging(
430
+ console, verbose, color = clibase.InitLogging(
438
431
  verbose,
439
432
  color=color,
440
- include_process=False, # decide if you want process names in logs
433
+ include_process=False,
441
434
  )
442
435
  # create context with the arguments we received.
443
436
  ctx.obj = TransConfig(
@@ -451,1957 +444,18 @@ def Main( # documentation is help/epilog/args # noqa: D103
451
444
  )
452
445
 
453
446
 
454
- # =============================== "PRIME"-like COMMANDS ============================================
455
-
456
-
457
- @app.command(
458
- 'isprime',
459
- help='Primality test with safe defaults, useful for any integer size.',
460
- epilog=(
461
- 'Example:\n\n\n\n'
462
- '$ poetry run transcrypto isprime 2305843009213693951\n\n'
463
- 'True\n\n'
464
- '$ poetry run transcrypto isprime 2305843009213693953\n\n'
465
- 'False'
466
- ),
467
- )
468
- @base.CLIErrorGuard
469
- def IsPrimeCLI( # documentation is help/epilog/args # noqa: D103
470
- *,
471
- ctx: typer.Context,
472
- n: str = typer.Argument(..., help='Integer to test, ≥ 1'),
473
- ) -> None:
474
- config: TransConfig = ctx.obj
475
- n_i: int = _ParseInt(n, min_value=1)
476
- config.console.print(str(modmath.IsPrime(n_i)))
477
-
478
-
479
- @app.command(
480
- 'primegen',
481
- help='Generate (stream) primes ≥ `start` (prints a limited `count` by default).',
482
- epilog=('Example:\n\n\n\n$ poetry run transcrypto primegen 100 -c 3\n\n101\n\n103\n\n107'),
483
- )
484
- @base.CLIErrorGuard
485
- def PrimeGenCLI( # documentation is help/epilog/args # noqa: D103
486
- *,
487
- ctx: typer.Context,
488
- start: str = typer.Argument(..., help='Starting integer (inclusive), ≥ 0'),
489
- count: int = typer.Option(1, '-c', '--count', min=1, help='How many to print, ≥ 1'),
490
- ) -> None:
491
- config: TransConfig = ctx.obj
492
- start_i: int = _ParseInt(start, min_value=0)
493
- for i, pr in enumerate(modmath.PrimeGenerator(start_i)):
494
- if i >= count:
495
- return
496
- config.console.print(pr)
497
-
498
-
499
- @app.command(
500
- 'mersenne',
501
- help=(
502
- 'Generate (stream) Mersenne prime exponents `k`, also outputting `2^k-1` '
503
- '(the Mersenne prime, `M`) and `M×2^(k-1)` (the associated perfect number), ' # noqa: RUF001
504
- 'starting at `min-k` and stopping once `k` > `max-k`.'
505
- ),
506
- epilog=(
507
- 'Example:\n\n\n\n'
508
- '$ poetry run transcrypto mersenne -k 0 -m 15\n\n'
509
- 'k=2 M=3 perfect=6\n\n'
510
- 'k=3 M=7 perfect=28\n\n'
511
- 'k=5 M=31 perfect=496\n\n'
512
- 'k=7 M=127 perfect=8128\n\n'
513
- 'k=13 M=8191 perfect=33550336\n\n'
514
- 'k=17 M=131071 perfect=8589869056'
515
- ),
516
- )
517
- @base.CLIErrorGuard
518
- def MersenneCLI( # documentation is help/epilog/args # noqa: D103
519
- *,
520
- ctx: typer.Context,
521
- min_k: int = typer.Option(2, '-k', '--min-k', min=1, help='Starting exponent `k`, ≥ 2'),
522
- max_k: int = typer.Option(10000, '-m', '--max-k', min=1, help='Stop once `k` > `max-k`, ≥ 2'),
523
- ) -> None:
524
- config: TransConfig = ctx.obj
525
- if max_k < min_k:
526
- raise base.InputError(f'max-k ({max_k}) must be >= min-k ({min_k})')
527
- for k, m, perfect in modmath.MersennePrimesGenerator(min_k):
528
- if k > max_k:
529
- return
530
- config.console.print(f'k={k} M={m} perfect={perfect}')
531
-
532
-
533
- # ================================== "*GCD" COMMANDS ===============================================
534
-
535
-
536
- @app.command(
537
- 'gcd',
538
- help='Greatest Common Divisor (GCD) of integers `a` and `b`.',
539
- epilog=(
540
- 'Example:\n\n\n\n'
541
- '$ poetry run transcrypto gcd 462 1071\n\n'
542
- '21\n\n'
543
- '$ poetry run transcrypto gcd 0 5\n\n'
544
- '5\n\n'
545
- '$ poetry run transcrypto gcd 127 13\n\n'
546
- '1'
547
- ),
548
- )
549
- @base.CLIErrorGuard
550
- def GcdCLI( # documentation is help/epilog/args # noqa: D103
551
- *,
552
- ctx: typer.Context,
553
- a: str = typer.Argument(..., help='Integer, ≥ 0'),
554
- b: str = typer.Argument(..., help="Integer, ≥ 0 (can't be both zero)"),
555
- ) -> None:
556
- config: TransConfig = ctx.obj
557
- a_i: int = _ParseInt(a, min_value=0)
558
- b_i: int = _ParseInt(b, min_value=0)
559
- if a_i == 0 and b_i == 0:
560
- raise base.InputError("`a` and `b` can't both be zero")
561
- config.console.print(base.GCD(a_i, b_i))
562
-
563
-
564
447
  @app.command(
565
- 'xgcd',
566
- help=(
567
- 'Extended Greatest Common Divisor (x-GCD) of integers `a` and `b`, '
568
- 'will return `(g, x, y)` where `a×x+b×y==g`.' # noqa: RUF001
569
- ),
570
- epilog=(
571
- 'Example:\n\n\n\n'
572
- '$ poetry run transcrypto xgcd 462 1071\n\n'
573
- '(21, 7, -3)\n\n'
574
- '$ poetry run transcrypto xgcd 0 5\n\n'
575
- '(5, 0, 1)\n\n'
576
- '$ poetry run transcrypto xgcd 127 13\n\n'
577
- '(1, 4, -39)'
578
- ),
579
- )
580
- @base.CLIErrorGuard
581
- def XgcdCLI( # documentation is help/epilog/args # noqa: D103
582
- *,
583
- ctx: typer.Context,
584
- a: str = typer.Argument(..., help='Integer, ≥ 0'),
585
- b: str = typer.Argument(..., help="Integer, ≥ 0 (can't be both zero)"),
586
- ) -> None:
587
- config: TransConfig = ctx.obj
588
- a_i: int = _ParseInt(a, min_value=0)
589
- b_i: int = _ParseInt(b, min_value=0)
590
- if a_i == 0 and b_i == 0:
591
- raise base.InputError("`a` and `b` can't both be zero")
592
- config.console.print(str(base.ExtendedGCD(a_i, b_i)))
593
-
594
-
595
- # ================================= "RANDOM" COMMAND ===============================================
596
-
597
-
598
- random_app = typer.Typer(
599
- no_args_is_help=True,
600
- help='Cryptographically secure randomness, from the OS CSPRNG.',
601
- )
602
- app.add_typer(random_app, name='random')
603
-
604
-
605
- @random_app.command(
606
- 'bits',
607
- help='Random integer with exact bit length = `bits` (MSB will be 1).',
608
- epilog=('Example:\n\n\n\n$ poetry run transcrypto random bits 16\n\n36650'),
609
- )
610
- @base.CLIErrorGuard
611
- def RandomBits( # documentation is help/epilog/args # noqa: D103
612
- *,
613
- ctx: typer.Context,
614
- bits: int = typer.Argument(..., min=8, help='Number of bits, ≥ 8'),
615
- ) -> None:
616
- config: TransConfig = ctx.obj
617
- config.console.print(base.RandBits(bits))
618
-
619
-
620
- @random_app.command(
621
- 'int',
622
- help='Uniform random integer in `[min, max]` range, inclusive.',
623
- epilog=('Example:\n\n\n\n$ poetry run transcrypto random int 1000 2000\n\n1628'),
624
- )
625
- @base.CLIErrorGuard
626
- def RandomInt( # documentation is help/epilog/args # noqa: D103
627
- *,
628
- ctx: typer.Context,
629
- min_: str = typer.Argument(..., help='Minimum, ≥ 0'),
630
- max_: str = typer.Argument(..., help='Maximum, > `min`'),
631
- ) -> None:
632
- config: TransConfig = ctx.obj
633
- min_i: int = _ParseInt(min_, min_value=0)
634
- max_i: int = _ParseInt(max_, min_value=min_i + 1)
635
- config.console.print(base.RandInt(min_i, max_i))
636
-
637
-
638
- @random_app.command(
639
- 'bytes',
640
- help='Generates `n` cryptographically secure random bytes.',
641
- epilog=(
642
- 'Example:\n\n\n\n'
643
- '$ poetry run transcrypto random bytes 32\n\n'
644
- '6c6f1f88cb93c4323285a2224373d6e59c72a9c2b82e20d1c376df4ffbe9507f'
645
- ),
646
- )
647
- @base.CLIErrorGuard
648
- def RandomBytes( # documentation is help/epilog/args # noqa: D103
649
- *,
650
- ctx: typer.Context,
651
- n: int = typer.Argument(..., min=1, help='Number of bytes, ≥ 1'),
652
- ) -> None:
653
- config: TransConfig = ctx.obj
654
- config.console.print(_BytesToText(base.RandBytes(n), config.output_format))
655
-
656
-
657
- @random_app.command(
658
- 'prime',
659
- help='Generate a random prime with exact bit length = `bits` (MSB will be 1).',
660
- epilog=('Example:\n\n\n\n$ poetry run transcrypto random prime 32\n\n2365910551'),
661
- )
662
- @base.CLIErrorGuard
663
- def RandomPrime( # documentation is help/epilog/args # noqa: D103
664
- *,
665
- ctx: typer.Context,
666
- bits: int = typer.Argument(..., min=11, help='Bit length, ≥ 11'),
667
- ) -> None:
668
- config: TransConfig = ctx.obj
669
- config.console.print(modmath.NBitRandomPrimes(bits).pop())
670
-
671
-
672
- # =================================== "MOD" COMMAND ================================================
673
-
674
-
675
- mod_app = typer.Typer(
676
- no_args_is_help=True,
677
- help='Modular arithmetic helpers.',
678
- )
679
- app.add_typer(mod_app, name='mod')
680
-
681
-
682
- @mod_app.command(
683
- 'inv',
684
- help=(
685
- 'Modular inverse: find integer 0≤`i`<`m` such that `a×i ≡ 1 (mod m)`. ' # noqa: RUF001
686
- 'Will only work if `gcd(a,m)==1`, else will fail with a message.'
687
- ),
688
- epilog=(
689
- 'Example:\n\n\n\n'
690
- '$ poetry run transcrypto mod inv 127 13\n\n'
691
- '4\n\n'
692
- '$ poetry run transcrypto mod inv 17 3120\n\n'
693
- '2753\n\n'
694
- '$ poetry run transcrypto mod inv 462 1071\n\n'
695
- '<<INVALID>> no modular inverse exists (ModularDivideError)'
696
- ),
697
- )
698
- @base.CLIErrorGuard
699
- def ModInv( # documentation is help/epilog/args # noqa: D103
700
- *,
701
- ctx: typer.Context,
702
- a: str = typer.Argument(..., help='Integer to invert'),
703
- m: str = typer.Argument(..., help='Modulus `m`, ≥ 2'),
704
- ) -> None:
705
- config: TransConfig = ctx.obj
706
- a_i: int = _ParseInt(a)
707
- m_i: int = _ParseInt(m, min_value=2)
708
- try:
709
- config.console.print(modmath.ModInv(a_i, m_i))
710
- except modmath.ModularDivideError:
711
- config.console.print('<<INVALID>> no modular inverse exists (ModularDivideError)')
712
-
713
-
714
- @mod_app.command(
715
- 'div',
716
- help=(
717
- 'Modular division: find integer 0≤`z`<`m` such that `z×y ≡ x (mod m)`. ' # noqa: RUF001
718
- 'Will only work if `gcd(y,m)==1` and `y!=0`, else will fail with a message.'
719
- ),
720
- epilog=(
721
- 'Example:\n\n\n\n'
722
- '$ poetry run transcrypto mod div 6 127 13\n\n'
723
- '11\n\n'
724
- '$ poetry run transcrypto mod div 6 0 13\n\n'
725
- '<<INVALID>> divide-by-zero or not invertible (ModularDivideError)'
726
- ),
727
- )
728
- @base.CLIErrorGuard
729
- def ModDiv( # documentation is help/epilog/args # noqa: D103
730
- *,
731
- ctx: typer.Context,
732
- x: str = typer.Argument(..., help='Integer'),
733
- y: str = typer.Argument(..., help='Integer, cannot be zero'),
734
- m: str = typer.Argument(..., help='Modulus `m`, ≥ 2'),
735
- ) -> None:
736
- config: TransConfig = ctx.obj
737
- x_i: int = _ParseInt(x)
738
- y_i: int = _ParseInt(y)
739
- m_i: int = _ParseInt(m, min_value=2)
740
- try:
741
- config.console.print(modmath.ModDiv(x_i, y_i, m_i))
742
- except modmath.ModularDivideError:
743
- config.console.print('<<INVALID>> divide-by-zero or not invertible (ModularDivideError)')
744
-
745
-
746
- @mod_app.command(
747
- 'exp',
748
- help='Modular exponentiation: `a^e mod m`. Efficient, can handle huge values.',
749
- epilog=(
750
- 'Example:\n\n\n\n'
751
- '$ poetry run transcrypto mod exp 438 234 127\n\n'
752
- '32\n\n'
753
- '$ poetry run transcrypto mod exp 438 234 89854\n\n'
754
- '60622'
755
- ),
756
- )
757
- @base.CLIErrorGuard
758
- def ModExp( # documentation is help/epilog/args # noqa: D103
759
- *,
760
- ctx: typer.Context,
761
- a: str = typer.Argument(..., help='Integer value'),
762
- e: str = typer.Argument(..., help='Integer exponent, ≥ 0'),
763
- m: str = typer.Argument(..., help='Modulus `m`, ≥ 2'),
764
- ) -> None:
765
- config: TransConfig = ctx.obj
766
- a_i: int = _ParseInt(a)
767
- e_i: int = _ParseInt(e, min_value=0)
768
- m_i: int = _ParseInt(m, min_value=2)
769
- config.console.print(modmath.ModExp(a_i, e_i, m_i))
770
-
771
-
772
- @mod_app.command(
773
- 'poly',
774
- help=(
775
- 'Efficiently evaluate polynomial with `coeff` coefficients at point `x` modulo `m` '
776
- '(`c₀+c₁×x+c₂×x²+…+cₙ×x^n mod m`).' # noqa: RUF001
777
- ),
778
- epilog=(
779
- 'Example:\n\n\n\n'
780
- '$ poetry run transcrypto mod poly 12 17 10 20 30\n\n'
781
- '14 # (10+20×12+30×12² ≡ 14 (mod 17))\n\n' # noqa: RUF001
782
- '$ poetry run transcrypto mod poly 10 97 3 0 0 1 1\n\n'
783
- '42 # (3+1×10³+1×10⁴ ≡ 42 (mod 97))' # noqa: RUF001
784
- ),
785
- )
786
- @base.CLIErrorGuard
787
- def ModPoly( # documentation is help/epilog/args # noqa: D103
788
- *,
789
- ctx: typer.Context,
790
- x: str = typer.Argument(..., help='Evaluation point `x`'),
791
- m: str = typer.Argument(..., help='Modulus `m`, ≥ 2'),
792
- coeff: list[str] = typer.Argument( # noqa: B008
793
- ...,
794
- help='Coefficients (constant-term first: `c₀+c₁×x+c₂×x²+…+cₙ×x^n`)', # noqa: RUF001
795
- ),
796
- ) -> None:
797
- config: TransConfig = ctx.obj
798
- x_i: int = _ParseInt(x)
799
- m_i: int = _ParseInt(m, min_value=2)
800
- coeff_i: list[int] = [_ParseInt(z) for z in coeff]
801
- config.console.print(modmath.ModPolynomial(x_i, coeff_i, m_i))
802
-
803
-
804
- @mod_app.command(
805
- 'lagrange',
806
- help=(
807
- 'Lagrange interpolation over modulus `m`: find the `f(x)` solution for the '
808
- 'given `x` and `zₙ:f(zₙ)` points `pt`. The modulus `m` must be a prime.'
809
- ),
810
- epilog=(
811
- 'Example:\n\n\n\n'
812
- '$ poetry run transcrypto mod lagrange 5 13 2:4 6:3 7:1\n\n'
813
- '3 # passes through (2,4), (6,3), (7,1)\n\n'
814
- '$ poetry run transcrypto mod lagrange 11 97 1:1 2:4 3:9 4:16 5:25\n\n'
815
- '24 # passes through (1,1), (2,4), (3,9), (4,16), (5,25)'
816
- ),
817
- )
818
- @base.CLIErrorGuard
819
- def ModLagrange( # documentation is help/epilog/args # noqa: D103
820
- *,
821
- ctx: typer.Context,
822
- x: str = typer.Argument(..., help='Evaluation point `x`'),
823
- m: str = typer.Argument(..., help='Modulus `m`, ≥ 2'),
824
- pt: list[str] = typer.Argument( # noqa: B008
825
- ...,
826
- help='Points `zₙ:f(zₙ)` as `key:value` pairs (e.g., `2:4 5:3 7:1`)',
827
- ),
828
- ) -> None:
829
- config: TransConfig = ctx.obj
830
- x_i: int = _ParseInt(x)
831
- m_i: int = _ParseInt(m, min_value=2)
832
- pts: dict[int, int] = dict(_ParseIntPairCLI(kv) for kv in pt)
833
- config.console.print(modmath.ModLagrangeInterpolate(x_i, pts, m_i))
834
-
835
-
836
- @mod_app.command(
837
- 'crt',
838
- help=(
839
- 'Solves Chinese Remainder Theorem (CRT) Pair: finds the unique integer 0≤`x`<`(m1×m2)` ' # noqa: RUF001
840
- 'satisfying both `x ≡ a1 (mod m1)` and `x ≡ a2 (mod m2)`, if `gcd(m1,m2)==1`.'
841
- ),
842
- epilog=(
843
- 'Example:\n\n\n\n'
844
- '$ poetry run transcrypto mod crt 6 7 127 13\n\n'
845
- '62\n\n'
846
- '$ poetry run transcrypto mod crt 12 56 17 19\n\n'
847
- '796\n\n'
848
- '$ poetry run transcrypto mod crt 6 7 462 1071\n\n'
849
- '<<INVALID>> moduli m1/m2 not co-prime (ModularDivideError)'
850
- ),
851
- )
852
- @base.CLIErrorGuard
853
- def ModCRT( # documentation is help/epilog/args # noqa: D103
854
- *,
855
- ctx: typer.Context,
856
- a1: str = typer.Argument(..., help='Integer residue for first congruence'),
857
- m1: str = typer.Argument(..., help='Modulus `m1`, ≥ 2'),
858
- a2: str = typer.Argument(..., help='Integer residue for second congruence'),
859
- m2: str = typer.Argument(..., help='Modulus `m2`, ≥ 2, !=`m1`, and `gcd(m1,m2)==1`'),
860
- ) -> None:
861
- config: TransConfig = ctx.obj
862
- a1_i: int = _ParseInt(a1)
863
- m1_i: int = _ParseInt(m1, min_value=2)
864
- a2_i: int = _ParseInt(a2)
865
- m2_i: int = _ParseInt(m2, min_value=2)
866
- try:
867
- config.console.print(modmath.CRTPair(a1_i, m1_i, a2_i, m2_i))
868
- except modmath.ModularDivideError:
869
- config.console.print('<<INVALID>> moduli `m1`/`m2` not co-prime (ModularDivideError)')
870
-
871
-
872
- # =================================== "HASH" COMMAND ===============================================
873
-
874
-
875
- hash_app = typer.Typer(
876
- no_args_is_help=True,
877
- help='Cryptographic Hashing (SHA-256 / SHA-512 / file).',
878
- )
879
- app.add_typer(hash_app, name='hash')
880
-
881
-
882
- @hash_app.command(
883
- 'sha256',
884
- help='SHA-256 of input `data`.',
885
- epilog=(
886
- 'Example:\n\n\n\n'
887
- '$ poetry run transcrypto -i bin hash sha256 xyz\n\n'
888
- '3608bca1e44ea6c4d268eb6db02260269892c0b42b86bbf1e77a6fa16c3c9282\n\n'
889
- '$ poetry run transcrypto -i b64 hash sha256 -- eHl6 # "xyz" in base-64\n\n'
890
- '3608bca1e44ea6c4d268eb6db02260269892c0b42b86bbf1e77a6fa16c3c9282'
891
- ),
892
- )
893
- @base.CLIErrorGuard
894
- def Hash256( # documentation is help/epilog/args # noqa: D103
895
- *,
896
- ctx: typer.Context,
897
- data: str = typer.Argument(..., help='Input data (raw text; or `--input-format <hex|b64|bin>`)'),
898
- ) -> None:
899
- config: TransConfig = ctx.obj
900
- bt: bytes = _BytesFromText(data, config.input_format)
901
- config.console.print(_BytesToText(base.Hash256(bt), config.output_format))
902
-
903
-
904
- @hash_app.command(
905
- 'sha512',
906
- help='SHA-512 of input `data`.',
907
- epilog=(
908
- 'Example:\n\n\n\n'
909
- '$ poetry run transcrypto -i bin hash sha512 xyz\n\n'
910
- '4a3ed8147e37876adc8f76328e5abcc1b470e6acfc18efea0135f983604953a5'
911
- '8e183c1a6086e91ba3e821d926f5fdeb37761c7ca0328a963f5e92870675b728\n\n'
912
- '$ poetry run transcrypto -i b64 hash sha512 -- eHl6 # "xyz" in base-64\n\n'
913
- '4a3ed8147e37876adc8f76328e5abcc1b470e6acfc18efea0135f983604953a5'
914
- '8e183c1a6086e91ba3e821d926f5fdeb37761c7ca0328a963f5e92870675b728'
915
- ),
916
- )
917
- @base.CLIErrorGuard
918
- def Hash512( # documentation is help/epilog/args # noqa: D103
919
- *,
920
- ctx: typer.Context,
921
- data: str = typer.Argument(..., help='Input data (raw text; or `--input-format <hex|b64|bin>`)'),
922
- ) -> None:
923
- config: TransConfig = ctx.obj
924
- bt: bytes = _BytesFromText(data, config.input_format)
925
- config.console.print(_BytesToText(base.Hash512(bt), config.output_format))
926
-
927
-
928
- @hash_app.command(
929
- 'file',
930
- help='SHA-256/512 hash of file contents, defaulting to SHA-256.',
448
+ 'markdown',
449
+ help='Emit Markdown docs for the CLI (see README.md section "Creating a New Version").',
931
450
  epilog=(
932
- 'Example:\n\n\n\n'
933
- '$ poetry run transcrypto hash file /etc/passwd --digest sha512\n\n'
934
- '8966f5953e79f55dfe34d3dc5b160ac4a4a3f9cbd1c36695a54e28d77c7874df'
935
- 'f8595502f8a420608911b87d336d9e83c890f0e7ec11a76cb10b03e757f78aea'
451
+ 'Example:\n\n\n\n$ poetry run transcrypto markdown > transcrypto.md\n\n<<saves CLI doc>>'
936
452
  ),
937
453
  )
938
- @base.CLIErrorGuard
939
- def HashFile( # documentation is help/epilog/args # noqa: D103
940
- *,
941
- ctx: typer.Context,
942
- path: pathlib.Path = typer.Argument( # noqa: B008
943
- ...,
944
- exists=True,
945
- file_okay=True,
946
- dir_okay=False,
947
- readable=True,
948
- resolve_path=True,
949
- help='Path to existing file',
950
- ),
951
- digest: str = typer.Option(
952
- 'sha256',
953
- '-d',
954
- '--digest',
955
- click_type=click.Choice(['sha256', 'sha512'], case_sensitive=False),
956
- help='Digest type, SHA-256 ("sha256") or SHA-512 ("sha512")',
957
- ),
958
- ) -> None:
454
+ @clibase.CLIErrorGuard
455
+ def Markdown(*, ctx: typer.Context) -> None: # documentation is help/epilog/args # noqa: D103
959
456
  config: TransConfig = ctx.obj
960
- config.console.print(_BytesToText(base.FileHash(str(path), digest=digest), config.output_format))
961
-
457
+ config.console.print(clibase.GenerateTyperHelpMarkdown(app, prog_name='transcrypto'))
962
458
 
963
- # =================================== "AES" COMMAND ================================================
964
459
 
965
-
966
- aes_app = typer.Typer(
967
- no_args_is_help=True,
968
- help=(
969
- 'AES-256 operations (GCM/ECB) and key derivation. '
970
- 'No measures are taken here to prevent timing attacks.'
971
- ),
972
- )
973
- app.add_typer(aes_app, name='aes')
974
-
975
-
976
- @aes_app.command(
977
- 'key',
978
- help=(
979
- 'Derive key from a password (PBKDF2-HMAC-SHA256) with custom expensive '
980
- 'salt and iterations. Very good/safe for simple password-to-key but not for '
981
- 'passwords databases (because of constant salt).'
982
- ),
983
- epilog=(
984
- 'Example:\n\n\n\n'
985
- '$ poetry run transcrypto -o b64 aes key "correct horse battery staple"\n\n'
986
- 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es=\n\n' # cspell:disable-line
987
- '$ poetry run transcrypto -p keyfile.out --protect hunter aes key '
988
- '"correct horse battery staple"\n\n'
989
- "AES key saved to 'keyfile.out'"
990
- ),
991
- )
992
- @base.CLIErrorGuard
993
- def AESKeyFromPass( # documentation is help/epilog/args # noqa: D103
994
- *,
995
- ctx: typer.Context,
996
- password: str = typer.Argument(..., help='Password (leading/trailing spaces ignored)'),
997
- ) -> None:
998
- config: TransConfig = ctx.obj
999
- aes_key: aes.AESKey = aes.AESKey.FromStaticPassword(password)
1000
- if config.key_path is not None:
1001
- _SaveObj(aes_key, str(config.key_path), config.protect)
1002
- config.console.print(f'AES key saved to {str(config.key_path)!r}')
1003
- else:
1004
- config.console.print(_BytesToText(aes_key.key256, config.output_format))
1005
-
1006
-
1007
- @aes_app.command(
1008
- 'encrypt',
1009
- help=(
1010
- 'AES-256-GCM: safely encrypt `plaintext` with `-k`/`--key` or with '
1011
- '`-p`/`--key-path` keyfile. All inputs are raw, or you '
1012
- 'can use `--input-format <hex|b64|bin>`. Attention: if you provide `-a`/`--aad` '
1013
- '(associated data, AAD), you will need to provide the same AAD when decrypting '
1014
- 'and it is NOT included in the `ciphertext`/CT returned by this method!'
1015
- ),
1016
- epilog=(
1017
- 'Example:\n\n\n\n'
1018
- '$ poetry run transcrypto -i b64 -o b64 aes encrypt -k '
1019
- 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= -- AAAAAAB4eXo=\n\n' # cspell:disable-line
1020
- 'F2_ZLrUw5Y8oDnbTP5t5xCUWX8WtVILLD0teyUi_37_4KHeV-YowVA==\n\n' # cspell:disable-line
1021
- '$ poetry run transcrypto -i b64 -o b64 aes encrypt -k '
1022
- 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= -a eHl6 -- AAAAAAB4eXo=\n\n' # cspell:disable-line
1023
- 'xOlAHPUPpeyZHId-f3VQ_QKKMxjIW0_FBo9WOfIBrzjn0VkVV6xTRA==' # cspell:disable-line
1024
- ),
1025
- )
1026
- @base.CLIErrorGuard
1027
- def AESEncrypt( # documentation is help/epilog/args # noqa: D103
1028
- *,
1029
- ctx: typer.Context,
1030
- plaintext: str = typer.Argument(..., help='Input data to encrypt (PT)'),
1031
- key: str | None = typer.Option(
1032
- None, '-k', '--key', help="Key if `-p`/`--key-path` wasn't used (32 bytes)"
1033
- ),
1034
- aad: str = typer.Option(
1035
- '',
1036
- '-a',
1037
- '--aad',
1038
- help='Associated data (optional; has to be separately sent to receiver/stored)',
1039
- ),
1040
- ) -> None:
1041
- config: TransConfig = ctx.obj
1042
- aes_key: aes.AESKey
1043
- if key:
1044
- key_bytes: bytes = _BytesFromText(key, config.input_format)
1045
- if len(key_bytes) != 32: # noqa: PLR2004
1046
- raise base.InputError(f'invalid AES key size: {len(key_bytes)} bytes (expected 32)')
1047
- aes_key = aes.AESKey(key256=key_bytes)
1048
- elif config.key_path is not None:
1049
- aes_key = _LoadObj(str(config.key_path), config.protect, aes.AESKey)
1050
- else:
1051
- raise base.InputError('provide -k/--key or -p/--key-path')
1052
- aad_bytes: bytes | None = _BytesFromText(aad, config.input_format) if aad else None
1053
- pt: bytes = _BytesFromText(plaintext, config.input_format)
1054
- ct: bytes = aes_key.Encrypt(pt, associated_data=aad_bytes)
1055
- config.console.print(_BytesToText(ct, config.output_format))
1056
-
1057
-
1058
- @aes_app.command(
1059
- 'decrypt',
1060
- help=(
1061
- 'AES-256-GCM: safely decrypt `ciphertext` with `-k`/`--key` or with '
1062
- '`-p`/`--key-path` keyfile. All inputs are raw, or you '
1063
- 'can use `--input-format <hex|b64|bin>`. Attention: if you provided `-a`/`--aad` '
1064
- '(associated data, AAD) during encryption, you will need to provide the same AAD now!'
1065
- ),
1066
- epilog=(
1067
- 'Example:\n\n\n\n'
1068
- '$ poetry run transcrypto -i b64 -o b64 aes decrypt -k '
1069
- 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= -- ' # cspell:disable-line
1070
- 'F2_ZLrUw5Y8oDnbTP5t5xCUWX8WtVILLD0teyUi_37_4KHeV-YowVA==\n\n' # cspell:disable-line
1071
- 'AAAAAAB4eXo=\n\n' # cspell:disable-line
1072
- '$ poetry run transcrypto -i b64 -o b64 aes decrypt -k '
1073
- 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= -a eHl6 -- ' # cspell:disable-line
1074
- 'xOlAHPUPpeyZHId-f3VQ_QKKMxjIW0_FBo9WOfIBrzjn0VkVV6xTRA==\n\n' # cspell:disable-line
1075
- 'AAAAAAB4eXo=' # cspell:disable-line
1076
- ),
1077
- )
1078
- @base.CLIErrorGuard
1079
- def AESDecrypt( # documentation is help/epilog/args # noqa: D103
1080
- *,
1081
- ctx: typer.Context,
1082
- ciphertext: str = typer.Argument(..., help='Input data to decrypt (CT)'),
1083
- key: str | None = typer.Option(
1084
- None, '-k', '--key', help="Key if `-p`/`--key-path` wasn't used (32 bytes)"
1085
- ),
1086
- aad: str = typer.Option(
1087
- '',
1088
- '-a',
1089
- '--aad',
1090
- help='Associated data (optional; has to be exactly the same as used during encryption)',
1091
- ),
1092
- ) -> None:
1093
- config: TransConfig = ctx.obj
1094
- aes_key: aes.AESKey
1095
- if key:
1096
- key_bytes: bytes = _BytesFromText(key, config.input_format)
1097
- if len(key_bytes) != 32: # noqa: PLR2004
1098
- raise base.InputError(f'invalid AES key size: {len(key_bytes)} bytes (expected 32)')
1099
- aes_key = aes.AESKey(key256=key_bytes)
1100
- elif config.key_path is not None:
1101
- aes_key = _LoadObj(str(config.key_path), config.protect, aes.AESKey)
1102
- else:
1103
- raise base.InputError('provide -k/--key or -p/--key-path')
1104
- # associated data, if any
1105
- aad_bytes: bytes | None = _BytesFromText(aad, config.input_format) if aad else None
1106
- ct: bytes = _BytesFromText(ciphertext, config.input_format)
1107
- pt: bytes = aes_key.Decrypt(ct, associated_data=aad_bytes)
1108
- config.console.print(_BytesToText(pt, config.output_format))
1109
-
1110
-
1111
- # ================================ "AES ECB" SUB-COMMAND ===========================================
1112
-
1113
-
1114
- aes_ecb_app = typer.Typer(
1115
- no_args_is_help=True,
1116
- help=(
1117
- 'AES-256-ECB: encrypt/decrypt 128 bit (16 bytes) hexadecimal blocks. UNSAFE, except '
1118
- 'for specifically encrypting hash blocks which are very much expected to look random. '
1119
- 'ECB mode will have the same output for the same input (no IV/nonce is used).'
1120
- ),
1121
- )
1122
- aes_app.add_typer(aes_ecb_app, name='ecb')
1123
-
1124
-
1125
- @aes_ecb_app.command(
1126
- 'encrypt',
1127
- help=(
1128
- 'AES-256-ECB: encrypt 16-bytes hex `plaintext` with `-k`/`--key` or with '
1129
- '`-p`/`--key-path` keyfile. UNSAFE, except for specifically encrypting hash blocks.'
1130
- ),
1131
- epilog=(
1132
- 'Example:\n\n\n\n'
1133
- '$ poetry run transcrypto -i b64 aes ecb -k '
1134
- 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= encrypt ' # cspell:disable-line
1135
- '00112233445566778899aabbccddeeff\n\n' # cspell:disable-line
1136
- '54ec742ca3da7b752e527b74e3a798d7'
1137
- ),
1138
- )
1139
- @base.CLIErrorGuard
1140
- def AESECBEncrypt( # documentation is help/epilog/args # noqa: D103
1141
- *,
1142
- ctx: typer.Context,
1143
- plaintext: str = typer.Argument(..., help='Plaintext block as 32 hex chars (16-bytes)'),
1144
- key: str | None = typer.Option(
1145
- None,
1146
- '-k',
1147
- '--key',
1148
- help=(
1149
- "Key if `-p`/`--key-path` wasn't used (32 bytes; raw, or you "
1150
- 'can use `--input-format <hex|b64|bin>`)'
1151
- ),
1152
- ),
1153
- ) -> None:
1154
- config: TransConfig = ctx.obj
1155
- plaintext = plaintext.strip()
1156
- if len(plaintext) != 32: # noqa: PLR2004
1157
- raise base.InputError('hexadecimal string must be exactly 32 hex chars')
1158
- if not _HEX_RE.match(plaintext):
1159
- raise base.InputError(f'invalid hexadecimal string: {plaintext!r}')
1160
- aes_key: aes.AESKey
1161
- if key:
1162
- key_bytes: bytes = _BytesFromText(key, config.input_format)
1163
- if len(key_bytes) != 32: # noqa: PLR2004
1164
- raise base.InputError(f'invalid AES key size: {len(key_bytes)} bytes (expected 32)')
1165
- aes_key = aes.AESKey(key256=key_bytes)
1166
- elif config.key_path is not None:
1167
- aes_key = _LoadObj(str(config.key_path), config.protect, aes.AESKey)
1168
- else:
1169
- raise base.InputError('provide -k/--key or -p/--key-path')
1170
- ecb: aes.AESKey.ECBEncoderClass = aes_key.ECBEncoder()
1171
- config.console.print(ecb.EncryptHex(plaintext))
1172
-
1173
-
1174
- @aes_ecb_app.command(
1175
- 'decrypt',
1176
- help=(
1177
- 'AES-256-ECB: decrypt 16-bytes hex `ciphertext` with `-k`/`--key` or with '
1178
- '`-p`/`--key-path` keyfile. UNSAFE, except for specifically encrypting hash blocks.'
1179
- ),
1180
- epilog=(
1181
- 'Example:\n\n\n\n'
1182
- '$ poetry run transcrypto -i b64 aes ecb -k '
1183
- 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= decrypt ' # cspell:disable-line
1184
- '54ec742ca3da7b752e527b74e3a798d7\n\n' # cspell:disable-line
1185
- '00112233445566778899aabbccddeeff' # cspell:disable-line
1186
- ),
1187
- )
1188
- @base.CLIErrorGuard
1189
- def AESECBDecrypt( # documentation is help/epilog/args # noqa: D103
1190
- *,
1191
- ctx: typer.Context,
1192
- ciphertext: str = typer.Argument(..., help='Ciphertext block as 32 hex chars (16-bytes)'),
1193
- key: str | None = typer.Option(
1194
- None,
1195
- '-k',
1196
- '--key',
1197
- help=(
1198
- "Key if `-p`/`--key-path` wasn't used (32 bytes; raw, or you "
1199
- 'can use `--input-format <hex|b64|bin>`)'
1200
- ),
1201
- ),
1202
- ) -> None:
1203
- config: TransConfig = ctx.obj
1204
- ciphertext = ciphertext.strip()
1205
- if len(ciphertext) != 32: # noqa: PLR2004
1206
- raise base.InputError('hexadecimal string must be exactly 32 hex chars')
1207
- if not _HEX_RE.match(ciphertext):
1208
- raise base.InputError(f'invalid hexadecimal string: {ciphertext!r}')
1209
- aes_key: aes.AESKey
1210
- if key:
1211
- key_bytes: bytes = _BytesFromText(key, config.input_format)
1212
- if len(key_bytes) != 32: # noqa: PLR2004
1213
- raise base.InputError(f'invalid AES key size: {len(key_bytes)} bytes (expected 32)')
1214
- aes_key = aes.AESKey(key256=key_bytes)
1215
- elif config.key_path is not None:
1216
- aes_key = _LoadObj(str(config.key_path), config.protect, aes.AESKey)
1217
- else:
1218
- raise base.InputError('provide -k/--key or -p/--key-path')
1219
- ecb: aes.AESKey.ECBEncoderClass = aes_key.ECBEncoder()
1220
- config.console.print(ecb.DecryptHex(ciphertext))
1221
-
1222
-
1223
- # ================================== "RSA" COMMAND =================================================
1224
-
1225
-
1226
- rsa_app = typer.Typer(
1227
- no_args_is_help=True,
1228
- help=(
1229
- 'RSA (Rivest-Shamir-Adleman) asymmetric cryptography. '
1230
- 'All methods require file key(s) as `-p`/`--key-path` (see provided examples). '
1231
- 'All non-int inputs are raw, or you can use `--input-format <hex|b64|bin>`. '
1232
- 'Attention: if you provide `-a`/`--aad` (associated data, AAD), '
1233
- 'you will need to provide the same AAD when decrypting/verifying and it is NOT included '
1234
- 'in the `ciphertext`/CT or `signature` returned by these methods! '
1235
- 'No measures are taken here to prevent timing attacks.'
1236
- ),
1237
- )
1238
- app.add_typer(rsa_app, name='rsa')
1239
-
1240
-
1241
- @rsa_app.command(
1242
- 'new',
1243
- help=(
1244
- 'Generate RSA private/public key pair with `bits` modulus size (prime sizes will be `bits`/2).'
1245
- ),
1246
- epilog=(
1247
- 'Example:\n\n\n\n'
1248
- '$ poetry run transcrypto -p rsa-key rsa new --bits 64 '
1249
- '# NEVER use such a small key: example only!\n\n'
1250
- "RSA private/public keys saved to 'rsa-key.priv/.pub'"
1251
- ),
1252
- )
1253
- @base.CLIErrorGuard
1254
- def RSANew( # documentation is help/epilog/args # noqa: D103
1255
- *,
1256
- ctx: typer.Context,
1257
- bits: int = typer.Option(
1258
- 3332,
1259
- '-b',
1260
- '--bits',
1261
- min=16,
1262
- help='Modulus size in bits, ≥16; the default (3332) is a safe size',
1263
- ),
1264
- ) -> None:
1265
- config: TransConfig = ctx.obj
1266
- base_path: str = _RequireKeyPath(config, 'rsa')
1267
- rsa_priv: rsa.RSAPrivateKey = rsa.RSAPrivateKey.New(bits)
1268
- rsa_pub: rsa.RSAPublicKey = rsa.RSAPublicKey.Copy(rsa_priv)
1269
- _SaveObj(rsa_priv, base_path + '.priv', config.protect)
1270
- _SaveObj(rsa_pub, base_path + '.pub', config.protect)
1271
- config.console.print(f'RSA private/public keys saved to {base_path + ".priv/.pub"!r}')
1272
-
1273
-
1274
- @rsa_app.command(
1275
- 'rawencrypt',
1276
- help=(
1277
- 'Raw encrypt *integer* `message` with public key (BEWARE: no OAEP/PSS padding or validation).'
1278
- ),
1279
- epilog=(
1280
- 'Example:\n\n\n\n'
1281
- '$ poetry run transcrypto -p rsa-key.pub rsa rawencrypt 999\n\n'
1282
- '6354905961171348600'
1283
- ),
1284
- )
1285
- @base.CLIErrorGuard
1286
- def RSARawEncrypt( # documentation is help/epilog/args # noqa: D103
1287
- *,
1288
- ctx: typer.Context,
1289
- message: str = typer.Argument(..., help='Integer message to encrypt, 1≤`message`<*modulus*'),
1290
- ) -> None:
1291
- config: TransConfig = ctx.obj
1292
- message_i: int = _ParseInt(message, min_value=1)
1293
- key_path: str = _RequireKeyPath(config, 'rsa')
1294
- rsa_pub: rsa.RSAPublicKey = rsa.RSAPublicKey.Copy(
1295
- _LoadObj(key_path, config.protect, rsa.RSAPublicKey)
1296
- )
1297
- config.console.print(rsa_pub.RawEncrypt(message_i))
1298
-
1299
-
1300
- @rsa_app.command(
1301
- 'rawdecrypt',
1302
- help=(
1303
- 'Raw decrypt *integer* `ciphertext` with private key '
1304
- '(BEWARE: no OAEP/PSS padding or validation).'
1305
- ),
1306
- epilog=(
1307
- 'Example:\n\n\n\n'
1308
- '$ poetry run transcrypto -p rsa-key.priv rsa rawdecrypt 6354905961171348600\n\n'
1309
- '999'
1310
- ),
1311
- )
1312
- @base.CLIErrorGuard
1313
- def RSARawDecrypt( # documentation is help/epilog/args # noqa: D103
1314
- *,
1315
- ctx: typer.Context,
1316
- ciphertext: str = typer.Argument(
1317
- ..., help='Integer ciphertext to decrypt, 1≤`ciphertext`<*modulus*'
1318
- ),
1319
- ) -> None:
1320
- config: TransConfig = ctx.obj
1321
- ciphertext_i: int = _ParseInt(ciphertext, min_value=1)
1322
- key_path: str = _RequireKeyPath(config, 'rsa')
1323
- rsa_priv: rsa.RSAPrivateKey = _LoadObj(key_path, config.protect, rsa.RSAPrivateKey)
1324
- config.console.print(rsa_priv.RawDecrypt(ciphertext_i))
1325
-
1326
-
1327
- @rsa_app.command(
1328
- 'rawsign',
1329
- help='Raw sign *integer* `message` with private key (BEWARE: no OAEP/PSS padding or validation).',
1330
- epilog=(
1331
- 'Example:\n\n\n\n'
1332
- '$ poetry run transcrypto -p rsa-key.priv rsa rawsign 999\n\n'
1333
- '7632909108672871784'
1334
- ),
1335
- )
1336
- @base.CLIErrorGuard
1337
- def RSARawSign( # documentation is help/epilog/args # noqa: D103
1338
- *,
1339
- ctx: typer.Context,
1340
- message: str = typer.Argument(..., help='Integer message to sign, 1≤`message`<*modulus*'),
1341
- ) -> None:
1342
- config: TransConfig = ctx.obj
1343
- message_i: int = _ParseInt(message, min_value=1)
1344
- key_path: str = _RequireKeyPath(config, 'rsa')
1345
- rsa_priv: rsa.RSAPrivateKey = _LoadObj(key_path, config.protect, rsa.RSAPrivateKey)
1346
- config.console.print(rsa_priv.RawSign(message_i))
1347
-
1348
-
1349
- @rsa_app.command(
1350
- 'rawverify',
1351
- help=(
1352
- 'Raw verify *integer* `signature` for *integer* `message` with public key '
1353
- '(BEWARE: no OAEP/PSS padding or validation).'
1354
- ),
1355
- epilog=(
1356
- 'Example:\n\n\n\n'
1357
- '$ poetry run transcrypto -p rsa-key.pub rsa rawverify 999 7632909108672871784\n\n'
1358
- 'RSA signature: OK\n\n'
1359
- '$ poetry run transcrypto -p rsa-key.pub rsa rawverify 999 7632909108672871785\n\n'
1360
- 'RSA signature: INVALID'
1361
- ),
1362
- )
1363
- @base.CLIErrorGuard
1364
- def RSARawVerify( # documentation is help/epilog/args # noqa: D103
1365
- *,
1366
- ctx: typer.Context,
1367
- message: str = typer.Argument(
1368
- ..., help='Integer message that was signed earlier, 1≤`message`<*modulus*'
1369
- ),
1370
- signature: str = typer.Argument(
1371
- ..., help='Integer putative signature for `message`, 1≤`signature`<*modulus*'
1372
- ),
1373
- ) -> None:
1374
- config: TransConfig = ctx.obj
1375
- message_i: int = _ParseInt(message, min_value=1)
1376
- signature_i: int = _ParseInt(signature, min_value=1)
1377
- key_path: str = _RequireKeyPath(config, 'rsa')
1378
- rsa_pub: rsa.RSAPublicKey = rsa.RSAPublicKey.Copy(
1379
- _LoadObj(key_path, config.protect, rsa.RSAPublicKey)
1380
- )
1381
- config.console.print(
1382
- 'RSA signature: '
1383
- + ('[green]OK[/]' if rsa_pub.RawVerify(message_i, signature_i) else '[red]INVALID[/]')
1384
- )
1385
-
1386
-
1387
- @rsa_app.command(
1388
- 'encrypt',
1389
- help='Encrypt `message` with public key.',
1390
- epilog=(
1391
- 'Example:\n\n\n\n'
1392
- '$ poetry run transcrypto -i bin -o b64 -p rsa-key.pub rsa encrypt "abcde" -a "xyz"\n\n'
1393
- 'AO6knI6xwq6TGR…Qy22jiFhXi1eQ=='
1394
- ),
1395
- )
1396
- @base.CLIErrorGuard
1397
- def RSAEncrypt( # documentation is help/epilog/args # noqa: D103
1398
- *,
1399
- ctx: typer.Context,
1400
- plaintext: str = typer.Argument(..., help='Message to encrypt'),
1401
- aad: str = typer.Option(
1402
- '',
1403
- '-a',
1404
- '--aad',
1405
- help='Associated data (optional; has to be separately sent to receiver/stored)',
1406
- ),
1407
- ) -> None:
1408
- config: TransConfig = ctx.obj
1409
- key_path: str = _RequireKeyPath(config, 'rsa')
1410
- rsa_pub: rsa.RSAPublicKey = _LoadObj(key_path, config.protect, rsa.RSAPublicKey)
1411
- aad_bytes: bytes | None = _BytesFromText(aad, config.input_format) if aad else None
1412
- pt: bytes = _BytesFromText(plaintext, config.input_format)
1413
- ct: bytes = rsa_pub.Encrypt(pt, associated_data=aad_bytes)
1414
- config.console.print(_BytesToText(ct, config.output_format))
1415
-
1416
-
1417
- @rsa_app.command(
1418
- 'decrypt',
1419
- help='Decrypt `ciphertext` with private key.',
1420
- epilog=(
1421
- 'Example:\n\n\n\n'
1422
- '$ poetry run transcrypto -i b64 -o bin -p rsa-key.priv rsa decrypt -a eHl6 -- '
1423
- 'AO6knI6xwq6TGR…Qy22jiFhXi1eQ==\n\n'
1424
- 'abcde'
1425
- ),
1426
- )
1427
- @base.CLIErrorGuard
1428
- def RSADecrypt( # documentation is help/epilog/args # noqa: D103
1429
- *,
1430
- ctx: typer.Context,
1431
- ciphertext: str = typer.Argument(..., help='Ciphertext to decrypt'),
1432
- aad: str = typer.Option(
1433
- '',
1434
- '-a',
1435
- '--aad',
1436
- help='Associated data (optional; has to be exactly the same as used during encryption)',
1437
- ),
1438
- ) -> None:
1439
- config: TransConfig = ctx.obj
1440
- key_path: str = _RequireKeyPath(config, 'rsa')
1441
- rsa_priv: rsa.RSAPrivateKey = _LoadObj(key_path, config.protect, rsa.RSAPrivateKey)
1442
- aad_bytes: bytes | None = _BytesFromText(aad, config.input_format) if aad else None
1443
- ct: bytes = _BytesFromText(ciphertext, config.input_format)
1444
- pt: bytes = rsa_priv.Decrypt(ct, associated_data=aad_bytes)
1445
- config.console.print(_BytesToText(pt, config.output_format))
1446
-
1447
-
1448
- @rsa_app.command(
1449
- 'sign',
1450
- help='Sign `message` with private key.',
1451
- epilog=(
1452
- 'Example:\n\n\n\n'
1453
- '$ poetry run transcrypto -i bin -o b64 -p rsa-key.priv rsa sign "xyz"\n\n'
1454
- '91TS7gC6LORiL…6RD23Aejsfxlw==' # cspell:disable-line
1455
- ),
1456
- )
1457
- @base.CLIErrorGuard
1458
- def RSASign( # documentation is help/epilog/args # noqa: D103
1459
- *,
1460
- ctx: typer.Context,
1461
- message: str = typer.Argument(..., help='Message to sign'),
1462
- aad: str = typer.Option(
1463
- '',
1464
- '-a',
1465
- '--aad',
1466
- help='Associated data (optional; has to be separately sent to receiver/stored)',
1467
- ),
1468
- ) -> None:
1469
- config: TransConfig = ctx.obj
1470
- key_path: str = _RequireKeyPath(config, 'rsa')
1471
- rsa_priv: rsa.RSAPrivateKey = _LoadObj(key_path, config.protect, rsa.RSAPrivateKey)
1472
- aad_bytes: bytes | None = _BytesFromText(aad, config.input_format) if aad else None
1473
- pt: bytes = _BytesFromText(message, config.input_format)
1474
- sig: bytes = rsa_priv.Sign(pt, associated_data=aad_bytes)
1475
- config.console.print(_BytesToText(sig, config.output_format))
1476
-
1477
-
1478
- @rsa_app.command(
1479
- 'verify',
1480
- help='Verify `signature` for `message` with public key.',
1481
- epilog=(
1482
- 'Example:\n\n\n\n'
1483
- '$ poetry run transcrypto -i b64 -p rsa-key.pub rsa verify -- eHl6 '
1484
- '91TS7gC6LORiL…6RD23Aejsfxlw==\n\n' # cspell:disable-line
1485
- 'RSA signature: OK\n\n'
1486
- '$ poetry run transcrypto -i b64 -p rsa-key.pub rsa verify -- eLl6 '
1487
- '91TS7gC6LORiL…6RD23Aejsfxlw==\n\n' # cspell:disable-line
1488
- 'RSA signature: INVALID'
1489
- ),
1490
- )
1491
- @base.CLIErrorGuard
1492
- def RSAVerify( # documentation is help/epilog/args # noqa: D103
1493
- *,
1494
- ctx: typer.Context,
1495
- message: str = typer.Argument(..., help='Message that was signed earlier'),
1496
- signature: str = typer.Argument(..., help='Putative signature for `message`'),
1497
- aad: str = typer.Option(
1498
- '',
1499
- '-a',
1500
- '--aad',
1501
- help='Associated data (optional; has to be exactly the same as used during signing)',
1502
- ),
1503
- ) -> None:
1504
- config: TransConfig = ctx.obj
1505
- key_path: str = _RequireKeyPath(config, 'rsa')
1506
- rsa_pub: rsa.RSAPublicKey = _LoadObj(key_path, config.protect, rsa.RSAPublicKey)
1507
- aad_bytes: bytes | None = _BytesFromText(aad, config.input_format) if aad else None
1508
- pt: bytes = _BytesFromText(message, config.input_format)
1509
- sig: bytes = _BytesFromText(signature, config.input_format)
1510
- config.console.print(
1511
- 'RSA signature: '
1512
- + ('[green]OK[/]' if rsa_pub.Verify(pt, sig, associated_data=aad_bytes) else '[red]INVALID[/]')
1513
- )
1514
-
1515
-
1516
- # ================================= "ELGAMAL" COMMAND ==============================================
1517
-
1518
-
1519
- eg_app = typer.Typer(
1520
- no_args_is_help=True,
1521
- help=(
1522
- 'El-Gamal asymmetric cryptography. '
1523
- 'All methods require file key(s) as `-p`/`--key-path` (see provided examples). '
1524
- 'All non-int inputs are raw, or you can use `--input-format <hex|b64|bin>`. '
1525
- 'Attention: if you provide `-a`/`--aad` (associated data, AAD), '
1526
- 'you will need to provide the same AAD when decrypting/verifying and it is NOT included '
1527
- 'in the `ciphertext`/CT or `signature` returned by these methods! '
1528
- 'No measures are taken here to prevent timing attacks.'
1529
- ),
1530
- )
1531
- app.add_typer(eg_app, name='elgamal')
1532
-
1533
-
1534
- @eg_app.command(
1535
- 'shared',
1536
- help=(
1537
- 'Generate a shared El-Gamal key with `bits` prime modulus size, which is the '
1538
- 'first step in key generation. '
1539
- 'The shared key can safely be used by any number of users to generate their '
1540
- 'private/public key pairs (with the `new` command). The shared keys are "public".'
1541
- ),
1542
- epilog=(
1543
- 'Example:\n\n\n\n'
1544
- '$ poetry run transcrypto -p eg-key elgamal shared --bits 64 '
1545
- '# NEVER use such a small key: example only!\n\n'
1546
- "El-Gamal shared key saved to 'eg-key.shared'"
1547
- ),
1548
- )
1549
- @base.CLIErrorGuard
1550
- def ElGamalShared( # documentation is help/epilog/args # noqa: D103
1551
- *,
1552
- ctx: typer.Context,
1553
- bits: int = typer.Option(
1554
- 3332,
1555
- '-b',
1556
- '--bits',
1557
- min=16,
1558
- help='Prime modulus (`p`) size in bits, ≥16; the default (3332) is a safe size',
1559
- ),
1560
- ) -> None:
1561
- config: TransConfig = ctx.obj
1562
- base_path: str = _RequireKeyPath(config, 'elgamal')
1563
- shared_eg: elgamal.ElGamalSharedPublicKey = elgamal.ElGamalSharedPublicKey.NewShared(bits)
1564
- _SaveObj(shared_eg, base_path + '.shared', config.protect)
1565
- config.console.print(f'El-Gamal shared key saved to {base_path + ".shared"!r}')
1566
-
1567
-
1568
- @eg_app.command(
1569
- 'new',
1570
- help='Generate an individual El-Gamal private/public key pair from a shared key.',
1571
- epilog=(
1572
- 'Example:\n\n\n\n'
1573
- '$ poetry run transcrypto -p eg-key elgamal new\n\n'
1574
- "El-Gamal private/public keys saved to 'eg-key.priv/.pub'"
1575
- ),
1576
- )
1577
- @base.CLIErrorGuard
1578
- def ElGamalNew(*, ctx: typer.Context) -> None: # documentation is help/epilog/args # noqa: D103
1579
- config: TransConfig = ctx.obj
1580
- base_path: str = _RequireKeyPath(config, 'elgamal')
1581
- shared_eg: elgamal.ElGamalSharedPublicKey = _LoadObj(
1582
- base_path + '.shared', config.protect, elgamal.ElGamalSharedPublicKey
1583
- )
1584
- eg_priv: elgamal.ElGamalPrivateKey = elgamal.ElGamalPrivateKey.New(shared_eg)
1585
- eg_pub: elgamal.ElGamalPublicKey = elgamal.ElGamalPublicKey.Copy(eg_priv)
1586
- _SaveObj(eg_priv, base_path + '.priv', config.protect)
1587
- _SaveObj(eg_pub, base_path + '.pub', config.protect)
1588
- config.console.print(f'El-Gamal private/public keys saved to {base_path + ".priv/.pub"!r}')
1589
-
1590
-
1591
- @eg_app.command(
1592
- 'rawencrypt',
1593
- help=(
1594
- 'Raw encrypt *integer* `message` with public key '
1595
- '(BEWARE: no ECIES-style KEM/DEM padding or validation).'
1596
- ),
1597
- epilog=(
1598
- 'Example:\n\n\n\n'
1599
- '$ poetry run transcrypto -p eg-key.pub elgamal rawencrypt 999\n\n'
1600
- '2948854810728206041:15945988196340032688'
1601
- ),
1602
- )
1603
- @base.CLIErrorGuard
1604
- def ElGamalRawEncrypt( # documentation is help/epilog/args # noqa: D103
1605
- *,
1606
- ctx: typer.Context,
1607
- message: str = typer.Argument(..., help='Integer message to encrypt, 1≤`message`<*modulus*'),
1608
- ) -> None:
1609
- config: TransConfig = ctx.obj
1610
- message_i: int = _ParseInt(message, min_value=1)
1611
- key_path: str = _RequireKeyPath(config, 'elgamal')
1612
- eg_pub: elgamal.ElGamalPublicKey = elgamal.ElGamalPublicKey.Copy(
1613
- _LoadObj(key_path, config.protect, elgamal.ElGamalPublicKey)
1614
- )
1615
- c1: int
1616
- c2: int
1617
- c1, c2 = eg_pub.RawEncrypt(message_i)
1618
- config.console.print(f'{c1}:{c2}')
1619
-
1620
-
1621
- @eg_app.command(
1622
- 'rawdecrypt',
1623
- help=(
1624
- 'Raw decrypt *integer* `ciphertext` with private key '
1625
- '(BEWARE: no ECIES-style KEM/DEM padding or validation).'
1626
- ),
1627
- epilog=(
1628
- 'Example:\n\n\n\n'
1629
- '$ poetry run transcrypto -p eg-key.priv elgamal rawdecrypt '
1630
- '2948854810728206041:15945988196340032688\n\n'
1631
- '999'
1632
- ),
1633
- )
1634
- @base.CLIErrorGuard
1635
- def ElGamalRawDecrypt( # documentation is help/epilog/args # noqa: D103
1636
- *,
1637
- ctx: typer.Context,
1638
- ciphertext: str = typer.Argument(
1639
- ...,
1640
- help=(
1641
- 'Integer ciphertext to decrypt; expects `c1:c2` format with 2 integers, `c1`,`c2`<*modulus*'
1642
- ),
1643
- ),
1644
- ) -> None:
1645
- config: TransConfig = ctx.obj
1646
- ciphertext_i: tuple[int, int] = _ParseIntPairCLI(ciphertext)
1647
- key_path: str = _RequireKeyPath(config, 'elgamal')
1648
- eg_priv: elgamal.ElGamalPrivateKey = _LoadObj(key_path, config.protect, elgamal.ElGamalPrivateKey)
1649
- config.console.print(eg_priv.RawDecrypt(ciphertext_i))
1650
-
1651
-
1652
- @eg_app.command(
1653
- 'rawsign',
1654
- help=(
1655
- 'Raw sign *integer* message with private key '
1656
- '(BEWARE: no ECIES-style KEM/DEM padding or validation). '
1657
- 'Output will 2 *integers* in a `s1:s2` format.'
1658
- ),
1659
- epilog=(
1660
- 'Example:\n\n\n\n'
1661
- '$ poetry run transcrypto -p eg-key.priv elgamal rawsign 999\n\n'
1662
- '4674885853217269088:14532144906178302633'
1663
- ),
1664
- )
1665
- @base.CLIErrorGuard
1666
- def ElGamalRawSign( # documentation is help/epilog/args # noqa: D103
1667
- *,
1668
- ctx: typer.Context,
1669
- message: str = typer.Argument(..., help='Integer message to sign, 1≤`message`<*modulus*'),
1670
- ) -> None:
1671
- config: TransConfig = ctx.obj
1672
- message_i: int = _ParseInt(message, min_value=1)
1673
- key_path: str = _RequireKeyPath(config, 'elgamal')
1674
- eg_priv: elgamal.ElGamalPrivateKey = _LoadObj(key_path, config.protect, elgamal.ElGamalPrivateKey)
1675
- s1: int
1676
- s2: int
1677
- s1, s2 = eg_priv.RawSign(message_i)
1678
- config.console.print(f'{s1}:{s2}')
1679
-
1680
-
1681
- @eg_app.command(
1682
- 'rawverify',
1683
- help=(
1684
- 'Raw verify *integer* `signature` for *integer* `message` with public key '
1685
- '(BEWARE: no ECIES-style KEM/DEM padding or validation).'
1686
- ),
1687
- epilog=(
1688
- 'Example:\n\n\n\n'
1689
- '$ poetry run transcrypto -p eg-key.pub elgamal rawverify 999 '
1690
- '4674885853217269088:14532144906178302633\n\n'
1691
- 'El-Gamal signature: OK\n\n'
1692
- '$ poetry run transcrypto -p eg-key.pub elgamal rawverify 999 '
1693
- '4674885853217269088:14532144906178302632\n\n'
1694
- 'El-Gamal signature: INVALID'
1695
- ),
1696
- )
1697
- @base.CLIErrorGuard
1698
- def ElGamalRawVerify( # documentation is help/epilog/args # noqa: D103
1699
- *,
1700
- ctx: typer.Context,
1701
- message: str = typer.Argument(
1702
- ..., help='Integer message that was signed earlier, 1≤`message`<*modulus*'
1703
- ),
1704
- signature: str = typer.Argument(
1705
- ...,
1706
- help=(
1707
- 'Integer putative signature for `message`; expects `s1:s2` format with 2 integers, '
1708
- '`s1`,`s2`<*modulus*'
1709
- ),
1710
- ),
1711
- ) -> None:
1712
- config: TransConfig = ctx.obj
1713
- message_i: int = _ParseInt(message, min_value=1)
1714
- signature_i: tuple[int, int] = _ParseIntPairCLI(signature)
1715
- key_path: str = _RequireKeyPath(config, 'elgamal')
1716
- eg_pub: elgamal.ElGamalPublicKey = elgamal.ElGamalPublicKey.Copy(
1717
- _LoadObj(key_path, config.protect, elgamal.ElGamalPublicKey)
1718
- )
1719
- config.console.print(
1720
- 'El-Gamal signature: '
1721
- + ('[green]OK[/]' if eg_pub.RawVerify(message_i, signature_i) else '[red]INVALID[/]')
1722
- )
1723
-
1724
-
1725
- @eg_app.command(
1726
- 'encrypt',
1727
- help='Encrypt `message` with public key.',
1728
- epilog=(
1729
- 'Example:\n\n\n\n'
1730
- '$ poetry run transcrypto -i bin -o b64 -p eg-key.pub elgamal encrypt "abcde" -a "xyz"\n\n'
1731
- 'CdFvoQ_IIPFPZLua…kqjhcUTspISxURg==' # cspell:disable-line
1732
- ),
1733
- )
1734
- @base.CLIErrorGuard
1735
- def ElGamalEncrypt( # documentation is help/epilog/args # noqa: D103
1736
- *,
1737
- ctx: typer.Context,
1738
- plaintext: str = typer.Argument(..., help='Message to encrypt'),
1739
- aad: str = typer.Option(
1740
- '',
1741
- '-a',
1742
- '--aad',
1743
- help='Associated data (optional; has to be separately sent to receiver/stored)',
1744
- ),
1745
- ) -> None:
1746
- config: TransConfig = ctx.obj
1747
- key_path: str = _RequireKeyPath(config, 'elgamal')
1748
- eg_pub: elgamal.ElGamalPublicKey = _LoadObj(key_path, config.protect, elgamal.ElGamalPublicKey)
1749
- aad_bytes: bytes | None = _BytesFromText(aad, config.input_format) if aad else None
1750
- pt: bytes = _BytesFromText(plaintext, config.input_format)
1751
- ct: bytes = eg_pub.Encrypt(pt, associated_data=aad_bytes)
1752
- config.console.print(_BytesToText(ct, config.output_format))
1753
-
1754
-
1755
- @eg_app.command(
1756
- 'decrypt',
1757
- help='Decrypt `ciphertext` with private key.',
1758
- epilog=(
1759
- 'Example:\n\n\n\n'
1760
- '$ poetry run transcrypto -i b64 -o bin -p eg-key.priv elgamal decrypt -a eHl6 -- '
1761
- 'CdFvoQ_IIPFPZLua…kqjhcUTspISxURg==\n\n' # cspell:disable-line
1762
- 'abcde'
1763
- ),
1764
- )
1765
- @base.CLIErrorGuard
1766
- def ElGamalDecrypt( # documentation is help/epilog/args # noqa: D103
1767
- *,
1768
- ctx: typer.Context,
1769
- ciphertext: str = typer.Argument(..., help='Ciphertext to decrypt'),
1770
- aad: str = typer.Option(
1771
- '',
1772
- '-a',
1773
- '--aad',
1774
- help='Associated data (optional; has to be exactly the same as used during encryption)',
1775
- ),
1776
- ) -> None:
1777
- config: TransConfig = ctx.obj
1778
- key_path: str = _RequireKeyPath(config, 'elgamal')
1779
- eg_priv: elgamal.ElGamalPrivateKey = _LoadObj(key_path, config.protect, elgamal.ElGamalPrivateKey)
1780
- aad_bytes: bytes | None = _BytesFromText(aad, config.input_format) if aad else None
1781
- ct: bytes = _BytesFromText(ciphertext, config.input_format)
1782
- pt: bytes = eg_priv.Decrypt(ct, associated_data=aad_bytes)
1783
- config.console.print(_BytesToText(pt, config.output_format))
1784
-
1785
-
1786
- @eg_app.command(
1787
- 'sign',
1788
- help='Sign message with private key.',
1789
- epilog=(
1790
- 'Example:\n\n\n\n'
1791
- '$ poetry run transcrypto -i bin -o b64 -p eg-key.priv elgamal sign "xyz"\n\n'
1792
- 'Xl4hlYK8SHVGw…0fCKJE1XVzA==' # cspell:disable-line
1793
- ),
1794
- )
1795
- @base.CLIErrorGuard
1796
- def ElGamalSign( # documentation is help/epilog/args # noqa: D103
1797
- *,
1798
- ctx: typer.Context,
1799
- message: str = typer.Argument(..., help='Message to sign'),
1800
- aad: str = typer.Option(
1801
- '',
1802
- '-a',
1803
- '--aad',
1804
- help='Associated data (optional; has to be separately sent to receiver/stored)',
1805
- ),
1806
- ) -> None:
1807
- config: TransConfig = ctx.obj
1808
- key_path: str = _RequireKeyPath(config, 'elgamal')
1809
- eg_priv: elgamal.ElGamalPrivateKey = _LoadObj(key_path, config.protect, elgamal.ElGamalPrivateKey)
1810
- aad_bytes: bytes | None = _BytesFromText(aad, config.input_format) if aad else None
1811
- pt: bytes = _BytesFromText(message, config.input_format)
1812
- sig: bytes = eg_priv.Sign(pt, associated_data=aad_bytes)
1813
- config.console.print(_BytesToText(sig, config.output_format))
1814
-
1815
-
1816
- @eg_app.command(
1817
- 'verify',
1818
- help='Verify `signature` for `message` with public key.',
1819
- epilog=(
1820
- 'Example:\n\n\n\n'
1821
- '$ poetry run transcrypto -i b64 -p eg-key.pub elgamal verify -- eHl6 '
1822
- 'Xl4hlYK8SHVGw…0fCKJE1XVzA==\n\n' # cspell:disable-line
1823
- 'El-Gamal signature: OK\n\n'
1824
- '$ poetry run transcrypto -i b64 -p eg-key.pub elgamal verify -- eLl6 '
1825
- 'Xl4hlYK8SHVGw…0fCKJE1XVzA==\n\n' # cspell:disable-line
1826
- 'El-Gamal signature: INVALID'
1827
- ),
1828
- )
1829
- @base.CLIErrorGuard
1830
- def ElGamalVerify( # documentation is help/epilog/args # noqa: D103
1831
- *,
1832
- ctx: typer.Context,
1833
- message: str = typer.Argument(..., help='Message that was signed earlier'),
1834
- signature: str = typer.Argument(..., help='Putative signature for `message`'),
1835
- aad: str = typer.Option(
1836
- '',
1837
- '-a',
1838
- '--aad',
1839
- help='Associated data (optional; has to be exactly the same as used during signing)',
1840
- ),
1841
- ) -> None:
1842
- config: TransConfig = ctx.obj
1843
- key_path: str = _RequireKeyPath(config, 'elgamal')
1844
- eg_pub: elgamal.ElGamalPublicKey = _LoadObj(key_path, config.protect, elgamal.ElGamalPublicKey)
1845
- aad_bytes: bytes | None = _BytesFromText(aad, config.input_format) if aad else None
1846
- pt: bytes = _BytesFromText(message, config.input_format)
1847
- sig: bytes = _BytesFromText(signature, config.input_format)
1848
- config.console.print(
1849
- 'El-Gamal signature: '
1850
- + ('[green]OK[/]' if eg_pub.Verify(pt, sig, associated_data=aad_bytes) else '[red]INVALID[/]')
1851
- )
1852
-
1853
-
1854
- # ================================== "DSA" COMMAND =================================================
1855
-
1856
-
1857
- dsa_app = typer.Typer(
1858
- no_args_is_help=True,
1859
- help=(
1860
- 'DSA (Digital Signature Algorithm) asymmetric signing/verifying. '
1861
- 'All methods require file key(s) as `-p`/`--key-path` (see provided examples). '
1862
- 'All non-int inputs are raw, or you can use `--input-format <hex|b64|bin>`. '
1863
- 'Attention: if you provide `-a`/`--aad` (associated data, AAD), '
1864
- 'you will need to provide the same AAD when decrypting/verifying and it is NOT included '
1865
- 'in the `signature` returned by these methods! '
1866
- 'No measures are taken here to prevent timing attacks.'
1867
- ),
1868
- )
1869
- app.add_typer(dsa_app, name='dsa')
1870
-
1871
-
1872
- @dsa_app.command(
1873
- 'shared',
1874
- help=(
1875
- 'Generate a shared DSA key with `p-bits`/`q-bits` prime modulus sizes, which is '
1876
- 'the first step in key generation. `q-bits` should be larger than the secrets that '
1877
- 'will be protected and `p-bits` should be much larger than `q-bits` (e.g. 4096/544). '
1878
- 'The shared key can safely be used by any number of users to generate their '
1879
- 'private/public key pairs (with the `new` command). The shared keys are "public".'
1880
- ),
1881
- epilog=(
1882
- 'Example:\n\n\n\n'
1883
- '$ poetry run transcrypto -p dsa-key dsa shared --p-bits 128 --q-bits 32 '
1884
- '# NEVER use such a small key: example only!\n\n'
1885
- "DSA shared key saved to 'dsa-key.shared'"
1886
- ),
1887
- )
1888
- @base.CLIErrorGuard
1889
- def DSAShared( # documentation is help/epilog/args # noqa: D103
1890
- *,
1891
- ctx: typer.Context,
1892
- p_bits: int = typer.Option(
1893
- 4096,
1894
- '-b',
1895
- '--p-bits',
1896
- min=16,
1897
- help='Prime modulus (`p`) size in bits, ≥16; the default (4096) is a safe size',
1898
- ),
1899
- q_bits: int = typer.Option(
1900
- 544,
1901
- '-q',
1902
- '--q-bits',
1903
- min=8,
1904
- help=(
1905
- 'Prime modulus (`q`) size in bits, ≥8; the default (544) is a safe size ***IFF*** you '
1906
- 'are protecting symmetric keys or regular hashes'
1907
- ),
1908
- ),
1909
- ) -> None:
1910
- config: TransConfig = ctx.obj
1911
- base_path: str = _RequireKeyPath(config, 'dsa')
1912
- dsa_shared: dsa.DSASharedPublicKey = dsa.DSASharedPublicKey.NewShared(p_bits, q_bits)
1913
- _SaveObj(dsa_shared, base_path + '.shared', config.protect)
1914
- config.console.print(f'DSA shared key saved to {base_path + ".shared"!r}')
1915
-
1916
-
1917
- @dsa_app.command(
1918
- 'new',
1919
- help='Generate an individual DSA private/public key pair from a shared key.',
1920
- epilog=(
1921
- 'Example:\n\n\n\n'
1922
- '$ poetry run transcrypto -p dsa-key dsa new\n\n'
1923
- "DSA private/public keys saved to 'dsa-key.priv/.pub'"
1924
- ),
1925
- )
1926
- @base.CLIErrorGuard
1927
- def DSANew(*, ctx: typer.Context) -> None: # documentation is help/epilog/args # noqa: D103
1928
- config: TransConfig = ctx.obj
1929
- base_path: str = _RequireKeyPath(config, 'dsa')
1930
- dsa_shared: dsa.DSASharedPublicKey = _LoadObj(
1931
- base_path + '.shared', config.protect, dsa.DSASharedPublicKey
1932
- )
1933
- dsa_priv: dsa.DSAPrivateKey = dsa.DSAPrivateKey.New(dsa_shared)
1934
- dsa_pub: dsa.DSAPublicKey = dsa.DSAPublicKey.Copy(dsa_priv)
1935
- _SaveObj(dsa_priv, base_path + '.priv', config.protect)
1936
- _SaveObj(dsa_pub, base_path + '.pub', config.protect)
1937
- config.console.print(f'DSA private/public keys saved to {base_path + ".priv/.pub"!r}')
1938
-
1939
-
1940
- @dsa_app.command(
1941
- 'rawsign',
1942
- help=(
1943
- 'Raw sign *integer* message with private key (BEWARE: no ECDSA/EdDSA padding or validation). '
1944
- 'Output will 2 *integers* in a `s1:s2` format.'
1945
- ),
1946
- epilog=(
1947
- 'Example:\n\n\n\n'
1948
- '$ poetry run transcrypto -p dsa-key.priv dsa rawsign 999\n\n'
1949
- '2395961484:3435572290'
1950
- ),
1951
- )
1952
- @base.CLIErrorGuard
1953
- def DSARawSign( # documentation is help/epilog/args # noqa: D103
1954
- *,
1955
- ctx: typer.Context,
1956
- message: str = typer.Argument(..., help='Integer message to sign, 1≤`message`<`q`'),
1957
- ) -> None:
1958
- config: TransConfig = ctx.obj
1959
- key_path: str = _RequireKeyPath(config, 'dsa')
1960
- dsa_priv: dsa.DSAPrivateKey = _LoadObj(key_path, config.protect, dsa.DSAPrivateKey)
1961
- message_i: int = _ParseInt(message, min_value=1)
1962
- m: int = message_i % dsa_priv.prime_seed
1963
- s1: int
1964
- s2: int
1965
- s1, s2 = dsa_priv.RawSign(m)
1966
- config.console.print(f'{s1}:{s2}')
1967
-
1968
-
1969
- @dsa_app.command(
1970
- 'rawverify',
1971
- help=(
1972
- 'Raw verify *integer* `signature` for *integer* `message` with public key '
1973
- '(BEWARE: no ECDSA/EdDSA padding or validation).'
1974
- ),
1975
- epilog=(
1976
- 'Example:\n\n\n\n'
1977
- '$ poetry run transcrypto -p dsa-key.pub dsa rawverify 999 2395961484:3435572290\n\n'
1978
- 'DSA signature: OK\n\n'
1979
- '$ poetry run transcrypto -p dsa-key.pub dsa rawverify 999 2395961484:3435572291\n\n'
1980
- 'DSA signature: INVALID'
1981
- ),
1982
- )
1983
- @base.CLIErrorGuard
1984
- def DSARawVerify( # documentation is help/epilog/args # noqa: D103
1985
- *,
1986
- ctx: typer.Context,
1987
- message: str = typer.Argument(
1988
- ..., help='Integer message that was signed earlier, 1≤`message`<`q`'
1989
- ),
1990
- signature: str = typer.Argument(
1991
- ...,
1992
- help=(
1993
- 'Integer putative signature for `message`; expects `s1:s2` format with 2 integers, '
1994
- '`s1`,`s2`<`q`'
1995
- ),
1996
- ),
1997
- ) -> None:
1998
- config: TransConfig = ctx.obj
1999
- key_path: str = _RequireKeyPath(config, 'dsa')
2000
- dsa_pub: dsa.DSAPublicKey = dsa.DSAPublicKey.Copy(
2001
- _LoadObj(key_path, config.protect, dsa.DSAPublicKey)
2002
- )
2003
- message_i: int = _ParseInt(message, min_value=1)
2004
- signature_i: tuple[int, int] = _ParseIntPairCLI(signature)
2005
- m: int = message_i % dsa_pub.prime_seed
2006
- config.console.print(
2007
- 'DSA signature: ' + ('[green]OK[/]' if dsa_pub.RawVerify(m, signature_i) else '[red]INVALID[/]')
2008
- )
2009
-
2010
-
2011
- @dsa_app.command(
2012
- 'sign',
2013
- help='Sign message with private key.',
2014
- epilog=(
2015
- 'Example:\n\n\n\n'
2016
- '$ poetry run transcrypto -i bin -o b64 -p dsa-key.priv dsa sign "xyz"\n\n'
2017
- 'yq8InJVpViXh9…BD4par2XuA='
2018
- ),
2019
- )
2020
- @base.CLIErrorGuard
2021
- def DSASign( # documentation is help/epilog/args # noqa: D103
2022
- *,
2023
- ctx: typer.Context,
2024
- message: str = typer.Argument(..., help='Message to sign'),
2025
- aad: str = typer.Option(
2026
- '',
2027
- '-a',
2028
- '--aad',
2029
- help='Associated data (optional; has to be separately sent to receiver/stored)',
2030
- ),
2031
- ) -> None:
2032
- config: TransConfig = ctx.obj
2033
- key_path: str = _RequireKeyPath(config, 'dsa')
2034
- dsa_priv: dsa.DSAPrivateKey = _LoadObj(key_path, config.protect, dsa.DSAPrivateKey)
2035
- aad_bytes: bytes | None = _BytesFromText(aad, config.input_format) if aad else None
2036
- pt: bytes = _BytesFromText(message, config.input_format)
2037
- sig: bytes = dsa_priv.Sign(pt, associated_data=aad_bytes)
2038
- config.console.print(_BytesToText(sig, config.output_format))
2039
-
2040
-
2041
- @dsa_app.command(
2042
- 'verify',
2043
- help='Verify `signature` for `message` with public key.',
2044
- epilog=(
2045
- 'Example:\n\n\n\n'
2046
- '$ poetry run transcrypto -i b64 -p dsa-key.pub dsa verify -- '
2047
- 'eHl6 yq8InJVpViXh9…BD4par2XuA=\n\n'
2048
- 'DSA signature: OK\n\n'
2049
- '$ poetry run transcrypto -i b64 -p dsa-key.pub dsa verify -- '
2050
- 'eLl6 yq8InJVpViXh9…BD4par2XuA=\n\n'
2051
- 'DSA signature: INVALID'
2052
- ),
2053
- )
2054
- @base.CLIErrorGuard
2055
- def DSAVerify( # documentation is help/epilog/args # noqa: D103
2056
- *,
2057
- ctx: typer.Context,
2058
- message: str = typer.Argument(..., help='Message that was signed earlier'),
2059
- signature: str = typer.Argument(..., help='Putative signature for `message`'),
2060
- aad: str = typer.Option(
2061
- '',
2062
- '-a',
2063
- '--aad',
2064
- help='Associated data (optional; has to be exactly the same as used during signing)',
2065
- ),
2066
- ) -> None:
2067
- config: TransConfig = ctx.obj
2068
- key_path: str = _RequireKeyPath(config, 'dsa')
2069
- dsa_pub: dsa.DSAPublicKey = _LoadObj(key_path, config.protect, dsa.DSAPublicKey)
2070
- aad_bytes: bytes | None = _BytesFromText(aad, config.input_format) if aad else None
2071
- pt: bytes = _BytesFromText(message, config.input_format)
2072
- sig: bytes = _BytesFromText(signature, config.input_format)
2073
- config.console.print(
2074
- 'DSA signature: '
2075
- + ('[green]OK[/]' if dsa_pub.Verify(pt, sig, associated_data=aad_bytes) else '[red]INVALID[/]')
2076
- )
2077
-
2078
-
2079
- # ================================== "BID" COMMAND =================================================
2080
-
2081
-
2082
- bid_app = typer.Typer(
2083
- no_args_is_help=True,
2084
- help=(
2085
- 'Bidding on a `secret` so that you can cryptographically convince a neutral '
2086
- 'party that the `secret` that was committed to previously was not changed. '
2087
- 'All methods require file key(s) as `-p`/`--key-path` (see provided examples). '
2088
- 'All non-int inputs are raw, or you can use `--input-format <hex|b64|bin>`. '
2089
- 'No measures are taken here to prevent timing attacks.'
2090
- ),
2091
- )
2092
- app.add_typer(bid_app, name='bid')
2093
-
2094
-
2095
- @bid_app.command(
2096
- 'new',
2097
- help=('Generate the bid files for `secret`.'),
2098
- epilog=(
2099
- 'Example:\n\n\n\n'
2100
- '$ poetry run transcrypto -i bin -p my-bid bid new "tomorrow it will rain"\n\n'
2101
- "Bid private/public commitments saved to 'my-bid.priv/.pub'"
2102
- ),
2103
- )
2104
- @base.CLIErrorGuard
2105
- def BidNew( # documentation is help/epilog/args # noqa: D103
2106
- *,
2107
- ctx: typer.Context,
2108
- secret: str = typer.Argument(..., help='Input data to bid to, the protected "secret"'),
2109
- ) -> None:
2110
- config: TransConfig = ctx.obj
2111
- base_path: str = _RequireKeyPath(config, 'bid')
2112
- secret_bytes: bytes = _BytesFromText(secret, config.input_format)
2113
- bid_priv: base.PrivateBid512 = base.PrivateBid512.New(secret_bytes)
2114
- bid_pub: base.PublicBid512 = base.PublicBid512.Copy(bid_priv)
2115
- _SaveObj(bid_priv, base_path + '.priv', config.protect)
2116
- _SaveObj(bid_pub, base_path + '.pub', config.protect)
2117
- config.console.print(f'Bid private/public commitments saved to {base_path + ".priv/.pub"!r}')
2118
-
2119
-
2120
- @bid_app.command(
2121
- 'verify',
2122
- help=('Verify the bid files for correctness and reveal the `secret`.'),
2123
- epilog=(
2124
- 'Example:\n\n\n\n'
2125
- '$ poetry run transcrypto -o bin -p my-bid bid verify\n\n'
2126
- 'Bid commitment: OK\n\n'
2127
- 'Bid secret:\n\n'
2128
- 'tomorrow it will rain'
2129
- ),
2130
- )
2131
- @base.CLIErrorGuard
2132
- def BidVerify(*, ctx: typer.Context) -> None: # documentation is help/epilog/args # noqa: D103
2133
- config: TransConfig = ctx.obj
2134
- base_path: str = _RequireKeyPath(config, 'bid')
2135
- bid_priv: base.PrivateBid512 = _LoadObj(base_path + '.priv', config.protect, base.PrivateBid512)
2136
- bid_pub: base.PublicBid512 = _LoadObj(base_path + '.pub', config.protect, base.PublicBid512)
2137
- bid_pub_expect: base.PublicBid512 = base.PublicBid512.Copy(bid_priv)
2138
- config.console.print(
2139
- 'Bid commitment: '
2140
- + (
2141
- '[green]OK[/]'
2142
- if (
2143
- bid_pub.VerifyBid(bid_priv.private_key, bid_priv.secret_bid) and bid_pub == bid_pub_expect
2144
- )
2145
- else '[red]INVALID[/]'
2146
- )
2147
- )
2148
- config.console.print('Bid secret:')
2149
- config.console.print(_BytesToText(bid_priv.secret_bid, config.output_format))
2150
-
2151
-
2152
- # ================================== "SSS" COMMAND =================================================
2153
-
2154
-
2155
- sss_app = typer.Typer(
2156
- no_args_is_help=True,
2157
- help=(
2158
- 'SSS (Shamir Shared Secret) secret sharing crypto scheme. '
2159
- 'All methods require file key(s) as `-p`/`--key-path` (see provided examples). '
2160
- 'All non-int inputs are raw, or you can use `--input-format <hex|b64|bin>`. '
2161
- 'No measures are taken here to prevent timing attacks.'
2162
- ),
2163
- )
2164
- app.add_typer(sss_app, name='sss')
2165
-
2166
-
2167
- @sss_app.command(
2168
- 'new',
2169
- help=(
2170
- 'Generate the private keys with `bits` prime modulus size and so that at least a '
2171
- '`minimum` number of shares are needed to recover the secret. '
2172
- 'This key will be used to generate the shares later (with the `shares` command).'
2173
- ),
2174
- epilog=(
2175
- 'Example:\n\n\n\n'
2176
- '$ poetry run transcrypto -p sss-key sss new 3 --bits 64 '
2177
- '# NEVER use such a small key: example only!\n\n'
2178
- "SSS private/public keys saved to 'sss-key.priv/.pub'"
2179
- ),
2180
- )
2181
- @base.CLIErrorGuard
2182
- def SSSNew( # documentation is help/epilog/args # noqa: D103
2183
- *,
2184
- ctx: typer.Context,
2185
- minimum: int = typer.Argument(
2186
- ..., min=2, help='Minimum number of shares required to recover secret, ≥ 2'
2187
- ),
2188
- bits: int = typer.Option(
2189
- 1024,
2190
- '-b',
2191
- '--bits',
2192
- min=16,
2193
- help=(
2194
- 'Prime modulus (`p`) size in bits, ≥16; the default (1024) is a safe size ***IFF*** you '
2195
- 'are protecting symmetric keys; the number of bits should be comfortably larger '
2196
- 'than the size of the secret you want to protect with this scheme'
2197
- ),
2198
- ),
2199
- ) -> None:
2200
- config: TransConfig = ctx.obj
2201
- base_path: str = _RequireKeyPath(config, 'sss')
2202
- sss_priv: sss.ShamirSharedSecretPrivate = sss.ShamirSharedSecretPrivate.New(minimum, bits)
2203
- sss_pub: sss.ShamirSharedSecretPublic = sss.ShamirSharedSecretPublic.Copy(sss_priv)
2204
- _SaveObj(sss_priv, base_path + '.priv', config.protect)
2205
- _SaveObj(sss_pub, base_path + '.pub', config.protect)
2206
- config.console.print(f'SSS private/public keys saved to {base_path + ".priv/.pub"!r}')
2207
-
2208
-
2209
- @sss_app.command(
2210
- 'rawshares',
2211
- help=(
2212
- 'Raw shares: Issue `count` private shares for an *integer* `secret` '
2213
- '(BEWARE: no modern message wrapping, padding or validation).'
2214
- ),
2215
- epilog=(
2216
- 'Example:\n\n\n\n'
2217
- '$ poetry run transcrypto -p sss-key sss rawshares 999 5\n\n'
2218
- "SSS 5 individual (private) shares saved to 'sss-key.share.1…5'\n\n"
2219
- '$ rm sss-key.share.2 sss-key.share.4 # this is to simulate only having shares 1,3,5'
2220
- ),
2221
- )
2222
- @base.CLIErrorGuard
2223
- def SSSRawShares( # documentation is help/epilog/args # noqa: D103
2224
- *,
2225
- ctx: typer.Context,
2226
- secret: str = typer.Argument(..., help='Integer secret to be protected, 1≤`secret`<*modulus*'),
2227
- count: int = typer.Argument(
2228
- ...,
2229
- min=1,
2230
- help=(
2231
- 'How many shares to produce; must be ≥ `minimum` used in `new` command or else the '
2232
- '`secret` would become unrecoverable'
2233
- ),
2234
- ),
2235
- ) -> None:
2236
- config: TransConfig = ctx.obj
2237
- base_path: str = _RequireKeyPath(config, 'sss')
2238
- sss_priv: sss.ShamirSharedSecretPrivate = _LoadObj(
2239
- base_path + '.priv', config.protect, sss.ShamirSharedSecretPrivate
2240
- )
2241
- if count < sss_priv.minimum:
2242
- raise base.InputError(
2243
- f'count ({count}) must be >= minimum ({sss_priv.minimum}) to allow secret recovery'
2244
- )
2245
- secret_i: int = _ParseInt(secret, min_value=1)
2246
- for i, share in enumerate(sss_priv.RawShares(secret_i, max_shares=count)):
2247
- _SaveObj(share, f'{base_path}.share.{i + 1}', config.protect)
2248
- config.console.print(
2249
- f'SSS {count} individual (private) shares saved to {base_path + ".share.1…" + str(count)!r}'
2250
- )
2251
-
2252
-
2253
- @sss_app.command(
2254
- 'rawrecover',
2255
- help=(
2256
- 'Raw recover *integer* secret from shares; will use any available shares '
2257
- 'that were found (BEWARE: no modern message wrapping, padding or validation).'
2258
- ),
2259
- epilog=(
2260
- 'Example:\n\n\n\n'
2261
- '$ poetry run transcrypto -p sss-key sss rawrecover\n\n'
2262
- "Loaded SSS share: 'sss-key.share.3'\n\n"
2263
- "Loaded SSS share: 'sss-key.share.5'\n\n"
2264
- "Loaded SSS share: 'sss-key.share.1' # using only 3 shares: number 2/4 are missing\n\n"
2265
- 'Secret:\n\n'
2266
- '999'
2267
- ),
2268
- )
2269
- @base.CLIErrorGuard
2270
- def SSSRawRecover(*, ctx: typer.Context) -> None: # documentation is help/epilog/args # noqa: D103
2271
- config: TransConfig = ctx.obj
2272
- base_path: str = _RequireKeyPath(config, 'sss')
2273
- sss_pub: sss.ShamirSharedSecretPublic = _LoadObj(
2274
- base_path + '.pub', config.protect, sss.ShamirSharedSecretPublic
2275
- )
2276
- subset: list[sss.ShamirSharePrivate] = []
2277
- for fname in glob.glob(base_path + '.share.*'): # noqa: PTH207
2278
- subset.append(_LoadObj(fname, config.protect, sss.ShamirSharePrivate))
2279
- config.console.print(f'Loaded SSS share: {fname!r}')
2280
- config.console.print('Secret:')
2281
- config.console.print(sss_pub.RawRecoverSecret(subset))
2282
-
2283
-
2284
- @sss_app.command(
2285
- 'rawverify',
2286
- help=(
2287
- 'Raw verify shares against a secret (private params; '
2288
- 'BEWARE: no modern message wrapping, padding or validation).'
2289
- ),
2290
- epilog=(
2291
- 'Example:\n\n\n\n'
2292
- '$ poetry run transcrypto -p sss-key sss rawverify 999\n\n'
2293
- "SSS share 'sss-key.share.3' verification: OK\n\n"
2294
- "SSS share 'sss-key.share.5' verification: OK\n\n"
2295
- "SSS share 'sss-key.share.1' verification: OK\n\n"
2296
- '$ poetry run transcrypto -p sss-key sss rawverify 998\n\n'
2297
- "SSS share 'sss-key.share.3' verification: INVALID\n\n"
2298
- "SSS share 'sss-key.share.5' verification: INVALID\n\n"
2299
- "SSS share 'sss-key.share.1' verification: INVALID"
2300
- ),
2301
- )
2302
- @base.CLIErrorGuard
2303
- def SSSRawVerify( # documentation is help/epilog/args # noqa: D103
2304
- *,
2305
- ctx: typer.Context,
2306
- secret: str = typer.Argument(..., help='Integer secret used to generate the shares, ≥ 1'),
2307
- ) -> None:
2308
- config: TransConfig = ctx.obj
2309
- base_path: str = _RequireKeyPath(config, 'sss')
2310
- sss_priv: sss.ShamirSharedSecretPrivate = _LoadObj(
2311
- base_path + '.priv', config.protect, sss.ShamirSharedSecretPrivate
2312
- )
2313
- secret_i: int = _ParseInt(secret, min_value=1)
2314
- for fname in glob.glob(base_path + '.share.*'): # noqa: PTH207
2315
- share: sss.ShamirSharePrivate = _LoadObj(fname, config.protect, sss.ShamirSharePrivate)
2316
- config.console.print(
2317
- f'SSS share {fname!r} verification: '
2318
- f'{"OK" if sss_priv.RawVerifyShare(secret_i, share) else "INVALID"}'
2319
- )
2320
-
2321
-
2322
- @sss_app.command(
2323
- 'shares',
2324
- help='Shares: Issue `count` private shares for a `secret`.',
2325
- epilog=(
2326
- 'Example:\n\n\n\n'
2327
- '$ poetry run transcrypto -i bin -p sss-key sss shares "abcde" 5\n\n'
2328
- "SSS 5 individual (private) shares saved to 'sss-key.share.1…5'\n\n"
2329
- '$ rm sss-key.share.2 sss-key.share.4 # this is to simulate only having shares 1,3,5'
2330
- ),
2331
- )
2332
- @base.CLIErrorGuard
2333
- def SSSShares( # documentation is help/epilog/args # noqa: D103
2334
- *,
2335
- ctx: typer.Context,
2336
- secret: str = typer.Argument(..., help='Secret to be protected'),
2337
- count: int = typer.Argument(
2338
- ...,
2339
- help=(
2340
- 'How many shares to produce; must be ≥ `minimum` used in `new` command or else the '
2341
- '`secret` would become unrecoverable'
2342
- ),
2343
- ),
2344
- ) -> None:
2345
- config: TransConfig = ctx.obj
2346
- base_path: str = _RequireKeyPath(config, 'sss')
2347
- sss_priv: sss.ShamirSharedSecretPrivate = _LoadObj(
2348
- base_path + '.priv', config.protect, sss.ShamirSharedSecretPrivate
2349
- )
2350
- if count < sss_priv.minimum:
2351
- raise base.InputError(
2352
- f'count ({count}) must be >= minimum ({sss_priv.minimum}) to allow secret recovery'
2353
- )
2354
- pt: bytes = _BytesFromText(secret, config.input_format)
2355
- for i, data_share in enumerate(sss_priv.MakeDataShares(pt, count)):
2356
- _SaveObj(data_share, f'{base_path}.share.{i + 1}', config.protect)
2357
- config.console.print(
2358
- f'SSS {count} individual (private) shares saved to {base_path + ".share.1…" + str(count)!r}'
2359
- )
2360
-
2361
-
2362
- @sss_app.command(
2363
- 'recover',
2364
- help='Recover secret from shares; will use any available shares that were found.',
2365
- epilog=(
2366
- 'Example:\n\n\n\n'
2367
- '$ poetry run transcrypto -o bin -p sss-key sss recover\n\n'
2368
- "Loaded SSS share: 'sss-key.share.3'\n\n"
2369
- "Loaded SSS share: 'sss-key.share.5'\n\n"
2370
- "Loaded SSS share: 'sss-key.share.1' # using only 3 shares: number 2/4 are missing\n\n"
2371
- 'Secret:\n\n'
2372
- 'abcde'
2373
- ),
2374
- )
2375
- @base.CLIErrorGuard
2376
- def SSSRecover(*, ctx: typer.Context) -> None: # documentation is help/epilog/args # noqa: D103
2377
- config: TransConfig = ctx.obj
2378
- base_path: str = _RequireKeyPath(config, 'sss')
2379
- subset: list[sss.ShamirSharePrivate] = []
2380
- data_share: sss.ShamirShareData | None = None
2381
- for fname in glob.glob(base_path + '.share.*'): # noqa: PTH207
2382
- share: sss.ShamirSharePrivate = _LoadObj(fname, config.protect, sss.ShamirSharePrivate)
2383
- subset.append(share)
2384
- if isinstance(share, sss.ShamirShareData):
2385
- data_share = share
2386
- config.console.print(f'Loaded SSS share: {fname!r}')
2387
- if data_share is None:
2388
- raise base.InputError('no data share found among the available shares')
2389
- pt: bytes = data_share.RecoverData(subset)
2390
- config.console.print('Secret:')
2391
- config.console.print(_BytesToText(pt, config.output_format))
2392
-
2393
-
2394
- # ================================ "MARKDOWN" COMMAND ==============================================
2395
-
2396
-
2397
- @app.command(
2398
- 'markdown',
2399
- help='Emit Markdown docs for the CLI (see README.md section "Creating a New Version").',
2400
- epilog=(
2401
- 'Example:\n\n\n\n$ poetry run transcrypto markdown > transcrypto.md\n\n<<saves CLI doc>>'
2402
- ),
2403
- )
2404
- @base.CLIErrorGuard
2405
- def Markdown(*, ctx: typer.Context) -> None: # documentation is help/epilog/args # noqa: D103
2406
- config: TransConfig = ctx.obj
2407
- config.console.print(base.GenerateTyperHelpMarkdown(app, prog_name='transcrypto'))
460
+ # Import CLI modules to register their commands with the app
461
+ from transcrypto.cli import aeshash, bidsecret, intmath, publicalgos # pyright: ignore[reportUnusedImport] # noqa: I001, E402, F401