Skip to content
11 changes: 11 additions & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
characters is maintained by Halfmoon Labs and various contributors.

Development Leads
````````````````

- Ryan Shea (https://github.com/rxl) <[email protected]>

Patches and Suggestions
- Michael Flaxman (https://github.com/mflaxman)
````````````````

17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,23 @@ A system for sharing secrets using Shamir's Secret Sharing Scheme.

#### Recovering secrets from shares

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

#### Shares too long? Use Bitcoin inspired base58 encoding instead of hex

>>> secret = Secret.from_printable_ascii("Hello, world!")
>>> b58_shares = secret.split(3, 5, share_enc='b58')
>>> print b58_shares
['2-dqqXbFouiv6aztG', '3-2NuD3PS2me78j8mo', '4-C4WafUspLBCcd8H', '5-2TFUFR7fYkFUiFcn', '6-nYMwed5TwCnZEaE']

#### Recovering secrets from base58 shares

>>> recovered_shares = ['2-dqqXbFouiv6aztG', '3-2NuD3PS2me78j8mo','4-C4WafUspLBCcd8H']
>>> recovered_secret = Secret.from_shares(recovered_shares, share_enc='b58')
>>> print recovered_secret.as_printable_ascii()
'Hello, world!'

You can also use integers or (a modification of) [the NATO phonetic alphabet](http://en.wikipedia.org/wiki/NATO_phonetic_alphabet] with `share_enc='int'` or `share_enc='nato'`.
108 changes: 108 additions & 0 deletions secretsharing/characters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
# Copied/inspired by 2014-03-30 from characters repo:
# https://raw.githubusercontent.com/onenameio/characters/

import string

# Integer


def int_to_charset(val, charset):
""" Turn a non-negative integer into a string.
"""
if not val >= 0:
raise ValueError('"val" must be a non-negative integer.')
if val == 0:
return charset[0]
output = ""
while val > 0:
val, digit = divmod(val, len(charset))
output += charset[digit]
# reverse the characters in the output and return
return output[::-1]


def charset_to_int(s, charset):
""" Turn a string into a non-negative integer.
"""
output = 0
for char in s:
output = output * len(charset) + charset.index(char)
return output


def change_charset(s, original_charset, target_charset):
""" Convert a string from one charset to another.
"""
if not isinstance(s, str):
raise ValueError('"s" must be a string.')

intermediate_integer = charset_to_int(s, original_charset)
output_string = int_to_charset(intermediate_integer, target_charset)
return output_string


# Hexadecimal

def hex_to_int(s):
return charset_to_int(s, string.hexdigits[0:16])


def int_to_hex(val):
return int_to_charset(val, string.hexdigits[0:16])


def is_hex(s):
try:
int(s, 16)
except ValueError:
return False
else:
return True

# Base 58

# https://en.bitcoin.it/wiki/Base58Check_encoding
B58_CHARS = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'


def b58_to_int(s):
return charset_to_int(s, B58_CHARS)


def int_to_b58(val):
return int_to_charset(val, B58_CHARS)


def is_b58(s):
for char in s:
if char not in B58_CHARS:
return False
return True

# Integer

def is_int(s):
# http://stackoverflow.com/a/1267145/1754586
try:
int(s)
return True
except ValueError:
return False

# Nato phonetic alphabet
# http://en.wikipedia.org/wiki/NATO_phonetic_alphabet
# 0 and l removed (kind of like b58)
NATO_CHARS = '123456789abcdefghijkmnopqrstuvwxyz'

def nato_to_int(s):
return charset_to_int(s, NATO_CHARS)

def int_to_nato(val):
return int_to_charset(val, NATO_CHARS)

def is_nato(s):
for char in s:
if char not in NATO_CHARS:
return False
return True
114 changes: 88 additions & 26 deletions secretsharing/shamir.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,89 +8,146 @@
"""

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 characters import (charset_to_int, int_to_charset, is_int,
hex_to_int, int_to_hex, is_hex,
b58_to_int, int_to_b58, is_b58, B58_CHARS,
nato_to_int, int_to_nato, is_nato, NATO_CHARS)

from .utils 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..."
def assert_share_enc_valid(share_enc):
assert share_enc in ('hex', 'b58', 'int', 'nato'), share_enc


def share_to_point(share, share_enc='hex'):
'''
share should be in the format:
`01-d051080de...` for hex
`1-2130008653...` for int
`2-3AwSUjLj59...` for b58
`2-3ciz388ixs...` for nato
'''
assert_share_enc_valid(share_enc)
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))
x, y = share.split('-')
if share_enc == 'hex':
if is_hex(x) and is_hex(y):
return (hex_to_int(x), hex_to_int(y))
elif share_enc == 'b58':
if is_b58(x) and is_b58(y):
return (b58_to_int(x), b58_to_int(y))
elif share_enc == 'int':
if is_int(x) and is_int(y):
return (int(x), int(y))
elif share_enc == 'nato':
if is_nato(x) and is_nato(y):
return (nato_to_int(x), nato_to_int(y))
raise ValueError('Share format is invalid.')

def point_to_share(point):
# point should be in the format (1, 4938573982723...)

def point_to_share(point, share_enc='enc'):
'''
point should be in the format (1, 4938573982723...)
'''
assert_share_enc_valid(share_enc)
if isinstance(point, tuple) and len(point) == 2:
if isinstance(point[0], (int, long)) and isinstance(point[1], (int, long)):
x,y = point
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!"
msg = 'The largest x coordinate for a share is 255.'
raise ValueError(msg)

if share_enc == 'hex':
clean_x = int_to_hex(x).zfill(2)
clean_y = int_to_hex(y)
elif share_enc == 'b58':
clean_x = int_to_b58(x)
clean_y = int_to_b58(y)
elif share_enc == 'int':
clean_x = x
clean_y = y
elif share_enc == 'nato':
clean_x = int_to_nato(x)
clean_y = int_to_nato(y)
else:
raise ValueError('No matching share_enc found')

return '%s-%s' % (clean_x, clean_y)

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.")
msg = "Secret contains characters that aren't in the charset."
raise ValueError(msg)
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_b58(cls, secret):
return cls.from_charset(secret, B58_CHARS)

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

@classmethod
def from_shares(cls, shares):
def from_shares(cls, shares, share_enc='hex'):
assert_share_enc_valid(share_enc)
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))
points.append(share_to_point(share, share_enc=share_enc))
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
"""
def split(self, threshold, num_shares, share_enc='hex'):
'''
Split the secret into shares.
The threshold is the total # of shares required to recover the secret.

Currently, you can return shares in hex, int, or b58 formats.
Feel free to add your own.
'''
assert_share_enc_valid(share_enc)
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!")
msg = "Error! Secret is too long for share calculation!"
raise ValueError(msg)
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))
shares.append(point_to_share(point, share_enc=share_enc))
return shares

def as_int(self):
Expand All @@ -102,6 +159,11 @@ def as_charset(self, charset):
def as_hex(self):
return self.as_charset(string.hexdigits[0:16])

def as_b58(self):
return self.as_charset(B58_CHARS)

def as_nato(self):
return self.as_charset(NATO_CHARS)

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

6 changes: 3 additions & 3 deletions secretsharing/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,15 @@ def get_polynomial_points(coefficients, num_points, prime):
return points

def modular_lagrange_interpolation(x, points, prime):
# break the points up into lists of x and y values
# 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)
# 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
# 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
Expand Down
5 changes: 1 addition & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

setup(
name='secretsharing',
version='0.1.2',
version='0.1.4',
url='https://github.com/halfmoonlabs/secretsharing',
license='MIT',
author='Halfmoon Labs',
Expand All @@ -18,9 +18,6 @@
'secretsharing',
],
zip_safe=False,
install_requires=[
'characters>=0.1',
],
classifiers=[
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
Expand Down
Loading