transcrypto 1.5.1__py3-none-any.whl → 1.7.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/sss.py CHANGED
@@ -1,7 +1,5 @@
1
- #!/usr/bin/env python3
2
- #
3
- # Copyright 2025 Daniel Balparda (balparda@github.com) - Apache-2.0 license
4
- #
1
+ # SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
2
+ # SPDX-License-Identifier: Apache-2.0
5
3
  """Balparda's TransCrypto Shamir Shared Secret (SSS) library.
6
4
 
7
5
  <https://en.wikipedia.org/wiki/Shamir's_secret_sharing>
@@ -11,15 +9,10 @@ from __future__ import annotations
11
9
 
12
10
  import dataclasses
13
11
  import logging
14
- # import pdb
15
- from typing import Collection, Generator, Self
16
-
17
- from . import base, modmath, aes
18
-
19
- __author__ = 'balparda@github.com'
20
- __version__: str = base.__version__ # version comes from base!
21
- __version_tuple__: tuple[int, ...] = base.__version_tuple__
12
+ from collections import abc
13
+ from typing import Self
22
14
 
15
+ from . import aes, base, modmath
23
16
 
24
17
  # fixed prefixes: do NOT ever change! will break all encryption and signature schemes
25
18
  _SSS_ENCRYPTION_AAD_PREFIX = b'transcrypto.SSS.Sharing.1.0\x00'
@@ -35,6 +28,7 @@ class ShamirSharedSecretPublic(base.CryptoKey):
35
28
  Attributes:
36
29
  minimum (int): minimum shares needed for recovery, ≥ 2
37
30
  modulus (int): prime modulus used for share generation, prime, ≥ 2
31
+
38
32
  """
39
33
 
40
34
  minimum: int
@@ -45,11 +39,9 @@ class ShamirSharedSecretPublic(base.CryptoKey):
45
39
 
46
40
  Raises:
47
41
  InputError: invalid inputs
42
+
48
43
  """
49
- super(ShamirSharedSecretPublic, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
50
- if (self.modulus < 2 or
51
- not modmath.IsPrime(self.modulus) or
52
- self.minimum < 2):
44
+ if self.modulus < 2 or not modmath.IsPrime(self.modulus) or self.minimum < 2: # noqa: PLR2004
53
45
  raise base.InputError(f'invalid modulus or minimum: {self}')
54
46
 
55
47
  def __str__(self) -> str:
@@ -57,11 +49,14 @@ class ShamirSharedSecretPublic(base.CryptoKey):
57
49
 
58
50
  Returns:
59
51
  string representation of ShamirSharedSecretPublic
52
+
60
53
  """
61
- return ('ShamirSharedSecretPublic('
62
- f'bits={self.modulus.bit_length()}, '
63
- f'minimum={self.minimum}, '
64
- f'modulus={base.IntToEncoded(self.modulus)})')
54
+ return (
55
+ 'ShamirSharedSecretPublic('
56
+ f'bits={self.modulus.bit_length()}, '
57
+ f'minimum={self.minimum}, '
58
+ f'modulus={base.IntToEncoded(self.modulus)})'
59
+ )
65
60
 
66
61
  @property
67
62
  def modulus_size(self) -> int:
@@ -69,7 +64,8 @@ class ShamirSharedSecretPublic(base.CryptoKey):
69
64
  return (self.modulus.bit_length() + 7) // 8
70
65
 
71
66
  def RawRecoverSecret(
72
- self, shares: Collection[ShamirSharePrivate], /, *, force_recover: bool = False) -> int:
67
+ self, shares: abc.Collection[ShamirSharePrivate], /, *, force_recover: bool = False
68
+ ) -> int:
73
69
  """Recover the secret from ShamirSharePrivate objects.
74
70
 
75
71
  BEWARE: This is raw SSS, no modern message wrapping, padding or validation!
@@ -88,6 +84,7 @@ class ShamirSharedSecretPublic(base.CryptoKey):
88
84
  Raises:
89
85
  InputError: invalid inputs
90
86
  CryptoError: secret cannot be recovered (number of shares < `minimum`)
87
+
91
88
  """
