Skip to content

Commit 719adf8

Browse files
committed
feat: Shamir's secret sharing
1 parent 9691cbb commit 719adf8

5 files changed

Lines changed: 143 additions & 89 deletions

File tree

pactus/crypto/sss/__init__.py

Whitespace-only changes.

pactus/crypto/sss/sss.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""
2+
The following Python implementation of Shamir's secret sharing is
3+
released into the Public Domain under the terms of CC0 and OWFa:
4+
https://creativecommons.org/publicdomain/zero/1.0/
5+
http://www.openwebfoundation.org/legal/the-owf-1-0-agreements/owfa-1-0
6+
7+
See the bottom few lines for usage. Tested on Python 2 and 3.
8+
"""
9+
10+
import functools
11+
import random
12+
13+
_RINT = functools.partial(random.SystemRandom().randint, 0)
14+
15+
16+
def _eval_at(poly, x, prime):
17+
"""
18+
Evaluates polynomial (coefficient tuple) at x, used to generate a
19+
shamir pool in make_random_shares below.
20+
"""
21+
accum = 0
22+
for coeff in reversed(poly):
23+
accum *= x
24+
accum += coeff
25+
accum %= prime
26+
27+
return accum
28+
29+
30+
def _extended_gcd(a: int, b: int) -> int:
31+
"""
32+
Division in integers modulus p means finding the inverse of the
33+
denominator modulo p and then multiplying the numerator by this
34+
inverse (Note: inverse of A is B such that A*B % p == 1). This can
35+
be computed via the extended Euclidean algorithm
36+
http://en.wikipedia.org/wiki/Modular_multiplicative_inverse#Computation
37+
"""
38+
x = 0
39+
last_x = 1
40+
y = 1
41+
last_y = 0
42+
while b != 0:
43+
quot = a // b
44+
a, b = b, a % b
45+
x, last_x = last_x - quot * x, x
46+
y, last_y = last_y - quot * y, y
47+
48+
return last_x, last_y
49+
50+
51+
def _divmod(num: int, den: int, p: int) -> int:
52+
"""
53+
Compute num / den modulo prime p
54+
55+
To explain this, the result will be such that:
56+
den * _divmod(num, den, p) % p == num
57+
"""
58+
inv, _ = _extended_gcd(den, p)
59+
60+
return num * inv
61+
62+
63+
def _lagrange_interpolate(x: int, x_s: list[int], y_s: list[int], p: int) -> int:
64+
"""
65+
Find the y-value for the given x, given n (x, y) points;
66+
k points will define a polynomial of up to kth order.
67+
"""
68+
k = len(x_s)
69+
assert k == len(set(x_s)), "points must be distinct"
70+
71+
def PI(vals): # upper-case PI -- product of inputs
72+
accum = 1
73+
for v in vals:
74+
accum *= v
75+
return accum
76+
77+
nums = [] # avoid inexact division
78+
dens = []
79+
for i in range(k):
80+
others = list(x_s)
81+
cur = others.pop(i)
82+
nums.append(PI(x - o for o in others))
83+
dens.append(PI(cur - o for o in others))
84+
85+
den = PI(dens)
86+
num = sum([_divmod(nums[i] * den * y_s[i] % p, dens[i], p) for i in range(k)])
87+
88+
return (_divmod(num, den, p) + p) % p
89+
90+
91+
def make_random_shares(secret: int, minimum: int, shares: int, prime: int) -> list[tuple[int, int]]:
92+
"""
93+
Generates a random shamir pool for a given secret, returns share points.
94+
"""
95+
if minimum > shares:
96+
raise ValueError("Pool secret would be irrecoverable.")
97+
poly = [secret] + [_RINT(prime - 1) for i in range(minimum - 1)]
98+
points = [(i, _eval_at(poly, i, prime)) for i in range(1, shares + 1)]
99+
100+
return points
101+
102+
103+
def recover_secret(shares: list[tuple[int, int]], prime: int) -> int:
104+
"""
105+
Recover the secret from share points
106+
(points (x,y) on the polynomial).
107+
"""
108+
if len(shares) < 2:
109+
msg = "need at least two shares"
110+
raise ValueError(msg)
111+
112+
x_s, y_s = zip(*shares)
113+
114+
return _lagrange_interpolate(0, x_s, y_s, prime)

pactus/utils/utils.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,3 @@ def encode_from_base256_with_type(hrp: str, typ: str, data: bytes) -> str:
1717
converted = bech32m.convertbits(list(data), 8, 5, pad=True)
1818
converted = [typ, *converted]
1919
return bech32m.bech32_encode(hrp, converted, bech32m.Encoding.BECH32M)
20-
21-
22-
def evaluate_polynomial(c: list[int], x: int, mod: int) -> int | None:
23-
"""
24-
Evaluate the polynomial f(x) = c[0] + c[1] * x + c[2] * x^2 + ... + c[n-1] * x^(n-1).
25-
26-
Args:
27-
c: List of polynomial coefficients (c[0] is the constant term)
28-
x: The value at which to evaluate the polynomial
29-
mod: The modulus to use for the evaluation
30-
31-
Returns:
32-
The computed value f(x) if success, None otherwise
33-
34-
"""
35-
if not c:
36-
return None
37-
38-
if len(c) == 1:
39-
return c[0]
40-
41-
y = c[-1]
42-
for i in range(len(c) - 2, -1, -1):
43-
y = (y * x + c[i]) % mod
44-
45-
return y

tests/test_crypto_sss.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import unittest
2+
from pactus.crypto.sss import sss
3+
4+
5+
class TestEvaluatePolynomial(unittest.TestCase):
6+
def test_wikipedia_example(self):
7+
# https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing
8+
self.assertEqual(sss._eval_at([1234, 166, 94], 1, 2**127 - 1), 1494)
9+
self.assertEqual(sss._eval_at([1234, 166, 94], 2, 2**127 - 1), 1942)
10+
self.assertEqual(sss._eval_at([1234, 166, 94], 3, 2**127 - 1), 2578)
11+
self.assertEqual(sss._eval_at([1234, 166, 94], 4, 2**127 - 1), 3402)
12+
self.assertEqual(sss._eval_at([1234, 166, 94], 5, 2**127 - 1), 4414)
13+
self.assertEqual(sss._eval_at([1234, 166, 94], 6, 2**127 - 1), 5614)
14+
15+
16+
class TestRecover(unittest.TestCase):
17+
def test_recover_secret_1(self):
18+
shares = [(1, 1494), (2, 1942), (3, 2578)]
19+
prime = 2**127 - 1
20+
self.assertEqual(sss.recover_secret(shares, prime), 1234)
21+
22+
def test_recover_secret_2(self):
23+
shares = [(1, 1494), (3, 2578), (6, 5614)]
24+
prime = 2**127 - 1
25+
self.assertEqual(sss.recover_secret(shares, prime), 1234)
26+
27+
28+
if __name__ == "__main__":
29+
unittest.main()

tests/test_utils.py

Lines changed: 0 additions & 63 deletions
This file was deleted.

0 commit comments

Comments
 (0)