transcrypto 1.0.3__py3-none-any.whl → 1.1.1__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.
@@ -2,1076 +2,1384 @@
2
2
  #
3
3
  # Copyright 2025 Daniel Balparda (balparda@github.com) - Apache-2.0 license
4
4
  #
5
- """Balparda's TransCrypto."""
6
-
7
- import dataclasses
8
- import datetime
9
- import logging
10
- import math
11
- # import pdb
12
- import secrets
13
- from typing import Collection, Generator, Optional, Reversible, Self
14
-
15
- __author__ = 'balparda@github.com'
16
- __version__: tuple[int, int, int] = (1, 0, 3) # v1.0.3, 2025-07-30
17
-
18
-
19
- FIRST_60_PRIMES: set[int] = {
20
- 2, 3, 5, 7, 11, 13, 17, 19, 23, 29,
21
- 31, 37, 41, 43, 47, 53, 59, 61, 67, 71,
22
- 73, 79, 83, 89, 97, 101, 103, 107, 109, 113,
23
- 127, 131, 137, 139, 149, 151, 157, 163, 167, 173,
24
- 179, 181, 191, 193, 197, 199, 211, 223, 227, 229,
25
- 233, 239, 241, 251, 257, 263, 269, 271, 277, 281,
26
- }
27
- FIRST_60_PRIMES_SORTED: list[int] = sorted(FIRST_60_PRIMES)
28
- COMPOSITE_60: int = math.prod(FIRST_60_PRIMES_SORTED)
29
- PRIME_60: int = FIRST_60_PRIMES_SORTED[-1]
30
- assert len(FIRST_60_PRIMES) == 60 and PRIME_60 == 281, f'should never happen: {PRIME_60=}'
31
- FIRST_49_MERSENNE: set[int] = { # <https://oeis.org/A000043>
32
- 2, 3, 5, 7, 13, 17, 19, 31, 61, 89,
33
- 107, 127, 521, 607, 1279, 2203, 2281, 3217, 4253, 4423,
34
- 9689, 9941, 11213, 19937, 21701, 23209, 44497, 86243, 110503, 132049,
35
- 216091, 756839, 859433, 1257787, 1398269, 2976221, 3021377, 6972593, 13466917, 20996011,
36
- 24036583, 25964951, 30402457, 32582657, 37156667, 42643801, 43112609, 57885161, 74207281,
37
- }
38
- FIRST_49_MERSENNE_SORTED: list[int] = sorted(FIRST_49_MERSENNE)
39
- assert len(FIRST_49_MERSENNE) == 49 and FIRST_49_MERSENNE_SORTED[-1] == 74207281, f'should never happen: {FIRST_49_MERSENNE_SORTED[-1]}'
40
-
41
- _SMALL_ENCRYPTION_EXPONENT = 7
42
- _BIG_ENCRYPTION_EXPONENT = 2 ** 16 + 1 # 65537
43
-
44
- _MAX_PRIMALITY_SAFETY = 100 # this is an absurd number, just to have a max
45
- _MAX_KEY_GENERATION_FAILURES = 15
46
-
47
- MIN_TM = int( # minimum allowed timestamp
48
- datetime.datetime(2000, 1, 1, 0, 0, 0).replace(tzinfo=datetime.timezone.utc).timestamp())
49
-
50
-
51
- class Error(Exception):
52
- """TransCrypto exception."""
53
-
54
-
55
- class InputError(Error):
56
- """Input exception (TransCrypto)."""
57
-
58
-
59
- class ModularDivideError(Error):
60
- """Divide-by-zero-like exception (TransCrypto)."""
61
-
62
-
63
- class CryptoError(Error):
64
- """Cryptographic exception (TransCrypto)."""
5
+ """Balparda's TransCrypto command line interface.
65
6
 
7
+ See README.md for documentation on how to use.
66
8
 
67
- def GCD(a: int, b: int, /) -> int:
68
- """Greatest Common Divisor for `a` and `b`, positive integers. Uses the Euclid method.
9
+ Notes on the layout (quick mental model):
69
10
 
