transcrypto 1.8.0__py3-none-any.whl → 2.0.3__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 +14 -12
- transcrypto/cli/bidsecret.py +19 -16
- transcrypto/cli/clibase.py +22 -142
- transcrypto/cli/intmath.py +24 -21
- transcrypto/cli/publicalgos.py +28 -26
- 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 +25 -11
- transcrypto/transcrypto.py +25 -15
- 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.3.dist-info}/METADATA +101 -101
- transcrypto-2.0.3.dist-info/RECORD +33 -0
- {transcrypto-1.8.0.dist-info → transcrypto-2.0.3.dist-info}/WHEEL +1 -1
- 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.3.dist-info}/entry_points.txt +0 -0
- {transcrypto-1.8.0.dist-info → transcrypto-2.0.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -12,14 +12,15 @@ import logging
|
|
|
12
12
|
from collections import abc
|
|
13
13
|
from typing import Self
|
|
14
14
|
|
|
15
|
-
from . import aes,
|
|
15
|
+
from transcrypto.core import aes, hashes, key, modmath
|
|
16
|
+
from transcrypto.utils import base, saferandom
|
|
16
17
|
|
|
17
18
|
# fixed prefixes: do NOT ever change! will break all encryption and signature schemes
|
|
18
19
|
_SSS_ENCRYPTION_AAD_PREFIX = b'transcrypto.SSS.Sharing.1.0\x00'
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
22
|
-
class ShamirSharedSecretPublic(
|
|
23
|
+
class ShamirSharedSecretPublic(key.CryptoKey):
|
|
23
24
|
"""Shamir Shared Secret (SSS) public part.
|
|
24
25
|
|
|
25
26
|
No measures are taken here to prevent timing attacks.
|
|
@@ -38,7 +39,7 @@ class ShamirSharedSecretPublic(base.CryptoKey):
|
|
|
38
39
|
"""Check data.
|
|
39
40
|
|
|
40
41
|
Raises:
|
|
41
|
-
InputError: invalid inputs
|
|
42
|
+
base.InputError: invalid inputs
|
|
42
43
|
|
|
43
44
|
"""
|
|
44
45
|
if self.modulus < 2 or not modmath.IsPrime(self.modulus) or self.minimum < 2: # noqa: PLR2004
|
|
@@ -82,8 +83,8 @@ class ShamirSharedSecretPublic(base.CryptoKey):
|
|
|
82
83
|
no "excess" shares, there can be no way to know if the recovered secret is the correct one
|
|
83
84
|
|
|
84
85
|
Raises:
|
|
85
|
-
InputError: invalid inputs
|
|
86
|
-
CryptoError: secret cannot be recovered (number of shares < `minimum`)
|
|
86
|
+
base.InputError: invalid inputs
|
|
87
|
+
key.CryptoError: secret cannot be recovered (number of shares < `minimum`)
|
|
87
88
|
|
|
88
89
|
"""
|
|
89
90
|
# check that we have enough shares by de-duping them first
|
|
@@ -107,7 +108,7 @@ class ShamirSharedSecretPublic(base.CryptoKey):
|
|
|
107
108
|
if force_recover and given_shares > 1:
|
|
108
109
|
logging.error(f'recovering secret even though: {mess}')
|
|
109
110
|
else:
|
|
110
|
-
raise
|
|
111
|
+
raise key.CryptoError(f'unrecoverable secret: {mess}')
|
|
111
112
|
# do the math
|
|
112
113
|
return modmath.ModLagrangeInterpolate(0, share_points, self.modulus)
|
|
113
114
|
|
|
@@ -147,7 +148,7 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
|
|
|
147
148
|
"""Check data.
|
|
148
149
|
|
|
149
150
|
Raises:
|
|
150
|
-
InputError: invalid inputs
|
|
151
|
+
base.InputError: invalid inputs
|
|
151
152
|
|
|
152
153
|
"""
|
|
153
154
|
super(ShamirSharedSecretPrivate, self).__post_init__()
|
|
@@ -172,7 +173,7 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
|
|
|
172
173
|
return (
|
|
173
174
|
'ShamirSharedSecretPrivate('
|
|
174
175
|
f'{super(ShamirSharedSecretPrivate, self).__str__()}, '
|
|
175
|
-
f'polynomial=[{", ".join(
|
|
176
|
+
f'polynomial=[{", ".join(hashes.ObfuscateSecret(i) for i in self.polynomial)}])'
|
|
176
177
|
)
|
|
177
178
|
|
|
178
179
|
def RawShare(self, secret: int, /, *, share_key: int = 0) -> ShamirSharePrivate:
|
|
@@ -192,7 +193,7 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
|
|
|
192
193
|
ShamirSharePrivate object
|
|
193
194
|
|
|
194
195
|
Raises:
|
|
195
|
-
InputError: invalid inputs
|
|
196
|
+
base.InputError: invalid inputs
|
|
196
197
|
|
|
197
198
|
"""
|
|
198
199
|
# test inputs
|
|
@@ -202,7 +203,7 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
|
|
|
202
203
|
if not share_key: # default is zero, and that means we generate it here
|
|
203
204
|
share_key = 0
|
|
204
205
|
while not share_key or share_key in self.polynomial or share_key >= self.modulus:
|
|
205
|
-
share_key =
|
|
206
|
+
share_key = saferandom.RandBits(self.modulus.bit_length() - 1)
|
|
206
207
|
else:
|
|
207
208
|
raise base.InputError(f'invalid share_key: {share_key=}')
|
|
208
209
|
# build object
|
|
@@ -229,7 +230,7 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
|
|
|
229
230
|
ShamirSharePrivate object
|
|
230
231
|
|
|
231
232
|
Raises:
|
|
232
|
-
InputError: invalid inputs
|
|
233
|
+
base.InputError: invalid inputs
|
|
233
234
|
|
|
234
235
|
"""
|
|
235
236
|
# test inputs
|
|
@@ -246,7 +247,7 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
|
|
|
246
247
|
or share_key in used_keys
|
|
247
248
|
or share_key >= self.modulus
|
|
248
249
|
):
|
|
249
|
-
share_key =
|
|
250
|
+
share_key = saferandom.RandBits(self.modulus.bit_length() - 1)
|
|
250
251
|
try:
|
|
251
252
|
yield self.RawShare(secret, share_key=share_key)
|
|
252
253
|
used_keys.add(share_key)
|
|
@@ -273,7 +274,7 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
|
|
|
273
274
|
list[ShamirShareData]: the list of shares with encrypted data
|
|
274
275
|
|
|
275
276
|
Raises:
|
|
276
|
-
InputError: invalid inputs
|
|
277
|
+
base.InputError: invalid inputs
|
|
277
278
|
|
|
278
279
|
"""
|
|
279
280
|
if total_shares < self.minimum:
|
|
@@ -281,7 +282,7 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
|
|
|
281
282
|
k: int = self.modulus_size
|
|
282
283
|
if k <= 32: # noqa: PLR2004
|
|
283
284
|
raise base.InputError(f'modulus too small for key operations: {k} bytes')
|
|
284
|
-
key256: bytes =
|
|
285
|
+
key256: bytes = saferandom.RandBytes(32)
|
|
285
286
|
shares: list[ShamirSharePrivate] = list(
|
|
286
287
|
self.RawShares(base.BytesToInt(key256), max_shares=total_shares)
|
|
287
288
|
)
|
|
@@ -290,7 +291,7 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
|
|
|
290
291
|
+ base.IntToFixedBytes(self.minimum, 8)
|
|
291
292
|
+ base.IntToFixedBytes(self.modulus, k)
|
|
292
293
|
)
|
|
293
|
-
aead_key: bytes =
|
|
294
|
+
aead_key: bytes = hashes.Hash512(_SSS_ENCRYPTION_AAD_PREFIX + key256)
|
|
294
295
|
ct: bytes = aes.AESKey(key256=aead_key[32:]).Encrypt(secret, associated_data=aad)
|
|
295
296
|
return [
|
|
296
297
|
ShamirShareData(
|
|
@@ -333,7 +334,7 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
|
|
|
333
334
|
ShamirSharedSecretPrivate object ready for use
|
|
334
335
|
|
|
335
336
|
Raises:
|
|
336
|
-
InputError: invalid inputs
|
|
337
|
+
base.InputError: invalid inputs
|
|
337
338
|
|
|
338
339
|
"""
|
|
339
340
|
# test inputs
|
|
@@ -348,7 +349,7 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
|
|
|
348
349
|
modulus: int = max(ordered_primes)
|
|
349
350
|
ordered_primes.remove(modulus)
|
|
350
351
|
# make polynomial be a random order
|
|
351
|
-
|
|
352
|
+
saferandom.RandShuffle(ordered_primes)
|
|
352
353
|
# build object
|
|
353
354
|
return cls(minimum=minimum_shares, modulus=modulus, polynomial=ordered_primes)
|
|
354
355
|
|
|
@@ -373,7 +374,7 @@ class ShamirSharePrivate(ShamirSharedSecretPublic):
|
|
|
373
374
|
"""Check data.
|
|
374
375
|
|
|
375
376
|
Raises:
|
|
376
|
-
InputError: invalid inputs
|
|
377
|
+
base.InputError: invalid inputs
|
|
377
378
|
|
|
378
379
|
"""
|
|
379
380
|
super(ShamirSharePrivate, self).__post_init__()
|
|
@@ -390,8 +391,8 @@ class ShamirSharePrivate(ShamirSharedSecretPublic):
|
|
|
390
391
|
return (
|
|
391
392
|
'ShamirSharePrivate('
|
|
392
393
|
f'{super(ShamirSharePrivate, self).__str__()}, '
|
|
393
|
-
f'share_key={
|
|
394
|
-
f'share_value={
|
|
394
|
+
f'share_key={hashes.ObfuscateSecret(self.share_key)}, '
|
|
395
|
+
f'share_value={hashes.ObfuscateSecret(self.share_value)})'
|
|
395
396
|
)
|
|
396
397
|
|
|
397
398
|
@classmethod
|
|
@@ -433,7 +434,7 @@ class ShamirShareData(ShamirSharePrivate):
|
|
|
433
434
|
"""Check data.
|
|
434
435
|
|
|
435
436
|
Raises:
|
|
436
|
-
InputError: invalid inputs
|
|
437
|
+
base.InputError: invalid inputs
|
|
437
438
|
|
|
438
439
|
"""
|
|
439
440
|
super(ShamirShareData, self).__post_init__()
|
|
@@ -450,7 +451,7 @@ class ShamirShareData(ShamirSharePrivate):
|
|
|
450
451
|
return (
|
|
451
452
|
'ShamirShareData('
|
|
452
453
|
f'{super(ShamirShareData, self).__str__()}, '
|
|
453
|
-
f'encrypted_data={
|
|
454
|
+
f'encrypted_data={hashes.ObfuscateSecret(self.encrypted_data)})'
|
|
454
455
|
)
|
|
455
456
|
|
|
456
457
|
def RecoverData(self, other_shares: list[ShamirSharePrivate]) -> bytes:
|
|
@@ -467,8 +468,8 @@ class ShamirShareData(ShamirSharePrivate):
|
|
|
467
468
|
bytes: Decrypted plaintext bytes
|
|
468
469
|
|
|
469
470
|
Raises:
|
|
470
|
-
InputError: invalid inputs
|
|
471
|
-
CryptoError: internal crypto failures, authentication failure, key mismatch, etc
|
|
471
|
+
base.InputError: invalid inputs
|
|
472
|
+
key.CryptoError: internal crypto failures, authentication failure, key mismatch, etc
|
|
472
473
|
|
|
473
474
|
"""
|
|
474
475
|
k: int = self.modulus_size
|
|
@@ -477,12 +478,12 @@ class ShamirShareData(ShamirSharePrivate):
|
|
|
477
478
|
# recover secret; raise if shares are invalid
|
|
478
479
|
secret: int = self.RawRecoverSecret([self, *other_shares])
|
|
479
480
|
if not 0 <= secret < (1 << 256):
|
|
480
|
-
raise
|
|
481
|
+
raise key.CryptoError('recovered key out of range for 256-bit key')
|
|
481
482
|
key256: bytes = base.IntToFixedBytes(secret, 32)
|
|
482
483
|
aad: bytes = (
|
|
483
484
|
_SSS_ENCRYPTION_AAD_PREFIX
|
|
484
485
|
+ base.IntToFixedBytes(self.minimum, 8)
|
|
485
486
|
+ base.IntToFixedBytes(self.modulus, k)
|
|
486
487
|
)
|
|
487
|
-
aead_key: bytes =
|
|
488
|
+
aead_key: bytes = hashes.Hash512(_SSS_ENCRYPTION_AAD_PREFIX + key256)
|
|
488
489
|
return aes.AESKey(key256=aead_key[32:]).Decrypt(self.encrypted_data, associated_data=aad)
|
transcrypto/profiler.py
CHANGED
|
@@ -21,17 +21,29 @@ from __future__ import annotations
|
|
|
21
21
|
import dataclasses
|
|
22
22
|
from collections import abc
|
|
23
23
|
|
|
24
|
+
import click
|
|
24
25
|
import typer
|
|
25
26
|
from rich import console as rich_console
|
|
26
27
|
|
|
27
28
|
from transcrypto.cli import clibase
|
|
29
|
+
from transcrypto.core import dsa, modmath
|
|
30
|
+
from transcrypto.utils import human, timer
|
|
31
|
+
from transcrypto.utils import logging as tc_logging
|
|
28
32
|
|
|
29
|
-
from . import __version__
|
|
33
|
+
from . import __version__
|
|
30
34
|
|
|
31
35
|
|
|
32
36
|
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
33
37
|
class ProfilerConfig(clibase.CLIConfig):
|
|
34
|
-
"""CLI global context, storing the configuration.
|
|
38
|
+
"""CLI global context, storing the configuration.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
serial (bool): Whether to run profiling serially (vs parallel)
|
|
42
|
+
repeats (int): Number of repetitions for each profiling run
|
|
43
|
+
confidence (int): Confidence level percentage for statistical analysis
|
|
44
|
+
bits (tuple[int, int, int]): Bit sizes range (start, stop, step) for profiling
|
|
45
|
+
|
|
46
|
+
"""
|
|
35
47
|
|
|
36
48
|
serial: bool
|
|
37
49
|
repeats: int
|
|
@@ -67,7 +79,7 @@ def Run() -> None:
|
|
|
67
79
|
@clibase.CLIErrorGuard
|
|
68
80
|
def Main( # documentation is help/epilog/args # noqa: D103
|
|
69
81
|
*,
|
|
70
|
-
ctx:
|
|
82
|
+
ctx: click.Context, # global context
|
|
71
83
|
version: bool = typer.Option(False, '--version', help='Show version and exit.'),
|
|
72
84
|
verbose: int = typer.Option(
|
|
73
85
|
0,
|
|
@@ -124,7 +136,9 @@ def Main( # documentation is help/epilog/args # noqa: D103
|
|
|
124
136
|
if version:
|
|
125
137
|
typer.echo(__version__)
|
|
126
138
|
raise typer.Exit(0)
|
|
127
|
-
|
|
139
|
+
# initialize logging and get console
|
|
140
|
+
console: rich_console.Console
|
|
141
|
+
console, verbose, color = tc_logging.InitLogging(
|
|
128
142
|
verbose,
|
|
129
143
|
color=color,
|
|
130
144
|
include_process=False, # decide if you want process names in logs
|
|
@@ -160,7 +174,7 @@ def Main( # documentation is help/epilog/args # noqa: D103
|
|
|
160
174
|
),
|
|
161
175
|
)
|
|
162
176
|
@clibase.CLIErrorGuard
|
|
163
|
-
def Primes(*, ctx:
|
|
177
|
+
def Primes(*, ctx: click.Context) -> None: # documentation is help/epilog/args # noqa: D103
|
|
164
178
|
config: ProfilerConfig = ctx.obj # get application global config
|
|
165
179
|
config.console.print(
|
|
166
180
|
f'Starting [yellow]{"SERIAL" if config.serial else "PARALLEL"} regular primes[/] test'
|
|
@@ -190,7 +204,7 @@ def Primes(*, ctx: typer.Context) -> None: # documentation is help/epilog/args
|
|
|
190
204
|
),
|
|
191
205
|
)
|
|
192
206
|
@clibase.CLIErrorGuard
|
|
193
|
-
def DSA(*, ctx:
|
|
207
|
+
def DSA(*, ctx: click.Context) -> None: # documentation is help/epilog/args # noqa: D103
|
|
194
208
|
config: ProfilerConfig = ctx.obj # get application global config
|
|
195
209
|
config.console.print(
|
|
196
210
|
f'Starting [yellow]{"SERIAL" if config.serial else "PARALLEL"} DSA primes[/] test'
|
|
@@ -211,7 +225,7 @@ def DSA(*, ctx: typer.Context) -> None: # documentation is help/epilog/args # n
|
|
|
211
225
|
)
|
|
212
226
|
@clibase.CLIErrorGuard
|
|
213
227
|
def Markdown() -> None: # documentation is help/epilog/args # noqa: D103
|
|
214
|
-
console: rich_console.Console =
|
|
228
|
+
console: rich_console.Console = tc_logging.Console()
|
|
215
229
|
console.print(clibase.GenerateTyperHelpMarkdown(app, prog_name='profiler'))
|
|
216
230
|
|
|
217
231
|
|
|
@@ -223,20 +237,20 @@ def _PrimeProfiler(
|
|
|
223
237
|
confidence: float,
|
|
224
238
|
/,
|
|
225
239
|
) -> None:
|
|
226
|
-
with
|
|
240
|
+
with timer.Timer(emit_log=False) as total_time:
|
|
227
241
|
primes: dict[int, list[float]] = {}
|
|
228
242
|
for n_bits in range(*n_bits_range):
|
|
229
243
|
# investigate for size n_bits
|
|
230
244
|
primes[n_bits] = []
|
|
231
245
|
for _ in range(repeats):
|
|
232
|
-
with
|
|
246
|
+
with timer.Timer(emit_log=False) as run_time:
|
|
233
247
|
pr: int = prime_callable(n_bits)
|
|
234
248
|
assert pr # noqa: S101
|
|
235
249
|
assert pr.bit_length() == n_bits # noqa: S101
|
|
236
250
|
primes[n_bits].append(run_time.elapsed)
|
|
237
251
|
# finished collecting n_bits-sized primes
|
|
238
|
-
measurements: str =
|
|
239
|
-
primes[n_bits], parser=
|
|
252
|
+
measurements: str = human.HumanizedMeasurements(
|
|
253
|
+
primes[n_bits], parser=human.HumanizedSeconds, confidence=confidence
|
|
240
254
|
)
|
|
241
255
|
console.print(f'{n_bits} → {measurements}')
|
|
242
256
|
console.print(f'Finished in {total_time}')
|
transcrypto/transcrypto.py
CHANGED
|
@@ -97,14 +97,14 @@ import pathlib
|
|
|
97
97
|
|
|
98
98
|
import click
|
|
99
99
|
import typer
|
|
100
|
+
from rich import console as rich_console
|
|
100
101
|
|
|
101
102
|
from transcrypto.cli import clibase
|
|
103
|
+
from transcrypto.core import aes, key
|
|
104
|
+
from transcrypto.utils import base, human
|
|
105
|
+
from transcrypto.utils import logging as tc_logging
|
|
102
106
|
|
|
103
|
-
from . import
|
|
104
|
-
__version__,
|
|
105
|
-
aes,
|
|
106
|
-
base,
|
|
107
|
-
)
|
|
107
|
+
from . import __version__
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
class IOFormat(enum.Enum):
|
|
@@ -117,7 +117,15 @@ class IOFormat(enum.Enum):
|
|
|
117
117
|
|
|
118
118
|
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
119
119
|
class TransConfig(clibase.CLIConfig):
|
|
120
|
-
"""CLI global context, storing the configuration.
|
|
120
|
+
"""CLI global context, storing the configuration.
|
|
121
|
+
|
|
122
|
+
Attributes:
|
|
123
|
+
input_format (IOFormat): Input data format (hex, b64, bin)
|
|
124
|
+
output_format (IOFormat): Output data format (hex, b64, bin)
|
|
125
|
+
key_path (pathlib.Path | None): Path to key file for crypto operations
|
|
126
|
+
protect (str | None): Password protection for key operations
|
|
127
|
+
|
|
128
|
+
"""
|
|
121
129
|
|
|
122
130
|
input_format: IOFormat
|
|
123
131
|
output_format: IOFormat
|
|
@@ -240,18 +248,18 @@ def BytesToText(b: bytes, fmt: IOFormat, /) -> str:
|
|
|
240
248
|
return base.BytesToEncoded(b)
|
|
241
249
|
|
|
242
250
|
|
|
243
|
-
def SaveObj(obj:
|
|
251
|
+
def SaveObj(obj: key.CryptoKey, path: str, password: str | None, /) -> None:
|
|
244
252
|
"""Save object.
|
|
245
253
|
|
|
246
254
|
Args:
|
|
247
|
-
obj (
|
|
255
|
+
obj (cryptokey.CryptoKey): object
|
|
248
256
|
path (str): path
|
|
249
257
|
password (str | None): password
|
|
250
258
|
|
|
251
259
|
"""
|
|
252
|
-
|
|
253
|
-
blob: bytes =
|
|
254
|
-
logging.info('saved object: %s (%s)', path,
|
|
260
|
+
encryption_key: aes.AESKey | None = aes.AESKey.FromStaticPassword(password) if password else None
|
|
261
|
+
blob: bytes = key.Serialize(obj, file_path=path, encryption_key=encryption_key)
|
|
262
|
+
logging.info('saved object: %s (%s)', path, human.HumanizedBytes(len(blob)))
|
|
255
263
|
|
|
256
264
|
|
|
257
265
|
def LoadObj[T](path: str, password: str | None, expect: type[T], /) -> T:
|
|
@@ -269,8 +277,8 @@ def LoadObj[T](path: str, password: str | None, expect: type[T], /) -> T:
|
|
|
269
277
|
T: loaded object
|
|
270
278
|
|
|
271
279
|
"""
|
|
272
|
-
|
|
273
|
-
obj: T =
|
|
280
|
+
decryption_key: aes.AESKey | None = aes.AESKey.FromStaticPassword(password) if password else None
|
|
281
|
+
obj: T = key.DeSerialize(file_path=path, decryption_key=decryption_key)
|
|
274
282
|
if not isinstance(obj, expect):
|
|
275
283
|
raise base.InputError(
|
|
276
284
|
f'Object loaded from {path} is of invalid type {type(obj)}, expected {expect}'
|
|
@@ -427,7 +435,9 @@ def Main( # documentation is help/epilog/args # noqa: D103
|
|
|
427
435
|
if version:
|
|
428
436
|
typer.echo(__version__)
|
|
429
437
|
raise typer.Exit(0)
|
|
430
|
-
|
|
438
|
+
# initialize logging and get console
|
|
439
|
+
console: rich_console.Console
|
|
440
|
+
console, verbose, color = tc_logging.InitLogging(
|
|
431
441
|
verbose,
|
|
432
442
|
color=color,
|
|
433
443
|
include_process=False,
|
|
@@ -452,7 +462,7 @@ def Main( # documentation is help/epilog/args # noqa: D103
|
|
|
452
462
|
),
|
|
453
463
|
)
|
|
454
464
|
@clibase.CLIErrorGuard
|
|
455
|
-
def Markdown(*, ctx:
|
|
465
|
+
def Markdown(*, ctx: click.Context) -> None: # documentation is help/epilog/args # noqa: D103
|
|
456
466
|
config: TransConfig = ctx.obj
|
|
457
467
|
config.console.print(clibase.GenerateTyperHelpMarkdown(app, prog_name='transcrypto'))
|
|
458
468
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Balparda's TransCrypto base library."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
import codecs
|
|
9
|
+
from collections import abc
|
|
10
|
+
|
|
11
|
+
# Data conversion utils
|
|
12
|
+
|
|
13
|
+
# JSON types
|
|
14
|
+
type JSONValue = bool | int | float | str | list[JSONValue] | dict[str, JSONValue] | None
|
|
15
|
+
type JSONDict = dict[str, JSONValue]
|
|
16
|
+
|
|
17
|
+
BytesToHex: abc.Callable[[bytes], str] = lambda b: b.hex()
|
|
18
|
+
BytesToInt: abc.Callable[[bytes], int] = lambda b: int.from_bytes(b, 'big', signed=False)
|
|
19
|
+
BytesToEncoded: abc.Callable[[bytes], str] = lambda b: base64.urlsafe_b64encode(b).decode('ascii')
|
|
20
|
+
|
|
21
|
+
HexToBytes: abc.Callable[[str], bytes] = bytes.fromhex
|
|
22
|
+
IntToFixedBytes: abc.Callable[[int, int], bytes] = lambda i, n: i.to_bytes(n, 'big', signed=False)
|
|
23
|
+
IntToBytes: abc.Callable[[int], bytes] = lambda i: IntToFixedBytes(i, (i.bit_length() + 7) // 8)
|
|
24
|
+
IntToEncoded: abc.Callable[[int], str] = lambda i: BytesToEncoded(IntToBytes(i))
|
|
25
|
+
EncodedToBytes: abc.Callable[[str], bytes] = lambda e: base64.urlsafe_b64decode(e.encode('ascii'))
|
|
26
|
+
|
|
27
|
+
PadBytesTo: abc.Callable[[bytes, int], bytes] = lambda b, i: b.rjust((i + 7) // 8, b'\x00')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Error(Exception):
|
|
31
|
+
"""TransCrypto exception."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class InputError(Error):
|
|
35
|
+
"""Input exception (TransCrypto)."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ImplementationError(Error, NotImplementedError):
|
|
39
|
+
"""Feature is not implemented yet (TransCrypto)."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def BytesToRaw(b: bytes, /) -> str:
|
|
43
|
+
r"""Convert bytes to double-quoted string with \\xNN escapes where needed.
|
|
44
|
+
|
|
45
|
+
1. map bytes 0..255 to same code points (latin1)
|
|
46
|
+
2. escape non-printables/backslash/quotes via unicode_escape
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
b (bytes): input
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
str: double-quoted string with \\xNN escapes where needed
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
inner: str = b.decode('latin1').encode('unicode_escape').decode('ascii')
|
|
56
|
+
return f'"{inner.replace('"', r"\"")}"'
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def RawToBytes(s: str, /) -> bytes:
|
|
60
|
+
r"""Convert double-quoted string with \\xNN escapes where needed to bytes.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
s (str): input (expects a double-quoted string; parses \\xNN, \n, \\ etc)
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
bytes: data
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
if len(s) >= 2 and s[0] == s[-1] == '"': # noqa: PLR2004
|
|
70
|
+
s = s[1:-1]
|
|
71
|
+
# decode backslash escapes to code points, then map 0..255 -> bytes
|
|
72
|
+
return codecs.decode(s, 'unicode_escape').encode('latin1')
|