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.
- transcrypto/__init__.py +7 -0
- transcrypto/aes.py +150 -44
- transcrypto/base.py +640 -411
- transcrypto/constants.py +20070 -1906
- transcrypto/dsa.py +132 -99
- transcrypto/elgamal.py +116 -84
- transcrypto/modmath.py +88 -78
- transcrypto/profiler.py +228 -180
- transcrypto/rsa.py +126 -90
- transcrypto/sss.py +122 -70
- transcrypto/transcrypto.py +2362 -1412
- {transcrypto-1.5.1.dist-info → transcrypto-1.7.0.dist-info}/METADATA +78 -58
- transcrypto-1.7.0.dist-info/RECORD +17 -0
- {transcrypto-1.5.1.dist-info → transcrypto-1.7.0.dist-info}/WHEEL +1 -2
- transcrypto-1.7.0.dist-info/entry_points.txt +4 -0
- transcrypto/safetrans.py +0 -1231
- transcrypto-1.5.1.dist-info/RECORD +0 -18
- transcrypto-1.5.1.dist-info/top_level.txt +0 -1
- {transcrypto-1.5.1.dist-info → transcrypto-1.7.0.dist-info}/licenses/LICENSE +0 -0
transcrypto/transcrypto.py
CHANGED
|
@@ -1,1457 +1,2407 @@
|
|
|
1
|
-
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
random
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
93
|
+
import dataclasses
|
|
27
94
|
import enum
|
|
28
95
|
import glob
|
|
29
96
|
import logging
|
|
30
|
-
|
|
31
|
-
import
|
|
32
|
-
from typing import Any
|
|
97
|
+
import pathlib
|
|
98
|
+
import re
|
|
99
|
+
from typing import Any
|
|
33
100
|
|
|
34
|
-
|
|
101
|
+
import click
|
|
102
|
+
import typer
|
|
35
103
|
|
|
36
|
-
|
|
37
|
-
__version__
|
|
38
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
207
|
+
case IOFormat.hex:
|
|
86
208
|
return base.HexToBytes(text)
|
|
87
|
-
case
|
|
209
|
+
case IOFormat.b64:
|
|
88
210
|
return base.EncodedToBytes(text)
|
|
89
211
|
|
|
90
212
|
|
|
91
|
-
def _BytesToText(b: bytes,
|
|
92
|
-
"""
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
227
|
+
case IOFormat.hex:
|
|
97
228
|
return base.BytesToHex(b)
|
|
98
|
-
case
|
|
229
|
+
case IOFormat.b64:
|
|
99
230
|
return base.BytesToEncoded(b)
|
|
100
231
|
|
|
101
232
|
|
|
102
|
-
def
|
|
103
|
-
"""
|
|
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
|
-
|
|
108
|
-
|
|
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, /) ->
|
|
115
|
-
"""Load object.
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
266
|
+
f'Object loaded from {path} is of invalid type {type(obj)}, expected {expect}'
|
|
267
|
+
)
|
|
121
268
|
return obj
|
|
122
269
|
|
|
123
270
|
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
#
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
#
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
#
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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'))
|