diff --git a/lib/pylibscrypt/__init__.py b/lib/pylibscrypt/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..764f925ce0681950e07488f1843f06b9ac1ed951 --- /dev/null +++ b/lib/pylibscrypt/__init__.py @@ -0,0 +1,63 @@ +# Copyright (c) 2014, Jan Varho +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Scrypt for Python""" + +__version__ = '1.4.0-git' + +# First, try loading libscrypt +_done = False +try: + from .pylibscrypt import * +except ImportError: + pass +else: + _done = True + +# If that didn't work, try the scrypt module +if not _done: + try: + from .pyscrypt import * + except ImportError: + pass + else: + _done = True + +# Next: libsodium +if not _done: + try: + from .pylibsodium import * + except ImportError: + pass + else: + _done = True + +# Unless we are on pypy, we want to try libsodium_salsa as well +if not _done: + import platform + if platform.python_implementation() != 'PyPy': + try: + from .pylibsodium_salsa import * + except ImportError: + pass + else: + _done = True + +# If that didn't work either, the inlined Python version +if not _done: + from .pypyscrypt_inline import * + +__all__ = ['scrypt', 'scrypt_mcf', 'scrypt_mcf_check'] + + diff --git a/lib/pylibscrypt/__pycache__/__init__.cpython-34.pyc b/lib/pylibscrypt/__pycache__/__init__.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a2a0a1fdebc7e9c5d9c915f308d52d6fb05536ad Binary files /dev/null and b/lib/pylibscrypt/__pycache__/__init__.cpython-34.pyc differ diff --git a/lib/pylibscrypt/__pycache__/common.cpython-34.pyc b/lib/pylibscrypt/__pycache__/common.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b069b1cdba0dd48a228008131151d7649bc75c5a Binary files /dev/null and b/lib/pylibscrypt/__pycache__/common.cpython-34.pyc differ diff --git a/lib/pylibscrypt/__pycache__/mcf.cpython-34.pyc b/lib/pylibscrypt/__pycache__/mcf.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a4bbee9fa25919f142a01f2522c3403ecc345aa Binary files /dev/null and b/lib/pylibscrypt/__pycache__/mcf.cpython-34.pyc differ diff --git a/lib/pylibscrypt/__pycache__/pbkdf2.cpython-34.pyc b/lib/pylibscrypt/__pycache__/pbkdf2.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..262909f5bd77a50500b01079d8110bb6a0662e93 Binary files /dev/null and b/lib/pylibscrypt/__pycache__/pbkdf2.cpython-34.pyc differ diff --git a/lib/pylibscrypt/__pycache__/pylibscrypt.cpython-34.pyc b/lib/pylibscrypt/__pycache__/pylibscrypt.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df0cf565d6fd9d4eb91e4cee1b23f01c29f0582c Binary files /dev/null and b/lib/pylibscrypt/__pycache__/pylibscrypt.cpython-34.pyc differ diff --git a/lib/pylibscrypt/__pycache__/pylibsodium.cpython-34.pyc b/lib/pylibscrypt/__pycache__/pylibsodium.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..826de49d1d2925b78d0a0bd07b44bbaa1a9b383a Binary files /dev/null and b/lib/pylibscrypt/__pycache__/pylibsodium.cpython-34.pyc differ diff --git a/lib/pylibscrypt/__pycache__/pylibsodium_salsa.cpython-34.pyc b/lib/pylibscrypt/__pycache__/pylibsodium_salsa.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fded72646b6f1543f4cc4ebf11fae7d3780657bc Binary files /dev/null and b/lib/pylibscrypt/__pycache__/pylibsodium_salsa.cpython-34.pyc differ diff --git a/lib/pylibscrypt/__pycache__/pypyscrypt_inline.cpython-34.pyc b/lib/pylibscrypt/__pycache__/pypyscrypt_inline.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..97c3dd5027ae493e1042b7e1ed043010eb99b4a3 Binary files /dev/null and b/lib/pylibscrypt/__pycache__/pypyscrypt_inline.cpython-34.pyc differ diff --git a/lib/pylibscrypt/__pycache__/pyscrypt.cpython-34.pyc b/lib/pylibscrypt/__pycache__/pyscrypt.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0bc0e240c509da046749fa23e729c0db48d17019 Binary files /dev/null and b/lib/pylibscrypt/__pycache__/pyscrypt.cpython-34.pyc differ diff --git a/lib/pylibscrypt/__pycache__/tests.cpython-34.pyc b/lib/pylibscrypt/__pycache__/tests.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c7edcc5d7371d01da9b17fc9369e61d12562c5f2 Binary files /dev/null and b/lib/pylibscrypt/__pycache__/tests.cpython-34.pyc differ diff --git a/lib/pylibscrypt/bench.py b/lib/pylibscrypt/bench.py new file mode 100644 index 0000000000000000000000000000000000000000..488dfceec32ba09d5dd1286c35d972f95c597fe9 --- /dev/null +++ b/lib/pylibscrypt/bench.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python + +# Copyright (c) 2014, Jan Varho +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Simple benchmark of python vs c scrypt""" + +import time + +from .common import * +from .pylibscrypt import scrypt +from .pypyscrypt_inline import scrypt as pyscrypt +from .pylibsodium_salsa import scrypt as pcscrypt + + +# Benchmark time in seconds +tmin = 5 +Nmax = 20 + +t1 = time.time() +for i in xrange(1, Nmax+1): + pyscrypt(b'password', b'NaCl', N=2**i) + if time.time() - t1 > tmin: + Nmax = i + break +t1 = time.time() - t1 +print('Using N = 2,4,..., 2**%d' % Nmax) +print('Python scrypt took %.2fs' % t1) + +t2 = time.time() +for i in xrange(1, Nmax+1): + pcscrypt(b'password', b'NaCl', N=2**i) +t2 = time.time() - t2 +print('Py + C scrypt took %.2fs' % t2) + +t3 = time.time() +for i in xrange(1, Nmax+1): + scrypt(b'password', b'NaCl', N=2**i) +t3 = time.time() - t3 +print('C scrypt took %.2fs' % t3) + +print('Python scrypt took %.2f times as long as C' % (t1 / t3)) +print('Py + C scrypt took %.2f times as long as C' % (t2 / t3)) + diff --git a/lib/pylibscrypt/common.py b/lib/pylibscrypt/common.py new file mode 100644 index 0000000000000000000000000000000000000000..241426970f1a97616971b6408e954d42769da01c --- /dev/null +++ b/lib/pylibscrypt/common.py @@ -0,0 +1,61 @@ +# Copyright (c) 2014, Jan Varho +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Common variables and functions used by scrypt implementations""" + +import numbers + + +SCRYPT_MCF_PREFIX_7 = b'$7$' +SCRYPT_MCF_PREFIX_s1 = b'$s1$' +SCRYPT_MCF_PREFIX_DEFAULT = b'$s1$' +SCRYPT_MCF_PREFIX_ANY = None + +SCRYPT_N = 1<<14 +SCRYPT_r = 8 +SCRYPT_p = 1 + +# The last one differs from libscrypt defaults, but matches the 'interactive' +# work factor from the original paper. For long term storage where runtime of +# key derivation is not a problem, you could use 16 as in libscrypt or better +# yet increase N if memory is plentiful. + +xrange = xrange if 'xrange' in globals() else range + +def check_args(password, salt, N, r, p, olen=64): + if not isinstance(password, bytes): + raise TypeError('password must be a byte string') + if not isinstance(salt, bytes): + raise TypeError('salt must be a byte string') + if not isinstance(N, numbers.Integral): + raise TypeError('N must be an integer') + if not isinstance(r, numbers.Integral): + raise TypeError('r must be an integer') + if not isinstance(p, numbers.Integral): + raise TypeError('p must be an integer') + if not isinstance(olen, numbers.Integral): + raise TypeError('length must be an integer') + if N > 2**63: + raise ValueError('N cannot be larger than 2**63') + if (N & (N - 1)) or N < 2: + raise ValueError('N must be a power of two larger than 1') + if r <= 0: + raise ValueError('r must be positive') + if p <= 0: + raise ValueError('p must be positive') + if r * p >= 2**30: + raise ValueError('r * p must be less than 2 ** 30') + if olen <= 0: + raise ValueError('length must be positive') + diff --git a/lib/pylibscrypt/fuzz.py b/lib/pylibscrypt/fuzz.py new file mode 100644 index 0000000000000000000000000000000000000000..91cd4e2b5498317af15e6a3f1dfdf24220178510 --- /dev/null +++ b/lib/pylibscrypt/fuzz.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python + +# Copyright (c) 2014, Jan Varho +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Fuzzes scrypt function input, comparing two implementations""" + +import itertools +import random +from random import randrange as rr +import unittest + + +class Skip(Exception): + pass + + +class Fuzzer(object): + """Fuzzes function input""" + def __init__(self, f, args, g=None, pass_good=None, pass_bad=None): + self.f = f + self.g = g + self.args = args + self.pass_good = pass_good + self.pass_bad = pass_bad + + @staticmethod + def get_random_int(): + return int((1<<rr(66)) * 1.3) + + @staticmethod + def get_random_bytes(lrange=None, skip=None): + if lrange is None: + v = bytearray(rr(2**rr(10))) + else: + v = bytearray(rr(*lrange)) + for i in range(len(v)): + v[i] = rr(256) + while v[i] == skip: + v[i] = rr(256) + return bytes(v) + + def get_good_args(self): + kwargs = {} + for a in self.args: + assert isinstance(a, dict) + if 'opt' in a and a['opt'] and random.randrange(2): + continue + if 'val' in a: + kwargs[a['name']] = a['val'] + elif 'vals' in a: + kwargs[a['name']] = random.choice(a['vals']) + elif 'valf' in a: + kwargs[a['name']] = a['valf']() + elif 'type' in a and a['type'] == 'int': + kwargs[a['name']] = self.get_random_int() + elif 'type' in a and a['type'] == 'bytes': + kwargs[a['name']] = self.get_random_bytes() + else: + raise ValueError + if 'none' in a and not random.randrange(10): + kwargs[a['name']] = None + if 'skip' in a and a['skip'](kwargs[a['name']]): + if 'opt' in a and a['opt']: + del kwargs[a['name']] + else: + raise Skip + return kwargs + + def get_bad_args(self, kwargs=None): + kwargs = kwargs or self.get_good_args() + a = random.choice(self.args) + if not 'opt' in a: + if not random.randrange(10): + del kwargs[a['name']] + return kwargs + if not 'type' in a: + return self.get_bad_args(kwargs) + + if not random.randrange(10): + wrongtype = [ + self.get_random_int(), self.get_random_bytes(), None, + 1.1*self.get_random_int(), 1.0*self.get_random_int() + ] + if a['type'] == 'int': + del wrongtype[0] + elif a['type'] == 'bytes': + del wrongtype[1] + v = random.choice(wrongtype) + try: + if 'valf' in a: + if a['valf'](v): + return self.get_bad_args(kwargs) + if 'skip' in a and a['skip'](v): + return self.get_bad_args(kwargs) + except TypeError: + pass # Surely bad enough + kwargs[a['name']] = v + return kwargs + + if a['type'] == 'int': + v = self.get_random_int() + if 'valf' in a: + if a['valf'](v): + return self.get_bad_args(kwargs) + if 'skip' in a and a['skip'](v): + return self.get_bad_args(kwargs) + kwargs[a['name']] = v + return kwargs + + if a['type'] == 'bytes' and 'valf' in a: + v = self.get_random_bytes() + if not a['valf'](v): + kwargs[a['name']] = v + return kwargs + + return self.get_bad_args(kwargs) + + def fuzz_good_run(self, tc): + try: + kwargs = self.get_good_args() + r1 = self.f(**kwargs) + r2 = self.g(**kwargs) if self.g is not None else None + if self.g is not None: + r2 = self.g(**kwargs) + except Skip: + tc.skipTest('slow') + except Exception as e: + assert False, ('unexpected exception', kwargs, e) + + try: + if self.pass_good: + tc.assertTrue(self.pass_good(r1, r2, kwargs), + msg=('unexpected output', r1, r2, kwargs)) + else: + if self.g is not None: + assert r1 == r2, ('f and g mismatch', kwargs, r1, r2) + tc.assertTrue(r1) + except Exception as e: + print ('unexpected exception', kwargs, r1, r2, e) + raise + + def fuzz_bad(self, f=None, kwargs=None): + f = f or self.f + kwargs = kwargs or self.get_bad_args() + return f(**kwargs) + + def fuzz_bad_run(self, tc): + try: + kwargs = self.get_bad_args() + except Skip: + tc.skipTest('slow') + for f in ((self.f,) if not self.g else (self.f, self.g)): + try: + r = self.fuzz_bad(f, kwargs) + assert False, ('no exception', kwargs, r) + except Skip: + tc.skipTest('slow') + except AssertionError: + raise + except Exception: + pass + + def testcase_good(self, tests=1, name='FuzzTestGood'): + testfs = {} + for i in range(tests): + testfs['test_fuzz_good_%d' % i] = lambda s: self.fuzz_good_run(s) + t = type(name, (unittest.TestCase,), testfs) + return t + + def testcase_bad(self, tests=1, name='FuzzTestBad'): + testfs = {} + for i in range(tests): + testfs['test_fuzz_bad_%d' % i] = lambda s: self.fuzz_bad_run(s) + t = type(name, (unittest.TestCase,), testfs) + return t + + def generate_tests(self, suite, count, name): + loader = unittest.defaultTestLoader + suite.addTest( + loader.loadTestsFromTestCase( + self.testcase_good(count, name + 'Good') + ) + ) + suite.addTest( + loader.loadTestsFromTestCase( + self.testcase_bad(count, name + 'Bad') + ) + ) + + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description='Fuzz testing') + parser.add_argument('-c', '--count', type=int, default=100) + parser.add_argument('-f', '--failfast', action='store_true') + clargs = parser.parse_args() + + modules = [] + try: + from . import pylibscrypt + modules.append((pylibscrypt, 'pylibscrypt')) + except ImportError: + pass + + try: + from . import pyscrypt + modules.append((pyscrypt, 'pyscrypt')) + except ImportError: + pass + + try: + from . import pylibsodium_salsa + modules.append((pylibsodium_salsa, 'pylibsodium_salsa')) + except ImportError: + pass + + try: + from . import pylibsodium + modules.append((pylibsodium, 'pylibsodium')) + except ImportError: + pass + + try: + from . import pypyscrypt_inline as pypyscrypt + modules.append((pypyscrypt, 'pypyscrypt')) + except ImportError: + pass + + scrypt_args = ( + {'name':'password', 'type':'bytes'}, + {'name':'salt', 'type':'bytes'}, + { + 'name':'N', 'type':'int', 'opt':False, + 'valf':(lambda N=None: 2**rr(1,6) if N is None else + 1 < N < 2**64 and not (N & (N - 1))), + 'skip':(lambda N: (N & (N - 1)) == 0 and N > 32 and N < 2**64) + }, + { + 'name':'r', 'type':'int', 'opt':True, + 'valf':(lambda r=None: rr(1, 16) if r is None else 0<r<2**30), + 'skip':(lambda r: r > 16 and r < 2**30) + }, + { + 'name':'p', 'type':'int', 'opt':True, + 'valf':(lambda p=None: rr(1, 16) if p is None else 0<p<2**30), + 'skip':(lambda p: p > 16 and p < 2**30) + }, + { + 'name':'olen', 'type':'int', 'opt':True, + 'valf':(lambda l=None: rr(1, 1000) if l is None else l >= 0), + 'skip':(lambda l: l < 0 or l > 1024) + }, + ) + + scrypt_mcf_args = ( + { + 'name':'password', 'type':'bytes', + 'valf':(lambda p=None: Fuzzer.get_random_bytes(skip=0) if p is None + else not b'\0' in p) + }, + { + 'name':'salt', 'type':'bytes', 'opt':False, 'none':True, + 'valf':(lambda s=None: Fuzzer.get_random_bytes((1,17)) if s is None + else 1 <= len(s) <= 16) + }, + { + 'name':'N', 'type':'int', 'opt':False, + 'valf':(lambda N=None: 2**rr(1,6) if N is None else + 1 < N < 2**64 and not (N & (N - 1))), + 'skip':(lambda N: (N & (N - 1)) == 0 and N > 32 and N < 2**64) + }, + { + 'name':'r', 'type':'int', 'opt':True, + 'valf':(lambda r=None: rr(1, 16) if r is None else 0<r<2**30), + 'skip':(lambda r: r > 16 and r < 2**30) + }, + { + 'name':'p', 'type':'int', 'opt':True, + 'valf':(lambda p=None: rr(1, 16) if p is None else 0<p<2**30), + 'skip':(lambda p: p > 16 and p < 2**30) + }, + { + 'name':'prefix', 'type':'bytes', 'opt':True, + 'vals':(b'$s1$', b'$7$'), + 'skip':(lambda p: p is None) + } + ) + + random.shuffle(modules) + suite = unittest.TestSuite() + loader = unittest.defaultTestLoader + for m1, m2 in itertools.permutations(modules, 2): + Fuzzer( + m1[0].scrypt, scrypt_args, m2[0].scrypt, + pass_good=lambda r1, r2, a: ( + isinstance(r1, bytes) and + (r2 is None or r1 == r2) and + (len(r1) == 64 if 'olen' not in a else len(r1) == a['olen']) + ) + ).generate_tests(suite, clargs.count, m1[1]) + Fuzzer( + m1[0].scrypt_mcf, scrypt_mcf_args, m2[0].scrypt_mcf, + pass_good=lambda r1, r2, a: ( + m2[0].scrypt_mcf_check(r1, a['password']) and + (r2 is None or m1[0].scrypt_mcf_check(r2, a['password'])) and + (r2 is None or 'salt' not in a or a['salt'] is None or r1 == r2) + ) + ).generate_tests(suite, clargs.count, m1[1]) + unittest.TextTestRunner(failfast=clargs.failfast).run(suite) + diff --git a/lib/pylibscrypt/inline.py b/lib/pylibscrypt/inline.py new file mode 100644 index 0000000000000000000000000000000000000000..54e8f5c06fb67a8852168968ed937d8875c36438 --- /dev/null +++ b/lib/pylibscrypt/inline.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +# Copyright (c) 2014, Jan Varho +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Inlines the salsa20 core lines into salsa20_8""" + +of = open('pylibscrypt/pypyscrypt_inline.py', 'w') +assert of + +def indent(line): + i = 0 + while i < len(line) and line[i] == ' ': + i += 1 + return i + +with open('pylibscrypt/pypyscrypt.py', 'r') as f: + in_loop = False + loop_indent = 0 + lc = 0 + rl = [] + skipping = False + for line in f: + lc += 1 + i = indent(line) + if line[i:].startswith('def R('): + skipping = True + elif line[i:].startswith('def array_overwrite('): + skipping = True + elif skipping: + if line[i:].startswith('def'): + of.write(line) + skipping = False + + elif line[i:].startswith('R('): + parts = line.split(';') + rl += parts + if len(rl) == 32: + # Interleave to reduce dependencies for pypy + rl1 = rl[:16] + rl2 = rl[16:] + rl1 = rl1[0::4] + rl1[1::4] + rl1[2::4] + rl1[3::4] + rl2 = rl2[0::4] + rl2[1::4] + rl2[2::4] + rl2[3::4] + rl = rl1 + rl2 + for p, q in zip(rl[::2], rl[1::2]): + pvals = p.split(',')[1:] + pvals = [int(v.strip(' )\n')) for v in pvals] + qvals = q.split(',')[1:] + qvals = [int(v.strip(' )\n')) for v in qvals] + of.write(' '*i) + of.write('a = (x[%d]+x[%d]) & 0xffffffff\n' % + (pvals[1], pvals[2])) + of.write(' '*i) + of.write('b = (x[%d]+x[%d]) & 0xffffffff\n' % + (qvals[1], qvals[2])) + of.write(' '*i) + of.write('x[%d] ^= (a << %d) | (a >> %d)\n' % + (pvals[0], pvals[3], 32 - pvals[3])) + of.write(' '*i) + of.write('x[%d] ^= (b << %d) | (b >> %d)\n' % + (qvals[0], qvals[3], 32 - qvals[3])) + + elif line[i:].startswith('array_overwrite('): + vals = line.split(',') + vals[0] = vals[0].split('(')[1] + vals[-1] = vals[-1].split(')')[0] + vals = [v.strip() for v in vals] + assert len(vals) == 5 + of.write(' '*i) + of.write(vals[2] + '[' + vals[3] + ':(' + vals[3] + ')+(' + + vals[4] + ')] = ' + vals[0] + '[' + vals[1] + ':(' + + vals[1] + ')+(' + vals[4] + ')]\n') + + else: + of.write(line) + if lc == 1: + of.write('\n# Automatically generated file, see inline.py\n') + diff --git a/lib/pylibscrypt/mcf.py b/lib/pylibscrypt/mcf.py new file mode 100644 index 0000000000000000000000000000000000000000..493ac942d5e4358e72cb803ea31f5cd1c82f6a7a --- /dev/null +++ b/lib/pylibscrypt/mcf.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python + +# Copyright (c) 2014, Jan Varho +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Modular Crypt Format support for scrypt + +Compatible with libscrypt scrypt_mcf_check also supports the $7$ format. + +libscrypt format: +$s1$NNrrpp$salt$hash +NN - hex encoded N log2 (two hex digits) +rr - hex encoded r in 1-255 +pp - hex encoded p in 1-255 +salt - base64 encoded salt 1-16 bytes decoded +hash - base64 encoded 64-byte scrypt hash + +$7$ format: +$7$Nrrrrrpppppsalt$hash +N - crypt base64 N log2 +rrrrr - crypt base64 r (little-endian 30 bits) +ppppp - crypt base64 p (little-endian 30 bits) +salt - raw salt (0-43 bytes that should be limited to crypt base64) +hash - crypt base64 encoded 32-byte scrypt hash (43 bytes) + +(crypt base64 is base64 with the alphabet: ./0-9A-Za-z) + +When reading, we are more lax, allowing salts and hashes to be longer and +incorrectly encoded, since the worst that can happen is that the password does +not verify. +""" + + +import base64, binascii +import os +import struct + +from .common import * + + +def _scrypt_mcf_encode_s1(N, r, p, salt, hash): + h64 = base64.b64encode(hash) + s64 = base64.b64encode(salt) + + t = 1 + while 2**t < N: + t += 1 + params = p + (r << 8) + (t << 16) + + return ( + b'$s1' + + ('$%06x' % params).encode() + + b'$' + s64 + + b'$' + h64 + ) + + +def _b64decode(b64): + for b in (b64, b64 + b'=', b64 + b'=='): + try: + return base64.b64decode(b) + except (TypeError, binascii.Error): + pass + raise ValueError('Incorrect base64 in MCF') + + +def _scrypt_mcf_decode_s1(mcf): + s = mcf.split(b'$') + if not (mcf.startswith(b'$s1$') and len(s) == 5): + return None + + params, s64, h64 = s[2:] + params = base64.b16decode(params, True) + salt = _b64decode(s64) + hash = _b64decode(h64) + + if len(params) != 3: + raise ValueError('Unrecognized MCF parameters') + t, r, p = struct.unpack('3B', params) + N = 2 ** t + return N, r, p, salt, hash, len(hash) + + +# Crypt base 64 +_cb64 = b'./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' +_cb64a = bytearray(_cb64) +_icb64 = ( + [None] * 46 + + [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, None, None, None, None, None, + None, None, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, None, None, None, + None, None, None, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63 + ] + + [None] * 133 +) + + +def _cb64enc(arr): + arr = bytearray(arr) + out = bytearray() + val = bits = pos = 0 + for b in arr: + val += b << bits + bits += 8 + while bits >= 0: + out.append(_cb64a[val & 0x3f]) + bits -= 6 + val = val >> 6 + return bytes(out) + + +def _scrypt_mcf_encode_7(N, r, p, salt, hash): + t = 1 + while 2**t < N: + t += 1 + return ( + b'$7$' + + # N + _cb64[t::64] + + # r + _cb64[r & 0x3f::64] + _cb64[(r >> 6) & 0x3f::64] + + _cb64[(r >> 12) & 0x3f::64] + _cb64[(r >> 18) & 0x3f::64] + + _cb64[(r >> 24) & 0x3f::64] + + # p + _cb64[p & 0x3f::64] + _cb64[(p >> 6) & 0x3f::64] + + _cb64[(p >> 12) & 0x3f::64] + _cb64[(p >> 18) & 0x3f::64] + + _cb64[(p >> 24) & 0x3f::64] + + # rest + salt + + b'$' + _cb64enc(hash) + ) + + +def _cb64dec(arr): + out = bytearray() + val = bits = pos = 0 + for b in arr: + val += _icb64[b] << bits + bits += 6 + if bits >= 8: + out.append(val & 0xff) + bits -= 8 + val >>= 8 + return out + + +def _scrypt_mcf_decode_7(mcf): + s = mcf.split(b'$') + if not (mcf.startswith(b'$7$') and len(s) == 4): + return None + + s64 = bytearray(s[2]) + h64 = bytearray(s[3]) + try: + N = 2 ** _icb64[s64[0]] + r = (_icb64[s64[1]] + (_icb64[s64[2]] << 6) + (_icb64[s64[3]] << 12) + + (_icb64[s64[4]] << 18) + (_icb64[s64[5]] << 24)) + p = (_icb64[s64[6]] + (_icb64[s64[7]] << 6) + (_icb64[s64[8]] << 12) + + (_icb64[s64[9]] << 18) + (_icb64[s64[10]] << 24)) + salt = bytes(s64[11:]) + hash = bytes(_cb64dec(h64)) + except (IndexError, TypeError): + raise ValueError('Unrecognized MCF format') + + return N, r, p, salt, hash, len(hash) + + +def _scrypt_mcf_7_is_standard(mcf): + params = _scrypt_mcf_decode_7(mcf) + if params is None: + return False + N, r, p, salt, hash, hlen = params + return len(salt) == 43 and hlen == 32 + + +def _scrypt_mcf_decode(mcf): + params = _scrypt_mcf_decode_s1(mcf) + if params is None: + params = _scrypt_mcf_decode_7(mcf) + if params is None: + raise ValueError('Unrecognized MCF hash') + return params + + +def scrypt_mcf(scrypt, password, salt=None, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, + prefix=SCRYPT_MCF_PREFIX_DEFAULT): + """Derives a Modular Crypt Format hash using the scrypt KDF given + + Expects the signature: + scrypt(password, salt, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, olen=64) + + If no salt is given, a random salt of 128+ bits is used. (Recommended.) + """ + if salt is not None and not (1 <= len(salt) <= 16): + raise ValueError('salt must be 1-16 bytes') + if r > 255: + raise ValueError('scrypt_mcf r out of range [1,255]') + if p > 255: + raise ValueError('scrypt_mcf p out of range [1,255]') + if N > 2**31: + raise ValueError('scrypt_mcf N out of range [2,2**31]') + if b'\0' in password: + raise ValueError('scrypt_mcf password must not contain zero bytes') + + if prefix == SCRYPT_MCF_PREFIX_s1: + if salt is None: + salt = os.urandom(16) + hash = scrypt(password, salt, N, r, p) + return _scrypt_mcf_encode_s1(N, r, p, salt, hash) + elif prefix == SCRYPT_MCF_PREFIX_7 or prefix == SCRYPT_MCF_PREFIX_ANY: + if salt is None: + salt = os.urandom(32) + salt = _cb64enc(salt) + hash = scrypt(password, salt, N, r, p, 32) + return _scrypt_mcf_encode_7(N, r, p, salt, hash) + else: + raise ValueError("Unrecognized MCF format") + + +def scrypt_mcf_check(scrypt, mcf, password): + """Returns True if the password matches the given MCF hash + + Supports both the libscrypt $s1$ format and the $7$ format. + """ + if not isinstance(mcf, bytes): + raise TypeError + if not isinstance(password, bytes): + raise TypeError + + N, r, p, salt, hash, hlen = _scrypt_mcf_decode(mcf) + h = scrypt(password, salt, N=N, r=r, p=p, olen=hlen) + return hash == h + diff --git a/lib/pylibscrypt/pbkdf2.py b/lib/pylibscrypt/pbkdf2.py new file mode 100644 index 0000000000000000000000000000000000000000..bedd5d628e39e0c665c379bff2bf2b4b9c356065 --- /dev/null +++ b/lib/pylibscrypt/pbkdf2.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python + +# Copyright (c) 2014, Jan Varho +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""PBKDF2 in pure Python, compatible with Python3.4 hashlib.pbkdf2_hmac""" + + +import hashlib +import hmac +import struct + +from .common import * + + +def pbkdf2_hmac(name, password, salt, rounds, dklen=None): + """Returns the result of the Password-Based Key Derivation Function 2""" + h = hmac.new(key=password, digestmod=lambda d=b'': hashlib.new(name, d)) + hs = h.copy() + hs.update(salt) + + blocks = bytearray() + dklen = hs.digest_size if dklen is None else dklen + block_count, last_size = divmod(dklen, hs.digest_size) + block_count += last_size > 0 + + for block_number in xrange(1, block_count + 1): + hb = hs.copy() + hb.update(struct.pack('>L', block_number)) + U = bytearray(hb.digest()) + + if rounds > 1: + Ui = U + for i in xrange(rounds - 1): + hi = h.copy() + hi.update(Ui) + Ui = bytearray(hi.digest()) + for j in xrange(hs.digest_size): + U[j] ^= Ui[j] + + blocks.extend(U) + + if last_size: + del blocks[dklen:] + return bytes(blocks) + +if __name__ == "__main__": + import sys + from . import tests + tests.run_pbkdf2_suite(sys.modules[__name__]) + diff --git a/lib/pylibscrypt/pylibscrypt.py b/lib/pylibscrypt/pylibscrypt.py new file mode 100644 index 0000000000000000000000000000000000000000..3bbc3fb1b49290fc7e4ef312955cbf4ffa63cfc1 --- /dev/null +++ b/lib/pylibscrypt/pylibscrypt.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python + +# Copyright (c) 2014, Jan Varho +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Scrypt implementation that calls into system libscrypt""" + + +import base64 +import ctypes, ctypes.util +from ctypes import c_char_p, c_size_t, c_uint64, c_uint32 +import os + +from .common import * +from . import mcf as mcf_mod + + +_libscrypt_soname = ctypes.util.find_library('scrypt') +if _libscrypt_soname is None: + raise ImportError('Unable to find libscrypt') + +try: + _libscrypt = ctypes.CDLL(_libscrypt_soname) + _libscrypt_scrypt = _libscrypt.libscrypt_scrypt + _libscrypt_mcf = _libscrypt.libscrypt_mcf + _libscrypt_check = _libscrypt.libscrypt_check +except OSError: + raise ImportError('Unable to load libscrypt: ' + _libscrypt_soname) +except AttributeError: + raise ImportError('Incompatible libscrypt: ' + _libscrypt_soname) + +_libscrypt_scrypt.argtypes = [ + c_char_p, # password + c_size_t, # password length + c_char_p, # salt + c_size_t, # salt length + c_uint64, # N + c_uint32, # r + c_uint32, # p + c_char_p, # out + c_size_t, # out length +] + +_libscrypt_mcf.argtypes = [ + c_uint64, # N + c_uint32, # r + c_uint32, # p + c_char_p, # salt + c_char_p, # hash + c_char_p, # out (125+ bytes) +] + +_libscrypt_check.argtypes = [ + c_char_p, # mcf (modified) + c_char_p, # hash +] + + +def scrypt(password, salt, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, olen=64): + """Returns a key derived using the scrypt key-derivarion function + + N must be a power of two larger than 1 but no larger than 2 ** 63 (insane) + r and p must be positive numbers such that r * p < 2 ** 30 + + The default values are: + N -- 2**14 (~16k) + r -- 8 + p -- 1 + + Memory usage is proportional to N*r. Defaults require about 16 MiB. + Time taken is proportional to N*p. Defaults take <100ms of a recent x86. + + The last one differs from libscrypt defaults, but matches the 'interactive' + work factor from the original paper. For long term storage where runtime of + key derivation is not a problem, you could use 16 as in libscrypt or better + yet increase N if memory is plentiful. + """ + check_args(password, salt, N, r, p, olen) + + out = ctypes.create_string_buffer(olen) + ret = _libscrypt_scrypt(password, len(password), salt, len(salt), + N, r, p, out, len(out)) + if ret: + raise ValueError + + return out.raw + + +def scrypt_mcf(password, salt=None, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, + prefix=SCRYPT_MCF_PREFIX_DEFAULT): + """Derives a Modular Crypt Format hash using the scrypt KDF + + Parameter space is smaller than for scrypt(): + N must be a power of two larger than 1 but no larger than 2 ** 31 + r and p must be positive numbers between 1 and 255 + Salt must be a byte string 1-16 bytes long. + + If no salt is given, a random salt of 128+ bits is used. (Recommended.) + """ + if (prefix != SCRYPT_MCF_PREFIX_s1 and prefix != SCRYPT_MCF_PREFIX_ANY): + return mcf_mod.scrypt_mcf(scrypt, password, salt, N, r, p, prefix) + if salt is None: + salt = os.urandom(16) + elif not (1 <= len(salt) <= 16): + raise ValueError('salt must be 1-16 bytes') + if N > 2**31: + raise ValueError('N > 2**31 not supported') + if b'\0' in password: + raise ValueError('scrypt_mcf password must not contain zero bytes') + + hash = scrypt(password, salt, N, r, p) + + h64 = base64.b64encode(hash) + s64 = base64.b64encode(salt) + + out = ctypes.create_string_buffer(125) + ret = _libscrypt_mcf(N, r, p, s64, h64, out) + if not ret: + raise ValueError + + out = out.raw.strip(b'\0') + # XXX: Hack to support old libscrypt (like in Ubuntu 14.04) + if len(out) == 123: + out = out + b'=' + + return out + + +def scrypt_mcf_check(mcf, password): + """Returns True if the password matches the given MCF hash""" + if not isinstance(mcf, bytes): + raise TypeError + if not isinstance(password, bytes): + raise TypeError + if len(mcf) != 124 or b'\0' in password: + return mcf_mod.scrypt_mcf_check(scrypt, mcf, password) + + mcfbuf = ctypes.create_string_buffer(mcf) + ret = _libscrypt_check(mcfbuf, password) + if ret < 0: + return mcf_mod.scrypt_mcf_check(scrypt, mcf, password) + + return bool(ret) + + +if __name__ == "__main__": + import sys + from . import tests + tests.run_scrypt_suite(sys.modules[__name__]) + diff --git a/lib/pylibscrypt/pylibsodium.py b/lib/pylibscrypt/pylibsodium.py new file mode 100644 index 0000000000000000000000000000000000000000..bf6949e81d6dbb837a8b827f0c7f781496132a89 --- /dev/null +++ b/lib/pylibscrypt/pylibsodium.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python + +# Copyright (c) 2014, Jan Varho +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Scrypt implementation that calls into system libsodium""" + + +import base64 +import ctypes, ctypes.util +from ctypes import c_char_p, c_size_t, c_uint64, c_uint32, c_void_p +import hashlib, hmac +import numbers +import platform +import struct +import sys + +from . import mcf as mcf_mod +from .common import * + +if platform.python_implementation() == 'PyPy': + from . import pypyscrypt_inline as scr_mod +else: + from . import pylibsodium_salsa as scr_mod + + +def _get_libsodium(): + ''' + Locate the nacl c libs to use + ''' + + __SONAMES = (13, 10, 5, 4) + # Import libsodium from system + sys_sodium = ctypes.util.find_library('sodium') + if sys_sodium is None: + sys_sodium = ctypes.util.find_library('libsodium') + + if sys_sodium: + return ctypes.CDLL(sys_sodium) + + # Import from local path + if sys.platform.startswith('win'): + + try: + return ctypes.cdll.LoadLibrary('libsodium') + except OSError: + pass + for soname_ver in __SONAMES: + try: + return ctypes.cdll.LoadLibrary( + 'libsodium-{0}'.format(soname_ver) + ) + except OSError: + pass + elif sys.platform.startswith('darwin'): + try: + return ctypes.cdll.LoadLibrary('libsodium.dylib') + except OSError: + pass + else: + try: + return ctypes.cdll.LoadLibrary('libsodium.so') + except OSError: + pass + + for soname_ver in __SONAMES: + try: + return ctypes.cdll.LoadLibrary( + 'libsodium.so.{0}'.format(soname_ver) + ) + except OSError: + pass + + +_lib = _get_libsodium() +if _lib is None: + raise ImportError('Unable to load libsodium') + +try: + _scrypt_ll = _lib.crypto_pwhash_scryptsalsa208sha256_ll + _scrypt_ll.argtypes = [ + c_void_p, # passwd + c_size_t, # passwdlen + c_void_p, # salt + c_size_t, # saltlen + c_uint64, # N + c_uint32, # r + c_uint32, # p + c_void_p, # buf + c_size_t, # buflen + ] +except AttributeError: + _scrypt_ll = None + +try: + _scrypt = _lib.crypto_pwhash_scryptsalsa208sha256 + _scrypt_str = _lib.crypto_pwhash_scryptsalsa208sha256_str + _scrypt_str_chk = _lib.crypto_pwhash_scryptsalsa208sha256_str_verify + _scrypt_str_bytes = _lib.crypto_pwhash_scryptsalsa208sha256_strbytes() + _scrypt_salt = _lib.crypto_pwhash_scryptsalsa208sha256_saltbytes() + if _scrypt_str_bytes != 102 and not _scrypt_ll: + raise ImportError('Incompatible libsodium: ' + _lib_soname) +except AttributeError: + try: + _scrypt = _lib.crypto_pwhash_scryptxsalsa208sha256 + _scrypt_str = _lib.crypto_pwhash_scryptxsalsa208sha256_str + _scrypt_str_chk = _lib.crypto_pwhash_scryptxsalsa208sha256_str_verify + _scrypt_str_bytes = _lib.crypto_pwhash_scryptxsalsa208sha256_strbytes() + _scrypt_salt = _lib.crypto_pwhash_scryptxsalsa208sha256_saltbytes + _scrypt_salt = _scrypt_salt() + if _scrypt_str_bytes != 102 and not _scrypt_ll: + raise ImportError('Incompatible libsodium: ' + _lib_soname) + except AttributeError: + if not _scrypt_ll: + raise ImportError('Incompatible libsodium: ' + _lib_soname) + +_scrypt.argtypes = [ + c_void_p, # out + c_uint64, # outlen + c_void_p, # passwd + c_uint64, # passwdlen + c_void_p, # salt + c_uint64, # opslimit + c_size_t, # memlimit +] + +_scrypt_str.argtypes = [ + c_void_p, # out (102 bytes) + c_void_p, # passwd + c_uint64, # passwdlen + c_uint64, # opslimit + c_size_t, # memlimit +] + +_scrypt_str_chk.argtypes = [ + c_char_p, # str (102 bytes) + c_void_p, # passwd + c_uint64, # passwdlen +] + + +def scrypt(password, salt, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, olen=64): + """Returns a key derived using the scrypt key-derivarion function + + N must be a power of two larger than 1 but no larger than 2 ** 63 (insane) + r and p must be positive numbers such that r * p < 2 ** 30 + + The default values are: + N -- 2**14 (~16k) + r -- 8 + p -- 1 + + Memory usage is proportional to N*r. Defaults require about 16 MiB. + Time taken is proportional to N*p. Defaults take <100ms of a recent x86. + + The last one differs from libscrypt defaults, but matches the 'interactive' + work factor from the original paper. For long term storage where runtime of + key derivation is not a problem, you could use 16 as in libscrypt or better + yet increase N if memory is plentiful. + """ + check_args(password, salt, N, r, p, olen) + + if _scrypt_ll: + out = ctypes.create_string_buffer(olen) + if _scrypt_ll(password, len(password), salt, len(salt), + N, r, p, out, olen): + raise ValueError + return out.raw + + if len(salt) != _scrypt_salt or r != 8 or (p & (p - 1)) or (N*p <= 512): + return scr_mod.scrypt(password, salt, N, r, p, olen) + + s = next(i for i in range(1, 64) if 2**i == N) + t = next(i for i in range(0, 30) if 2**i == p) + m = 2**(10 + s) + o = 2**(5 + t + s) + if s > 53 or t + s > 58: + raise ValueError + out = ctypes.create_string_buffer(olen) + if _scrypt(out, olen, password, len(password), salt, o, m) != 0: + raise ValueError + return out.raw + + +def scrypt_mcf(password, salt=None, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, + prefix=SCRYPT_MCF_PREFIX_DEFAULT): + """Derives a Modular Crypt Format hash using the scrypt KDF + + Parameter space is smaller than for scrypt(): + N must be a power of two larger than 1 but no larger than 2 ** 31 + r and p must be positive numbers between 1 and 255 + Salt must be a byte string 1-16 bytes long. + + If no salt is given, a random salt of 128+ bits is used. (Recommended.) + """ + if N < 2 or (N & (N - 1)): + raise ValueError('scrypt N must be a power of 2 greater than 1') + if p > 255 or p < 1: + raise ValueError('scrypt_mcf p out of range [1,255]') + if N > 2**31: + raise ValueError('scrypt_mcf N out of range [2,2**31]') + + if (salt is not None or r != 8 or (p & (p - 1)) or (N*p <= 512) or + prefix not in (SCRYPT_MCF_PREFIX_7, SCRYPT_MCF_PREFIX_s1, + SCRYPT_MCF_PREFIX_ANY) or + _scrypt_ll): + return mcf_mod.scrypt_mcf(scrypt, password, salt, N, r, p, prefix) + + s = next(i for i in range(1, 32) if 2**i == N) + t = next(i for i in range(0, 8) if 2**i == p) + m = 2**(10 + s) + o = 2**(5 + t + s) + mcf = ctypes.create_string_buffer(102) + if _scrypt_str(mcf, password, len(password), o, m) != 0: + return mcf_mod.scrypt_mcf(scrypt, password, salt, N, r, p, prefix) + + if prefix in (SCRYPT_MCF_PREFIX_7, SCRYPT_MCF_PREFIX_ANY): + return mcf.raw.strip(b'\0') + + _N, _r, _p, salt, hash, olen = mcf_mod._scrypt_mcf_decode_7(mcf.raw[:-1]) + assert _N == N and _r == r and _p == p, (_N, _r, _p, N, r, p, o, m) + return mcf_mod._scrypt_mcf_encode_s1(N, r, p, salt, hash) + + +def scrypt_mcf_check(mcf, password): + """Returns True if the password matches the given MCF hash""" + if mcf_mod._scrypt_mcf_7_is_standard(mcf) and not _scrypt_ll: + return _scrypt_str_chk(mcf, password, len(password)) == 0 + return mcf_mod.scrypt_mcf_check(scrypt, mcf, password) + + +if __name__ == "__main__": + import sys + from . import tests + try: + from . import pylibscrypt + scr_mod = pylibscrypt + except ImportError: + pass + tests.run_scrypt_suite(sys.modules[__name__]) + diff --git a/lib/pylibscrypt/pylibsodium_salsa.py b/lib/pylibscrypt/pylibsodium_salsa.py new file mode 100644 index 0000000000000000000000000000000000000000..34057e632625f66c8301c098eaa37926a662da56 --- /dev/null +++ b/lib/pylibscrypt/pylibsodium_salsa.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python + +# Copyright (c) 2014 Richard Moore +# Copyright (c) 2014 Jan Varho +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Scrypt implementation that calls into system libsodium""" + + +import base64 +import ctypes, ctypes.util +from ctypes import c_char_p, c_size_t, c_uint64, c_uint32, c_void_p +import hashlib, hmac +import numbers +import struct +import sys + +from . import mcf as mcf_mod +from .common import * + + +def _get_libsodium(): + ''' + Locate the nacl c libs to use + ''' + + __SONAMES = (13, 10, 5, 4) + # Import libsodium from system + sys_sodium = ctypes.util.find_library('sodium') + if sys_sodium is None: + sys_sodium = ctypes.util.find_library('libsodium') + + if sys_sodium: + return ctypes.CDLL(sys_sodium) + + # Import from local path + if sys.platform.startswith('win'): + try: + return ctypes.cdll.LoadLibrary('libsodium') + except OSError: + pass + for soname_ver in __SONAMES: + try: + return ctypes.cdll.LoadLibrary( + 'libsodium-{0}'.format(soname_ver) + ) + except OSError: + pass + elif sys.platform.startswith('darwin'): + try: + return ctypes.cdll.LoadLibrary('libsodium.dylib') + except OSError: + pass + else: + try: + return ctypes.cdll.LoadLibrary('libsodium.so') + except OSError: + pass + + for soname_ver in __SONAMES: + try: + return ctypes.cdll.LoadLibrary( + 'libsodium.so.{0}'.format(soname_ver) + ) + except OSError: + pass + + +_libsodium = _get_libsodium() +if _libsodium is None: + raise ImportError('Unable to load libsodium') + +try: + _libsodium_salsa20_8 = _libsodium.crypto_core_salsa208 +except AttributeError: + raise ImportError('Incompatible libsodium: ') + +_libsodium_salsa20_8.argtypes = [ + c_void_p, # out (16*4 bytes) + c_void_p, # in (4*4 bytes) + c_void_p, # k (8*4 bytes) + c_void_p, # c (4*4 bytes) +] + + +# Python 3.4+ have PBKDF2 in hashlib, so use it... +if 'pbkdf2_hmac' in dir(hashlib): + _pbkdf2 = hashlib.pbkdf2_hmac +else: + # but fall back to Python implementation in < 3.4 + from pbkdf2 import pbkdf2_hmac as _pbkdf2 + + +def scrypt(password, salt, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, olen=64): + """Returns a key derived using the scrypt key-derivarion function + + N must be a power of two larger than 1 but no larger than 2 ** 63 (insane) + r and p must be positive numbers such that r * p < 2 ** 30 + + The default values are: + N -- 2**14 (~16k) + r -- 8 + p -- 1 + + Memory usage is proportional to N*r. Defaults require about 16 MiB. + Time taken is proportional to N*p. Defaults take <100ms of a recent x86. + + The last one differs from libscrypt defaults, but matches the 'interactive' + work factor from the original paper. For long term storage where runtime of + key derivation is not a problem, you could use 16 as in libscrypt or better + yet increase N if memory is plentiful. + """ + def array_overwrite(source, s_start, dest, d_start, length): + dest[d_start:d_start + length] = source[s_start:s_start + length] + + + def blockxor(source, s_start, dest, d_start, length): + for i in xrange(length): + dest[d_start + i] ^= source[s_start + i] + + + def integerify(B, r): + """A bijection from ({0, 1} ** k) to {0, ..., (2 ** k) - 1""" + + Bi = (2 * r - 1) * 8 + return B[Bi] & 0xffffffff + + + def salsa20_8(B, x): + """Salsa 20/8 using libsodium + + NaCL/libsodium includes crypto_core_salsa208, but unfortunately it + expects the data in a different order, so we need to mix it up a bit. + """ + hi = 0xffffffff00000000 + lo = 0x00000000ffffffff + struct.pack_into('<9Q', x, 0, + (B[0] & lo) + (B[2] & hi), (B[5] & lo) + (B[7] & hi), # c + B[3], B[4], # in + B[0], B[1], (B[2] & lo) + (B[5] & hi), # pad k pad + B[6], B[7], + ) + + c = ctypes.addressof(x) + i = c + 4*4 + k = c + 9*4 + + _libsodium_salsa20_8(c, i, k, c) + + B[:] = struct.unpack('<8Q8x', x) + + + def blockmix_salsa8(BY, Yi, r): + """Blockmix; Used by SMix""" + + start = (2 * r - 1) * 8 + X = BY[start:start+8] # BlockMix - 1 + x = ctypes.create_string_buffer(8*9) + + for i in xrange(2 * r): # BlockMix - 2 + blockxor(BY, i * 8, X, 0, 8) # BlockMix - 3(inner) + salsa20_8(X, x) # BlockMix - 3(outer) + array_overwrite(X, 0, BY, Yi + (i * 8), 8) # BlockMix - 4 + + for i in xrange(r): # BlockMix - 6 + array_overwrite(BY, Yi + (i * 2) * 8, BY, i * 8, 8) + array_overwrite(BY, Yi + (i*2 + 1) * 8, BY, (i + r) * 8, 8) + + + def smix(B, Bi, r, N, V, X): + """SMix; a specific case of ROMix based on Salsa20/8""" + + array_overwrite(B, Bi, X, 0, 16 * r) # ROMix - 1 + + for i in xrange(N): # ROMix - 2 + array_overwrite(X, 0, V, i * (16 * r), 16 * r) # ROMix - 3 + blockmix_salsa8(X, 16 * r, r) # ROMix - 4 + + for i in xrange(N): # ROMix - 6 + j = integerify(X, r) & (N - 1) # ROMix - 7 + blockxor(V, j * (16 * r), X, 0, 16 * r) # ROMix - 8(inner) + blockmix_salsa8(X, 16 * r, r) # ROMix - 9(outer) + + array_overwrite(X, 0, B, Bi, 16 * r) # ROMix - 10 + + check_args(password, salt, N, r, p, olen) + + # Everything is lists of 64-bit uints for all but pbkdf2 + try: + B = _pbkdf2('sha256', password, salt, 1, p * 128 * r) + B = list(struct.unpack('<%dQ' % (len(B) // 8), B)) + XY = [0] * (32 * r) + V = [0] * (16 * r * N) + except (MemoryError, OverflowError): + raise ValueError("scrypt parameters don't fit in memory") + + for i in xrange(p): + smix(B, i * 16 * r, r, N, V, XY) + + B = struct.pack('<%dQ' % len(B), *B) + return _pbkdf2('sha256', password, B, 1, olen) + + +def scrypt_mcf(password, salt=None, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, + prefix=b'$s1$'): + """Derives a Modular Crypt Format hash using the scrypt KDF + + Parameter space is smaller than for scrypt(): + N must be a power of two larger than 1 but no larger than 2 ** 31 + r and p must be positive numbers between 1 and 255 + Salt must be a byte string 1-16 bytes long. + + If no salt is given, a random salt of 128+ bits is used. (Recommended.) + """ + return mcf_mod.scrypt_mcf(scrypt, password, salt, N, r, p, prefix) + + +def scrypt_mcf_check(mcf, password): + """Returns True if the password matches the given MCF hash""" + return mcf_mod.scrypt_mcf_check(scrypt, mcf, password) + + +if __name__ == "__main__": + import sys + from . import tests + tests.run_scrypt_suite(sys.modules[__name__]) + diff --git a/lib/pylibscrypt/pypyscrypt.py b/lib/pylibscrypt/pypyscrypt.py new file mode 100644 index 0000000000000000000000000000000000000000..4b088e12f3784a11be5c0382e31968439b3c296b --- /dev/null +++ b/lib/pylibscrypt/pypyscrypt.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python + +# Copyright (c) 2014 Richard Moore +# Copyright (c) 2014 Jan Varho +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Python implementation of Scrypt password-based key derivation function""" + +# Scrypt definition: +# http://www.tarsnap.com/scrypt/scrypt.pdf + +# It was originally written for a pure-Python Litecoin CPU miner: +# https://github.com/ricmoo/nightminer +# Imported to this project from: +# https://github.com/ricmoo/pyscrypt +# And owes thanks to: +# https://github.com/wg/scrypt + + +import hashlib, hmac +import struct + +from . import mcf as mcf_mod +from .common import * + + +# Python 3.4+ have PBKDF2 in hashlib, so use it... +if 'pbkdf2_hmac' in dir(hashlib): + _pbkdf2 = hashlib.pbkdf2_hmac +else: + # but fall back to Python implementation in < 3.4 + from pbkdf2 import pbkdf2_hmac as _pbkdf2 + + +def array_overwrite(source, s_start, dest, d_start, length): + dest[d_start:d_start + length] = source[s_start:s_start + length] + + +def blockxor(source, s_start, dest, d_start, length): + for i in xrange(length): + dest[d_start + i] ^= source[s_start + i] + + +def integerify(B, r): + """A bijection from ({0, 1} ** k) to {0, ..., (2 ** k) - 1""" + + Bi = (2 * r - 1) * 16 + return B[Bi] + + +def R(X, destination, a1, a2, b): + """A single Salsa20 row operation""" + + a = (X[a1] + X[a2]) & 0xffffffff + X[destination] ^= ((a << b) | (a >> (32 - b))) + + +def salsa20_8(B, x, src, s_start, dest, d_start): + """Salsa20/8 http://en.wikipedia.org/wiki/Salsa20""" + + # Merged blockxor for speed + for i in xrange(16): + x[i] = B[i] = B[i] ^ src[s_start + i] + + # This is the actual Salsa 20/8: four identical double rounds + for i in xrange(4): + R(x, 4, 0,12, 7);R(x, 8, 4, 0, 9);R(x,12, 8, 4,13);R(x, 0,12, 8,18) + R(x, 9, 5, 1, 7);R(x,13, 9, 5, 9);R(x, 1,13, 9,13);R(x, 5, 1,13,18) + R(x,14,10, 6, 7);R(x, 2,14,10, 9);R(x, 6, 2,14,13);R(x,10, 6, 2,18) + R(x, 3,15,11, 7);R(x, 7, 3,15, 9);R(x,11, 7, 3,13);R(x,15,11, 7,18) + R(x, 1, 0, 3, 7);R(x, 2, 1, 0, 9);R(x, 3, 2, 1,13);R(x, 0, 3, 2,18) + R(x, 6, 5, 4, 7);R(x, 7, 6, 5, 9);R(x, 4, 7, 6,13);R(x, 5, 4, 7,18) + R(x,11,10, 9, 7);R(x, 8,11,10, 9);R(x, 9, 8,11,13);R(x,10, 9, 8,18) + R(x,12,15,14, 7);R(x,13,12,15, 9);R(x,14,13,12,13);R(x,15,14,13,18) + + # While we are handling the data, write it to the correct dest. + # The latter half is still part of salsa20 + for i in xrange(16): + dest[d_start + i] = B[i] = (x[i] + B[i]) & 0xffffffff + + +def blockmix_salsa8(BY, Yi, r): + """Blockmix; Used by SMix""" + + start = (2 * r - 1) * 16 + X = BY[start:start+16] # BlockMix - 1 + tmp = [0]*16 + + for i in xrange(2 * r): # BlockMix - 2 + #blockxor(BY, i * 16, X, 0, 16) # BlockMix - 3(inner) + salsa20_8(X, tmp, BY, i * 16, BY, Yi + i*16) # BlockMix - 3(outer) + #array_overwrite(X, 0, BY, Yi + (i * 16), 16) # BlockMix - 4 + + for i in xrange(r): # BlockMix - 6 + array_overwrite(BY, Yi + (i * 2) * 16, BY, i * 16, 16) + array_overwrite(BY, Yi + (i*2 + 1) * 16, BY, (i + r) * 16, 16) + + +def smix(B, Bi, r, N, V, X): + """SMix; a specific case of ROMix based on Salsa20/8""" + + array_overwrite(B, Bi, X, 0, 32 * r) # ROMix - 1 + + for i in xrange(N): # ROMix - 2 + array_overwrite(X, 0, V, i * (32 * r), 32 * r) # ROMix - 3 + blockmix_salsa8(X, 32 * r, r) # ROMix - 4 + + for i in xrange(N): # ROMix - 6 + j = integerify(X, r) & (N - 1) # ROMix - 7 + blockxor(V, j * (32 * r), X, 0, 32 * r) # ROMix - 8(inner) + blockmix_salsa8(X, 32 * r, r) # ROMix - 9(outer) + + array_overwrite(X, 0, B, Bi, 32 * r) # ROMix - 10 + + +def scrypt(password, salt, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, olen=64): + """Returns a key derived using the scrypt key-derivarion function + + N must be a power of two larger than 1 but no larger than 2 ** 63 (insane) + r and p must be positive numbers such that r * p < 2 ** 30 + + The default values are: + N -- 2**14 (~16k) + r -- 8 + p -- 1 + + Memory usage is proportional to N*r. Defaults require about 16 MiB. + Time taken is proportional to N*p. Defaults take <100ms of a recent x86. + + The last one differs from libscrypt defaults, but matches the 'interactive' + work factor from the original paper. For long term storage where runtime of + key derivation is not a problem, you could use 16 as in libscrypt or better + yet increase N if memory is plentiful. + """ + + check_args(password, salt, N, r, p, olen) + + # Everything is lists of 32-bit uints for all but pbkdf2 + try: + B = _pbkdf2('sha256', password, salt, 1, p * 128 * r) + B = list(struct.unpack('<%dI' % (len(B) // 4), B)) + XY = [0] * (64 * r) + V = [0] * (32 * r * N) + except (MemoryError, OverflowError): + raise ValueError("scrypt parameters don't fit in memory") + + for i in xrange(p): + smix(B, i * 32 * r, r, N, V, XY) + + B = struct.pack('<%dI' % len(B), *B) + return _pbkdf2('sha256', password, B, 1, olen) + + +def scrypt_mcf(password, salt=None, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, + prefix=SCRYPT_MCF_PREFIX_DEFAULT): + """Derives a Modular Crypt Format hash using the scrypt KDF + + Parameter space is smaller than for scrypt(): + N must be a power of two larger than 1 but no larger than 2 ** 31 + r and p must be positive numbers between 1 and 255 + Salt must be a byte string 1-16 bytes long. + + If no salt is given, a random salt of 128+ bits is used. (Recommended.) + """ + return mcf_mod.scrypt_mcf(scrypt, password, salt, N, r, p, prefix) + + +def scrypt_mcf_check(mcf, password): + """Returns True if the password matches the given MCF hash""" + return mcf_mod.scrypt_mcf_check(scrypt, mcf, password) + + +__all__ = ['scrypt', 'scrypt_mcf', 'scrypt_mcf_check'] + + +if __name__ == "__main__": + import sys + from . import tests + tests.run_scrypt_suite(sys.modules[__name__]) + diff --git a/lib/pylibscrypt/pypyscrypt_inline.py b/lib/pylibscrypt/pypyscrypt_inline.py new file mode 100644 index 0000000000000000000000000000000000000000..75f497e9a181a54e9fe1be9b21f438c3e20872d2 --- /dev/null +++ b/lib/pylibscrypt/pypyscrypt_inline.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python + +# Automatically generated file, see inline.py + +# Copyright (c) 2014 Richard Moore +# Copyright (c) 2014 Jan Varho +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Python implementation of Scrypt password-based key derivation function""" + +# Scrypt definition: +# http://www.tarsnap.com/scrypt/scrypt.pdf + +# It was originally written for a pure-Python Litecoin CPU miner: +# https://github.com/ricmoo/nightminer +# Imported to this project from: +# https://github.com/ricmoo/pyscrypt +# And owes thanks to: +# https://github.com/wg/scrypt + + +import hashlib, hmac +import struct + +from . import mcf as mcf_mod +from .common import * + + +# Python 3.4+ have PBKDF2 in hashlib, so use it... +if 'pbkdf2_hmac' in dir(hashlib): + _pbkdf2 = hashlib.pbkdf2_hmac +else: + # but fall back to Python implementation in < 3.4 + from pbkdf2 import pbkdf2_hmac as _pbkdf2 + + +def blockxor(source, s_start, dest, d_start, length): + for i in xrange(length): + dest[d_start + i] ^= source[s_start + i] + + +def integerify(B, r): + """A bijection from ({0, 1} ** k) to {0, ..., (2 ** k) - 1""" + + Bi = (2 * r - 1) * 16 + return B[Bi] + + +def salsa20_8(B, x, src, s_start, dest, d_start): + """Salsa20/8 http://en.wikipedia.org/wiki/Salsa20""" + + # Merged blockxor for speed + for i in xrange(16): + x[i] = B[i] = B[i] ^ src[s_start + i] + + # This is the actual Salsa 20/8: four identical double rounds + for i in xrange(4): + a = (x[0]+x[12]) & 0xffffffff + b = (x[5]+x[1]) & 0xffffffff + x[4] ^= (a << 7) | (a >> 25) + x[9] ^= (b << 7) | (b >> 25) + a = (x[10]+x[6]) & 0xffffffff + b = (x[15]+x[11]) & 0xffffffff + x[14] ^= (a << 7) | (a >> 25) + x[3] ^= (b << 7) | (b >> 25) + a = (x[4]+x[0]) & 0xffffffff + b = (x[9]+x[5]) & 0xffffffff + x[8] ^= (a << 9) | (a >> 23) + x[13] ^= (b << 9) | (b >> 23) + a = (x[14]+x[10]) & 0xffffffff + b = (x[3]+x[15]) & 0xffffffff + x[2] ^= (a << 9) | (a >> 23) + x[7] ^= (b << 9) | (b >> 23) + a = (x[8]+x[4]) & 0xffffffff + b = (x[13]+x[9]) & 0xffffffff + x[12] ^= (a << 13) | (a >> 19) + x[1] ^= (b << 13) | (b >> 19) + a = (x[2]+x[14]) & 0xffffffff + b = (x[7]+x[3]) & 0xffffffff + x[6] ^= (a << 13) | (a >> 19) + x[11] ^= (b << 13) | (b >> 19) + a = (x[12]+x[8]) & 0xffffffff + b = (x[1]+x[13]) & 0xffffffff + x[0] ^= (a << 18) | (a >> 14) + x[5] ^= (b << 18) | (b >> 14) + a = (x[6]+x[2]) & 0xffffffff + b = (x[11]+x[7]) & 0xffffffff + x[10] ^= (a << 18) | (a >> 14) + x[15] ^= (b << 18) | (b >> 14) + a = (x[0]+x[3]) & 0xffffffff + b = (x[5]+x[4]) & 0xffffffff + x[1] ^= (a << 7) | (a >> 25) + x[6] ^= (b << 7) | (b >> 25) + a = (x[10]+x[9]) & 0xffffffff + b = (x[15]+x[14]) & 0xffffffff + x[11] ^= (a << 7) | (a >> 25) + x[12] ^= (b << 7) | (b >> 25) + a = (x[1]+x[0]) & 0xffffffff + b = (x[6]+x[5]) & 0xffffffff + x[2] ^= (a << 9) | (a >> 23) + x[7] ^= (b << 9) | (b >> 23) + a = (x[11]+x[10]) & 0xffffffff + b = (x[12]+x[15]) & 0xffffffff + x[8] ^= (a << 9) | (a >> 23) + x[13] ^= (b << 9) | (b >> 23) + a = (x[2]+x[1]) & 0xffffffff + b = (x[7]+x[6]) & 0xffffffff + x[3] ^= (a << 13) | (a >> 19) + x[4] ^= (b << 13) | (b >> 19) + a = (x[8]+x[11]) & 0xffffffff + b = (x[13]+x[12]) & 0xffffffff + x[9] ^= (a << 13) | (a >> 19) + x[14] ^= (b << 13) | (b >> 19) + a = (x[3]+x[2]) & 0xffffffff + b = (x[4]+x[7]) & 0xffffffff + x[0] ^= (a << 18) | (a >> 14) + x[5] ^= (b << 18) | (b >> 14) + a = (x[9]+x[8]) & 0xffffffff + b = (x[14]+x[13]) & 0xffffffff + x[10] ^= (a << 18) | (a >> 14) + x[15] ^= (b << 18) | (b >> 14) + + # While we are handling the data, write it to the correct dest. + # The latter half is still part of salsa20 + for i in xrange(16): + dest[d_start + i] = B[i] = (x[i] + B[i]) & 0xffffffff + + +def blockmix_salsa8(BY, Yi, r): + """Blockmix; Used by SMix""" + + start = (2 * r - 1) * 16 + X = BY[start:start+16] # BlockMix - 1 + tmp = [0]*16 + + for i in xrange(2 * r): # BlockMix - 2 + #blockxor(BY, i * 16, X, 0, 16) # BlockMix - 3(inner) + salsa20_8(X, tmp, BY, i * 16, BY, Yi + i*16) # BlockMix - 3(outer) + #array_overwrite(X, 0, BY, Yi + (i * 16), 16) # BlockMix - 4 + + for i in xrange(r): # BlockMix - 6 + BY[i * 16:(i * 16)+(16)] = BY[Yi + (i * 2) * 16:(Yi + (i * 2) * 16)+(16)] + BY[(i + r) * 16:((i + r) * 16)+(16)] = BY[Yi + (i*2 + 1) * 16:(Yi + (i*2 + 1) * 16)+(16)] + + +def smix(B, Bi, r, N, V, X): + """SMix; a specific case of ROMix based on Salsa20/8""" + + X[0:(0)+(32 * r)] = B[Bi:(Bi)+(32 * r)] + + for i in xrange(N): # ROMix - 2 + V[i * (32 * r):(i * (32 * r))+(32 * r)] = X[0:(0)+(32 * r)] + blockmix_salsa8(X, 32 * r, r) # ROMix - 4 + + for i in xrange(N): # ROMix - 6 + j = integerify(X, r) & (N - 1) # ROMix - 7 + blockxor(V, j * (32 * r), X, 0, 32 * r) # ROMix - 8(inner) + blockmix_salsa8(X, 32 * r, r) # ROMix - 9(outer) + + B[Bi:(Bi)+(32 * r)] = X[0:(0)+(32 * r)] + + +def scrypt(password, salt, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, olen=64): + """Returns a key derived using the scrypt key-derivarion function + + N must be a power of two larger than 1 but no larger than 2 ** 63 (insane) + r and p must be positive numbers such that r * p < 2 ** 30 + + The default values are: + N -- 2**14 (~16k) + r -- 8 + p -- 1 + + Memory usage is proportional to N*r. Defaults require about 16 MiB. + Time taken is proportional to N*p. Defaults take <100ms of a recent x86. + + The last one differs from libscrypt defaults, but matches the 'interactive' + work factor from the original paper. For long term storage where runtime of + key derivation is not a problem, you could use 16 as in libscrypt or better + yet increase N if memory is plentiful. + """ + + check_args(password, salt, N, r, p, olen) + + # Everything is lists of 32-bit uints for all but pbkdf2 + try: + B = _pbkdf2('sha256', password, salt, 1, p * 128 * r) + B = list(struct.unpack('<%dI' % (len(B) // 4), B)) + XY = [0] * (64 * r) + V = [0] * (32 * r * N) + except (MemoryError, OverflowError): + raise ValueError("scrypt parameters don't fit in memory") + + for i in xrange(p): + smix(B, i * 32 * r, r, N, V, XY) + + B = struct.pack('<%dI' % len(B), *B) + return _pbkdf2('sha256', password, B, 1, olen) + + +def scrypt_mcf(password, salt=None, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, + prefix=SCRYPT_MCF_PREFIX_DEFAULT): + """Derives a Modular Crypt Format hash using the scrypt KDF + + Parameter space is smaller than for scrypt(): + N must be a power of two larger than 1 but no larger than 2 ** 31 + r and p must be positive numbers between 1 and 255 + Salt must be a byte string 1-16 bytes long. + + If no salt is given, a random salt of 128+ bits is used. (Recommended.) + """ + return mcf_mod.scrypt_mcf(scrypt, password, salt, N, r, p, prefix) + + +def scrypt_mcf_check(mcf, password): + """Returns True if the password matches the given MCF hash""" + return mcf_mod.scrypt_mcf_check(scrypt, mcf, password) + + +__all__ = ['scrypt', 'scrypt_mcf', 'scrypt_mcf_check'] + + +if __name__ == "__main__": + import sys + from . import tests + tests.run_scrypt_suite(sys.modules[__name__]) + diff --git a/lib/pylibscrypt/pyscrypt.py b/lib/pylibscrypt/pyscrypt.py new file mode 100644 index 0000000000000000000000000000000000000000..5cfe1bbda7c2eb1bb93e7e9eaa0950f9578e9e4e --- /dev/null +++ b/lib/pylibscrypt/pyscrypt.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python + +# Copyright (c) 2014, Jan Varho +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Scrypt implementation that calls into the 'scrypt' python module""" + + +import numbers + +from scrypt import hash as _scrypt + +from . import mcf as mcf_mod +from .common import * + + +# scrypt < 0.6 doesn't support hash length +try: + _scrypt(b'password', b'NaCl', N=2, r=1, p=1, buflen=42) +except TypeError: + raise ImportError('scrypt module version unsupported, 0.6+ required') + + +def scrypt(password, salt, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, olen=64): + """Returns a key derived using the scrypt key-derivarion function + + N must be a power of two larger than 1 but no larger than 2 ** 63 (insane) + r and p must be positive numbers such that r * p < 2 ** 30 + + The default values are: + N -- 2**14 (~16k) + r -- 8 + p -- 1 + + Memory usage is proportional to N*r. Defaults require about 16 MiB. + Time taken is proportional to N*p. Defaults take <100ms of a recent x86. + + The last one differs from libscrypt defaults, but matches the 'interactive' + work factor from the original paper. For long term storage where runtime of + key derivation is not a problem, you could use 16 as in libscrypt or better + yet increase N if memory is plentiful. + """ + check_args(password, salt, N, r, p, olen) + + try: + return _scrypt(password=password, salt=salt, N=N, r=r, p=p, buflen=olen) + except: + raise ValueError + + +def scrypt_mcf(password, salt=None, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, + prefix=SCRYPT_MCF_PREFIX_DEFAULT): + """Derives a Modular Crypt Format hash using the scrypt KDF + + Parameter space is smaller than for scrypt(): + N must be a power of two larger than 1 but no larger than 2 ** 31 + r and p must be positive numbers between 1 and 255 + Salt must be a byte string 1-16 bytes long. + + If no salt is given, a random salt of 128+ bits is used. (Recommended.) + """ + return mcf_mod.scrypt_mcf(scrypt, password, salt, N, r, p, prefix) + + +def scrypt_mcf_check(mcf, password): + """Returns True if the password matches the given MCF hash""" + return mcf_mod.scrypt_mcf_check(scrypt, mcf, password) + + +if __name__ == "__main__": + import sys + from . import tests + tests.run_scrypt_suite(sys.modules[__name__]) + diff --git a/lib/pylibscrypt/tests.py b/lib/pylibscrypt/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..95e1b1869d4cb6d80b1c46b8db65f385a42c2f62 --- /dev/null +++ b/lib/pylibscrypt/tests.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python + +# Copyright (c) 2014, Jan Varho +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Tests scrypt and PBKDF2 implementations""" + + +import base64 +import hashlib +import unittest + + +class ScryptTests(unittest.TestCase): + """Tests an scrypt implementation from module""" + set_up_lambda = None + tear_down_lambda = None + + def setUp(self): + if not self.module: + self.skipTest('module not tested') + if self.set_up_lambda: + self.set_up_lambda() + + def tearDown(self): + if self.tear_down_lambda: + self.tear_down_lambda() + + def _test_vector(self, vector): + pw, s, N, r, p, h, m = vector + self.assertEqual( + self.module.scrypt(pw, s, N, r, p), + base64.b16decode(h, True) + ) + if m is not None: + self.assertEqual( + self.module.scrypt_mcf(pw, s, N, r, p), + m + ) + self.assertTrue(self.module.scrypt_mcf_check(m, pw)) + self.assertFalse(self.module.scrypt_mcf_check(m, b'x' + pw)) + + def test_vector0(self): + self._test_vector(( + b'', b'', 16, 1, 1, + b'77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442' + b'fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906', + None + )) + + def test_vector1(self): + if self.fast: + self.skipTest('slow testcase') + self._test_vector(( + b'password', b'NaCl', 1024, 8, 16, + b'fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b373162' + b'2eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640', + b'$s1$0a0810$TmFDbA==$/bq+HJ00cgB4VucZDQHp/nxq18vII3gw53N2Y0s3MWIu' + b'rzDZLiKjiG/xCSedmDDaxyevuUqD7m2DYMvfoswGQA==' + )) + + def test_vector2(self): + if self.fast: + self.skipTest('slow testcase') + self._test_vector(( + b'pleaseletmein', b'SodiumChloride', 16384, 8, 1, + b'7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2' + b'd5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887', + b'$s1$0e0801$U29kaXVtQ2hsb3JpZGU=$cCO9yzr9c0hGHAbNgf046/2o+7qQT44+' + b'qbVD9lRdofLVQylVYT8Pz2LUlwUkKpr55h6F3A1lHkDfzwF7RVdYhw==' + )) + + def test_vector3(self): + self._test_vector(( + b'password', b'NaCl', 2, 8, 1, + b'e5ed8edc019edfef2d3ced0896faf9eec6921dcc68125ce81c10d53474ce' + b'1be545979159700d324e77c68d34c553636a8429c4f3c99b9566466877f9' + b'dca2b92b', + b'$s1$010801$TmFDbA==$5e2O3AGe3+8tPO0Ilvr57saSHcxoElzoHBDVNHTO' + b'G+VFl5FZcA0yTnfGjTTFU2NqhCnE88mblWZGaHf53KK5Kw==' + )) + + def test_vector4(self): + self._test_vector(( + b'pleaseletmein', b'SodiumChloride', 4, 1, 1, + b'BB1D77016C543A99FE632C9C43C60180FD05E0CAC8B29374DBD1854569CB' + b'534F487240CFC069D6A59A35F2FA5C7428B21D9BE9F84315446D5371119E' + b'016FEDF7', + b'$s1$020101$U29kaXVtQ2hsb3JpZGU=$ux13AWxUOpn+YyycQ8YBgP0F4MrI' + b'spN029GFRWnLU09IckDPwGnWpZo18vpcdCiyHZvp+EMVRG1TcRGeAW/t9w==' + )) + + def test_vector5(self): + if self.fast: + self.skipTest('slow testcase') + self._test_vector(( + b'pleaseletmein', b'X'*32, 2**10, 8, 1, + b'cd81f46bd79125651e017a1bf5a28295f68d4b68d397815514bfdc2f3684' + b'f034ae2a5df332a48e915f7567306df2d401387b70d8f02f83bd6f4c69ff' + b'89d2663c', + None + )) + + def test_vector6(self): + self._test_vector(( + b'pa\0ss', b'salt'*4, 32, 2, 2, + b'76c5260f1dc6339512ae87143d799089f5b508c823c870a3d55f641efa84' + b'63a813221050c93a44255ac8027804c49a87c1ecc9911356b9fc17e06eda' + b'85f23ff5', + None + )) + + def test_vector7(self): + if self.fast: + self.skipTest('slow testcase') + self._test_vector(( + b'pleaseletmein', b'X'*32, 2**10, 8, 2, + b'1693cc02b680b18b3d0a874d459d23a5f63ff4f9de0fb5917ef899226af6' + b'bd33d3a3dfe569d3b6f4f762f0cb64f5f406d485aca501a54645d7389fe6' + b'e28e261e', + None + )) + + def test_bytes_enforced(self): + self.assertRaises(TypeError, self.module.scrypt, u'pass', b'salt') + self.assertRaises(TypeError, self.module.scrypt, 42, b'salt') + self.assertRaises(TypeError, self.module.scrypt, b'pass', None) + self.assertRaises(TypeError, self.module.scrypt_mcf, u'mcf', b'pass') + self.assertRaises(TypeError, self.module.scrypt_mcf, object, b'pass') + + def test_salt_length_mcf(self): + pw = b'pass' + self.assertRaises(ValueError, self.module.scrypt_mcf, pw, b'') + self.assertRaises(ValueError, self.module.scrypt_mcf, pw, b'a'*17) + + def test_salt_generation(self): + pw, N = b'pass', 2 + m1 = self.module.scrypt_mcf(pw, N=N) + m2 = self.module.scrypt_mcf(pw, N=N) + self.assertNotEqual(m1, m2) + self.assertTrue(self.module.scrypt_mcf_check(m1, pw)) + self.assertTrue(self.module.scrypt_mcf_check(m2, pw)) + + def test_invalid_N(self): + pw, s = b'password', b'salt'*8 + self.assertRaises(TypeError, self.module.scrypt, pw, s, 7.5) + self.assertRaises(ValueError, self.module.scrypt, pw, s, -1) + self.assertRaises(ValueError, self.module.scrypt, pw, s, 1) + self.assertRaises(ValueError, self.module.scrypt, pw, s, 42) + self.assertRaises(ValueError, self.module.scrypt, pw, s, 2**66) + self.assertRaises(ValueError, self.module.scrypt, pw, s, 2**66+2) + self.assertRaises(ValueError, self.module.scrypt_mcf, pw, None, 1) + self.assertRaises(ValueError, self.module.scrypt_mcf, pw, None, 2**32) + + def test_huge_N(self): + pw, s = b'password', b'salt'*8 + self.assertRaises(ValueError, self.module.scrypt, pw, s, 2**50) + self.assertRaises(ValueError, self.module.scrypt, pw, s, 2**60) + self.assertRaises(ValueError, self.module.scrypt_mcf, pw, + N=2**31, prefix=b'$7$') + + def test_invalid_r(self): + pw, s, N = b'password', b'salt', 2 + self.assertRaises(ValueError, self.module.scrypt, pw, s, N, 0) + self.assertRaises(ValueError, self.module.scrypt, pw, s, N, -1) + self.assertRaises(TypeError, self.module.scrypt, pw, s, N, 7.5) + self.assertRaises(ValueError, self.module.scrypt, pw, s, N, 2**31) + self.assertRaises(ValueError, self.module.scrypt_mcf, pw, s, N, 256) + + def test_invalid_p(self): + pw, s, N = b'password', b'salt', 2 + self.assertRaises(ValueError, self.module.scrypt, pw, s, N, 1, 0) + self.assertRaises(ValueError, self.module.scrypt, pw, s, N, 1, -2**31) + self.assertRaises(TypeError, self.module.scrypt, pw, s, N, 1, 7.5) + self.assertRaises(ValueError, self.module.scrypt, pw, s, N, 2**35) + self.assertRaises(ValueError, self.module.scrypt_mcf, pw, s, N, 1, 256) + + def test_olen(self): + pw, s, N = b'password', b'salt', 2 + self.assertEquals(len(self.module.scrypt(pw, s, N, olen=42)), 42) + self.assertEquals(len(self.module.scrypt(pw, s, N, olen=100)), 100) + self.assertRaises(TypeError, self.module.scrypt, pw, s, N, olen=b'7') + self.assertRaises(ValueError, self.module.scrypt, pw, s, N, olen=-1) + + def test_invalid_olen(self): + pw, s, N = b'password', b'salt', 2**10 + self.assertRaises(TypeError, self.module.scrypt, pw, s, N, olen=b'7') + self.assertRaises(ValueError, self.module.scrypt, pw, s, N, olen=-1) + + def test_mcf(self): + pw = b'password' + self.assertRaises(ValueError, self.module.scrypt_mcf_check, b'', pw) + self.assertRaises(ValueError, self.module.scrypt_mcf_check, + b'$s1$ffffffff$aaaa$bbbb', pw) + self.assertRaises(TypeError, self.module.scrypt_mcf_check, u'mcf', pw) + self.assertRaises(TypeError, self.module.scrypt_mcf_check, b'mcf', 42) + + def test_mcf_padding(self): + if self.fast: + self.skipTest('slow testcase') + pw = b'pleaseletmein' + m1 = ( + b'$s1$020101$U29kaXVtQ2hsb3JpZGU$ux13AWxUOpn+YyycQ8YBgP0F4MrI' + b'spN029GFRWnLU09IckDPwGnWpZo18vpcdCiyHZvp+EMVRG1TcRGeAW/t9w==' + ) + m2 = ( + b'$s1$020101$U29kaXVtQ2hsb3JpZGU=$ux13AWxUOpn+YyycQ8YBgP0F4MrI' + b'spN029GFRWnLU09IckDPwGnWpZo18vpcdCiyHZvp+EMVRG1TcRGeAW/t9w=' + ) + m3 = ( + b'$s1$020101$U29kaXVtQ2hsb3JpZGU=$ux13AWxUOpn+YyycQ8YBgP0F4MrI' + b'spN029GFRWnLU09IckDPwGnWpZo18vpcdCiyHZvp+EMVRG1TcRGeAW/t9' + ) + self.assertTrue(self.module.scrypt_mcf_check(m1, pw)) + self.assertTrue(self.module.scrypt_mcf_check(m2, pw)) + self.assertRaises(ValueError, self.module.scrypt_mcf_check, m3, pw) + + def test_mcf_nonstandard(self): + pw = b'pass' + m1 = ( # empty salt + b'$s1$010801$$WA1vBj+HFlIk7pG/OPS5bY4NKHBGeGIxEY99farnu2C9uOHxKe' + b'LWP3sCXRvP98F7lVi2JNT/Bmte38iodf81VEYB0Nu3pBw9JqTwiCAqMwL+2kqB' + ) + m2 = ( # 31 byte hash + b'$7$16..../....l/htqjrI38qNowkQZL8RxFVxS8JV9PPJr1+A/WTQWiU' + b'$wOcPY0vsHHshxa0u87FDhmTo42WZr0JbSHY2w2Zkyr1' + ) + m3 = ( # 44 byte salt, 31 byte hash + b'$7$12..../....aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + b'$14hkhieutTQcbq.iU1FDZzYz1vW8NPYowy4WERDM70' + ) + self.assertTrue(self.module.scrypt_mcf_check(m1, pw)) + self.assertTrue(self.module.scrypt_mcf_check(m2, pw)) + self.assertTrue(self.module.scrypt_mcf_check(m3, pw)) + + def test_mcf_7(self): + if self.fast: + self.skipTest('slow testcase') + p, m = b'pleaseletmein', ( + b'$7$C6..../....SodiumChloride' + b'$kBGj9fHznVYFQMEn/qDCfrDevf9YDtcDdKvEqHJLV8D' + ) + self.assertTrue(self.module.scrypt_mcf_check(m, p)) + self.assertFalse(self.module.scrypt_mcf_check(m, b'X'+p)) + self.assertRaises(ValueError, self.module.scrypt_mcf_check, + b'$7$$', p + ) + self.assertRaises(ValueError, self.module.scrypt_mcf_check, + b'$7$$$', p + ) + + def test_mcf_7_2(self): + if self.fast: + self.skipTest('slow testcase') + p = b'pleaseletmein' + m1 = self.module.scrypt_mcf(p, None, 2**10, 8, 2, b'$7$') + self.assertTrue(m1.startswith(b'$7$')) + self.assertTrue(self.module.scrypt_mcf_check(m1, p)) + m2 = self.module.scrypt_mcf(p, None, 2**10, 8, 2, b'$s1$') + self.assertTrue(m2.startswith(b'$s1$')) + self.assertTrue(self.module.scrypt_mcf_check(m1, p)) + + def test_mcf_7_fast(self): + p, m1 = b'pleaseletmein', ( + b'$7$06..../....SodiumChloride' + b'$ENlyo6fGw4PCcDBOFepfSZjFUnVatHzCcW55.ZGz3B0' + ) + self.assertTrue(self.module.scrypt_mcf_check(m1, p)) + m2 = self.module.scrypt_mcf(p, b'NaCl', 4, 8, 1, b'$7$') + self.assertTrue(self.module.scrypt_mcf_check(m2, p)) + + def test_mcf_unknown(self): + p = b'pleaseletmein' + self.assertRaises(ValueError, self.module.scrypt_mcf, p, prefix=b'$$') + + def test_mcf_null(self): + p1, p2, p3 = b'please', b'please\0letmein', b'pleaseletmein' + self.assertRaises(ValueError, self.module.scrypt_mcf, p2, N=4) + m = ( + b'$s1$020801$m8/OZVv4hi8rHFVTvOH3tQ==$jwi4vgiCjyqrZKOaksMFks5A' + b'M9ZRcrVPhAwqT1iRMTqXYrwkTngwjR2rwbAet9cSGdFfSverOEVLiLuUzG4k' + b'Hg==' + ) + self.assertTrue(self.module.scrypt_mcf_check(m, p2)) + self.assertFalse(self.module.scrypt_mcf_check(m, p1)) + self.assertFalse(self.module.scrypt_mcf_check(m, p3)) + + +def load_scrypt_suite(name, module, fast=True): + loader = unittest.defaultTestLoader + tests = type(name, (ScryptTests,), {'module': module, 'fast': fast}) + return unittest.defaultTestLoader.loadTestsFromTestCase(tests) + + +def run_scrypt_suite(module, fast=False): + suite = unittest.TestSuite() + suite.addTest(load_scrypt_suite('scryptTests', module, fast)) + unittest.TextTestRunner().run(suite) + + +class PBKDF2Tests(unittest.TestCase): + """Tests a PBKDF2 implementation from module""" + def setUp(self): + if not self.module: + self.skipTest('module not tested') + + def _test_vector(self, vector): + n, p, s, c, l, h = vector + h = base64.b16decode(h, True) + self.assertEquals(self.module.pbkdf2_hmac(n, p, s, c, l), h) + + def test_vector1(self): + self._test_vector(('sha1', b'password', b'salt', 1, 20, + b'0c60c80f961f0e71f3a9b524af6012062fe037a6')) + + def test_vector2(self): + self._test_vector(('sha1', b'pass\0word', b'sa\0lt', 4096, 16, + b'56fa6aa75548099dcc37d7f03425e0c3')) + + def test_vector3(self): + self._test_vector(('sha256', b'password', b'NaCl', 7, 42, + b'8cb94b8721e20e643be099f3c31d332456b4c26f55' + b'b6403950267dc2b3c0806bda709a3f2d7f6107db73')) + + def test_long_key(self): + self.module.pbkdf2_hmac('sha256', b'pass'*100, b'NaCl', 2, 20) + + +def load_pbkdf2_suite(name, module): + loader = unittest.defaultTestLoader + tests = type(name, (PBKDF2Tests,), {'module': module}) + return unittest.defaultTestLoader.loadTestsFromTestCase(tests) + + +def run_pbkdf2_suite(module, fast=False): + suite = unittest.TestSuite() + suite.addTest(load_pbkdf2_suite('scryptTests', module)) + unittest.TextTestRunner().run(suite) + + +if __name__ == "__main__": + suite = unittest.TestSuite() + try: + from . import pylibscrypt + suite.addTest(load_scrypt_suite('pylibscryptTests', pylibscrypt, True)) + except ImportError: + suite.addTest(load_scrypt_suite('pylibscryptTests', None, True)) + + try: + from . import pyscrypt + suite.addTest(load_scrypt_suite('pyscryptTests', pyscrypt, True)) + except ImportError: + suite.addTest(load_scrypt_suite('pyscryptTests', None, True)) + + try: + from . import pylibsodium + suite.addTest(load_scrypt_suite('pylibsodiumTests', + pylibsodium, True)) + from . import pylibscrypt + loader = unittest.defaultTestLoader + def set_up_ll(self): + if not self.module._scrypt_ll: + self.skipTest('no ll') + self.tmp_ll = self.module._scrypt_ll + self.tmp_scr = self.module.scr_mod + self.module._scrypt_ll = None + self.module.scr_mod = pylibscrypt + def tear_down_ll(self): + self.module._scrypt_ll = self.tmp_ll + self.module.scr_mod = self.tmp_scr + tmp = type( + 'pylibsodiumFallbackTests', (ScryptTests,), + { + 'module': pylibsodium, 'fast': False, + 'set_up_lambda': set_up_ll, + 'tear_down_lambda': tear_down_ll, + } + ) + suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(tmp)) + except ImportError: + suite.addTest(load_scrypt_suite('pylibsodiumTests', None, True)) + + try: + from . import pylibsodium_salsa + suite.addTest(load_scrypt_suite('pylibsodium_salsaTests', + pylibsodium_salsa, True)) + except ImportError: + suite.addTest(load_scrypt_suite('pylibsodium_salsaTests', None, True)) + + try: + from . import pypyscrypt_inline as pypyscrypt + suite.addTest(load_scrypt_suite('pypyscryptTests', pypyscrypt, True)) + except ImportError: + suite.addTest(load_scrypt_suite('pypyscryptTests', None, True)) + + try: + from . import pbkdf2 + suite.addTest(load_pbkdf2_suite('pbkdf2', pbkdf2)) + except ImportError: + suite.addTest(load_pbkdf2_suite('pbkdf2', None)) + + if 'pbkdf2_hmac' in dir(hashlib): + suite.addTest(load_pbkdf2_suite('hashlib_pbkdf2', hashlib)) + else: + suite.addTest(load_pbkdf2_suite('hashlib_pbkdf2', None)) + + unittest.TextTestRunner().run(suite) + diff --git a/setup.py b/setup.py index 8317c343545aea95172ab19342bf10c6a7d60390..b4a0739e34df62b0d3314cee1688fdbd3a05fa71 100644 --- a/setup.py +++ b/setup.py @@ -12,9 +12,9 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), 'lib'))) sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), 'src'))) print(sys.path) -includes = ["sip", "re", "json", "logging", "hashlib", "os", "urllib", "ucoinpy", "requests"] +includes = ["sip", "re", "json", "logging", "hashlib", "os", "urllib", "ucoinpy", "pylibscrypt", "requests"] excludes = [] -packages = ["libnacl", "pylibscrypt"] +packages = ["libnacl"] includefiles = [] if sys.platform == "win32":