A Pure Python implementation of Shamir's Secret Sharing
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

295 lines
8.3 KiB

  1. # Copyright 2023 John-Mark Gurney.
  2. #
  3. # Redistribution and use in source and binary forms, with or without
  4. # modification, are permitted provided that the following conditions
  5. # are met:
  6. # 1. Redistributions of source code must retain the above copyright
  7. # notice, this list of conditions and the following disclaimer.
  8. # 2. Redistributions in binary form must reproduce the above copyright
  9. # notice, this list of conditions and the following disclaimer in the
  10. # documentation and/or other materials provided with the distribution.
  11. #
  12. # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
  13. # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  14. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  15. # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
  16. # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  17. # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
  18. # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  19. # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  20. # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  21. # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  22. # SUCH DAMAGE.
  23. #
  24. #
  25. # ls shamirss.py | entr sh -c ' date; python -m coverage run -m unittest shamirss && coverage report -m'
  26. #
  27. import functools
  28. import operator
  29. import secrets
  30. import unittest.mock
  31. random = secrets.SystemRandom()
  32. def _makered(x, y):
  33. '''Make reduction table entry.
  34. given x * 2^8, reduce it assuming polynomial y.
  35. '''
  36. x = x << 8
  37. for i in range(3, -1, -1):
  38. if x & (1 << (i + 8)):
  39. x ^= (0x100 + y) << i
  40. assert x < 256
  41. return x
  42. def evalpoly(polynomial, powers):
  43. return sum(( x * y for x, y in zip(polynomial, powers)), 0)
  44. def create_shares(data, k, nshares):
  45. '''Given data, create nshares, such that given any k shares,
  46. data can be recovered.
  47. data must be bytes, or able to be converted to bytes.
  48. The return value will be a list of length nshares. Each element
  49. will be a tuple of (<int in range [1, nshares + 1)>, <bytes>).'''
  50. data = bytes(data)
  51. powers = (None, ) + tuple(GF2p8(x).powerseries(k - 1) for x in
  52. range(1, nshares + 1))
  53. coeffs = [ [ x ] + [ random.randint(1, 255) for y in
  54. range(k - 1) ] for idx, x in enumerate(data) ]
  55. return [ (x, bytes([ int(evalpoly(coeffs[idx],
  56. powers[x])) for idx, val in enumerate(data) ])) for x in
  57. range(1, nshares + 1) ]
  58. def recover_data(shares, k):
  59. '''Recover the value given shares, where k is needed.
  60. shares must be as least length of k.'''
  61. if len(shares) < k:
  62. raise ValueError('not enough shares to recover')
  63. return bytes([ int(sum([ GF2p8(y[idx]) *
  64. functools.reduce(operator.mul, [ pix * ((GF2p8(pix) - x) ** -1) for
  65. pix, piy in shares[:k] if pix != x ], 1) for x, y in shares[:k] ],
  66. 0)) for idx in range(len(shares[0][1]))])
  67. class GF2p8:
  68. _invcache = (None, 1, 195, 130, 162, 126, 65, 90, 81, 54, 63, 172, 227, 104, 45, 42, 235, 155, 27, 53, 220, 30, 86, 165, 178, 116, 52, 18, 213, 100, 21, 221, 182, 75, 142, 251, 206, 233, 217, 161, 110, 219, 15, 44, 43, 14, 145, 241, 89, 215, 58, 244, 26, 19, 9, 80, 169, 99, 50, 245, 201, 204, 173, 10, 91, 6, 230, 247, 71, 191, 190, 68, 103, 123, 183, 33, 175, 83, 147, 255, 55, 8, 174, 77, 196, 209, 22, 164, 214, 48, 7, 64, 139, 157, 187, 140, 239, 129, 168, 57, 29, 212, 122, 72, 13, 226, 202, 176, 199, 222, 40, 218, 151, 210, 242, 132, 25, 179, 185, 135, 167, 228, 102, 73, 149, 153, 5, 163, 238, 97, 3, 194, 115, 243, 184, 119, 224, 248, 156, 92, 95, 186, 34, 250, 240, 46, 254, 78, 152, 124, 211, 112, 148, 125, 234, 17, 138, 93, 188, 236, 216, 39, 4, 127, 87, 23, 229, 120, 98, 56, 171, 170, 11, 62, 82, 76, 107, 203, 24, 117, 192, 253, 32, 74, 134, 118, 141, 94, 158, 237, 70, 69, 180, 252, 131, 2, 84, 208, 223, 108, 205, 60, 106, 177, 61, 200, 36, 232, 197, 85, 113, 150, 101, 28, 88, 49, 160, 38, 111, 41, 20, 31, 109, 198, 136, 249, 105, 12, 121, 166, 66, 246, 207, 37, 154, 16, 159, 189, 128, 96, 144, 47, 114, 133, 51, 59, 231, 67, 137, 225, 143, 35, 193, 181, 146, 79)
  69. @staticmethod
  70. def _primativemul(a, b):
  71. masks = [ 0, 0xff ]
  72. r = 0
  73. for i in range(0, 8):
  74. mask = a & 1
  75. r ^= (masks[mask] & b) << i
  76. a = a >> 1
  77. return r
  78. # polynomial 0x187
  79. _reduce = tuple(_makered(x, 0x87) for x in range(0, 16))
  80. def __init__(self, v):
  81. if v >= 256:
  82. raise ValueError('%d is not a member of GF(2^8)' % v)
  83. self._v = v
  84. # basic operations
  85. def __add__(self, o):
  86. if isinstance(o, int):
  87. return self + self.__class__(o)
  88. return self.__class__(self._v ^ o._v)
  89. def __radd__(self, o):
  90. return self.__add__(o)
  91. def __sub__(self, o):
  92. return self.__add__(o)
  93. def __rsub__(self, o):
  94. return self.__sub__(o)
  95. def __mul__(self, o):
  96. if isinstance(o, int):
  97. o = self.__class__(o)
  98. m = o._v
  99. # multiply
  100. r = self._primativemul(self._v, m)
  101. # reduce
  102. r ^= self._reduce[r >> 12] << 4
  103. r ^= self._reduce[(r >> 8) & 0xf ]
  104. r &= 0xff
  105. return self.__class__(r)
  106. def __rmul__(self, o):
  107. return self.__mul__(o)
  108. def __pow__(self, x):
  109. if x == -1 and self._invcache:
  110. return GF2p8(self._invcache[self._v])
  111. if x < 0:
  112. x += 255
  113. v = self.__class__(1)
  114. # TODO - make faster via caching and squaring
  115. for i in range(x):
  116. v *= self
  117. return v
  118. def powerseries(self, cnt):
  119. '''Generate [ self ** 0, self ** 1, ..., self ** cnt ].'''
  120. r = [ 1 ]
  121. for i in range(1, cnt):
  122. r.append(r[-1] * self)
  123. return r
  124. def __eq__(self, o):
  125. if isinstance(o, int):
  126. return self._v == o
  127. return self._v == o._v
  128. def __int__(self):
  129. return self._v
  130. def __repr__(self):
  131. return '%s(%d)' % (self.__class__.__name__, self._v)
  132. class TestShamirSS(unittest.TestCase):
  133. def test_evalpoly(self):
  134. a = GF2p8(random.randint(0, 255))
  135. powers = a.powerseries(5)
  136. vals = [ GF2p8(random.randint(0, 255)) for x in range(5) ]
  137. r = evalpoly(vals, powers)
  138. self.assertEqual(r, vals[0] + vals[1] * powers[1] + vals[2] *
  139. powers[2] + vals[3] * powers[3] + vals[4] * powers[4])
  140. r = evalpoly(vals[:3], powers)
  141. self.assertEqual(r, vals[0] + vals[1] * powers[1] + vals[2] *
  142. powers[2])
  143. def test_create_shares(self):
  144. self.assertRaises(TypeError, create_shares, '', 1, 1)
  145. val = bytes([ random.randint(0, 255) for x in range(100) ])
  146. a = create_shares(val, 2, 3)
  147. # that it has the number of shares
  148. self.assertEqual(len(a), 3)
  149. # that the length of the share data matches passed in data
  150. self.assertEqual(len(a[0][1]), len(val))
  151. # that one share isn't enough
  152. self.assertRaises(ValueError, recover_data, [ a[0] ], 2)
  153. self.assertEqual(val, recover_data(a[:2], 2))
  154. def test_gf2p8_inv(self):
  155. a = GF2p8(random.randint(0, 255))
  156. with unittest.mock.patch.object(GF2p8, '_invcache', []) as pinvc:
  157. ainv = a ** -1
  158. self.assertEqual(a * ainv, 1)
  159. invcache = (None, ) + \
  160. tuple(int(GF2p8(x) ** -1) for x in range(1, 256))
  161. if GF2p8._invcache != invcache: # pragma: no cover
  162. print('inv cache:', repr(invcache))
  163. self.assertEqual(GF2p8._invcache, invcache)
  164. def test_gf2p8_power(self):
  165. a = GF2p8(random.randint(0, 255))
  166. v = GF2p8(1)
  167. for i in range(10):
  168. self.assertEqual(a ** i, v)
  169. v = v * a
  170. for i in range(10):
  171. a = GF2p8(random.randint(0, 255))
  172. powers = a.powerseries(10)
  173. for j in range(10):
  174. self.assertEqual(powers[j], a ** j)
  175. def test_gf2p8_errors(self):
  176. self.assertRaises(ValueError, GF2p8, 1000)
  177. def test_gf2p8(self):
  178. self.assertEqual(int(GF2p8(5)), 5)
  179. self.assertEqual(repr(GF2p8(5)), 'GF2p8(5)')
  180. for i in range(10):
  181. a = GF2p8(random.randint(0, 255))
  182. b = GF2p8(random.randint(0, 255))
  183. c = GF2p8(random.randint(0, 255))
  184. self.assertEqual(a * 0, 0)
  185. # Identity
  186. self.assertEqual(a + 0, a)
  187. self.assertEqual(a * 1, a)
  188. self.assertEqual(0 + a, a)
  189. self.assertEqual(1 * a, a)
  190. self.assertEqual(0 - a, a)
  191. # Associativity
  192. self.assertEqual((a + b) + c, a + (b + c))
  193. self.assertEqual((a * b) * c, a * (b * c))
  194. # Communitative
  195. self.assertEqual(a + b, b + a)
  196. self.assertEqual(a * b, b * a)
  197. # Distributive
  198. self.assertEqual(a * (b + c), a * b + a * c)
  199. self.assertEqual((b + c) * a, b * a + c * a)
  200. # Basic mul
  201. self.assertEqual(GF2p8(0x80) * 2, 0x87)
  202. self.assertEqual(GF2p8(0x80) * 6,
  203. (0x80 * 6) ^ (0x187 << 1))
  204. self.assertEqual(GF2p8(0x80) * 8,
  205. (0x80 * 8) ^ (0x187 << 2) ^ (0x187 << 1) ^ 0x187)
  206. self.assertEqual(a + b - b, a)