transcrypto 2.3.2__tar.gz → 2.5.0__tar.gz

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.
Files changed (38) hide show
  1. {transcrypto-2.3.2 → transcrypto-2.5.0}/PKG-INFO +3 -2
  2. {transcrypto-2.3.2 → transcrypto-2.5.0}/README.md +1 -0
  3. {transcrypto-2.3.2 → transcrypto-2.5.0}/pyproject.toml +7 -5
  4. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/__init__.py +1 -1
  5. transcrypto-2.5.0/src/transcrypto/cli/safeaeshash.py +258 -0
  6. transcrypto-2.5.0/src/transcrypto/cli/safebidsecret.py +220 -0
  7. transcrypto-2.5.0/src/transcrypto/cli/safeintmath.py +89 -0
  8. transcrypto-2.5.0/src/transcrypto/cli/safepublicalgos.py +345 -0
  9. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/hashes.py +3 -3
  10. transcrypto-2.5.0/src/transcrypto/safetrans.py +429 -0
  11. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/transcrypto.py +10 -91
  12. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/utils/timer.py +44 -4
  13. {transcrypto-2.3.2 → transcrypto-2.5.0}/LICENSE +0 -0
  14. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/cli/__init__.py +0 -0
  15. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/cli/aeshash.py +0 -0
  16. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/cli/bidsecret.py +0 -0
  17. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/cli/clibase.py +0 -0
  18. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/cli/intmath.py +0 -0
  19. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/cli/publicalgos.py +0 -0
  20. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/__init__.py +0 -0
  21. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/aes.py +0 -0
  22. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/bid.py +0 -0
  23. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/constants.py +0 -0
  24. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/dsa.py +0 -0
  25. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/elgamal.py +0 -0
  26. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/key.py +0 -0
  27. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/modmath.py +0 -0
  28. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/rsa.py +0 -0
  29. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/sss.py +0 -0
  30. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/profiler.py +0 -0
  31. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/py.typed +0 -0
  32. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/utils/__init__.py +0 -0
  33. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/utils/base.py +3 -3
  34. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/utils/config.py +3 -3
  35. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/utils/human.py +0 -0
  36. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/utils/logging.py +0 -0
  37. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/utils/saferandom.py +0 -0
  38. {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/utils/stats.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: transcrypto
3
- Version: 2.3.2
3
+ Version: 2.5.0
4
4
  Summary: Basic crypto primitives, not intended for actual use, but as a companion to --Criptografia, Métodos e Algoritmos--
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -19,7 +19,7 @@ Requires-Dist: cryptography (>=46.0)
19
19
  Requires-Dist: gmpy2 (>=2.3)
20
20
  Requires-Dist: platformdirs (>=4.9)
21
21
  Requires-Dist: rich (>=14.3.3)
22
- Requires-Dist: typer (>=0.24)
22
+ Requires-Dist: typer (>=0.24.1)
23
23
  Requires-Dist: zstandard (>=0.25)
24
24
  Project-URL: Changelog, https://github.com/balparda/transcrypto/blob/main/CHANGELOG.md
25
25
  Project-URL: Homepage, https://github.com/balparda/transcrypto
@@ -107,6 +107,7 @@ All that being said, extreme care was taken that this is a good library with a s
107
107
  ## CLI Apps
108
108
 
109
109
  - [TransCrypto/`transcrypto`](transcrypto.md): Does all the operations but allows you to shoot yourself in the foot;
110
+ - [SafeTrans/`safetrans`](safetrans.md): Safer CLI version, if you want to actually use it for security, no raw operations, better defined interface;
110
111
  - [Profiler/`profiler`](profiler.md): Measure transcrypto performance.
111
112
 
112
113
  ## Programming API
@@ -77,6 +77,7 @@ All that being said, extreme care was taken that this is a good library with a s
77
77
  ## CLI Apps
78
78
 
79
79
  - [TransCrypto/`transcrypto`](transcrypto.md): Does all the operations but allows you to shoot yourself in the foot;
80
+ - [SafeTrans/`safetrans`](safetrans.md): Safer CLI version, if you want to actually use it for security, no raw operations, better defined interface;
80
81
  - [Profiler/`profiler`](profiler.md): Measure transcrypto performance.
81
82
 
82
83
  ## Programming API
@@ -12,7 +12,7 @@ build-backend = "poetry.core.masonry.api"
12
12
  [project]
13
13
 
14
14
  name = "transcrypto"
15
- version = "2.3.2" # also update src/transcrypto/__init__.py
15
+ version = "2.5.0" # also update src/transcrypto/__init__.py
16
16
 
17
17
  description = "Basic crypto primitives, not intended for actual use, but as a companion to --Criptografia, Métodos e Algoritmos--"
18
18
  license = "Apache-2.0"
@@ -59,7 +59,7 @@ license-files = [ "LICENSE" ]
59
59
 
60
60
  dependencies = [
61
61
 
62
- "typer>=0.24", # if this changes, also change: [tool.poetry.dependencies]
62
+ "typer>=0.24.1", # if this changes, also change: [tool.poetry.dependencies]
63
63
  "rich>=14.3.3", # 14.3.2 hangs GitHub CI pipeline
64
64
  "platformdirs>=4.9",
65
65
 
@@ -79,6 +79,7 @@ dependencies = [
79
79
  # if you change/add a line here also edit [tool.poetry.scripts] below and re-run `poetry sync`
80
80
 
81
81
  transcrypto = "transcrypto.transcrypto:Run"
82
+ safetrans = "transcrypto.safetrans:Run"
82
83
  profiler = "transcrypto.profiler:Run"
83
84
 
84
85
  ####################################################################################################
@@ -88,6 +89,7 @@ profiler = "transcrypto.profiler:Run"
88
89
  # keep this in sync with [project.scripts] above
89
90
 
90
91
  transcrypto = "transcrypto.transcrypto:Run"
92
+ safetrans = "transcrypto.safetrans:Run"
91
93
  profiler = "transcrypto.profiler:Run"
92
94
 
93
95
  ####################################################################################################
@@ -122,10 +124,10 @@ typer = { version = "^0.24", extras = ["all"] }
122
124
  [tool.poetry.group.dev.dependencies]
123
125
 
124
126
  # basic, all projects should have
125
- ruff = "~0.15.2"
126
- mypy = "~1.19.1"
127
+ ruff = "^0.15.9"
128
+ mypy = "^1.20"
127
129
  pytest = "^9.0"
128
- pytest-cov = "^7.0"
130
+ pytest-cov = "^7.1"
129
131
  pytest-flakefinder = "^1.1"
130
132
  pyright = "^1.1"
131
133
  pre-commit = "^4.5"
@@ -3,5 +3,5 @@
3
3
  """Basic cryptography primitives implementation."""
4
4
 
5
5
  __all__: list[str] = ['__author__', '__version__']
6
- __version__ = '2.3.2' # remember to also update pyproject.toml
6
+ __version__ = '2.5.0' # remember to also update pyproject.toml
7
7
  __author__ = 'Daniel Balparda <balparda@github.com>'
@@ -0,0 +1,258 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Balparda's TransCrypto safe CLI: AES and Hash commands."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import pathlib
8
+ import re
9
+
10
+ import click
11
+ import typer
12
+
13
+ from transcrypto import safetrans
14
+ from transcrypto.cli import clibase
15
+ from transcrypto.core import aes, hashes
16
+ from transcrypto.utils import base
17
+
18
+ _HEX_RE = re.compile(r'^[0-9a-fA-F]+$')
19
+
20
+ # =================================== "HASH" COMMAND ===============================================
21
+
22
+
23
+ hash_app = typer.Typer(
24
+ no_args_is_help=True,
25
+ help='Cryptographic Hashing (SHA-256 / SHA-512 / file).',
26
+ )
27
+ safetrans.app.add_typer(hash_app, name='hash')
28
+
29
+
30
+ @hash_app.command(
31
+ 'sha256',
32
+ help='SHA-256 of input `data`.',
33
+ epilog=(
34
+ 'Example:\n\n\n\n'
35
+ '$ poetry run safetrans -i bin hash sha256 xyz\n\n'
36
+ '3608bca1e44ea6c4d268eb6db02260269892c0b42b86bbf1e77a6fa16c3c9282\n\n'
37
+ '$ poetry run safetrans -i b64 hash sha256 -- eHl6 # "xyz" in base-64\n\n'
38
+ '3608bca1e44ea6c4d268eb6db02260269892c0b42b86bbf1e77a6fa16c3c9282'
39
+ ),
40
+ )
41
+ @clibase.CLIErrorGuard
42
+ def Hash256( # documentation is help/epilog/args # noqa: D103
43
+ *,
44
+ ctx: click.Context,
45
+ data: str = typer.Argument(..., help='Input data (raw text; or `--input-format <hex|b64|bin>`)'),
46
+ ) -> None:
47
+ config: safetrans.TransConfig = ctx.obj
48
+ bt: bytes = safetrans.BytesFromText(data, config.input_format)
49
+ config.console.print(safetrans.BytesToText(hashes.Hash256(bt), config.output_format))
50
+
51
+
52
+ @hash_app.command(
53
+ 'sha512',
54
+ help='SHA-512 of input `data`.',
55
+ epilog=(
56
+ 'Example:\n\n\n\n'
57
+ '$ poetry run safetrans -i bin hash sha512 xyz\n\n'
58
+ '4a3ed8147e37876adc8f76328e5abcc1b470e6acfc18efea0135f983604953a5'
59
+ '8e183c1a6086e91ba3e821d926f5fdeb37761c7ca0328a963f5e92870675b728\n\n'
60
+ '$ poetry run safetrans -i b64 hash sha512 -- eHl6 # "xyz" in base-64\n\n'
61
+ '4a3ed8147e37876adc8f76328e5abcc1b470e6acfc18efea0135f983604953a5'
62
+ '8e183c1a6086e91ba3e821d926f5fdeb37761c7ca0328a963f5e92870675b728'
63
+ ),
64
+ )
65
+ @clibase.CLIErrorGuard
66
+ def Hash512( # documentation is help/epilog/args # noqa: D103
67
+ *,
68
+ ctx: click.Context,
69
+ data: str = typer.Argument(..., help='Input data (raw text; or `--input-format <hex|b64|bin>`)'),
70
+ ) -> None:
71
+ config: safetrans.TransConfig = ctx.obj
72
+ bt: bytes = safetrans.BytesFromText(data, config.input_format)
73
+ config.console.print(safetrans.BytesToText(hashes.Hash512(bt), config.output_format))
74
+
75
+
76
+ @hash_app.command(
77
+ 'file',
78
+ help='SHA-256/512 hash of file contents, defaulting to SHA-256.',
79
+ epilog=(
80
+ 'Example:\n\n\n\n'
81
+ '$ poetry run safetrans hash file /etc/passwd --digest sha512\n\n'
82
+ '8966f5953e79f55dfe34d3dc5b160ac4a4a3f9cbd1c36695a54e28d77c7874df'
83
+ 'f8595502f8a420608911b87d336d9e83c890f0e7ec11a76cb10b03e757f78aea'
84
+ ),
85
+ )
86
+ @clibase.CLIErrorGuard
87
+ def HashFile( # documentation is help/epilog/args # noqa: D103
88
+ *,
89
+ ctx: click.Context,
90
+ path: pathlib.Path = typer.Argument( # noqa: B008
91
+ ...,
92
+ exists=True,
93
+ file_okay=True,
94
+ dir_okay=False,
95
+ readable=True,
96
+ resolve_path=True,
97
+ help='Path to existing file',
98
+ ),
99
+ digest: str = typer.Option(
100
+ 'sha256',
101
+ '-d',
102
+ '--digest',
103
+ click_type=click.Choice(['sha256', 'sha512'], case_sensitive=False),
104
+ help='Digest type, SHA-256 ("sha256") or SHA-512 ("sha512")',
105
+ ),
106
+ ) -> None:
107
+ config: safetrans.TransConfig = ctx.obj
108
+ config.console.print(
109
+ safetrans.BytesToText(hashes.FileHash(str(path), digest=digest), config.output_format)
110
+ )
111
+
112
+
113
+ # =================================== "AES" COMMAND ================================================
114
+
115
+
116
+ aes_app = typer.Typer(
117
+ no_args_is_help=True,
118
+ help=(
119
+ 'AES-256 operations (GCM/ECB) and key derivation. '
120
+ 'No measures are taken here to prevent timing attacks.'
121
+ ),
122
+ )
123
+ safetrans.app.add_typer(aes_app, name='aes')
124
+
125
+
126
+ @aes_app.command(
127
+ 'key',
128
+ help=(
129
+ 'Derive key from a password (PBKDF2-HMAC-SHA256) with custom expensive '
130
+ 'salt and iterations. Very good/safe for simple password-to-key but not for '
131
+ 'passwords databases (because of constant salt).'
132
+ ),
133
+ epilog=(
134
+ 'Example:\n\n\n\n'
135
+ '$ poetry run safetrans -o b64 aes key "correct horse battery staple"\n\n'
136
+ 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es=\n\n' # cspell:disable-line
137
+ '$ poetry run safetrans -p keyfile.out --protect hunter aes key '
138
+ '"correct horse battery staple"\n\n'
139
+ "AES key saved to 'keyfile.out'"
140
+ ),
141
+ )
142
+ @clibase.CLIErrorGuard
143
+ def AESKeyFromPass( # documentation is help/epilog/args # noqa: D103
144
+ *,
145
+ ctx: click.Context,
146
+ password: str = typer.Argument(..., help='Password (leading/trailing spaces ignored)'),
147
+ ) -> None:
148
+ config: safetrans.TransConfig = ctx.obj
149
+ aes_key: aes.AESKey = aes.AESKey.FromStaticPassword(password)
150
+ if config.key_path is not None:
151
+ safetrans.SaveObj(aes_key, str(config.key_path), config.protect)
152
+ config.console.print(f'AES key saved to {str(config.key_path)!r}')
153
+ else:
154
+ config.console.print(safetrans.BytesToText(aes_key.key256, config.output_format))
155
+
156
+
157
+ @aes_app.command(
158
+ 'encrypt',
159
+ help=(
160
+ 'AES-256-GCM: safely encrypt `plaintext` with `-k`/`--key` or with '
161
+ '`-p`/`--key-path` keyfile. All inputs are raw, or you '
162
+ 'can use `--input-format <hex|b64|bin>`. Attention: if you provide `-a`/`--aad` '
163
+ '(associated data, AAD), you will need to provide the same AAD when decrypting '
164
+ 'and it is NOT included in the `ciphertext`/CT returned by this method!'
165
+ ),
166
+ epilog=(
167
+ 'Example:\n\n\n\n'
168
+ '$ poetry run safetrans -i b64 -o b64 aes encrypt -k '
169
+ 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= -- AAAAAAB4eXo=\n\n' # cspell:disable-line
170
+ 'F2_ZLrUw5Y8oDnbTP5t5xCUWX8WtVILLD0teyUi_37_4KHeV-YowVA==\n\n' # cspell:disable-line
171
+ '$ poetry run safetrans -i b64 -o b64 aes encrypt -k '
172
+ 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= -a eHl6 -- AAAAAAB4eXo=\n\n' # cspell:disable-line
173
+ 'xOlAHPUPpeyZHId-f3VQ_QKKMxjIW0_FBo9WOfIBrzjn0VkVV6xTRA==' # cspell:disable-line
174
+ ),
175
+ )
176
+ @clibase.CLIErrorGuard
177
+ def AESEncrypt( # documentation is help/epilog/args # noqa: D103
178
+ *,
179
+ ctx: click.Context,
180
+ plaintext: str = typer.Argument(..., help='Input data to encrypt (PT)'),
181
+ key: str | None = typer.Option(
182
+ None, '-k', '--key', help="Key if `-p`/`--key-path` wasn't used (32 bytes)"
183
+ ),
184
+ aad: str = typer.Option(
185
+ '',
186
+ '-a',
187
+ '--aad',
188
+ help='Associated data (optional; has to be separately sent to receiver/stored)',
189
+ ),
190
+ ) -> None:
191
+ config: safetrans.TransConfig = ctx.obj
192
+ aes_key: aes.AESKey
193
+ if key:
194
+ key_bytes: bytes = safetrans.BytesFromText(key, config.input_format)
195
+ if len(key_bytes) != 32: # noqa: PLR2004
196
+ raise base.InputError(f'invalid AES key size: {len(key_bytes)} bytes (expected 32)')
197
+ aes_key = aes.AESKey(key256=key_bytes)
198
+ elif config.key_path is not None:
199
+ aes_key = safetrans.LoadObj(str(config.key_path), config.protect, aes.AESKey)
200
+ else:
201
+ raise base.InputError('provide -k/--key or -p/--key-path')
202
+ aad_bytes: bytes | None = safetrans.BytesFromText(aad, config.input_format) if aad else None
203
+ pt: bytes = safetrans.BytesFromText(plaintext, config.input_format)
204
+ ct: bytes = aes_key.Encrypt(pt, associated_data=aad_bytes)
205
+ config.console.print(safetrans.BytesToText(ct, config.output_format))
206
+
207
+
208
+ @aes_app.command(
209
+ 'decrypt',
210
+ help=(
211
+ 'AES-256-GCM: safely decrypt `ciphertext` with `-k`/`--key` or with '
212
+ '`-p`/`--key-path` keyfile. All inputs are raw, or you '
213
+ 'can use `--input-format <hex|b64|bin>`. Attention: if you provided `-a`/`--aad` '
214
+ '(associated data, AAD) during encryption, you will need to provide the same AAD now!'
215
+ ),
216
+ epilog=(
217
+ 'Example:\n\n\n\n'
218
+ '$ poetry run safetrans -i b64 -o b64 aes decrypt -k '
219
+ 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= -- ' # cspell:disable-line
220
+ 'F2_ZLrUw5Y8oDnbTP5t5xCUWX8WtVILLD0teyUi_37_4KHeV-YowVA==\n\n' # cspell:disable-line
221
+ 'AAAAAAB4eXo=\n\n' # cspell:disable-line
222
+ '$ poetry run safetrans -i b64 -o b64 aes decrypt -k '
223
+ 'DbWJ_ZrknLEEIoq_NpoCQwHYfjskGokpueN2O_eY0es= -a eHl6 -- ' # cspell:disable-line
224
+ 'xOlAHPUPpeyZHId-f3VQ_QKKMxjIW0_FBo9WOfIBrzjn0VkVV6xTRA==\n\n' # cspell:disable-line
225
+ 'AAAAAAB4eXo=' # cspell:disable-line
226
+ ),
227
+ )
228
+ @clibase.CLIErrorGuard
229
+ def AESDecrypt( # documentation is help/epilog/args # noqa: D103
230
+ *,
231
+ ctx: click.Context,
232
+ ciphertext: str = typer.Argument(..., help='Input data to decrypt (CT)'),
233
+ key: str | None = typer.Option(
234
+ None, '-k', '--key', help="Key if `-p`/`--key-path` wasn't used (32 bytes)"
235
+ ),
236
+ aad: str = typer.Option(
237
+ '',
238
+ '-a',
239
+ '--aad',
240
+ help='Associated data (optional; has to be exactly the same as used during encryption)',
241
+ ),
242
+ ) -> None:
243
+ config: safetrans.TransConfig = ctx.obj
244
+ aes_key: aes.AESKey
245
+ if key:
246
+ key_bytes: bytes = safetrans.BytesFromText(key, config.input_format)
247
+ if len(key_bytes) != 32: # noqa: PLR2004
248
+ raise base.InputError(f'invalid AES key size: {len(key_bytes)} bytes (expected 32)')
249
+ aes_key = aes.AESKey(key256=key_bytes)
250
+ elif config.key_path is not None:
251
+ aes_key = safetrans.LoadObj(str(config.key_path), config.protect, aes.AESKey)
252
+ else:
253
+ raise base.InputError('provide -k/--key or -p/--key-path')
254
+ # associated data, if any
255
+ aad_bytes: bytes | None = safetrans.BytesFromText(aad, config.input_format) if aad else None
256
+ ct: bytes = safetrans.BytesFromText(ciphertext, config.input_format)
257
+ pt: bytes = aes_key.Decrypt(ct, associated_data=aad_bytes)
258
+ config.console.print(safetrans.BytesToText(pt, config.output_format))
@@ -0,0 +1,220 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Balparda's TransCrypto safe CLI: Bid secret and SSS commands."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import glob
8
+
9
+ import click
10
+ import typer
11
+
12
+ from transcrypto import safetrans
13
+ from transcrypto.cli import clibase
14
+ from transcrypto.core import bid, sss
15
+ from transcrypto.utils import base
16
+
17
+ # ================================== "BID" COMMAND =================================================
18
+
19
+
20
+ bid_app = typer.Typer(
21
+ no_args_is_help=True,
22
+ help=(
23
+ 'Bidding on a `secret` so that you can cryptographically convince a neutral '
24
+ 'party that the `secret` that was committed to previously was not changed. '
25
+ 'All methods require file key(s) as `-p`/`--key-path` (see provided examples). '
26
+ 'All non-int inputs are raw, or you can use `--input-format <hex|b64|bin>`. '
27
+ 'No measures are taken here to prevent timing attacks.'
28
+ ),
29
+ )
30
+ safetrans.app.add_typer(bid_app, name='bid')
31
+
32
+
33
+ @bid_app.command(
34
+ 'new',
35
+ help=('Generate the bid files for `secret`.'),
36
+ epilog=(
37
+ 'Example:\n\n\n\n'
38
+ '$ poetry run safetrans -i bin -p my-bid bid new "tomorrow it will rain"\n\n'
39
+ "Bid private/public commitments saved to 'my-bid.priv/.pub'"
40
+ ),
41
+ )
42
+ @clibase.CLIErrorGuard
43
+ def BidNew( # documentation is help/epilog/args # noqa: D103
44
+ *,
45
+ ctx: click.Context,
46
+ secret: str = typer.Argument(..., help='Input data to bid to, the protected "secret"'),
47
+ ) -> None:
48
+ config: safetrans.TransConfig = ctx.obj
49
+ base_path: str = safetrans.RequireKeyPath(config, 'bid')
50
+ secret_bytes: bytes = safetrans.BytesFromText(secret, config.input_format)
51
+ bid_priv: bid.PrivateBid512 = bid.PrivateBid512.New(secret_bytes)
52
+ bid_pub: bid.PublicBid512 = bid.PublicBid512.Copy(bid_priv)
53
+ safetrans.SaveObj(bid_priv, base_path + '.priv', config.protect)
54
+ safetrans.SaveObj(bid_pub, base_path + '.pub', config.protect)
55
+ config.console.print(f'Bid private/public commitments saved to {base_path + ".priv/.pub"!r}')
56
+
57
+
58
+ @bid_app.command(
59
+ 'verify',
60
+ help=('Verify the bid files for correctness and reveal the `secret`.'),
61
+ epilog=(
62
+ 'Example:\n\n\n\n'
63
+ '$ poetry run safetrans -o bin -p my-bid bid verify\n\n'
64
+ 'Bid commitment: OK\n\n'
65
+ 'Bid secret:\n\n'
66
+ 'tomorrow it will rain'
67
+ ),
68
+ )
69
+ @clibase.CLIErrorGuard
70
+ def BidVerify(*, ctx: click.Context) -> None: # documentation is help/epilog/args # noqa: D103
71
+ config: safetrans.TransConfig = ctx.obj
72
+ base_path: str = safetrans.RequireKeyPath(config, 'bid')
73
+ bid_priv: bid.PrivateBid512 = safetrans.LoadObj(
74
+ base_path + '.priv', config.protect, bid.PrivateBid512
75
+ )
76
+ bid_pub: bid.PublicBid512 = safetrans.LoadObj(
77
+ base_path + '.pub', config.protect, bid.PublicBid512
78
+ )
79
+ bid_pub_expect: bid.PublicBid512 = bid.PublicBid512.Copy(bid_priv)
80
+ config.console.print(
81
+ 'Bid commitment: '
82
+ + (
83
+ '[green]OK[/]'
84
+ if (
85
+ bid_pub.VerifyBid(bid_priv.private_key, bid_priv.secret_bid) and bid_pub == bid_pub_expect
86
+ )
87
+ else '[red]INVALID[/]'
88
+ )
89
+ )
90
+ config.console.print('Bid secret:')
91
+ config.console.print(safetrans.BytesToText(bid_priv.secret_bid, config.output_format))
92
+
93
+
94
+ # ================================== "SSS" COMMAND =================================================
95
+
96
+
97
+ sss_app = typer.Typer(
98
+ no_args_is_help=True,
99
+ help=(
100
+ 'SSS (Shamir Shared Secret) secret sharing crypto scheme. '
101
+ 'All methods require file key(s) as `-p`/`--key-path` (see provided examples). '
102
+ 'All non-int inputs are raw, or you can use `--input-format <hex|b64|bin>`. '
103
+ 'No measures are taken here to prevent timing attacks.'
104
+ ),
105
+ )
106
+ safetrans.app.add_typer(sss_app, name='sss')
107
+
108
+
109
+ @sss_app.command(
110
+ 'new',
111
+ help=(
112
+ 'Generate the private keys with `bits` prime modulus size and so that at least a '
113
+ '`minimum` number of shares are needed to recover the secret. '
114
+ 'This key will be used to generate the shares later (with the `shares` command).'
115
+ ),
116
+ epilog=(
117
+ 'Example:\n\n\n\n'
118
+ '$ poetry run safetrans -p sss-key sss new 3 --bits 64 '
119
+ '# NEVER use such a small key: example only!\n\n'
120
+ "SSS private/public keys saved to 'sss-key.priv/.pub'"
121
+ ),
122
+ )
123
+ @clibase.CLIErrorGuard
124
+ def SSSNew( # documentation is help/epilog/args # noqa: D103
125
+ *,
126
+ ctx: click.Context,
127
+ minimum: int = typer.Argument(
128
+ ..., min=2, help='Minimum number of shares required to recover secret, ≥ 2'
129
+ ),
130
+ bits: int = typer.Option(
131
+ 1024,
132
+ '-b',
133
+ '--bits',
134
+ min=16,
135
+ help=(
136
+ 'Prime modulus (`p`) size in bits, ≥16; the default (1024) is a safe size ***IFF*** you '
137
+ 'are protecting symmetric keys; the number of bits should be comfortably larger '
138
+ 'than the size of the secret you want to protect with this scheme'
139
+ ),
140
+ ),
141
+ ) -> None:
142
+ config: safetrans.TransConfig = ctx.obj
143
+ base_path: str = safetrans.RequireKeyPath(config, 'sss')
144
+ sss_priv: sss.ShamirSharedSecretPrivate = sss.ShamirSharedSecretPrivate.New(minimum, bits)
145
+ sss_pub: sss.ShamirSharedSecretPublic = sss.ShamirSharedSecretPublic.Copy(sss_priv)
146
+ safetrans.SaveObj(sss_priv, base_path + '.priv', config.protect)
147
+ safetrans.SaveObj(sss_pub, base_path + '.pub', config.protect)
148
+ config.console.print(f'SSS private/public keys saved to {base_path + ".priv/.pub"!r}')
149
+
150
+
151
+ @sss_app.command(
152
+ 'shares',
153
+ help='Shares: Issue `count` private shares for a `secret`.',
154
+ epilog=(
155
+ 'Example:\n\n\n\n'
156
+ '$ poetry run safetrans -i bin -p sss-key sss shares "abcde" 5\n\n'
157
+ "SSS 5 individual (private) shares saved to 'sss-key.share.1…5'\n\n"
158
+ '$ rm sss-key.share.2 sss-key.share.4 # this is to simulate only having shares 1,3,5'
159
+ ),
160
+ )
161
+ @clibase.CLIErrorGuard
162
+ def SSSShares( # documentation is help/epilog/args # noqa: D103
163
+ *,
164
+ ctx: click.Context,
165
+ secret: str = typer.Argument(..., help='Secret to be protected'),
166
+ count: int = typer.Argument(
167
+ ...,
168
+ help=(
169
+ 'How many shares to produce; must be ≥ `minimum` used in `new` command or else the '
170
+ '`secret` would become unrecoverable'
171
+ ),
172
+ ),
173
+ ) -> None:
174
+ config: safetrans.TransConfig = ctx.obj
175
+ base_path: str = safetrans.RequireKeyPath(config, 'sss')
176
+ sss_priv: sss.ShamirSharedSecretPrivate = safetrans.LoadObj(
177
+ base_path + '.priv', config.protect, sss.ShamirSharedSecretPrivate
178
+ )
179
+ if count < sss_priv.minimum:
180
+ raise base.InputError(
181
+ f'count ({count}) must be >= minimum ({sss_priv.minimum}) to allow secret recovery'
182
+ )
183
+ pt: bytes = safetrans.BytesFromText(secret, config.input_format)
184
+ for i, data_share in enumerate(sss_priv.MakeDataShares(pt, count)):
185
+ safetrans.SaveObj(data_share, f'{base_path}.share.{i + 1}', config.protect)
186
+ config.console.print(
187
+ f'SSS {count} individual (private) shares saved to {base_path + ".share.1…" + str(count)!r}'
188
+ )
189
+
190
+
191
+ @sss_app.command(
192
+ 'recover',
193
+ help='Recover secret from shares; will use any available shares that were found.',
194
+ epilog=(
195
+ 'Example:\n\n\n\n'
196
+ '$ poetry run safetrans -o bin -p sss-key sss recover\n\n'
197
+ "Loaded SSS share: 'sss-key.share.3'\n\n"
198
+ "Loaded SSS share: 'sss-key.share.5'\n\n"
199
+ "Loaded SSS share: 'sss-key.share.1' # using only 3 shares: number 2/4 are missing\n\n"
200
+ 'Secret:\n\n'
201
+ 'abcde'
202
+ ),
203
+ )
204
+ @clibase.CLIErrorGuard
205
+ def SSSRecover(*, ctx: click.Context) -> None: # documentation is help/epilog/args # noqa: D103
206
+ config: safetrans.TransConfig = ctx.obj
207
+ base_path: str = safetrans.RequireKeyPath(config, 'sss')
208
+ subset: list[sss.ShamirSharePrivate] = []
209
+ data_share: sss.ShamirShareData | None = None
210
+ for fname in glob.glob(base_path + '.share.*'): # noqa: PTH207
211
+ share: sss.ShamirSharePrivate = safetrans.LoadObj(fname, config.protect, sss.ShamirSharePrivate)
212
+ subset.append(share)
213
+ if isinstance(share, sss.ShamirShareData):
214
+ data_share = share
215
+ config.console.print(f'Loaded SSS share: {fname!r}')
216
+ if data_share is None:
217
+ raise base.InputError('no data share found among the available shares')
218
+ pt: bytes = data_share.RecoverData(subset)
219
+ config.console.print('Secret:')
220
+ config.console.print(safetrans.BytesToText(pt, config.output_format))
@@ -0,0 +1,89 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Balparda's TransCrypto safe CLI: Integer mathematics commands."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import click
8
+ import typer
9
+
10
+ from transcrypto import safetrans
11
+ from transcrypto.cli import clibase
12
+ from transcrypto.core import modmath
13
+ from transcrypto.utils import saferandom
14
+
15
+ # ================================= "RANDOM" COMMAND ===============================================
16
+
17
+
18
+ random_app = typer.Typer(
19
+ no_args_is_help=True,
20
+ help='Cryptographically secure randomness, from the OS CSPRNG.',
21
+ )
22
+ safetrans.app.add_typer(random_app, name='random')
23
+
24
+
25
+ @random_app.command(
26
+ 'bits',
27
+ help='Random integer with exact bit length = `bits` (MSB will be 1).',
28
+ epilog=('Example:\n\n\n\n$ poetry run safetrans random bits 16\n\n36650'),
29
+ )
30
+ @clibase.CLIErrorGuard
31
+ def RandomBits( # documentation is help/epilog/args # noqa: D103
32
+ *,
33
+ ctx: click.Context,
34
+ bits: int = typer.Argument(..., min=8, help='Number of bits, ≥ 8'),
35
+ ) -> None:
36
+ config: safetrans.TransConfig = ctx.obj
37
+ config.console.print(saferandom.RandBits(bits))
38
+
39
+
40
+ @random_app.command(
41
+ 'int',
42
+ help='Uniform random integer in `[min, max]` range, inclusive.',
43
+ epilog=('Example:\n\n\n\n$ poetry run safetrans random int 1000 2000\n\n1628'),
44
+ )
45
+ @clibase.CLIErrorGuard
46
+ def RandomInt( # documentation is help/epilog/args # noqa: D103
47
+ *,
48
+ ctx: click.Context,
49
+ min_: str = typer.Argument(..., help='Minimum, ≥ 0'),
50
+ max_: str = typer.Argument(..., help='Maximum, > `min`'),
51
+ ) -> None:
52
+ config: safetrans.TransConfig = ctx.obj
53
+ min_i: int = safetrans.ParseInt(min_, min_value=0)
54
+ max_i: int = safetrans.ParseInt(max_, min_value=min_i + 1)
55
+ config.console.print(saferandom.RandInt(min_i, max_i))
56
+
57
+
58
+ @random_app.command(
59
+ 'bytes',
60
+ help='Generates `n` cryptographically secure random bytes.',
61
+ epilog=(
62
+ 'Example:\n\n\n\n'
63
+ '$ poetry run safetrans random bytes 32\n\n'
64
+ '6c6f1f88cb93c4323285a2224373d6e59c72a9c2b82e20d1c376df4ffbe9507f'
65
+ ),
66
+ )
67
+ @clibase.CLIErrorGuard
68
+ def RandomBytes( # documentation is help/epilog/args # noqa: D103
69
+ *,
70
+ ctx: click.Context,
71
+ n: int = typer.Argument(..., min=1, help='Number of bytes, ≥ 1'),
72
+ ) -> None:
73
+ config: safetrans.TransConfig = ctx.obj
74
+ config.console.print(safetrans.BytesToText(saferandom.RandBytes(n), config.output_format))
75
+
76
+
77
+ @random_app.command(
78
+ 'prime',
79
+ help='Generate a random prime with exact bit length = `bits` (MSB will be 1).',
80
+ epilog=('Example:\n\n\n\n$ poetry run safetrans random prime 32\n\n2365910551'),
81
+ )
82
+ @clibase.CLIErrorGuard
83
+ def RandomPrime( # documentation is help/epilog/args # noqa: D103
84
+ *,
85
+ ctx: click.Context,
86
+ bits: int = typer.Argument(..., min=11, help='Bit length, ≥ 11'),
87
+ ) -> None:
88
+ config: safetrans.TransConfig = ctx.obj
89
+ config.console.print(modmath.NBitRandomPrimes(bits).pop())