92
89
  # check that we have enough shares by de-duping them first
93
90
  share_points: dict[int, int] = {}
@@ -98,7 +95,8 @@ class ShamirSharedSecretPublic(base.CryptoKey):
98
95
  if k in share_points:
99
96
  if v != share_points[k]:
100
97
  raise base.InputError(
101
- f'{share} key/value {k}/{v} duplicated with conflicting value in {share_dict[k]}')
98
+ f'{share} key/value {k}/{v} duplicated with conflicting value in {share_dict[k]}'
99
+ )
102
100
  logging.warning(f'{share} key/value {k}/{v} is a duplicate of {share_dict[k]}: DISCARDED')
103
101
  continue
104
102
  share_points[k] = v
@@ -115,7 +113,15 @@ class ShamirSharedSecretPublic(base.CryptoKey):
115
113
 
116
114
  @classmethod
117
115
  def Copy(cls, other: ShamirSharedSecretPublic, /) -> Self:
118
- """Initialize a public key by taking the public parts of a public/private key."""
116
+ """Initialize a public key by taking the public parts of a public/private key.
117
+
118
+ Args:
119
+ other (ShamirSharedSecretPublic): object to copy from
120
+
121
+ Returns:
122
+ Self: a new ShamirSharedSecretPublic
123
+
124
+ """
119
125
  return cls(minimum=other.minimum, modulus=other.modulus)
120
126
 
121
127
 
@@ -132,6 +138,7 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
132
138
 
133
139
  Attributes:
134
140
  polynomial (list[int]): prime coefficients for generation poly., each modulus.bit_length() size
141
+
135
142
  """
136
143
 
137
144
  polynomial: list[int]
@@ -141,13 +148,18 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
141
148
 
142
149
  Raises:
143
150
  InputError: invalid inputs
151
+
144
152
  """
