transcrypto 1.0.3__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/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)})')