transcrypto 1.1.2__py3-none-any.whl → 1.2.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/rsa.py CHANGED
@@ -14,8 +14,7 @@ import logging
14
14
  # import pdb
15
15
  from typing import Self
16
16
 
17
- from . import base
18
- from . import modmath
17
+ from . import base, modmath, aes
19
18
 
20
19
  __author__ = 'balparda@github.com'
21
20
  __version__: str = base.__version__ # version comes from base!
@@ -27,13 +26,15 @@ _BIG_ENCRYPTION_EXPONENT = 2 ** 16 + 1 # 65537
27
26
 
28
27
  _MAX_KEY_GENERATION_FAILURES = 15
29
28
 
29
+ # fixed prefixes: do NOT ever change! will break all encryption and signature schemes
30
+ _RSA_ENCRYPTION_AAD_PREFIX = b'transcrypto.RSA.Encryption.1.0\x00'
31
+ _RSA_SIGNATURE_HASH_PREFIX = b'transcrypto.RSA.Signature.1.0\x00'
32
+
30
33
 
31
34
  @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
32
- class RSAPublicKey(base.CryptoKey):
35
+ class RSAPublicKey(base.CryptoKey, base.Encryptor, base.Verifier):
33
36
  """RSA (Rivest-Shamir-Adleman) key, with the public part of the key.
34
37
 
35
- BEWARE: This is raw RSA, no OAEP or PSS padding or validation!
36
- These are pedagogical/raw primitives; do not use for new protocols.
37
38
  No measures are taken here to prevent timing attacks.
38
39
 
39
40
  By default and deliberate choice the encryption exponent will be either 7 or 65537,
@@ -70,12 +71,20 @@ class RSAPublicKey(base.CryptoKey):
70
71
  string representation of RSAPublicKey
71
72
  """
72
73
  return ('RSAPublicKey('
74
+ f'bits={self.public_modulus.bit_length()}, '
73
75
  f'public_modulus={base.IntToEncoded(self.public_modulus)}, '
74
76
  f'encrypt_exp={base.IntToEncoded(self.encrypt_exp)})')
75
77
 
76
- def Encrypt(self, message: int, /) -> int:
78
+ @property
79
+ def modulus_size(self) -> int:
80
+ """Modulus size in bytes. The number of bytes used in Encrypt/Decrypt/Sign/Verify."""
81
+ return (self.public_modulus.bit_length() + 7) // 8
82
+
83
+ def RawEncrypt(self, message: int, /) -> int:
77
84
  """Encrypt `message` with this public key.
78
85
 
86
+ BEWARE: This is raw RSA, no OAEP or PSS padding or validation!
87
+ These are pedagogical/raw primitives; do not use for new protocols.
79
88
  We explicitly disallow `message` to be zero.
80
89
 
81
90
  Args:
@@ -93,9 +102,52 @@ class RSAPublicKey(base.CryptoKey):
93
102
  # encrypt
94
103
  return modmath.ModExp(message, self.encrypt_exp, self.public_modulus)
95
104
 
96
- def VerifySignature(self, message: int, signature: int, /) -> bool:
105
+ def Encrypt(self, plaintext: bytes, /, *, associated_data: bytes | None = None) -> bytes:
106
+ """Encrypt `plaintext` and return `ciphertext`.
107
+
108
+ • Let k = ceil(log2(n))/8 be the modulus size in bytes.
109
+ • Pick random r ∈ [2, n-1]
110
+ • ct = r^e mod n
111
+ • return Padded(ct, k) + AES-256-GCM(key=SHA512(r)[32:], plaintext,
112
+ associated_data="prefix" + len(aad) + aad + Padded(ct, k))
113
+
114
+ We pick fresh random r, send ct = r^e mod n, and derive the DEM key from r,
115
+ then use AES-GCM for the payload. This is the classic RSA-KEM construction.
116
+ With AEAD as the DEM, we get strong confidentiality and ciphertext integrity
117
+ (CCA resistance in the ROM under standard assumptions). There are no
118
+ Bleichenbacher-style issue because we do not expose any padding semantics.
119
+
120
+ Args:
121
+ plaintext (bytes): Data to encrypt.
122
+ associated_data (bytes, optional): Optional AAD; must be provided again on decrypt
123
+
124
+ Returns:
125
+ bytes: Ciphertext; see above:
126
+ Padded(ct, k) + AES-256-GCM(key=SHA512(r)[32:], plaintext,
127
+ associated_data="prefix" + len(aad) + aad + Padded(ct, k))
128
+
129
+ Raises:
130
+ InputError: invalid inputs
131
+ CryptoError: internal crypto failures
132
+ """
133
+ # generate random r and encrypt it
134
+ r: int = 0
135
+ while not 1 < r < self.public_modulus or base.GCD(r, self.public_modulus) != 1:
136
+ r = base.RandBits(self.public_modulus.bit_length())
137
+ k: int = self.modulus_size
138
+ ct: bytes = base.IntToFixedBytes(self.RawEncrypt(r), k)
139
+ assert len(ct) == k, 'should never happen: c_kem should be exactly k bytes'
140
+ # encrypt plaintext with AES-256-GCM using SHA512(r)[32:] as key; return ct || Encrypt(...)
141
+ ss: bytes = base.Hash512(base.IntToFixedBytes(r, k))
142
+ aad: bytes = b'' if associated_data is None else associated_data
143
+ aad_prime: bytes = _RSA_ENCRYPTION_AAD_PREFIX + base.IntToFixedBytes(len(aad), 8) + aad + ct
144
+ return ct + aes.AESKey(key256=ss[32:]).Encrypt(plaintext, associated_data=aad_prime)
145
+
146
+ def RawVerify(self, message: int, signature: int, /) -> bool:
97
147
  """Verify a signature. True if OK; False if failed verification.
