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