diff options
| author | Eli Collins <elic@assurancetechnologies.com> | 2016-06-10 20:21:32 -0400 |
|---|---|---|
| committer | Eli Collins <elic@assurancetechnologies.com> | 2016-06-10 20:21:32 -0400 |
| commit | 83280f2468e6119bdeca4cb4a90712496985254d (patch) | |
| tree | dfe2a940f286e5f95d81816dcd187818cf6225c0 | |
| parent | 1a34ab41c924c8e5a9de0f28b3ae15b42521102e (diff) | |
| download | passlib-83280f2468e6119bdeca4cb4a90712496985254d.tar.gz | |
passlib.handlers.scrypt: created scrypt hash handler, complete with UTs and docs
* scrypt kdf code
- relocated scrypt kdf code to passlib.crypto.scrypt
- py3 compat fixes
- split UTs out into separate file
- removed "non-power of 2" support, not needed.
- added wrapper which can toggle between builtin backend,
and extenrnal scrypt package.
- factored out n/r/p validation code so it can be used independantly
of calling kdf itself.
* passlib.handlers.scrypt: added scrypt handler which wraps the kdf.
- added some custom test strings, as well as adapted some reference values
from the scrypt whitepaper.
- added documentation page
- integrated scrypt kdf wrapper w/ hash's HasManyBackends api
| -rw-r--r-- | CHANGES | 3 | ||||
| -rw-r--r-- | docs/install.rst | 5 | ||||
| -rw-r--r-- | docs/lib/passlib.hash.rst | 1 | ||||
| -rw-r--r-- | docs/lib/passlib.hash.scrypt.rst | 120 | ||||
| -rw-r--r-- | docs/new_app_quickstart.rst | 18 | ||||
| -rw-r--r-- | passlib/crypto/scrypt/__init__.py | 193 | ||||
| -rw-r--r-- | passlib/crypto/scrypt/_builtin.py | 244 | ||||
| -rw-r--r-- | passlib/crypto/scrypt/_gen_files.py (renamed from passlib/utils/scrypt/_gen_files.py) | 20 | ||||
| -rw-r--r-- | passlib/crypto/scrypt/_salsa.py (renamed from passlib/utils/scrypt/_salsa.py) | 2 | ||||
| -rw-r--r-- | passlib/handlers/scrypt.py | 268 | ||||
| -rw-r--r-- | passlib/registry.py | 1 | ||||
| -rw-r--r-- | passlib/tests/test_crypto_scrypt.py | 597 | ||||
| -rw-r--r-- | passlib/tests/test_handlers.py | 83 | ||||
| -rw-r--r-- | passlib/utils/compat/__init__.py | 11 | ||||
| -rw-r--r-- | passlib/utils/handlers.py | 49 | ||||
| -rw-r--r-- | passlib/utils/scrypt/__init__.py | 332 | ||||
| -rw-r--r-- | tox.ini | 23 |
17 files changed, 1608 insertions, 362 deletions
@@ -73,6 +73,9 @@ Major Changes to upgrade to py-bcrypt 0.3 or another bcrypt library if you are using the :doc:`bcrypt </lib/passlib.hash.bcrypt>` hash. + * :class:`~passlib.hash.scrypt` -- New password hash format which uses + the SCrypt KDF (:issue:`8`). + Minor Internal Changes ---------------------- * The shared :class:`!PasswordHash` unittests now check all hash handlers for diff --git a/docs/install.rst b/docs/install.rst index ece2198..4e9b50f 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -48,6 +48,11 @@ Optional Libraries functions used by some PBKDF2-based hashes, but it is not required even in that case. +* `SCrypt <https://pypi.python.org/pypi/scrypt>`_ + + If installed, this will be used to provider support for the :class:`~passlib.hash.scrypt` + hash algorithm. If not installed, a MUCH slower builtin reference implementation will be used. + Installation Instructions ========================= To install from PyPi using :command:`pip`:: diff --git a/docs/lib/passlib.hash.rst b/docs/lib/passlib.hash.rst index a86416c..802d49a 100644 --- a/docs/lib/passlib.hash.rst +++ b/docs/lib/passlib.hash.rst @@ -129,6 +129,7 @@ they can be used compatibly along side other modular crypt format hashes. passlib.hash.cta_pbkdf2_sha1 passlib.hash.dlitz_pbkdf2_sha1 passlib.hash.scram + passlib.hash.scrypt * :class:`passlib.hash.bsd_nthash` - FreeBSD's MCF-compatible :doc:`nthash <passlib.hash.nthash>` encoding diff --git a/docs/lib/passlib.hash.scrypt.rst b/docs/lib/passlib.hash.scrypt.rst new file mode 100644 index 0000000..9629af7 --- /dev/null +++ b/docs/lib/passlib.hash.scrypt.rst @@ -0,0 +1,120 @@ +================================================================== +:class:`passlib.hash.scrypt` - SCrypt +================================================================== + +.. versionadded:: 1.7 + +.. currentmodule:: passlib.hash + +This is a custom hash scheme provided by Passlib which allows storing password hashes +generated using the SCrypt [#scrypt-home]_ key derivation function, and is designed +as the of a new generation of "memory hard" functions. + +.. warning:: + + Be careful when using this algorithm, as the memory and CPU requirements + needed to achieve adequate security are generally higher than acceptable for heavily used + production systems [#scrypt-cost]_: unlike many password hashes, increasing + the rounds value of scrypt will increase the *memory* required, as well as the time. + +This class can be used directly as follows:: + + >>> from passlib.hash import scrypt + + >>> # generate new salt, encrypt password + >>> h = scrypt.hash("password") + >>> h + '$2a$12$NT0I31Sa7ihGEWpka9ASYrEFkhuTNeBQ2xfZskIiiJeyFXhRgS.Sy' + + >>> # the same, but with an explicit number of rounds + >>> scrypt.hash("password", rounds=8) + '$scrypt$16,8,1$aM15713r3Xsvxbi31lqr1Q$nFNh2CVHVjNldFVKDHDlm4CbdRSCdEBsjjJxD.iCs5E' + + >>> # verify password + >>> scrypt.verify("password", h) + True + >>> scrypt.verify("wrong", h) + False + +.. note:: + + It is strongly recommended that you install + `scrypt <https://pypi.python.org/pypi/scrypt>`_ + when using this hash. + +.. seealso:: the generic :ref:`PasswordHash usage examples <password-hash-examples>` + +Interface +========= +.. autoclass:: scrypt() + +Scrypt Backends +--------------- + +This class will use the first available of two possible backends: + +1. The C-accelarated `scrypt <https://pypi.python.org/pypi/scrypt>`_ package, if installed. +2. A pure-python implementation of SCrypt, built into Passlib. + +.. warning:: + + *It is strongly recommended to install the external scrypt package*. + + The pure-python backend is intended as a reference and last-resort implementation only; + it is 10-100x too slow to be usable in production at a secure ``rounds`` cost. + +Format & Algorithm +================== +This Scrypt hash format is compatible with the :ref:`modular-crypt-format`, and uses ``$scrypt$`` as the identifying prefix +for all its strings. An example hash (of ``password``) is: + + ``$scrypt$16,8,1$aM15713r3Xsvxbi31lqr1Q$nFNh2CVHVjNldFVKDHDlm4CbdRSCdEBsjjJxD.iCs5E`` + +This string has the format :samp:`$scrypt${nExp},{r},{p}${salt}${checksum}`, where: + +* :samp:`{nExp}` is the exponent for calculating SCRYPT's cost parameter (N), encoded as a decimal digit, + (nExp is 16 in the example, corresponding to n=65536). + +* :samp:`{r}` is the value of SCRYPT's block size parameter (r), encoded as a decimal digit, + (r is 8 in the example). + +* :samp:`{p}` is the value of SCRYPT's parallel count parameter (p), encoded as a decimal digit, + (p is 1 in the example). + +* :samp:`{salt}` - this is the :func:`adapted base64 encoding <passlib.utils.ab64_encode>` + of the raw salt bytes passed into the SCRYPT function (``aM15713r3Xsvxbi31lqr1Q`` in the example). + +* :samp:`{checksum}` - this is the :func:`adapted base64 encoding <passlib.utils.ab64_encode>` + of the raw derived key bytes returned from the SCRYPT function. + This hash currently always uses 32 bytes, resulting in a 43-character checksum. + (``nFNh2CVHVjNldFVKDHDlm4CbdRSCdEBsjjJxD.iCs5E`` in the example). + +The algorithm used by all of these schemes is deliberately identical and simple: +The password is encoded into UTF-8 if not already encoded, +and run throught the SCRYPT function; along with the salt, and the values of n, r, and p. +The first 32 bytes of the returned result are encoded as the checksum. + +See `<http://www.tarsnap.com/scrypt.html>`_ for the canonical description of the scrypt kdf. + +Security Issues +=============== +See the warning at the top of this page about the tradeoff between memory usage +and security that comes as part of altering scrypt's rounds parameter. + +Deviations +========== +There is not a standardized format for encoding scrypt-hashed passwords in a manner compatible +with the modular crypt format; the format documented here is specific to passlib. + +That said, the raw contents of these hashes should be identical to the output of any other +scrypt kdf implementation. + +.. rubric:: Footnotes + +.. [#scrypt-home] the SCrypt KDF homepage - + `<http://www.tarsnap.com/scrypt.html>`_ + +.. [#scrypt-cost] posts discussing security implications of scrypt's tying memory cost to calculation time - + `<http://blog.ircmaxell.com/2014/03/why-i-dont-recommend-scrypt.html>`_, + `<http://security.stackexchange.com/questions/26245/is-bcrypt-better-than-scrypt>`_, + `<http://security.stackexchange.com/questions/4781/do-any-security-experts-recommend-bcrypt-for-password-storage>`_ diff --git a/docs/new_app_quickstart.rst b/docs/new_app_quickstart.rst index 57bb8c5..b47b836 100644 --- a/docs/new_app_quickstart.rst +++ b/docs/new_app_quickstart.rst @@ -166,19 +166,11 @@ standard format for encoding password hashes using this algorithm What about SCrypt? .................. -`SCrypt <http://www.tarsnap.com/scrypt.html>`_ is the leading contender -to be the next-generation password hash algorithm. It offers many advances -over all of the above hashes; the primary feature being that it has -a variable *memory* cost as well as time cost. It is incredibly well designed, -and looks to likely replace all the others in this section. - -However, it is still young by comparison to the others; and has not been as thoroughly -tested, or widely implemented. The only Python wrapper that exists -does not even expose the underlying :func:`!scrypt` function, -but is rather a file encryption tool. -Due to these reasons, SCrypt has not yet been integrated into Passlib. - -.. seealso:: :issue:`8` of the Passlib bugtracker, for the current status of Passlib's SCrypt support. +`SCrypt <http://www.tarsnap.com/scrypt.html>`_ is the first in a class of "memory-hard" +key derivation functions. Passlib supports hashing passwords with SCrypt via the +:class:`~passlib.hash.scrypt` password hash. However, it is not currently recommended for use +unless you know what you're doing, as selection of an appropriate secure set of rounds parameters +is very dependant on your serve load, and may frequently not be possible. Creating and Using a CryptContext ================================= diff --git a/passlib/crypto/scrypt/__init__.py b/passlib/crypto/scrypt/__init__.py new file mode 100644 index 0000000..8fa3e78 --- /dev/null +++ b/passlib/crypto/scrypt/__init__.py @@ -0,0 +1,193 @@ +"""passlib.utils.scrypt -- scrypt hash frontend and help utilities""" +#========================================================================== +# imports +#========================================================================== +from __future__ import absolute_import +# core +import logging; log = logging.getLogger(__name__) +from warnings import warn +# pkg +from passlib import exc +from passlib.utils import to_bytes +from passlib.utils.compat import PYPY +# local +__all__ =[ + "validate", + "scrypt", +] + +#========================================================================== +# config validation +#========================================================================== + +#: max output length in bytes +MAX_KEYLEN = ((1 << 32) - 1) * 32 + +#: max ``r * p`` limit +MAX_RP = (1 << 30) - 1 + +# TODO: unittests for this function +def validate(n, r, p): + """ + helper which validates a set of scrypt config parameters. + scrypt will take ``O(n * r * p)`` time and ``O(n * r)`` memory. + limitations are that ``n = 2**<positive integer>``, ``n < 2**(16*r)``, ``r * p < 2 ** 30``. + + :param n: scrypt rounds + :param r: scrypt block size + :param p: scrypt parallel factor + """ + if r < 1: + raise ValueError("r must be > 0: r=%r" % r) + + if p < 1: + raise ValueError("p must be > 0: p=%r" % p) + + if r * p > MAX_RP: + # pbkdf2-hmac-sha256 limitation - it will be requested to generate ``p*(2*r)*64`` bytes, + # but pbkdf2 can do max of (2**31-1) blocks, and sha-256 has 32 byte block size... + # so ``(2**31-1)*32 >= p*r*128`` -> ``r*p < 2**30`` + raise ValueError("r * p must be < 2**30: r=%r, p=%r" % (r,p)) + + if n < 2 or n & (n - 1): + raise ValueError("n must be > 1, and a power of 2: n=%r" % n) + + return True + +# TODO: configuration picker (may need psutil for full effect) + +#========================================================================== +# hash frontend +#========================================================================== + +#: backend function used by scrypt(), filled in by _set_backend() +_scrypt = None + +#: name of backend currently in use, exposed for informational purposes. +backend = None + +def scrypt(secret, salt, n, r, p=1, keylen=32): + """run SCrypt key derivation function using specified parameters. + + :arg secret: + passphrase string (unicode is encoded to bytes using utf-8). + + :arg salt: + salt string (unicode is encoded to bytes using utf-8). + + :arg n: + integer 'N' parameter + + :arg r: + integer 'r' parameter + + :arg p: + integer 'p' parameter + + :arg keylen: + number of bytes of key to generate. + defaults to 32 (the internal block size). + + :returns: + a *keylen*-sized bytes instance + + SCrypt imposes a number of constraints on it's input parameters: + + * ``r * p < 2**30`` -- due to a limitation of PBKDF2-HMAC-SHA256. + * ``keylen < (2**32 - 1) * 32`` -- due to a limitation of PBKDF2-HMAC-SHA256. + * ``n`` must a be a power of 2, and > 1 -- internal limitation of scrypt() implementation + + :raises ValueError: if the provided parameters are invalid (see constraints above). + + .. warning:: + + Unless the third-party ``scrypt <https://pypi.python.org/pypi/scrypt/>``_ package + is installed, passlib will use a builtin pure-python implementation of scrypt, + which is *considerably* slower (and thus requires a much lower / less secure + ``n`` value in order to be usuable). Installing the :mod:`!scrypt` package + is strongly recommended. + """ + validate(n, r, p) + secret = to_bytes(secret, param="secret") + salt = to_bytes(salt, param="salt") + if keylen < 0: + raise ValueError("keylen must be >= 0") + if keylen > MAX_KEYLEN: + raise ValueError("keylen too large, must be <= %d" % MAX_KEYLEN) + return _scrypt(secret, salt, n, r, p, keylen) + +#: list of potential backends +backend_values = ("scrypt", "builtin") + +def _builtin_first_run(*args, **kwds): + """ + wrapper which issues warning first time it's used, + then replaces itself with actual function + (assumes this will be installed as _scrypt global when called) + """ + slowdown = 10 if PYPY else 100 + warn("Using builtin scrypt backend, which is %dx slower than is required " + "for adequate security. Installing scrypt support (via 'pip install scrypt') " + "is strongly recommended" % slowdown, exc.PasslibSecurityWarning) + from ._builtin import ScryptEngine + global _scrypt + _scrypt = ScryptEngine.execute + return _scrypt(*args, **kwds) + +def _load_backend(name): + """ + try to load specified scrypt backend + """ + if name == "scrypt": + # try to import the ctypes-based scrypt hash function provided by the + # ``scrypt <https://pypi.python.org/pypi/scrypt/>``_ package. + try: + from scrypt import hash + return hash + except ImportError: + pass + try: + import scrypt + except ImportError as err: + if "scrypt" not in str(err): + # e.g. if cffi isn't set up right + # user should try importing scrypt explicitly to diagnose problem. + warn("'scrypt' package failed to import correctly (possible installation issue?)", + exc.PasslibWarning) + # else: package just isn't installed + else: + warn("'scrypt' package is too old (lacks ``hash()`` method)", exc.PasslibWarning) + return None + if name == "builtin": + return _builtin_first_run + raise ValueError("unknown scrypt backend %r" % name) + +def _set_backend(name): + """ + set backend for scrypt(). if name not specified, loads first available. + + :raises ~passlib.exc.MissingBackendError: if backend can't be found + + .. note:: mainly intended to be called by unittests, and scrypt hash handler + """ + if name == "default": + for name in backend_values: + hash = _load_backend(name) + if hash: + break + else: + raise exc.MissingBackendError("no scrypt backends available") + else: + hash = _load_backend(name) + if not hash: + raise exc.MissingBackendError("scrypt backend %r not available" % name) + global _scrypt, backend + backend = name + _scrypt = hash + +# initialize backend +_set_backend("default") + +#========================================================================== +# eof +#========================================================================== diff --git a/passlib/crypto/scrypt/_builtin.py b/passlib/crypto/scrypt/_builtin.py new file mode 100644 index 0000000..e9bb305 --- /dev/null +++ b/passlib/crypto/scrypt/_builtin.py @@ -0,0 +1,244 @@ +"""passlib.utils.scrypt._builtin -- scrypt() kdf in pure-python""" +#========================================================================== +# imports +#========================================================================== +# core +import operator +import struct +# pkg +from passlib.utils.compat import izip +from passlib.crypto.digest import pbkdf2_hmac +from passlib.crypto.scrypt._salsa import salsa20 +# local +__all__ =[ + "ScryptEngine", +] + +#========================================================================== +# scrypt engine +#========================================================================== +class ScryptEngine(object): + """ + helper class used to run scrypt kdf, see scrypt() for frontend + + .. warning:: + this class does NO validation of the input ranges or types. + + it's not intended to be used directly, + but only as a backend for :func:`passlib.utils.scrypt.scrypt()`. + """ + #================================================================= + # instance attrs + #================================================================= + + # primary scrypt config parameters + n = 0 + r = 0 + p = 0 + + # derived values & objects + smix_bytes = 0 + iv_bytes = 0 + bmix_len = 0 + bmix_half_len = 0 + bmix_struct = None + integerify = None + + #================================================================= + # frontend + #================================================================= + @classmethod + def execute(cls, secret, salt, n, r, p, keylen): + """create engine & run scrypt() hash calculation""" + return cls(n, r, p).run(secret, salt, keylen) + + #================================================================= + # init + #================================================================= + def __init__(self, n, r, p): + # store config + self.n = n + self.r = r + self.p = p + self.smix_bytes = r << 7 # num bytes in smix input - 2*r*16*4 + self.iv_bytes = self.smix_bytes * p + self.bmix_len = bmix_len = r << 5 # length of bmix block list - 32*r integers + self.bmix_half_len = r << 4 + assert struct.calcsize("I") == 4 + self.bmix_struct = struct.Struct("<" + str(bmix_len) + "I") + + # use optimized bmix for certain cases + if r == 1: + self.bmix = self._bmix_1 + + # pick best integerify function - integerify(bmix_block) should + # take last 64 bytes of block and return a little-endian integer. + # since it's immediately converted % n, we only have to extract + # the first 32 bytes if n < 2**32 - which due to the current + # internal representation, is already unpacked as a 32-bit int. + if n <= 0xFFFFffff: + integerify = operator.itemgetter(-16) + else: + assert n <= 0xFFFFffffFFFFffff + ig1 = operator.itemgetter(-16) + ig2 = operator.itemgetter(-17) + def integerify(X): + return ig1(X) | (ig2(X)<<32) + self.integerify = integerify + + #================================================================= + # frontend + #================================================================= + def run(self, secret, salt, keylen): + """ + run scrypt kdf for specified secret, salt, and keylen + + .. note:: + + * time cost is ``O(n * r * p)`` + * mem cost is ``O(n * r)`` + """ + # stretch salt into initial byte array via pbkdf2 + iv_bytes = self.iv_bytes + input = pbkdf2_hmac("sha256", secret, salt, rounds=1, keylen=iv_bytes) + + # split initial byte array into 'p' mflen-sized chunks, + # and run each chunk through smix() to generate output chunk. + smix = self.smix + if self.p == 1: + output = smix(input) + else: + # XXX: *could* use threading here, if really high p values encountered, + # but would tradeoff for more memory usage. + smix_bytes = self.smix_bytes + output = b''.join( + smix(input[offset:offset+smix_bytes]) + for offset in range(0, iv_bytes, smix_bytes) + ) + + # stretch final byte array into output via pbkdf2 + return pbkdf2_hmac("sha256", secret, output, rounds=1, keylen=keylen) + + #================================================================= + # smix() helper + #================================================================= + def smix(self, input): + """run SCrypt smix function on a single input block + + :arg input: + byte string containing input data. + interpreted as 32*r little endian 4 byte integers. + + :returns: + byte string containing output data + derived by mixing input using n & r parameters. + + .. note:: time & mem cost are both ``O(n * r)`` + """ + # gather locals + bmix = self.bmix + bmix_struct = self.bmix_struct + integerify = self.integerify + n = self.n + + # parse input into 32*r integers ('X' in scrypt source) + # mem cost -- O(r) + buffer = list(bmix_struct.unpack(input)) + + # starting with initial buffer contents, derive V s.t. + # V[0]=initial_buffer ... V[i] = bmix(V[i-1], V[i-1]) ... V[n-1] = bmix(V[n-2], V[n-2]) + # final buffer contents should equal bmix(V[n-1], V[n-1]) + # + # time cost -- O(n * r) -- n loops, bmix is O(r) + # mem cost -- O(n * r) -- V is n-element array of r-element tuples + # NOTE: could do time / memory tradeoff to shrink size of V + def vgen(): + i = 0 + while i < n: + last = tuple(buffer) + yield last + bmix(last, buffer) + i += 1 + V = list(vgen()) + + # generate result from X & V. + # + # time cost -- O(n * r) -- loops n times, calls bmix() which has O(r) time cost + # mem cost -- O(1) -- allocates nothing, calls bmix() which has O(1) mem cost + get_v_elem = V.__getitem__ + n_mask = n - 1 + i = 0 + while i < n: + j = integerify(buffer) & n_mask + result = tuple(a ^ b for a, b in izip(buffer, get_v_elem(j))) + bmix(result, buffer) + i += 1 + + # # NOTE: we could easily support arbitrary values of ``n``, not just powers of 2, + # # but very few implementations have that ability, so not enabling it for now... + # if not n_is_log_2: + # while i < n: + # j = integerify(buffer) % n + # tmp = tuple(a^b for a,b in izip(buffer, get_v_elem(j))) + # bmix(tmp,buffer) + # i += 1 + + # repack tmp + return bmix_struct.pack(*buffer) + + #================================================================= + # bmix() helper + #================================================================= + def bmix(self, source, target): + """ + block mixing function used by smix() + uses salsa20/8 core to mix block contents. + + :arg source: + source to read from. + should be list of 32*r 4-byte integers + (2*r salsa20 blocks). + + :arg target: + target to write to. + should be list with same size as source. + the existing value of this buffer is ignored. + + .. warning:: + + this operates *in place* on target, + so source & target should NOT be same list. + + .. note:: + + * time cost is ``O(r)`` -- loops 16*r times, salsa20() has ``O(1)`` cost. + + * memory cost is ``O(1)`` -- salsa20() uses 16 x uint4, + all other operations done in-place. + """ + ## assert source is not target + # Y[-1] = B[2r-1], Y[i] = hash( Y[i-1] xor B[i]) + # B' <-- (Y_0, Y_2 ... Y_{2r-2}, Y_1, Y_3 ... Y_{2r-1}) */ + half = self.bmix_half_len # 16*r out of 32*r - start of Y_1 + tmp = source[-16:] # 'X' in scrypt source + siter = iter(source) + j = 0 + while j < half: + jn = j+16 + target[j:jn] = tmp = salsa20(a ^ b for a, b in izip(tmp, siter)) + target[half+j:half+jn] = tmp = salsa20(a ^ b for a, b in izip(tmp, siter)) + j = jn + + def _bmix_1(self, source, target): + """special bmix() method optimized for ``r=1`` case""" + B = source[16:] + target[:16] = tmp = salsa20(a ^ b for a, b in izip(B, iter(source))) + target[16:] = salsa20(a ^ b for a, b in izip(tmp, B)) + + #================================================================= + # eoc + #================================================================= + +#========================================================================== +# eof +#========================================================================== diff --git a/passlib/utils/scrypt/_gen_files.py b/passlib/crypto/scrypt/_gen_files.py index 77cdd29..55ddfae 100644 --- a/passlib/utils/scrypt/_gen_files.py +++ b/passlib/crypto/scrypt/_gen_files.py @@ -1,4 +1,4 @@ -"""passlib._gen_salsa - meta script that generates _salsa.py""" +"""passlib.utils.scrypt._gen_files - meta script that generates _salsa.py""" #========================================================================== # imports #========================================================================== @@ -92,8 +92,8 @@ def main(): TLIST=TLIST, ) - write("""\ -\"""passlib.utils._salsa - salsa 20/8 core, autogenerated by _gen_salsa.py\""" + write('''\ +"""passlib.utils.scrypt._salsa - salsa 20/8 core, autogenerated by _gen_salsa.py""" #================================================================= # salsa function #================================================================= @@ -111,15 +111,15 @@ def salsa20(input): i = 0 while i < 4: -""" % kwds) +''' % kwds) for idx, (target, source1, source2, rotate) in enumerate(_SALSA_OPS): - write("""\ + write('''\ # salsa op %(idx)d: [%(it)d] ^= ([%(is1)d]+[%(is2)d])<<<%(rot1)d t = (%(src1)s + %(src2)s) & 0xffffffff %(dst)s ^= ((t & 0x%(rmask)08x) << %(rot1)d) | (t >> %(rot2)d) -""" % dict( +''' % dict( idx=idx, is1 = source1, is2=source2, it=target, src1=VNAMES[source1], src2=VNAMES[source2], @@ -129,22 +129,22 @@ def salsa20(input): rot2=32-rotate, )) - write("""\ + write('''\ i += 1 -""") +''') for idx in range(16): write(PAD + "b%d = (b%d + v%d) & 0xffffffff\n" % (idx,idx,idx)) - write("""\ + write('''\ return %(TLIST)s #================================================================= # eof #================================================================= -""" % kwds) +''' % kwds) if __name__ == "__main__": main() diff --git a/passlib/utils/scrypt/_salsa.py b/passlib/crypto/scrypt/_salsa.py index 9fb8612..9112732 100644 --- a/passlib/utils/scrypt/_salsa.py +++ b/passlib/crypto/scrypt/_salsa.py @@ -1,4 +1,4 @@ -"""passlib.utils._salsa - salsa 20/8 core, autogenerated by _gen_salsa.py""" +"""passlib.utils.scrypt._salsa - salsa 20/8 core, autogenerated by _gen_salsa.py""" #================================================================= # salsa function #================================================================= diff --git a/passlib/handlers/scrypt.py b/passlib/handlers/scrypt.py new file mode 100644 index 0000000..5e729d7 --- /dev/null +++ b/passlib/handlers/scrypt.py @@ -0,0 +1,268 @@ +"""passlib.handlers.scrypt -- scrypt password hash""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement, absolute_import +# core +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.crypto import scrypt as _scrypt +from passlib.utils import ab64_decode, ab64_encode, to_bytes, classproperty +from passlib.utils.compat import int_types, u, uascii_to_str +import passlib.utils.handlers as uh +# local +__all__ = [ + "scrypt", +] + +#============================================================================= +# handler +#============================================================================= +class scrypt(uh.HasRounds, uh.HasRawChecksum, uh.HasRawSalt, uh.GenericHandler): + """This class implements an SCrypt-based password [#scrypt-home]_ hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, a variable number of rounds, + as well as some custom tuning parameters unique to scrypt (see below). + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If specified, the length must be between 0-1024 bytes. + If not specified, one will be auto-generated (this is recommended). + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 16 bytes, but can be any value between 0 and 1024. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 16, but must be within ``range(1,32)``. + + .. warning:: + + Unlike many hash algorithms, increasing the rounds value + will increase both the time *and memory* required to hash a password. + + :type block_size: int + :param block_size: + Optional block size to pass to scrypt hash function (the ``r`` parameter). + Useful for tuning scrypt to optimal performance for your CPU architecture. + Defaults to 8. + + :type parallel_count: int + :param parallel_count: + Optional parallel_count to pass to scrypt hash function (the ``p`` parameter). + Defaults to 1. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. note:: + + The underlying scrypt hash function has a number of limitations + on it's parameter values, which forbids certain combinations of settings. + The requirements are: + + * ``linear_rounds = 2**<some positive integer>`` + * ``linear_rounds < 2**(16 * block_size)`` + * ``block_size * parallel_count <= 2**30-1`` + + .. todo:: + + This class currently does not support configuring default values + for ``block_size`` or ``parallel_count`` via a :class:`~passlib.context.CryptContext` + configuration. + """ + + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "scrypt" + ident = u("$scrypt$") + + # NOTE: scrypt supports arbitrary output sizes. since it's output runs through + # pbkdf2-hmac-sha256 before returning, and this could be raised eventually... + # but a 256-bit digest is more than sufficient for password hashing. + checksum_size = 32 + setting_kwds = ("salt", "salt_size", "rounds") + + #--HasSalt-- + default_salt_size = 16 + min_salt_size = 0 + max_salt_size = 1024 + + #--HasRounds-- + # TODO: would like to dynamically pick this based on system + default_rounds = 16 + min_rounds = 1 + max_rounds = 31 # limited by scrypt alg + rounds_cost = "log2" + + # TODO: make default block size & parallel count configurable via using(), + # and deprecatable via .needs_update() + + #=================================================================== + # instance attrs + #=================================================================== + + block_size = 8 + + parallel_count = 1 + + #=================================================================== + # formatting + #=================================================================== + + # format: + # $scrypt$<nExp>,<r>,<p>$<salt>[$<checksum>] + # nExp, r, p -- decimal-encoded positive integer, no zero-padding + # nExp -- log cost setting + # r -- block size setting (usually 8) + # p -- parallel_count setting (usually 1) + # salt, checksum -- ab64 encoded + + @classmethod + def from_string(cls, hash): + settings, salt, chk = uh.parse_mc3_long(hash, cls.ident, handler=cls) + parts = settings.split(",") + if len(parts) == 3: + nexp, r, p = parts + else: + raise uh.exc.MalformedHashError(cls, "malformed settings field") + salt = ab64_decode(salt.encode("ascii")) + if chk: + chk = ab64_decode(chk.encode("ascii")) + return cls(rounds=uh.parse_int(nexp, param="rounds", handler=cls), + block_size=uh.parse_int(r, param="block_size", handler=cls), + parallel_count=uh.parse_int(p, param="parallel_count", handler=cls), + salt=salt, + checksum=chk) + + def to_string(self, withchk=True): + tail = ab64_encode(self.salt).decode("ascii") + if withchk and self.checksum: + tail = u("%s$%s") % (tail, ab64_encode(self.checksum).decode("ascii")) + hash = u("%s%d,%d,%d$%s") % (self.ident, self.rounds, self.block_size, + self.parallel_count, tail) + return uascii_to_str(hash) + + #=================================================================== + # init + #=================================================================== + def __init__(self, block_size=None, parallel_count=None, **kwds): + super(scrypt, self).__init__(**kwds) + self.block_size = self._norm_block_size(block_size) + self.parallel_count = self._norm_parallel_count(parallel_count) + _scrypt.validate(self.linear_rounds, self.block_size, self.parallel_count) + + @property + def linear_rounds(self): + return 1 << self.rounds + + def _norm_block_size(self, block_size): + return self._norm_integer(block_size, self.block_size, "block_size") + + def _norm_parallel_count(self, parallel_count): + return self._norm_integer(parallel_count, self.parallel_count, "parallel_count") + + # XXX: this might be generally useful, could move to utils.handlers... + def _norm_integer(self, value, default, param, min=1, max=None): + """ + helper to normalize and validate an integer value + + :arg value: value provided to constructor + :arg default: default value if none provided. if set to ``None``, value is required. + :arg param: name of parameter (xxx: move to first arg?) + :param min: minimum value (defaults to 1) + :param max: maximum value (default ``None`` means no maximum) + :returns: validated value + """ + # fill in default + if value is None: + if not self.use_defaults: + raise TypeError("no %s specified" % param) + if default is None: + raise TypeError("%s %s value must be specified explicitly" % (self.name, param)) + value = default + + # check type + if not isinstance(value, int_types): + raise uh.exc.ExpectedTypeError(value, "integer", param) + + # check min bound + if value < min: + msg = "%s too low (%s requires %s >= %d)" % (param, self.name, param, min) + if self.relaxed: + warn(msg, uh.exc.PasslibHashWarning) + value = min + else: + raise ValueError(msg) + + # check max bound + if max and value > max: + msg = "%s too high (%s requires %s <= %d)" % (param, self.name, param, max) + if self.relaxed: + warn(msg, uh.exc.PasslibHashWarning) + value = max + else: + raise ValueError(msg) + + return value + + #=================================================================== + # backend configuration + # NOTE: this following HasManyBackends' API, but provides it's own implementation, + # which actually switches the backend that 'passlib.crypto.scrypt.scrypt()' uses. + #=================================================================== + + @classproperty + def backends(cls): + return _scrypt.backend_values + + @classproperty + def backend(cls): + return _scrypt.backend + + @classmethod + def get_backend(cls): + return cls.backend + + @classmethod + def has_backend(cls, name="any"): + if name == "any" or name == "default": + return True + else: + return _scrypt._load_backend(name) is not None + + @classmethod + def set_backend(cls, name="any"): + if name != "any": + _scrypt._set_backend(name) + + #=================================================================== + # calc checksum + #=================================================================== + def _calc_checksum(self, secret): + secret = to_bytes(secret, param="secret") + return _scrypt.scrypt(secret, self.salt, n=self.linear_rounds, r=self.block_size, + p=self.parallel_count, keylen=self.checksum_size) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/passlib/registry.py b/passlib/registry.py index 9d22e35..3ac2dc4 100644 --- a/passlib/registry.py +++ b/passlib/registry.py @@ -146,6 +146,7 @@ _locations = dict( postgres_md5 = "passlib.handlers.postgres", roundup_plaintext = "passlib.handlers.roundup", scram = "passlib.handlers.scram", + scrypt = "passlib.handlers.scrypt", sha1_crypt = "passlib.handlers.sha1_crypt", sha256_crypt = "passlib.handlers.sha2_crypt", sha512_crypt = "passlib.handlers.sha2_crypt", diff --git a/passlib/tests/test_crypto_scrypt.py b/passlib/tests/test_crypto_scrypt.py new file mode 100644 index 0000000..e42fa75 --- /dev/null +++ b/passlib/tests/test_crypto_scrypt.py @@ -0,0 +1,597 @@ +"""tests for passlib.utils.scrypt""" +#============================================================================= +# imports +#============================================================================= +# core +from binascii import hexlify +import hashlib +import logging; log = logging.getLogger(__name__) +import random +import struct +import warnings +# site +# pkg +from passlib import exc +from passlib.utils import classproperty, getrandbytes +from passlib.utils.compat import PYPY, u, bascii_to_str +from passlib.tests.utils import TestCase, skipUnless, TEST_MODE +from passlib.tests.test_crypto_digest import hb +# subject +from passlib.crypto import scrypt as scrypt_mod +# local +__all__ = [ + "ScryptEngineTest", + "BuiltinScryptTest", + "FastScryptTest", +] + +#============================================================================= +# support functions +#============================================================================= +def hexstr(data): + """return bytes as hex str""" + return bascii_to_str(hexlify(data)) + +def unpack_int4_list(data, check_count=None): + """unpack bytes as list of uint4 values""" + count = len(data) // 4 + assert check_count is None or check_count == count + return struct.unpack("<%dI" % count, data) + +def seed_bytes(seed, count): + """ + generate random reference bytes from specified seed. + used to generate some predictable test vectors. + """ + if hasattr(seed, "encode"): + seed = seed.encode("ascii") + buf = b'' + i = 0 + while len(buf) < count: + buf += hashlib.sha256(seed + struct.pack("<I", i)).digest() + i += 1 + return buf[:count] + +#============================================================================= +# test builtin engine's internals +#============================================================================= +class ScryptEngineTest(TestCase): + descriptionPrefix = "passlib.crypto.scrypt._builtin" + + def test_smix(self): + """smix()""" + from passlib.crypto.scrypt._builtin import ScryptEngine + + #----------------------------------------------------------------------- + # test vector from (expired) scrypt rfc draft + # (https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01, section 9) + #----------------------------------------------------------------------- + + input = hb(""" + f7 ce 0b 65 3d 2d 72 a4 10 8c f5 ab e9 12 ff dd + 77 76 16 db bb 27 a7 0e 82 04 f3 ae 2d 0f 6f ad + 89 f6 8f 48 11 d1 e8 7b cc 3b d7 40 0a 9f fd 29 + 09 4f 01 84 63 95 74 f3 9a e5 a1 31 52 17 bc d7 + 89 49 91 44 72 13 bb 22 6c 25 b5 4d a8 63 70 fb + cd 98 43 80 37 46 66 bb 8f fc b5 bf 40 c2 54 b0 + 67 d2 7c 51 ce 4a d5 fe d8 29 c9 0b 50 5a 57 1b + 7f 4d 1c ad 6a 52 3c da 77 0e 67 bc ea af 7e 89 + """) + + output = hb(""" + 79 cc c1 93 62 9d eb ca 04 7f 0b 70 60 4b f6 b6 + 2c e3 dd 4a 96 26 e3 55 fa fc 61 98 e6 ea 2b 46 + d5 84 13 67 3b 99 b0 29 d6 65 c3 57 60 1f b4 26 + a0 b2 f4 bb a2 00 ee 9f 0a 43 d1 9b 57 1a 9c 71 + ef 11 42 e6 5d 5a 26 6f dd ca 83 2c e5 9f aa 7c + ac 0b 9c f1 be 2b ff ca 30 0d 01 ee 38 76 19 c4 + ae 12 fd 44 38 f2 03 a0 e4 e1 c4 7e c3 14 86 1f + 4e 90 87 cb 33 39 6a 68 73 e8 f9 d2 53 9a 4b 8e + """) + + # NOTE: p value should be ignored, so testing w/ random inputs. + engine = ScryptEngine(n=16, r=1, p=random.randint(1, 1023)) + self.assertEqual(engine.smix(input), output) + + def test_bmix(self): + """bmix()""" + from passlib.crypto.scrypt._builtin import ScryptEngine + + # NOTE: bmix() call signature currently takes in list of 32*r uint4 elements, + # and writes to target buffer of same size. + + def check_bmix(r, input, output): + """helper to check bmix() output against reference""" + # NOTE: * n & p values should be ignored, so testing w/ random inputs. + # * target buffer contents should be ignored, so testing w/ random inputs. + engine = ScryptEngine(r=r, n=1 << random.randint(1, 32), p=random.randint(1, 1023)) + target = [random.randint(0, 1 << 32) for _ in range((2 * r) * 16)] + engine.bmix(input, target) + self.assertEqual(target, list(output)) + + # ScryptEngine special-cases bmix() for r=1. + # this removes the special case patching, so we also test original bmix function. + if r == 1: + del engine.bmix + target = [random.randint(0, 1 << 32) for _ in range((2 * r) * 16)] + engine.bmix(input, target) + self.assertEqual(target, list(output)) + + #----------------------------------------------------------------------- + # test vector from (expired) scrypt rfc draft + # (https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01, section 8) + #----------------------------------------------------------------------- + + # NOTE: this pair corresponds to the first input & output pair + # from the test vector in test_smix(), above. + # NOTE: original reference lists input & output as two separate 64 byte blocks. + # current internal representation used by bmix() uses single 2*r*16 array of uint4, + # combining all the B blocks into a single flat array. + input = unpack_int4_list(hb(""" + f7 ce 0b 65 3d 2d 72 a4 10 8c f5 ab e9 12 ff dd + 77 76 16 db bb 27 a7 0e 82 04 f3 ae 2d 0f 6f ad + 89 f6 8f 48 11 d1 e8 7b cc 3b d7 40 0a 9f fd 29 + 09 4f 01 84 63 95 74 f3 9a e5 a1 31 52 17 bc d7 + + 89 49 91 44 72 13 bb 22 6c 25 b5 4d a8 63 70 fb + cd 98 43 80 37 46 66 bb 8f fc b5 bf 40 c2 54 b0 + 67 d2 7c 51 ce 4a d5 fe d8 29 c9 0b 50 5a 57 1b + 7f 4d 1c ad 6a 52 3c da 77 0e 67 bc ea af 7e 89 + """), 32) + + output = unpack_int4_list(hb(""" + a4 1f 85 9c 66 08 cc 99 3b 81 ca cb 02 0c ef 05 + 04 4b 21 81 a2 fd 33 7d fd 7b 1c 63 96 68 2f 29 + b4 39 31 68 e3 c9 e6 bc fe 6b c5 b7 a0 6d 96 ba + e4 24 cc 10 2c 91 74 5c 24 ad 67 3d c7 61 8f 81 + + 20 ed c9 75 32 38 81 a8 05 40 f6 4c 16 2d cd 3c + 21 07 7c fe 5f 8d 5f e2 b1 a4 16 8f 95 36 78 b7 + 7d 3b 3d 80 3b 60 e4 ab 92 09 96 e5 9b 4d 53 b6 + 5d 2a 22 58 77 d5 ed f5 84 2c b9 f1 4e ef e4 25 + """), 32) + +# check_bmix(1, input, output) + + #----------------------------------------------------------------------- + # custom test vector for r=2 + # used to check for bmix() breakage while optimizing implementation. + #----------------------------------------------------------------------- + + r = 2 + input = unpack_int4_list(seed_bytes("bmix with r=2", 128 * r)) + + output = unpack_int4_list(hb(""" + ba240854954f4585f3d0573321f10beee96f12acdc1feb498131e40512934fd7 + 43e8139c17d0743c89d09ac8c3582c273c60ab85db63e410d049a9e17a42c6a1 + + 6c7831b11bf370266afdaff997ae1286920dea1dedf0f4a1795ba710ba9017f1 + a374400766f13ebd8969362de2d153965e9941bdde0768fa5b53e8522f116ce0 + + d14774afb88f46cd919cba4bc64af7fca0ecb8732d1fc2191e0d7d1b6475cb2e + e3db789ee478d056c4eb6c6e28b99043602dbb8dfb60c6e048bf90719da8d57d + + 3c42250e40ab79a1ada6aae9299b9790f767f54f388d024a1465b30cbbe9eb89 + 002d4f5c215c4259fac4d083bac5fb0b47463747d568f40bb7fa87c42f0a1dc1 + """), 32 * r) + + check_bmix(r, input, output) + + #----------------------------------------------------------------------- + # custom test vector for r=3 + # used to check for bmix() breakage while optimizing implementation. + #----------------------------------------------------------------------- + + r = 3 + input = unpack_int4_list(seed_bytes("bmix with r=3", 128 * r)) + + output = unpack_int4_list(hb(""" + 11ddd8cf60c61f59a6e5b128239bdc77b464101312c88bd1ccf6be6e75461b29 + 7370d4770c904d0b09c402573cf409bf2db47b91ba87d5a3de469df8fb7a003c + + 95a66af96dbdd88beddc8df51a2f72a6f588d67e7926e9c2b676c875da13161e + b6262adac39e6b3003e9a6fbc8c1a6ecf1e227c03bc0af3e5f8736c339b14f84 + + c7ae5b89f5e16d0faf8983551165f4bb712d97e4f81426e6b78eb63892d3ff54 + 80bf406c98e479496d0f76d23d728e67d2a3d2cdbc4a932be6db36dc37c60209 + + a5ca76ca2d2979f995f73fe8182eefa1ce0ba0d4fc27d5b827cb8e67edd6552f + 00a5b3ab6b371bd985a158e728011314eb77f32ade619b3162d7b5078a19886c + + 06f12bc8ae8afa46489e5b0239954d5216967c928982984101e4a88bae1f60ae + 3f8a456e169a8a1c7450e7955b8a13a202382ae19d41ce8ef8b6a15eeef569a7 + + 20f54c48e44cb5543dda032c1a50d5ddf2919030624978704eb8db0290052a1f + 5d88989b0ef931b6befcc09e9d5162320e71e80b89862de7e2f0b6c67229b93f + """), 32 * r) + + check_bmix(r, input, output) + + #----------------------------------------------------------------------- + # custom test vector for r=4 + # used to check for bmix() breakage while optimizing implementation. + #----------------------------------------------------------------------- + + r = 4 + input = unpack_int4_list(seed_bytes("bmix with r=4", 128 * r)) + + output = unpack_int4_list(hb(""" + 803fcf7362702f30ef43250f20bc6b1b8925bf5c4a0f5a14bbfd90edce545997 + 3047bd81655f72588ca93f5c2f4128adaea805e0705a35e14417101fdb1c498c + + 33bec6f4e5950d66098da8469f3fe633f9a17617c0ea21275185697c0e4608f7 + e6b38b7ec71704a810424637e2c296ca30d9cbf8172a71a266e0393deccf98eb + + abc430d5f144eb0805308c38522f2973b7b6a48498851e4c762874497da76b88 + b769b471fbfc144c0e8e859b2b3f5a11f51604d268c8fd28db55dff79832741a + + 1ac0dfdaff10f0ada0d93d3b1f13062e4107c640c51df05f4110bdda15f51b53 + 3a75bfe56489a6d8463440c78fb8c0794135e38591bdc5fa6cec96a124178a4a + + d1a976e985bfe13d2b4af51bd0fc36dd4cfc3af08efe033b2323a235205dc43d + e57778a492153f9527338b3f6f5493a03d8015cd69737ee5096ad4cbe660b10f + + b75b1595ddc96e3748f5c9f61fba1ef1f0c51b6ceef8bbfcc34b46088652e6f7 + edab61521cbad6e69b77be30c9c97ea04a4af359dafc205c7878cc9a6c5d122f + + 8d77f3cbe65ab14c3c491ef94ecb3f5d2c2dd13027ea4c3606262bb3c9ce46e7 + dc424729dc75f6e8f06096c0ad8ad4d549c42f0cad9b33cb95d10fb3cadba27c + + 5f4bf0c1ac677c23ba23b64f56afc3546e62d96f96b58d7afc5029f8168cbab4 + 533fd29fc83c8d2a32b81923992e4938281334e0c3694f0ee56f8ff7df7dc4ae + """), 32 * r) + + check_bmix(r, input, output) + + def test_salsa(self): + """salsa20()""" + from passlib.crypto.scrypt._builtin import salsa20 + + # NOTE: salsa2() currently operates on lists of 16 uint4 elements, + # which is what unpack_int4_list(hb(() is for... + + #----------------------------------------------------------------------- + # test vector from (expired) scrypt rfc draft + # (https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01, section 7) + #----------------------------------------------------------------------- + + # NOTE: this pair corresponds to the first input & output pair + # from the test vector in test_bmix(), above. + + input = unpack_int4_list(hb(""" + 7e 87 9a 21 4f 3e c9 86 7c a9 40 e6 41 71 8f 26 + ba ee 55 5b 8c 61 c1 b5 0d f8 46 11 6d cd 3b 1d + ee 24 f3 19 df 9b 3d 85 14 12 1e 4b 5a c5 aa 32 + 76 02 1d 29 09 c7 48 29 ed eb c6 8d b8 b8 c2 5e + """)) + + output = unpack_int4_list(hb(""" + a4 1f 85 9c 66 08 cc 99 3b 81 ca cb 02 0c ef 05 + 04 4b 21 81 a2 fd 33 7d fd 7b 1c 63 96 68 2f 29 + b4 39 31 68 e3 c9 e6 bc fe 6b c5 b7 a0 6d 96 ba + e4 24 cc 10 2c 91 74 5c 24 ad 67 3d c7 61 8f 81 + """)) + self.assertEqual(salsa20(input), output) + + #----------------------------------------------------------------------- + # custom test vector, + # used to check for salsa20() breakage while optimizing _gen_files output. + #----------------------------------------------------------------------- + input = list(range(16)) + output = unpack_int4_list(hb(""" + f518dd4fb98883e0a87954c05cab867083bb8808552810752285a05822f56c16 + 9d4a2a0fd2142523d758c60b36411b682d53860514b871d27659042a5afa475d + """)) + self.assertEqual(salsa20(input), output) + + #============================================================================= + # eof + #============================================================================= + +#============================================================================= +# test scrypt +#============================================================================= +class _CommonScryptTest(TestCase): + """ + base class for testing various scrypt backends against same set of reference vectors. + """ + #============================================================================= + # class attrs + #============================================================================= + + @classproperty + def descriptionPrefix(cls): + return "passlib.utils.scrypt.scrypt() <%s backend>" % cls.backend + backend = None + + #============================================================================= + # setup + #============================================================================= + def setUp(self): + assert self.backend + scrypt_mod._set_backend(self.backend) + super(_CommonScryptTest, self).setUp() + + #============================================================================= + # reference vectors + #============================================================================= + + reference_vectors = [ + # entry format: (secret, salt, n, r, p, keylen, result) + + #------------------------------------------------------------------------ + # test vectors from scrypt whitepaper -- + # http://www.tarsnap.com/scrypt/scrypt.pdf, appendix b + # + # also present in (expired) scrypt rfc draft -- + # https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01, section 11 + #------------------------------------------------------------------------ + ("", "", 16, 1, 1, 64, hb(""" + 77 d6 57 62 38 65 7b 20 3b 19 ca 42 c1 8a 04 97 + f1 6b 48 44 e3 07 4a e8 df df fa 3f ed e2 14 42 + fc d0 06 9d ed 09 48 f8 32 6a 75 3a 0f c8 1f 17 + e8 d3 e0 fb 2e 0d 36 28 cf 35 e2 0c 38 d1 89 06 + """)), + + ("password", "NaCl", 1024, 8, 16, 64, hb(""" + fd ba be 1c 9d 34 72 00 78 56 e7 19 0d 01 e9 fe + 7c 6a d7 cb c8 23 78 30 e7 73 76 63 4b 37 31 62 + 2e af 30 d9 2e 22 a3 88 6f f1 09 27 9d 98 30 da + c7 27 af b9 4a 83 ee 6d 83 60 cb df a2 cc 06 40 + """)), + + # NOTE: the following are skipped for all backends unless TEST_MODE="full" + + ("pleaseletmein", "SodiumChloride", 16384, 8, 1, 64, hb(""" + 70 23 bd cb 3a fd 73 48 46 1c 06 cd 81 fd 38 eb + fd a8 fb ba 90 4f 8e 3e a9 b5 43 f6 54 5d a1 f2 + d5 43 29 55 61 3f 0f cf 62 d4 97 05 24 2a 9a f9 + e6 1e 85 dc 0d 65 1e 40 df cf 01 7b 45 57 58 87 + """)), + + # NOTE: the following are always skipped for the builtin backend, + # (just takes too long to be worth it) + + ("pleaseletmein", "SodiumChloride", 1048576, 8, 1, 64, hb(""" + 21 01 cb 9b 6a 51 1a ae ad db be 09 cf 70 f8 81 + ec 56 8d 57 4a 2f fd 4d ab e5 ee 98 20 ad aa 47 + 8e 56 fd 8f 4b a5 d0 9f fa 1c 6d 92 7c 40 f4 c3 + 37 30 40 49 e8 a9 52 fb cb f4 5c 6f a7 7a 41 a4 + """)), + ] + + def test_reference_vectors(self): + """reference vectors""" + for secret, salt, n, r, p, keylen, result in self.reference_vectors: + if n >= 1024 and TEST_MODE(max="default"): + # skip large values unless we're running full test suite + continue + if n > 16384 and self.backend == "builtin": + # skip largest vector for builtin, takes WAAY too long + # (46s under pypy, ~5m under cpython) + continue + log.debug("scrypt reference vector: %r %r n=%r r=%r p=%r", secret, salt, n, r, p) + self.assertEqual(scrypt_mod.scrypt(secret, salt, n, r, p, keylen), result) + + #============================================================================= + # fuzz testing + #============================================================================= + + _already_tested_others = None + + def test_other_backends(self): + """compare output to other backends""" + # only run once, since test is symetric. + # maybe this means it should go somewhere else? + if self._already_tested_others: + raise self.skipTest("already run under %r backend test" % self._already_tested_others) + self._already_tested_others = self.backend + + # get available backends + orig = scrypt_mod.backend + available = set(name for name in scrypt_mod.backend_values + if scrypt_mod._load_backend(name)) + scrypt_mod._set_backend(orig) + available.discard(self.backend) + if not available: + raise self.skipTest("no other backends found") + + warnings.filterwarnings("ignore", "(?i)using builtin scrypt backend", + category=exc.PasslibSecurityWarning) + + # generate some random options, + # and cross-check output + fast_builtin = PYPY + for _ in range(10): + # NOTE: keeping values low due to builtin test + secret = getrandbytes(random, random.randint(0, 64)) + salt = getrandbytes(random, random.randint(0, 64)) + n = 1<<random.randint(1, 10) + r = random.randint(1, 8) + p = random.randint(1, 3) + ks = random.randint(0, 64) + previous = None + backends = set() + for name in available: + scrypt_mod._set_backend(name) + self.assertNotIn(scrypt_mod._scrypt, backends) + backends.add(scrypt_mod._scrypt) + result = hexstr(scrypt_mod.scrypt(secret, salt, n, r, p, ks)) + if previous is not None: + self.assertEqual(result, previous, + msg="%r output differs from others %r: %r" % + (name, available, [secret, salt, n, r, p, ks])) + + #============================================================================= + # test input types + #============================================================================= + def test_backend(self): + """backend management""" + # clobber backend + scrypt_mod.backend = None + scrypt_mod._scrypt = None + self.assertRaises(TypeError, scrypt_mod.scrypt, 's', 's', 2, 2, 2, 16) + + # reload backend + scrypt_mod._set_backend(self.backend) + self.assertEqual(scrypt_mod.backend, self.backend) + scrypt_mod.scrypt('s', 's', 2, 2, 2, 16) + + # throw error for unknown backend + self.assertRaises(ValueError, scrypt_mod._set_backend, 'xxx') + self.assertEqual(scrypt_mod.backend, self.backend) + + def test_secret_param(self): + """'secret' parameter""" + + def run_scrypt(secret): + return hexstr(scrypt_mod.scrypt(secret, "salt", 2, 2, 2, 16)) + + # unicode + TEXT = u("abc\u00defg") + self.assertEqual(run_scrypt(TEXT), '05717106997bfe0da42cf4779a2f8bd8') + + # utf8 bytes + TEXT_UTF8 = b'abc\xc3\x9efg' + self.assertEqual(run_scrypt(TEXT_UTF8), '05717106997bfe0da42cf4779a2f8bd8') + + # latin1 bytes + TEXT_LATIN1 = b'abc\xdefg' + self.assertEqual(run_scrypt(TEXT_LATIN1), '770825d10eeaaeaf98e8a3c40f9f441d') + + # accept empty string + self.assertEqual(run_scrypt(""), 'ca1399e5fae5d3b9578dcd2b1faff6e2') + + # reject other types + self.assertRaises(TypeError, run_scrypt, None) + self.assertRaises(TypeError, run_scrypt, 1) + + def test_salt_param(self): + """'salt' parameter""" + + def run_scrypt(salt): + return hexstr(scrypt_mod.scrypt("secret", salt, 2, 2, 2, 16)) + + # unicode + TEXT = u("abc\u00defg") + self.assertEqual(run_scrypt(TEXT), 'a748ec0f4613929e9e5f03d1ab741d88') + + # utf8 bytes + TEXT_UTF8 = b'abc\xc3\x9efg' + self.assertEqual(run_scrypt(TEXT_UTF8), 'a748ec0f4613929e9e5f03d1ab741d88') + + # latin1 bytes + TEXT_LATIN1 = b'abc\xdefg' + self.assertEqual(run_scrypt(TEXT_LATIN1), '91d056fb76fb6e9a7d1cdfffc0a16cd1') + + # reject other types + self.assertRaises(TypeError, run_scrypt, None) + self.assertRaises(TypeError, run_scrypt, 1) + + def test_n_param(self): + """'n' (rounds) parameter""" + + def run_scrypt(n): + return hexstr(scrypt_mod.scrypt("secret", "salt", n, 2, 2, 16)) + + # must be > 1, and a power of 2 + self.assertRaises(ValueError, run_scrypt, -1) + self.assertRaises(ValueError, run_scrypt, 0) + self.assertRaises(ValueError, run_scrypt, 1) + self.assertEqual(run_scrypt(2), 'dacf2bca255e2870e6636fa8c8957a66') + self.assertRaises(ValueError, run_scrypt, 3) + self.assertRaises(ValueError, run_scrypt, 15) + self.assertEqual(run_scrypt(16), '0272b8fc72bc54b1159340ed99425233') + + def test_r_param(self): + """'r' (block size) parameter""" + def run_scrypt(r, n=2, p=2): + return hexstr(scrypt_mod.scrypt("secret", "salt", n, r, p, 16)) + + # must be > 1 + self.assertRaises(ValueError, run_scrypt, -1) + self.assertRaises(ValueError, run_scrypt, 0) + self.assertEqual(run_scrypt(1), '3d630447d9f065363b8a79b0b3670251') + self.assertEqual(run_scrypt(2), 'dacf2bca255e2870e6636fa8c8957a66') + self.assertEqual(run_scrypt(5), '114f05e985a903c27237b5578e763736') + + # reject r*p >= 2**30 + self.assertRaises(ValueError, run_scrypt, (1<<30), p=1) + self.assertRaises(ValueError, run_scrypt, (1<<30) / 2, p=2) + + def test_p_param(self): + """'p' (parallel_count) parameter""" + def run_scrypt(p, n=2, r=2): + return hexstr(scrypt_mod.scrypt("secret", "salt", n, r, p, 16)) + + # must be > 1 + self.assertRaises(ValueError, run_scrypt, -1) + self.assertRaises(ValueError, run_scrypt, 0) + self.assertEqual(run_scrypt(1), 'f2960ea8b7d48231fcec1b89b784a6fa') + self.assertEqual(run_scrypt(2), 'dacf2bca255e2870e6636fa8c8957a66') + self.assertEqual(run_scrypt(5), '848a0eeb2b3543e7f543844d6ca79782') + + # reject r*p >= 2**30 + self.assertRaises(ValueError, run_scrypt, (1<<30), r=1) + self.assertRaises(ValueError, run_scrypt, (1<<30) / 2, r=2) + + def test_keylen_param(self): + """'keylen' parameter""" + def run_scrypt(keylen): + return hexstr(scrypt_mod.scrypt("secret", "salt", 2, 2, 2, keylen)) + + # must be > 0 + self.assertRaises(ValueError, run_scrypt, -1) + self.assertEqual(run_scrypt(0), '') + self.assertEqual(run_scrypt(1), 'da') + + # pick random value + ksize = random.randint(0, 1<<10) + self.assertEqual(len(run_scrypt(ksize)), 2*ksize) # 2 hex chars per output + + # one more than upper bound + self.assertRaises(ValueError, run_scrypt, ((2**32) - 1) * 32 + 1) + + #============================================================================= + # eoc + #============================================================================= + +# NOTE: builtin version runs VERY slow (except under PyPy, where it's only 11x slower), +# so skipping under quick test mode. +@skipUnless(PYPY or TEST_MODE(min="default"), "skipped under current test mode") +class BuiltinScryptTest(_CommonScryptTest): + backend = "builtin" + + def setUp(self): + super(BuiltinScryptTest, self).setUp() + warnings.filterwarnings("ignore", "(?i)using builtin scrypt backend", + category=exc.PasslibSecurityWarning) + + def test_missing_backend(self): + """backend management -- missing backend""" + if _can_import_scrypt(): + raise self.skipTest("'scrypt' backend is present") + self.assertRaises(exc.MissingBackendError, scrypt_mod._set_backend, 'scrypt') + +def _can_import_scrypt(): + """check if scrypt package is importable""" + try: + import scrypt + except ImportError as err: + if "scrypt" in str(err): + return False + raise + return True + +@skipUnless(_can_import_scrypt(), "'scrypt' package not found") +class ScryptPackageTest(_CommonScryptTest): + backend = "scrypt" + + def test_default_backend(self): + """backend management -- default backend""" + scrypt_mod._set_backend("default") + self.assertEqual(scrypt_mod.backend, "scrypt") + +#============================================================================= +# eof +#============================================================================= diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py index 13ccfb0..d246784 100644 --- a/passlib/tests/test_handlers.py +++ b/passlib/tests/test_handlers.py @@ -1873,6 +1873,89 @@ class scram_test(HandlerCase): self.assertRaises(ValueError, vfull, 'tape', h) #============================================================================= +# scrypt hash +#============================================================================= +class _scrypt_test(HandlerCase): + handler = hash.scrypt + + known_correct_hashes = [ + # + # excepted from test vectors from scrypt whitepaper + # (http://www.tarsnap.com/scrypt/scrypt.pdf, appendix b), + # and encoded using passlib's custom format + # + + # salt=b"" + ("", "$scrypt$4,1,1$$d9ZXYjhleyA7GcpCwYoEl/FrSETjB0ro39/6P.3iFEI"), + + # salt=b"nacl" + ("password", "$scrypt$10,8,16$TmFDbA$/bq.HJ00cgB4VucZDQHp/nxq18vII3gw53N2Y0s3MWI"), + + # + # custom + # + + # simple test + ("test", '$scrypt$8,8,1$wlhLyXmP8b53bm1NKYVQqg$mTpvG8lzuuDk.DWz8HZIB6Vum6erDuUm0As5yU.VxWA'), + + # different block value + ("password", '$scrypt$8,2,1$dO6d0xoDoLT2PofQGoNQag$g/Wf2A0vhHhaJM.addK61QPBthSmYB6uVTtQzh8CM3o'), + + # different rounds + (UPASS_TABLE, '$scrypt$7,8,1$jjGmtDamdA4BQAjBeA9BSA$OiWRHhQtpDx7M/793x6UXK14AD512jg/qNm/hkWZG4M'), + + # alt encoding + (PASS_TABLE_UTF8, '$scrypt$7,8,1$jjGmtDamdA4BQAjBeA9BSA$OiWRHhQtpDx7M/793x6UXK14AD512jg/qNm/hkWZG4M'), + + # diff block & parallel counts as well + ("nacl", '$scrypt$1,4,2$yhnD.J.Tci4lZCwFgHCuVQ$fAsEWmxSHuC0cHKMwKVFPzrQukgvK09Sj.NueTSxKds') + ] + + if TEST_MODE("full"): + # add some hashes with larger rounds value. + known_correct_hashes.extend([ + # + # from scrypt whitepaper + # + + # salt=b"SodiumChloride" + ("pleaseletmein", "$scrypt$14,8,1$U29kaXVtQ2hsb3JpZGU" + "$cCO9yzr9c0hGHAbNgf046/2o.7qQT44.qbVD9lRdofI"), + + ]) + + known_malformed_hashes = [ + # missing 'p' value + '$scrypt$10,1$wvif8/4fg1Cq9V7L2dv73w$bJcLia1lyfQ1X2x0xflehwVXPzWIUQWWdnlGwfVzBeQ', + + # zero padded rounds + '$scrypt$010,1,1$wvif8/4fg1Cq9V7L2dv73w$bJcLia1lyfQ1X2x0xflehwVXPzWIUQWWdnlGwfVzBeQ', + + # rounds too low + '$scrypt$0,1,1$wvif8/4fg1Cq9V7L2dv73w$bJcLia1lyfQ1X2x0xflehwVXPzWIUQWWdnlGwfVzBeQ', + + # invalid block size + '$scrypt$10,A,1$wvif8/4fg1Cq9V7L2dv73w$bJcLia1lyfQ1X2x0xflehwVXPzWIUQWWdnlGwfVzBeQ', + + # r*p too large + '$scrypt$10,134217728,8$wvif8/4fg1Cq9V7L2dv73w$bJcLia1lyfQ1X2x0xflehwVXPzWIUQWWdnlGwfVzBeQ', + ] + + def populate_settings(self, kwds): + # builtin is still just way too slow. + if self.backend == "builtin": + kwds.setdefault("rounds", 6) + super(_scrypt_test, self).populate_settings(kwds) + + def fuzz_setting_rounds(self): + # decrease default rounds for fuzz testing to speed up volume. + return randintgauss(4, 10, 6, 1) + +# create test cases for specific backends +scrypt_scrypt_test = _scrypt_test.create_backend_case("scrypt") +scrypt_builtin_test = _scrypt_test.create_backend_case("builtin") + +#============================================================================= # (netbsd's) sha1 crypt #============================================================================= class _sha1_crypt_test(HandlerCase): diff --git a/passlib/utils/compat/__init__.py b/passlib/utils/compat/__init__.py index e4cd07e..5a8a35c 100644 --- a/passlib/utils/compat/__init__.py +++ b/passlib/utils/compat/__init__.py @@ -21,8 +21,10 @@ PY26 = sys.version_info < (2,7) #------------------------------------------------------------------------ JYTHON = sys.platform.startswith('java') -if hasattr(sys, "pypy_version_info") and sys.pypy_version_info < (2,0): - raise AssertionError("passlib requires pypy >= 2.0 (as of passlib 1.7)") +PYPY = hasattr(sys, "pypy_version_info") + +if PYPY and sys.pypy_version_info < (2,0): + raise RuntimeError("passlib requires pypy >= 2.0 (as of passlib 1.7)") #============================================================================= # common imports @@ -235,12 +237,15 @@ if PY3: def nextgetter(obj): return obj.__next__ + + izip = zip + else: irange = xrange ##lrange = range lmap = map - from itertools import imap + from itertools import imap, izip def iteritems(d): return d.iteritems() diff --git a/passlib/utils/handlers.py b/passlib/utils/handlers.py index 16575d4..a934388 100644 --- a/passlib/utils/handlers.py +++ b/passlib/utils/handlers.py @@ -188,6 +188,48 @@ def parse_mc3(hash, prefix, sep=_UDOLLAR, rounds_base=10, # return result return rounds, salt, chk or None +def parse_mc3_long(hash, prefix, sep=_UDOLLAR, handler=None): + """ + parse hash using 3-part modular crypt format, + with complex settings string instead of simple rounds. + otherwise works same as :func:`parse_mc3` + """ + # detect prefix + hash = to_unicode(hash, "ascii", "hash") + assert isinstance(prefix, unicode) + if not hash.startswith(prefix): + raise exc.InvalidHashError(handler) + + # parse 3-part hash or 2-part config string + assert isinstance(sep, unicode) + parts = hash[len(prefix):].split(sep) + if len(parts) == 3: + return parts + elif len(parts) == 2: + settings, salt = parts + return settings, salt, None + else: + raise exc.MalformedHashError(handler) + +def parse_int(source, base=10, default=None, param="value", handler=None): + """ + helper to parse an integer config field + + :arg source: unicode source string + :param base: numeric base + :param default: optional default if source is empty + :param param: name of variable, for error msgs + :param handler: handler class, for error msgs + """ + if source.startswith(_UZERO) and source != _UZERO: + raise exc.MalformedHashError(handler, "zero-padded %s field" % param) + elif source: + return int(source, base) + elif default is None: + raise exc.MalformedHashError(handler, "empty %s field" % param) + else: + return default + #============================================================================= # formatting helpers #============================================================================= @@ -640,6 +682,7 @@ class GenericHandler(MinimalHandler): return (key for key in cls.setting_kwds if key not in cls._unparsed_settings) + # XXX: make this a global function? @staticmethod def _sanitize(value, char=u("*")): """default method to obscure sensitive fields""" @@ -1836,9 +1879,6 @@ class HasManyBackends(GenericHandler): #: when no backends are available. _no_backend_suggestion = None - #: flag used by _try_alternate_backend to prevent recursion - __tab_active = None - @classmethod def get_backend(cls): """return name of currently active backend. @@ -1960,6 +2000,9 @@ class HasManyBackends(GenericHandler): raise exc.MissingBackendError("%s: backend not available: %s" % (cls.name, name)) # load backend into class + # NOTE: not overwriting _calc_checksum() directly, so that classes can provide + # common behavior in that method, + # and then invoke _calc_checksum_backend() to do the work. assert callable(calc) cls._calc_checksum_backend = calc cls._backend = name diff --git a/passlib/utils/scrypt/__init__.py b/passlib/utils/scrypt/__init__.py deleted file mode 100644 index dbe5c41..0000000 --- a/passlib/utils/scrypt/__init__.py +++ /dev/null @@ -1,332 +0,0 @@ -"""passlib.utils.scrypt - SCrypt key derivation function in pure-python. - -(c) 2011 Eli Collins <elic@assurancetechnologies.com> - -NOTICE -====== -This module is just a feasibility study, to see if it's possible -to implement SCrypt in pure Python in any meaningful way. - -The current approach uses lists of integers, allowing them to be -passed directly into salsa20 without decoding. Byte strings, array objects, -have all proved slower. - -If run as a script, this module will run a limited number of the SCrypt -test vectors... though currently any value of ``n>128`` is too slow -to be useful, and that's far too low for secure purposes. -""" -#========================================================================== -# imports -#========================================================================== -# core -from itertools import izip, chain -import operator -import struct -from warnings import warn -# pkg -from passlib.utils import BEMPTY -from passlib.utils.pbkdf2 import pbkdf2 -from passlib.utils.scrypt._salsa import salsa20 -# local -__all__ =[ - "scrypt", -] -#========================================================================== -# constants -#========================================================================== - -MAX_KEYLEN = ((1<<32)-1)*32 -MAX_RP = (1<<30) - -class ScryptCompatWarning(UserWarning): - pass - -#========================================================================== -# scrypt engine -#========================================================================== -class _ScryptEngine(object): - """helper class used to run scrypt kdf, see scrypt() for frontend""" - #================================================================= - # instance attrs - #================================================================= - - #: scrypt config - n = 0 - r = 0 - p = 0 - - #================================================================= - # init - #================================================================= - def __init__(self, n, r, p): - # validate config - if p < 1: - raise ValueError("p must be >= 1") - if r*p >= MAX_RP: - # pbkdf2 limitation - it will be requested to generate - # p*(2*r)*64 bytes worth of data from sha-256. - # pbkdf2 can do max of (2**31-1) blocks, - # and sha-256 has 64 byte block size. - raise ValueError("r*p must be < (1<<30)") - if n < 1: - raise ValueError("n must be >= 1") - n_is_log2 = not (n&(n-1)) - if not n_is_log2: - # NOTE: this is due to the way the reference scrypt integerify is - # only coded for powers of two, and doesn't have a fallback. - warn("Running scrypt with an 'N' value that's not a power of 2, " - "such values aren't supported by the reference SCrypt implementation", - ScryptCompatWarning) - - # store config - self.n = n - self.n_is_log2 = n_is_log2 - self.r = r - self.p = p - self.smix_bytes = r<<7 # num bytes in smix input - 2*r*16*4 - self.iv_bytes = self.smix_bytes * p - self.bmix_len = bmix_len = r<<5 # length of bmix block list - 32*r integers - self.bmix_half_len = r<<4 - assert struct.calcsize("I") == 4 - self.bmix_struct = struct.Struct("<" + str(bmix_len) + "I") - - # pick optimized bmix for certain cases - if r == 1: - self.bmix = self._bmix_1 - - # pick best integerify function - integerify(bmix_block) should - # take last 64 bytes of block and return a little-endian integer. - # since it's immediately converted % n, we only have to extract - # the first 32 bytes if n < 2**32 - which due to the current - # internal representation, is already unpacked as a 32-bit int. - if n <= 0xFFFFffff: - integerify = operator.itemgetter(-16) - else: - assert n <= 0xFFFFffffFFFFffff - ig1 = operator.itemgetter(-16) - ig2 = operator.itemgetter(-17) - def integerify(X): - return ig1(X) | (ig2(X)<<32) - self.integerify = integerify - - #================================================================= - # frontend - #================================================================= - def scrypt(self, secret, salt, keylen): - """run scrypt kdf for specified secret, salt, and keylen""" - # validate inputs - if keylen > MAX_KEYLEN: - raise ValueError("keylen too large") - - # stretch salt into initial byte array via pbkdf2 - iv_bytes = self.iv_bytes - input = pbkdf2(secret, salt, rounds=1, - keylen=iv_bytes, prf="hmac-sha256") - - # split initial byte array into 'p' mflen-sized chunks, - # and run each chunk through smix() to generate output chunk. - smix = self.smix - if self.p == 1: - output = smix(input) - else: - smix_bytes = self.smix_bytes - output = BEMPTY.join( - smix(input[offset:offset+smix_bytes]) - for offset in range(0, iv_bytes, smix_bytes) - ) - - # stretch final byte array into output via pbkdf2 - return pbkdf2(secret, output, rounds=1, - keylen=keylen, prf="hmac-sha256") - - #================================================================= - # smix() - #================================================================= - def smix(self, input): - """run SCrypt smix function on a single input block - - :arg input: - byte string containing input data. - interpreted as 32*r little endian 4 byte integers. - - :returns: - byte string containing output data - derived by mixing input using n & r parameters. - """ - # gather locals - bmix = self.bmix - bmix_struct = self.bmix_struct - integerify = self.integerify - n = self.n - - # parse input into 32*r integers - X = list(bmix_struct.unpack(input)) - - # starting with X, derive V s.t. V[0]=X; V[i] = bmix(X, V[i-1]); - # final X should equal bmix(X,V[n-1]) - def vgen(): - i = 0 - while i < n: - tmp = tuple(X) - yield tmp - bmix(tmp,X) - i += 1 - V = list(vgen()) - - # generate result from X & V. - gv = V.__getitem__ - i = 0 - if self.n_is_log2: - mask = n-1 - while i < n: - j = integerify(X) & mask - tmp = tuple(a^b for a,b in izip(X, gv(j))) - bmix(tmp,X) - i += 1 - else: - while i < n: - j = integerify(X) % n - tmp = tuple(a^b for a,b in izip(X, gv(j))) - bmix(tmp,X) - i += 1 - - # repack tmp - return bmix_struct.pack(*X) - - #================================================================= - # bmix() - #================================================================= - def bmix(self, source, target): - """block mixing function used by smix() - uses salsa20/8 core to mix block contents. - - :arg source: - source to read from. - should be list of 32*r integers. - :arg target: - target to write to. - should be list of 32*r integers. - - source & target should NOT be same list. - """ - # Y[-1] = B[2r-1], Y[i] = hash( Y[i-1] xor B[i]) - # B' <-- (Y_0, Y_2 ... Y_{2r-2}, Y_1, Y_3 ... Y_{2r-1}) */ - half = self.bmix_half_len # 16*r out of 32*r - start of Y_1 - X = source[-16:] - siter = iter(source) - j = 0 - while j < half: - jn = j+16 - target[j:jn] = X = salsa20(a ^ b for a, b in izip(X, siter)) - target[half+j:half+jn] = X = salsa20(a ^ b - for a, b in izip(X, siter)) - j = jn - - def _bmix_1(self, source, target): - "special bmix for handling r=1 case" - B = source[16:] - target[:16] = X = salsa20(a ^ b for a, b in izip(B, iter(source))) - target[16:] = salsa20(a ^ b for a, b in izip(X, B)) - - #================================================================= - # eoc - #================================================================= - -def scrypt(secret, salt, n, r, p, keylen): - """run SCrypt key derivation function using specified parameters. - - :arg secret: passphrase as bytes - :arg salt: salt as bytes - :arg n: integer 'N' parameter - :arg r: integer 'r' parameter - :arg p: integer 'p' parameter - :arg keylen: number of bytes of key to generate - - :returns: a *keylen*-sized bytes instance - - :raises ValueError: - If any of the following constraints are false: - - * ``r*p<2**30`` - due to a limitation of PBKDF2-HMAC-SHA256. - * ``keylen < (2**32-1)*32`` - due to a limitation of PBKDF2-HMAC-SHA256. - * ``n`` must a be a power of 2 - for compatibility with - the reference SCrypt implementation, which omits support for other - values of ``n``. - """ - if not isinstance(secret, bytes): - raise TypeError("secret must be bytes, not %s" % (type(secret),)) - if not isinstance(salt, bytes): - raise TypeError("salt must be bytes, not %s" % (type(salt),)) - engine = _ScryptEngine(n,r,p) - return engine.scrypt(secret, salt, keylen) - -#========================================================================== -# tests -#========================================================================== -import re -from binascii import unhexlify, hexlify - -def uh(value): - return unhexlify(re.sub(r"[\s:]","", value)) - -def test1(): -# return scrypt("","",1<<9,8,1,64) - - assert scrypt("", "", 16, 1, 1, 64) == uh(""" - 77 d6 57 62 38 65 7b 20 3b 19 ca 42 c1 8a 04 97 - f1 6b 48 44 e3 07 4a e8 df df fa 3f ed e2 14 42 - fc d0 06 9d ed 09 48 f8 32 6a 75 3a 0f c8 1f 17 - e8 d3 e0 fb 2e 0d 36 28 cf 35 e2 0c 38 d1 89 06 - """) - - assert scrypt("password", "NaCl", 1024, 8, 16, 64) == uh(""" - fd ba be 1c 9d 34 72 00 78 56 e7 19 0d 01 e9 fe - 7c 6a d7 cb c8 23 78 30 e7 73 76 63 4b 37 31 62 - 2e af 30 d9 2e 22 a3 88 6f f1 09 27 9d 98 30 da - c7 27 af b9 4a 83 ee 6d 83 60 cb df a2 cc 06 40 - """) - -def test_reference_vectors(): - # run quick test on salsa bit. - assert struct.pack("<16I",*salsa20(range(16))) == \ - uh('f518dd4fb98883e0a87954c05cab867083bb8808552810752285a05822f56c16' - '9d4a2a0fd2142523d758c60b36411b682d53860514b871d27659042a5afa475d') - - # test vectors from scrypt whitepaper - - # http://www.tarsnap.com/scrypt/scrypt.pdf - assert scrypt("", "", 16, 1, 1, 64) == uh(""" - 77 d6 57 62 38 65 7b 20 3b 19 ca 42 c1 8a 04 97 - f1 6b 48 44 e3 07 4a e8 df df fa 3f ed e2 14 42 - fc d0 06 9d ed 09 48 f8 32 6a 75 3a 0f c8 1f 17 - e8 d3 e0 fb 2e 0d 36 28 cf 35 e2 0c 38 d1 89 06 - """) - - assert scrypt("password", "NaCl", 1024, 8, 16, 64) == uh(""" - fd ba be 1c 9d 34 72 00 78 56 e7 19 0d 01 e9 fe - 7c 6a d7 cb c8 23 78 30 e7 73 76 63 4b 37 31 62 - 2e af 30 d9 2e 22 a3 88 6f f1 09 27 9d 98 30 da - c7 27 af b9 4a 83 ee 6d 83 60 cb df a2 cc 06 40 - """) - - assert scrypt("pleaseletmein", "SodiumChloride", 16384, 8, 1, 64) == uh(""" - 70 23 bd cb 3a fd 73 48 46 1c 06 cd 81 fd 38 eb - fd a8 fb ba 90 4f 8e 3e a9 b5 43 f6 54 5d a1 f2 - d5 43 29 55 61 3f 0f cf 62 d4 97 05 24 2a 9a f9 - e6 1e 85 dc 0d 65 1e 40 df cf 01 7b 45 57 58 87 - """) - return - - assert scrypt("pleaseletmein", "SodiumChloride", 1048576, 8,1,64) == uh(""" - 21 01 cb 9b 6a 51 1a ae ad db be 09 cf 70 f8 81 - ec 56 8d 57 4a 2f fd 4d ab e5 ee 98 20 ad aa 47 - 8e 56 fd 8f 4b a5 d0 9f fa 1c 6d 92 7c 40 f4 c3 - 37 30 40 49 e8 a9 52 fb cb f4 5c 6f a7 7a 41 a4 - """) - -if __name__ == "__main__": - test_reference_vectors() - print "tests passed" - -#========================================================================== -# eof -#========================================================================== @@ -204,6 +204,29 @@ commands = nosetests {posargs:--randomize passlib.tests} #=========================================================================== +# scrypt backend testing +# NOTE: 'scrypt' package is currently not compatible w/ PYPY, +# or we'd add it to normal set of tests, +# and use this to test our reference implementation instead. +#=========================================================================== + +[testenv:scrypt-scrypt-py2] +basepython = python2 +deps = + {[testenv:py27]_mindeps} + scrypt +commands = + nosetests {posargs:--randomize passlib.tests.test_handlers:scrypt_scrypt_test} + +[testenv:scrypt-scrypt-py3] +basepython = python3 +deps = + {[testenv]_mindeps} + scrypt +commands = + nosetests {posargs:--randomize passlib.tests.test_handlers:scrypt_scrypt_test} + +#=========================================================================== # Django integration testing # # currently supports Django 1.8+ |