98
148
 
149
+ BEWARE: This is raw RSA, no OAEP or PSS padding or validation!
150
+ These are pedagogical/raw primitives; do not use for new protocols.
99
151
  We explicitly disallow `message` to be zero.
100
152
 
101
153
  Args:
@@ -109,7 +161,68 @@ class RSAPublicKey(base.CryptoKey):
109
161
  Raises:
110
162
  InputError: invalid inputs
111
163
  """
112
- return self.Encrypt(signature) == message
164
+ return self.RawEncrypt(signature) == message
165
+
166
+ def _DomainSeparatedHash(
167
+ self, message: bytes, associated_data: bytes | None, salt: bytes, /) -> int:
168
+ """Compute the domain-separated hash for signing and verifying.
169
+
170
+ Args:
171
+ message (bytes): message to sign/verify
172
+ associated_data (bytes | None): optional associated data
173
+ salt (bytes): salt to use in the hash
174
+
175
+ Returns:
176
+ int: integer representation of the hash output;
177
+ Hash512("prefix" || len(aad) || aad || message || salt)
178
+
179
+ Raises:
180
+ CryptoError: hash output is out of range
181
+ """
182
+ aad: bytes = b'' if associated_data is None else associated_data
183
+ la: bytes = base.IntToFixedBytes(len(aad), 8)
184
+ assert len(salt) == 64, 'should never happen: salt should be exactly 64 bytes'
185
+ y: int = base.BytesToInt(base.Hash512(_RSA_SIGNATURE_HASH_PREFIX + la + aad + message + salt))
186
+ if not 1 < y < self.public_modulus or base.GCD(y, self.public_modulus) != 1:
187
+ # will only reasonably happen if modulus is small
188
+ raise base.CryptoError(f'hash output {y} is out of range/invalid {self.public_modulus}')
189
+ return y
190
+
191
+ def Verify(
192
+ self, message: bytes, signature: bytes, /, *, associated_data: bytes | None = None) -> bool:
193
+ """Verify a `signature` for `message`. True if OK; False if failed verification.
194
+
195
+ • Let k = ceil(log2(n))/8 be the modulus size in bytes.
196
+ • Split signature in two parts: the first 64 bytes is salt, the rest is s
197
+ • y_check = s^e mod n
198
+ • return y_check == Hash512("prefix" || len(aad) || aad || message || salt)
199
+ • return False for any malformed signature
200
+
201
+ Args:
202
+ message (bytes): Data that was signed
203
+ signature (bytes): Signature data to verify
204
+ associated_data (bytes, optional): Optional AAD (must match what was used during signing)
205
+
206
+ Returns:
207
+ True if signature is valid, False otherwise
208
+
209
+ Raises:
210
+ InputError: invalid inputs
211
+ CryptoError: internal crypto failures, authentication failure, key mismatch, etc
212
+ """
213
+ k: int = self.modulus_size
214
+ if k <= 64:
215
+ raise base.InputError(f'modulus too small for signing operations: {k} bytes')
216
+ if len(signature) != (64 + k):
217
+ logging.info(f'invalid signature length: {len(signature)} ; expected {64 + k}')
218
+ return False
219
+ try:
220
+ return self.RawVerify(
221
+ self._DomainSeparatedHash(message, associated_data, signature[:64]),
222
+ base.BytesToInt(signature[64:]))
223
+ except base.InputError as err:
224
+ logging.info(err)
225
+ return False
113
226
 
114
227
  @classmethod
115
228
  def Copy(cls, other: RSAPublicKey, /) -> Self:
@@ -154,7 +267,8 @@ class RSAObfuscationPair(RSAPublicKey):
154
267
  Returns:
155
268
  string representation of RSAObfuscationPair without leaking secrets
156
269
  """
