transcrypto 1.8.0__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- transcrypto/__init__.py +1 -1
- transcrypto/cli/aeshash.py +6 -4
- transcrypto/cli/bidsecret.py +10 -8
- transcrypto/cli/clibase.py +12 -132
- transcrypto/cli/intmath.py +8 -6
- transcrypto/cli/publicalgos.py +2 -1
- transcrypto/core/__init__.py +3 -0
- transcrypto/{aes.py → core/aes.py} +17 -29
- transcrypto/core/bid.py +161 -0
- transcrypto/{dsa.py → core/dsa.py} +28 -27
- transcrypto/{elgamal.py → core/elgamal.py} +33 -32
- transcrypto/core/hashes.py +96 -0
- transcrypto/core/key.py +735 -0
- transcrypto/{modmath.py → core/modmath.py} +91 -17
- transcrypto/{rsa.py → core/rsa.py} +51 -50
- transcrypto/{sss.py → core/sss.py} +27 -26
- transcrypto/profiler.py +21 -8
- transcrypto/transcrypto.py +24 -14
- transcrypto/utils/__init__.py +3 -0
- transcrypto/utils/base.py +72 -0
- transcrypto/utils/human.py +278 -0
- transcrypto/utils/logging.py +139 -0
- transcrypto/utils/saferandom.py +102 -0
- transcrypto/utils/stats.py +360 -0
- transcrypto/utils/timer.py +175 -0
- {transcrypto-1.8.0.dist-info → transcrypto-2.0.0.dist-info}/METADATA +100 -100
- transcrypto-2.0.0.dist-info/RECORD +33 -0
- transcrypto/base.py +0 -1637
- transcrypto-1.8.0.dist-info/RECORD +0 -23
- /transcrypto/{constants.py → core/constants.py} +0 -0
- {transcrypto-1.8.0.dist-info → transcrypto-2.0.0.dist-info}/WHEEL +0 -0
- {transcrypto-1.8.0.dist-info → transcrypto-2.0.0.dist-info}/entry_points.txt +0 -0
- {transcrypto-1.8.0.dist-info → transcrypto-2.0.0.dist-info}/licenses/LICENSE +0 -0
transcrypto/__init__.py
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
"""Basic cryptography primitives implementation."""
|
|
4
4
|
|
|
5
5
|
__all__: list[str] = ['__author__', '__version__']
|
|
6
|
-
__version__ = '
|
|
6
|
+
__version__ = '2.0.0' # remember to also update pyproject.toml
|
|
7
7
|
__author__ = 'Daniel Balparda <balparda@github.com>'
|
transcrypto/cli/aeshash.py
CHANGED
|
@@ -10,8 +10,10 @@ import re
|
|
|
10
10
|
import click
|
|
11
11
|
import typer
|
|
12
12
|
|
|
13
|
-
from transcrypto import
|
|
13
|
+
from transcrypto import transcrypto
|
|
14
14
|
from transcrypto.cli import clibase
|
|
15
|
+
from transcrypto.core import aes, hashes
|
|
16
|
+
from transcrypto.utils import base
|
|
15
17
|
|
|
16
18
|
_HEX_RE = re.compile(r'^[0-9a-fA-F]+$')
|
|
17
19
|
|
|
@@ -44,7 +46,7 @@ def Hash256( # documentation is help/epilog/args # noqa: D103
|
|
|
44
46
|
) -> None:
|
|
45
47
|
config: transcrypto.TransConfig = ctx.obj
|
|
46
48
|
bt: bytes = transcrypto.BytesFromText(data, config.input_format)
|
|
47
|
-
config.console.print(transcrypto.BytesToText(
|
|
49
|
+
config.console.print(transcrypto.BytesToText(hashes.Hash256(bt), config.output_format))
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
@hash_app.command(
|
|
@@ -68,7 +70,7 @@ def Hash512( # documentation is help/epilog/args # noqa: D103
|
|
|
68
70
|
) -> None:
|
|
69
71
|
config: transcrypto.TransConfig = ctx.obj
|
|
70
72
|
bt: bytes = transcrypto.BytesFromText(data, config.input_format)
|
|
71
|
-
config.console.print(transcrypto.BytesToText(
|
|
73
|
+
config.console.print(transcrypto.BytesToText(hashes.Hash512(bt), config.output_format))
|
|
72
74
|
|
|
73
75
|
|
|
74
76
|
@hash_app.command(
|
|
@@ -104,7 +106,7 @@ def HashFile( # documentation is help/epilog/args # noqa: D103
|
|
|
104
106
|
) -> None:
|
|
105
107
|
config: transcrypto.TransConfig = ctx.obj
|
|
106
108
|
config.console.print(
|
|
107
|
-
transcrypto.BytesToText(
|
|
109
|
+
transcrypto.BytesToText(hashes.FileHash(str(path), digest=digest), config.output_format)
|
|
108
110
|
)
|
|
109
111
|
|
|
110
112
|
|
transcrypto/cli/bidsecret.py
CHANGED
|
@@ -8,8 +8,10 @@ import glob
|
|
|
8
8
|
|
|
9
9
|
import typer
|
|
10
10
|
|
|
11
|
-
from transcrypto import
|
|
11
|
+
from transcrypto import transcrypto
|
|
12
12
|
from transcrypto.cli import clibase
|
|
13
|
+
from transcrypto.core import bid, sss
|
|
14
|
+
from transcrypto.utils import base
|
|
13
15
|
|
|
14
16
|
# ================================== "BID" COMMAND =================================================
|
|
15
17
|
|
|
@@ -45,8 +47,8 @@ def BidNew( # documentation is help/epilog/args # noqa: D103
|
|
|
45
47
|
config: transcrypto.TransConfig = ctx.obj
|
|
46
48
|
base_path: str = transcrypto.RequireKeyPath(config, 'bid')
|
|
47
49
|
secret_bytes: bytes = transcrypto.BytesFromText(secret, config.input_format)
|
|
48
|
-
bid_priv:
|
|
49
|
-
bid_pub:
|
|
50
|
+
bid_priv: bid.PrivateBid512 = bid.PrivateBid512.New(secret_bytes)
|
|
51
|
+
bid_pub: bid.PublicBid512 = bid.PublicBid512.Copy(bid_priv)
|
|
50
52
|
transcrypto.SaveObj(bid_priv, base_path + '.priv', config.protect)
|
|
51
53
|
transcrypto.SaveObj(bid_pub, base_path + '.pub', config.protect)
|
|
52
54
|
config.console.print(f'Bid private/public commitments saved to {base_path + ".priv/.pub"!r}')
|
|
@@ -67,13 +69,13 @@ def BidNew( # documentation is help/epilog/args # noqa: D103
|
|
|
67
69
|
def BidVerify(*, ctx: typer.Context) -> None: # documentation is help/epilog/args # noqa: D103
|
|
68
70
|
config: transcrypto.TransConfig = ctx.obj
|
|
69
71
|
base_path: str = transcrypto.RequireKeyPath(config, 'bid')
|
|
70
|
-
bid_priv:
|
|
71
|
-
base_path + '.priv', config.protect,
|
|
72
|
+
bid_priv: bid.PrivateBid512 = transcrypto.LoadObj(
|
|
73
|
+
base_path + '.priv', config.protect, bid.PrivateBid512
|
|
72
74
|
)
|
|
73
|
-
bid_pub:
|
|
74
|
-
base_path + '.pub', config.protect,
|
|
75
|
+
bid_pub: bid.PublicBid512 = transcrypto.LoadObj(
|
|
76
|
+
base_path + '.pub', config.protect, bid.PublicBid512
|
|
75
77
|
)
|
|
76
|
-
bid_pub_expect:
|
|
78
|
+
bid_pub_expect: bid.PublicBid512 = bid.PublicBid512.Copy(bid_priv)
|
|
77
79
|
config.console.print(
|
|
78
80
|
'Bid commitment: '
|
|
79
81
|
+ (
|
transcrypto/cli/clibase.py
CHANGED
|
@@ -7,8 +7,6 @@ from __future__ import annotations
|
|
|
7
7
|
import dataclasses
|
|
8
8
|
import functools
|
|
9
9
|
import logging
|
|
10
|
-
import os
|
|
11
|
-
import threading
|
|
12
10
|
from collections import abc
|
|
13
11
|
from typing import cast
|
|
14
12
|
|
|
@@ -16,139 +14,21 @@ import click
|
|
|
16
14
|
import typer
|
|
17
15
|
from click import testing as click_testing
|
|
18
16
|
from rich import console as rich_console
|
|
19
|
-
from rich import logging as rich_logging
|
|
20
17
|
|
|
21
|
-
from transcrypto import base
|
|
18
|
+
from transcrypto.utils import base
|
|
19
|
+
from transcrypto.utils import logging as tc_logging
|
|
22
20
|
|
|
23
|
-
# Logging
|
|
24
|
-
_LOG_FORMAT_NO_PROCESS: str = '%(funcName)s: %(message)s'
|
|
25
|
-
_LOG_FORMAT_WITH_PROCESS: str = '%(processName)s/' + _LOG_FORMAT_NO_PROCESS
|
|
26
|
-
_LOG_FORMAT_DATETIME: str = '[%Y%m%d-%H:%M:%S]' # e.g., [20240131-13:45:30]
|
|
27
|
-
_LOG_LEVELS: dict[int, int] = {
|
|
28
|
-
0: logging.ERROR,
|
|
29
|
-
1: logging.WARNING,
|
|
30
|
-
2: logging.INFO,
|
|
31
|
-
3: logging.DEBUG,
|
|
32
|
-
}
|
|
33
|
-
_LOG_COMMON_PROVIDERS: set[str] = {
|
|
34
|
-
'werkzeug',
|
|
35
|
-
'gunicorn.error',
|
|
36
|
-
'gunicorn.access',
|
|
37
|
-
'uvicorn',
|
|
38
|
-
'uvicorn.error',
|
|
39
|
-
'uvicorn.access',
|
|
40
|
-
'django.server',
|
|
41
|
-
}
|
|
42
21
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def Console() -> rich_console.Console:
|
|
48
|
-
"""Get the global console instance.
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
rich.console.Console: The global console instance.
|
|
52
|
-
|
|
53
|
-
"""
|
|
54
|
-
with __console_lock:
|
|
55
|
-
if __console_singleton is None:
|
|
56
|
-
return rich_console.Console() # fallback console if InitLogging hasn't been called yet
|
|
57
|
-
return __console_singleton
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def ResetConsole() -> None:
|
|
61
|
-
"""Reset the global console instance."""
|
|
62
|
-
global __console_singleton # noqa: PLW0603
|
|
63
|
-
with __console_lock:
|
|
64
|
-
__console_singleton = None
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def InitLogging(
|
|
68
|
-
verbosity: int,
|
|
69
|
-
/,
|
|
70
|
-
*,
|
|
71
|
-
include_process: bool = False,
|
|
72
|
-
soft_wrap: bool = False,
|
|
73
|
-
color: bool | None = False,
|
|
74
|
-
) -> tuple[rich_console.Console, int, bool]:
|
|
75
|
-
"""Initialize logger (with RichHandler) and get a rich.console.Console singleton.
|
|
76
|
-
|
|
77
|
-
This method will also return the actual decided values for verbosity and color use.
|
|
78
|
-
If you have a CLI app that uses this, its pytests should call `ResetConsole()` in a fixture, like:
|
|
79
|
-
|
|
80
|
-
from transcrypto import logging
|
|
81
|
-
@pytest.fixture(autouse=True)
|
|
82
|
-
def _reset_base_logging() -> Generator[None, None, None]: # type: ignore
|
|
83
|
-
logging.ResetConsole()
|
|
84
|
-
yield # stop
|
|
85
|
-
|
|
86
|
-
Args:
|
|
87
|
-
verbosity (int): Logging verbosity level: 0==ERROR, 1==WARNING, 2==INFO, 3==DEBUG
|
|
88
|
-
include_process (bool, optional): Whether to include process name in log output.
|
|
89
|
-
soft_wrap (bool, optional): Whether to enable soft wrapping in the console.
|
|
90
|
-
Default is False, and it means rich will hard-wrap long lines (by adding line breaks).
|
|
91
|
-
color (bool | None, optional): Whether to enable/disable color output in the console.
|
|
92
|
-
If None, respects NO_COLOR env var.
|
|
93
|
-
|
|
94
|
-
Returns:
|
|
95
|
-
tuple[rich_console.Console, int, bool]:
|
|
96
|
-
(The initialized console instance, actual log level, actual color use)
|
|
22
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
23
|
+
class CLIConfig:
|
|
24
|
+
"""CLI global context, storing the configuration.
|
|
97
25
|
|
|
98
|
-
|
|
99
|
-
|
|
26
|
+
Attributes:
|
|
27
|
+
console (rich_console.Console): Rich console instance for output
|
|
28
|
+
verbose (int): Verbosity level (0-3)
|
|
29
|
+
color (bool | None): Color preference (None=auto, True=force, False=disable)
|
|
100
30
|
|
|
101
31
|
"""
|
|
102
|
-
global __console_singleton # noqa: PLW0603
|
|
103
|
-
with __console_lock:
|
|
104
|
-
if __console_singleton is not None:
|
|
105
|
-
raise RuntimeError(
|
|
106
|
-
'calling InitLogging() more than once is forbidden; '
|
|
107
|
-
'use Console() to get a console after first creation'
|
|
108
|
-
)
|
|
109
|
-
# set level
|
|
110
|
-
logging_level: int = _LOG_LEVELS.get(min(verbosity, 3), logging.ERROR)
|
|
111
|
-
# respect NO_COLOR unless the caller has already decided (treat env presence as "disable color")
|
|
112
|
-
no_color: bool = (
|
|
113
|
-
False
|
|
114
|
-
if (os.getenv('NO_COLOR') is None and color is None)
|
|
115
|
-
else ((os.getenv('NO_COLOR') is not None) if color is None else (not color))
|
|
116
|
-
)
|
|
117
|
-
# create console and configure logging
|
|
118
|
-
console = rich_console.Console(soft_wrap=soft_wrap, no_color=no_color)
|
|
119
|
-
logging.basicConfig(
|
|
120
|
-
level=logging_level,
|
|
121
|
-
format=_LOG_FORMAT_WITH_PROCESS if include_process else _LOG_FORMAT_NO_PROCESS,
|
|
122
|
-
datefmt=_LOG_FORMAT_DATETIME,
|
|
123
|
-
handlers=[
|
|
124
|
-
rich_logging.RichHandler( # we show name/line, but want time & level
|
|
125
|
-
console=console,
|
|
126
|
-
rich_tracebacks=True,
|
|
127
|
-
show_time=True,
|
|
128
|
-
show_level=True,
|
|
129
|
-
show_path=True,
|
|
130
|
-
),
|
|
131
|
-
],
|
|
132
|
-
force=True, # force=True to override any previous logging config
|
|
133
|
-
)
|
|
134
|
-
# configure common loggers
|
|
135
|
-
logging.captureWarnings(True)
|
|
136
|
-
for name in _LOG_COMMON_PROVIDERS:
|
|
137
|
-
log: logging.Logger = logging.getLogger(name)
|
|
138
|
-
log.handlers.clear()
|
|
139
|
-
log.propagate = True
|
|
140
|
-
log.setLevel(logging_level)
|
|
141
|
-
__console_singleton = console # need a global statement to re-bind this one
|
|
142
|
-
logging.info(
|
|
143
|
-
f'Logging initialized at level {logging.getLevelName(logging_level)} / '
|
|
144
|
-
f'{"NO " if no_color else ""}COLOR'
|
|
145
|
-
)
|
|
146
|
-
return (console, logging_level, not no_color)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
150
|
-
class CLIConfig:
|
|
151
|
-
"""CLI global context, storing the configuration."""
|
|
152
32
|
|
|
153
33
|
console: rich_console.Console
|
|
154
34
|
verbose: int
|
|
@@ -183,9 +63,9 @@ def CLIErrorGuard[**P](fn: abc.Callable[P, None], /) -> abc.Callable[P, None]:
|
|
|
183
63
|
obj.console.print(str(err)) # print only error message
|
|
184
64
|
# no context
|
|
185
65
|
elif logging.getLogger().getEffectiveLevel() < logging.INFO:
|
|
186
|
-
Console().print(str(err)) # print only error message (DEBUG
|
|
66
|
+
tc_logging.Console().print(str(err)) # print only error message (DEBUG is verbose already)
|
|
187
67
|
else:
|
|
188
|
-
Console().print_exception() # print full traceback (less verbose mode needs it)
|
|
68
|
+
tc_logging.Console().print_exception() # print full traceback (less verbose mode needs it)
|
|
189
69
|
|
|
190
70
|
return _Wrapper
|
|
191
71
|
|
|
@@ -267,7 +147,7 @@ def GenerateTyperHelpMarkdown(
|
|
|
267
147
|
# build command path
|
|
268
148
|
command_path: str = ' '.join([prog_name, *path]).strip()
|
|
269
149
|
heading_prefix: str = '#' * max(1, heading_level + len(path))
|
|
270
|
-
ResetConsole() # ensure clean state for
|
|
150
|
+
tc_logging.ResetConsole() # ensure clean state for the command
|
|
271
151
|
# invoke --help for this command path
|
|
272
152
|
result: click_testing.Result = runner.invoke(
|
|
273
153
|
click_root,
|
transcrypto/cli/intmath.py
CHANGED
|
@@ -6,8 +6,10 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
|
-
from transcrypto import
|
|
9
|
+
from transcrypto import transcrypto
|
|
10
10
|
from transcrypto.cli import clibase
|
|
11
|
+
from transcrypto.core import modmath
|
|
12
|
+
from transcrypto.utils import base, saferandom
|
|
11
13
|
|
|
12
14
|
# =============================== "PRIME"-like COMMANDS ============================================
|
|
13
15
|
|
|
@@ -116,7 +118,7 @@ def GcdCLI( # documentation is help/epilog/args # noqa: D103
|
|
|
116
118
|
b_i: int = transcrypto.ParseInt(b, min_value=0)
|
|
117
119
|
if a_i == 0 and b_i == 0:
|
|
118
120
|
raise base.InputError("`a` and `b` can't both be zero")
|
|
119
|
-
config.console.print(
|
|
121
|
+
config.console.print(modmath.GCD(a_i, b_i))
|
|
120
122
|
|
|
121
123
|
|
|
122
124
|
@transcrypto.app.command(
|
|
@@ -147,7 +149,7 @@ def XgcdCLI( # documentation is help/epilog/args # noqa: D103
|
|
|
147
149
|
b_i: int = transcrypto.ParseInt(b, min_value=0)
|
|
148
150
|
if a_i == 0 and b_i == 0:
|
|
149
151
|
raise base.InputError("`a` and `b` can't both be zero")
|
|
150
|
-
config.console.print(str(
|
|
152
|
+
config.console.print(str(modmath.ExtendedGCD(a_i, b_i)))
|
|
151
153
|
|
|
152
154
|
|
|
153
155
|
# ================================= "RANDOM" COMMAND ===============================================
|
|
@@ -172,7 +174,7 @@ def RandomBits( # documentation is help/epilog/args # noqa: D103
|
|
|
172
174
|
bits: int = typer.Argument(..., min=8, help='Number of bits, ≥ 8'),
|
|
173
175
|
) -> None:
|
|
174
176
|
config: transcrypto.TransConfig = ctx.obj
|
|
175
|
-
config.console.print(
|
|
177
|
+
config.console.print(saferandom.RandBits(bits))
|
|
176
178
|
|
|
177
179
|
|
|
178
180
|
@random_app.command(
|
|
@@ -190,7 +192,7 @@ def RandomInt( # documentation is help/epilog/args # noqa: D103
|
|
|
190
192
|
config: transcrypto.TransConfig = ctx.obj
|
|
191
193
|
min_i: int = transcrypto.ParseInt(min_, min_value=0)
|
|
192
194
|
max_i: int = transcrypto.ParseInt(max_, min_value=min_i + 1)
|
|
193
|
-
config.console.print(
|
|
195
|
+
config.console.print(saferandom.RandInt(min_i, max_i))
|
|
194
196
|
|
|
195
197
|
|
|
196
198
|
@random_app.command(
|
|
@@ -209,7 +211,7 @@ def RandomBytes( # documentation is help/epilog/args # noqa: D103
|
|
|
209
211
|
n: int = typer.Argument(..., min=1, help='Number of bytes, ≥ 1'),
|
|
210
212
|
) -> None:
|
|
211
213
|
config: transcrypto.TransConfig = ctx.obj
|
|
212
|
-
config.console.print(transcrypto.BytesToText(
|
|
214
|
+
config.console.print(transcrypto.BytesToText(saferandom.RandBytes(n), config.output_format))
|
|
213
215
|
|
|
214
216
|
|
|
215
217
|
@random_app.command(
|
transcrypto/cli/publicalgos.py
CHANGED
|
@@ -6,8 +6,9 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
|
-
from transcrypto import
|
|
9
|
+
from transcrypto import transcrypto
|
|
10
10
|
from transcrypto.cli import clibase
|
|
11
|
+
from transcrypto.core import dsa, elgamal, rsa
|
|
11
12
|
|
|
12
13
|
# ================================== "RSA" COMMAND =================================================
|
|
13
14
|
|
|
@@ -25,7 +25,8 @@ from cryptography.hazmat.primitives import hashes as hazmat_hashes
|
|
|
25
25
|
from cryptography.hazmat.primitives.ciphers import algorithms, modes
|
|
26
26
|
from cryptography.hazmat.primitives.kdf import pbkdf2 as hazmat_pbkdf2
|
|
27
27
|
|
|
28
|
-
from . import
|
|
28
|
+
from transcrypto.core import hashes, key
|
|
29
|
+
from transcrypto.utils import base, saferandom
|
|
29
30
|
|
|
30
31
|
# these fixed salt/iterations are for password->key generation only; NEVER use them to
|
|
31
32
|
# build a database of passwords because it would not be safe; NEVER change them or the
|
|
@@ -41,7 +42,7 @@ assert _PASSWORD_ITERATIONS == (6075308 + 1) // 3, 'should never happen: constan
|
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
44
|
-
class AESKey(
|
|
45
|
+
class AESKey(key.CryptoKey, key.Encryptor, key.Decryptor):
|
|
45
46
|
"""Advanced Encryption Standard (AES) 256 bits key (32 bytes).
|
|
46
47
|
|
|
47
48
|
No measures are taken here to prevent timing attacks.
|
|
@@ -57,7 +58,7 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
57
58
|
"""Check data.
|
|
58
59
|
|
|
59
60
|
Raises:
|
|
60
|
-
InputError: invalid inputs
|
|
61
|
+
base.InputError: invalid inputs
|
|
61
62
|
|
|
62
63
|
"""
|
|
63
64
|
if len(self.key256) != 32: # noqa: PLR2004
|
|
@@ -70,7 +71,7 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
70
71
|
string representation of AESKey without leaking secrets
|
|
71
72
|
|
|
72
73
|
"""
|
|
73
|
-
return f'AESKey(key256={
|
|
74
|
+
return f'AESKey(key256={hashes.ObfuscateSecret(self.key256)})'
|
|
74
75
|
|
|
75
76
|
@classmethod
|
|
76
77
|
def FromStaticPassword(cls, str_password: str, /) -> Self:
|
|
@@ -98,7 +99,7 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
98
99
|
AESKey crypto key to use (URL-safe base64-encoded 32-byte key)
|
|
99
100
|
|
|
100
101
|
Raises:
|
|
101
|
-
InputError: empty password
|
|
102
|
+
base.InputError: empty password
|
|
102
103
|
|
|
103
104
|
"""
|
|
104
105
|
str_password = str_password.strip()
|
|
@@ -112,7 +113,7 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
112
113
|
)
|
|
113
114
|
return cls(key256=kdf.derive(str_password.encode('utf-8')))
|
|
114
115
|
|
|
115
|
-
class ECBEncoderClass(
|
|
116
|
+
class ECBEncoderClass(key.Encryptor, key.Decryptor):
|
|
116
117
|
"""The simplest encryption possible (UNSAFE if misused): 128 bit block AES-ECB, 256 bit key.
|
|
117
118
|
|
|
118
119
|
Note: Due to ECB encoding, this class is only safe-ish for blocks of random-looking data,
|
|
@@ -162,7 +163,7 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
162
163
|
bytes: Ciphertext, a block of 128 bits (16 bytes)
|
|
163
164
|
|
|
164
165
|
Raises:
|
|
165
|
-
InputError: invalid inputs
|
|
166
|
+
base.InputError: invalid inputs
|
|
166
167
|
|
|
167
168
|
"""
|
|
168
169
|
if associated_data is not None:
|
|
@@ -189,7 +190,7 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
189
190
|
bytes: Decrypted plaintext, a block of 128 bits (16 bytes)
|
|
190
191
|
|
|
191
192
|
Raises:
|
|
192
|
-
InputError: invalid inputs
|
|
193
|
+
base.InputError: invalid inputs
|
|
193
194
|
|
|
194
195
|
"""
|
|
195
196
|
if associated_data is not None:
|
|
@@ -227,7 +228,7 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
227
228
|
str: encrypted hexadecimal block (length==64)
|
|
228
229
|
|
|
229
230
|
Raises:
|
|
230
|
-
InputError: invalid inputs
|
|
231
|
+
base.InputError: invalid inputs
|
|
231
232
|
|
|
232
233
|
"""
|
|
233
234
|
if len(plaintext_hex) != 64: # noqa: PLR2004
|
|
@@ -262,7 +263,7 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
262
263
|
str: plaintext hexadecimal block (length==64)
|
|
263
264
|
|
|
264
265
|
Raises:
|
|
265
|
-
InputError: invalid inputs
|
|
266
|
+
base.InputError: invalid inputs
|
|
266
267
|
|
|
267
268
|
"""
|
|
268
269
|
if len(ciphertext_hex) != 64: # noqa: PLR2004
|
|
@@ -297,7 +298,7 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
297
298
|
must encode it within the returned bytes (or document how to retrieve it)
|
|
298
299
|
|
|
299
300
|
"""
|
|
300
|
-
iv: bytes =
|
|
301
|
+
iv: bytes = saferandom.RandBytes(16)
|
|
301
302
|
cipher: ciphers.Cipher[modes.GCM] = ciphers.Cipher(
|
|
302
303
|
algorithms.AES256(self.key256), modes.GCM(iv)
|
|
303
304
|
)
|
|
@@ -339,12 +340,14 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
339
340
|
bytes: Decrypted plaintext bytes
|
|
340
341
|
|
|
341
342
|
Raises:
|
|
342
|
-
InputError: invalid inputs
|
|
343
|
-
CryptoError: internal crypto failures, authentication failure, key mismatch, etc
|
|
343
|
+
base.InputError: invalid inputs
|
|
344
|
+
key.CryptoError: internal crypto failures, authentication failure, key mismatch, etc
|
|
344
345
|
|
|
345
346
|
"""
|
|
346
347
|
if len(ciphertext) < 32: # noqa: PLR2004
|
|
347
348
|
raise base.InputError(f'AES256+GCM should have ≥32 bytes IV/CT/tag: {len(ciphertext)}')
|
|
349
|
+
iv: bytes
|
|
350
|
+
tag: bytes
|
|
348
351
|
iv, tag = ciphertext[:16], ciphertext[-16:]
|
|
349
352
|
decryptor: ciphers.CipherContext = ciphers.Cipher(
|
|
350
353
|
algorithms.AES256(self.key256), modes.GCM(iv, tag)
|
|
@@ -354,19 +357,4 @@ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
|
|
|
354
357
|
try:
|
|
355
358
|
return decryptor.update(ciphertext[16:-16]) + decryptor.finalize()
|
|
356
359
|
except crypt_exceptions.InvalidTag as err:
|
|
357
|
-
raise
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
def _TestCryptoKeyEncoding(obj: base.CryptoKey, tp: type[base.CryptoKey]) -> None: # pyright: ignore[reportUnusedFunction]
|
|
361
|
-
"""Test encoding for a CryptoKey instance. Only for use from test modules."""
|
|
362
|
-
assert tp.FromJSON(obj.json) == obj # noqa: S101
|
|
363
|
-
assert tp.FromJSON(obj.formatted_json) == obj # noqa: S101
|
|
364
|
-
assert tp.Load(obj.blob) == obj # noqa: S101
|
|
365
|
-
assert tp.Load(obj.encoded) == obj # noqa: S101
|
|
366
|
-
assert tp.Load(obj.hex) == obj # noqa: S101
|
|
367
|
-
assert tp.Load(obj.raw) == obj # noqa: S101
|
|
368
|
-
key = AESKey(key256=b'x' * 32)
|
|
369
|
-
assert tp.Load(obj.Blob(key=key), key=key) == obj # noqa: S101
|
|
370
|
-
assert tp.Load(obj.Encoded(key=key), key=key) == obj # noqa: S101
|
|
371
|
-
assert tp.Load(obj.Hex(key=key), key=key) == obj # noqa: S101
|
|
372
|
-
assert tp.Load(obj.Raw(key=key), key=key) == obj # noqa: S101
|
|
360
|
+
raise key.CryptoError('failed decryption') from err
|
transcrypto/core/bid.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Balparda's TransCrypto bidding protocols."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import dataclasses
|
|
8
|
+
from typing import Self
|
|
9
|
+
|
|
10
|
+
from transcrypto.core import hashes, key
|
|
11
|
+
from transcrypto.utils import base, saferandom
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
15
|
+
class PublicBid512(key.CryptoKey):
|
|
16
|
+
"""Public commitment to a (cryptographically secure) bid that can be revealed/validated later.
|
|
17
|
+
|
|
18
|
+
Bid is computed as: public_hash = Hash512(public_key || private_key || secret_bid)
|
|
19
|
+
|
|
20
|
+
Everything is bytes. The public part is (public_key, public_hash) and the private
|
|
21
|
+
part is (private_key, secret_bid). The whole computation can be checked later.
|
|
22
|
+
|
|
23
|
+
No measures are taken here to prevent timing attacks (probably not a concern).
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
public_key (bytes): 512-bits random value
|
|
27
|
+
public_hash (bytes): SHA-512 hash of (public_key || private_key || secret_bid)
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
public_key: bytes
|
|
32
|
+
public_hash: bytes
|
|
33
|
+
|
|
34
|
+
def __post_init__(self) -> None:
|
|
35
|
+
"""Check data.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
base.InputError: invalid inputs
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
if len(self.public_key) != 64 or len(self.public_hash) != 64: # noqa: PLR2004
|
|
42
|
+
raise base.InputError(f'invalid public_key or public_hash: {self}')
|
|
43
|
+
|
|
44
|
+
def __str__(self) -> str:
|
|
45
|
+
"""Safe string representation of the PublicBid.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
string representation of PublicBid
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
return (
|
|
52
|
+
'PublicBid512('
|
|
53
|
+
f'public_key={base.BytesToEncoded(self.public_key)}, '
|
|
54
|
+
f'public_hash={base.BytesToHex(self.public_hash)})'
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def VerifyBid(self, private_key: bytes, secret: bytes, /) -> bool:
|
|
58
|
+
"""Verify a bid. True if OK; False if failed verification.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
private_key (bytes): 512-bits private key
|
|
62
|
+
secret (bytes): Any number of bytes (≥1) to bid on (e.g., UTF-8 encoded string)
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
True if bid is valid, False otherwise
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
# creating the PrivateBid object will validate everything; InputError we allow to propagate
|
|
70
|
+
PrivateBid512(
|
|
71
|
+
public_key=self.public_key,
|
|
72
|
+
public_hash=self.public_hash,
|
|
73
|
+
private_key=private_key,
|
|
74
|
+
secret_bid=secret,
|
|
75
|
+
)
|
|
76
|
+
return True # if we got here, all is good
|
|
77
|
+
except key.CryptoError:
|
|
78
|
+
return False # bid does not match the public commitment
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def Copy(cls, other: PublicBid512, /) -> Self:
|
|
82
|
+
"""Initialize a public bid by taking the public parts of a public/private bid.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
other (PublicBid512): the bid to copy from
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Self: an initialized PublicBid512
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
return cls(public_key=other.public_key, public_hash=other.public_hash)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
95
|
+
class PrivateBid512(PublicBid512):
|
|
96
|
+
"""Private bid that can be revealed and validated against a public commitment (see PublicBid).
|
|
97
|
+
|
|
98
|
+
Attributes:
|
|
99
|
+
private_key (bytes): 512-bits random value
|
|
100
|
+
secret_bid (bytes): Any number of bytes (≥1) to bid on (e.g., UTF-8 encoded string)
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
private_key: bytes
|
|
105
|
+
secret_bid: bytes
|
|
106
|
+
|
|
107
|
+
def __post_init__(self) -> None:
|
|
108
|
+
"""Check data.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
base.InputError: invalid inputs
|
|
112
|
+
key.CryptoError: bid does not match the public commitment
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
super(PrivateBid512, self).__post_init__()
|
|
116
|
+
if len(self.private_key) != 64 or len(self.secret_bid) < 1: # noqa: PLR2004
|
|
117
|
+
raise base.InputError(f'invalid private_key or secret_bid: {self}')
|
|
118
|
+
if self.public_hash != hashes.Hash512(self.public_key + self.private_key + self.secret_bid):
|
|
119
|
+
raise key.CryptoError(f'inconsistent bid: {self}')
|
|
120
|
+
|
|
121
|
+
def __str__(self) -> str:
|
|
122
|
+
"""Safe (no secrets) string representation of the PrivateBid.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
string representation of PrivateBid without leaking secrets
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
return (
|
|
129
|
+
'PrivateBid512('
|
|
130
|
+
f'{super(PrivateBid512, self).__str__()}, '
|
|
131
|
+
f'private_key={hashes.ObfuscateSecret(self.private_key)}, '
|
|
132
|
+
f'secret_bid={hashes.ObfuscateSecret(self.secret_bid)})'
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def New(cls, secret: bytes, /) -> Self:
|
|
137
|
+
"""Make the `secret` into a new bid.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
secret (bytes): Any number of bytes (≥1) to bid on (e.g., UTF-8 encoded string)
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
PrivateBid object ready for use (use PublicBid.Copy() to get the public part)
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
base.InputError: invalid inputs
|
|
147
|
+
|
|
148
|
+
"""
|
|
149
|
+
# test inputs
|
|
150
|
+
if len(secret) < 1:
|
|
151
|
+
raise base.InputError(f'invalid secret length: {len(secret)}')
|
|
152
|
+
# generate random values
|
|
153
|
+
public_key: bytes = saferandom.RandBytes(64) # 512 bits
|
|
154
|
+
private_key: bytes = saferandom.RandBytes(64) # 512 bits
|
|
155
|
+
# build object
|
|
156
|
+
return cls(
|
|
157
|
+
public_key=public_key,
|
|
158
|
+
public_hash=hashes.Hash512(public_key + private_key + secret),
|
|
159
|
+
private_key=private_key,
|
|
160
|
+
secret_bid=secret,
|
|
161
|
+
)
|