summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2016-06-10 20:21:32 -0400
committerEli Collins <elic@assurancetechnologies.com>2016-06-10 20:21:32 -0400
commit83280f2468e6119bdeca4cb4a90712496985254d (patch)
treedfe2a940f286e5f95d81816dcd187818cf6225c0
parent1a34ab41c924c8e5a9de0f28b3ae15b42521102e (diff)
downloadpasslib-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--CHANGES3
-rw-r--r--docs/install.rst5
-rw-r--r--docs/lib/passlib.hash.rst1
-rw-r--r--docs/lib/passlib.hash.scrypt.rst120
-rw-r--r--docs/new_app_quickstart.rst18
-rw-r--r--passlib/crypto/scrypt/__init__.py193
-rw-r--r--passlib/crypto/scrypt/_builtin.py244
-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.py268
-rw-r--r--passlib/registry.py1
-rw-r--r--passlib/tests/test_crypto_scrypt.py597
-rw-r--r--passlib/tests/test_handlers.py83
-rw-r--r--passlib/utils/compat/__init__.py11
-rw-r--r--passlib/utils/handlers.py49
-rw-r--r--passlib/utils/scrypt/__init__.py332
-rw-r--r--tox.ini23
17 files changed, 1608 insertions, 362 deletions
diff --git a/CHANGES b/CHANGES
index f78ce89..6dcb457 100644
--- a/CHANGES
+++ b/CHANGES
@@ -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
-#==========================================================================
diff --git a/tox.ini b/tox.ini
index a67710a..0f2f45d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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+