157
- return (f'RSAObfuscationPair({super(RSAObfuscationPair, self).__str__()}, ' # pylint: disable=super-with-arguments
270
+ return ('RSAObfuscationPair('
271
+ f'{super(RSAObfuscationPair, self).__str__()}, ' # pylint: disable=super-with-arguments
158
272
  f'random_key={base.ObfuscateSecret(self.random_key)}, '
159
273
  f'key_inverse={base.ObfuscateSecret(self.key_inverse)})')
160
274
 
@@ -198,11 +312,11 @@ class RSAObfuscationPair(RSAPublicKey):
198
312
  """
199
313
  # verify that obfuscated signature is valid
200
314
  obfuscated: int = self.ObfuscateMessage(message)
201
- if not self.VerifySignature(obfuscated, signature):
315
+ if not self.RawVerify(obfuscated, signature):
202
316
  raise base.CryptoError(f'obfuscated message was not signed: {message=} ; {signature=}')
203
317
  # compute signature for original message and check it
204
318
  original: int = (signature * self.key_inverse) % self.public_modulus
205
- if not self.VerifySignature(message, original):
319
+ if not self.RawVerify(message, original):
206
320
  raise base.CryptoError(f'failed signature recovery: {message=} ; {signature=}')
207
321
  return original
208
322
 
@@ -243,11 +357,9 @@ class RSAObfuscationPair(RSAPublicKey):
243
357
 
244
358
 
245
359
  @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
246
- class RSAPrivateKey(RSAPublicKey):
360
+ class RSAPrivateKey(RSAPublicKey, base.Decryptor, base.Signer): # pylint: disable=too-many-ancestors
247
361
  """RSA (Rivest-Shamir-Adleman) private key.
248
362
 
249
- BEWARE: This is raw RSA, no OAEP or PSS padding or validation!
250
- These are pedagogical/raw primitives; do not use for new protocols.
251
363
  No measures are taken here to prevent timing attacks.
252
364
 
253
365
  The attributes modulus_p (p), modulus_q (q) and decrypt_exp (d) are "enough" for a working key,
@@ -280,7 +392,7 @@ class RSAPrivateKey(RSAPublicKey):
280
392
  """
281
393
  super(RSAPrivateKey, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
282
394
  phi: int = (self.modulus_p - 1) * (self.modulus_q - 1)
283
- min_prime_distance: int = 2 ** (self.public_modulus.bit_length() // 3 + 1)
395
+ min_prime_distance: int = 1 << (self.public_modulus.bit_length() // 4) # n**(1/4)
284
396
  if (self.modulus_p < 2 or not modmath.IsPrime(self.modulus_p) or # pylint: disable=too-many-boolean-expressions
285
397
  self.modulus_q < 3 or not modmath.IsPrime(self.modulus_q) or
286
398
  self.modulus_q <= self.modulus_p or
@@ -313,14 +425,17 @@ class RSAPrivateKey(RSAPublicKey):
313
425
  Returns:
314
426
  string representation of RSAPrivateKey without leaking secrets
315
427
  """
316
- return (f'RSAPrivateKey({super(RSAPrivateKey, self).__str__()}, ' # pylint: disable=super-with-arguments
428
+ return ('RSAPrivateKey('
429
+ f'{super(RSAPrivateKey, self).__str__()}, ' # pylint: disable=super-with-arguments
317
430
  f'modulus_p={base.ObfuscateSecret(self.modulus_p)}, '
318
431
  f'modulus_q={base.ObfuscateSecret(self.modulus_q)}, '
319
432
  f'decrypt_exp={base.ObfuscateSecret(self.decrypt_exp)})')
320
433
 
321
- def Decrypt(self, ciphertext: int, /) -> int:
434
+ def RawDecrypt(self, ciphertext: int, /) -> int:
322
435
  """Decrypt `ciphertext` with this private key.
323
436
 
437
+ BEWARE: This is raw RSA, no OAEP or PSS padding or validation!
438
+ These are pedagogical/raw primitives; do not use for new protocols.
324
439
  We explicitly allow `ciphertext` to be zero for completeness, but it shouldn't be in practice.
325
440
 
326
441
  Args:
@@ -342,9 +457,44 @@ class RSAPrivateKey(RSAPublicKey):
342
457
  h: int = (self.q_inverse_p * (m_p - m_q)) % self.modulus_p
343
458
  return (m_q + h * self.modulus_q) % self.public_modulus
344
459
 
345
- def Sign(self, message: int, /) -> int:
460
+ def Decrypt(self, ciphertext: bytes, /, *, associated_data: bytes | None = None) -> bytes:
461
+ """Decrypt `ciphertext` and return the original `plaintext`.
462
+
463
+ • Let k = ceil(log2(n))/8 be the modulus size in bytes.
464
+ • Split ciphertext in two parts: the first k bytes is ct, the rest is AES-256-GCM
465
+ • r = ct^d mod n
466
+ • return AES-256-GCM(key=SHA512(r)[32:], ciphertext,
467
+ associated_data="prefix" + len(aad) + aad + Padded(ct, k))
468
+
469
+ Args:
470
+ ciphertext (bytes): Data to decrypt; see Encrypt() above:
471
+ Padded(ct, k) + AES-256-GCM(key=SHA512(r)[32:], plaintext,
472
+ associated_data="prefix" + len(aad) + aad + Padded(ct, k))
473
+ associated_data (bytes, optional): Optional AAD (must match what was used during encrypt)
474
+
475
+ Returns:
476
+ bytes: Decrypted plaintext bytes
477
+
478
+ Raises:
479
+ InputError: invalid inputs
480
+ CryptoError: internal crypto failures, authentication failure, key mismatch, etc
481
+ """
482
+ k: int = self.modulus_size
483
+ if len(ciphertext) < (k + 32):
484
+ raise base.InputError(f'invalid ciphertext length: {len(ciphertext)} ; {k=}')
485
+ # split ciphertext in two parts: the first k bytes is ct, the rest is AES-256-GCM
486
+ rsa_ct, aes_ct = ciphertext[:k], ciphertext[k:]
487
+ r: int = self.RawDecrypt(base.BytesToInt(rsa_ct))
488
+ ss: bytes = base.Hash512(base.IntToFixedBytes(r, k))
489
+ aad: bytes = b'' if associated_data is None else associated_data
490
+ aad_prime: bytes = _RSA_ENCRYPTION_AAD_PREFIX + base.IntToFixedBytes(len(aad), 8) + aad + rsa_ct
491
+ return aes.AESKey(key256=ss[32:]).Decrypt(aes_ct, associated_data=aad_prime)
492
+
493
+ def RawSign(self, message: int, /) -> int:
346
494
  """Sign `message` with this private key.
347
495
 
496
+ BEWARE: This is raw RSA, no OAEP or PSS padding or validation!
497
+ These are pedagogical/raw primitives; do not use for new protocols.
348
498
  We explicitly disallow `message` to be zero.
349
499
 
350
500
  Args:
@@ -361,7 +511,41 @@ class RSAPrivateKey(RSAPublicKey):
361
511
  if not 0 < message < self.public_modulus:
362
512
  raise base.InputError(f'invalid message: {message=}')
363
513
  # call decryption
364
- return self.Decrypt(message)
514
+ return self.RawDecrypt(message)
515
+
516
+ def Sign(self, message: bytes, /, *, associated_data: bytes | None = None) -> bytes:
517
+ """Sign `message` and return the `signature`.
518
+
519
+ • Let k = ceil(log2(n))/8 be the modulus size in bytes.
520
+ • Pick random salt of 64 bytes
521
+ • s = (Hash512("prefix" || len(aad) || aad || message || salt))^d mod n
522
+ • return salt || Padded(s, k)
523
+
524
+ This is basically Full-Domain Hash RSA with a 512-bit hash and per-signature salt,
525
+ which is EUF-CMA secure in the ROM. Our domain-separation prefix and explicit AAD
526
+ length prefix are both correct and remove composition/ambiguity pitfalls.
527
+ There are no Bleichenbacher-style issue because we do not expose any padding semantics.
528
+
529
+ Args:
530
+ message (bytes): Data to sign.
531
+ associated_data (bytes, optional): Optional AAD for AEAD modes; must be
532
+ provided again on decrypt
533
+
534
+ Returns:
535
+ bytes: Signature; salt || Padded(s, k) - see above
536
+
537
+ Raises:
538
+ InputError: invalid inputs
539
+ CryptoError: internal crypto failures
540
+ """
541
+ k: int = self.modulus_size
542
+ if k <= 64:
543
+ raise base.InputError(f'modulus too small for signing operations: {k} bytes')
544
+ salt: bytes = base.RandBytes(64)
545
+ s_int: int = self.RawSign(self._DomainSeparatedHash(message, associated_data, salt))
546
+ s_bytes: bytes = base.IntToFixedBytes(s_int, k)
547
+ assert len(s_bytes) == k, 'should never happen: s_bytes should be exactly k bytes'
548
+ return salt + s_bytes
365
549
 
366
550
  @classmethod
367
551
  def New(cls, bit_length: int, /) -> Self:
transcrypto/sss.py CHANGED
@@ -14,25 +14,23 @@ import logging
14
14
  # import pdb
15
15
  from typing import Collection, Generator, Self
16
16
 
17
- from . import base
18
- from . import modmath
17
+ from . import base, modmath, aes
19
18
 
20
19
  __author__ = 'balparda@github.com'
21
20
  __version__: str = base.__version__ # version comes from base!
22
21
  __version_tuple__: tuple[int, ...] = base.__version_tuple__
23
22
 
24
23
 
24
+ # fixed prefixes: do NOT ever change! will break all encryption and signature schemes
25
+ _SSS_ENCRYPTION_AAD_PREFIX = b'transcrypto.SSS.Sharing.1.0\x00'
26
+
27
+
25
28
  @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
26
29
  class ShamirSharedSecretPublic(base.CryptoKey):
27
30
  """Shamir Shared Secret (SSS) public part.
28
31
 
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
32
  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.
33
+ Malicious share injection is possible! Add MAC or digital signature in hostile settings.
36
34
 
37
35
  Attributes:
38
36
  minimum (int): minimum shares needed for recovery, ≥ 2
@@ -61,13 +59,24 @@ class ShamirSharedSecretPublic(base.CryptoKey):
61
59
  string representation of ShamirSharedSecretPublic
62
60
  """
63
61
  return ('ShamirSharedSecretPublic('
62
+ f'bits={self.modulus.bit_length()}, '
64
63
  f'minimum={self.minimum}, '
65
64
  f'modulus={base.IntToEncoded(self.modulus)})')
66
65
 
67
- def RecoverSecret(
66
+ @property
67
+ def modulus_size(self) -> int:
68
+ """Modulus size in bytes. The number of bytes used in MakeDataShares/RecoverData."""
69
+ return (self.modulus.bit_length() + 7) // 8
70
+
71
+ def RawRecoverSecret(
68
72
  self, shares: Collection[ShamirSharePrivate], /, *, force_recover: bool = False) -> int:
69
73
  """Recover the secret from ShamirSharePrivate objects.
70
74
 
75
+ BEWARE: This is raw SSS, no modern message wrapping, padding or validation!
76
+ These are pedagogical/raw primitives; do not use for new protocols.
77
+ This is the information-theoretic SSS but with no authentication or binding between
78
+ share and secret.
79
+
71
80
  Args:
72
81
  shares (Collection[ShamirSharePrivate]): shares to use to recover the secret
73
82
  force_recover (bool, optional): if True will try to recover (default: False)
@@ -114,9 +123,8 @@ class ShamirSharedSecretPublic(base.CryptoKey):
114
123
  class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
115
124
  """Shamir Shared Secret (SSS) private keys.
116
125
 
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
126
  No measures are taken here to prevent timing attacks.
127
+ Malicious share injection is possible! Add MAC or digital signature in hostile settings.
120
128
 
121
129
  We deliberately choose prime coefficients. This shrinks the key-space and leaks a bit of
122
130
  structure. It is "unusual", but with large enough modulus (bit length > ~ 500) it makes no
@@ -148,12 +156,18 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
148
156
  Returns:
149
157
  string representation of ShamirSharedSecretPrivate without leaking secrets
150
158
  """
151
- return (f'ShamirSharedSecretPrivate({super(ShamirSharedSecretPrivate, self).__str__()}, ' # pylint: disable=super-with-arguments
159
+ return ('ShamirSharedSecretPrivate('
160
+ f'{super(ShamirSharedSecretPrivate, self).__str__()}, ' # pylint: disable=super-with-arguments
152
161
  f'polynomial=[{", ".join(base.ObfuscateSecret(i) for i in self.polynomial)}])')
153
162
 
154
- def Share(self, secret: int, /, *, share_key: int = 0) -> ShamirSharePrivate:
163
+ def RawShare(self, secret: int, /, *, share_key: int = 0) -> ShamirSharePrivate:
155
164
  """Make a new ShamirSharePrivate for the `secret`.
156
165
 
166
+ BEWARE: This is raw SSS, no modern message wrapping, padding or validation!
167
+ These are pedagogical/raw primitives; do not use for new protocols.
168
+ This is the information-theoretic SSS but with no authentication or binding between
169
+ share and secret.
170
+
157
171
  Args:
158
172
  secret (int): secret message to encrypt and share, 0 ≤ s < modulus
159
173
  share_key (int, optional): if given, a random value to use, 1 ≤ r < modulus;
@@ -181,10 +195,15 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
181
195
  share_key=share_key,
182
196
  share_value=modmath.ModPolynomial(share_key, [secret] + self.polynomial, self.modulus))
183
197
 
184
- def Shares(
198
+ def RawShares(
185
199
  self, secret: int, /, *, max_shares: int = 0) -> Generator[ShamirSharePrivate, None, None]:
186
200
  """Make any number of ShamirSharePrivate for the `secret`.
187
201
 
202
+ BEWARE: This is raw SSS, no modern message wrapping, padding or validation!
203
+ These are pedagogical/raw primitives; do not use for new protocols.
204
+ This is the information-theoretic SSS but with no authentication or binding between
205
+ share and secret.
206
+
188
207
  Args:
189
208
  secret (int): secret message to encrypt and share, 0 ≤ s < modulus
190
209
  max_shares (int, optional): if given, number (≥ 2) of shares to generate; else infinite
@@ -206,16 +225,63 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
206
225
  while not share_key or share_key in self.polynomial or share_key in used_keys:
207
226
  share_key = base.RandBits(self.modulus.bit_length() - 1)
208
227
  try:
209
- yield self.Share(secret, share_key=share_key)
228
+ yield self.RawShare(secret, share_key=share_key)
210
229
  used_keys.add(share_key)
211
230
  count += 1
212
231
  except base.InputError as err:
213
232
  # it could happen, for example, that the share_key will generate a value of 0
214
233
  logging.warning(err)
215
234
 
216
- def VerifyShare(self, secret: int, share: ShamirSharePrivate, /) -> bool:
235
+ def MakeDataShares(self, secret: bytes, total_shares: int, /) -> list[ShamirShareData]:
236
+ """Make `total_shares` ShamirShareData objects with encrypted `secret`.
237
+
238
+ • Let k = ceil(log2(n))/8 be the modulus size in bytes
239
+ • r = random 32 bytes
240
+ • shares = SSS.Shares(r, total_shares)
241
+ • ct = AES-256-GCM(key=SHA512("prefix" + r)[32:], plaintext=secret,
242
+ associated_data="prefix" + minimum + modulus)
243
+ • return [share + ct for share in shares]
244
+
245
+ Args:
246
+ secret (bytes): Data to encrypt and distribute (encrypted) in each share.
247
+ total_shares (int): Number of shares to make, ≥ minimum
248
+
249
+ Returns:
250
+ list[ShamirShareData]: the list of shares with encrypted data
251
+
252
+ Raises:
253
+ InputError: invalid inputs
254
+ CryptoError: internal crypto failures
255
+ """
256
+ if total_shares < self.minimum:
257
+ raise base.InputError(f'invalid total_shares: {total_shares=} < {self.minimum=}')
258
+ k: int = self.modulus_size
259
+ if k <= 32:
260
+ raise base.InputError(f'modulus too small for key operations: {k} bytes')
261
+ key256: bytes = base.RandBytes(32)
262
+ shares: list[ShamirSharePrivate] = list(
263
+ self.RawShares(base.BytesToInt(key256), max_shares=total_shares))
264
+ aad: bytes = (
265
+ _SSS_ENCRYPTION_AAD_PREFIX +
266
+ base.IntToFixedBytes(self.minimum, 8) + base.IntToFixedBytes(self.modulus, k))
267
+ aead_key: bytes = base.Hash512(_SSS_ENCRYPTION_AAD_PREFIX + key256)
268
+ ct: bytes = aes.AESKey(key256=aead_key[32:]).Encrypt(secret, associated_data=aad)
269
+ return [ShamirShareData(
270
+ minimum=s.minimum,
271
+ modulus=s.modulus,
272
+ share_key=s.share_key,
273
+ share_value=s.share_value,
274
+ encrypted_data=ct,
275
+ ) for s in shares]
276
+
277
+ def RawVerifyShare(self, secret: int, share: ShamirSharePrivate, /) -> bool:
217
278
  """Verify a ShamirSharePrivate object for the `secret`.
218
279
 
280
+ BEWARE: This is raw SSS, no modern message wrapping, padding or validation!
281
+ These are pedagogical/raw primitives; do not use for new protocols.
282
+ This is the information-theoretic SSS but with no authentication or binding between
283
+ share and secret.
284
+
219
285
  Args:
220
286
  secret (int): secret message to encrypt and share, 0 ≤ s < modulus
221
287
  share (ShamirSharePrivate): share to verify
@@ -226,7 +292,7 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
226
292
  Raises:
227
293
  InputError: invalid inputs
228
294
  """
229
- return share == self.Share(secret, share_key=share.share_key)
295
+ return share == self.RawShare(secret, share_key=share.share_key)
230
296
 
231
297
  @classmethod
232
298
  def New(cls, minimum_shares: int, bit_length: int, /) -> Self:
@@ -265,9 +331,8 @@ class ShamirSharedSecretPrivate(ShamirSharedSecretPublic):
265
331
  class ShamirSharePrivate(ShamirSharedSecretPublic):
266
332
  """Shamir Shared Secret (SSS) one share.
267
333
 
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
334
  No measures are taken here to prevent timing attacks.
335
+ Malicious share injection is possible! Add MAC or digital signature in hostile settings.
271
336
 
272
337
  Attributes:
273
338
  share_key (int): share secret key; a randomly picked value, 1 ≤ k < modulus
@@ -294,6 +359,80 @@ class ShamirSharePrivate(ShamirSharedSecretPublic):
294
359
  Returns:
295
360
  string representation of ShamirSharePrivate without leaking secrets
296
361
  """
297
- return (f'ShamirSharePrivate({super(ShamirSharePrivate, self).__str__()}, ' # pylint: disable=super-with-arguments
362
+ return ('ShamirSharePrivate('
363
+ f'{super(ShamirSharePrivate, self).__str__()}, ' # pylint: disable=super-with-arguments
298
364
  f'share_key={base.ObfuscateSecret(self.share_key)}, '
299
365
  f'share_value={base.ObfuscateSecret(self.share_value)})')
366
+
367
+ @classmethod
368
+ def CopyShare(cls, other: ShamirSharePrivate, /) -> Self:
369
+ """Initialize a share taking the parts of another share."""
370
+ return cls(
371
+ minimum=other.minimum, modulus=other.modulus,
372
+ share_key=other.share_key, share_value=other.share_value)
373
+
374
+
375
+ @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
376
+ class ShamirShareData(ShamirSharePrivate):
377
+ """Shamir Shared Secret (SSS) one share.
378
+
379
+ No measures are taken here to prevent timing attacks.
380
+ Malicious share injection is possible! Add MAC or digital signature in hostile settings.
381
+
382
+ Attributes:
383
+ share_key (int): share secret key; a randomly picked value, 1 ≤ k < modulus
384
+ share_value (int): share secret value, 1 ≤ v < modulus; (k, v) is a "point" of f(k)=v
385
+ """
386
+
387
+ encrypted_data: bytes
388
+
389
+ def __post_init__(self) -> None:
390
+ """Check data.
391
+
392
+ Raises:
393
+ InputError: invalid inputs
394
+ """
395
+ super(ShamirShareData, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
396
+ if len(self.encrypted_data) < 32:
397
+ raise base.InputError(f'AES256+GCM SSS should have ≥32 bytes IV/CT/tag: {self}')
398
+
399
+ def __str__(self) -> str:
400
+ """Safe (no secrets) string representation of the ShamirShareData.
401
+
402
+ Returns:
403
+ string representation of ShamirShareData without leaking secrets
404
+ """
405
+ return ('ShamirShareData('
406
+ f'{super(ShamirShareData, self).__str__()}, ' # pylint: disable=super-with-arguments
407
+ f'encrypted_data={base.ObfuscateSecret(self.encrypted_data)})')
408
+
409
+ def RecoverData(self, other_shares: list[ShamirSharePrivate]) -> bytes:
410
+ """Recover the encrypted data from ShamirSharePrivate objects.
411
+
412
+ * key256 = SSS.RecoverSecret([this] + other_shares)
413
+ * return AES-256-GCM(key=SHA512("prefix" + key256)[32:], ciphertext=encrypted_data,
414
+ associated_data="prefix" + minimum + modulus)
415
+
416
+ Args:
417
+ other_shares (list[ShamirSharePrivate]): Other shares to use to recover the secret
418
+
419
+ Returns:
420
+ bytes: Decrypted plaintext bytes
421
+
422
+ Raises:
423
+ InputError: invalid inputs
424
+ CryptoError: internal crypto failures, authentication failure, key mismatch, etc
425
+ """
426
+ k: int = self.modulus_size
427
+ if k <= 32:
428
+ raise base.InputError(f'modulus too small for key operations: {k} bytes')
429
+ # recover secret; raise if shares are invalid
430
+ secret: int = self.RawRecoverSecret([self] + other_shares)
431
+ if not 0 <= secret < (1 << 256):
432
+ raise base.CryptoError('recovered key out of range for 256-bit key')
433
+ key256: bytes = base.IntToFixedBytes(secret, 32)
434
+ aad: bytes = (
435
+ _SSS_ENCRYPTION_AAD_PREFIX +
436
+ base.IntToFixedBytes(self.minimum, 8) + base.IntToFixedBytes(self.modulus, k))
437
+ aead_key: bytes = base.Hash512(_SSS_ENCRYPTION_AAD_PREFIX + key256)
438
+ return aes.AESKey(key256=aead_key[32:]).Decrypt(self.encrypted_data, associated_data=aad)