Skip to content

Commit

Permalink
create basic secret sharing library
Browse files Browse the repository at this point in the history
  • Loading branch information
shea256 committed Feb 5, 2014
1 parent 6fd4755 commit b607e96
Show file tree
Hide file tree
Showing 8 changed files with 343 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ nosetests.xml
.mr.developer.cfg
.project
.pydevproject

ignore
Empty file added AUTHORS
Empty file.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,39 @@
secretsharing
=============

A system for sharing secrets using Shamir's Secret Sharing Scheme.

## Sample Usage

#### Creating secrets from strings

>>> from secretsharing.shamir import Secret
>>> secret = Secret.from_printable_ascii("Hello, world!")
>>> secret.as_printable_ascii()
'Hello, world!'
>>> secret.as_int()
43142121247394322427211362L

#### Creating secrets from integers

>>> secret = Secret(43142121247394322427211362L)

#### Spliting secrets into shares

>>> shares = secret.split(3, 5)
>>> for s in shares:
... print s:
...
01-762cfaba2802c2191e486f
02-1762f2ca77fbd06de2565c4
03-123b648dc47453748d24662
04-17ec24f587e9b535924ea48
05-8753401c25bf5b0f1d5177

#### Recovering secrets from shares

>>> recovered_shares = ['02-1762f2ca77fbd06de2565c4', '04-17ec24f587e9b535924ea48', '05-8753401c25bf5b0f1d5177']
>>> recovered_secret = Secret.from_shares(recovered_shares)
>>> recovered_secret.as_printable_ascii()
'Hello, world!'

12 changes: 12 additions & 0 deletions secretsharing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
"""
Secret Sharing
~~~~~
:copyright: (c) 2014 by Halfmoon Labs
:license: MIT, see LICENSE for more details.
"""

__version__ = '0.1.0'

from .shamir import Secret
108 changes: 108 additions & 0 deletions secretsharing/math.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
"""
Secret Sharing
~~~~~
:copyright: (c) 2014 by Halfmoon Labs
:license: MIT, see LICENSE for more details.
"""

import random

