Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alternate Character Encodings (base 58, int, nato phonetic), Remove Dependency, General Cleanup #2

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
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