transcrypto 1.0.2__py3-none-any.whl → 1.1.1__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/aes.py +257 -0
- transcrypto/base.py +1018 -0
- transcrypto/dsa.py +336 -0
- transcrypto/elgamal.py +333 -0
- transcrypto/modmath.py +535 -0
- transcrypto/rsa.py +416 -0
- transcrypto/sss.py +299 -0
- transcrypto/transcrypto.py +1367 -276
- transcrypto-1.1.1.dist-info/METADATA +2257 -0
- transcrypto-1.1.1.dist-info/RECORD +15 -0
- transcrypto-1.0.2.dist-info/METADATA +0 -147
- transcrypto-1.0.2.dist-info/RECORD +0 -8
- {transcrypto-1.0.2.dist-info → transcrypto-1.1.1.dist-info}/WHEEL +0 -0
- {transcrypto-1.0.2.dist-info → transcrypto-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {transcrypto-1.0.2.dist-info → transcrypto-1.1.1.dist-info}/top_level.txt +0 -0
transcrypto/sss.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025 Daniel Balparda (balparda@github.com) - Apache-2.0 license
|
|
4
|
+
#
|
|
5
|
+
"""Balparda's TransCrypto Shamir Shared Secret (SSS) library.
|
|
6
|
+
|
|
7
|
+
<https://en.wikipedia.org/wiki/Shamir's_secret_sharing>
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import dataclasses
|
|
13
|
+
import logging
|
|
14
|
+
# import pdb
|
|
15
|
+
from typing import Collection, Generator, Self
|
|
16
|
+
|
|
17
|
+
from . import base
|
|
18
|
+
from . import modmath
|
|
19
|
+
|
|
20
|
+
__author__ = 'balparda@github.com'
|
|
21
|
+
__version__: str = base.__version__ # version comes from base!
|
|
22
|
+
__version_tuple__: tuple[int, ...] = base.__version_tuple__
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
26
|
+
class ShamirSharedSecretPublic(base.CryptoKey):
|
|
27
|
+
"""Shamir Shared Secret (SSS) public part.
|
|
28
|
+
|
|
29
|
+
BEWARE: This is raw SSS, no modern message wrapping, padding or validation!
|
|
30
|
+
These are pedagogical/raw primitives; do not use for new protocols.
|
|
31
|
+
No measures are taken here to prevent timing attacks.
|
|
32
|
+
|
|
33
|
+
This is the information-theoretic SSS but with no authentication or binding between
|
|
34
|
+
share and secret. Malicious share injection is possible! Add MAC or digital signature
|
|
35
|
+
in hostile settings.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
minimum (int): minimum shares needed for recovery, ≥ 2
|
|
39
|
+
modulus (int): prime modulus used for share generation, prime, ≥ 2
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
minimum: int
|
|
43
|
+
modulus: int
|
|
44
|
+
|
|
45
|
+
def __post_init__(self) -> None:
|
|
46
|
+
"""Check data.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
InputError: invalid inputs
|
|
50
|
+
"""
|
|
51
|
+
super(ShamirSharedSecretPublic, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
|
|
52
|
+
if (self.modulus < 2 or
|
|
53
|
+
not modmath.IsPrime(self.modulus) or
|
|
54
|
+
self.minimum < 2):
|
|
55
|
+
raise base.InputError(f'invalid modulus or minimum: {self}')
|
|
56
|
+
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
"""Safe string representation of the ShamirSharedSecretPublic.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
string representation of ShamirSharedSecretPublic
|
|
62
|
+
"""
|
|
63
|
+
return ('ShamirSharedSecretPublic('
|
|
64
|
+
f'minimum={self.minimum}, '
|
|
65
|
+
f'modulus={base.IntToEncoded(self.modulus)})')
|
|
66
|
+
|
|
67
|
+
def RecoverSecret(
|
|
68
|
+
self, shares: Collection[ShamirSharePrivate], /, *, force_recover: bool = False) -> int:
|
|
69
|
+
"""Recover the secret from ShamirSharePrivate objects.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
shares (Collection[ShamirSharePrivate]): shares to use to recover the secret
|
|
73
|
+
force_recover (bool, optional): if True will try to recover (default: False)
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
the integer secret if all shares are correct and in the correct number; if there are
|
|
77
|
+
no "excess" shares, there can be no way to know if the recovered secret is the correct one
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
InputError: invalid inputs
|
|
81
|
+
CryptoError: secret cannot be recovered (number of shares < `minimum`)
|
|
82
|
+
"""
|
|
83
|
+
# check that we have enough shares by de-duping them first
|
|
84
|
+
share_points: dict[int, int] = {}
|
|
85
|
+
share_dict: dict[int, ShamirSharePrivate] = {}
|
|
86
|
+
for share in shares:
|
|
87
|
+
k: int = share.share_key % self.modulus
|
|
88
|
+
v: int = share.share_value % self.modulus
|
|
89
|
+
if k in share_points:
|
|
90
|
+
if v != share_points[k]:
|
|
91
|
+
raise base.InputError(
|
|
92
|
+
f'{share} key/value {k}/{v} duplicated with conflicting value in {share_dict[k]}')
|
|
93
|
+
logging.warning(f'{share} key/value {k}/{v} is a duplicate of {share_dict[k]}: DISCARDED')
|
|
94
|
+
continue
|
|
95
|
+
share_points[k] = v
|
|
96
|
+
share_dict[k] = share
|
|
97
|
+
# if we don't have enough shares, complain loudly
|
|
98
|
+
if (given_shares := len(share_points)) < self.minimum:
|
|
99
|
+
mess: str = f'distinct shares {given_shares} < minimum shares {self.minimum}'
|
|
100
|
+
if force_recover and given_shares > 1:
|
|
101
|
+
logging.error(f'recovering secret even though: {mess}')
|
|
102
|
+
else:
|
|
103
|
+
raise base.CryptoError(f'unrecoverable secret: {mess}')
|
|
104
|
+
# do the math
|
|
105
|
+
return modmath.ModLagrangeInterpolate(0, share_points, self.modulus)
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def Copy(cls, other: ShamirSharedSecretPublic, /) -> Self:
|
|
109
|
+
"""Initialize a public key by taking the public parts of a public/private key."""
|
|
110
|
+
return cls(minimum=other.minimum, modulus=other.modulus)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
114
|
+
class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
|
|
115
|
+
"""Shamir Shared Secret (SSS) private keys.
|
|
116
|
+
|
|
117
|
+
BEWARE: This is raw SSS, no modern message wrapping, padding or validation!
|
|
118
|
+
These are pedagogical/raw primitives; do not use for new protocols.
|
|
119
|
+
No measures are taken here to prevent timing attacks.
|
|
120
|
+
|
|
121
|
+
We deliberately choose prime coefficients. This shrinks the key-space and leaks a bit of
|
|
122
|
+
structure. It is "unusual", but with large enough modulus (bit length > ~ 500) it makes no
|
|
123
|
+
difference because there will be plenty entropy in these primes.
|
|
124
|
+
|
|
125
|
+
Attributes:
|
|
126
|
+
polynomial (list[int]): prime coefficients for generation poly., each modulus.bit_length() size
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
polynomial: list[int]
|
|
130
|
+
|
|
131
|
+
def __post_init__(self) -> None:
|
|
132
|
+
"""Check data.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
InputError: invalid inputs
|
|
136
|
+
"""
|
|
137
|
+
super(ShamirSharedSecretPrivate, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
|
|
138
|
+
if (len(self.polynomial) != self.minimum - 1 or # exactly this size
|
|
139
|
+
len(set(self.polynomial)) != self.minimum - 1 or # no duplicate
|
|
140
|
+
self.modulus in self.polynomial or # different from modulus
|
|
141
|
+
any(not modmath.IsPrime(p) or p.bit_length() != self.modulus.bit_length()
|
|
142
|
+
for p in self.polynomial)): # all primes and the right size
|
|
143
|
+
raise base.InputError(f'invalid polynomial: {self}')
|
|
144
|
+
|
|
145
|
+
def __str__(self) -> str:
|
|
146
|
+
"""Safe (no secrets) string representation of the ShamirSharedSecretPrivate.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
string representation of ShamirSharedSecretPrivate without leaking secrets
|
|
150
|
+
"""
|
|
151
|
+
return (f'ShamirSharedSecretPrivate({super(ShamirSharedSecretPrivate, self).__str__()}, ' # pylint: disable=super-with-arguments
|
|
152
|
+
f'polynomial=[{", ".join(base.ObfuscateSecret(i) for i in self.polynomial)}])')
|
|
153
|
+
|
|
154
|
+
def Share(self, secret: int, /, *, share_key: int = 0) -> ShamirSharePrivate:
|
|
155
|
+
"""Make a new ShamirSharePrivate for the `secret`.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
secret (int): secret message to encrypt and share, 0 ≤ s < modulus
|
|
159
|
+
share_key (int, optional): if given, a random value to use, 1 ≤ r < modulus;
|
|
160
|
+
else will generate randomly
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
ShamirSharePrivate object
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
InputError: invalid inputs
|
|
167
|
+
"""
|
|
168
|
+
# test inputs
|
|
169
|
+
if not 0 <= secret < self.modulus:
|
|
170
|
+
raise base.InputError(f'invalid secret: {secret=}')
|
|
171
|
+
if not 1 <= share_key < self.modulus:
|
|
172
|
+
if not share_key: # default is zero, and that means we generate it here
|
|
173
|
+
share_key = 0
|
|
174
|
+
while not share_key or share_key in self.polynomial:
|
|
175
|
+
share_key = base.RandBits(self.modulus.bit_length() - 1)
|
|
176
|
+
else:
|
|
177
|
+
raise base.InputError(f'invalid share_key: {share_key=}')
|
|
178
|
+
# build object
|
|
179
|
+
return ShamirSharePrivate(
|
|
180
|
+
minimum=self.minimum, modulus=self.modulus,
|
|
181
|
+
share_key=share_key,
|
|
182
|
+
share_value=modmath.ModPolynomial(share_key, [secret] + self.polynomial, self.modulus))
|
|
183
|
+
|
|
184
|
+
def Shares(
|
|
185
|
+
self, secret: int, /, *, max_shares: int = 0) -> Generator[ShamirSharePrivate, None, None]:
|
|
186
|
+
"""Make any number of ShamirSharePrivate for the `secret`.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
secret (int): secret message to encrypt and share, 0 ≤ s < modulus
|
|
190
|
+
max_shares (int, optional): if given, number (≥ 2) of shares to generate; else infinite
|
|
191
|
+
|
|
192
|
+
Yields:
|
|
193
|
+
ShamirSharePrivate object
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
InputError: invalid inputs
|
|
197
|
+
"""
|
|
198
|
+
# test inputs
|
|
199
|
+
if max_shares and max_shares < self.minimum:
|
|
200
|
+
raise base.InputError(f'invalid max_shares: {max_shares=} < {self.minimum=}')
|
|
201
|
+
# generate shares
|
|
202
|
+
count: int = 0
|
|
203
|
+
used_keys: set[int] = set()
|
|
204
|
+
while not max_shares or count < max_shares:
|
|
205
|
+
share_key: int = 0
|
|
206
|
+
while not share_key or share_key in self.polynomial or share_key in used_keys:
|
|
207
|
+
share_key = base.RandBits(self.modulus.bit_length() - 1)
|
|
208
|
+
try:
|
|
209
|
+
yield self.Share(secret, share_key=share_key)
|
|
210
|
+
used_keys.add(share_key)
|
|
211
|
+
count += 1
|
|
212
|
+
except base.InputError as err:
|
|
213
|
+
# it could happen, for example, that the share_key will generate a value of 0
|
|
214
|
+
logging.warning(err)
|
|
215
|
+
|
|
216
|
+
def VerifyShare(self, secret: int, share: ShamirSharePrivate, /) -> bool:
|
|
217
|
+
"""Verify a ShamirSharePrivate object for the `secret`.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
secret (int): secret message to encrypt and share, 0 ≤ s < modulus
|
|
221
|
+
share (ShamirSharePrivate): share to verify
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
True if share is valid; False otherwise
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
InputError: invalid inputs
|
|
228
|
+
"""
|
|
229
|
+
return share == self.Share(secret, share_key=share.share_key)
|
|
230
|
+
|
|
231
|
+
@classmethod
|
|
232
|
+
def New(cls, minimum_shares: int, bit_length: int, /) -> Self:
|
|
233
|
+
"""Makes a new private SSS object of `bit_length` bits prime modulus and coefficients.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
minimum_shares (int): minimum shares needed for recovery, ≥ 2
|
|
237
|
+
bit_length (int): number of bits in the primes, ≥ 10
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
ShamirSharedSecretPrivate object ready for use
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
InputError: invalid inputs
|
|
244
|
+
"""
|
|
245
|
+
# test inputs
|
|
246
|
+
if minimum_shares < 2:
|
|
247
|
+
raise base.InputError(f'at least 2 shares are needed: {minimum_shares=}')
|
|
248
|
+
if bit_length < 10:
|
|
249
|
+
raise base.InputError(f'invalid bit length: {bit_length=}')
|
|
250
|
+
# make the primes
|
|
251
|
+
unique_primes: set[int] = set()
|
|
252
|
+
while len(unique_primes) < minimum_shares:
|
|
253
|
+
unique_primes.add(modmath.NBitRandomPrime(bit_length))
|
|
254
|
+
# get the largest prime for the modulus
|
|
255
|
+
ordered_primes: list[int] = list(unique_primes)
|
|
256
|
+
modulus: int = max(ordered_primes)
|
|
257
|
+
ordered_primes.remove(modulus)
|
|
258
|
+
# make polynomial be a random order
|
|
259
|
+
base.RandShuffle(ordered_primes)
|
|
260
|
+
# build object
|
|
261
|
+
return cls(minimum=minimum_shares, modulus=modulus, polynomial=ordered_primes)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
265
|
+
class ShamirSharePrivate(ShamirSharedSecretPublic):
|
|
266
|
+
"""Shamir Shared Secret (SSS) one share.
|
|
267
|
+
|
|
268
|
+
BEWARE: This is raw SSS, no modern message wrapping, padding or validation!
|
|
269
|
+
These are pedagogical/raw primitives; do not use for new protocols.
|
|
270
|
+
No measures are taken here to prevent timing attacks.
|
|
271
|
+
|
|
272
|
+
Attributes:
|
|
273
|
+
share_key (int): share secret key; a randomly picked value, 1 ≤ k < modulus
|
|
274
|
+
share_value (int): share secret value, 1 ≤ v < modulus; (k, v) is a "point" of f(k)=v
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
share_key: int
|
|
278
|
+
share_value: int
|
|
279
|
+
|
|
280
|
+
def __post_init__(self) -> None:
|
|
281
|
+
"""Check data.
|
|
282
|
+
|
|
283
|
+
Raises:
|
|
284
|
+
InputError: invalid inputs
|
|
285
|
+
"""
|
|
286
|
+
super(ShamirSharePrivate, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
|
|
287
|
+
if (not 0 < self.share_key < self.modulus or
|
|
288
|
+
not 0 < self.share_value < self.modulus):
|
|
289
|
+
raise base.InputError(f'invalid share: {self}')
|
|
290
|
+
|
|
291
|
+
def __str__(self) -> str:
|
|
292
|
+
"""Safe (no secrets) string representation of the ShamirSharePrivate.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
string representation of ShamirSharePrivate without leaking secrets
|
|
296
|
+
"""
|
|
297
|
+
return (f'ShamirSharePrivate({super(ShamirSharePrivate, self).__str__()}, ' # pylint: disable=super-with-arguments
|
|
298
|
+
f'share_key={base.ObfuscateSecret(self.share_key)}, '
|
|
299
|
+
f'share_value={base.ObfuscateSecret(self.share_value)})')
|