145
- super(ShamirSharedSecretPrivate, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
146
- if (len(self.polynomial) != self.minimum - 1 or # exactly this size
147
- len(set(self.polynomial)) != self.minimum - 1 or # no duplicate
148
- self.modulus in self.polynomial or # different from modulus
149
- any(not modmath.IsPrime(p) or p.bit_length() != self.modulus.bit_length()
150
- for p in self.polynomial)): # all primes and the right size
153
+ super(ShamirSharedSecretPrivate, self).__post_init__()
154
+ if (
155
+ len(self.polynomial) != self.minimum - 1 # exactly this size
156
+ or len(set(self.polynomial)) != self.minimum - 1 # no duplicate
157
+ or self.modulus in self.polynomial # different from modulus
158
+ or any(
159
+ not modmath.IsPrime(p) or p.bit_length() != self.modulus.bit_length()
160
+ for p in self.polynomial
161
+ )
162
+ ): # all primes and the right size
151
163
  raise base.InputError(f'invalid polynomial: {self}')
152
164
 
153
165
  def __str__(self) -> str:
@@ -155,10 +167,13 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
155
167
 
156
168
  Returns:
157
169
  string representation of ShamirSharedSecretPrivate without leaking secrets
170
+
158
171
  """
159
- return ('ShamirSharedSecretPrivate('
160
- f'{super(ShamirSharedSecretPrivate, self).__str__()}, ' # pylint: disable=super-with-arguments
161
- f'polynomial=[{", ".join(base.ObfuscateSecret(i) for i in self.polynomial)}])')
172
+ return (
173
+ 'ShamirSharedSecretPrivate('
174
+ f'{super(ShamirSharedSecretPrivate, self).__str__()}, '
175
+ f'polynomial=[{", ".join(base.ObfuscateSecret(i) for i in self.polynomial)}])'
176
+ )
162
177
 
163
178
  def RawShare(self, secret: int, /, *, share_key: int = 0) -> ShamirSharePrivate:
164
179
  """Make a new ShamirSharePrivate for the `secret`.
@@ -178,6 +193,7 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
178
193
 
179
194
  Raises:
180
195
  InputError: invalid inputs
196
+
181
197
  """
182
198
  # test inputs
183
199
  if not 0 <= secret < self.modulus:
@@ -185,18 +201,19 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
185
201
  if not 1 <= share_key < self.modulus:
186
202
  if not share_key: # default is zero, and that means we generate it here
187
203
  share_key = 0
188
- while not share_key or share_key in self.polynomial:
204
+ while not share_key or share_key in self.polynomial or share_key >= self.modulus:
189
205
  share_key = base.RandBits(self.modulus.bit_length() - 1)
190
206
  else:
191
207
  raise base.InputError(f'invalid share_key: {share_key=}')
192
208
  # build object
193
209
  return ShamirSharePrivate(
194
- minimum=self.minimum, modulus=self.modulus,
195
- share_key=share_key,
196
- share_value=modmath.ModPolynomial(share_key, [secret] + self.polynomial, self.modulus))
210
+ minimum=self.minimum,
211
+ modulus=self.modulus,
212
+ share_key=share_key,
213
+ share_value=modmath.ModPolynomial(share_key, [secret, *self.polynomial], self.modulus),
214
+ )
197
215
 
198
- def RawShares(
199
- self, secret: int, /, *, max_shares: int = 0) -> Generator[ShamirSharePrivate, None, None]:
216
+ def RawShares(self, secret: int, /, *, max_shares: int = 0) -> abc.Generator[ShamirSharePrivate]:
200
217
  """Make any number of ShamirSharePrivate for the `secret`.
201
218
 
202
219
  BEWARE: This is raw SSS, no modern message wrapping, padding or validation!
@@ -213,6 +230,7 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
213
230
 
214
231
  Raises:
215
232
  InputError: invalid inputs
233
+
216
234
  """
217
235
  # test inputs
218
236
  if max_shares and max_shares < self.minimum:
@@ -222,7 +240,12 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
222
240
  used_keys: set[int] = set()
223
241
  while not max_shares or count < max_shares:
224
242
  share_key: int = 0
225
- while not share_key or share_key in self.polynomial or share_key in used_keys:
243
+ while (
244
+ not share_key
245
+ or share_key in self.polynomial
246
+ or share_key in used_keys
247
+ or share_key >= self.modulus
248
+ ):
226
249
  share_key = base.RandBits(self.modulus.bit_length() - 1)
227
250
  try:
228
251
  yield self.RawShare(secret, share_key=share_key)
@@ -251,28 +274,34 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
251
274
 
252
275
  Raises:
253
276
  InputError: invalid inputs
254
- CryptoError: internal crypto failures
277
+
255
278
  """
256
279
  if total_shares < self.minimum:
257
280
  raise base.InputError(f'invalid total_shares: {total_shares=} < {self.minimum=}')
258
281
  k: int = self.modulus_size
259
- if k <= 32:
282
+ if k <= 32: # noqa: PLR2004
260
283
  raise base.InputError(f'modulus too small for key operations: {k} bytes')
261
284
  key256: bytes = base.RandBytes(32)
262
285
  shares: list[ShamirSharePrivate] = list(
263
- self.RawShares(base.BytesToInt(key256), max_shares=total_shares))
286
+ self.RawShares(base.BytesToInt(key256), max_shares=total_shares)
287
+ )
264
288
  aad: bytes = (
265
- _SSS_ENCRYPTION_AAD_PREFIX +
266
- base.IntToFixedBytes(self.minimum, 8) + base.IntToFixedBytes(self.modulus, k))
289
+ _SSS_ENCRYPTION_AAD_PREFIX
290
+ + base.IntToFixedBytes(self.minimum, 8)
291
+ + base.IntToFixedBytes(self.modulus, k)
292
+ )
267
293
  aead_key: bytes = base.Hash512(_SSS_ENCRYPTION_AAD_PREFIX + key256)
268
294
  ct: bytes = aes.AESKey(key256=aead_key[32:]).Encrypt(secret, associated_data=aad)
