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.
- {transcrypto-2.3.2 → transcrypto-2.5.0}/PKG-INFO +3 -2
- {transcrypto-2.3.2 → transcrypto-2.5.0}/README.md +1 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/pyproject.toml +7 -5
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/__init__.py +1 -1
- transcrypto-2.5.0/src/transcrypto/cli/safeaeshash.py +258 -0
- transcrypto-2.5.0/src/transcrypto/cli/safebidsecret.py +220 -0
- transcrypto-2.5.0/src/transcrypto/cli/safeintmath.py +89 -0
- transcrypto-2.5.0/src/transcrypto/cli/safepublicalgos.py +345 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/hashes.py +3 -3
- transcrypto-2.5.0/src/transcrypto/safetrans.py +429 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/transcrypto.py +10 -91
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/utils/timer.py +44 -4
- {transcrypto-2.3.2 → transcrypto-2.5.0}/LICENSE +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/cli/__init__.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/cli/aeshash.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/cli/bidsecret.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/cli/clibase.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/cli/intmath.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/cli/publicalgos.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/__init__.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/aes.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/bid.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/constants.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/dsa.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/elgamal.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/key.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/modmath.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/rsa.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/core/sss.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/profiler.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/py.typed +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/utils/__init__.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/utils/base.py +3 -3
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/utils/config.py +3 -3
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/utils/human.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/utils/logging.py +0 -0
- {transcrypto-2.3.2 → transcrypto-2.5.0}/src/transcrypto/utils/saferandom.py +0 -0
- {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
|
+
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.
|
|
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 = "
|
|
126
|
-
mypy = "
|
|
127
|
+
ruff = "^0.15.9"
|
|
128
|
+
mypy = "^1.20"
|
|
127
129
|
pytest = "^9.0"
|
|
128
|
-
pytest-cov = "^7.
|
|
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.
|
|
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())
|