diff --git a/pactus/crypto/sss/__init__.py b/pactus/crypto/sss/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pactus/crypto/sss/sss.py b/pactus/crypto/sss/sss.py new file mode 100644 index 0000000..8f66994 --- /dev/null +++ b/pactus/crypto/sss/sss.py @@ -0,0 +1,115 @@ +""" +The following Python implementation of Shamir's secret sharing is +released into the Public Domain under the terms of CC0 and OWFa: +https://creativecommons.org/publicdomain/zero/1.0/ +http://www.openwebfoundation.org/legal/the-owf-1-0-agreements/owfa-1-0. + +See the bottom few lines for usage. Tested on Python 2 and 3. +""" +from __future__ import annotations + +import functools +import random + +_RINT = functools.partial(random.SystemRandom().randint, 0) + + +def _eval_at(poly: list[int], x: int, prime: int) -> int: + """ + Evaluate polynomial (coefficient tuple) at x, used to generate a + shamir pool in make_random_shares below. + """ + accum = 0 + for coeff in reversed(poly): + accum *= x + accum += coeff + accum %= prime + + return accum + + +def _extended_gcd(a: int, b: int) -> int: + """ + Division in integers modulus p means finding the inverse of the + denominator modulo p and then multiplying the numerator by this + inverse (Note: inverse of A is B such that A*B % p == 1). This can + be computed via the extended Euclidean algorithm + http://en.wikipedia.org/wiki/Modular_multiplicative_inverse#Computation. + """ + x = 0 + last_x = 1 + y = 1 + last_y = 0 + while b != 0: + quot = a // b + a, b = b, a % b + x, last_x = last_x - quot * x, x + y, last_y = last_y - quot * y, y + + return last_x, last_y + + +def _divmod(num: int, den: int, p: int) -> int: + """ + Compute num / den modulo prime p. + + To explain this, the result will be such that: + den * _divmod(num, den, p) % p == num + """ + inv, _ = _extended_gcd(den, p) + + return num * inv + + +def _lagrange_interpolate(x: int, x_s: list[int], y_s: list[int], p: int) -> int: + """ + Find the y-value for the given x, given n (x, y) points; + k points will define a polynomial of up to kth order. + """ + k = len(x_s) + if k != len(set(x_s)): + msg = "points must be distinct" + raise ValueError(msg) + + def _pi(vals: list[int]) -> int: # upper-case PI -- product of inputs + accum = 1 + for v in vals: + accum *= v + return accum + + nums = [] # avoid inexact division + dens = [] + for i in range(k): + others = list(x_s) + cur = others.pop(i) + nums.append(_pi(x - o for o in others)) + dens.append(_pi(cur - o for o in others)) + + den = _pi(dens) + num = sum([_divmod(nums[i] * den * y_s[i] % p, dens[i], p) for i in range(k)]) + + return (_divmod(num, den, p) + p) % p + + +def make_random_shares(secret: int, minimum: int, shares: int, prime: int) -> list[tuple[int, int]]: + """Generate a random shamir pool for a given secret, returns share points.""" + if minimum > shares: + msg = "Pool secret would be irrecoverable." + raise ValueError(msg) + poly = [secret] + [_RINT(prime - 1) for i in range(minimum - 1)] + return [(i, _eval_at(poly, i, prime)) for i in range(1, shares + 1)] + + + +def recover_secret(shares: list[tuple[int, int]], prime: int) -> int: + """ + Recover the secret from share points + (points (x,y) on the polynomial). + """ + if len(shares) < 2: + msg = "need at least two shares" + raise ValueError(msg) + + x_s, y_s = zip(*shares) + + return _lagrange_interpolate(0, x_s, y_s, prime) diff --git a/pactus/utils/utils.py b/pactus/utils/utils.py index 39e9ad9..69df1b2 100644 --- a/pactus/utils/utils.py +++ b/pactus/utils/utils.py @@ -17,29 +17,3 @@ def encode_from_base256_with_type(hrp: str, typ: str, data: bytes) -> str: converted = bech32m.convertbits(list(data), 8, 5, pad=True) converted = [typ, *converted] return bech32m.bech32_encode(hrp, converted, bech32m.Encoding.BECH32M) - - -def evaluate_polynomial(c: list[int], x: int, mod: int) -> int | None: - """ - Evaluate the polynomial f(x) = c[0] + c[1] * x + c[2] * x^2 + ... + c[n-1] * x^(n-1). - - Args: - c: List of polynomial coefficients (c[0] is the constant term) - x: The value at which to evaluate the polynomial - mod: The modulus to use for the evaluation - - Returns: - The computed value f(x) if success, None otherwise - - """ - if not c: - return None - - if len(c) == 1: - return c[0] - - y = c[-1] - for i in range(len(c) - 2, -1, -1): - y = (y * x + c[i]) % mod - - return y diff --git a/tests/test_crypto_sss.py b/tests/test_crypto_sss.py new file mode 100644 index 0000000..36c5fa4 --- /dev/null +++ b/tests/test_crypto_sss.py @@ -0,0 +1,29 @@ +import unittest +from pactus.crypto.sss import sss + + +class TestEvaluatePolynomial(unittest.TestCase): + def test_wikipedia_example(self): + # https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing + self.assertEqual(sss._eval_at([1234, 166, 94], 1, 2**127 - 1), 1494) + self.assertEqual(sss._eval_at([1234, 166, 94], 2, 2**127 - 1), 1942) + self.assertEqual(sss._eval_at([1234, 166, 94], 3, 2**127 - 1), 2578) + self.assertEqual(sss._eval_at([1234, 166, 94], 4, 2**127 - 1), 3402) + self.assertEqual(sss._eval_at([1234, 166, 94], 5, 2**127 - 1), 4414) + self.assertEqual(sss._eval_at([1234, 166, 94], 6, 2**127 - 1), 5614) + + +class TestRecover(unittest.TestCase): + def test_recover_secret_1(self): + shares = [(1, 1494), (2, 1942), (3, 2578)] + prime = 2**127 - 1 + self.assertEqual(sss.recover_secret(shares, prime), 1234) + + def test_recover_secret_2(self): + shares = [(1, 1494), (3, 2578), (6, 5614)] + prime = 2**127 - 1 + self.assertEqual(sss.recover_secret(shares, prime), 1234) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index 0a91a52..0000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,63 +0,0 @@ -import unittest -from pactus.utils import utils - - -class TestEvaluatePolynomial(unittest.TestCase): - def test_empty_coefficients(self): - self.assertIsNone(utils.evaluate_polynomial([], 2, 7)) - - def test_single_coefficient(self): - self.assertEqual(utils.evaluate_polynomial([5], 10, 7), 5) - - def test_x_zero(self): - # f(0) = c[0] % mod - self.assertEqual(utils.evaluate_polynomial([3, 2, 1], 0, 5), 3 % 5) - - def test_x_one(self): - # f(1) = sum(c) % mod - self.assertEqual(utils.evaluate_polynomial([1, 2, 3], 1, 7), (1 + 2 + 3) % 7) - - def test_multiple_coefficients(self): - # f(2) = 1 + 2*2 + 3*2^2 = 1 + 4 + 12 = 17 % 5 = 2 - self.assertEqual(utils.evaluate_polynomial([1, 2, 3], 2, 5), 2) - - def test_negative_coefficients(self): - # f(2) = -1 + 2*2 + (-3)*2^2 = -1 + 4 - 12 = -9 % 7 = 5 - self.assertEqual(utils.evaluate_polynomial([-1, 2, -3], 2, 7), 5) - - def test_negative_x(self): - # f(-1) = 2 + 3*(-1) + 4*(-1)^2 = 2 - 3 + 4 = 3 % 6 = 3 - self.assertEqual(utils.evaluate_polynomial([2, 3, 4], -1, 6), 3) - - def test_large_modulus(self): - # f(3) = 2 + 4*3 + 5*9 = 2 + 12 + 45 = 59 % 1000 = 59 - self.assertEqual(utils.evaluate_polynomial([2, 4, 5], 3, 1000), 59) - - def test_modulus_one(self): - # Any value mod 1 is 0 - self.assertEqual(utils.evaluate_polynomial([1, 2, 3], 5, 1), 0) - - def test_wikipedia_example(self): - # https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing - self.assertEqual( - utils.evaluate_polynomial([1234, 166, 94], 1, 2**127 - 1), 1494 - ) - self.assertEqual( - utils.evaluate_polynomial([1234, 166, 94], 2, 2**127 - 1), 1942 - ) - self.assertEqual( - utils.evaluate_polynomial([1234, 166, 94], 3, 2**127 - 1), 2578 - ) - self.assertEqual( - utils.evaluate_polynomial([1234, 166, 94], 4, 2**127 - 1), 3402 - ) - self.assertEqual( - utils.evaluate_polynomial([1234, 166, 94], 5, 2**127 - 1), 4414 - ) - self.assertEqual( - utils.evaluate_polynomial([1234, 166, 94], 6, 2**127 - 1), 5614 - ) - - -if __name__ == "__main__": - unittest.main()