def egcd(a, b):
if a == 0:
return (b, 0, 1)
else:
g, y, x = egcd(b % a, a)
return (g, x - (b // a) * y, y)

def mod_inverse(k, prime):
k = k % prime
if k < 0:
r = egcd(prime, -k)[2]
else:
r = egcd(prime, k)[2]
return (prime + r) % prime

def get_mersenne_primes():
""" Returns all the mersenne primes with less than 500 digits.
All primes:
3, 7, 31, 127, 8191, 131071, 524287, 2147483647L, 2305843009213693951L,
618970019642690137449562111L, 162259276829213363391578010288127L,
170141183460469231731687303715884105727L,
68647976601306097149...12574028291115057151L, (157 digits)
53113799281676709868...70835393219031728127L, (183 digits)
10407932194664399081...20710555703168729087L, (386 digits)
"""
mersenne_prime_exponents = [
2, 3, 5, 7, 13, 17, 19, 31, 61, 89, 107, 127, 521, 607, 1279
]
primes = []
for exp in mersenne_prime_exponents:
prime = long(1)
for i in range(exp):
prime *= 2
prime -= 1
primes.append(prime)
return primes

def get_large_enough_prime(batch):
""" Returns a prime number that is greater all the numbers in the batch.
"""
# build a list of primes
primes = get_mersenne_primes()
# find a prime that is greater than all the numbers in the batch
for prime in primes:
numbers_greater_than_prime = [i for i in batch if i > prime]
if len(numbers_greater_than_prime) == 0:
return prime
return None

def random_polynomial(degree, intercept, upper_bound):
""" Generates a random polynomial with positive coefficients.
"""
if degree < 0:
raise ValueError('Degree must be a non-negative number.')
coefficients = [intercept]
for i in range(degree):
random_coeff = random.randint(0, upper_bound-1)
coefficients.append(random_coeff)
return coefficients

def get_polynomial_points(coefficients, num_points, prime):
""" Calculates the first n polynomial points.
[ (1, f(1)), (2, f(2)), ... (n, f(n)) ]
"""
points = []
for x in range(1, num_points+1):
# start with x=1 and calculate the value of y
y = coefficients[0]
# calculate each term and add it to y, using modular math
for i in range(1, len(coefficients)):
exponentiation = (long(x)**i) % prime
term = (coefficients[i] * exponentiation) % prime
y = (y + term) % prime
# add the point to the list of points
points.append((x, y))
return points

def modular_lagrange_interpolation(x, points, prime):
# break the points up into lists of x and y values
x_values, y_values = zip(*points)
# initialize f(x) and begin the calculation: f(x) = SUM( y_i * l_i(x) )
f_x = long(0)
for i in range(len(points)):
# evaluate the lagrange basis polynomial l_i(x)
numerator, denominator = 1, 1
for j in range(len(points)):
# don't compute a polynomial fraction if i equals j
if i == j: continue
# compute a fraction and update the existing numerator + denominator
numerator = (numerator * (x - x_values[j])) % prime
denominator = (denominator * (x_values[i] - x_values[j])) % prime
# get the polynomial from the numerator + mod inverse of the denominator
lagrange_polynomial = numerator * mod_inverse(denominator, prime)
# multiply the current y and the evaluated polynomial and add it to f(x)
f_x = (prime + f_x + (y_values[i] * lagrange_polynomial)) % prime
return f_x

107 changes: 107 additions & 0 deletions secretsharing/shamir.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
"""
Secret Sharing
~~~~~
:copyright: (c) 2014 by Halfmoon Labs
:license: MIT, see LICENSE for more details.
"""

import string
from characters.charset import charset_to_int, int_to_charset
from characters.hex import hex_to_int, int_to_hex, is_hex

from .math import get_large_enough_prime, random_polynomial, \
get_polynomial_points, modular_lagrange_interpolation

def share_to_point(share):
# share should be in the format "01-d051080de7..."
if isinstance(share, str) and share.count('-') == 1:
x,y = share.split('-')
if is_hex(x) and is_hex(y):
return (hex_to_int(x), hex_to_int(y))
raise ValueError('Share format is invalid.')

def point_to_share(point):
# point should be in the format (1, 4938573982723...)
if isinstance(point, tuple) and len(point) == 2:
if isinstance(point[0], (int, long)) and isinstance(point[1], (int, long)):
x,y = point
if x > 255:
raise ValueError('The largest x coordinate for a share is 255.')
hex_x, hex_y = int_to_hex(x).zfill(2), int_to_hex(y)
return hex_x + '-' + hex_y
else:
print "ah!"
raise ValueError('Point format is invalid. Must be a pair of integers.')

class Secret():
def __init__(self, secret_int):
if not isinstance(secret_int, (int, long)) and secret_int >= 0:
raise ValueError("Secret must be a non-negative integer.")
self._secret = secret_int

@classmethod
def from_charset(cls, secret, charset):
if not isinstance(secret, str):
raise ValueError("Secret must be a string.")
if not isinstance(charset, str):
raise ValueError("Charset must be a string.")
if (set(secret) - set(charset)):
raise ValueError("Secret contains characters that aren't in the charset.")
secret_int = charset_to_int(secret, charset)
return cls(secret_int)

@classmethod
def from_hex(cls, secret):
return cls.from_charset(secret, string.hexdigits[0:16])

@classmethod
def from_printable_ascii(cls, secret):
return cls.from_charset(secret, string.printable)

@classmethod
def from_shares(cls, shares):
if not isinstance(shares, list):
raise ValueError("Shares must be in list form.")
for share in shares:
if not isinstance(share, str):
raise ValueError("Each share must be a string.")
points = []
for share in shares:
points.append(share_to_point(share))
x_values, y_values = zip(*points)
prime = get_large_enough_prime(y_values)
free_coefficient = modular_lagrange_interpolation(0, points, prime)
secret_int = free_coefficient
return cls(secret_int)

def split(self, threshold, num_shares):
""" Split the secret into shares. The threshold is the total number of
"""
if threshold < 2:
raise ValueError("Threshold must be >= 2.")
if threshold > num_shares:
raise ValueError("Threshold must be < the total number of shares.")
prime = get_large_enough_prime([self._secret, num_shares])
if not prime:
raise ValueError("Error! Secret is too long for share calculation!")
coefficients = random_polynomial(threshold-1, self._secret, prime)
points = get_polynomial_points(coefficients, num_shares, prime)
shares = []
for point in points:
shares.append(point_to_share(point))
return shares

def as_int(self):
return self._secret

def as_charset(self, charset):
return int_to_charset(self._secret, charset)

def as_hex(self):
return self.as_charset(string.hexdigits[0:16])

def as_printable_ascii(self):
return self.as_charset(string.printable)

30 changes: 30 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
Secret Sharing
==============
"""

from setuptools import setup

setup(
name='secretsharing',
version='0.1.0',
url='https://github.com/halfmoonlabs/secretsharing',
license='MIT',
author='Halfmoon Labs',
author_email='[email protected]',
description="Tools for sharing secrets, including shamir's secret sharing scheme.",
packages=[
'secretsharing',
],
zip_safe=False,
install_requires=[
'characters>=0.1'
],
classifiers=[
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
],
)
47 changes: 47 additions & 0 deletions tests/unit_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
"""
Secret Sharing
~~~~~
:copyright: (c) 2014 by Halfmoon Labs
:license: MIT, see LICENSE for more details.
"""

import unittest
from test import test_support

from secretsharing.shamir import Secret

class ShamirSharingTest(unittest.TestCase):
def setUp(self):
pass

def tearDown(self):
pass

def secret_message_to_shares(secret_message):

return shares

def test_short_secret(self):
secret_message = 'Hello, world!'
secret = Secret.from_printable_ascii(secret_message)
shares = secret.split(3, 5)
assert(Secret.from_shares(shares[0:3]).as_printable_ascii() == secret_message)

def test_long_secret(self):
secret_message = '1054dcaf130fd0c0a51cb0aa762df16faa5f9ee444f3a82f45f62c579635d1b7cba1b5f76587ecba66b'
secret = Secret.from_hex(secret_message)
shares = secret.split(3, 5)
assert(Secret.from_shares(shares[0:3]).as_hex() == secret_message)

def test_main():

test_support.run_unittest(
ShamirSharingTest,
)

if __name__ == '__main__':
test_main()


0 comments on commit b607e96

Please sign in to comment.