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.

448 lines
11 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. '''
  28. An implementation of Shamir's Secret Sharing.
  29. This is over GF(2^256), so unlike some other implementations that are
  30. over primes, it is valid for ALL values, and the output will be exactly
  31. the same length as the secret. This limits the number of shares to
  32. 255.
  33. Sample usage:
  34. ```
  35. import random
  36. from shamirss import *
  37. data = random.SystemRandom().randbytes(32)
  38. shares = create_shares(data, 3, 5)
  39. rdata = recover_data([ shares[1], shares[2], shares[4] ], 3)
  40. print(rdata == data)
  41. ```
  42. '''
  43. import functools
  44. import itertools
  45. import operator
  46. import secrets
  47. import unittest.mock
  48. random = secrets.SystemRandom()
  49. __all__ = [
  50. 'create_shares',
  51. 'recover_data',
  52. 'GF2p8',
  53. ]
  54. def _makered(x, y):
  55. '''Make reduction table entry.
  56. given x * 2^8, reduce it assuming polynomial y.
  57. '''
  58. x = x << 8
  59. for i in range(3, -1, -1):
  60. if x & (1 << (i + 8)):
  61. x ^= (0x100 + y) << i
  62. assert x < 256
  63. return x
  64. def evalpoly(polynomial, powers):
  65. return sum(( x * y for x, y in zip(polynomial, powers,
  66. strict=True)), 0)
  67. def create_shares(data, k, nshares):
  68. '''Given data, create nshares, such that given any k shares,
  69. data can be recovered.
  70. data must be bytes, or able to be converted to bytes, e.g. a list
  71. of ints in the range [ 0, 255 ].
  72. The return value will be a list of length nshares. Each element
  73. will be a tuple of (<int in range [ 1, nshares ]>, <bytes>).'''
  74. data = bytes(data)
  75. #print(repr(data), repr(k), repr(nshares))
  76. powers = (None, ) + tuple(GF2p8(x).powerseries(k - 1) for x in
  77. range(1, nshares + 1))
  78. coeffs = [ [ x ] + [ random.randint(0, 255) for y in
  79. range(k - 1) ] for idx, x in enumerate(data) ]
  80. return [ (x, bytes([ int(evalpoly(coeffs[idx],
  81. powers[x])) for idx, val in enumerate(data) ])) for x in
  82. range(1, nshares + 1) ]
  83. def recover_data(shares, k):
  84. '''Recover the value given shares, where k is the number of
  85. shares needed.
  86. shares must be as least length of k.
  87. Each element of shares is from one returned by create_shares,
  88. that is a tuple of an int and bytes.'''
  89. if len(shares) < k:
  90. raise ValueError('not enough shares to recover')
  91. return bytes([ int(sum(( GF2p8(y[idx]) *
  92. functools.reduce(operator.mul, ( pix * ((GF2p8(pix) - x) ** -1) for
  93. pix, piy in shares[:k] if pix != x ), 1) for x, y in shares[:k] ),
  94. 0)) for idx in range(len(shares[0][1]))])
  95. class GF2p8:
  96. # polynomial 0x187
  97. '''An implementation of GF(2^8). It uses the polynomial 0x187
  98. or x^8 + x^7 + x^2 + x + 1.
  99. '''
  100. _invcache = b'\x00\x01\xc3\x82\xa2~AZQ6?\xac\xe3h-*\xeb\x9b\x1b5\xdc\x1eV\xa5\xb2t4\x12\xd5d\x15\xdd\xb6K\x8e\xfb\xce\xe9\xd9\xa1n\xdb\x0f,+\x0e\x91\xf1Y\xd7:\xf4\x1a\x13\tP\xa9c2\xf5\xc9\xcc\xad\n[\x06\xe6\xf7G\xbf\xbeDg{\xb7!\xafS\x93\xff7\x08\xaeM\xc4\xd1\x16\xa4\xd60\x07@\x8b\x9d\xbb\x8c\xef\x81\xa89\x1d\xd4zH\r\xe2\xca\xb0\xc7\xde(\xda\x97\xd2\xf2\x84\x19\xb3\xb9\x87\xa7\xe4fI\x95\x99\x05\xa3\xeea\x03\xc2s\xf3\xb8w\xe0\xf8\x9c\\_\xba"\xfa\xf0.\xfeN\x98|\xd3p\x94}\xea\x11\x8a]\xbc\xec\xd8\'\x04\x7fW\x17\xe5xb8\xab\xaa\x0b>RLk\xcb\x18u\xc0\xfd J\x86v\x8d^\x9e\xedFE\xb4\xfc\x83\x02T\xd0\xdfl\xcd<j\xb1=\xc8$\xe8\xc5Uq\x96e\x1cX1\xa0&o)\x14\x1fm\xc6\x88\xf9i\x0cy\xa6B\xf6\xcf%\x9a\x10\x9f\xbd\x80`\x90/r\x853;\xe7C\x89\xe1\x8f#\xc1\xb5\x92O'
  101. @staticmethod
  102. def _primativemul(a, b):
  103. masks = [ 0, 0xff ]
  104. r = 0
  105. for i in range(0, 8):
  106. mask = a & 1
  107. r ^= (masks[mask] & b) << i
  108. a = a >> 1
  109. return r
  110. # bytes is smaller, 49 vs 168 bytes, so should fit in a cache line
  111. _reduce = b'\x00\x87\x89\x0e\x95\x12\x1c\x9b\xad*$\xa38\xbf\xb16'
  112. def __init__(self, v):
  113. '''v must be in the range [ 0, 255 ].
  114. Create an element of GF(2^8).
  115. The operators have been overloaded, so most normal math works.
  116. It will also automatically promote non-GF2p8 numbers if
  117. possible, e.g. GF2p8(5) + 10 works.
  118. '''
  119. if v >= 256 or v < 0:
  120. raise ValueError('%d is not a member of GF(2^8)' % v)
  121. self._v = int(v)
  122. if self._v != v:
  123. raise ValueError('%d is not a member of GF(2^8)' % v)
  124. # basic operations
  125. def __add__(self, o):
  126. if not isinstance(o, self.__class__):
  127. o = self.__class__(o)
  128. return self.__class__(self._v ^ o._v)
  129. def __radd__(self, o):
  130. return self.__add__(o)
  131. def __sub__(self, o):
  132. return self.__add__(o)
  133. def __rsub__(self, o):
  134. return self.__sub__(o)
  135. def __mul__(self, o):
  136. if not isinstance(o, self.__class__):
  137. o = self.__class__(o)
  138. m = o._v
  139. # possibly use log tables:
  140. # a = GF2p8(0x87)
  141. # logtbl = { idx: a ** idx for idx in range(256) }
  142. # invlogtbl = { v: k for k, v in logtbl.items() }
  143. # len(invlogtbl)
  144. # 255
  145. # invlogtbl[GF2p8(6)] + invlogtbl[GF2p8(213)]
  146. # 254
  147. # logtbl[254]
  148. # GF2p8(119)
  149. # multiply
  150. r = self._primativemul(self._v, m)
  151. # reduce
  152. r ^= self._reduce[r >> 12] << 4
  153. r ^= self._reduce[(r >> 8) & 0xf ]
  154. r &= 0xff
  155. return self.__class__(r)
  156. def __rmul__(self, o):
  157. return self.__mul__(o)
  158. def __truediv__(self, o):
  159. if not isinstance(o, self.__class__):
  160. o = self.__class__(o)
  161. return self * (o ** -1)
  162. def __rtruediv__(self, o):
  163. if not isinstance(o, self.__class__):
  164. o = self.__class__(o)
  165. return o * (self ** -1)
  166. def __pow__(self, x):
  167. if self._v == 0:
  168. if x < 0:
  169. raise ZeroDivisionError
  170. return self
  171. if x == -1 and self._invcache:
  172. return self.__class__(self._invcache[self._v])
  173. # we loop after 255, so no need to do extra work
  174. x %= 255
  175. # Note: not constant time, also, not optimial algorithm
  176. # The art of computer programming vol 2. ยง 4.6.3 Algorithm A
  177. # https://archive.org/details/artofcomputerpro0000knut/page/400/mode/2up
  178. # A1
  179. n = x
  180. y = self.__class__(1)
  181. z = self
  182. while n:
  183. # A2
  184. n, isodd = divmod(n, 2)
  185. if not isodd:
  186. # A5
  187. z *= z
  188. continue
  189. # A3
  190. y *= z
  191. # A4
  192. if not n:
  193. break
  194. # A5
  195. z *= z
  196. return y
  197. def powerseries(self, cnt):
  198. '''Generate [ self ** 0, self ** 1, ..., self ** cnt ].'''
  199. r = [ self.__class__(1) ]
  200. for i in range(1, cnt + 1):
  201. r.append(r[-1] * self)
  202. return r
  203. def __eq__(self, o):
  204. if not isinstance(o, self.__class__):
  205. o = self.__class__(o)
  206. return self._v == o._v
  207. def __int__(self):
  208. return self._v
  209. def __hash__(self):
  210. return hash(self._v)
  211. def __repr__(self):
  212. return '%s(%d)' % (self.__class__.__name__, self._v)
  213. class TestShamirSS(unittest.TestCase):
  214. def test_evalpoly(self):
  215. a = GF2p8(random.randint(0, 255))
  216. powers = a.powerseries(4)
  217. self.assertTrue(all(isinstance(x, GF2p8) for x in powers))
  218. vals = [ GF2p8(random.randint(0, 255)) for x in range(5) ]
  219. r = evalpoly(vals, powers)
  220. self.assertEqual(r, vals[0] + vals[1] * powers[1] + vals[2] *
  221. powers[2] + vals[3] * powers[3] + vals[4] * powers[4])
  222. r = evalpoly(vals[:3], powers[:3])
  223. self.assertEqual(r, vals[0] + vals[1] * powers[1] + vals[2] *
  224. powers[2])
  225. self.assertRaises(ValueError, evalpoly, [1], [1, 2])
  226. def test_create_shares(self):
  227. self.assertRaises(TypeError, create_shares, '', 1, 1)
  228. val = bytes([ random.randint(0, 255) for x in range(100) ])
  229. #val = b'this is a test of english text.'
  230. #val = b'1234'
  231. a = create_shares(val, 2, 3)
  232. self.assertNotIn(val, set(x[1] for x in a))
  233. # that it has the number of shares
  234. self.assertEqual(len(a), 3)
  235. # that the length of the share data matches passed in data
  236. self.assertEqual(len(a[0][1]), len(val))
  237. # that one share isn't enough
  238. self.assertRaises(ValueError, recover_data, [ a[0] ], 2)
  239. for i, j in itertools.combinations(range(3), 2):
  240. self.assertEqual(val, recover_data([ a[i], a[j] ], 2))
  241. self.assertEqual(val, recover_data([ a[j], a[i] ], 2))
  242. a = create_shares(val, 15, 30)
  243. for i in range(5):
  244. self.assertEqual(val, recover_data([ a[j] for j in random.sample(range(30), 15) ], 15))
  245. def test_gf2p8_reduce(self):
  246. reduce = bytes((_makered(x, 0x87) for x in range(0, 16)))
  247. if GF2p8._reduce != reduce: # pragma: no cover
  248. print('reduce:', repr(reduce))
  249. self.assertEqual(GF2p8._reduce, reduce)
  250. def test_gf2p8_inv(self):
  251. a = GF2p8(random.randint(0, 255))
  252. with unittest.mock.patch.object(GF2p8, '_invcache', ()) as pinvc:
  253. ainv = a ** -1
  254. self.assertEqual(a * ainv, 1)
  255. invcache = bytes((0, ) + \
  256. tuple(int(GF2p8(x) ** -1) for x in range(1, 256)))
  257. if GF2p8._invcache != invcache: # pragma: no cover
  258. print('inv cache:', repr(invcache))
  259. self.assertEqual(GF2p8._invcache, invcache)
  260. def test_gf2p8_power(self):
  261. zero = GF2p8(0)
  262. self.assertEqual(zero ** 5, zero)
  263. with self.assertRaises(ZeroDivisionError):
  264. zero ** -1
  265. a = GF2p8(random.randint(0, 255))
  266. v = GF2p8(1)
  267. for i in range(260):
  268. self.assertEqual(a ** i, v)
  269. v = v * a
  270. for i in range(10):
  271. neg = random.randint(-600, -1)
  272. p = neg
  273. while p < 0:
  274. p += 255
  275. self.assertEqual(a ** neg, a ** p)
  276. for i in range(10):
  277. a = GF2p8(random.randint(0, 255))
  278. powers = a.powerseries(10)
  279. for j in range(11):
  280. self.assertEqual(powers[j], a ** j)
  281. def test_gf2p8_errors(self):
  282. self.assertRaises(ValueError, GF2p8, 1000)
  283. self.assertRaises(ValueError, GF2p8, 40.5)
  284. self.assertRaises(ValueError, GF2p8, -1)
  285. def test_gf2p8_div(self):
  286. self.assertEqual(GF2p8(10) / 11, GF2p8(11) ** -1 * GF2p8(10))
  287. self.assertEqual(10 / GF2p8(11), GF2p8(11) ** -1 * GF2p8(10))
  288. def test_gf2p8(self):
  289. self.assertEqual(int(GF2p8(5)), 5)
  290. self.assertEqual(repr(GF2p8(5)), 'GF2p8(5)')
  291. for i in range(10):
  292. a = GF2p8(random.randint(0, 255))
  293. b = GF2p8(random.randint(0, 255))
  294. c = GF2p8(random.randint(0, 255))
  295. # Hashable
  296. { a, b, c }
  297. self.assertEqual(a * 0, 0)
  298. # Identity
  299. self.assertEqual(a + 0, a)
  300. self.assertEqual(a * 1, a)
  301. self.assertEqual(0 + a, a)
  302. self.assertEqual(1 * a, a)
  303. self.assertEqual(0 - a, a)
  304. # Associativity
  305. self.assertEqual((a + b) + c, a + (b + c))
  306. self.assertEqual((a * b) * c, a * (b * c))
  307. # Communitative
  308. self.assertEqual(a + b, b + a)
  309. self.assertEqual(a * b, b * a)
  310. # Distributive
  311. self.assertEqual(a * (b + c), a * b + a * c)
  312. self.assertEqual((b + c) * a, b * a + c * a)
  313. # Basic mul
  314. self.assertEqual(GF2p8(0x80) * 2, 0x87)
  315. self.assertEqual(GF2p8(0x80) * 6,
  316. (0x80 * 6) ^ (0x187 << 1))
  317. self.assertEqual(GF2p8(0x80) * 8,
  318. (0x80 * 8) ^ (0x187 << 2) ^ (0x187 << 1) ^ 0x187)
  319. self.assertEqual(a + b - b, a)