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 CHANGED
@@ -3,5 +3,5 @@
3
3
  """Basic cryptography primitives implementation."""
4
4
 
5
5
  __all__: list[str] = ['__author__', '__version__']
6
- __version__ = '1.8.0' # remember to also update pyproject.toml
6
+ __version__ = '2.0.0' # remember to also update pyproject.toml
7
7
  __author__ = 'Daniel Balparda <balparda@github.com>'
@@ -10,8 +10,10 @@ import re
10
10
  import click
11
11
  import typer
12
12
 
13
- from transcrypto import aes, base, transcrypto
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(base.Hash256(bt), config.output_format))
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(base.Hash512(bt), config.output_format))
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(base.FileHash(str(path), digest=digest), config.output_format)
109
+ transcrypto.BytesToText(hashes.FileHash(str(path), digest=digest), config.output_format)
108
110
  )
109
111
 
110
112
 
@@ -8,8 +8,10 @@ import glob
8
8
 
9
9
  import typer
10
10
 
11
- from transcrypto import base, sss, transcrypto
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: base.PrivateBid512 = base.PrivateBid512.New(secret_bytes)
49
- bid_pub: base.PublicBid512 = base.PublicBid512.Copy(bid_priv)
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: base.PrivateBid512 = transcrypto.LoadObj(
71
- base_path + '.priv', config.protect, base.PrivateBid512
72
+ bid_priv: bid.PrivateBid512 = transcrypto.LoadObj(
73
+ base_path + '.priv', config.protect, bid.PrivateBid512
72
74
  )
73
- bid_pub: base.PublicBid512 = transcrypto.LoadObj(
74
- base_path + '.pub', config.protect, base.PublicBid512
75
+ bid_pub: bid.PublicBid512 = transcrypto.LoadObj(
76
+ base_path + '.pub', config.protect, bid.PublicBid512
75
77
  )
76
- bid_pub_expect: base.PublicBid512 = base.PublicBid512.Copy(bid_priv)
78
+ bid_pub_expect: bid.PublicBid512 = bid.PublicBid512.Copy(bid_priv)
77
79
  config.console.print(
78
80
  'Bid commitment: '
79
81
  + (
@@ -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
- __console_lock: threading.RLock = threading.RLock()
44
- __console_singleton: rich_console.Console | None = None
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
- Raises:
99
- RuntimeError: if you call this more than once
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 level is verbose already)
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 each command (also it raises on duplicate loggers)
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,
@@ -6,8 +6,10 @@ from __future__ import annotations
6
6
 
7
7
  import typer
8
8
 
9
- from transcrypto import base, modmath, transcrypto
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(base.GCD(a_i, b_i))
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(base.ExtendedGCD(a_i, b_i)))
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(base.RandBits(bits))
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(base.RandInt(min_i, max_i))
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(base.RandBytes(n), config.output_format))
214
+ config.console.print(transcrypto.BytesToText(saferandom.RandBytes(n), config.output_format))
213
215
 
214
216
 
215
217
  @random_app.command(
@@ -6,8 +6,9 @@ from __future__ import annotations
6
6
 
7
7
  import typer
8
8
 
9
- from transcrypto import dsa, elgamal, rsa, transcrypto
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
 
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Core crypto logic."""
@@ -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 base
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(base.CryptoKey, base.Encryptor, base.Decryptor):
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={base.ObfuscateSecret(self.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(base.Encryptor, base.Decryptor):
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 = base.RandBytes(16)
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 base.CryptoError('failed decryption') from err
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
@@ -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
+ )