70
- Args:
71
- a (int): integer a 0
72
- b (int): integer b ≥ 0
11
+ isprime, primegen, mersenne
12
+ gcd, xgcd, and grouped mod inv|div|exp|poly|lagrange|crt
13
+ random bits|int|bytes|prime, hash sha256|sha512|file
14
+ aes key frompass, aes encrypt|decrypt (GCM), aes ecb encrypt|decrypt
15
+ rsa new|encrypt|decrypt|sign|verify (integer messages)
16
+ elgamal shared|new|encrypt|decrypt|sign|verify
17
+ dsa shared|new|sign|verify
18
+ bid new|verify
19
+ sss new|shares|recover|verify
20
+ """
73
21
 
74
- Returns:
75
- gcd(a, b)
22
+ from __future__ import annotations
76
23
 
77
- Raises:
78
- InputError: invalid inputs
79
- """
80
- # test inputs
81
- if a < 0 or b < 0:
82
- raise InputError(f'negative input: {a=} , {b=}')
83
- # algo needs to start with a >= b
84
- if a < b:
85
- a, b = b, a
86
- # euclid
87
- while b:
88
- r: int = a % b
89
- a, b = b, r
90
- return a
91
-
92
-
93
- def ExtendedGCD(a: int, b: int, /) -> tuple[int, int, int]:
94
- """Greatest Common Divisor Extended for `a` and `b`, positive integers. Uses the Euclid method.
95
-
96
- Args:
97
- a (int): integer a ≥ 0
98
- b (int): integer b ≥ 0
99
-
100
- Returns:
101
- (gcd, x, y) so that a * x + b * y = gcd
102
- x and y may be negative integers or zero but won't be both zero.
103
-
104
- Raises:
105
- InputError: invalid inputs
106
- """
107
- # test inputs
108
- if a < 0 or b < 0:
109
- raise InputError(f'negative input: {a=} , {b=}')
110
- # algo needs to start with a >= b (but we remember if we did swap)
111
- swapped = False
112
- if a < b:
113
- a, b = b, a
114
- swapped = True
115
- # trivial case
116
- if not b:
117
- return (a, 0 if swapped else 1, 1 if swapped else 0)
118
- # euclid
119
- x1, x2, y1, y2 = 0, 1, 1, 0
120
- while b:
121
- q, r = divmod(a, b)
122
- x, y = x2 - q * x1, y2 - q * y1
123
- a, b, x1, x2, y1, y2 = b, r, x, x1, y, y1
124
- return (a, y2 if swapped else x2, x2 if swapped else y2)
125
-
126
-
127
- def ModInv(x: int, m: int, /) -> int:
128
- """Modular inverse of `x` modulo `m`: a `y` such that (x * y) % m == 1 if GCD(x, m) == 1.
129
-
130
- Args:
131
- x (int): integer to invert, x ≥ 0
132
- m (int): modulo, m ≥ 1
133
-
134
- Returns:
135
- positive integer `y` such that (x * y) % m == 1
136
- this only exists if GCD(x, m) == 1, so to guarantee an inverse `m` must be prime
137
-
138
- Raises:
139
- InputError: invalid modulus or x
140
- ModularDivideError: divide-by-zero, i.e., GCD(x, m) != 1 or x == 0
141
- """
142
- # test inputs
143
- if m < 1:
144
- raise InputError(f'invalid modulus: {m=}')
145
- if not 0 <= x < m:
146
- raise InputError(f'invalid input: {x=}')
147
- # easy special cases: 0 and 1
148
- if not x: # "division by 0"
149
- gcd = m
150
- raise ModularDivideError(f'null inverse {x=} mod {m=} with {gcd=}')
151
- if x == 1: # trivial degenerate case
152
- return 1
153
- # compute actual extended GCD and see if we will have an inverse
154
- gcd, y, w = ExtendedGCD(x, m)
155
- if gcd != 1:
156
- raise ModularDivideError(f'invalid inverse {x=} mod {m=} with {gcd=}')
157
- assert y and w and y >= -m, f'should never happen: {x=} mod {m=} -> {w=} ; {y=}'
158
- return y if y >= 0 else (y + m)
159
-
160
-
161
- def ModDiv(x: int, y: int, m: int, /) -> int:
162
- """Modular division of `x`/`y` modulo `m`, if GCD(y, m) == 1.
163
-
164
- Args:
165
- x (int): integer, x ≥ 0
166
- y (int): integer, y ≥ 0
167
- m (int): modulo, m ≥ 1
168
-
169
- Returns:
170
- positive integer `z` such that (z * y) % m == x
171
- this only exists if GCD(y, m) == 1, so to guarantee an inverse `m` must be prime
172
-
173
- Raises:
174
- InputError: invalid modulus or x or y
175
- ModularDivideError: divide-by-zero, i.e., GCD(y, m) != 1 or y == 0
176
- """
177
- return ((x % m) * ModInv(y % m, m)) % m
178
-
179
-
180
- def ModExp(x: int, y: int, m: int, /) -> int:
181
- """Modular exponential: returns (x ** y) % m efficiently (can handle huge values).
182
-
183
- Args:
184
- x (int): integer, x ≥ 0
185
- y (int): integer, y ≥ 0
186
- m (int): modulo, m ≥ 1
187
-
188
- Returns:
189
- (x ** y) mod m
190
-
191
- Raises:
192
- InputError: invalid inputs
193
- """
194
- # test inputs
195
- if x < 0 or y < 0:
196
- raise InputError(f'negative input: {x=} , {y=}')
197
- if m < 1:
198
- raise InputError(f'invalid modulus: {m=}')
199
- # trivial cases
200
- if not y or x == 1:
201
- return 1 % m
202
- if not x:
203
- return 0 # 0**0==1 was already taken care of by previous condition
204
- if y == 1:
205
- return x % m
206
- # now both x > 1 and y > 1
207
- z: int = 1
208
- while y:
209
- y, odd = divmod(y, 2)
210
- if odd:
211
- z = (z * x) % m
212
- x = (x * x) % m
213
- return z
214
-
215
-
216
- def ModPolynomial(x: int, polynomial: Reversible[int], m: int, /) -> int:
217
- """Evaluates polynomial `poly` (coefficient iterable) at `x` modulus `m`.
218
-
219
- Evaluate a polynomial at `x` under a modulus `m` using Horner's rule. Horner rewrites:
220
- a_0 + a_1 x + a_2 x^2 + … + a_n x^n
221
- = (…((a_n x + a_{n-1}) x + a_{n-2}) … ) x + a_0
222
- This uses exactly n multiplies and n adds, and lets us take `% m` at each
223
- step so intermediate numbers never explode.
224
-
225
- Args:
226
- x (int) The evaluation point (x ≥ 0)
227
- polynomial (Reversible[int]): Iterable of coefficients a_0, a_1, …, a_n
228
- (constant term first); it must be reversible because Horner's rule consumes
229
- coefficients from highest degree downwards
230
- m (int): Modulus (m ≥ 1); if you expect multiplicative inverses elsewhere, should be prime
231
-
232
- Returns:
233
- f(x) mod m
234
-
235
- Raises:
236
- InputError: invalid inputs
237
- """
238
- # test inputs
239
- if x < 0 or not polynomial:
240
- raise InputError(f'negative input or no polynomial: {x=} ; {polynomial=}')
241
- if m < 1:
242
- raise InputError(f'invalid modulus: {m=}')
243
- # loop over polynomial coefficients
244
- total: int = 0
245
- x %= m
246
- for coefficient in reversed(polynomial):
247
- total = (total * x + coefficient) % m
248
- return total
249
-
250
-
251
- def ModLagrangeInterpolate(x: int, points: dict[int, int], m: int, /) -> int:
252
- """Find the f(x) solution for the given `x` and {x: y} `points` modulus prime `m`.
253
-
254
- Given `points` will define a polynomial of up to len(points) order.
255
- Evaluate (interpolate) the unique polynomial of degree ≤ (n-1) that passes
256
- through the given points (x_i, y_i), and return f(x) modulo a prime `m`.
257
-
258
- Lagrange interpolation writes the polynomial as:
259
- f(X) = Σ_{i=0}^{n-1} y_i * L_i(X)
260
- where
261
- L_i(X) = Π_{j≠i} (X - x_j) / (x_i - x_j)
262
- are the Lagrange basis polynomials. Each L_i(x_i) = 1 and L_i(x_j)=0 for j≠i,
263
- so f matches every supplied point.
264
-
265
- In modular arithmetic we replace division by multiplication with modular
266
- inverses. Because `m` is prime (or at least co-prime with every denominator),
267
- every (x_i - x_j) has an inverse `mod m`.
268
-
269
- Args:
270
- x (int): The x-value at which to evaluate the interpolated polynomial, x ≥ 0
271
- points (dict[int, int]): A mapping {x_i: y_i}, where all 0 ≤ x_i < m and 0 ≤ y_i < m, minimum of 2
272
- m (int): Prime modulus (m ≥ 2); we need modular inverses, so gcd(denominator, m) must be 1
273
-
274
- Returns:
275
- y-value solution for f(x) mod m given `points` mapping
276
-
277
- Raises:
278
- InputError: invalid inputs
279
- """
280
- # test inputs
281
- if x < 0:
282
- raise InputError(f'negative input: {x=}')
283
- if m < 2:
284
- raise InputError(f'invalid modulus: {m=}')
285
- if len(points) < 2 or any(not 0 <= k < m or not 0 <= v < m for k, v in points.items()):
286
- raise InputError(f'invalid points: {points=}')
287
- # compute everything term-by-term
288
- x %= m
289
- result: int = 0
290
- for xi, yi in points.items():
291
- # build numerator and denominator of L_i(x)
292
- num: int = 1 # Π (x - x_j)
293
- den: int = 1 # Π (xi - x_j)
294
- for xj in points:
295
- if xj == xi:
296
- continue
297
- num = (num * (x - xj)) % m
298
- den = (den * (xi - xj)) % m
299
- # add to the result: (y_i * L_i(x)) = (y_i * num / den)
300
- result = (result + ModDiv(yi * num, den, m)) % m
301
- # done
302
- return result
303
-
304
-
305
- def FermatIsPrime(
306
- n: int, /, *,
307
- safety: int = 10,
308
- witnesses: Optional[set[int]] = None) -> bool:
309
- """Primality test of `n` by Fermat's algo (n > 0). DO NOT RELY!
310
-
311
- Will execute Fermat's algo for non-trivial `n` (n > 3 and odd).
312
- <https://en.wikipedia.org/wiki/Fermat_primality_test>
313
-
314
- This is for didactical uses only, as it is reasonably easy for this algo to fail
315
- on simple cases. For example, 8911 will fail for many sets of 10 random witnesses.
316
- (See <https://en.wikipedia.org/wiki/Carmichael_number> to understand better.)
317
- Miller-Rabin below (MillerRabinIsPrime) has been tuned to be VERY reliable by default.
318
-
319
- Args:
320
- n (int): Number to test primality
321
- safety (int, optional): Maximum witnesses to use (only if witnesses is not given)
322
- witnesses (set[int], optional): If given will use exactly these witnesses, in order
323
-
324
- Returns:
325
- False if certainly not prime ; True if (probabilistically) prime
326
-
327
- Raises:
328
- InputError: invalid inputs
329
- """
330
- # test inputs and test for trivial cases: 1, 2, 3, divisible by 2
331
- if n < 1:
332
- raise InputError(f'invalid number: {n=}')
333
- if n in (2, 3):
334
- return True
335
- if n == 1 or not n % 2:
336
- return False
337
- # n is odd and >= 5 so now we generate witnesses (if needed)
338
- # degenerate case is: n==5, max_safety==2 => randint(2, 3) => {2, 3}
339
- if not witnesses:
340
- max_safety: int = min(n // 2, _MAX_PRIMALITY_SAFETY)
341
- if safety < 1:
342
- raise InputError(f'out of bounds safety: 1 <= {safety=} <= {max_safety}')
343
- safety = max_safety if safety > max_safety else safety
344
- witnesses = set()
345
- rand = secrets.SystemRandom()
346
- while len(witnesses) < safety:
347
- witnesses.add(rand.randint(2, n - 2))
348
- # we have our witnesses: do the actual Fermat algo
349
- for w in sorted(witnesses):
350
- if not 2 <= w <= (n - 2):
351
- raise InputError(f'out of bounds witness: 2 <= {w=} <= {n - 2}')
352
- if ModExp(w, n - 1, n) != 1:
353
- # number is proved to be composite
354
- return False
355
- # we declare the number PROBABLY a prime to the limits of this test
356
- return True
357
-
358
-
359
- def _MillerRabinWitnesses(n: int, /) -> set[int]: # pylint: disable=too-many-return-statements
360
- """Generates a reasonable set of Miller-Rabin witnesses for testing primality of `n`.
361
-
362
- For n < 3317044064679887385961981 it is precise. That is more than 2**81. See:
363
- <https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test#Testing_against_small_sets_of_bases>
364
-
365
- For n >= 3317044064679887385961981 it is probabilistic, but computes an number of witnesses
366
- that should make the test fail less than once in 2**80 tries (once in 10^25). For all intent and
367
- purposes it "never" fails.
368
-
369
- Args:
370
- n (int): number, n ≥ 5
371
-
372
- Returns:
373
- {witness1, witness2, ...} for either "certainty" of primality or error chance < 10**25
374
-
375
- Raises:
376
- InputError: invalid inputs
377
- """
378
- # test inputs
379
- if n < 5:
380
- raise InputError(f'invalid number: {n=}')
381
- # for some "smaller" values there is research that shows these sets are always enough
382
- if n < 2047:
383
- return {2} # "safety" 1, but 100% coverage
384
- if n < 9080191:
385
- return {31, 73} # "safety" 2, but 100% coverage
386
- if n < 4759123141:
387
- return {2, 7, 61} # "safety" 3, but 100% coverage
388
- if n < 2152302898747:
389
- return set(FIRST_60_PRIMES_SORTED[:5]) # "safety" 5, but 100% coverage
390
- if n < 341550071728321:
391
- return set(FIRST_60_PRIMES_SORTED[:7]) # "safety" 7, but 100% coverage
392
- if n < 18446744073709551616: # 2 ** 64
393
- return set(FIRST_60_PRIMES_SORTED[:12]) # "safety" 12, but 100% coverage
394
- if n < 3317044064679887385961981: # > 2 ** 81
395
- return set(FIRST_60_PRIMES_SORTED[:13]) # "safety" 13, but 100% coverage
396
- # here n should be greater than 2 ** 81, so safety should be 34 or less
397
- n_bits: int = n.bit_length()
398
- assert n_bits >= 82, f'should never happen: {n=} -> {n_bits=}'
399
- safety: int = int(math.ceil(0.375 + 1.59 / (0.000590 * n_bits))) if n_bits <= 1700 else 2
400
- assert 1 < safety <= 34, f'should never happen: {n=} -> {n_bits=} ; {safety=}'
401
- return set(FIRST_60_PRIMES_SORTED[:safety])
402
-
403
-
404
- def _MillerRabinSR(n: int, /) -> tuple[int, int]:
405
- """Generates (s, r) where (2 ** s) * r == (n - 1) hold true, for odd n > 5.
406
-
407
- It should be always true that: s >= 1 and r >= 1 and r is odd.
408
-
409
- Args:
410
- n (int): odd number, n ≥ 5
411
-
412
- Returns:
413
- (s, r) so that (2 ** s) * r == (n - 1)
414
-
415
- Raises:
416
- InputError: invalid inputs
417
- """
418
- # test inputs
419
- if n < 5 or not n % 2:
420
- raise InputError(f'invalid odd number: {n=}')
421
- # divide by 2 until we can't anymore
422
- s: int = 1
423
- r: int = (n - 1) // 2
424
- while not r % 2:
425
- s += 1
426
- r //= 2
427
- # make sure everything checks out and return
428
- assert 1 <= r <= n and r % 2, f'should never happen: {n=} -> {r=}'
429
- return (s, r)
430
-
431
-
432
- def MillerRabinIsPrime(
433
- n: int, /, *,
434
- witnesses: Optional[set[int]] = None) -> bool:
435
- """Primality test of `n` by Miller-Rabin's algo (n > 0).
436
-
437
- Will execute Miller-Rabin's algo for non-trivial `n` (n > 3 and odd).
438
- <https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test>
439
-
440
- Args:
441
- n (int): Number to test primality, n ≥ 1
442
- witnesses (set[int], optional): If given will use exactly these witnesses, in order
443
-
444
- Returns:
445
- False if certainly not prime ; True if (probabilistically) prime
446
-
447
- Raises:
448
- InputError: invalid inputs
449
- """
450
- # test inputs and test for trivial cases: 1, 2, 3, divisible by 2
451
- if n < 1:
452
- raise InputError(f'invalid number: {n=}')
453
- if n in (2, 3):
454
- return True
455
- if n == 1 or not n % 2:
456
- return False
457
- # n is odd and >= 5; find s and r so that (2 ** s) * r == (n - 1)
458
- s, r = _MillerRabinSR(n)
459
- # do the Miller-Rabin algo
460
- n_limits: tuple[int, int] = (1, n - 1)
461
- y: int
462
- for w in sorted(witnesses if witnesses else _MillerRabinWitnesses(n)):
463
- if not 2 <= w <= (n - 2):
464
- raise InputError(f'out of bounds witness: 2 <= {w=} <= {n - 2}')
465
- x: int = ModExp(w, r, n)
466
- if x not in n_limits:
467
- for _ in range(s): # s >= 1 so will execute at least once
468
- y = (x * x) % n
469
- if y == 1 and x not in n_limits:
470
- return False # number is proved to be composite
471
- x = y
472
- if x != 1:
473
- return False # number is proved to be composite
474
- # we declare the number PROBABLY a prime to the limits of this test
475
- return True
476
-
477
-
478
- def IsPrime(n: int, /) -> bool:
479
- """Primality test of `n` (n > 0).
480
-
481
- Args:
482
- n (int): Number to test primality, n ≥ 1
483
-
484
- Returns:
485
- False if certainly not prime ; True if (probabilistically) prime
486
-
487
- Raises:
488
- InputError: invalid inputs
489
- """
490
- # is number divisible by (one of the) first 60 primes? test should eliminate 80%+ of candidates
491
- if n > PRIME_60 and GCD(n, COMPOSITE_60) != 1:
492
- return False
493
- # do the (more expensive) Miller-Rabin primality test
494
- return MillerRabinIsPrime(n)
495
-
496
-
497
- def PrimeGenerator(start: int, /) -> Generator[int, None, None]:
498
- """Generates all primes from `start` until loop is broken. Tuned for huge numbers.
499
-
500
- Args:
501
- start (int): number at which to start generating primes, start ≥ 0
502
-
503
- Yields:
504
- prime numbers (int)
505
-
506
- Raises:
507
- InputError: invalid inputs
508
- """
509
- # test inputs and make sure we start at an odd number
510
- if start < 0:
511
- raise InputError(f'invalid number: {start=}')
512
- # handle start of sequence manually if needed... because we have here the only EVEN prime...
513
- if start <= 2:
514
- yield 2
515
- start = 3
516
- # we now focus on odd numbers only and loop forever
517
- n: int = (start if start % 2 else start + 1) - 2 # n >= 1 always
518
- while True:
519
- n += 2 # next odd number
520
- if IsPrime(n):
521
- yield n # found a prime
522
-
523
-
524
- def NBitRandomPrime(n_bits: int, /) -> int:
525
- """Generates a random prime with (guaranteed) `n_bits` binary representation length.
526
-
527
- Args:
528
- n_bits (int): Number of guaranteed bits in prime representation, n ≥ 4
529
-
530
- Returns:
531
- random prime with `n_bits` bits
532
-
533
- Raises:
534
- InputError: invalid inputs
535
- """
536
- # test inputs
537
- if n_bits < 4:
538
- raise InputError(f'invalid n: {n_bits=}')
539
- # get a random number with guaranteed bit size
540
- min_start: int = 2 ** (n_bits - 1)
541
- prime: int = 0
542
- while prime.bit_length() != n_bits:
543
- start_point: int = secrets.randbits(n_bits)
544
- while start_point < min_start:
545
- # i know we could just set the bit, but IMO it is better to get another entirely
546
- start_point = secrets.randbits(n_bits)
547
- prime = next(PrimeGenerator(start_point))
548
- return prime
549
-
550
-
551
- def MersennePrimesGenerator(start: int, /) -> Generator[tuple[int, int, int], None, None]:
552
- """Generates all Mersenne prime (2 ** n - 1) exponents from 2**start until loop is broken.
553
-
554
- <https://en.wikipedia.org/wiki/List_of_Mersenne_primes_and_perfect_numbers>
555
-
556
- Args:
557
- start (int): exponent at which to start generating primes, start ≥ 0
558
-
559
- Yields:
560
- (exponent, mersenne_prime, perfect_number), given some exponent `n` that will be exactly:
561
- (n, 2 ** n - 1, (2 ** (n - 1)) * (2 ** n - 1))
562
-
563
- Raises:
564
- InputError: invalid inputs
565
- """
566
- # we now loop forever over prime exponents
567
- # "The exponents p corresponding to Mersenne primes must themselves be prime."
568
- for n in PrimeGenerator(start if start >= 1 else 1):
569
- mersenne: int = 2 ** n - 1
570
- if IsPrime(mersenne):
571
- yield (n, mersenne, (2 ** (n - 1)) * mersenne) # found: also yield perfect number
572
-
573
-
574
- @dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
575
- class CryptoKey:
576
- """A cryptographic key."""
577
-
578
- def __post_init__(self) -> None:
579
- """Check data."""
580
-
581
-
582
- @dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
583
- class RSAPublicKey(CryptoKey):
584
- """RSA (Rivest-Shamir-Adleman) key, with the public part of the key.
585
-
586
- Attributes:
587
- public_modulus (int): modulus (p * q), ≥ 6
588
- encrypt_exp (int): encryption exponent, 3 ≤ e < modulus, (e * decrypt) % ((p-1) * (q-1)) == 1
589
- """
590
-
591
- public_modulus: int
592
- encrypt_exp: int
593
-
594
- def __post_init__(self) -> None:
595
- """Check data.
596
-
597
- Raises:
598
- InputError: invalid inputs
599
- """
600
- super(RSAPublicKey, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
601
- if self.public_modulus < 6 or IsPrime(self.public_modulus):
602
- # only a full factors check can prove modulus is product of only 2 primes, which is impossible
603
- # to do for large numbers here; the private key checks the relationship though
604
- raise InputError(f'invalid public_modulus: {self}')
605
- if not 2 < self.encrypt_exp < self.public_modulus or not IsPrime(self.encrypt_exp):
606
- # technically, encrypt_exp < phi, but again the private key tests for this explicitly
607
- raise InputError(f'invalid encrypt_exp: {self}')
608
-
609
- def Encrypt(self, message: int, /) -> int:
610
- """Encrypt `message` with this public key.
611
-
612
- Args:
613
- message (int): message to encrypt, 1 ≤ m < modulus
614
-
615
- Returns:
616
- encrypted message (int, 1 ≤ m < modulus) = (m ** encrypt_exp) mod modulus
617
-
618
- Raises:
619
- InputError: invalid inputs
620
- """
621
- # test inputs
622
- if not 0 < message < self.public_modulus:
623
- raise InputError(f'invalid message: {message=}')
624
- # encrypt
625
- return ModExp(message, self.encrypt_exp, self.public_modulus)
626
-
627
- def VerifySignature(self, message: int, signature: int, /) -> bool:
628
- """Verify a signature. True if OK; False if failed verification.
629
-
630
- Args:
631
- message (int): message that was signed by key owner, 1 ≤ m < modulus
632
- signature (int): signature, 1 ≤ s < modulus
633
-
634
- Returns:
635
- True if signature is valid, False otherwise;
636
- (signature ** encrypt_exp) mod modulus == message
637
-
638
- Raises:
639
- InputError: invalid inputs
640
- """
641
- return self.Encrypt(signature) == message
642
-
643
- @classmethod
644
- def Copy(cls, other: 'RSAPublicKey', /) -> Self:
645
- """Initialize a public key by taking the public parts of a public/private key."""
646
- return cls(public_modulus=other.public_modulus, encrypt_exp=other.encrypt_exp)
647
-
648
-
649
- @dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
650
- class RSAObfuscationPair(RSAPublicKey):
651
- """RSA (Rivest-Shamir-Adleman) obfuscation pair for a public key.
652
-
653
- Attributes:
654
- random_key (int): random value key, 2 ≤ k < modulus
655
- key_inverse (int): inverse for `random_key` in relation to the RSA public key, 2 ≤ i < modulus
656
- """
24
+ import argparse
25
+ import enum
26
+ import glob
27
+ import logging
28
+ # import pdb
29
+ import sys
30
+ from typing import Any, Iterable, Sequence
657
31
 
658
- random_key: int
659
- key_inverse: int
660
-
661
- def __post_init__(self) -> None:
662
- """Check data.
663
-
664
- Raises:
665
- InputError: invalid inputs
666
- CryptoError: modulus math is inconsistent with values
667
- """
668
- super(RSAObfuscationPair, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
669
- if (not 1 < self.random_key < self.public_modulus or
670
- not 1 < self.key_inverse < self.public_modulus or
671
- self.random_key in (self.key_inverse, self.encrypt_exp, self.public_modulus)):
672
- raise InputError(f'invalid keys: {self}')
673
- if (self.random_key * self.key_inverse) % self.public_modulus != 1:
674
- raise CryptoError(f'inconsistent keys: {self}')
675
-
676
- def ObfuscateMessage(self, message: int, /) -> int:
677
- """Convert message to an obfuscated message to be signed by this key's owner.
678
-
679
- Args:
680
- message (int): message to obfuscate before signature, 1 ≤ m < modulus
681
-
682
- Returns:
683
- obfuscated message (int, 1 ≤ m < modulus) = (m * (random_key ** encrypt_exp)) mod modulus
684
-
685
- Raises:
686
- InputError: invalid inputs
687
- """
688
- # test inputs
689
- if not 0 < message < self.public_modulus:
690
- raise InputError(f'invalid message: {message=}')
691
- # encrypt
692
- return (message * ModExp(
693
- self.random_key, self.encrypt_exp, self.public_modulus)) % self.public_modulus
694
-
695
- def RevealOriginalSignature(self, message: int, signature: int, /) -> int:
696
- """Recover original signature for `message` from obfuscated `signature`.
697
-
698
- Args:
699
- message (int): original message before obfuscation, 1 ≤ m < modulus
700
- signature (int): signature for obfuscated message (not `message`!), 1 ≤ s < modulus
701
-
702
- Returns:
703
- original signature (int, 1 ≤ s < modulus) to `message`;
704
- signature * key_inverse mod modulus
705
-
706
- Raises:
707
- InputError: invalid inputs
708
- CryptoError: some signatures were invalid (either plain or obfuscated)
709
- """
710
- # verify that obfuscated signature is valid
711
- obfuscated: int = self.ObfuscateMessage(message)
712
- if not self.VerifySignature(obfuscated, signature):
713
- raise CryptoError(f'obfuscated message was not signed: {message=} ; {signature=}')
714
- # compute signature for original message and check it
715
- original: int = (signature * self.key_inverse) % self.public_modulus
716
- if not self.VerifySignature(message, original):
717
- raise CryptoError(f'failed signature recovery: {message=} ; {signature=}')
718
- return original
719
-
720
- @classmethod
721
- def New(cls, key: RSAPublicKey, /) -> Self:
722
- """New obfuscation pair for this `key`, respecting the size of the public modulus.
723
-
724
- Args:
725
- key (RSAPublicKey): public RSA key to use as base for a new RSAObfuscationPair
726
-
727
- Returns:
728
- RSAObfuscationPair object ready for use
729
- """
730
- # find a suitable random key based on the bit_length
731
- random_key: int = 0
732
- key_inverse: int = 0
733
- while (not random_key or not key_inverse or
734
- random_key == key.encrypt_exp or
735
- random_key == key_inverse or
736
- key_inverse == key.encrypt_exp):
737
- random_key = secrets.randbits(key.public_modulus.bit_length() - 1)
738
- try:
739
- key_inverse = ModInv(random_key, key.public_modulus)
740
- except ModularDivideError:
741
- key_inverse = 0
742
- # build object
743
- return cls(
744
- public_modulus=key.public_modulus, encrypt_exp=key.encrypt_exp,
745
- random_key=random_key, key_inverse=key_inverse)
746
-
747
-
748
- @dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
749
- class RSAPrivateKey(RSAPublicKey):
750
- """RSA (Rivest-Shamir-Adleman) private key.
751
-
752
- Attributes:
753
- modulus_p (int): prime number p, ≥ 2
754
- modulus_q (int): prime number q, ≥ 3 and > p
755
- decrypt_exp (int): decryption exponent, 2 ≤ d < modulus, (encrypt * d) % ((p-1) * (q-1)) == 1
756
- """
32
+ from . import base, modmath, rsa, sss, elgamal, dsa, aes
757
33
 
758
- modulus_p: int
759
- modulus_q: int
760
- decrypt_exp: int
761
-
762
- def __post_init__(self) -> None:
763
- """Check data.
764
-
765
- Raises:
766
- InputError: invalid inputs
767
- CryptoError: modulus math is inconsistent with values
768
- """
769
- super(RSAPrivateKey, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
770
- phi: int = (self.modulus_p - 1) * (self.modulus_q - 1)
771
- min_prime_distance: int = 2 ** (self.public_modulus.bit_length() // 3 + 1)
772
- if (self.modulus_p < 2 or not IsPrime(self.modulus_p) or # pylint: disable=too-many-boolean-expressions
773
- self.modulus_q < 3 or not IsPrime(self.modulus_q) or
774
- self.modulus_q <= self.modulus_p or
775
- (self.modulus_q - self.modulus_p) < min_prime_distance or
776
- self.encrypt_exp in (self.modulus_p, self.modulus_q) or
777
- self.encrypt_exp >= phi or
778
- self.decrypt_exp in (self.encrypt_exp, self.modulus_p, self.modulus_q, phi)):
779
- # encrypt_exp has to be less than phi;
780
- # if p − q < 2*(n**(1/4)) then solving for p and q is trivial
781
- raise InputError(f'invalid modulus_p or modulus_q: {self}')
782
- min_decrypt_length: int = self.public_modulus.bit_length() // 2 + 1
783
- if not (2 ** min_decrypt_length) < self.decrypt_exp < self.public_modulus:
784
- # if decrypt_exp < public_modulus**(1/4)/3, then decrypt_exp can be computed efficiently
785
- # from public_modulus and encrypt_exp so we make sure it is larger than public_modulus**(1/2)
786
- raise InputError(f'invalid decrypt_exp: {self}')
787
- if self.modulus_p * self.modulus_q != self.public_modulus:
788
- raise CryptoError(f'inconsistent modulus_p * modulus_q: {self}')
789
- if (self.encrypt_exp * self.decrypt_exp) % phi != 1:
790
- raise CryptoError(f'inconsistent exponents: {self}')
791
-
792
- def Decrypt(self, message: int, /) -> int:
793
- """Decrypt `message` with this private key.
794
-
795
- Args:
796
- message (int): message to encrypt, 1 ≤ m < modulus
797
-
798
- Returns:
799
- decrypted message (int, 1 m < modulus) = (m ** decrypt_exp) mod modulus
800
-
801
- Raises:
802
- InputError: invalid inputs
803
- """
804
- # test inputs
805
- if not 0 < message < self.public_modulus:
806
- raise InputError(f'invalid message: {message=}')
807
- # decrypt
808
- return ModExp(message, self.decrypt_exp, self.public_modulus)
809
-
810
- def Sign(self, message: int, /) -> int:
811
- """Sign `message` with this private key.
812
-
813
- Args:
814
- message (int): message to sign, 1 ≤ m < modulus
815
-
816
- Returns:
817
- signed message (int, 1 ≤ m < modulus) = (m ** decrypt_exp) mod modulus;
818
- identical to Decrypt()
819
-
820
- Raises:
821
- InputError: invalid inputs
822
- """
823
- return self.Decrypt(message)
824
-
825
- @classmethod
826
- def New(cls, bit_length: int, /) -> Self:
827
- """Make a new private key of `bit_length` bits (primes p & q will be half this length).
828
-
829
- Args:
830
- bit_length (int): number of bits in the modulus, ≥ 11; primes p & q will be half this length
831
-
832
- Returns:
833
- RSAPrivateKey object ready for use
834
-
835
- Raises:
836
- InputError: invalid inputs
837
- """
838
- # test inputs
839
- if bit_length < 11:
840
- raise InputError(f'invalid bit length: {bit_length=}')
841
- # generate primes / modulus
842
- failures: int = 0
843
- while True:
844
- try:
845
- primes: list[int] = [NBitRandomPrime(bit_length // 2), NBitRandomPrime(bit_length // 2)]
846
- modulus: int = primes[0] * primes[1]
847
- while modulus.bit_length() != bit_length or primes[0] == primes[1]:
848
- primes.remove(min(primes))
849
- primes.append(NBitRandomPrime(
850
- bit_length // 2 + (bit_length % 2 if modulus.bit_length() < bit_length else 0)))
851
- modulus = primes[0] * primes[1]
852
- # build object
853
- phi: int = (primes[0] - 1) * (primes[1] - 1)
854
- prime_exp: int = (_SMALL_ENCRYPTION_EXPONENT if phi <= _BIG_ENCRYPTION_EXPONENT else
855
- _BIG_ENCRYPTION_EXPONENT)
856
- obj: Self = cls(
857
- modulus_p=min(primes), # "p" is always the smaller
858
- modulus_q=max(primes), # "q" is always the larger
859
- public_modulus=modulus,
860
- encrypt_exp=prime_exp,
861
- decrypt_exp=ModInv(prime_exp, phi),
862
- )
863
- return obj
864
- except (InputError, ModularDivideError) as err:
865
- failures += 1
866
- if failures >= _MAX_KEY_GENERATION_FAILURES:
867
- raise CryptoError(f'failed key generation {failures} times') from err
868
- logging.warning(err)
869
-
870
-
871
- @dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
872
- class ShamirSharedSecretPublic(CryptoKey):
873
- """Shamir Shared Secret (SSS) public part (<https://en.wikipedia.org/wiki/Shamir's_secret_sharing>).
874
-
875
- Attributes:
876
- minimum (int): minimum shares needed for recovery, ≥ 2
877
- modulus (int): prime modulus used for share generation, prime, ≥ 2
34
+ __author__ = 'balparda@github.com'
35
+ __version__: str = base.__version__ # version comes from base!
36
+ __version_tuple__: tuple[int, ...] = base.__version_tuple__
37
+
38
+
39
+ def _ParseInt(s: str, /) -> int:
40
+ """Parse int, try to determine if binary, octal, decimal, or hexadecimal."""
41
+ s = s.strip().lower().replace('_', '')
42
+ base_guess = 10
43
+ if s.startswith('0x'):
44
+ base_guess = 16
45
+ elif s.startswith('0b'):
46
+ base_guess = 2
47
+ elif s.startswith('0o'):
48
+ base_guess = 8
49
+ return int(s, base_guess)
50
+
51
+
52
+ def _ParseIntList(items: Iterable[str], /) -> list[int]:
53
+ """Parse list of strings into list of ints."""
54
+ return [_ParseInt(x) for x in items]
55
+
56
+
57
+ class _StrBytesType(enum.Enum):
58
+ """Type of bytes encoded as string."""
59
+ RAW = 0
60
+ HEXADECIMAL = 1
61
+ BASE64 = 2
62
+
63
+ @staticmethod
64
+ def FromFlags(is_hex: bool, is_base64: bool, is_bin: bool, /) -> _StrBytesType:
65
+ """Use flags to determine the type."""
66
+ if sum((is_hex, is_base64, is_bin)) > 1:
67
+ raise base.InputError('Only one of --hex, --b64, --bin can be set, if any.')
68
+ if is_bin:
69
+ return _StrBytesType.RAW
70
+ if is_base64:
71
+ return _StrBytesType.BASE64
72
+ return _StrBytesType.HEXADECIMAL # default
73
+
74
+
75
+ def _BytesFromText(text: str, tp: _StrBytesType, /) -> bytes:
76
+ """Parse bytes as hex, base64, or raw."""
77
+ match tp:
78
+ case _StrBytesType.RAW:
79
+ return text.encode('utf-8')
80
+ case _StrBytesType.HEXADECIMAL:
81
+ return base.HexToBytes(text)
82
+ case _StrBytesType.BASE64:
83
+ return base.EncodedToBytes(text)
84
+
85
+
86
+ def _BytesToText(b: bytes, tp: _StrBytesType, /) -> str:
87
+ """Output bytes as hex, base64, or raw."""
88
+ match tp:
89
+ case _StrBytesType.RAW:
90
+ return b.decode('utf-8', errors='replace')
91
+ case _StrBytesType.HEXADECIMAL:
92
+ return base.BytesToHex(b)
93
+ case _StrBytesType.BASE64:
94
+ return base.BytesToEncoded(b)
95
+
96
+
97
+ def _MaybePasswordKey(password: str | None, /) -> aes.AESKey | None:
98
+ """Generate a key if there is a password."""
99
+ return aes.AESKey.FromStaticPassword(password) if password else None
100
+
101
+
102
+ def _SaveObj(obj: Any, path: str, password: str | None, /) -> None:
103
+ """Save object."""
104
+ key: aes.AESKey | None = _MaybePasswordKey(password)
105
+ blob: bytes = base.Serialize(obj, file_path=path, key=key)
106
+ logging.info('saved object: %s (%s)', path, base.HumanizedBytes(len(blob)))
107
+
108
+
109
+ def _LoadObj(path: str, password: str | None, expect: type, /) -> Any:
110
+ """Load object."""
111
+ key: aes.AESKey | None = _MaybePasswordKey(password)
112
+ obj: Any = base.DeSerialize(file_path=path, key=key)
113
+ if not isinstance(obj, expect):
114
+ raise base.InputError(
115
+ f'Object loaded from {path} is of invalid type {type(obj)}, expected {expect}')
116
+ return obj
117
+
118
+
119
+ def _FlagNames(a: argparse.Action, /) -> list[str]:
120
+ # Positional args have empty 'option_strings'; otherwise use them (e.g., ['-v','--verbose'])
121
+ if a.option_strings:
122
+ return list(a.option_strings)
123
+ if a.nargs:
124
+ if isinstance(a.metavar, str) and a.metavar:
125
+ # e.g., nargs=2, metavar='FILE'
126
+ return [a.metavar]
127
+ if isinstance(a.metavar, tuple):
128
+ # e.g., nargs=2, metavar=('FILE1', 'FILE2')
129
+ return list(a.metavar)
130
+ # Otherwise, it’s a positional arg with no flags, so return the destination name
131
+ return [a.dest]
132
+
133
+
134
+ def _ActionIsSubparser(a: argparse.Action, /) -> bool:
135
+ return isinstance(a, argparse._SubParsersAction) # type: ignore[attr-defined] # pylint: disable=protected-access
136
+
137
+
138
+ def _FormatDefault(a: argparse.Action, /) -> str:
139
+ if a.default is argparse.SUPPRESS:
140
+ return ''
141
+ if isinstance(a.default, bool):
142
+ return ' (default: on)' if a.default else ''
143
+ if a.default in (None, '', 0, False):
144
+ return ''
145
+ return f' (default: {a.default})'
146
+
147
+
148
+ def _FormatChoices(a: argparse.Action, /) -> str:
149
+ return f' choices: {list(a.choices)}' if getattr(a, 'choices', None) else '' # type:ignore
150
+
151
+
152
+ def _FormatType(a: argparse.Action, /) -> str:
153
+ t: Any | None = getattr(a, 'type', None)
154
+ if t is None:
155
+ return ''
156
+ # Show clean type names (int, str, float); for callables, just say 'custom'
157
+ return f' type: {t.__name__ if hasattr(t, "__name__") else "custom"}'
158
+
159
+
160
+ def _FormatNArgs(a: argparse.Action, /) -> str:
161
+ return f' nargs: {a.nargs}' if getattr(a, 'nargs', None) not in (None, 0) else ''
162
+
163
+
164
+ def _RowsForActions(actions: Sequence[argparse.Action], /) -> list[tuple[str, str]]:
165
+ rows: list[tuple[str, str]] = []
166
+ for a in actions:
167
+ if _ActionIsSubparser(a):
168
+ continue
169
+ # skip the built-in help action; it’s implied
170
+ if getattr(a, 'help', '') == argparse.SUPPRESS or isinstance(a, argparse._HelpAction): # type: ignore[attr-defined] # pylint: disable=protected-access
171
+ continue
172
+ flags: str = ', '.join(_FlagNames(a))
173
+ meta: str = ''.join(
174
+ (_FormatType(a), _FormatNArgs(a), _FormatChoices(a), _FormatDefault(a))).strip()
175
+ desc: str = (a.help or '').strip()
176
+ if meta:
177
+ desc = f'{desc} [{meta}]' if desc else f'[{meta}]'
178
+ rows.append((flags, desc))
179
+ return rows
180
+
181
+
182
+ def _MarkdownTable(
183
+ rows: Sequence[tuple[str, str]],
184
+ headers: tuple[str, str] = ('Option/Arg', 'Description'), /) -> str:
185
+ if not rows:
186
+ return ''
187
+ out: list[str] = ['| ' + headers[0] + ' | ' + headers[1] + ' |', '|---|---|']
188
+ for left, right in rows:
189
+ out.append(f'| `{left}` | {right} |')
190
+ return '\n'.join(out)
191
+
192
+
193
+ def _WalkSubcommands(
194
+ parser: argparse.ArgumentParser, path: list[str] | None = None, /) -> list[
195
+ tuple[list[str], argparse.ArgumentParser, Any]]:
196
+ path = path or []
197
+ items: list[tuple[list[str], argparse.ArgumentParser, Any]] = []
198
+ # sub_action = None
199
+ name: str
200
+ sp: argparse.ArgumentParser
201
+ for action in parser._actions: # type: ignore[attr-defined] # pylint: disable=protected-access
202
+ if _ActionIsSubparser(action):
203
+ # sub_action = a # type: ignore[assignment]
204
+ for name, sp in action.choices.items(): # type:ignore
205
+ items.append((path + [name], sp, action)) # type:ignore
206
+ items.extend(_WalkSubcommands(sp, path + [name])) # type:ignore
207
+ return items
208
+
209
+
210
+ def _HelpText(sub_parser: argparse.ArgumentParser, parent_sub_action: Any, /) -> str:
211
+ if parent_sub_action is not None:
212
+ for choice_action in parent_sub_action._choices_actions: # type: ignore # pylint: disable=protected-access
213
+ if choice_action.dest == sub_parser.prog.split()[-1]:
214
+ return choice_action.help or ''
215
+ return ''
216
+
217
+
218
+ def _GenerateCLIMarkdown() -> str: # pylint: disable=too-many-locals
219
+ """Return a Markdown doc section that reflects the current _BuildParser() tree.
220
+
221
+ Will treat epilog strings as examples, splitting on '$$' to get multiple examples.
878
222
  """
879
-
880
- minimum: int
881
- modulus: int
882
-
883
- def __post_init__(self) -> None:
884
- """Check data.
885
-
886
- Raises:
887
- InputError: invalid inputs
888
- """
889
- super(ShamirSharedSecretPublic, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
890
- if (self.modulus < 2 or
891
- not IsPrime(self.modulus) or
892
- self.minimum < 2):
893
- raise InputError(f'invalid modulus or minimum: {self}')
894
-
895
- def RecoverSecret(
896
- self, shares: Collection['ShamirSharePrivate'], /, *, force_recover: bool = False) -> int:
897
- """Recover the secret from ShamirSharePrivate objects.
898
-
899
- Raises:
900
- InputError: invalid inputs
901
- CryptoError: secret cannot be recovered
902
- """
903
- # check that we have enough shares
904
- share_points: dict[int, int] = {s.share_key: s.share_value for s in shares} # de-dup guaranteed
905
- if (given_shares := len(share_points)) < self.minimum:
906
- mess: str = f'distinct shares {given_shares} < minimum shares {self.minimum}'
907
- if force_recover and given_shares > 1:
908
- logging.error('recovering secret even though: %s', mess)
223
+ parser: argparse.ArgumentParser = _BuildParser()
224
+ assert parser.prog == 'poetry run transcrypto', 'should never happen: module name changed?'
225
+ prog: str = 'transcrypto' # no '.py' needed because poetry run has an alias
226
+ lines: list[str] = ['']
227
+ # Header + global flags
228
+ lines.append('## Command-Line Interface\n')
229
+ lines.append(
230
+ f'`{prog}` is a command-line utility that provides access to all core functionality '
231
+ 'described in this documentation. It serves as a convenient wrapper over the Python APIs, '
232
+ 'enabling **cryptographic operations**, **number theory functions**, **secure randomness '
233
+ 'generation**, **hashing**, **AES**, **RSA**, **El-Gamal**, **DSA**, **bidding**, **SSS**, '
234
+ 'and other utilities without writing code.\n')
235
+ lines.append('Invoke with:\n')
236
+ lines.append('```bash')
237
+ lines.append(f'poetry run {prog} <command> [sub-command] [options...]')
238
+ lines.append('```\n')
239
+ # Global options table
240
+ global_rows: list[tuple[str, str]] = _RowsForActions(parser._actions) # type: ignore[attr-defined] # pylint: disable=protected-access
241
+ if global_rows:
242
+ lines.append('### Global Options\n')
243
+ lines.append(_MarkdownTable(global_rows))
244
+ lines.append('')
245
+ # Top-level commands summary
246
+ lines.append('### Top-Level Commands\n')
247
+ # Find top-level subparsers to list available commands
248
+ top_subs: list[argparse.Action] = [a for a in parser._actions if _ActionIsSubparser(a)] # type: ignore[attr-defined] # pylint: disable=protected-access
249
+ for action in top_subs:
250
+ for name, sp in action.choices.items(): # type: ignore[union-attr]
251
+ help_text: str = (sp.description or sp.format_usage().splitlines()[0]).strip() # type:ignore
252
+ short: str = (sp.help if hasattr(sp, 'help') else '') or '' # type:ignore
253
+ help_text = short or help_text # type:ignore
254
+ help_text = help_text.replace('usage: ', '').strip() # type:ignore
255
+ lines.append(f'- **`{name}`** — `{help_text}`')
256
+ lines.append('')
257
+ if parser.epilog:
258
+ lines.append('```bash')
259
+ lines.append(parser.epilog)
260
+ lines.append('```\n')
261
+ # Detailed sections per (sub)command
262
+ for path, sub_parser, parent_sub_action in _WalkSubcommands(parser):
263
+ if len(path) == 1:
264
+ lines.append('---\n') # horizontal rule between top-level commands
265
+ header: str = ' '.join(path)
266
+ lines.append(f'###{"" if len(path) == 1 else "#"} `{header}`') # (header level 3 or 4)
267
+ # Usage block
268
+ help_text = _HelpText(sub_parser, parent_sub_action)
269
+ if help_text:
270
+ lines.append(f'\n{help_text}')
271
+ usage: str = sub_parser.format_usage().replace('usage: ', '').strip()
272
+ lines.append('\n```bash')
273
+ lines.append(str(usage))
274
+ lines.append('```\n')
275
+ # Options/args table
276
+ rows: list[tuple[str, str]] = _RowsForActions(sub_parser._actions) # type: ignore[attr-defined] # pylint: disable=protected-access
277
+ if rows:
278
+ lines.append(_MarkdownTable(rows))
279
+ lines.append('')
280
+ # Examples (if any) - stored in epilog argument
281
+ epilog: str = sub_parser.epilog.strip() if sub_parser.epilog else ''
282
+ if epilog:
283
+ lines.append('**Example:**\n')
284
+ lines.append('```bash')
285
+ for epilog_line in epilog.split('$$'):
286
+ lines.append(f'$ poetry run {prog} {epilog_line.strip()}')
287
+ lines.append('```\n')
288
+ # join all lines as the markdown string
289
+ return '\n'.join(lines)
290
+
291
+
292
+ def _BuildParser() -> argparse.ArgumentParser: # pylint: disable=too-many-statements,too-many-locals
293
+ """Construct the CLI argument parser (kept in sync with the docs)."""
294
+ # ========================= main parser ==========================================================
295
+ parser: argparse.ArgumentParser = argparse.ArgumentParser(
296
+ prog='poetry run transcrypto',
297
+ description=('transcrypto: CLI for number theory, hashing, '
298
+ 'AES, RSA, El-Gamal, DSA, bidding, SSS, and utilities.'),
299
+ epilog=(
300
+ 'Examples:\n\n'
301
+ ' # --- Randomness ---\n'
302
+ ' poetry run transcrypto random bits 16\n'
303
+ ' poetry run transcrypto random int 1000 2000\n'
304
+ ' poetry run transcrypto random bytes 32\n'
305
+ ' poetry run transcrypto random prime 64\n\n'
306
+ ' # --- Primes ---\n'
307
+ ' poetry run transcrypto isprime 428568761\n'
308
+ ' poetry run transcrypto primegen 100 -c 3\n'
309
+ ' poetry run transcrypto mersenne -k 2 -C 17\n\n'
310
+ ' # --- Integer / Modular Math ---\n'
311
+ ' poetry run transcrypto gcd 462 1071\n'
312
+ ' poetry run transcrypto xgcd 127 13\n'
313
+ ' poetry run transcrypto mod inv 17 97\n'
314
+ ' poetry run transcrypto mod div 6 127 13\n'
315
+ ' poetry run transcrypto mod exp 438 234 127\n'
316
+ ' poetry run transcrypto mod poly 12 17 10 20 30\n'
317
+ ' poetry run transcrypto mod lagrange 5 13 2:4 6:3 7:1\n'
318
+ ' poetry run transcrypto mod crt 6 7 127 13\n\n'
319
+ ' # --- Hashing ---\n'
320
+ ' poetry run transcrypto hash sha256 xyz\n'
321
+ ' poetry run transcrypto --b64 hash sha512 eHl6\n'
322
+ ' poetry run transcrypto hash file /etc/passwd --digest sha512\n\n'
323
+ ' # --- AES ---\n'
324
+ ' poetry run transcrypto --out-b64 aes key "correct horse battery staple"\n'
325
+ ' poetry run transcrypto --b64 --out-b64 aes encrypt -k "<b64key>" "secret"\n'
326
+ ' poetry run transcrypto --b64 --out-b64 aes decrypt -k "<b64key>" "<ciphertext>"\n'
327
+ ' poetry run transcrypto aes ecb -k "<b64key>" encrypt "<128bithexblock>"\n' # cspell:disable-line
328
+ ' poetry run transcrypto aes ecb -k "<b64key>" decrypt "<128bithexblock>"\n\n' # cspell:disable-line
329
+ ' # --- RSA ---\n'
330
+ ' poetry run transcrypto -p rsa-key rsa new --bits 2048\n'
331
+ ' poetry run transcrypto -p rsa-key.pub rsa encrypt <plaintext>\n'
332
+ ' poetry run transcrypto -p rsa-key.priv rsa decrypt <ciphertext>\n'
333
+ ' poetry run transcrypto -p rsa-key.priv rsa sign <message>\n'
334
+ ' poetry run transcrypto -p rsa-key.pub rsa verify <message> <signature>\n\n'
335
+ ' # --- ElGamal ---\n'
336
+ ' poetry run transcrypto -p eg-key elgamal shared --bits 2048\n'
337
+ ' poetry run transcrypto -p eg-key elgamal new\n'
338
+ ' poetry run transcrypto -p eg-key.pub elgamal encrypt <plaintext>\n'
339
+ ' poetry run transcrypto -p eg-key.priv elgamal decrypt <c1:c2>\n'
340
+ ' poetry run transcrypto -p eg-key.priv elgamal sign <message>\n'
341
+ ' poetry run transcrypto-p eg-key.pub elgamal verify <message> <s1:s2>\n\n'
342
+ ' # --- DSA ---\n'
343
+ ' poetry run transcrypto -p dsa-key dsa shared --p-bits 2048 --q-bits 256\n'
344
+ ' poetry run transcrypto -p dsa-key dsa new\n'
345
+ ' poetry run transcrypto -p dsa-key.priv dsa sign <message>\n'
346
+ ' poetry run transcrypto -p dsa-key.pub dsa verify <message> <s1:s2>\n\n'
347
+ ' # --- Public Bid ---\n'
348
+ ' poetry run transcrypto --bin bid new "tomorrow it will rain"\n'
349
+ ' poetry run transcrypto --out-bin bid verify\n\n'
350
+ ' # --- Shamir Secret Sharing (SSS) ---\n'
351
+ ' poetry run transcrypto -p sss-key sss new 3 --bits 1024\n'
352
+ ' poetry run transcrypto -p sss-key sss shares <secret> 5\n'
353
+ ' poetry run transcrypto -p sss-key sss recover\n'
354
+ ' poetry run transcrypto -p sss-key sss verify <secret>'
355
+ ),
356
+ formatter_class=argparse.RawTextHelpFormatter)
357
+ sub = parser.add_subparsers(dest='command')
358
+
359
+ # ========================= global flags =========================================================
360
+ # -v/-vv/-vvv/-vvvv for ERROR/WARN/INFO/DEBUG
361
+ parser.add_argument(
362
+ '-v', '--verbose', action='count', default=0,
363
+ help='Increase verbosity (use -v/-vv/-vvv/-vvvv for ERROR/WARN/INFO/DEBUG)')
364
+
365
+ # --hex/--b64/--bin for input mode (default hex)
366
+ in_grp = parser.add_mutually_exclusive_group()
367
+ in_grp.add_argument('--hex', action='store_true', help='Treat inputs as hex string (default)')
368
+ in_grp.add_argument('--b64', action='store_true', help='Treat inputs as base64url')
369
+ in_grp.add_argument('--bin', action='store_true', help='Treat inputs as binary (bytes)')
370
+
371
+ # --out-hex/--out-b64/--out-bin for output mode (default hex)
372
+ out_grp = parser.add_mutually_exclusive_group()
373
+ out_grp.add_argument('--out-hex', action='store_true', help='Outputs as hex (default)')
374
+ out_grp.add_argument('--out-b64', action='store_true', help='Outputs as base64url')
375
+ out_grp.add_argument('--out-bin', action='store_true', help='Outputs as binary (bytes)')
376
+
377
+ # key loading/saving from/to file, with optional password; will only work with some commands
378
+ parser.add_argument(
379
+ '-p', '--key-path', type=str, default='',
380
+ help='File path to serialized key object, if key is needed for operation')
381
+ parser.add_argument(
382
+ '--protect', type=str, default='',
383
+ help='Password to encrypt/decrypt key file if using the `-p`/`--key-path` option')
384
+
385
+ # ========================= randomness ===========================================================
386
+
387
+ # Cryptographically secure randomness
388
+ p_rand: argparse.ArgumentParser = sub.add_parser(
389
+ 'random', help='Cryptographically secure randomness, from the OS CSPRNG.')
390
+ rsub = p_rand.add_subparsers(dest='rand_command')
391
+
392
+ # Random bits
393
+ p_rand_bits: argparse.ArgumentParser = rsub.add_parser(
394
+ 'bits',
395
+ help='Random integer with exact bit length = `bits` (MSB will be 1).',
396
+ epilog='random bits 16\n36650')
397
+ p_rand_bits.add_argument('bits', type=int, help='Number of bits, ≥ 8')
398
+
399
+ # Random integer in [min, max]
400
+ p_rand_int: argparse.ArgumentParser = rsub.add_parser(
401
+ 'int',
402
+ help='Uniform random integer in `[min, max]` range, inclusive.',
403
+ epilog='random int 1000 2000\n1628')
404
+ p_rand_int.add_argument('min', type=str, help='Minimum, ≥ 0')
405
+ p_rand_int.add_argument('max', type=str, help='Maximum, > `min`')
406
+
407
+ # Random bytes
408
+ p_rand_bytes: argparse.ArgumentParser = rsub.add_parser(
409
+ 'bytes',
410
+ help='Generates `n` cryptographically secure random bytes.',
411
+ epilog='random bytes 32\n6c6f1f88cb93c4323285a2224373d6e59c72a9c2b82e20d1c376df4ffbe9507f')
412
+ p_rand_bytes.add_argument('n', type=int, help='Number of bytes, ≥ 1')
413
+
414
+ # Random prime with given bit length
415
+ p_rand_prime: argparse.ArgumentParser = rsub.add_parser(
416
+ 'prime',
417
+ help='Generate a random prime with exact bit length = `bits` (MSB will be 1).',
418
+ epilog='random prime 32\n2365910551')
419
+ p_rand_prime.add_argument('bits', type=int, help='Bit length, ≥ 11')
420
+
421
+ # ========================= primes ===============================================================
422
+
423
+ # Primality test with safe defaults
424
+ p_isprime: argparse.ArgumentParser = sub.add_parser(
425
+ 'isprime',
426
+ help='Primality test with safe defaults, useful for any integer size.',
427
+ epilog='isprime 2305843009213693951\nTrue $$ isprime 2305843009213693953\nFalse')
428
+ p_isprime.add_argument(
429
+ 'n', type=str, help='Integer to test, ≥ 1')
430
+
431
+ # Primes generator
432
+ p_pg: argparse.ArgumentParser = sub.add_parser(
433
+ 'primegen',
434
+ help='Generate (stream) primes ≥ `start` (prints a limited `count` by default).',
435
+ epilog='primegen 100 -c 3\n101\n103\n107')
436
+ p_pg.add_argument('start', type=str, help='Starting integer (inclusive)')
437
+ p_pg.add_argument(
438
+ '-c', '--count', type=int, default=10, help='How many to print (0 = unlimited)')
439
+
440
+ # Mersenne primes generator
441
+ p_mersenne: argparse.ArgumentParser = sub.add_parser(
442
+ 'mersenne',
443
+ help=('Generate (stream) Mersenne prime exponents `k`, also outputting `2^k-1` '
444
+ '(the Mersenne prime, `M`) and `M×2^(k-1)` (the associated perfect number), '
445
+ 'starting at `min-k` and stopping once `k` > `cutoff-k`.'),
446
+ epilog=('mersenne -k 0 -C 15\nk=2 M=3 perfect=6\nk=3 M=7 perfect=28\n'
447
+ 'k=5 M=31 perfect=496\nk=7 M=127 perfect=8128\n'
448
+ 'k=13 M=8191 perfect=33550336\nk=17 M=131071 perfect=8589869056'))
449
+ p_mersenne.add_argument(
450
+ '-k', '--min-k', type=int, default=1, help='Starting exponent `k`, ≥ 1')
451
+ p_mersenne.add_argument(
452
+ '-C', '--cutoff-k', type=int, default=10000, help='Stop once `k` > `cutoff-k`')
453
+
454
+ # ========================= integer / modular math ===============================================
455
+
456
+ # GCD
457
+ p_gcd: argparse.ArgumentParser = sub.add_parser(
458
+ 'gcd',
459
+ help='Greatest Common Divisor (GCD) of integers `a` and `b`.',
460
+ epilog='gcd 462 1071\n21 $$ gcd 0 5\n5 $$ gcd 127 13\n1')
461
+ p_gcd.add_argument('a', type=str, help='Integer, ≥ 0')
462
+ p_gcd.add_argument('b', type=str, help='Integer, ≥ 0 (can\'t be both zero)')
463
+
464
+ # Extended GCD
465
+ p_xgcd: argparse.ArgumentParser = sub.add_parser(
466
+ 'xgcd',
467
+ help=('Extended Greatest Common Divisor (x-GCD) of integers `a` and `b`, '
468
+ 'will return `(g, x, y)` where `a×x+b×y==g`.'),
469
+ epilog='xgcd 462 1071\n(21, 7, -3) $$ xgcd 0 5\n(5, 0, 1) $$ xgcd 127 13\n(1, 4, -39)')
470
+ p_xgcd.add_argument('a', type=str, help='Integer, ≥ 0')
471
+ p_xgcd.add_argument('b', type=str, help='Integer, ≥ 0 (can\'t be both zero)')
472
+
473
+ # Modular math group
474
+ p_mod: argparse.ArgumentParser = sub.add_parser('mod', help='Modular arithmetic helpers.')
475
+ mod_sub = p_mod.add_subparsers(dest='mod_command')
476
+
477
+ # Modular inverse
478
+ p_mi: argparse.ArgumentParser = mod_sub.add_parser(
479
+ 'inv',
480
+ help=('Modular inverse: find integer 0≤`i`<`m` such that `a×i ≡ 1 (mod m)`. '
481
+ 'Will only work if `gcd(a,m)==1`, else will fail with a message.'),
482
+ epilog=('mod inv 127 13\n4 $$ mod inv 17 3120\n2753 $$ '
483
+ 'mod inv 462 1071\n<<INVALID>> no modular inverse exists (ModularDivideError)'))
484
+ p_mi.add_argument('a', type=str, help='Integer to invert')
485
+ p_mi.add_argument('m', type=str, help='Modulus `m`, ≥ 2')
486
+
487
+ # Modular division
488
+ p_md: argparse.ArgumentParser = mod_sub.add_parser(
489
+ 'div',
490
+ help=('Modular division: find integer 0≤`z`<`m` such that `z×y ≡ x (mod m)`. '
491
+ 'Will only work if `gcd(y,m)==1` and `y!=0`, else will fail with a message.'),
492
+ epilog=('mod div 6 127 13\n11 $$ '
493
+ 'mod div 6 0 13\n<<INVALID>> no modular inverse exists (ModularDivideError)'))
494
+ p_md.add_argument('x', type=str, help='Integer')
495
+ p_md.add_argument('y', type=str, help='Integer, cannot be zero')
496
+ p_md.add_argument('m', type=str, help='Modulus `m`, ≥ 2')
497
+
498
+ # Modular exponentiation
499
+ p_me: argparse.ArgumentParser = mod_sub.add_parser(
500
+ 'exp',
501
+ help='Modular exponentiation: `a^e mod m`. Efficient, can handle huge values.',
502
+ epilog='mod exp 438 234 127\n32 $$ mod exp 438 234 89854\n60622')
503
+ p_me.add_argument('a', type=str, help='Integer')
504
+ p_me.add_argument('e', type=str, help='Integer, ≥ 0')
505
+ p_me.add_argument('m', type=str, help='Modulus `m`, ≥ 2')
506
+
507
+ # Polynomial evaluation mod m
508
+ p_mp: argparse.ArgumentParser = mod_sub.add_parser(
509
+ 'poly',
510
+ help=('Efficiently evaluate polynomial with `coeff` coefficients at point `x` modulo `m` '
511
+ '(`c₀+c₁×x+c₂×x²+…+cₙ×xⁿ mod m`).'),
512
+ epilog=('mod poly 12 17 10 20 30\n14 # (10+20×12+30×12² ≡ 14 (mod 17)) $$ '
513
+ 'mod poly 10 97 3 0 0 1 1\n42 # (3+1×10³+1×10⁴ ≡ 42 (mod 97))'))
514
+ p_mp.add_argument('x', type=str, help='Evaluation point `x`')
515
+ p_mp.add_argument('m', type=str, help='Modulus `m`, ≥ 2')
516
+ p_mp.add_argument(
517
+ 'coeff', nargs='+', help='Coefficients (constant-term first: `c₀+c₁×x+c₂×x²+…+cₙ×xⁿ`)')
518
+
519
+ # Lagrange interpolation mod m
520
+ p_ml: argparse.ArgumentParser = mod_sub.add_parser(
521
+ 'lagrange',
522
+ help=('Lagrange interpolation over modulus `m`: find the `f(x)` solution for the '
523
+ 'given `x` and `zₙ:f(zₙ)` points `pt`. The modulus `m` must be a prime.'),
524
+ epilog=('mod lagrange 5 13 2:4 6:3 7:1\n3 # passes through (2,4), (6,3), (7,1) $$ '
525
+ 'mod lagrange 11 97 1:1 2:4 3:9 4:16 5:25\n24 '
526
+ '# passes through (1,1), (2,4), (3,9), (4,16), (5,25)'))
527
+ p_ml.add_argument('x', type=str, help='Evaluation point `x`')
528
+ p_ml.add_argument('m', type=str, help='Modulus `m`, ≥ 2')
529
+ p_ml.add_argument(
530
+ 'pt', nargs='+', help='Points `zₙ:f(zₙ)` as `key:value` pairs (e.g., `2:4 5:3 7:1`)')
531
+
532
+ # Chinese Remainder Theorem for 2 equations
533
+ p_crt: argparse.ArgumentParser = mod_sub.add_parser(
534
+ 'crt',
535
+ help=('Solves Chinese Remainder Theorem (CRT) Pair: finds the unique integer 0≤`x`<`(m1×m2)` '
536
+ 'satisfying both `x ≡ a1 (mod m1)` and `x ≡ a2 (mod m2)`, if `gcd(m1,m2)==1`.'),
537
+ epilog=('mod crt 6 7 127 13\n62 $$ mod crt 12 56 17 19\n796 $$ '
538
+ 'mod crt 6 7 462 1071\n<<INVALID>> moduli m1/m2 not co-prime (ModularDivideError)'))
539
+ p_crt.add_argument('a1', type=str, help='Integer residue for first congruence')
540
+ p_crt.add_argument('m1', type=str, help='Modulus `m1`, ≥ 2 and `gcd(m1,m2)==1`')
541
+ p_crt.add_argument('a2', type=str, help='Integer residue for second congruence')
542
+ p_crt.add_argument('m2', type=str, help='Modulus `m2`, ≥ 2 and `gcd(m1,m2)==1`')
543
+
544
+ # ========================= hashing ==============================================================
545
+
546
+ # Hashing group
547
+ p_hash: argparse.ArgumentParser = sub.add_parser(
548
+ 'hash', help='Cryptographic Hashing (SHA-256 / SHA-512 / file).')
549
+ hash_sub = p_hash.add_subparsers(dest='hash_command')
550
+
551
+ # SHA-256
552
+ p_h256: argparse.ArgumentParser = hash_sub.add_parser(
553
+ 'sha256',
554
+ help='SHA-256 of input `data`.',
555
+ epilog=('--bin hash sha256 xyz\n'
556
+ '3608bca1e44ea6c4d268eb6db02260269892c0b42b86bbf1e77a6fa16c3c9282 $$'
557
+ '--b64 hash sha256 eHl6 # "xyz" in base-64\n'
558
+ '3608bca1e44ea6c4d268eb6db02260269892c0b42b86bbf1e77a6fa16c3c9282'))
559
+ p_h256.add_argument('data', type=str, help='Input data (raw text; or use --hex/--b64/--bin)')
560
+
561
+ # SHA-512
562
+ p_h512 = hash_sub.add_parser(
563
+ 'sha512',
564
+ help='SHA-512 of input `data`.',
565
+ epilog=('--bin hash sha512 xyz\n'
566
+ '4a3ed8147e37876adc8f76328e5abcc1b470e6acfc18efea0135f983604953a5'
567
+ '8e183c1a6086e91ba3e821d926f5fdeb37761c7ca0328a963f5e92870675b728 $$'
568
+ '--b64 hash sha512 eHl6 # "xyz" in base-64\n'
569
+ '4a3ed8147e37876adc8f76328e5abcc1b470e6acfc18efea0135f983604953a5'
570
+ '8e183c1a6086e91ba3e821d926f5fdeb37761c7ca0328a963f5e92870675b728'))
571
+ p_h512.add_argument('data', type=str, help='Input data (raw text; or use --hex/--b64/--bin)')
572
+
573
+ # Hash file contents (streamed)
574
+ p_hf: argparse.ArgumentParser = hash_sub.add_parser(
575
+ 'file',
576
+ help='SHA-256/512 hash of file contents, defaulting to SHA-256.',
577
+ epilog=('hash file /etc/passwd --digest sha512\n'
578
+ '8966f5953e79f55dfe34d3dc5b160ac4a4a3f9cbd1c36695a54e28d77c7874df'
579
+ 'f8595502f8a420608911b87d336d9e83c890f0e7ec11a76cb10b03e757f78aea'))
580
+ p_hf.add_argument('path', type=str, help='Path to existing file')
581
+ p_hf.add_argument('--digest', choices=['sha256', 'sha512'], default='sha256',
582
+ help='Digest type, SHA-256 ("sha256") or SHA-512 ("sha512")')
583
+
584
+ # ========================= AES (GCM + ECB helper) ===============================================
585
+
586
+ # AES group
587
+ p_aes: argparse.ArgumentParser = sub.add_parser(
588
+ 'aes',
589
+ help=('AES-256 operations (GCM/ECB) and key derivation. '
590
+ 'No measures are taken here to prevent timing attacks.'))
591
+ aes_sub = p_aes.add_subparsers(dest='aes_command')
592
+
593
+ # Derive key from password
594
+ p_aes_key_pass: argparse.ArgumentParser = aes_sub.add_parser(
595
+ 'key',
596
+ help=('Derive key from a password (PBKDF2-HMAC-SHA256) with custom expensive '
597
+ 'salt and iterations. Very good/safe for simple password-to-key but not for '
598
+ 'passwords databases (because of constant salt).'),
599
+ epilog=('--out-b64 aes key "correct horse battery staple"\n'
600
+ 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= $$ ' # cspell:disable-line
601
+ '-p keyfile.out --protect hunter aes key "correct horse battery staple"\n'
602
+ 'AES key saved to \'keyfile.out\''))
603
+ p_aes_key_pass.add_argument(
604
+ 'password', type=str, help='Password (leading/trailing spaces ignored)')
605
+
606
+ # AES-256-GCM encrypt
607
+ p_aes_enc: argparse.ArgumentParser = aes_sub.add_parser(
608
+ 'encrypt',
609
+ help=('AES-256-GCM: safely encrypt `plaintext` with `-k`/`--key` or with '
610
+ '`-p`/`--key-path` keyfile. All inputs are raw, or you '
611
+ 'can use `--bin`/`--hex`/`--b64` flags. Attention: if you provide `-a`/`--aad` '
612
+ '(associated data, AAD), you will need to provide the same AAD when decrypting '
613
+ 'and it is NOT included in the `ciphertext`/CT returned by this method!'),
614
+ epilog=('--b64 --out-b64 aes encrypt -k DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= ' # cspell:disable-line
615
+ 'AAAAAAB4eXo=\nF2_ZLrUw5Y8oDnbTP5t5xCUWX8WtVILLD0teyUi_37_4KHeV-YowVA== $$ ' # cspell:disable-line
616
+ '--b64 --out-b64 aes encrypt -k DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= -a eHl6 ' # cspell:disable-line
617
+ 'AAAAAAB4eXo=\nxOlAHPUPpeyZHId-f3VQ_QKKMxjIW0_FBo9WOfIBrzjn0VkVV6xTRA==')) # cspell:disable-line
618
+ p_aes_enc.add_argument('plaintext', type=str, help='Input data to encrypt (PT)')
619
+ p_aes_enc.add_argument(
620
+ '-k', '--key', type=str, default='', help='Key if `-p`/`--key-path` wasn\'t used (32 bytes)')
621
+ p_aes_enc.add_argument(
622
+ '-a', '--aad', type=str, default='',
623
+ help='Associated data (optional; has to be separately sent to receiver/stored)')
624
+
625
+ # AES-256-GCM decrypt
626
+ p_aes_dec: argparse.ArgumentParser = aes_sub.add_parser(
627
+ 'decrypt',
628
+ help=('AES-256-GCM: safely decrypt `ciphertext` with `-k`/`--key` or with '
629
+ '`-p`/`--key-path` keyfile. All inputs are raw, or you '
630
+ 'can use `--bin`/`--hex`/`--b64` flags. Attention: if you provided `-a`/`--aad` '
631
+ '(associated data, AAD) during encryption, you will need to provide the same AAD now!'),
632
+ epilog=('--b64 --out-b64 aes decrypt -k DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= ' # cspell:disable-line
633
+ 'F2_ZLrUw5Y8oDnbTP5t5xCUWX8WtVILLD0teyUi_37_4KHeV-YowVA==\nAAAAAAB4eXo= $$ ' # cspell:disable-line
634
+ '--b64 --out-b64 aes decrypt -k DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= ' # cspell:disable-line
635
+ '-a eHl6 xOlAHPUPpeyZHId-f3VQ_QKKMxjIW0_FBo9WOfIBrzjn0VkVV6xTRA==\nAAAAAAB4eXo=')) # cspell:disable-line
636
+ p_aes_dec.add_argument('ciphertext', type=str, help='Input data to decrypt (CT)')
637
+ p_aes_dec.add_argument(
638
+ '-k', '--key', type=str, default='', help='Key if `-p`/`--key-path` wasn\'t used (32 bytes)')
639
+ p_aes_dec.add_argument(
640
+ '-a', '--aad', type=str, default='',
641
+ help='Associated data (optional; has to be exactly the same as used during encryption)')
642
+
643
+ # AES-ECB
644
+ p_aes_ecb: argparse.ArgumentParser = aes_sub.add_parser(
645
+ 'ecb',
646
+ help=('AES-256-ECB: encrypt/decrypt 128 bit (16 bytes) hexadecimal blocks. UNSAFE, except '
647
+ 'for specifically encrypting hash blocks which are very much expected to look random. '
648
+ 'ECB mode will have the same output for the same input (no IV/nonce is used).'))
649
+ p_aes_ecb.add_argument(
650
+ '-k', '--key', type=str, default='',
651
+ help=('Key if `-p`/`--key-path` wasn\'t used (32 bytes; raw, or you '
652
+ 'can use `--bin`/`--hex`/`--b64` flags)'))
653
+ aes_ecb_sub = p_aes_ecb.add_subparsers(dest='aes_ecb_command')
654
+
655
+ # AES-ECB encrypt 16-byte hex block
656
+ p_aes_ecb_e: argparse.ArgumentParser = aes_ecb_sub.add_parser(
657
+ 'encrypt',
658
+ help=('AES-256-ECB: encrypt 16-bytes hex `plaintext` with `-k`/`--key` or with '
659
+ '`-p`/`--key-path` keyfile. UNSAFE, except for specifically encrypting hash blocks.'),
660
+ epilog=('--b64 aes ecb -k DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= encrypt ' # cspell:disable-line
661
+ '00112233445566778899aabbccddeeff\n54ec742ca3da7b752e527b74e3a798d7'))
662
+ p_aes_ecb_e.add_argument('plaintext', type=str, help='Plaintext block as 32 hex chars (16-bytes)')
663
+
664
+ # AES-ECB decrypt 16-byte hex block
665
+ p_aes_scb_d: argparse.ArgumentParser = aes_ecb_sub.add_parser(
666
+ 'decrypt',
667
+ help=('AES-256-ECB: decrypt 16-bytes hex `ciphertext` with `-k`/`--key` or with '
668
+ '`-p`/`--key-path` keyfile. UNSAFE, except for specifically encrypting hash blocks.'),
669
+ epilog=('--b64 aes ecb -k DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= decrypt ' # cspell:disable-line
670
+ '54ec742ca3da7b752e527b74e3a798d7\n00112233445566778899aabbccddeeff')) # cspell:disable-line
671
+ p_aes_scb_d.add_argument(
672
+ 'ciphertext', type=str, help='Ciphertext block as 32 hex chars (16-bytes)')
673
+
674
+ # ========================= RSA ==================================================================
675
+
676
+ # RSA group
677
+ p_rsa: argparse.ArgumentParser = sub.add_parser(
678
+ 'rsa',
679
+ help=('Raw RSA (Rivest-Shamir-Adleman) asymmetric cryptography over *integers* '
680
+ '(BEWARE: no OAEP/PSS padding or validation). '
681
+ 'These are pedagogical/raw primitives; do not use for new protocols. '
682
+ 'No measures are taken here to prevent timing attacks. '
683
+ 'All methods require file key(s) as `-p`/`--key-path` (see provided examples).'))
684
+ rsa_sub = p_rsa.add_subparsers(dest='rsa_command')
685
+
686
+ # Generate new RSA private key
687
+ p_rsa_new: argparse.ArgumentParser = rsa_sub.add_parser(
688
+ 'new',
689
+ help=('Generate RSA private/public key pair with `bits` modulus size '
690
+ '(prime sizes will be `bits`/2). '
691
+ 'Requires `-p`/`--key-path` to set the basename for output files.'),
692
+ epilog=('-p rsa-key rsa new --bits 64 # NEVER use such a small key: example only!\n'
693
+ 'RSA private/public keys saved to \'rsa-key.priv/.pub\''))
694
+ p_rsa_new.add_argument(
695
+ '--bits', type=int, default=3332, help='Modulus size in bits; the default is a safe size')
696
+
697
+ # Encrypt integer with public key
698
+ p_rsa_enc: argparse.ArgumentParser = rsa_sub.add_parser(
699
+ 'encrypt',
700
+ help='Encrypt integer `message` with public key.',
701
+ epilog='-p rsa-key.pub rsa encrypt 999\n6354905961171348600')
702
+ p_rsa_enc.add_argument(
703
+ 'message', type=str, help='Integer message to encrypt, 1≤`message`<*modulus*')
704
+
705
+ # Decrypt integer ciphertext with private key
706
+ p_rsa_dec: argparse.ArgumentParser = rsa_sub.add_parser(
707
+ 'decrypt',
708
+ help='Decrypt integer `ciphertext` with private key.',
709
+ epilog='-p rsa-key.priv rsa decrypt 6354905961171348600\n999')
710
+ p_rsa_dec.add_argument(
711
+ 'ciphertext', type=str, help='Integer ciphertext to decrypt, 1≤`ciphertext`<*modulus*')
712
+
713
+ # Sign integer message with private key
714
+ p_rsa_sig: argparse.ArgumentParser = rsa_sub.add_parser(
715
+ 'sign',
716
+ help='Sign integer `message` with private key.',
717
+ epilog='-p rsa-key.priv rsa sign 999\n7632909108672871784')
718
+ p_rsa_sig.add_argument('message', type=str, help='Integer message to sign, 1≤`message`<*modulus*')
719
+
720
+ # Verify integer signature with public key
721
+ p_rsa_ver: argparse.ArgumentParser = rsa_sub.add_parser(
722
+ 'verify',
723
+ help='Verify integer `signature` for integer `message` with public key.',
724
+ epilog=('-p rsa-key.pub rsa verify 999 7632909108672871784\nRSA signature: OK $$ '
725
+ '-p rsa-key.pub rsa verify 999 7632909108672871785\nRSA signature: INVALID'))
726
+ p_rsa_ver.add_argument(
727
+ 'message', type=str, help='Integer message that was signed earlier, 1≤`message`<*modulus*')
728
+ p_rsa_ver.add_argument(
729
+ 'signature', type=str,
730
+ help='Integer putative signature for `message`, 1≤`signature`<*modulus*')
731
+
732
+ # ========================= ElGamal ==============================================================
733
+
734
+ # ElGamal group
735
+ p_eg: argparse.ArgumentParser = sub.add_parser(
736
+ 'elgamal',
737
+ help=('Raw El-Gamal asymmetric cryptography over *integers* '
738
+ '(BEWARE: no ECIES-style KEM/DEM padding or validation). These are '
739
+ 'pedagogical/raw primitives; do not use for new protocols. '
740
+ 'No measures are taken here to prevent timing attacks. '
741
+ 'All methods require file key(s) as `-p`/`--key-path` (see provided examples).'))
742
+ eg_sub = p_eg.add_subparsers(dest='eg_command')
743
+
744
+ # Generate shared (p,g) params
745
+ p_eg_shared: argparse.ArgumentParser = eg_sub.add_parser(
746
+ 'shared',
747
+ help=('Generate a shared El-Gamal key with `bits` prime modulus size, which is the '
748
+ 'first step in key generation. '
749
+ 'The shared key can safely be used by any number of users to generate their '
750
+ 'private/public key pairs (with the `new` command). The shared keys are "public". '
751
+ 'Requires `-p`/`--key-path` to set the basename for output files.'),
752
+ epilog=('-p eg-key elgamal shared --bits 64 # NEVER use such a small key: example only!\n'
753
+ 'El-Gamal shared key saved to \'eg-key.shared\''))
754
+ p_eg_shared.add_argument(
755
+ '--bits', type=int, default=3332,
756
+ help='Prime modulus (`p`) size in bits; the default is a safe size')
757
+
758
+ # Generate individual private key from shared (p,g)
759
+ eg_sub.add_parser(
760
+ 'new',
761
+ help='Generate an individual El-Gamal private/public key pair from a shared key.',
762
+ epilog='-p eg-key elgamal new\nEl-Gamal private/public keys saved to \'eg-key.priv/.pub\'')
763
+
764
+ # Encrypt integer with public key
765
+ p_eg_enc: argparse.ArgumentParser = eg_sub.add_parser(
766
+ 'encrypt',
767
+ help='Encrypt integer `message` with public key.',
768
+ epilog='-p eg-key.pub elgamal encrypt 999\n2948854810728206041:15945988196340032688')
769
+ p_eg_enc.add_argument(
770
+ 'message', type=str, help='Integer message to encrypt, 1≤`message`<*modulus*')
771
+
772
+ # Decrypt El-Gamal ciphertext tuple (c1,c2)
773
+ p_eg_dec: argparse.ArgumentParser = eg_sub.add_parser(
774
+ 'decrypt',
775
+ help='Decrypt integer `ciphertext` with private key.',
776
+ epilog='-p eg-key.priv elgamal decrypt 2948854810728206041:15945988196340032688\n999')
777
+ p_eg_dec.add_argument(
778
+ 'ciphertext', type=str,
779
+ help=('Integer ciphertext to decrypt; expects `c1:c2` format with 2 integers, '
780
+ ' 2≤`c1`,`c2`<*modulus*'))
781
+
782
+ # Sign integer message with private key
783
+ p_eg_sig: argparse.ArgumentParser = eg_sub.add_parser(
784
+ 'sign',
785
+ help='Sign integer message with private key. Output will 2 integers in a `s1:s2` format.',
786
+ epilog='-p eg-key.priv elgamal sign 999\n4674885853217269088:14532144906178302633')
787
+ p_eg_sig.add_argument('message', type=str, help='Integer message to sign, 1≤`message`<*modulus*')
788
+
789
+ # Verify El-Gamal signature (s1,s2)
790
+ p_eg_ver: argparse.ArgumentParser = eg_sub.add_parser(
791
+ 'verify',
792
+ help='Verify integer `signature` for integer `message` with public key.',
793
+ epilog=('-p eg-key.pub elgamal verify 999 4674885853217269088:14532144906178302633\n'
794
+ 'El-Gamal signature: OK $$ '
795
+ '-p eg-key.pub elgamal verify 999 4674885853217269088:14532144906178302632\n'
796
+ 'El-Gamal signature: INVALID'))
797
+ p_eg_ver.add_argument(
798
+ 'message', type=str, help='Integer message that was signed earlier, 1≤`message`<*modulus*')
799
+ p_eg_ver.add_argument(
800
+ 'signature', type=str,
801
+ help=('Integer putative signature for `message`; expects `s1:s2` format with 2 integers, '
802
+ ' 2≤`s1`,`s2`<*modulus*'))
803
+
804
+ # ========================= DSA ==================================================================
805
+
806
+ # DSA group
807
+ p_dsa: argparse.ArgumentParser = sub.add_parser(
808
+ 'dsa',
809
+ help=('Raw DSA (Digital Signature Algorithm) asymmetric signing over *integers* '
810
+ '(BEWARE: no ECDSA/EdDSA padding or validation). These are pedagogical/raw '
811
+ 'primitives; do not use for new protocols. '
812
+ 'No measures are taken here to prevent timing attacks. '
813
+ 'All methods require file key(s) as `-p`/`--key-path` (see provided examples).'))
814
+ dsa_sub = p_dsa.add_subparsers(dest='dsa_command')
815
+
816
+ # Generate shared (p,q,g) params
817
+ p_dsa_shared: argparse.ArgumentParser = dsa_sub.add_parser(
818
+ 'shared',
819
+ help=('Generate a shared DSA key with `p-bits`/`q-bits` prime modulus sizes, which is '
820
+ 'the first step in key generation. `q-bits` should be larger than the secrets that '
821
+ 'will be protected and `p-bits` should be much larger than `q-bits` (e.g. 3584/256). '
822
+ 'The shared key can safely be used by any number of users to generate their '
823
+ 'private/public key pairs (with the `new` command). The shared keys are "public". '
824
+ 'Requires `-p`/`--key-path` to set the basename for output files.'),
825
+ epilog=('-p dsa-key dsa shared --p-bits 128 --q-bits 32 '
826
+ '# NEVER use such a small key: example only!\n'
827
+ 'DSA shared key saved to \'dsa-key.shared\''))
828
+ p_dsa_shared.add_argument(
829
+ '--p-bits', type=int, default=3584,
830
+ help='Prime modulus (`p`) size in bits; the default is a safe size')
831
+ p_dsa_shared.add_argument(
832
+ '--q-bits', type=int, default=256,
833
+ help=('Prime modulus (`q`) size in bits; the default is a safe size ***IFF*** you '
834
+ 'are protecting symmetric keys or regular hashes'))
835
+
836
+ # Generate individual private key from shared (p,q,g)
837
+ dsa_sub.add_parser(
838
+ 'new',
839
+ help='Generate an individual DSA private/public key pair from a shared key.',
840
+ epilog='-p dsa-key dsa new\nDSA private/public keys saved to \'dsa-key.priv/.pub\'')
841
+
842
+ # Sign integer m with private key
843
+ p_dsa_sign: argparse.ArgumentParser = dsa_sub.add_parser(
844
+ 'sign',
845
+ help='Sign integer message with private key. Output will 2 integers in a `s1:s2` format.',
846
+ epilog='-p dsa-key.priv dsa sign 999\n2395961484:3435572290')
847
+ p_dsa_sign.add_argument('message', type=str, help='Integer message to sign, 1≤`message`<`q`')
848
+
849
+ # Verify DSA signature (s1,s2)
850
+ p_dsa_verify: argparse.ArgumentParser = dsa_sub.add_parser(
851
+ 'verify',
852
+ help='Verify integer `signature` for integer `message` with public key.',
853
+ epilog=('-p dsa-key.pub dsa verify 999 2395961484:3435572290\nDSA signature: OK $$ '
854
+ '-p dsa-key.pub dsa verify 999 2395961484:3435572291\nDSA signature: INVALID'))
855
+ p_dsa_verify.add_argument(
856
+ 'message', type=str, help='Integer message that was signed earlier, 1≤`message`<`q`')
857
+ p_dsa_verify.add_argument(
858
+ 'signature', type=str,
859
+ help=('Integer putative signature for `message`; expects `s1:s2` format with 2 integers, '
860
+ ' 2≤`s1`,`s2`<`q`'))
861
+
862
+ # ========================= Public Bid ===========================================================
863
+
864
+ # bidding group
865
+ p_bid: argparse.ArgumentParser = sub.add_parser(
866
+ 'bid',
867
+ help=('Bidding on a `secret` so that you can cryptographically convince a neutral '
868
+ 'party that the `secret` that was committed to previously was not changed. '
869
+ 'All methods require file key(s) as `-p`/`--key-path` (see provided examples).'))
870
+ bid_sub = p_bid.add_subparsers(dest='bid_command')
871
+
872
+ # Generate a new bid
873
+ p_bid_new: argparse.ArgumentParser = bid_sub.add_parser(
874
+ 'new',
875
+ help=('Generate the bid files for `secret`. '
876
+ 'Requires `-p`/`--key-path` to set the basename for output files.'),
877
+ epilog=('--bin -p my-bid bid new "tomorrow it will rain"\n'
878
+ 'Bid private/public commitments saved to \'my-bid.priv/.pub\''))
879
+ p_bid_new.add_argument('secret', type=str, help='Input data to bid to, the protected "secret"')
880
+
881
+ # verify bid
882
+ bid_sub.add_parser(
883
+ 'verify',
884
+ help=('Verify the bid files for correctness and reveal the `secret`. '
885
+ 'Requires `-p`/`--key-path` to set the basename for output files.'),
886
+ epilog=('--out-bin -p my-bid bid verify\n'
887
+ 'Bid commitment: OK\nBid secret:\ntomorrow it will rain'))
888
+
889
+ # ========================= Shamir Secret Sharing ================================================
890
+
891
+ # SSS group
892
+ p_sss: argparse.ArgumentParser = sub.add_parser(
893
+ 'sss',
894
+ help=('Raw SSS (Shamir Shared Secret) secret sharing crypto scheme over *integers* '
895
+ '(BEWARE: no modern message wrapping, padding or validation). These are '
896
+ 'pedagogical/raw primitives; do not use for new protocols. '
897
+ 'No measures are taken here to prevent timing attacks. '
898
+ 'All methods require file key(s) as `-p`/`--key-path` (see provided examples).'))
899
+ sss_sub = p_sss.add_subparsers(dest='sss_command')
900
+
901
+ # Generate new SSS params (t, prime, coefficients)
902
+ p_sss_new: argparse.ArgumentParser = sss_sub.add_parser(
903
+ 'new',
904
+ help=('Generate the private keys with `bits` prime modulus size and so that at least a '
905
+ '`minimum` number of shares are needed to recover the secret. '
906
+ 'This key will be used to generate the shares later (with the `shares` command). '
907
+ 'Requires `-p`/`--key-path` to set the basename for output files.'),
908
+ epilog=('-p sss-key sss new 3 --bits 64 # NEVER use such a small key: example only!\n'
909
+ 'SSS private/public keys saved to \'sss-key.priv/.pub\''))
910
+ p_sss_new.add_argument(
911
+ 'minimum', type=int, help='Minimum number of shares required to recover secret, ≥ 2')
912
+ p_sss_new.add_argument(
913
+ '--bits', type=int, default=1024,
914
+ help=('Prime modulus (`p`) size in bits; the default is a safe size ***IFF*** you '
915
+ 'are protecting symmetric keys; the number of bits should be comfortably larger '
916
+ 'than the size of the secret you want to protect with this scheme'))
917
+
918
+ # Issue N shares for a secret
919
+ p_sss_shares: argparse.ArgumentParser = sss_sub.add_parser(
920
+ 'shares',
921
+ help='Issue `count` private shares for an integer `secret`.',
922
+ epilog=('-p sss-key sss shares 999 5\n'
923
+ 'SSS 5 individual (private) shares saved to \'sss-key.share.1…5\'\n'
924
+ '$ rm sss-key.share.2 sss-key.share.4 '
925
+ '# this is to simulate only having shares 1,3,5'))
926
+ p_sss_shares.add_argument(
927
+ 'secret', type=str, help='Integer secret to be protected, 1≤`secret`<*modulus*')
928
+ p_sss_shares.add_argument(
929
+ 'count', type=int,
930
+ help=('How many shares to produce; must be ≥ `minimum` used in `new` command or else the '
931
+ '`secret` would become unrecoverable'))
932
+
933
+ # Recover secret from shares
934
+ sss_sub.add_parser(
935
+ 'recover',
936
+ help='Recover secret from shares; will use any available shares that were found.',
937
+ epilog=('-p sss-key sss recover\n'
938
+ 'Loaded SSS share: \'sss-key.share.3\'\n'
939
+ 'Loaded SSS share: \'sss-key.share.5\'\n'
940
+ 'Loaded SSS share: \'sss-key.share.1\' '
941
+ '# using only 3 shares: number 2/4 are missing\n'
942
+ 'Secret:\n999'))
943
+
944
+ # Verify a share against a secret
945
+ p_sss_verify: argparse.ArgumentParser = sss_sub.add_parser(
946
+ 'verify',
947
+ help='Verify shares against a secret (private params).',
948
+ epilog=('-p sss-key sss verify 999\n'
949
+ 'SSS share \'sss-key.share.3\' verification: OK\n'
950
+ 'SSS share \'sss-key.share.5\' verification: OK\n'
951
+ 'SSS share \'sss-key.share.1\' verification: OK $$ '
952
+ '-p sss-key sss verify 998\n'
953
+ 'SSS share \'sss-key.share.3\' verification: INVALID\n'
954
+ 'SSS share \'sss-key.share.5\' verification: INVALID\n'
955
+ 'SSS share \'sss-key.share.1\' verification: INVALID'))
956
+ p_sss_verify.add_argument(
957
+ 'secret', type=str, help='Integer secret used to generate the shares, 1≤`secret`<*modulus*')
958
+
959
+ # ========================= Markdown Generation ==================================================
960
+
961
+ # Documentation generation
962
+ doc: argparse.ArgumentParser = sub.add_parser(
963
+ 'doc', help='Documentation utilities. (Not for regular use: these are developer utils.)')
964
+ doc_sub = doc.add_subparsers(dest='doc_command')
965
+ doc_sub.add_parser(
966
+ 'md',
967
+ help='Emit Markdown docs for the CLI (see README.md section "Creating a New Version").',
968
+ epilog=('doc md > CLI.md\n'
969
+ '$ ./tools/inject_md_includes.py\n'
970
+ 'inject: README.md updated with included content'))
971
+
972
+ return parser
973
+
974
+
975
+ def AESCommand(
976
+ args: argparse.Namespace, in_format: _StrBytesType, out_format: _StrBytesType, /) -> None:
977
+ """Execute `aes` command."""
978
+ pt: bytes
979
+ ct: bytes
980
+ aes_cmd: str = args.aes_command.lower().strip() if args.aes_command else ''
981
+ match aes_cmd:
982
+ case 'key':
983
+ aes_key: aes.AESKey = aes.AESKey.FromStaticPassword(args.password)
984
+ if args.key_path:
985
+ _SaveObj(aes_key, args.key_path, args.protect or None)
986
+ print(f'AES key saved to {args.key_path!r}')
909
987
  else:
910
- raise CryptoError(f'unrecoverable secret: {mess}')
911
- # do the math
912
- return ModLagrangeInterpolate(0, share_points, self.modulus)
913
-
914
- @classmethod
915
- def Copy(cls, other: 'ShamirSharedSecretPublic', /) -> Self:
916
- """Initialize a public key by taking the public parts of a public/private key."""
917
- return cls(minimum=other.minimum, modulus=other.modulus)
918
-
919
-
920
- @dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
921
- class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
922
- """Shamir Shared Secret (SSS) private keys (<https://en.wikipedia.org/wiki/Shamir's_secret_sharing>).
923
-
924
- Attributes:
925
- polynomial (list[int]): prime coefficients for generation poly., each modulus.bit_length() size
926
- """
927
-
928
- polynomial: list[int]
929
-
930
- def __post_init__(self) -> None:
931
- """Check data.
932
-
933
- Raises:
934
- InputError: invalid inputs
935
- """
936
- super(ShamirSharedSecretPrivate, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
937
- if (len(self.polynomial) != self.minimum - 1 or # exactly this size
938
- len(set(self.polynomial)) != self.minimum - 1 or # no duplicate
939
- self.modulus in self.polynomial or # different from modulus
940
- any(not IsPrime(p) or p.bit_length() != self.modulus.bit_length()
941
- for p in self.polynomial)): # all primes and the right size
942
- raise InputError(f'invalid polynomial: {self}')
943
-
944
- def Share(self, secret: int, /, *, share_key: int = 0) -> 'ShamirSharePrivate':
945
- """Make a new ShamirSharePrivate for the `secret`.
946
-
947
- Args:
948
- secret (int): secret message to encrypt and share, 0 ≤ s < modulus
949
- share_key (int, optional): if given, a random value to use, 1 ≤ r < modulus;
950
- else will generate randomly
951
-
952
- Returns:
953
- ShamirSharePrivate object
954
-
955
- Raises:
956
- InputError: invalid inputs
957
- """
958
- # test inputs
959
- if not 0 <= secret < self.modulus:
960
- raise InputError(f'invalid secret: {secret=}')
961
- if not 1 < share_key < self.modulus:
962
- if not share_key: # default is zero, and that means we generate it here
963
- sr = secrets.SystemRandom()
964
- share_key = 0
965
- while not share_key or share_key in self.polynomial:
966
- share_key = sr.randint(2, self.modulus - 1)
988
+ print(_BytesToText(aes_key.key256, out_format))
989
+ case 'encrypt':
990
+ if args.key:
991
+ aes_key = aes.AESKey(key256=_BytesFromText(args.key, in_format))
992
+ elif args.key_path:
993
+ aes_key = _LoadObj(args.key_path, args.protect or None, aes.AESKey)
967
994
  else:
968
- raise InputError(f'invalid share_key: {secret=}')
969
- # build object
970
- return ShamirSharePrivate(
971
- minimum=self.minimum, modulus=self.modulus,
972
- share_key=share_key,
973
- share_value=ModPolynomial(share_key, [secret] + self.polynomial, self.modulus))
974
-
975
- def Shares(
976
- self, secret: int, /, *, max_shares: int = 0) -> Generator['ShamirSharePrivate', None, None]:
977
- """Make any number of ShamirSharePrivate for the `secret`.
978
-
979
- Args:
980
- secret (int): secret message to encrypt and share, 0 s < modulus
981
- max_shares (int, optional): if given, number (≥ 2) of shares to generate; else infinite
982
-
983
- Yields:
984
- ShamirSharePrivate object
985
-
986
- Raises:
987
- InputError: invalid inputs
988
- """
989
- # test inputs
990
- if max_shares and max_shares < self.minimum:
991
- raise InputError(f'invalid max_shares: {max_shares=} < {self.minimum=}')
992
- # generate shares
993
- sr = secrets.SystemRandom()
994
- count: int = 0
995
- used_keys: set[int] = set()
996
- while not max_shares or count < max_shares:
997
- share_key: int = 0
998
- while not share_key or share_key in self.polynomial or share_key in used_keys:
999
- share_key = sr.randint(2, self.modulus - 1)
1000
- try:
1001
- yield self.Share(secret, share_key=share_key)
1002
- used_keys.add(share_key)
1003
- count += 1
1004
- except InputError as err:
1005
- # it could happen, for example, that the share_key will generate a value of 0
1006
- logging.warning(err)
1007
-
1008
- def VerifyShare(self, secret: int, share: 'ShamirSharePrivate', /) -> bool:
1009
- """Make a new ShamirSharePrivate for the `secret`.
1010
-
1011
- Args:
1012
- secret (int): secret message to encrypt and share, 0 ≤ s < modulus
1013
- share (ShamirSharePrivate): share to verify
1014
-
1015
- Returns:
1016
- True if share is valid; False otherwise
1017
-
1018
- Raises:
1019
- InputError: invalid inputs
1020
- """
1021
- return share == self.Share(secret, share_key=share.share_key)
1022
-
1023
- @classmethod
1024
- def New(cls, minimum_shares: int, bit_length: int, /) -> Self:
1025
- """Make a new public sharing prime modulus of `bit_length` bits.
1026
-
1027
- Args:
1028
- minimum_shares (int): minimum shares needed for recovery, ≥ 2
1029
- bit_length (int): number of bits in the primes, ≥ 10
1030
-
1031
- Returns:
1032
- ShamirSharedSecretPrivate object ready for use
1033
-
1034
- Raises:
1035
- InputError: invalid inputs
1036
- """
1037
- # test inputs
1038
- if minimum_shares < 2:
1039
- raise InputError(f'at least 2 shares are needed: {minimum_shares=}')
1040
- if bit_length < 10:
1041
- raise InputError(f'invalid bit length: {bit_length=}')
1042
- # make the primes
1043
- unique_primes: set[int] = set()
1044
- while len(unique_primes) < minimum_shares:
1045
- unique_primes.add(NBitRandomPrime(bit_length))
1046
- # get the largest prime for the modulus
1047
- ordered_primes: list[int] = list(unique_primes)
1048
- modulus: int = max(ordered_primes)
1049
- ordered_primes.remove(modulus)
1050
- # make polynomial be a random order
1051
- secrets.SystemRandom().shuffle(ordered_primes)
1052
- # build object
1053
- return cls(minimum=minimum_shares, modulus=modulus, polynomial=ordered_primes)
1054
-
1055
-
1056
- @dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
1057
- class ShamirSharePrivate(ShamirSharedSecretPublic):
1058
- """Shamir Shared Secret (SSS) one share (<https://en.wikipedia.org/wiki/Shamir's_secret_sharing>).
1059
-
1060
- Attributes:
1061
- share_key (int): share secret key; a randomly picked value, 1 ≤ k < modulus
1062
- share_value (int): share secret value, 1 v < modulus; (k, v) is a "point" of f(k)=v
1063
- """
1064
-
1065
- share_key: int
1066
- share_value: int
1067
-
1068
- def __post_init__(self) -> None:
1069
- """Check data.
1070
-
1071
- Raises:
1072
- InputError: invalid inputs
1073
- """
1074
- super(ShamirSharePrivate, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
1075
- if (not 0 < self.share_key < self.modulus or
1076
- not 0 < self.share_value < self.modulus):
1077
- raise InputError(f'invalid share: {self}')
995
+ raise base.InputError('provide -k/--key or -p/--key-path')
996
+ aad: bytes | None = _BytesFromText(args.aad, in_format) if args.aad else None
997
+ pt = _BytesFromText(args.plaintext, in_format)
998
+ ct = aes_key.Encrypt(pt, associated_data=aad)
999
+ print(_BytesToText(ct, out_format))
1000
+ case 'decrypt':
1001
+ if args.key:
1002
+ aes_key = aes.AESKey(key256=_BytesFromText(args.key, in_format))
1003
+ elif args.key_path:
1004
+ aes_key = _LoadObj(args.key_path, args.protect or None, aes.AESKey)
1005
+ else:
1006
+ raise base.InputError('provide -k/--key or -p/--key-path')
1007
+ aad = _BytesFromText(args.aad, in_format) if args.aad else None
1008
+ ct = _BytesFromText(args.ciphertext, in_format)
1009
+ pt = aes_key.Decrypt(ct, associated_data=aad)
1010
+ print(_BytesToText(pt, out_format))
1011
+ case 'ecb':
1012
+ ecb_cmd: str = args.aes_ecb_command.lower().strip() if args.aes_ecb_command else ''
1013
+ if args.key:
1014
+ aes_key = aes.AESKey(key256=_BytesFromText(args.key, in_format))
1015
+ elif args.key_path:
1016
+ aes_key = _LoadObj(args.key_path, args.protect or None, aes.AESKey)
1017
+ else:
1018
+ raise base.InputError('provide -k/--key or -p/--key-path')
1019
+ match ecb_cmd:
1020
+ case 'encrypt':
1021
+ ecb: aes.AESKey.ECBEncoderClass = aes_key.ECBEncoder()
1022
+ print(ecb.EncryptHex(args.plaintext))
1023
+ case 'decrypt':
1024
+ ecb = aes_key.ECBEncoder()
1025
+ print(ecb.DecryptHex(args.ciphertext))
1026
+ case _:
1027
+ raise NotImplementedError()
1028
+ case _:
1029
+ raise NotImplementedError()
1030
+
1031
+
1032
+ def RSACommand(args: argparse.Namespace, /) -> None:
1033
+ """Execute `rsa` command."""
1034
+ c: int
1035
+ m: int
1036
+ rsa_cmd: str = args.rsa_command.lower().strip() if args.rsa_command else ''
1037
+ match rsa_cmd:
1038
+ case 'new':
1039
+ rsa_priv: rsa.RSAPrivateKey = rsa.RSAPrivateKey.New(args.bits)
1040
+ rsa_pub: rsa.RSAPublicKey = rsa.RSAPublicKey.Copy(rsa_priv)
1041
+ _SaveObj(rsa_priv, args.key_path + '.priv', args.protect or None)
1042
+ _SaveObj(rsa_pub, args.key_path + '.pub', args.protect or None)
1043
+ print(f'RSA private/public keys saved to {args.key_path + ".priv/.pub"!r}')
1044
+ case 'encrypt':
1045
+ rsa_pub = rsa.RSAPublicKey.Copy(
1046
+ _LoadObj(args.key_path, args.protect or None, rsa.RSAPublicKey))
1047
+ m = _ParseInt(args.message)
1048
+ print(rsa_pub.Encrypt(m))
1049
+ case 'decrypt':
1050
+ rsa_priv = _LoadObj(args.key_path, args.protect or None, rsa.RSAPrivateKey)
1051
+ c = _ParseInt(args.ciphertext)
1052
+ print(rsa_priv.Decrypt(c))
1053
+ case 'sign':
1054
+ rsa_priv = _LoadObj(args.key_path, args.protect or None, rsa.RSAPrivateKey)
1055
+ m = _ParseInt(args.message)
1056
+ print(rsa_priv.Sign(m))
1057
+ case 'verify':
1058
+ rsa_pub = rsa.RSAPublicKey.Copy(
1059
+ _LoadObj(args.key_path, args.protect or None, rsa.RSAPublicKey))
1060
+ m = _ParseInt(args.message)
1061
+ sig: int = _ParseInt(args.signature)
1062
+ print('RSA signature: ' + ('OK' if rsa_pub.VerifySignature(m, sig) else 'INVALID'))
1063
+ case _:
1064
+ raise NotImplementedError()
1065
+
1066
+
1067
+ def ElGamalCommand(args: argparse.Namespace, /) -> None:
1068
+ """Execute `elgamal` command."""
1069
+ c1: str
1070
+ c2: str
1071
+ m: int
1072
+ ss: tuple[int, int]
1073
+ eg_cmd: str = args.eg_command.lower().strip() if args.eg_command else ''
1074
+ match eg_cmd:
1075
+ case 'shared':
1076
+ shared_eg: elgamal.ElGamalSharedPublicKey = elgamal.ElGamalSharedPublicKey.NewShared(
1077
+ args.bits)
1078
+ _SaveObj(shared_eg, args.key_path + '.shared', args.protect or None)
1079
+ print(f'El-Gamal shared key saved to {args.key_path + ".shared"!r}')
1080
+ case 'new':
1081
+ eg_priv: elgamal.ElGamalPrivateKey = elgamal.ElGamalPrivateKey.New(
1082
+ _LoadObj(args.key_path + '.shared', args.protect or None, elgamal.ElGamalSharedPublicKey))
1083
+ eg_pub: elgamal.ElGamalPublicKey = elgamal.ElGamalPublicKey.Copy(eg_priv)
1084
+ _SaveObj(eg_priv, args.key_path + '.priv', args.protect or None)
1085
+ _SaveObj(eg_pub, args.key_path + '.pub', args.protect or None)
1086
+ print(f'El-Gamal private/public keys saved to {args.key_path + ".priv/.pub"!r}')
1087
+ case 'encrypt':
1088
+ eg_pub = elgamal.ElGamalPublicKey.Copy(
1089
+ _LoadObj(args.key_path, args.protect or None, elgamal.ElGamalPublicKey))
1090
+ m = _ParseInt(args.message)
1091
+ ss = eg_pub.Encrypt(m)
1092
+ print(f'{ss[0]}:{ss[1]}')
1093
+ case 'decrypt':
1094
+ eg_priv = _LoadObj(args.key_path, args.protect or None, elgamal.ElGamalPrivateKey)
1095
+ c1, c2 = args.ciphertext.split(':')
1096
+ ss = (_ParseInt(c1), _ParseInt(c2))
1097
+ print(eg_priv.Decrypt(ss))
1098
+ case 'sign':
1099
+ eg_priv = _LoadObj(args.key_path, args.protect or None, elgamal.ElGamalPrivateKey)
1100
+ m = _ParseInt(args.message)
1101
+ ss = eg_priv.Sign(m)
1102
+ print(f'{ss[0]}:{ss[1]}')
1103
+ case 'verify':
1104
+ eg_pub = elgamal.ElGamalPublicKey.Copy(
1105
+ _LoadObj(args.key_path, args.protect or None, elgamal.ElGamalPublicKey))
1106
+ m = _ParseInt(args.message)
1107
+ c1, c2 = args.signature.split(':')
1108
+ ss = (_ParseInt(c1), _ParseInt(c2))
1109
+ print('El-Gamal signature: ' + ('OK' if eg_pub.VerifySignature(m, ss) else 'INVALID'))
1110
+ case _:
1111
+ raise NotImplementedError()
1112
+
1113
+
1114
+ def DSACommand(args: argparse.Namespace, /) -> None:
1115
+ """Execute `dsa` command."""
1116
+ c1: str
1117
+ c2: str
1118
+ m: int
1119
+ ss: tuple[int, int]
1120
+ dsa_cmd: str = args.dsa_command.lower().strip() if args.dsa_command else ''
1121
+ match dsa_cmd:
1122
+ case 'shared':
1123
+ dsa_shared: dsa.DSASharedPublicKey = dsa.DSASharedPublicKey.NewShared(
1124
+ args.p_bits, args.q_bits)
1125
+ _SaveObj(dsa_shared, args.key_path + '.shared', args.protect or None)
1126
+ print(f'DSA shared key saved to {args.key_path + ".shared"!r}')
1127
+ case 'new':
1128
+ dsa_priv: dsa.DSAPrivateKey = dsa.DSAPrivateKey.New(
1129
+ _LoadObj(args.key_path + '.shared', args.protect or None, dsa.DSASharedPublicKey))
1130
+ dsa_pub: dsa.DSAPublicKey = dsa.DSAPublicKey.Copy(dsa_priv)
1131
+ _SaveObj(dsa_priv, args.key_path + '.priv', args.protect or None)
1132
+ _SaveObj(dsa_pub, args.key_path + '.pub', args.protect or None)
1133
+ print(f'DSA private/public keys saved to {args.key_path + ".priv/.pub"!r}')
1134
+ case 'sign':
1135
+ dsa_priv = _LoadObj(args.key_path, args.protect or None, dsa.DSAPrivateKey)
1136
+ m = _ParseInt(args.message) % dsa_priv.prime_seed
1137
+ ss = dsa_priv.Sign(m)
1138
+ print(f'{ss[0]}:{ss[1]}')
1139
+ case 'verify':
1140
+ dsa_pub = dsa.DSAPublicKey.Copy(
1141
+ _LoadObj(args.key_path, args.protect or None, dsa.DSAPublicKey))
1142
+ m = _ParseInt(args.message) % dsa_pub.prime_seed
1143
+ c1, c2 = args.signature.split(':')
1144
+ ss = (_ParseInt(c1), _ParseInt(c2))
1145
+ print('DSA signature: ' + ('OK' if dsa_pub.VerifySignature(m, ss) else 'INVALID'))
1146
+ case _:
1147
+ raise NotImplementedError()
1148
+
1149
+
1150
+ def BidCommand(
1151
+ args: argparse.Namespace, in_format: _StrBytesType, out_format: _StrBytesType, /) -> None:
1152
+ """Execute `bid` command."""
1153
+ bid_cmd: str = args.bid_command.lower().strip() if args.bid_command else ''
1154
+ match bid_cmd:
1155
+ case 'new':
1156
+ secret: bytes = _BytesFromText(args.secret, in_format)
1157
+ bid_priv: base.PrivateBid = base.PrivateBid.New(secret)
1158
+ bid_pub: base.PublicBid = base.PublicBid.Copy(bid_priv)
1159
+ _SaveObj(bid_priv, args.key_path + '.priv', args.protect or None)
1160
+ _SaveObj(bid_pub, args.key_path + '.pub', args.protect or None)
1161
+ print(f'Bid private/public commitments saved to {args.key_path + ".priv/.pub"!r}')
1162
+ case 'verify':
1163
+ bid_priv = _LoadObj(args.key_path + '.priv', args.protect or None, base.PrivateBid)
1164
+ bid_pub = _LoadObj(args.key_path + '.pub', args.protect or None, base.PublicBid)
1165
+ bid_pub_expect: base.PublicBid = base.PublicBid.Copy(bid_priv)
1166
+ print('Bid commitment: ' + (
1167
+ 'OK' if (bid_pub.VerifyBid(bid_priv.private_key, bid_priv.secret_bid) and
1168
+ bid_pub == bid_pub_expect) else 'INVALID'))
1169
+ print('Bid secret:')
1170
+ print(_BytesToText(bid_priv.secret_bid, out_format))
1171
+ case _:
1172
+ raise NotImplementedError()
1173
+
1174
+
1175
+ def SSSCommand(args: argparse.Namespace, /) -> None:
1176
+ """Execute `sss` command."""
1177
+ sss_cmd: str = args.sss_command.lower().strip() if args.sss_command else ''
1178
+ match sss_cmd:
1179
+ case 'new':
1180
+ sss_priv: sss.ShamirSharedSecretPrivate = sss.ShamirSharedSecretPrivate.New(
1181
+ args.minimum, args.bits)
1182
+ sss_pub: sss.ShamirSharedSecretPublic = sss.ShamirSharedSecretPublic.Copy(sss_priv)
1183
+ _SaveObj(sss_priv, args.key_path + '.priv', args.protect or None)
1184
+ _SaveObj(sss_pub, args.key_path + '.pub', args.protect or None)
1185
+ print(f'SSS private/public keys saved to {args.key_path + ".priv/.pub"!r}')
1186
+ case 'shares':
1187
+ sss_priv = _LoadObj(
1188
+ args.key_path + '.priv', args.protect or None, sss.ShamirSharedSecretPrivate)
1189
+ secret: int = _ParseInt(args.secret)
1190
+ sss_share: sss.ShamirSharePrivate
1191
+ for i, sss_share in enumerate(sss_priv.Shares(secret, max_shares=args.count)):
1192
+ _SaveObj(sss_share, f'{args.key_path}.share.{i + 1}', args.protect or None)
1193
+ print(f'SSS {args.count} individual (private) shares saved to '
1194
+ f'{args.key_path + ".share.1…" + str(args.count)!r}')
1195
+ case 'recover':
1196
+ sss_pub = _LoadObj(args.key_path + '.pub', args.protect or None, sss.ShamirSharedSecretPublic)
1197
+ subset: list[sss.ShamirSharePrivate] = []
1198
+ for fname in glob.glob(args.key_path + '.share.*'):
1199
+ sss_share = _LoadObj(fname, args.protect or None, sss.ShamirSharePrivate)
1200
+ subset.append(sss_share)
1201
+ print(f'Loaded SSS share: {fname!r}')
1202
+ print('Secret:')
1203
+ print(sss_pub.RecoverSecret(subset))
1204
+ case 'verify':
1205
+ sss_priv = _LoadObj(
1206
+ args.key_path + '.priv', args.protect or None, sss.ShamirSharedSecretPrivate)
1207
+ secret = _ParseInt(args.secret)
1208
+ for fname in glob.glob(args.key_path + '.share.*'):
1209
+ sss_share = _LoadObj(fname, args.protect or None, sss.ShamirSharePrivate)
1210
+ print(f'SSS share {fname!r} verification: '
1211
+ f'{"OK" if sss_priv.VerifyShare(secret, sss_share) else "INVALID"}')
1212
+ case _:
1213
+ raise NotImplementedError()
1214
+
1215
+
1216
+ def main(argv: list[str] | None = None, /) -> int: # pylint: disable=invalid-name,too-many-locals,too-many-branches,too-many-statements
1217
+ """Main entry point."""
1218
+ # build the parser and parse args
1219
+ parser: argparse.ArgumentParser = _BuildParser()
1220
+ args: argparse.Namespace = parser.parse_args(argv)
1221
+ # take care of global options
1222
+ levels: list[int] = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
1223
+ logging.basicConfig(
1224
+ level=levels[min(args.verbose, len(levels) - 1)], # type: ignore
1225
+ format=getattr(base, 'LOG_FORMAT', '%(levelname)s:%(message)s'))
1226
+ logging.captureWarnings(True)
1227
+ in_format: _StrBytesType = _StrBytesType.FromFlags(args.hex, args.b64, args.bin)
1228
+ out_format: _StrBytesType = _StrBytesType.FromFlags(args.out_hex, args.out_b64, args.out_bin)
1229
+
1230
+ a: int
1231
+ b: int
1232
+ e: int
1233
+ i: int
1234
+ m: int
1235
+ n: int
1236
+ x: int
1237
+ y: int
1238
+ bt: bytes
1239
+
1240
+ try:
1241
+ # get the command, do basic checks and switch
1242
+ command: str = args.command.lower().strip() if args.command else ''
1243
+ if command in ('rsa', 'elgamal', 'dsa', 'bid', 'sss') and not args.key_path:
1244
+ raise base.InputError(f'you must provide -p/--key-path option for {command!r}')
1245
+ match command:
1246
+ # -------- primes ----------
1247
+ case 'isprime':
1248
+ n = _ParseInt(args.n)
1249
+ print(modmath.IsPrime(n))
1250
+ case 'primegen':
1251
+ start: int = _ParseInt(args.start)
1252
+ count: int = args.count
1253
+ i = 0
1254
+ for p in modmath.PrimeGenerator(start):
1255
+ print(p)
1256
+ i += 1
1257
+ if count and i >= count:
1258
+ break
1259
+ case 'mersenne':
1260
+ for k, m_p, perfect in modmath.MersennePrimesGenerator(args.min_k):
1261
+ print(f'k={k} M={m_p} perfect={perfect}')
1262
+ if k > args.cutoff_k:
1263
+ break
1264
+
1265
+ # -------- integer / modular ----------
1266
+ case 'gcd':
1267
+ a, b = _ParseInt(args.a), _ParseInt(args.b)
1268
+ print(base.GCD(a, b))
1269
+ case 'xgcd':
1270
+ a, b = _ParseInt(args.a), _ParseInt(args.b)
1271
+ print(base.ExtendedGCD(a, b))
1272
+ case 'mod':
1273
+ mod_command: str = args.mod_command.lower().strip() if args.mod_command else ''
1274
+ match mod_command:
1275
+ case 'inv':
1276
+ a, m = _ParseInt(args.a), _ParseInt(args.m)
1277
+ try:
1278
+ print(modmath.ModInv(a, m))
1279
+ except modmath.ModularDivideError:
1280
+ print('<<INVALID>> no modular inverse exists (ModularDivideError)')
1281
+ case 'div':
1282
+ x, y, m = _ParseInt(args.x), _ParseInt(args.y), _ParseInt(args.m)
1283
+ try:
1284
+ print(modmath.ModDiv(x, y, m))
1285
+ except modmath.ModularDivideError:
1286
+ print('<<INVALID>> no modular inverse exists (ModularDivideError)')
1287
+ case 'exp':
1288
+ a, e, m = _ParseInt(args.a), _ParseInt(args.e), _ParseInt(args.m)
1289
+ print(modmath.ModExp(a, e, m))
1290
+ case 'poly':
1291
+ x, m = _ParseInt(args.x), _ParseInt(args.m)
1292
+ coeffs: list[int] = _ParseIntList(args.coeff)
1293
+ print(modmath.ModPolynomial(x, coeffs, m))
1294
+ case 'lagrange':
1295
+ x, m = _ParseInt(args.x), _ParseInt(args.m)
1296
+ pts: dict[int, int] = {}
1297
+ k_s: str
1298
+ v_s: str
1299
+ for kv in args.pt:
1300
+ k_s, v_s = kv.split(':', 1)
1301
+ pts[_ParseInt(k_s)] = _ParseInt(v_s)
1302
+ print(modmath.ModLagrangeInterpolate(x, pts, m))
1303
+ case 'crt':
1304
+ crt_tuple: tuple[int, int, int, int] = (
1305
+ _ParseInt(args.a1), _ParseInt(args.m1), _ParseInt(args.a2), _ParseInt(args.m2))
1306
+ try:
1307
+ print(modmath.CRTPair(*crt_tuple))
1308
+ except modmath.ModularDivideError:
1309
+ print('<<INVALID>> moduli m1/m2 not co-prime (ModularDivideError)')
1310
+ case _:
1311
+ raise NotImplementedError()
1312
+
1313
+ # -------- randomness / hashing ----------
1314
+ case 'random':
1315
+ rand_cmd: str = args.rand_command.lower().strip() if args.rand_command else ''
1316
+ match rand_cmd:
1317
+ case 'bits':
1318
+ print(base.RandBits(args.bits))
1319
+ case 'int':
1320
+ print(base.RandInt(_ParseInt(args.min), _ParseInt(args.max)))
1321
+ case 'bytes':
1322
+ print(base.BytesToHex(base.RandBytes(args.n)))
1323
+ case 'prime':
1324
+ print(modmath.NBitRandomPrime(args.bits))
1325
+ case _:
1326
+ raise NotImplementedError()
1327
+ case 'hash':
1328
+ hash_cmd: str = args.hash_command.lower().strip() if args.hash_command else ''
1329
+ match hash_cmd:
1330
+ case 'sha256':
1331
+ bt = _BytesFromText(args.data, in_format)
1332
+ digest: bytes = base.Hash256(bt)
1333
+ print(_BytesToText(digest, out_format))
1334
+ case 'sha512':
1335
+ bt = _BytesFromText(args.data, in_format)
1336
+ digest = base.Hash512(bt)
1337
+ print(_BytesToText(digest, out_format))
1338
+ case 'file':
1339
+ digest = base.FileHash(args.path, digest=args.digest)
1340
+ print(_BytesToText(digest, out_format))
1341
+ case _:
1342
+ raise NotImplementedError()
1343
+
1344
+ # -------- AES / RSA / El-Gamal / DSA / SSS ----------
1345
+ case 'aes':
1346
+ AESCommand(args, in_format, out_format)
1347
+
1348
+ case 'rsa':
1349
+ RSACommand(args)
1350
+
1351
+ case 'elgamal':
1352
+ ElGamalCommand(args)
1353
+
1354
+ case 'dsa':
1355
+ DSACommand(args)
1356
+
1357
+ case 'bid':
1358
+ BidCommand(args, in_format, out_format)
1359
+
1360
+ case 'sss':
1361
+ SSSCommand(args)
1362
+
1363
+ # -------- Documentation ----------
1364
+ case 'doc':
1365
+ doc_command: str = (
1366
+ args.doc_command.lower().strip() if getattr(args, 'doc_command', '') else '')
1367
+ match doc_command:
1368
+ case 'md':
1369
+ print(_GenerateCLIMarkdown())
1370
+ case _:
1371
+ raise NotImplementedError()
1372
+
1373
+ case _:
1374
+ parser.print_help()
1375
+
1376
+ except NotImplementedError as err:
1377
+ print(f'Invalid command: {err}')
1378
+ except (base.Error, ValueError) as err:
1379
+ print(str(err))
1380
+
1381
+ return 0
1382
+
1383
+
1384
+ if __name__ == '__main__':
1385
+ sys.exit(main())