269
- return [ShamirShareData(
295
+ return [
296
+ ShamirShareData(
270
297
  minimum=s.minimum,
271
298
  modulus=s.modulus,
272
299
  share_key=s.share_key,
273
300
  share_value=s.share_value,
274
301
  encrypted_data=ct,
275
- ) for s in shares]
302
+ )
303
+ for s in shares
304
+ ]
276
305
 
277
306
  def RawVerifyShare(self, secret: int, share: ShamirSharePrivate, /) -> bool:
278
307
  """Verify a ShamirSharePrivate object for the `secret`.
@@ -289,14 +318,12 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
289
318
  Returns:
290
319
  True if share is valid; False otherwise
291
320
 
292
- Raises:
293
- InputError: invalid inputs
294
321
  """
295
322
  return share == self.RawShare(secret, share_key=share.share_key)
296
323
 
297
324
  @classmethod
298
325
  def New(cls, minimum_shares: int, bit_length: int, /) -> Self:
299
- """Makes a new private SSS object of `bit_length` bits prime modulus and coefficients.
326
+ """Make a new private SSS object of `bit_length` bits prime modulus and coefficients.
300
327
 
301
328
  Args:
302
329
  minimum_shares (int): minimum shares needed for recovery, ≥ 2
@@ -307,11 +334,12 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
307
334
 
308
335
  Raises:
309
336
  InputError: invalid inputs
337
+
310
338
  """
311
339
  # test inputs
312
- if minimum_shares < 2:
340
+ if minimum_shares < 2: # noqa: PLR2004
313
341
  raise base.InputError(f'at least 2 shares are needed: {minimum_shares=}')
314
- if bit_length < 10:
342
+ if bit_length < 10: # noqa: PLR2004
315
343
  raise base.InputError(f'invalid bit length: {bit_length=}')
316
344
  # make the primes
317
345
  unique_primes: set[int] = modmath.NBitRandomPrimes(bit_length, n_primes=minimum_shares)
@@ -335,6 +363,7 @@ class ShamirSharePrivate(ShamirSharedSecretPublic):
335
363
  Attributes:
336
364
  share_key (int): share secret key; a randomly picked value, 1 ≤ k < modulus
337
365
  share_value (int): share secret value, 1 ≤ v < modulus; (k, v) is a "point" of f(k)=v
366
+
338
367
  """
339
368
 
340
369
  share_key: int
@@ -345,10 +374,10 @@ class ShamirSharePrivate(ShamirSharedSecretPublic):
345
374
 
346
375
  Raises:
347
376
  InputError: invalid inputs
377
+
348
378
  """
349
- super(ShamirSharePrivate, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
350
- if (not 0 < self.share_key < self.modulus or
351
- not 0 < self.share_value < self.modulus):
379
+ super(ShamirSharePrivate, self).__post_init__()
380
+ if not 0 < self.share_key < self.modulus or not 0 < self.share_value < self.modulus:
352
381
  raise base.InputError(f'invalid share: {self}')
353
382
 
354
383
  def __str__(self) -> str:
@@ -356,18 +385,32 @@ class ShamirSharePrivate(ShamirSharedSecretPublic):
356
385
 
357
386
  Returns:
358
387
  string representation of ShamirSharePrivate without leaking secrets
388
+
359
389
  """
360
- return ('ShamirSharePrivate('
361
- f'{super(ShamirSharePrivate, self).__str__()}, ' # pylint: disable=super-with-arguments
362
- f'share_key={base.ObfuscateSecret(self.share_key)}, '
363
- f'share_value={base.ObfuscateSecret(self.share_value)})')
390
+ return (
391
+ 'ShamirSharePrivate('
392
+ f'{super(ShamirSharePrivate, self).__str__()}, '
393
+ f'share_key={base.ObfuscateSecret(self.share_key)}, '
394
+ f'share_value={base.ObfuscateSecret(self.share_value)})'
395
+ )
364
396
 
365
397
  @classmethod
366
398
  def CopyShare(cls, other: ShamirSharePrivate, /) -> Self:
367
- """Initialize a share taking the parts of another share."""
399
+ """Initialize a share taking the parts of another share.
400
+
401
+ Args:
402
+ other (ShamirSharePrivate): object to copy from
403
+
404
+ Returns:
405
+ Self: a new ShamirSharePrivate
406
+
407
+ """
368
408
  return cls(
369
- minimum=other.minimum, modulus=other.modulus,
370
- share_key=other.share_key, share_value=other.share_value)
409
+ minimum=other.minimum,
410
+ modulus=other.modulus,
411
+ share_key=other.share_key,
412
+ share_value=other.share_value,
413
+ )
371
414
 
372
415
 
373
416
  @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
@@ -380,6 +423,8 @@ class ShamirShareData(ShamirSharePrivate):
380
423
  Attributes:
381
424
  share_key (int): share secret key; a randomly picked value, 1 ≤ k < modulus
382
425
  share_value (int): share secret value, 1 ≤ v < modulus; (k, v) is a "point" of f(k)=v
426
+ encrypted_data (bytes): AES-256-GCM encrypted secret data with IV and tag
427
+
383
428
  """
384
429
 
385
430
  encrypted_data: bytes
@@ -389,9 +434,10 @@ class ShamirShareData(ShamirSharePrivate):
389
434
 
390
435
  Raises:
391
436
  InputError: invalid inputs
437
+
392
438
  """
393
- super(ShamirShareData, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
394
- if len(self.encrypted_data) < 32:
439
+ super(ShamirShareData, self).__post_init__()
440
+ if len(self.encrypted_data) < 32: # noqa: PLR2004
395
441
  raise base.InputError(f'AES256+GCM SSS should have ≥32 bytes IV/CT/tag: {self}')
396
442
 
397
443
  def __str__(self) -> str:
@@ -399,10 +445,13 @@ class ShamirShareData(ShamirSharePrivate):
399
445
 
400
446
  Returns:
401
447
  string representation of ShamirShareData without leaking secrets
448
+
402
449
  """
403
- return ('ShamirShareData('
404
- f'{super(ShamirShareData, self).__str__()}, ' # pylint: disable=super-with-arguments
405
- f'encrypted_data={base.ObfuscateSecret(self.encrypted_data)})')
450
+ return (
451
+ 'ShamirShareData('
452
+ f'{super(ShamirShareData, self).__str__()}, '
453
+ f'encrypted_data={base.ObfuscateSecret(self.encrypted_data)})'
454
+ )
406
455
 
407
456
  def RecoverData(self, other_shares: list[ShamirSharePrivate]) -> bytes:
408
457
  """Recover the encrypted data from ShamirSharePrivate objects.
@@ -420,17 +469,20 @@ class ShamirShareData(ShamirSharePrivate):
420
469
  Raises:
421
470
  InputError: invalid inputs
422
471
  CryptoError: internal crypto failures, authentication failure, key mismatch, etc
472
+
423
473
  """
424
474
  k: int = self.modulus_size
425
- if k <= 32:
475
+ if k <= 32: # noqa: PLR2004
426
476
  raise base.InputError(f'modulus too small for key operations: {k} bytes')
427
477
  # recover secret; raise if shares are invalid
428
- secret: int = self.RawRecoverSecret([self] + other_shares)
478
+ secret: int = self.RawRecoverSecret([self, *other_shares])
429
479
  if not 0 <= secret < (1 << 256):
430
480
  raise base.CryptoError('recovered key out of range for 256-bit key')
431
481
  key256: bytes = base.IntToFixedBytes(secret, 32)
432
482
  aad: bytes = (
433
- _SSS_ENCRYPTION_AAD_PREFIX +
434
- base.IntToFixedBytes(self.minimum, 8) + base.IntToFixedBytes(self.modulus, k))
483
+ _SSS_ENCRYPTION_AAD_PREFIX
484
+ + base.IntToFixedBytes(self.minimum, 8)
485
+ + base.IntToFixedBytes(self.modulus, k)
486
+ )
435
487
  aead_key: bytes = base.Hash512(_SSS_ENCRYPTION_AAD_PREFIX + key256)
436
488
  return aes.AESKey(key256=aead_key[32:]).Decrypt(self.encrypted_data, associated_data=aad)