diff --git a/.gitignore b/.gitignore index 2847011..e95a39d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,6 @@ nosetests.xml .project .pydevproject -ignore \ No newline at end of file +# pycharm +.idea/ +.cache/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 3e07db7..51eea65 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,4 @@ language: python python: - "2.7" -script: python unit_tests.py \ No newline at end of file +script: pytest \ No newline at end of file diff --git a/AUTHORS b/AUTHORS index 5406aa1..5a0e113 100644 --- a/AUTHORS +++ b/AUTHORS @@ -7,4 +7,5 @@ Development Leads Patches and Suggestions ```````````````` -- Michael Flaxman (https://github.com/mflaxman) \ No newline at end of file +- Michael Flaxman (https://github.com/mflaxman) +- Michael Loeffler (https://github.com/ml31415) diff --git a/secretsharing/__init__.py b/secretsharing/__init__.py index b1e8570..a7695a5 100644 --- a/secretsharing/__init__.py +++ b/secretsharing/__init__.py @@ -1,13 +1,4 @@ -# -*- coding: utf-8 -*- -""" - Secret Sharing - ~~~~~ - - :copyright: (c) 2014 by Halfmoon Labs - :license: MIT, see LICENSE for more details. -""" - -__version__ = '0.2.7' +__version__ = '0.2.8' from .sharing import secret_int_to_points, points_to_secret_int, \ point_to_share_string, share_string_to_point, SecretSharer, \ diff --git a/secretsharing/charset.py b/secretsharing/charset.py new file mode 100644 index 0000000..1e2db77 --- /dev/null +++ b/secretsharing/charset.py @@ -0,0 +1,62 @@ +from six import integer_types + + +def int_to_charset(x, charset): + """ Turn a non-negative integer into a string. + """ + if not (isinstance(x, integer_types) and x >= 0): + raise ValueError("x must be a non-negative integer.") + if x == 0: + return charset[0] + output = "" + while x > 0: + x, digit = divmod(x, len(charset)) + output += charset[digit] + return output + + +def charset_to_int(s, charset): + """ Turn a string into a non-negative integer. + """ + if not isinstance(s, (str)): + raise ValueError("s must be a string.") + if (set(s) - set(charset)): + raise ValueError("s has chars that aren't in the charset.") + return sum(len(charset)**i * charset.index(char) for i, char in enumerate(s)) + + + +""" Base16 includes numeric digits and the letters a through f. Here, + we use the lowecase letters. +""" +base16_chars = '0123456789abcdef' + +""" The Base58 character set allows for strings that avoid visual ambiguity + when typed. It consists of all the alphanumeric characters except for + "0", "O", "I", and "l", which look similar in some fonts. + https://en.bitcoin.it/wiki/Base58Check_encoding +""" +base58_chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + +""" The Base32 character set allows for accurate transcribing by hand. + It consists of uppercase letters + numerals, excluding "0", "1", + "8", + which could look similar to "O", "I", and "B" and so are omitted. + http://en.wikipedia.org/wiki/Base32 +""" +base32_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" + +""" The z-base-32 character set is similar to the standard Base32 character + set, except it uses lowercase letters + numerals and chooses to exclude + "0", "l", "v", + "2". The set is also permuted so that easier chars + occur more frequently. + http://philzimmermann.com/docs/human-oriented-base-32-encoding.txt +""" +zbase32_chars = "ybndrfg8ejkmcpqxot1uwisza345h769" + +""" The Base64 character set is a popular encoding for transmitting data + over media that are designed for textual data. It includes all alphanumeric + characters plus two bonus characters, usually "+" and "/". + http://en.wikipedia.org/wiki/Base64 +""" +base64_chars = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789+/") diff --git a/secretsharing/polynomials.py b/secretsharing/polynomials.py index 3530602..d36a400 100644 --- a/secretsharing/polynomials.py +++ b/secretsharing/polynomials.py @@ -1,13 +1,14 @@ -# -*- coding: utf-8 -*- -""" - Secret Sharing - ~~~~~ +import random - :copyright: (c) 2014 by Halfmoon Labs - :license: MIT, see LICENSE for more details. -""" -from utilitybelt import secure_randint as randint +def randint(min_value, max_value, system_random=None): + """ Return a random integer N such that a <= N <= b. + Uses SystemRandom for generating random numbers. + (which uses os.urandom(), which pulls from /dev/urandom) + """ + if not system_random: + system_random = random.SystemRandom() + return system_random.randint(min_value, max_value) def egcd(a, b): diff --git a/secretsharing/primes.py b/secretsharing/primes.py deleted file mode 100644 index 417ea8b..0000000 --- a/secretsharing/primes.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Secret Sharing - ~~~~~ - - :copyright: (c) 2014 by Halfmoon Labs - :license: MIT, see LICENSE for more details. -""" - - -def calculate_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 = 1 - for i in range(exp): - prime *= 2 - prime -= 1 - primes.append(prime) - return primes - -SMALLEST_257BIT_PRIME = (2**256 + 297) -SMALLEST_321BIT_PRIME = (2**320 + 27) -SMALLEST_385BIT_PRIME = (2**384 + 231) -STANDARD_PRIMES = calculate_mersenne_primes() + [ - SMALLEST_257BIT_PRIME, SMALLEST_321BIT_PRIME, SMALLEST_385BIT_PRIME -] -STANDARD_PRIMES.sort() - - -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 = STANDARD_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 diff --git a/secretsharing/sharing.py b/secretsharing/sharing.py index cd4e278..65d1b1d 100644 --- a/secretsharing/sharing.py +++ b/secretsharing/sharing.py @@ -1,20 +1,35 @@ -# -*- coding: utf-8 -*- -""" - Secret Sharing - ~~~~~ +import string +from six import integer_types - :copyright: (c) 2014 by Halfmoon Labs - :license: MIT, see LICENSE for more details. -""" +from .charset import (int_to_charset, charset_to_int, base58_chars, + base32_chars, zbase32_chars) +from .polynomials import (random_polynomial, get_polynomial_points, + modular_lagrange_interpolation) -import string -from six import integer_types -from utilitybelt import int_to_charset, charset_to_int, base58_chars, \ - base32_chars, zbase32_chars -from .primes import get_large_enough_prime -from .polynomials import random_polynomial, \ - get_polynomial_points, modular_lagrange_interpolation +def mersenne_primes(): + """ Return mersenne primes: + 3, 7, 31, 127, 8191, 131071, 524287, 2147483647L, 2305843009213693951L, ... + """ + exponents = [ + 2, 3, 5, 7, 13, 17, 19, 31, 61, 89, 107, 127, 521, 607, 1279, 2203, 2281, + 3217, 4253, 4423, 9689, 9941, 11213, 19937, 21701, 23209, 44497, 86243, + 110503, 132049, 216091, 756839, 859433, 1257787, 1398269, 2976221, 3021377, + 6972593, 13466917, 20996011, 24036583, 25964951, 30402457, 32582657, 37156667, + 42643801, 43112609, 57885161, 74207281 + ] + for exp in exponents: + yield 2**exp - 1 + + +def get_large_enough_prime(batch): + """ Returns a prime number that is greater all the numbers in the batch. + """ + # find a prime that is greater than all the numbers in the batch + for prime in mersenne_primes(): + numbers_greater_than_prime = sum(1 for i in batch if i > prime) + if not numbers_greater_than_prime: + return prime def secret_int_to_points(secret_int, point_threshold, num_points, prime=None): @@ -100,8 +115,8 @@ class SecretSharer(): character set of the secrets and the character set of the shares that it expects to be dealing with. """ - secret_charset = string.hexdigits[0:16] - share_charset = string.hexdigits[0:16] + secret_charset = string.hexdigits[:16] + share_charset = string.hexdigits[:16] def __init__(self): pass @@ -128,15 +143,12 @@ def recover_secret(cls, shares): class HexToHexSecretSharer(SecretSharer): """ Standard sharer for converting hex secrets to hex shares. """ - secret_charset = string.hexdigits[0:16] - share_charset = string.hexdigits[0:16] class PlaintextToHexSecretSharer(SecretSharer): """ Good for converting secret messages into standard hex shares. """ secret_charset = string.printable - share_charset = string.hexdigits[0:16] class BitcoinToB58SecretSharer(SecretSharer): @@ -147,17 +159,15 @@ class BitcoinToB58SecretSharer(SecretSharer): share_charset = base58_chars -class BitcoinToB32SecretSharer(SecretSharer): +class BitcoinToB32SecretSharer(BitcoinToB58SecretSharer): """ Good for converting Bitcoin secret keys into shares that can be reliably and conveniently transcribed. """ - secret_charset = base58_chars share_charset = base32_chars -class BitcoinToZB32SecretSharer(SecretSharer): +class BitcoinToZB32SecretSharer(BitcoinToB58SecretSharer): """ Good for converting Bitcoin secret keys into shares that can be reliably and conveniently transcribed. """ - secret_charset = base58_chars share_charset = zbase32_chars diff --git a/secretsharing/tests/__init__.py b/secretsharing/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/secretsharing/tests/test_sharing.py b/secretsharing/tests/test_sharing.py new file mode 100644 index 0000000..aaae24b --- /dev/null +++ b/secretsharing/tests/test_sharing.py @@ -0,0 +1,63 @@ +import random +import pytest + +from ..charset import base32_chars, base58_chars, base64_chars, int_to_charset, charset_to_int +from .. import (secret_int_to_points, points_to_secret_int, + point_to_share_string, share_string_to_point, SecretSharer, + HexToHexSecretSharer, PlaintextToHexSecretSharer, + BitcoinToB58SecretSharer, BitcoinToB32SecretSharer, + BitcoinToZB32SecretSharer) + + +@pytest.fixture(params=[BitcoinToB32SecretSharer, BitcoinToB58SecretSharer, + BitcoinToZB32SecretSharer]) +def btc_sharer_class(request): + return request.param + +splits = [(2, 2), (2, 3), (4, 7), (5, 9)] + +def split_and_recover_secret(sharer_class, m, n, secret): + shares = sharer_class.split_secret(secret, m, n) + random.shuffle(shares) + for i in range(n - m): + recovered_secret = sharer_class.recover_secret(shares[i:m+i]) + assert(recovered_secret == secret) + recovered_secret = sharer_class.recover_secret(shares[i:m+i+1]) + assert(recovered_secret == secret) + unrecovered_secret = sharer_class.recover_secret(shares[i:m+i-1]) + assert(unrecovered_secret != secret) + + +@pytest.mark.parametrize('charset', [base32_chars, base58_chars, base64_chars], + ids=lambda x: 'Base' + str(len(x))) +def test_int_to_charset(charset): + for i in range(30, 100, 4): + secret_int = 2**i + 3*i + 7 + secret_str = int_to_charset(secret_int, charset) + assert secret_int == charset_to_int(secret_str, charset) + + +@pytest.mark.parametrize(['m', 'n'], splits) +def test_hex_to_hex_sharing(m, n): + split_and_recover_secret(SecretSharer, m, n, + "c4bbcb1fbec99d65bf59d85c8cb62ee2db963f0fe106f483d9afa73bd4e39a8a") + + +@pytest.mark.parametrize(['m', 'n'], splits) +def test_printable_ascii_to_hex_sharing(m, n): + split_and_recover_secret(PlaintextToHexSecretSharer, m, n, + "0000correct horse battery staple") + + +@pytest.mark.parametrize(['m', 'n'], splits) +def test_btc_sharing(btc_sharer_class, m, n): + split_and_recover_secret(btc_sharer_class, m, n, + "5KJvsngHeMpm884wtkJNzQGaCErckhHJBGFsvd3VyK5qMZXj3hS") + + +@pytest.mark.parametrize(['m', 'n'], splits) +def test_hex_to_base64_sharing(m, n): + sharer_class = SecretSharer + sharer_class.share_charset = base64_chars + split_and_recover_secret(sharer_class, m, n, + "c4bbcb1fbec99d65bf59d85c8cb62ee2db963f0fe106f483d9afa73bd4e39a8a") diff --git a/setup.py b/setup.py index 317eef3..5d43318 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name='secretsharing', - version='0.2.6', + version='0.2.8', url='https://github.com/onenameio/secret-sharing', license='MIT', author='Halfmoon Labs', @@ -21,7 +21,6 @@ zip_safe=False, install_requires=[ 'six', - 'utilitybelt', ], classifiers=[ 'Intended Audience :: Developers', diff --git a/unit_tests.py b/unit_tests.py deleted file mode 100644 index 79d6566..0000000 --- a/unit_tests.py +++ /dev/null @@ -1,94 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Secret Sharing - ~~~~~ - - :copyright: (c) 2014 by Halfmoon Labs - :license: MIT, see LICENSE for more details. -""" - -import random -import unittest -from test import test_support -from utilitybelt import base64_chars -from secretsharing import secret_int_to_points, points_to_secret_int, \ - point_to_share_string, share_string_to_point, SecretSharer, \ - HexToHexSecretSharer, PlaintextToHexSecretSharer, \ - BitcoinToB58SecretSharer, BitcoinToB32SecretSharer, \ - BitcoinToZB32SecretSharer - - -class ShamirSharingTest(unittest.TestCase): - def setUp(self): - pass - - def tearDown(self): - pass - - def split_and_recover_secret(self, sharer_class, m, n, secret): - shares = sharer_class.split_secret(secret, m, n) - random.shuffle(shares) - recovered_secret = sharer_class.recover_secret(shares[0:m]) - assert(recovered_secret == secret) - - def test_hex_to_hex_sharing(self): - recovered_secret = self.split_and_recover_secret( - SecretSharer, 3, 5, - "c4bbcb1fbec99d65bf59d85c8cb62ee2db963f0fe106f483d9afa73bd4e39a8a") - - def test_printable_ascii_to_hex_sharing(self): - recovered_secret = self.split_and_recover_secret( - PlaintextToHexSecretSharer, 3, 5, - "correct horse battery staple") - - def test_b58_to_b32_sharing(self): - recovered_secret = self.split_and_recover_secret( - BitcoinToB32SecretSharer, 3, 5, - "5KJvsngHeMpm884wtkJNzQGaCErckhHJBGFsvd3VyK5qMZXj3hS") - - def test_b58_to_zb32_sharing(self): - recovered_secret = self.split_and_recover_secret( - BitcoinToZB32SecretSharer, 3, 5, - "5KJvsngHeMpm884wtkJNzQGaCErckhHJBGFsvd3VyK5qMZXj3hS") - - def test_b58_to_b58_sharing(self): - recovered_secret = self.split_and_recover_secret( - BitcoinToB58SecretSharer, 3, 5, - "5KJvsngHeMpm884wtkJNzQGaCErckhHJBGFsvd3VyK5qMZXj3hS") - - def test_hex_to_base64_sharing(self): - sharer_class = SecretSharer - sharer_class.share_charset = base64_chars - recovered_secret = self.split_and_recover_secret( - sharer_class, 3, 5, - "c4bbcb1fbec99d65bf59d85c8cb62ee2db963f0fe106f483d9afa73bd4e39a8a") - - def test_2_of_3_sharing(self): - recovered_secret = self.split_and_recover_secret( - SecretSharer, 2, 3, - "c4bbcb1fbec99d65bf59d85c8cb62ee2db963f0fe106f483d9afa73bd4e39a8a") - - def test_4_of_7_sharing(self): - recovered_secret = self.split_and_recover_secret( - SecretSharer, 4, 7, - "c4bbcb1fbec99d65bf59d85c8cb62ee2db963f0fe106f483d9afa73bd4e39a8a") - - def test_5_of_9_sharing(self): - recovered_secret = self.split_and_recover_secret( - SecretSharer, 5, 9, - "c4bbcb1fbec99d65bf59d85c8cb62ee2db963f0fe106f483d9afa73bd4e39a8a") - - def test_2_of_2_sharing(self): - recovered_secret = self.split_and_recover_secret( - SecretSharer, 2, 2, - "c4bbcb1fbec99d65bf59d85c8cb62ee2db963f0fe106f483d9afa73bd4e39a8a") - - -def test_main(): - test_support.run_unittest( - ShamirSharingTest - ) - - -if __name__ == '__main__': - test_main() \ No newline at end of file