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.
@@ -12,14 +12,15 @@ import logging
12
12
  from collections import abc
13
13
  from typing import Self
14
14
 
15
- from . import aes, base, modmath
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(base.CryptoKey):
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 base.CryptoError(f'unrecoverable secret: {mess}')
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(base.ObfuscateSecret(i) for i in self.polynomial)}])'
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 = base.RandBits(self.modulus.bit_length() - 1)
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 = base.RandBits(self.modulus.bit_length() - 1)
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 = base.RandBytes(32)
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 = base.Hash512(_SSS_ENCRYPTION_AAD_PREFIX + key256)
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
- base.RandShuffle(ordered_primes)
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={base.ObfuscateSecret(self.share_key)}, '
394
- f'share_value={base.ObfuscateSecret(self.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={base.ObfuscateSecret(self.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 base.CryptoError('recovered key out of range for 256-bit key')
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 = base.Hash512(_SSS_ENCRYPTION_AAD_PREFIX + key256)
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
@@ -25,13 +25,24 @@ import typer
25
25
  from rich import console as rich_console
26
26
 
27
27
  from transcrypto.cli import clibase
28
+ from transcrypto.core import dsa, modmath
29
+ from transcrypto.utils import human, timer
30
+ from transcrypto.utils import logging as tc_logging
28
31
 
29
- from . import __version__, base, dsa, modmath
32
+ from . import __version__
30
33
 
31
34
 
32
35
  @dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
33
36
  class ProfilerConfig(clibase.CLIConfig):
34
- """CLI global context, storing the configuration."""
37
+ """CLI global context, storing the configuration.
38
+
39
+ Attributes:
40
+ serial (bool): Whether to run profiling serially (vs parallel)
41
+ repeats (int): Number of repetitions for each profiling run
42
+ confidence (int): Confidence level percentage for statistical analysis
43
+ bits (tuple[int, int, int]): Bit sizes range (start, stop, step) for profiling
44
+
45
+ """
35
46
 
36
47
  serial: bool
37
48
  repeats: int
@@ -124,7 +135,9 @@ def Main( # documentation is help/epilog/args # noqa: D103
124
135
  if version:
125
136
  typer.echo(__version__)
126
137
  raise typer.Exit(0)
127
- console, verbose, color = clibase.InitLogging(
138
+ # initialize logging and get console
139
+ console: rich_console.Console
140
+ console, verbose, color = tc_logging.InitLogging(
128
141
  verbose,
129
142
  color=color,
130
143
  include_process=False, # decide if you want process names in logs
@@ -211,7 +224,7 @@ def DSA(*, ctx: typer.Context) -> None: # documentation is help/epilog/args # n
211
224
  )
212
225
  @clibase.CLIErrorGuard
213
226
  def Markdown() -> None: # documentation is help/epilog/args # noqa: D103
214
- console: rich_console.Console = clibase.Console()
227
+ console: rich_console.Console = tc_logging.Console()
215
228
  console.print(clibase.GenerateTyperHelpMarkdown(app, prog_name='profiler'))
216
229
 
217
230
 
@@ -223,20 +236,20 @@ def _PrimeProfiler(
223
236
  confidence: float,
224
237
  /,
225
238
  ) -> None:
226
- with base.Timer(emit_log=False) as total_time:
239
+ with timer.Timer(emit_log=False) as total_time:
227
240
  primes: dict[int, list[float]] = {}
228
241
  for n_bits in range(*n_bits_range):
229
242
  # investigate for size n_bits
230
243
  primes[n_bits] = []
231
244
  for _ in range(repeats):
232
- with base.Timer(emit_log=False) as run_time:
245
+ with timer.Timer(emit_log=False) as run_time:
233
246
  pr: int = prime_callable(n_bits)
234
247
  assert pr # noqa: S101
235
248
  assert pr.bit_length() == n_bits # noqa: S101
236
249
  primes[n_bits].append(run_time.elapsed)
237
250
  # finished collecting n_bits-sized primes
238
- measurements: str = base.HumanizedMeasurements(
239
- primes[n_bits], parser=base.HumanizedSeconds, confidence=confidence
251
+ measurements: str = human.HumanizedMeasurements(
252
+ primes[n_bits], parser=human.HumanizedSeconds, confidence=confidence
240
253
  )
241
254
  console.print(f'{n_bits} → {measurements}')
242
255
  console.print(f'Finished in {total_time}')
@@ -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: base.CryptoKey, path: str, password: str | None, /) -> None:
251
+ def SaveObj(obj: key.CryptoKey, path: str, password: str | None, /) -> None:
244
252
  """Save object.
245
253
 
246
254
  Args:
247
- obj (base.CryptoKey): object
255
+ obj (cryptokey.CryptoKey): object
248
256
  path (str): path
249
257
  password (str | None): password
250
258
 
251
259
  """
252
- key: aes.AESKey | None = aes.AESKey.FromStaticPassword(password) if password else None
253
- blob: bytes = base.Serialize(obj, file_path=path, key=key)
254
- logging.info('saved object: %s (%s)', path, base.HumanizedBytes(len(blob)))
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
- key: aes.AESKey | None = aes.AESKey.FromStaticPassword(password) if password else None
273
- obj: T = base.DeSerialize(file_path=path, key=key)
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
- console, verbose, color = clibase.InitLogging(
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,
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Utility modules."""
@@ -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')