diff --git a/readme.md b/readme.md --- a/readme.md +++ b/readme.md @@ -10,7 +10,7 @@ Can be run straight from the cloned repo ## Performance ## -As it is, the code is not very fast. Splitting takes _n_ evaluations of a polynomial of order _k_ over Galois field 256, leading to _O(n\*k)_ finite field multiplications. Reconstruction of the constant parameters during joining similarly takes _O(k\*k)_ multiplications. +As it is, the code is not very fast. Let's assume we have a secret of length _m_. For each byte, the splitting takes _n_ evaluations of a polynomial of order _k_ over Galois field 256, leading to _O(n\*k\*m)_ finite field multiplications. Reconstruction of the constant parameters during joining takes _O(k\*k + k\*m)_ multiplications. Benchmark results, all values mean _seconds per byte_ of the secret length: @@ -21,23 +21,23 @@ Benchmark results, all values mean _seco - - + + - - + + - - + + - - + +
2 / 3 (a Raspberry Pi 3)7.99e-050.0004288.32e-050.000435
2 / 3 (a laptop)1e-056.7e-051.09e-057.17e-05
254 / 254 (a Raspberry Pi 3)0.4170.4710.4140.0314
254 / 254 (a laptop)0.04310.05420.04350.00347
diff --git a/src/shamira/condensed.py b/src/shamira/condensed.py --- a/src/shamira/condensed.py +++ b/src/shamira/condensed.py @@ -27,7 +27,7 @@ for i in range(256): L[acc] = i acc = gfmul(acc, g) L[1] = 0 -inv = [E[255-L[i]] if i!=0 else None for i in range(256)] # multiplicative inverse +INV = [E[255-L[i]] if i!=0 else None for i in range(256)] # multiplicative inverse def get_constant_coef(*points): @@ -42,7 +42,7 @@ def get_constant_coef(*points): for j in range(k): if i==j: continue (xj, yj) = points[j] - prod = gfmul(prod, (gfmul(xj, inv[xj^x]))) + prod = gfmul(prod, (gfmul(xj, INV[xj^x]))) res ^= gfmul(y, prod) return res diff --git a/src/shamira/core.py b/src/shamira/core.py --- a/src/shamira/core.py +++ b/src/shamira/core.py @@ -43,11 +43,13 @@ def reconstruct_raw(*shares): if len({x for (x, _) in shares}) < len(shares): raise MalformedShare("Found a non-unique share. Please check your inputs.") - secret_len = len(shares[0][1]) + (xs, payloads) = zip(*shares) + secret_len = len(payloads[0]) res = [None]*secret_len + weights = gf256.compute_weights(xs) for i in range(secret_len): - points = [(x, s[i]) for (x, s) in shares] - res[i] = (gf256.get_constant_coef(*points)) + ys = [s[i] for s in payloads] + res[i] = (gf256.get_constant_coef(weights, ys)) return bytes(res) diff --git a/src/shamira/gf256.py b/src/shamira/gf256.py --- a/src/shamira/gf256.py +++ b/src/shamira/gf256.py @@ -2,6 +2,9 @@ """Arithmetic operations on Galois Field 2**8. See https://en.wikipedia.org/wiki/Finite_field_arithmetic""" +from functools import reduce +import operator + def _gfmul(a, b): """Basic multiplication. Russian peasant algorithm.""" @@ -47,18 +50,25 @@ def evaluate(coefs, x): return res -def get_constant_coef(*points): +def get_constant_coef(weights, y_coords): """Compute constant polynomial coefficient given the points. See https://en.wikipedia.org/wiki/Shamir's_Secret_Sharing#Computationally_Efficient_Approach""" - k = len(points) - res = 0 - for i in range(k): - (x, y) = points[i] - prod = 1 - for j in range(k): - if i==j: continue - (xj, yj) = points[j] - prod = gfmul(prod, (gfmul(xj, INV[xj^x]))) - res ^= gfmul(y, prod) + return reduce( + operator.xor, + map(lambda ab: gfmul(*ab), zip(weights, y_coords)) + ) + + +def compute_weights(x_coords): + assert x_coords + + res = [ + reduce( + gfmul, + (gfmul(xj, INV[xj^xi]) for xj in x_coords if xi!=xj), + 1 + ) for xi in x_coords + ] + return res diff --git a/src/shamira/tests/test_gf256.py b/src/shamira/tests/test_gf256.py --- a/src/shamira/tests/test_gf256.py +++ b/src/shamira/tests/test_gf256.py @@ -30,7 +30,9 @@ class TestGF256(TestCase): self.assertEqual(evaluate([a0, a1, a2, a3], 1), a0^a1^a2^a3) # polynomial at 1 == sum of coefficients def test_get_constant_coef(self): - self.assertEqual(get_constant_coef((1, 1), (2, 2), (3, 3)), 0) + weights = compute_weights((1, 2, 3)) + ys = (1, 2, 3) + self.assertEqual(get_constant_coef(weights, ys), 0) random.seed(17) random_matches = 0 @@ -55,7 +57,10 @@ class TestGF256(TestCase): coefs = [random.randint(0, 255) for i in range(k)] points = [(j, evaluate(coefs, j)) for j in range(1, 256)] random.shuffle(points) - return (get_constant_coef(*points[:m]), coefs[0]) + + (xs, ys) = zip(*points[:m]) + weights = compute_weights(xs) + return (get_constant_coef(weights, ys), coefs[0]) if __name__=='__main__': diff --git a/src/shamira/tests/test_shamira.py b/src/shamira/tests/test_shamira.py --- a/src/shamira/tests/test_shamira.py +++ b/src/shamira/tests/test_shamira.py @@ -28,11 +28,17 @@ class TestShamira(TestCase): with self.assertRaises(ValueError): # not castable to int _share_byte("x", 2, 3) - vals = _share_byte(ord(b"a"), 2, 3) - points = list(zip(range(1, 256), vals)) - self.assertEqual(gf256.get_constant_coef(*points), ord(b"a")) - self.assertEqual(gf256.get_constant_coef(*points[:2]), ord(b"a")) - self.assertNotEqual(gf256.get_constant_coef(*points[:1]), ord(b"a")) # underdetermined => random + ys = _share_byte(ord(b"a"), 2, 3) + xs = list(range(1, 4)) + + weights = gf256.compute_weights(xs) + self.assertEqual(gf256.get_constant_coef(weights, ys), ord(b"a")) + + weights = gf256.compute_weights(xs[:2]) + self.assertEqual(gf256.get_constant_coef(weights, ys[:2]), ord(b"a")) + + weights = gf256.compute_weights(xs[:1]) + self.assertNotEqual(gf256.get_constant_coef(weights, ys[:1]), ord(b"a")) # underdetermined => random def test_generate_reconstruct_raw(self): for (k, n) in [(2, 3), (254, 254)]: