transcrypto 1.6.0__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/safetrans.py DELETED
@@ -1,1228 +0,0 @@
1
- #!/usr/bin/env python3
2
- #
3
- # Copyright 2025 Daniel Balparda (balparda@github.com) - Apache-2.0 license
4
- #
5
- """Balparda's SafeTrans(Crypto) command line interface: the safe version of TransCrypto.
6
-
7
- See README.md for documentation on how to use.
8
-
9
- Notes on the layout (quick mental model):
10
-
11
- isprime
12
- random bits|int|bytes|prime
13
- hash sha256|sha512|file
14
- aes new|frompass|encrypt|decrypt
15
- rsa new|encrypt|decrypt|sign|verify
16
- dsa shared|new|sign|verify
17
- bid new|verify
18
- sss new|shares|recover
19
- doc md
20
-
21
-
22
- Great question — crypto CLIs juggle a messy mix of bytes, strings, and files. The trick is to make sources and sinks explicit and consistent, and to give users a small set of composable rules that work the same across all subcommands.
23
-
24
- Below is a battle-tested pattern (inspired by OpenSSL, age, gpg, minisign, libsodium tools) plus a compact argparse skeleton you can drop in.
25
-
26
-
27
-
28
- Design principles
29
- 1. Uniform “data specifiers” for inputs
30
- Any argument that represents bytes should accept the same mini-grammar:
31
- • @path → read bytes from a file (@- means stdin)
32
- • hex:deadbeef → decode hex
33
- • b64:... → decode base64 (URL-safe b64u: optional)
34
- • str:hello → UTF-8 encode the literal
35
- • raw:... → byte literals via \\xNN escapes (rare but handy)
36
- Integers and enums are not data specs; they’re normal flags (--bits 256, --curve ed25519).
37
- 2. Explicit output format & sink
38
- Split format from destination:
39
- • Format (mutually exclusive): --out-raw | --out-hex | --out-b64 | --out-json
40
- • Destination: --out - (stdout, default) or --out path
41
- • Multi-file outputs use --out-prefix /path/to/prefix (e.g., write prefix.key, prefix.pub)
42
- 3. Streaming defaults
43
- • If an operation produces a single blob, default to stdout.
44
- • If stdout is a TTY and format is binary, refuse unless --force or a non-TTY sink is chosen.
45
- 4. Schema for structured results
46
- When outputs are multi-field (e.g., keygen with pub+priv), offer --out-json with stable field names and base64/hex encodings. Pair with --out-prefix for file emission.
47
- 5. Predictable subcommands & option names
48
- Keep verbs clear and consistent:
49
- • rand, hash, kdf, enc, dec, sign, verify, mac, keygen, derive, wrap, unwrap.
50
- Reuse the same flag names everywhere (--key, --aad, --nonce, --msg).
51
- 6. File type inference is a bonus, not a rule
52
- You may infer formats from extensions (.pem, .der, .jwk, .b64, .hex), but never rely on it: users can always override using data specifiers or --in-format/--key-format.
53
- 7. Safety foot-guns removed
54
- • Private key outputs default to files with 0600 perms; refuse TTY unless --force.
55
- • Zeroize sensitive buffers where feasible.
56
- • Don’t echo secrets to logs; support --quiet.
57
- 8. Machine-friendly behavior
58
- • Exit codes: 0 ok, 1 usage/validation error, 2 crypto failure (verification failed), 3 I/O error.
59
- • --json responses are single-line by default; add --pretty for humans.
60
-
61
-
62
-
63
- Mini-grammar (inputs)
64
-
65
- EBNF:
66
-
67
- DataSpec := '@' Path
68
- | 'hex:' HexString
69
- | 'b64:' Base64String
70
- | 'b64u:' Base64UrlString
71
- | 'str:' Utf8Text
72
- | 'raw:' BackslashEscapes
73
- | '-' ; shorthand for '@-'
74
-
75
- Examples:
76
- • --msg @message.bin
77
- • --msg - (stdin)
78
- • --key @id_ed25519 or --key @key.pem
79
- • --aad str:metadata-v1
80
- • --nonce hex:00112233...
81
-
82
- For params that must be integers, be generous but explicit:
83
- • --bits 2048
84
- • Allow suffixes: --bytes 64, --duration 2h, --iters 1M.
85
-
86
-
87
-
88
- Output policy
89
- • Single blob:
90
- • default sink: stdout
91
- • default format: hex for short (<1KiB) unknown blobs? (or pick a project-wide default)
92
- • user can force: --out-b64, --out-raw, --out-hex
93
- • sink override: --out path
94
- • Multi-artifact:
95
- • --out-prefix prefix → prefix.pub, prefix.key, etc.
96
- • OR --out-json to emit a single structured result (fields encoded as hex/b64)
97
- • Optional --armor synonym for --out-b64 on legacy-style commands
98
-
99
-
100
-
101
- Subcommand layout (suggested)
102
- • rand → --bytes N → bytes
103
- • hash → --alg sha256 --msg DataSpec → digest
104
- • mac → --alg hmac-sha256 --key DataSpec --msg DataSpec → tag
105
- • enc/dec → --alg chacha20-poly1305 --key --nonce --aad --in DataSpec
106
- • sign/verify → --alg ed25519 --key/--pub --msg
107
- • keygen → --alg ed25519 [writes files or JSON]
108
- • kdf → --alg hkdf-sha256 --ikm --salt --info --bytes N
109
-
110
- Every subcommand accepts the shared output flags and --quiet/--verbose.
111
-
112
-
113
-
114
- UX examples
115
-
116
- # Random 32 bytes to stdout, base64
117
- tool rand --bytes 32 --out-b64
118
-
119
- # SHA-256 of a file to hex on stdout
120
- tool hash --alg sha256 --msg @file.bin --out-hex
121
-
122
- # Encrypt file, AAD as literal string, nonce as hex; write ciphertext to file
123
- tool enc --alg chacha20-poly1305 \
124
- --key @key.bin --nonce hex:001122... --aad str:invoice-2025-09 \
125
- --in @plain.bin --out cipher.bin --out-raw
126
-
127
- # Sign from stdin, key from file, tag to stdout base64
128
- cat message | tool sign --alg ed25519 --key @sk --msg - --out-b64
129
-
130
- # Verify (exit code 0 on success, 2 on failure)
131
- tool verify --alg ed25519 --pub @pk --msg @msg.bin --sig b64:ABCD...
132
-
133
- # Keypair to files with secure perms
134
- tool keygen --alg ed25519 --out-prefix ~/.keys/alice # writes alice.key 0600, alice.pub 0644
135
-
136
- # Same keypair as JSON (machine-friendly)
137
- tool keygen --alg ed25519 --out-json | jq .
138
-
139
-
140
-
141
-
142
- argparse skeleton (2-space indents, single quotes)
143
-
144
- import argparse, base64, binascii, os, sys, json, stat
145
-
146
- # ---------- Data parsing ----------
147
-
148
- class UsageError(Exception): pass
149
-
150
- def read_dataspec(s: str) -> bytes:
151
- if s == '-' or s == '@-':
152
- return sys.stdin.buffer.read()
153
- if s.startswith('@'):
154
- path = s[1:]
155
- with open(path, 'rb') as f:
156
- return f.read()
157
- if s.startswith('hex:'):
158
- try:
159
- return binascii.unhexlify(s[4:].strip())
160
- except binascii.Error as e:
161
- raise UsageError(f'invalid hex: {e}')
162
- if s.startswith('b64:') or s.startswith('b64u:'):
163
- data = s.split(':', 1)[1]
164
- altchars = b'-_' if s.startswith('b64u:') else None
165
- try:
166
- return base64.b64decode(data, validate=True, altchars=altchars)
167
- except binascii.Error as e:
168
- raise UsageError(f'invalid base64: {e}')
169
- if s.startswith('str:'):
170
- return s[4:].encode('utf-8')
171
- if s.startswith('raw:'):
172
- return bytes(s[4:], 'utf-8').decode('unicode_escape').encode('latin1')
173
- raise UsageError('expected DataSpec like @path, -, hex:, b64:, b64u:, str:, raw:')
174
-
175
- def parse_int(s: str) -> int:
176
- mult = 1
177
- if s.lower().endswith('k'):
178
- mult, s = 1024, s[:-1]
179
- elif s.lower().endswith('m'):
180
- mult, s = 1024*1024, s[:-1]
181
- try:
182
- n = int(s, 0)
183
- except ValueError:
184
- raise UsageError('invalid integer')
185
- return n * mult
186
-
187
- # ---------- Output handling ----------
188
-
189
- def write_single_blob(data: bytes, args):
190
- if args.out == '-' and sys.stdout.isatty() and args.out_raw and not args.force:
191
- raise UsageError('refusing to print binary to TTY (use --force or --out FILE)')
192
- if args.out_hex:
193
- out = binascii.hexlify(data).decode('ascii')
194
- out_bytes = (out + '\n').encode()
195
- elif args.out_b64:
196
- out = base64.b64encode(data).decode('ascii')
197
- out_bytes = (out + '\n').encode()
198
- elif args.out_json:
199
- payload = {'data_b64': base64.b64encode(data).decode('ascii')}
200
- out_bytes = (json.dumps(payload, indent=2 if args.pretty else None) + '\n').encode()
201
- else:
202
- # raw
203
- out_bytes = data
204
-
205
- if args.out == '-' or args.out is None:
206
- sys.stdout.buffer.write(out_bytes)
207
- else:
208
- mode = 0o600 if getattr(args, 'sensitive', False) else 0o644
209
- with open(args.out, 'wb') as f:
210
- f.write(out_bytes)
211
- os.chmod(args.out, mode)
212
-
213
- def write_prefixed(files: dict[str, bytes], prefix: str, sensitive_keys=('key', 'sk', 'priv')):
214
- if not prefix:
215
- raise UsageError('must provide --out-prefix for multi-file outputs')
216
- for suffix, content in files.items():
217
- path = f'{prefix}.{suffix}'
218
- mode = 0o600 if any(s in suffix for s in sensitive_keys) else 0o644
219
- with open(path, 'wb') as f:
220
- f.write(content)
221
- os.chmod(path, mode)
222
- return [f'{prefix}.{s}' for s in files]
223
-
224
- # ---------- Subcommands ----------
225
-
226
- def cmd_rand(args):
227
- n = parse_int(args.bytes)
228
- data = os.urandom(n)
229
- write_single_blob(data, args)
230
-
231
- def cmd_hash(args):
232
- import hashlib
233
- msg = read_dataspec(args.msg)
234
- h = hashlib.new(args.alg)
235
- h.update(msg)
236
- write_single_blob(h.digest(), args)
237
-
238
- def cmd_keygen(args):
239
- # placeholder example: ed25519 using cryptography
240
- from cryptography.hazmat.primitives.asymmetric import ed25519
241
- from cryptography.hazmat.primitives import serialization
242
- sk = ed25519.Ed25519PrivateKey.generate()
243
- pk = sk.public_key()
244
-
245
- sk_bytes = sk.private_bytes(
246
- encoding=serialization.Encoding.Raw,
247
- format=serialization.PrivateFormat.Raw,
248
- encryption_algorithm=serialization.NoEncryption()
249
- )
250
- pk_bytes = pk.public_bytes(
251
- encoding=serialization.Encoding.Raw,
252
- format=serialization.PublicFormat.Raw
253
- )
254
-
255
- if args.out_json:
256
- obj = {
257
- 'alg': 'ed25519',
258
- 'sk_b64': base64.b64encode(sk_bytes).decode('ascii'),
259
- 'pk_b64': base64.b64encode(pk_bytes).decode('ascii'),
260
- }
261
- payload = json.dumps(obj, indent=2 if args.pretty else None) + '\n'
262
- sys.stdout.write(payload)
263
- return
264
-
265
- if args.out_prefix:
266
- write_prefixed({'key': sk_bytes, 'pub': pk_bytes}, args.out_prefix)
267
- else:
268
- # default: write secret to file, pub to stdout hex
269
- args.sensitive = True
270
- if not args.out:
271
- raise UsageError('provide --out (private key) or use --out-prefix/--out-json')
272
- write_single_blob(sk_bytes, args)
273
- # print pub to stderr for visibility without mixing streams
274
- pub_hex = binascii.hexlify(pk_bytes).decode('ascii')
275
- print(f'public key (hex): {pub_hex}', file=sys.stderr)
276
-
277
- # ---------- Parser ----------
278
-
279
- def build_parser():
280
- p = argparse.ArgumentParser(prog='tool', description='Crypto toolbox')
281
- p.add_argument('--quiet', action='store_true')
282
- p.add_argument('--verbose', action='store_true')
283
-
284
- out = argparse.ArgumentParser(add_help=False)
285
- g = out.add_mutually_exclusive_group()
286
- g.add_argument('--out-raw', dest='out_raw', action='store_true')
287
- g.add_argument('--out-hex', dest='out_hex', action='store_true')
288
- g.add_argument('--out-b64', dest='out_b64', action='store_true')
289
- g.add_argument('--out-json', dest='out_json', action='store_true')
290
- out.add_argument('--pretty', action='store_true')
291
- out.add_argument('--out', default='-')
292
- out.add_argument('--out-prefix')
293
- out.add_argument('--force', action='store_true')
294
-
295
- sub = p.add_subparsers(dest='cmd', required=True)
296
-
297
- pr = sub.add_parser('rand', parents=[out], help='random bytes')
298
- pr.add_argument('--bytes', required=True, help='number of bytes (e.g., 32, 1k)')
299
- pr.set_defaults(func=cmd_rand)
300
-
301
- ph = sub.add_parser('hash', parents=[out], help='hash message')
302
- ph.add_argument('--alg', required=True, choices=['sha256','sha512','blake2b'])
303
- ph.add_argument('--msg', required=True, help='DataSpec')
304
- ph.set_defaults(func=cmd_hash)
305
-
306
- pk = sub.add_parser('keygen', parents=[out], help='generate keypair')
307
- pk.add_argument('--alg', required=True, choices=['ed25519'])
308
- pk.set_defaults(func=cmd_keygen)
309
-
310
- return p
311
-
312
- def main(argv=None):
313
- parser = build_parser()
314
- try:
315
- args = parser.parse_args(argv)
316
- # default to raw unless an encoding flag chosen
317
- if not (args.out_hex or args.out_b64 or args.out_json):
318
- args.out_raw = True
319
- args.func(args)
320
- except UsageError as e:
321
- print(f'error: {e}', file=sys.stderr)
322
- sys.exit(1)
323
-
324
- if __name__ == '__main__':
325
- main()
326
-
327
-
328
-
329
-
330
- How other tools approach it (patterns to copy)
331
- • OpenSSL: -in/-out for files; PEM/DER auto-detected by headers; -passin schemes (file:, env:) — good precedent for scheme-style sources.
332
- • GnuPG: --armor (base64) vs raw; refuses to dump secrets to TTY without explicit flags; separate --output vs stdout.
333
- • age / minisign: simple verbs, -i for identity (key file), -a/--armor for base64, defaults to stdout for single outputs.
334
- • libsodium CLI: clear distinction between --bin, --hex, --base64 across commands.
335
-
336
- These all lean on: consistent flags, scheme prefixes for inputs, explicit armor flags, and safe defaults.
337
-
338
-
339
-
340
- Gotchas & tips
341
- • Binary to TTY: protect users by default (refuse unless --force).
342
- • Permissions: set 0600 for secrets; warn if file exists unless --overwrite.
343
- • Determinism: --deterministic where relevant (tests, vectors).
344
- • Stateful formats: PEM/JWK support is valuable; expose --key-format {raw,pem,jwk} and auto-detect with override.
345
- • AAD & friends: treat as DataSpec (so users can pass str: or b64: seamlessly).
346
- • Exit code semantics: especially verify → 2 on bad signature (distinguish from parse errors).
347
- • Docs: put the DataSpec grammar and examples at the top of --help for every subcommand.
348
-
349
-
350
-
351
- If you want, tell me a couple of concrete operations you’ll ship first (e.g., ed25519 sign/verify, chacha20-poly1305 enc/dec, hkdf), and I’ll tailor the subparsers and validators for those right away.
352
-
353
- Awesome list. Here’s a clean, consistent CLI contract for each item—built on the “DataSpec + output/sink” pattern we discussed. I’ll give you: (1) command shapes with flags, inputs/outputs, and exit-code behavior, and (2) an argparse scaffold you can paste in and extend.
354
-
355
- I’m sticking to 2-space indents and single quotes.
356
-
357
-
358
-
359
- Shared conventions (recap)
360
- • DataSpec for any bytes: @path, -, hex:…, b64:…, b64u:…, str:…, raw:…
361
- • Output: --out-{raw,hex,b64,json} (one of), --out -|PATH, --out-prefix PREFIX
362
- • Safety: secrets written with 0600, refuse binary to TTY unless --force
363
- • Exit codes: 0=ok, 1=usage/validation, 2=cryptographic failure (e.g., verify false), 3=I/O
364
-
365
-
366
-
367
- Commands & flags
368
-
369
- 1) isprime
370
- • Purpose: primality test for large integers
371
- • Inputs
372
- • --n INT (required). Accept decimal or 0x…
373
- • --rounds R (Miller–Rabin reps, default 40)
374
- • Output
375
- • Default: true/false\n on stdout
376
- • --out-json → {"n":"…","probable_prime":true,"rounds":40,"confidence":"1-2^-40"}
377
- • Exit codes: 0 always if test ran; optionally --expect {prime,composite} makes exit 0 only if matched, else 2.
378
-
379
- 2) rand
380
-
381
- Generates randomness in various shapes.
382
- • Submodes (mutually exclusive):
383
- • rand bytes --bytes N
384
- • rand int --bits N (uniform in [0, 2^N) as big-endian integer)
385
- • rand bits --bits N (ASCII 0/1 string unless --out-raw)
386
- • rand prime --bits N [--safe] [--public-exp E] (safe → p s.t. (p-1)/2 prime)
387
- • Output: single blob (respect --out-*). For int, if textual, print base-10; if --out-hex, print hex; --out-raw prints big-endian bytes.
388
-
389
- 3) hash
390
- • Inputs:
391
- • --alg {sha256,sha512,blake2b} (extend as needed)
392
- • --msg DataSpec (use @file for files; - for stdin)
393
- • Output: digest (default hex); support --out-{hex,b64,raw,json}
394
- • JSON: {"alg":"sha256","digest_b64":"…"}
395
- • Notes: If users say “file”, they can just pass --msg @path.
396
-
397
- 4) aes
398
-
399
- Symmetric crypto. Keep keys as DataSpec.
400
- • Common flags:
401
- • --mode {gcm,ctr,cbc} (default gcm)
402
- • --key DataSpec or --from-pass str:PASSWORD --kdf {argon2id,pbkdf2} [--salt DataSpec] [--iters N] [--mem-mb N] [--parallelism N]
403
- • --iv|--nonce DataSpec (if omitted, auto-generate & emit alongside ciphertext)
404
- • --aad DataSpec (GCM)
405
- • Subcommands:
406
- • aes new --size {128,192,256} → random key bytes (sensitive out policy)
407
- • aes frompass ... --size {128,192,256} → derived key bytes
408
- • aes encrypt --in DataSpec [common flags]
409
- • Output:
410
- • Default single-part format (GCM): emit a compact JSON unless user picks a raw format:
411
- • JSON: {"mode":"gcm","nonce_b64":"…","aad_b64":"…","ct_b64":"…","tag_b64":"…"}
412
- • Or with --out-raw: raw nonce||ct||tag (define ordering), document it clearly.
413
- • aes decrypt --in DataSpec [common flags]
414
- • Accept either JSON bundle or raw concatenation (auto-detect unless --in-format {json,raw}).
415
- • Exit codes: 2 on auth/tag failure.
416
-
417
- 5) rsa
418
- • Subcommands:
419
- • rsa new --bits N [--e 65537]
420
- • Output:
421
- • --out-prefix PREFIX → PREFIX.key (PKCS#1 or PKCS#8 raw/DER/PEM per --format), PREFIX.pub
422
- • or --out-json → {"alg":"rsa","n_b64":"…","e":65537,"d_b64":"…","pkcs8_pem":"…"}
423
- • Enforce 0600 on private.
424
- • rsa encrypt --pub DataSpec --in DataSpec [--pad {oaep,pkcs1v15}] [--hash sha256]
425
- • rsa decrypt --key DataSpec [same padding flags] --in DataSpec
426
- • rsa sign --key DataSpec --msg DataSpec [--pad pss|pkcs1v15] [--hash sha256]
427
- • rsa verify --pub DataSpec --msg DataSpec --sig DataSpec [same flags]
428
- • Outputs:
429
- • encrypt → ciphertext (blob)
430
- • sign → signature (blob)
431
- • verify → prints ok/fail; exit 0/2
432
-
433
- 6) dsa and dh
434
-
435
- Small note: “DSA” is for signing; shared-secret is “DH/EC Diffie-Hellman.” I’ll split them:
436
- • dsa: dsa new|sign|verify (mirrors rsa but --alg dsa1024/2048 etc.)
437
- • dh (if you meant “shared”):
438
- • dh shared --priv DataSpec --peer DataSpec [--group {ffdhe2048,…}|--curve x25519|secp256r1]
439
- • Output: shared secret bytes (blob). Often you’ll immediately feed this to HKDF; consider --kdf … --bytes N that outputs a KDF’d key instead of raw shared secret.
440
-
441
- 7) bid
442
-
443
- Ambiguous name. Two likely meanings: blind signatures (e.g., RSA-blind for anonymous credentials) or binary ID/address generator. I’ll make it a namespace you can fill later; the interface below works for blind signatures:
444
- • bid new --alg rsa --bits N → issuer keys (like rsa new)
445
- • bid sign --key DataSpec --msg DataSpec --blind → blind signature flow (you may need a --token DataSpec and a --context str:…)
446
- • bid verify --pub DataSpec --msg DataSpec --sig DataSpec
447
- If that’s not what you meant, keep the namespace and plug the real semantics in; the IO pattern (DataSpec in, blob/json out) still fits.
448
-
449
- 8) sss (Shamir’s Secret Sharing)
450
- • Subcommands:
451
- • sss new --secret DataSpec --threshold K --shares N [--id str:label]
452
- • Output:
453
- • --out-prefix PREFIX → writes PREFIX.1, PREFIX.2, … each a compact share file (recommend binary with header; also allow --armor for base64)
454
- • --out-json → {"k":K,"n":N,"id":"…","shares":[{"i":1,"b64":"…"}, …]}
455
- • sss shares --in @PREFIX.* (or many --in) → prints metadata / validity (handy for audits)
456
- • sss recover --in @s1 --in @s2 ... (≥K files or b64: specs)
457
- • Output: recovered secret (blob)
458
-
459
-
460
-
461
- Examples (quick)
462
-
463
- # isprime
464
- tool isprime --n 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF --rounds 64 --out-json
465
-
466
- # random
467
- tool rand bytes --bytes 32 --out-b64
468
- tool rand int --bits 128 # decimal
469
- tool rand int --bits 128 --out-hex # hex
470
- tool rand prime --bits 256 --safe --out-hex
471
-
472
- # hash
473
- tool hash --alg sha256 --msg @file.bin --out-hex
474
-
475
- # aes
476
- tool aes new --size 256 --out @key.bin
477
- tool aes encrypt --mode gcm --key @key.bin --in @plain.bin --aad str:invoice-2025 --out-json
478
- tool aes decrypt --key @key.bin --in @cipher.json --out @plain.bin
479
-
480
- # rsa
481
- tool rsa new --bits 3072 --out-prefix ~/.keys/alice
482
- tool rsa sign --key @~/.keys/alice.key --msg @doc.pdf --pad pss --hash sha256 --out-b64
483
- tool rsa verify --pub @~/.keys/alice.pub --msg @doc.pdf --sig b64:ABCD…
484
-
485
- # dh (shared)
486
- tool dh shared --curve x25519 --priv @me.key --peer @peer.pub --out-b64
487
- tool dh shared --curve x25519 --priv @me.key --peer @peer.pub --kdf hkdf-sha256 --bytes 32 --out-b64
488
-
489
- # sss
490
- tool sss new --secret @master.key --threshold 3 --shares 5 --out-prefix ./vault/share
491
- tool sss recover --in @vault/share.1 --in @vault/share.3 --in @vault/share.4 --out @master.key
492
-
493
-
494
-
495
-
496
- argparse scaffold (drop-in)
497
-
498
- def add_shared_output(parent):
499
- g = parent.add_mutually_exclusive_group()
500
- g.add_argument('--out-raw', dest='out_raw', action='store_true')
501
- g.add_argument('--out-hex', dest='out_hex', action='store_true')
502
- g.add_argument('--out-b64', dest='out_b64', action='store_true')
503
- g.add_argument('--out-json', dest='out_json', action='store_true')
504
- parent.add_argument('--pretty', action='store_true')
505
- parent.add_argument('--out', default='-')
506
- parent.add_argument('--out-prefix')
507
- parent.add_argument('--force', action='store_true')
508
-
509
- def build_parser():
510
- p = argparse.ArgumentParser(prog='tool', description='Crypto toolbox')
511
- p.add_argument('--quiet', action='store_true')
512
- p.add_argument('--verbose', action='store_true')
513
-
514
- sub = p.add_subparsers(dest='cmd', required=True)
515
-
516
- # isprime
517
- p_ip = sub.add_parser('isprime', help='primality test')
518
- add_shared_output(p_ip)
519
- p_ip.add_argument('--n', required=True, help='integer (dec or 0x…)')
520
- p_ip.add_argument('--rounds', type=int, default=40)
521
- p_ip.add_argument('--expect', choices=['prime','composite'])
522
- p_ip.set_defaults(func=cmd_isprime)
523
-
524
- # rand
525
- p_rand = sub.add_parser('rand', help='randomness utilities')
526
- subr = p_rand.add_subparsers(dest='sub', required=True)
527
-
528
- pr_bytes = subr.add_parser('bytes', help='random bytes')
529
- add_shared_output(pr_bytes)
530
- pr_bytes.add_argument('--bytes', required=True)
531
- pr_bytes.set_defaults(func=cmd_rand_bytes)
532
-
533
- pr_int = subr.add_parser('int', help='random integer')
534
- add_shared_output(pr_int)
535
- pr_int.add_argument('--bits', required=True)
536
- pr_int.set_defaults(func=cmd_rand_int)
537
-
538
- pr_bits = subr.add_parser('bits', help='random bitstring')
539
- add_shared_output(pr_bits)
540
- pr_bits.add_argument('--bits', required=True)
541
- pr_bits.set_defaults(func=cmd_rand_bits)
542
-
543
- pr_prime = subr.add_parser('prime', help='random prime')
544
- add_shared_output(pr_prime)
545
- pr_prime.add_argument('--bits', required=True)
546
- pr_prime.add_argument('--safe', action='store_true')
547
- pr_prime.set_defaults(func=cmd_rand_prime)
548
-
549
- # hash
550
- p_hash = sub.add_parser('hash', help='hash a message')
551
- add_shared_output(p_hash)
552
- p_hash.add_argument('--alg', required=True, choices=['sha256','sha512','blake2b'])
553
- p_hash.add_argument('--msg', required=True, help='DataSpec')
554
- p_hash.set_defaults(func=cmd_hash)
555
-
556
- # aes
557
- p_aes = sub.add_parser('aes', help='AES operations')
558
- sub_aes = p_aes.add_subparsers(dest='sub', required=True)
559
-
560
- aes_new = sub_aes.add_parser('new', help='generate random AES key')
561
- add_shared_output(aes_new)
562
- aes_new.add_argument('--size', type=int, choices=[128,192,256], required=True)
563
- aes_new.set_defaults(func=cmd_aes_new)
564
-
565
- aes_frompass = sub_aes.add_parser('frompass', help='derive AES key from password')
566
- add_shared_output(aes_frompass)
567
- aes_frompass.add_argument('--from-pass', required=True, help='str:… or DataSpec')
568
- aes_frompass.add_argument('--kdf', choices=['argon2id','pbkdf2'], default='argon2id')
569
- aes_frompass.add_argument('--salt', help='DataSpec')
570
- aes_frompass.add_argument('--iters', type=int)
571
- aes_frompass.add_argument('--mem-mb', type=int)
572
- aes_frompass.add_argument('--parallelism', type=int)
573
- aes_frompass.add_argument('--size', type=int, choices=[128,192,256], required=True)
574
- aes_frompass.set_defaults(func=cmd_aes_frompass)
575
-
576
- def add_aes_io(sp):
577
- add_shared_output(sp)
578
- sp.add_argument('--in', dest='inp', required=True, help='DataSpec')
579
- sp.add_argument('--mode', choices=['gcm','ctr','cbc'], default='gcm')
580
- sp.add_argument('--key', help='DataSpec')
581
- sp.add_argument('--from-pass', help='str:… or DataSpec')
582
- sp.add_argument('--kdf', choices=['argon2id','pbkdf2'])
583
- sp.add_argument('--salt', help='DataSpec')
584
- sp.add_argument('--iters', type=int)
585
- sp.add_argument('--iv', '--nonce', dest='nonce', help='DataSpec')
586
- sp.add_argument('--aad', help='DataSpec')
587
-
588
- aes_enc = sub_aes.add_parser('encrypt', help='encrypt data')
589
- add_aes_io(aes_enc)
590
- aes_enc.set_defaults(func=cmd_aes_encrypt)
591
-
592
- aes_dec = sub_aes.add_parser('decrypt', help='decrypt data')
593
- add_aes_io(aes_dec)
594
- aes_dec.add_argument('--in-format', choices=['auto','json','raw'], default='auto')
595
- aes_dec.set_defaults(func=cmd_aes_decrypt)
596
-
597
- # rsa
598
- p_rsa = sub.add_parser('rsa', help='RSA operations')
599
- sr = p_rsa.add_subparsers(dest='sub', required=True)
600
-
601
- rsa_new = sr.add_parser('new', help='generate RSA keypair')
602
- add_shared_output(rsa_new)
603
- rsa_new.add_argument('--bits', type=int, required=True)
604
- rsa_new.add_argument('--e', type=int, default=65537)
605
- rsa_new.set_defaults(func=cmd_rsa_new)
606
-
607
- def add_rsa_keying(sp, need_pub=False, need_priv=False):
608
- add_shared_output(sp)
609
- if need_pub:
610
- sp.add_argument('--pub', required=True, help='DataSpec')
611
- if need_priv:
612
- sp.add_argument('--key', required=True, help='DataSpec')
613
- sp.add_argument('--pad', choices=['oaep','pkcs1v15'], default='oaep')
614
- sp.add_argument('--hash', choices=['sha256','sha512'], default='sha256')
615
-
616
- rsa_enc = sr.add_parser('encrypt', help='RSA encrypt')
617
- add_rsa_keying(rsa_enc, need_pub=True)
618
- rsa_enc.add_argument('--in', dest='inp', required=True, help='DataSpec')
619
- rsa_enc.set_defaults(func=cmd_rsa_encrypt)
620
-
621
- rsa_dec = sr.add_parser('decrypt', help='RSA decrypt')
622
- add_rsa_keying(rsa_dec, need_priv=True)
623
- rsa_dec.add_argument('--in', dest='inp', required=True, help='DataSpec')
624
- rsa_dec.set_defaults(func=cmd_rsa_decrypt)
625
-
626
- rsa_sign = sr.add_parser('sign', help='RSA sign')
627
- add_shared_output(rsa_sign)
628
- rsa_sign.add_argument('--key', required=True, help='DataSpec')
629
- rsa_sign.add_argument('--msg', required=True, help='DataSpec')
630
- rsa_sign.add_argument('--pad', choices=['pss','pkcs1v15'], default='pss')
631
- rsa_sign.add_argument('--hash', choices=['sha256','sha512'], default='sha256')
632
- rsa_sign.set_defaults(func=cmd_rsa_sign)
633
-
634
- rsa_verify = sr.add_parser('verify', help='RSA verify')
635
- add_shared_output(rsa_verify)
636
- rsa_verify.add_argument('--pub', required=True, help='DataSpec')
637
- rsa_verify.add_argument('--msg', required=True, help='DataSpec')
638
- rsa_verify.add_argument('--sig', required=True, help='DataSpec')
639
- rsa_verify.add_argument('--pad', choices=['pss','pkcs1v15'], default='pss')
640
- rsa_verify.add_argument('--hash', choices=['sha256','sha512'], default='sha256')
641
- rsa_verify.set_defaults(func=cmd_rsa_verify)
642
-
643
- # dsa
644
- p_dsa = sub.add_parser('dsa', help='DSA signing')
645
- sd = p_dsa.add_subparsers(dest='sub', required=True)
646
- dsa_new = sd.add_parser('new', help='DSA keypair'); add_shared_output(dsa_new)
647
- dsa_new.add_argument('--bits', type=int, required=True); dsa_new.set_defaults(func=cmd_dsa_new)
648
- dsa_sign = sd.add_parser('sign', help='DSA sign'); add_shared_output(dsa_sign)
649
- dsa_sign.add_argument('--key', required=True); dsa_sign.add_argument('--msg', required=True)
650
- dsa_sign.set_defaults(func=cmd_dsa_sign)
651
- dsa_verify = sd.add_parser('verify', help='DSA verify'); add_shared_output(dsa_verify)
652
- dsa_verify.add_argument('--pub', required=True); dsa_verify.add_argument('--msg', required=True); dsa_verify.add_argument('--sig', required=True)
653
- dsa_verify.set_defaults(func=cmd_dsa_verify)
654
-
655
- # dh (shared secret)
656
- p_dh = sub.add_parser('dh', help='Diffie-Hellman key agreement')
657
- sdh = p_dh.add_subparsers(dest='sub', required=True)
658
- dh_shared = sdh.add_parser('shared', help='compute shared secret'); add_shared_output(dh_shared)
659
- dh_shared.add_argument('--priv', required=True); dh_shared.add_argument('--peer', required=True)
660
- dh_shared.add_argument('--group', help='ffdhe… or secp…'); dh_shared.add_argument('--curve', help='x25519, secp256r1, …')
661
- dh_shared.add_argument('--kdf', choices=['none','hkdf-sha256','hkdf-sha512'], default='none')
662
- dh_shared.add_argument('--bytes', type=int)
663
- dh_shared.set_defaults(func=cmd_dh_shared)
664
-
665
- # bid (placeholder)
666
- p_bid = sub.add_parser('bid', help='blind-id namespace'); sbid = p_bid.add_subparsers(dest='sub', required=True)
667
- bid_new = sbid.add_parser('new', help='issuer keys'); add_shared_output(bid_new); bid_new.set_defaults(func=cmd_bid_new)
668
- bid_verify = sbid.add_parser('verify', help='verify token'); add_shared_output(bid_verify)
669
- bid_verify.add_argument('--pub', required=True); bid_verify.add_argument('--msg', required=True); bid_verify.add_argument('--sig', required=True)
670
- bid_verify.set_defaults(func=cmd_bid_verify)
671
-
672
- # sss
673
- p_sss = sub.add_parser('sss', help='Shamir secret sharing'); ssss = p_sss.add_subparsers(dest='sub', required=True)
674
- sss_new = ssss.add_parser('new', help='split secret'); add_shared_output(sss_new)
675
- sss_new.add_argument('--secret', required=True); sss_new.add_argument('--threshold', type=int, required=True); sss_new.add_argument('--shares', type=int, required=True); sss_new.add_argument('--id')
676
- sss_new.set_defaults(func=cmd_sss_new)
677
- sss_shares = ssss.add_parser('shares', help='inspect shares'); add_shared_output(sss_shares)
678
- sss_shares.add_argument('--in', dest='inputs', action='append', required=True)
679
- sss_shares.set_defaults(func=cmd_sss_shares)
680
- sss_rec = ssss.add_parser('recover', help='recover secret'); add_shared_output(sss_rec)
681
- sss_rec.add_argument('--in', dest='inputs', action='append', required=True)
682
- sss_rec.set_defaults(func=cmd_sss_recover)
683
-
684
- return p
685
-
686
-
687
-
688
-
689
- If you want, I can fill in concrete implementations next (e.g., Miller–Rabin for isprime, X25519 for dh shared, AES-GCM JSON bundle, RSA PSS, SSS split/recover). Tell me which two or three you want first and I’ll wire them up.
690
-
691
- """
692
-
693
- from __future__ import annotations
694
-
695
- import argparse
696
- # import pdb
697
- import sys
698
-
699
- from rich import console as rich_console
700
-
701
- from . import base
702
-
703
- __author__ = 'balparda@github.com'
704
- __version__: str = base.__version__ # version comes from base!
705
- __version_tuple__: tuple[int, ...] = base.__version_tuple__
706
-
707
-
708
- def _BuildParser() -> argparse.ArgumentParser: # pylint: disable=too-many-statements,too-many-locals
709
- """Construct the CLI argument parser (kept in sync with the docs)."""
710
- # ========================= main parser ==========================================================
711
- parser: argparse.ArgumentParser = argparse.ArgumentParser(
712
- prog='poetry run safetrans',
713
- description=('safetrans: CLI for safe random, primes, hashing, '
714
- 'AES, RSA, DSA, bidding, and secret sharing.'),
715
- epilog=(
716
- 'Examples:\n\n'
717
- ' # --- Randomness / Primes ---\n'
718
- ' poetry run transcrypto random bits 16\n'
719
- ' poetry run transcrypto random bytes 32\n'
720
- ' poetry run transcrypto random int 1000 2000\n'
721
- ' poetry run transcrypto random prime 64\n\n'
722
- ' poetry run transcrypto isprime 428568761\n'
723
- ' # --- Hashing ---\n'
724
- ' poetry run transcrypto hash sha256 xyz\n'
725
- ' poetry run transcrypto --b64 hash sha512 -- eHl6\n'
726
- ' poetry run transcrypto hash file /etc/passwd --digest sha512\n\n'
727
- ' # --- AES ---\n'
728
- ' poetry run transcrypto --out-b64 aes new\n'
729
- ' poetry run transcrypto --out-b64 aes key "correct horse battery staple"\n'
730
- ' poetry run transcrypto --b64 --out-b64 aes encrypt -k "<b64key>" -- "secret"\n'
731
- ' poetry run transcrypto --b64 --out-b64 aes decrypt -k "<b64key>" -- "<ciphertext>"\n'
732
- ' # --- RSA ---\n'
733
- ' poetry run transcrypto -p rsa-key rsa new --bits 2048\n'
734
- ' poetry run transcrypto --bin --out-b64 -p rsa-key.pub rsa encrypt -a <aad> <plaintext>\n'
735
- ' poetry run transcrypto --b64 --out-bin -p rsa-key.priv rsa decrypt -a <aad> -- <ciphertext>\n'
736
- ' poetry run transcrypto --bin --out-b64 -p rsa-key.priv rsa sign <message>\n'
737
- ' poetry run transcrypto --b64 -p rsa-key.pub rsa verify -- <message> <signature>\n\n'
738
- ' # --- DSA ---\n'
739
- ' poetry run transcrypto -p dsa-key dsa shared --p-bits 2048 --q-bits 256\n'
740
- ' poetry run transcrypto -p dsa-key dsa new\n'
741
- ' poetry run transcrypto --bin --out-b64 -p dsa-key.priv dsa sign <message>\n'
742
- ' poetry run transcrypto --b64 -p dsa-key.pub dsa verify -- <message> <signature>\n\n'
743
- ' # --- Public Bid ---\n'
744
- ' poetry run transcrypto --bin bid new "tomorrow it will rain"\n'
745
- ' poetry run transcrypto --out-bin bid verify\n\n'
746
- ' # --- Shamir Secret Sharing (SSS) ---\n'
747
- ' poetry run transcrypto -p sss-key sss new 3 --bits 1024\n'
748
- ' poetry run transcrypto --bin -p sss-key sss shares <secret> <n>\n'
749
- ' poetry run transcrypto --out-bin -p sss-key sss recover\n'
750
- ),
751
- formatter_class=argparse.RawTextHelpFormatter)
752
- sub = parser.add_subparsers(dest='command')
753
-
754
- # ========================= global flags =========================================================
755
- # -v/-vv/-vvv/-vvvv for ERROR/WARN/INFO/DEBUG
756
- parser.add_argument(
757
- '-v', '--verbose', action='count', default=0,
758
- help='Increase verbosity (use -v/-vv/-vvv/-vvvv for ERROR/WARN/INFO/DEBUG)')
759
-
760
- parser.add_argument(
761
- '-i', '--in', type=str, default='',
762
- help=('Input: "int" is (decimal) integer, "hex" is hexadecimal, '
763
- '"b64" is base-64 encoded, "bin" is binary, '
764
- 'anything else is considered a "/file/path" or "/file/path/prefix" to be used to '
765
- 'read binary objects; '
766
- 'default is intentionally empty because each command will explain what default '
767
- 'behavior it will use, eg. for `random int` the default is decimal int but '
768
- 'for `rsa encrypt` the default is to expect a file prefix'))
769
-
770
- parser.add_argument(
771
- '-o', '--out', type=str, default='',
772
- help=('Output: "int" is (decimal) integer, "hex" is hexadecimal, '
773
- '"b64" is base-64 encoded, "bin" is binary, '
774
- 'anything else is considered a "/file/path" or "/file/path/prefix" to be used to '
775
- 'output binary objects; '
776
- 'default is intentionally empty because each command will explain what default '
777
- 'behavior it will use, eg. for `random int` the default is hexadecimal but '
778
- 'for `rsa new` the default is to expect a file prefix'))
779
-
780
- # # --hex/--b64/--bin for input mode (default hex)
781
- # in_grp = parser.add_mutually_exclusive_group()
782
- # in_grp.add_argument('--hex', action='store_true', help='Treat inputs as hex string (default)')
783
- # in_grp.add_argument(
784
- # '--b64', action='store_true',
785
- # help=('Treat inputs as base64url; sometimes base64 will start with "-" and that can '
786
- # 'conflict with flags, so use "--" before positional args if needed'))
787
- # in_grp.add_argument('--bin', action='store_true', help='Treat inputs as binary (bytes)')
788
-
789
- # # --out-hex/--out-b64/--out-bin for output mode (default hex)
790
- # out_grp = parser.add_mutually_exclusive_group()
791
- # out_grp.add_argument('--out-hex', action='store_true', help='Outputs as hex (default)')
792
- # out_grp.add_argument('--out-b64', action='store_true', help='Outputs as base64url')
793
- # out_grp.add_argument('--out-bin', action='store_true', help='Outputs as binary (bytes)')
794
-
795
- # # key loading/saving from/to file, with optional password; will only work with some commands
796
- # parser.add_argument(
797
- # '-p', '--key-path', type=str, default='',
798
- # help='File path to serialized key object, if key is needed for operation')
799
- # parser.add_argument(
800
- # '--protect', type=str, default='',
801
- # help='Password to encrypt/decrypt key file if using the `-p`/`--key-path` option')
802
-
803
- # ========================= randomness / primes ==================================================
804
-
805
- # Cryptographically secure randomness
806
- p_rand: argparse.ArgumentParser = sub.add_parser(
807
- 'random', help='Cryptographically secure randomness, from the OS CSPRNG.')
808
- rsub = p_rand.add_subparsers(dest='rand_command')
809
-
810
- # Random bits
811
- p_rand_bits: argparse.ArgumentParser = rsub.add_parser(
812
- 'bits',
813
- help=('Random bytes with exact bit length `bits` (≥ 8, MSB will be 1). '
814
- 'By default, `-o/--out` is hexadecimal.'),
815
- epilog='random bits 16\n36650')##
816
- p_rand_bits.add_argument('bits', type=int, help='Number of bits to generate, ≥ 8')
817
-
818
- # Random bytes
819
- p_rand_bytes: argparse.ArgumentParser = rsub.add_parser(
820
- 'bytes',
821
- help=('Random bytes with exact bit length `n` (≥ 1, MSB will be 1). '
822
- 'By default, `-o/--out` is hexadecimal.'),
823
- epilog='random bytes 32\n6c6f1f88cb93c4323285a2224373d6e59c72a9c2b82e20d1c376df4ffbe9507f')##
824
- p_rand_bytes.add_argument('n', type=int, help='Number of bytes to generate, ≥ 1')
825
-
826
- # Random integer in [min, max]
827
- p_rand_int: argparse.ArgumentParser = rsub.add_parser(
828
- 'int',
829
- help=('Uniform random integer in `[min, max]` range, inclusive. '
830
- 'By default, `-o/--out` is hexadecimal.'),
831
- epilog='random int 1000 2000\n1628')
832
- p_rand_int.add_argument('min', type=str, help='Minimum, ≥ 0')
833
- p_rand_int.add_argument('max', type=str, help='Maximum, > `min`')
834
-
835
- # Random prime with given bit length
836
- p_rand_prime: argparse.ArgumentParser = rsub.add_parser(
837
- 'prime',
838
- help=('Random prime with exact bit length `bits` (≥ 11, MSB will be 1). '
839
- 'By default, `-o/--out` is hexadecimal.'),
840
- epilog='random prime 32\n2365910551')##
841
- p_rand_prime.add_argument('bits', type=int, help='Number of bits to generate, ≥ 11')
842
-
843
- # Primality test with safe defaults
844
- p_isprime: argparse.ArgumentParser = sub.add_parser(
845
- 'isprime',
846
- help='Primality test with safe defaults, useful for any integer size.',
847
- epilog='isprime 2305843009213693951\nTrue $$ isprime 2305843009213693953\nFalse')
848
- p_isprime.add_argument(
849
- 'n', type=str, help='Integer to test, ≥ 1')
850
-
851
- # ========================= hashing ==============================================================
852
-
853
- # Hashing group
854
- p_hash: argparse.ArgumentParser = sub.add_parser(
855
- 'hash', help='Cryptographic Hashing (SHA-256 / SHA-512 / file).')
856
- hash_sub = p_hash.add_subparsers(dest='hash_command')
857
-
858
- # SHA-256
859
- p_h256: argparse.ArgumentParser = hash_sub.add_parser(
860
- 'sha256',
861
- help='SHA-256 of input `data`.',
862
- epilog=('--bin hash sha256 xyz\n'
863
- '3608bca1e44ea6c4d268eb6db02260269892c0b42b86bbf1e77a6fa16c3c9282 $$'
864
- '--b64 hash sha256 -- eHl6 # "xyz" in base-64\n'
865
- '3608bca1e44ea6c4d268eb6db02260269892c0b42b86bbf1e77a6fa16c3c9282'))
866
- p_h256.add_argument('data', type=str, help='Input data (raw text; or use --hex/--b64/--bin)')
867
-
868
- # SHA-512
869
- p_h512 = hash_sub.add_parser(
870
- 'sha512',
871
- help='SHA-512 of input `data`.',
872
- epilog=('--bin hash sha512 xyz\n'
873
- '4a3ed8147e37876adc8f76328e5abcc1b470e6acfc18efea0135f983604953a5'
874
- '8e183c1a6086e91ba3e821d926f5fdeb37761c7ca0328a963f5e92870675b728 $$'
875
- '--b64 hash sha512 -- eHl6 # "xyz" in base-64\n'
876
- '4a3ed8147e37876adc8f76328e5abcc1b470e6acfc18efea0135f983604953a5'
877
- '8e183c1a6086e91ba3e821d926f5fdeb37761c7ca0328a963f5e92870675b728'))
878
- p_h512.add_argument('data', type=str, help='Input data (raw text; or use --hex/--b64/--bin)')
879
-
880
- # Hash file contents (streamed)
881
- p_hf: argparse.ArgumentParser = hash_sub.add_parser(
882
- 'file',
883
- help='SHA-256/512 hash of file contents, defaulting to SHA-256.',
884
- epilog=('hash file /etc/passwd --digest sha512\n'
885
- '8966f5953e79f55dfe34d3dc5b160ac4a4a3f9cbd1c36695a54e28d77c7874df'
886
- 'f8595502f8a420608911b87d336d9e83c890f0e7ec11a76cb10b03e757f78aea'))
887
- p_hf.add_argument('path', type=str, help='Path to existing file')
888
- p_hf.add_argument('--digest', choices=['sha256', 'sha512'], default='sha256',
889
- help='Digest type, SHA-256 ("sha256") or SHA-512 ("sha512")')
890
-
891
- # ========================= AES (GCM + ECB helper) ===============================================
892
-
893
- # AES group
894
- p_aes: argparse.ArgumentParser = sub.add_parser(
895
- 'aes',
896
- help=('AES-256 operations (GCM/ECB) and key derivation. '
897
- 'No measures are taken here to prevent timing attacks.'))
898
- aes_sub = p_aes.add_subparsers(dest='aes_command')
899
-
900
- # Derive key from password
901
- p_aes_key_new: argparse.ArgumentParser = aes_sub.add_parser(
902
- 'key',
903
- help=('Derive key from a password (PBKDF2-HMAC-SHA256) with custom expensive '
904
- 'salt and iterations. Very good/safe for simple password-to-key but not for '
905
- 'passwords databases (because of constant salt).'),
906
- epilog=('--out-b64 aes key "correct horse battery staple"\n'
907
- 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= $$ ' # cspell:disable-line
908
- '-p keyfile.out --protect hunter aes key "correct horse battery staple"\n'
909
- 'AES key saved to \'keyfile.out\''))
910
- p_aes_key_new.add_argument(
911
- 'password', type=str, help='Password (leading/trailing spaces ignored)')
912
-
913
- # Derive key from password
914
- p_aes_key_pass: argparse.ArgumentParser = aes_sub.add_parser(
915
- 'new',
916
- help=('Derive key from a password (PBKDF2-HMAC-SHA256) with custom expensive '
917
- 'salt and iterations. Very good/safe for simple password-to-key but not for '
918
- 'passwords databases (because of constant salt).'),
919
- epilog=('--out-b64 aes key "correct horse battery staple"\n'
920
- 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= $$ ' # cspell:disable-line
921
- '-p keyfile.out --protect hunter aes key "correct horse battery staple"\n'
922
- 'AES key saved to \'keyfile.out\''))
923
- p_aes_key_pass.add_argument(
924
- 'password', type=str, help='Password (leading/trailing spaces ignored)')
925
-
926
- # AES-256-GCM encrypt
927
- p_aes_enc: argparse.ArgumentParser = aes_sub.add_parser(
928
- 'encrypt',
929
- help=('AES-256-GCM: safely encrypt `plaintext` with `-k`/`--key` or with '
930
- '`-p`/`--key-path` keyfile. All inputs are raw, or you '
931
- 'can use `--bin`/`--hex`/`--b64` flags. Attention: if you provide `-a`/`--aad` '
932
- '(associated data, AAD), you will need to provide the same AAD when decrypting '
933
- 'and it is NOT included in the `ciphertext`/CT returned by this method!'),
934
- epilog=('--b64 --out-b64 aes encrypt -k DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= -- ' # cspell:disable-line
935
- 'AAAAAAB4eXo=\nF2_ZLrUw5Y8oDnbTP5t5xCUWX8WtVILLD0teyUi_37_4KHeV-YowVA== $$ ' # cspell:disable-line
936
- '--b64 --out-b64 aes encrypt -k DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= -a eHl6 ' # cspell:disable-line
937
- '-- AAAAAAB4eXo=\nxOlAHPUPpeyZHId-f3VQ_QKKMxjIW0_FBo9WOfIBrzjn0VkVV6xTRA==')) # cspell:disable-line
938
- p_aes_enc.add_argument('plaintext', type=str, help='Input data to encrypt (PT)')
939
- p_aes_enc.add_argument(
940
- '-k', '--key', type=str, default='', help='Key if `-p`/`--key-path` wasn\'t used (32 bytes)')
941
- p_aes_enc.add_argument(
942
- '-a', '--aad', type=str, default='',
943
- help='Associated data (optional; has to be separately sent to receiver/stored)')
944
-
945
- # AES-256-GCM decrypt
946
- p_aes_dec: argparse.ArgumentParser = aes_sub.add_parser(
947
- 'decrypt',
948
- help=('AES-256-GCM: safely decrypt `ciphertext` with `-k`/`--key` or with '
949
- '`-p`/`--key-path` keyfile. All inputs are raw, or you '
950
- 'can use `--bin`/`--hex`/`--b64` flags. Attention: if you provided `-a`/`--aad` '
951
- '(associated data, AAD) during encryption, you will need to provide the same AAD now!'),
952
- epilog=('--b64 --out-b64 aes decrypt -k DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= -- ' # cspell:disable-line
953
- 'F2_ZLrUw5Y8oDnbTP5t5xCUWX8WtVILLD0teyUi_37_4KHeV-YowVA==\nAAAAAAB4eXo= $$ ' # cspell:disable-line
954
- '--b64 --out-b64 aes decrypt -k DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= ' # cspell:disable-line
955
- '-a eHl6 -- xOlAHPUPpeyZHId-f3VQ_QKKMxjIW0_FBo9WOfIBrzjn0VkVV6xTRA==\nAAAAAAB4eXo=')) # cspell:disable-line
956
- p_aes_dec.add_argument('ciphertext', type=str, help='Input data to decrypt (CT)')
957
- p_aes_dec.add_argument(
958
- '-k', '--key', type=str, default='', help='Key if `-p`/`--key-path` wasn\'t used (32 bytes)')
959
- p_aes_dec.add_argument(
960
- '-a', '--aad', type=str, default='',
961
- help='Associated data (optional; has to be exactly the same as used during encryption)')
962
-
963
- # ========================= RSA ==================================================================
964
-
965
- # RSA group
966
- p_rsa: argparse.ArgumentParser = sub.add_parser(
967
- 'rsa',
968
- help=('RSA (Rivest-Shamir-Adleman) asymmetric cryptography. '
969
- 'No measures are taken here to prevent timing attacks. '
970
- 'All methods require file key(s) as `-p`/`--key-path` (see provided examples).'))
971
- rsa_sub = p_rsa.add_subparsers(dest='rsa_command')
972
-
973
- # Generate new RSA private key
974
- p_rsa_new: argparse.ArgumentParser = rsa_sub.add_parser(
975
- 'new',
976
- help=('Generate RSA private/public key pair with `bits` modulus size '
977
- '(prime sizes will be `bits`/2). '
978
- 'Requires `-p`/`--key-path` to set the basename for output files.'),
979
- epilog=('-p rsa-key rsa new --bits 64 # NEVER use such a small key: example only!\n'
980
- 'RSA private/public keys saved to \'rsa-key.priv/.pub\''))
981
- p_rsa_new.add_argument(
982
- '--bits', type=int, default=3332, help='Modulus size in bits; the default is a safe size')
983
-
984
- # Encrypt with public key
985
- p_rsa_enc_safe: argparse.ArgumentParser = rsa_sub.add_parser(
986
- 'encrypt',
987
- help='Encrypt `message` with public key.',
988
- epilog=('--bin --out-b64 -p rsa-key.pub rsa encrypt "abcde" -a "xyz"\n'
989
- 'AO6knI6xwq6TGR…Qy22jiFhXi1eQ=='))
990
- p_rsa_enc_safe.add_argument('plaintext', type=str, help='Message to encrypt')
991
- p_rsa_enc_safe.add_argument(
992
- '-a', '--aad', type=str, default='',
993
- help='Associated data (optional; has to be separately sent to receiver/stored)')
994
-
995
- # Decrypt ciphertext with private key
996
- p_rsa_dec_safe: argparse.ArgumentParser = rsa_sub.add_parser(
997
- 'decrypt',
998
- help='Decrypt `ciphertext` with private key.',
999
- epilog=('--b64 --out-bin -p rsa-key.priv rsa decrypt -a eHl6 -- '
1000
- 'AO6knI6xwq6TGR…Qy22jiFhXi1eQ==\nabcde'))
1001
- p_rsa_dec_safe.add_argument('ciphertext', type=str, help='Ciphertext to decrypt')
1002
- p_rsa_dec_safe.add_argument(
1003
- '-a', '--aad', type=str, default='',
1004
- help='Associated data (optional; has to be exactly the same as used during encryption)')
1005
-
1006
- # Sign message with private key
1007
- p_rsa_sig_safe: argparse.ArgumentParser = rsa_sub.add_parser(
1008
- 'sign',
1009
- help='Sign `message` with private key.',
1010
- epilog='--bin --out-b64 -p rsa-key.priv rsa sign "xyz"\n91TS7gC6LORiL…6RD23Aejsfxlw==') # cspell:disable-line
1011
- p_rsa_sig_safe.add_argument('message', type=str, help='Message to sign')
1012
- p_rsa_sig_safe.add_argument(
1013
- '-a', '--aad', type=str, default='',
1014
- help='Associated data (optional; has to be separately sent to receiver/stored)')
1015
-
1016
- # Verify signature with public key
1017
- p_rsa_ver_safe: argparse.ArgumentParser = rsa_sub.add_parser(
1018
- 'verify',
1019
- help='Verify `signature` for `message` with public key.',
1020
- epilog=('--b64 -p rsa-key.pub rsa verify -- eHl6 '
1021
- '91TS7gC6LORiL…6RD23Aejsfxlw==\nRSA signature: OK $$ ' # cspell:disable-line
1022
- '--b64 -p rsa-key.pub rsa verify -- eLl6 '
1023
- '91TS7gC6LORiL…6RD23Aejsfxlw==\nRSA signature: INVALID')) # cspell:disable-line
1024
- p_rsa_ver_safe.add_argument('message', type=str, help='Message that was signed earlier')
1025
- p_rsa_ver_safe.add_argument('signature', type=str, help='Putative signature for `message`')
1026
- p_rsa_ver_safe.add_argument(
1027
- '-a', '--aad', type=str, default='',
1028
- help='Associated data (optional; has to be exactly the same as used during signing)')
1029
-
1030
- # ========================= DSA ==================================================================
1031
-
1032
- # DSA group
1033
- p_dsa: argparse.ArgumentParser = sub.add_parser(
1034
- 'dsa',
1035
- help=('DSA (Digital Signature Algorithm) asymmetric signing/verifying. '
1036
- 'No measures are taken here to prevent timing attacks. '
1037
- 'All methods require file key(s) as `-p`/`--key-path` (see provided examples).'))
1038
- dsa_sub = p_dsa.add_subparsers(dest='dsa_command')
1039
-
1040
- # Generate shared (p,q,g) params
1041
- p_dsa_shared: argparse.ArgumentParser = dsa_sub.add_parser(
1042
- 'shared',
1043
- help=('Generate a shared DSA key with `p-bits`/`q-bits` prime modulus sizes, which is '
1044
- 'the first step in key generation. `q-bits` should be larger than the secrets that '
1045
- 'will be protected and `p-bits` should be much larger than `q-bits` (e.g. 4096/544). '
1046
- 'The shared key can safely be used by any number of users to generate their '
1047
- 'private/public key pairs (with the `new` command). The shared keys are "public". '
1048
- 'Requires `-p`/`--key-path` to set the basename for output files.'),
1049
- epilog=('-p dsa-key dsa shared --p-bits 128 --q-bits 32 '
1050
- '# NEVER use such a small key: example only!\n'
1051
- 'DSA shared key saved to \'dsa-key.shared\''))
1052
- p_dsa_shared.add_argument(
1053
- '--p-bits', type=int, default=4096,
1054
- help='Prime modulus (`p`) size in bits; the default is a safe size')
1055
- p_dsa_shared.add_argument(
1056
- '--q-bits', type=int, default=544,
1057
- help=('Prime modulus (`q`) size in bits; the default is a safe size ***IFF*** you '
1058
- 'are protecting symmetric keys or regular hashes'))
1059
-
1060
- # Generate individual private key from shared (p,q,g)
1061
- dsa_sub.add_parser(
1062
- 'new',
1063
- help='Generate an individual DSA private/public key pair from a shared key.',
1064
- epilog='-p dsa-key dsa new\nDSA private/public keys saved to \'dsa-key.priv/.pub\'')
1065
-
1066
- # Sign message with private key
1067
- p_dsa_sign_safe: argparse.ArgumentParser = dsa_sub.add_parser(
1068
- 'sign',
1069
- help='Sign message with private key.',
1070
- epilog='--bin --out-b64 -p dsa-key.priv dsa sign "xyz"\nyq8InJVpViXh9…BD4par2XuA=')
1071
- p_dsa_sign_safe.add_argument('message', type=str, help='Message to sign')
1072
- p_dsa_sign_safe.add_argument(
1073
- '-a', '--aad', type=str, default='',
1074
- help='Associated data (optional; has to be separately sent to receiver/stored)')
1075
-
1076
- # Verify DSA signature (s1,s2)
1077
- p_dsa_verify_safe: argparse.ArgumentParser = dsa_sub.add_parser(
1078
- 'verify',
1079
- help='Verify `signature` for `message` with public key.',
1080
- epilog=('--b64 -p dsa-key.pub dsa verify -- eHl6 yq8InJVpViXh9…BD4par2XuA=\n'
1081
- 'DSA signature: OK $$ '
1082
- '--b64 -p dsa-key.pub dsa verify -- eLl6 yq8InJVpViXh9…BD4par2XuA=\n'
1083
- 'DSA signature: INVALID'))
1084
- p_dsa_verify_safe.add_argument('message', type=str, help='Message that was signed earlier')
1085
- p_dsa_verify_safe.add_argument('signature', type=str, help='Putative signature for `message`')
1086
- p_dsa_verify_safe.add_argument(
1087
- '-a', '--aad', type=str, default='',
1088
- help='Associated data (optional; has to be exactly the same as used during signing)')
1089
-
1090
- # ========================= Public Bid ===========================================================
1091
-
1092
- # bidding group
1093
- p_bid: argparse.ArgumentParser = sub.add_parser(
1094
- 'bid',
1095
- help=('Bidding on a `secret` so that you can cryptographically convince a neutral '
1096
- 'party that the `secret` that was committed to previously was not changed. '
1097
- 'All methods require file key(s) as `-p`/`--key-path` (see provided examples).'))
1098
- bid_sub = p_bid.add_subparsers(dest='bid_command')
1099
-
1100
- # Generate a new bid
1101
- p_bid_new: argparse.ArgumentParser = bid_sub.add_parser(
1102
- 'new',
1103
- help=('Generate the bid files for `secret`. '
1104
- 'Requires `-p`/`--key-path` to set the basename for output files.'),
1105
- epilog=('--bin -p my-bid bid new "tomorrow it will rain"\n'
1106
- 'Bid private/public commitments saved to \'my-bid.priv/.pub\''))
1107
- p_bid_new.add_argument('secret', type=str, help='Input data to bid to, the protected "secret"')
1108
-
1109
- # verify bid
1110
- bid_sub.add_parser(
1111
- 'verify',
1112
- help=('Verify the bid files for correctness and reveal the `secret`. '
1113
- 'Requires `-p`/`--key-path` to set the basename for output files.'),
1114
- epilog=('--out-bin -p my-bid bid verify\n'
1115
- 'Bid commitment: OK\nBid secret:\ntomorrow it will rain'))
1116
-
1117
- # ========================= Shamir Secret Sharing ================================================
1118
-
1119
- # SSS group
1120
- p_sss: argparse.ArgumentParser = sub.add_parser(
1121
- 'sss',
1122
- help=('SSS (Shamir Shared Secret) secret sharing crypto scheme. '
1123
- 'No measures are taken here to prevent timing attacks. '
1124
- 'All methods require file key(s) as `-p`/`--key-path` (see provided examples).'))
1125
- sss_sub = p_sss.add_subparsers(dest='sss_command')
1126
-
1127
- # Generate new SSS params (t, prime, coefficients)
1128
- p_sss_new: argparse.ArgumentParser = sss_sub.add_parser(
1129
- 'new',
1130
- help=('Generate the private keys with `bits` prime modulus size and so that at least a '
1131
- '`minimum` number of shares are needed to recover the secret. '
1132
- 'This key will be used to generate the shares later (with the `shares` command). '
1133
- 'Requires `-p`/`--key-path` to set the basename for output files.'),
1134
- epilog=('-p sss-key sss new 3 --bits 64 # NEVER use such a small key: example only!\n'
1135
- 'SSS private/public keys saved to \'sss-key.priv/.pub\''))
1136
- p_sss_new.add_argument(
1137
- 'minimum', type=int, help='Minimum number of shares required to recover secret, ≥ 2')
1138
- p_sss_new.add_argument(
1139
- '--bits', type=int, default=1024,
1140
- help=('Prime modulus (`p`) size in bits; the default is a safe size ***IFF*** you '
1141
- 'are protecting symmetric keys; the number of bits should be comfortably larger '
1142
- 'than the size of the secret you want to protect with this scheme'))
1143
-
1144
- # Issue N shares for a secret
1145
- p_sss_shares_safe: argparse.ArgumentParser = sss_sub.add_parser(
1146
- 'shares',
1147
- help='Shares: Issue `count` private shares for a `secret`.',
1148
- epilog=('--bin -p sss-key sss shares "abcde" 5\n'
1149
- 'SSS 5 individual (private) shares saved to \'sss-key.share.1…5\'\n'
1150
- '$ rm sss-key.share.2 sss-key.share.4 '
1151
- '# this is to simulate only having shares 1,3,5'))
1152
- p_sss_shares_safe.add_argument('secret', type=str, help='Secret to be protected')
1153
- p_sss_shares_safe.add_argument(
1154
- 'count', type=int,
1155
- help=('How many shares to produce; must be ≥ `minimum` used in `new` command or else the '
1156
- '`secret` would become unrecoverable'))
1157
-
1158
- # Recover secret from shares
1159
- sss_sub.add_parser(
1160
- 'recover',
1161
- help='Recover secret from shares; will use any available shares that were found.',
1162
- epilog=('--out-bin -p sss-key sss recover\n'
1163
- 'Loaded SSS share: \'sss-key.share.3\'\n'
1164
- 'Loaded SSS share: \'sss-key.share.5\'\n'
1165
- 'Loaded SSS share: \'sss-key.share.1\' '
1166
- '# using only 3 shares: number 2/4 are missing\n'
1167
- 'Secret:\nabcde'))
1168
-
1169
- # ========================= Markdown Generation ==================================================
1170
-
1171
- # Documentation generation
1172
- doc: argparse.ArgumentParser = sub.add_parser(
1173
- 'doc', help='Documentation utilities. (Not for regular use: these are developer utils.)')
1174
- doc_sub = doc.add_subparsers(dest='doc_command')
1175
- doc_sub.add_parser(
1176
- 'md',
1177
- help='Emit Markdown docs for the CLI (see README.md section "Creating a New Version").',
1178
- epilog='doc md > transcrypto.md\n<<saves file>>')
1179
-
1180
- return parser
1181
-
1182
-
1183
- def main(argv: list[str] | None = None, /) -> int: # pylint: disable=invalid-name,too-many-locals,too-many-branches,too-many-statements
1184
- """Main entry point."""
1185
- # build the parser and parse args
1186
- parser: argparse.ArgumentParser = _BuildParser()
1187
- args: argparse.Namespace = parser.parse_args(argv)
1188
- # take care of global options
1189
- console: rich_console.Console = base.InitLogging(args.verbose, soft_wrap=True)
1190
-
1191
- try:
1192
- # get the command, do basic checks and switch
1193
- command: str = args.command.lower().strip() if args.command else ''
1194
- match command:
1195
- # -------- TODO ----------
1196
- case 'TODO':
1197
- pass
1198
-
1199
- # -------- Documentation ----------
1200
- case 'doc':
1201
- doc_command: str = (
1202
- args.doc_command.lower().strip() if getattr(args, 'doc_command', '') else '')
1203
- match doc_command:
1204
- case 'md':
1205
- console.print(base.GenerateCLIMarkdown(
1206
- 'safetrans', _BuildParser(), description=(
1207
- '`safetrans` is a command-line utility that provides ***safe*** crypto '
1208
- 'primitives. It serves as a convenient wrapper over the Python APIs, '
1209
- 'enabling only safe **cryptographic operations**, '
1210
- '**number theory functions**, **secure randomness generation**, **hashing**, '
1211
- '**AES**, **RSA**, **DSA**, **bidding**, **SSS**, '
1212
- 'and other utilities without writing code.')))
1213
- case _:
1214
- raise NotImplementedError()
1215
-
1216
- case _:
1217
- parser.print_help()
1218
-
1219
- except NotImplementedError as err:
1220
- console.print(f'Invalid command: {err}')
1221
- except (base.Error, ValueError) as err:
1222
- console.print(str(err))
1223
-
1224
- return 0
1225
-
1226
-
1227
- if __name__ == '__main__':
1228
- sys.exit(main())