transcrypto 1.5.1__py3-none-any.whl → 1.7.0__py3-none-any.whl

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