summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2019-11-11 15:15:07 -0500
committerEli Collins <elic@assurancetechnologies.com>2019-11-11 15:15:07 -0500
commitf339799ff1c60dc57347f403e15371d934f3f060 (patch)
treec1f47e2fb59d67b7dc6289c129c14a4bad30ddd3
parent190e4a35eb29619f52dd17fec66e5638dc0b2173 (diff)
downloadpasslib-f339799ff1c60dc57347f403e15371d934f3f060.tar.gz
passlib.crypto.scrypt: add support for hashlib.scrypt() backend (fixes issue 86)
-rw-r--r--docs/history/1.7.rst5
-rw-r--r--docs/lib/passlib.hash.scrypt.rst13
-rw-r--r--passlib/crypto/scrypt/__init__.py75
-rw-r--r--passlib/tests/test_crypto_scrypt.py57
-rw-r--r--passlib/tests/test_handlers_scrypt.py1
5 files changed, 133 insertions, 18 deletions
diff --git a/docs/history/1.7.rst b/docs/history/1.7.rst
index 9b3fbbf..4b066fb 100644
--- a/docs/history/1.7.rst
+++ b/docs/history/1.7.rst
@@ -18,6 +18,11 @@ New Features
Now defaults to "ID" hashes instead of "I" hashes, but this can be overridden via ``type`` keyword.
(:issue:`101`)
+* .. py:currentmodule:: passlib.hash
+
+ :class:`scrypt`: Now uses python 3.6 stdlib's :func:`hashlib.scrypt` as backend,
+ if present (:issue:`86`).
+
Bugfixes
--------
diff --git a/docs/lib/passlib.hash.scrypt.rst b/docs/lib/passlib.hash.scrypt.rst
index 5c3677c..1d45f53 100644
--- a/docs/lib/passlib.hash.scrypt.rst
+++ b/docs/lib/passlib.hash.scrypt.rst
@@ -55,16 +55,21 @@ Scrypt Backends
This class will use the first available of two possible backends:
-1. The C-accelerated `scrypt <https://pypi.python.org/pypi/scrypt>`_ package, if installed.
-2. A pure-python implementation of SCrypt, built into Passlib.
+1. Python stdlib's :func:`hashlib.scrypt` method (only present for Python 3.6+ and OpenSSL 1.1+)
+2. The C-accelerated `scrypt <https://pypi.python.org/pypi/scrypt>`_ package, if installed.
+3. A pure-python implementation of SCrypt, built into Passlib.
.. warning::
- *It is strongly recommended to install the external scrypt package*.
-
+ If :func:`hashlib.scrypt` is not present on your system, 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.
+.. versionchanged:: 1.7.2
+
+ Added support for using stdlib's :func:`hashlib.scrypt`
+
Format & Algorithm
==================
This Scrypt hash format is compatible with the :ref:`PHC Format <phc-format>` and :ref:`modular-crypt-format`,
diff --git a/passlib/crypto/scrypt/__init__.py b/passlib/crypto/scrypt/__init__.py
index 16b9feb..c71873a 100644
--- a/passlib/crypto/scrypt/__init__.py
+++ b/passlib/crypto/scrypt/__init__.py
@@ -1,4 +1,8 @@
-"""passlib.utils.scrypt -- scrypt hash frontend and help utilities"""
+"""
+passlib.utils.scrypt -- scrypt hash frontend and help utilities
+
+XXX: add this module to public docs?
+"""
#==========================================================================
# imports
#==========================================================================
@@ -20,6 +24,13 @@ __all__ =[
# config validation
#==========================================================================
+#: internal global constant for setting stdlib scrypt's maxmem (int bytes).
+#: set to -1 to auto-calculate (see _load_stdlib_backend() below)
+#: set to 0 for openssl default (32mb according to python docs)
+#: TODO: standardize this across backends, and expose support via scrypt hash config;
+#: currently not very configurable, and only applies to stdlib backend.
+SCRYPT_MAXMEM = -1
+
#: max output length in bytes
MAX_KEYLEN = ((1 << 32) - 1) * 32
@@ -54,6 +65,33 @@ def validate(n, r, p):
return True
+
+UINT32_SIZE = 4
+
+
+def estimate_maxmem(n, r, p, fudge=1.05):
+ """
+ calculate memory required for parameter combination.
+ assumes parameters have already been validated.
+
+ .. warning::
+ this is derived from OpenSSL's scrypt maxmem formula;
+ and may not be correct for other implementations
+ (additional buffers, different parallelism tradeoffs, etc).
+ """
+ # XXX: expand to provide upper bound for diff backends, or max across all of them?
+ # NOTE: openssl's scrypt() enforces it's maxmem parameter based on calc located at
+ # <openssl/providers/default/kdfs/scrypt.c>, ending in line containing "Blen + Vlen > maxmem"
+ # using the following formula:
+ # Blen = p * 128 * r
+ # Vlen = 32 * r * (N + 2) * sizeof(uint32_t)
+ # total_bytes = Blen + Vlen
+ maxmem = r * (128 * p + 32 * (n + 2) * UINT32_SIZE)
+ # add fudge factor so we don't have off-by-one mismatch w/ openssl
+ maxmem = int(maxmem * fudge)
+ return maxmem
+
+
# TODO: configuration picker (may need psutil for full effect)
#==========================================================================
@@ -154,11 +192,44 @@ def _load_cffi_backend():
return None
+def _load_stdlib_backend():
+ """
+ Attempt to load stdlib scrypt() implement and return wrapper.
+ Returns None if not found.
+ """
+ try:
+ # new in python 3.6, if compiled with openssl >= 1.1
+ from hashlib import scrypt as stdlib_scrypt
+ except ImportError:
+ return None
+
+ def stdlib_scrypt_wrapper(secret, salt, n, r, p, keylen):
+ # work out appropriate "maxmem" parameter
+ #
+ # TODO: would like to enforce a single "maxmem" policy across all backends;
+ # and maybe expose this via scrypt hasher config.
+ #
+ # for now, since parameters should all be coming from internally-controlled sources
+ # (password hashes), using policy of "whatever memory the parameters needs".
+ # furthermore, since stdlib scrypt is only place that needs this,
+ # currently calculating exactly what maxmem needs to make things work for stdlib call.
+ # as hack, this can be overriden via SCRYPT_MAXMEM above,
+ # would like to formalize all of this.
+ maxmem = SCRYPT_MAXMEM
+ if maxmem < 0:
+ maxmem = estimate_maxmem(n, r, p)
+ return stdlib_scrypt(password=secret, salt=salt, n=n, r=r, p=p, dklen=keylen,
+ maxmem=maxmem)
+
+ return stdlib_scrypt_wrapper
+
+
#: list of potential backends
-backend_values = ("scrypt", "builtin")
+backend_values = ("stdlib", "scrypt", "builtin")
#: dict mapping backend name -> loader
_backend_loaders = dict(
+ stdlib=_load_stdlib_backend,
scrypt=_load_cffi_backend, # XXX: rename backend constant to "cffi"?
builtin=_load_builtin_backend,
)
diff --git a/passlib/tests/test_crypto_scrypt.py b/passlib/tests/test_crypto_scrypt.py
index 9666667..578a7aa 100644
--- a/passlib/tests/test_crypto_scrypt.py
+++ b/passlib/tests/test_crypto_scrypt.py
@@ -560,6 +560,36 @@ class _CommonScryptTest(TestCase):
# eoc
#=============================================================================
+
+#-----------------------------------------------------------------------
+# check what backends 'should' be available
+#-----------------------------------------------------------------------
+
+def _can_import_cffi_scrypt():
+ try:
+ import scrypt
+ except ImportError as err:
+ if "scrypt" in str(err):
+ return False
+ raise
+ return True
+
+has_cffi_scrypt = _can_import_cffi_scrypt()
+
+
+def _can_import_stdlib_scrypt():
+ try:
+ from hashlib import scrypt
+ return True
+ except ImportError:
+ return False
+
+has_stdlib_scrypt = _can_import_stdlib_scrypt()
+
+#-----------------------------------------------------------------------
+# test individual backends
+#-----------------------------------------------------------------------
+
# 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")
@@ -573,29 +603,32 @@ class BuiltinScryptTest(_CommonScryptTest):
def test_missing_backend(self):
"""backend management -- missing backend"""
- if _can_import_scrypt():
- raise self.skipTest("'scrypt' backend is present")
+ if has_stdlib_scrypt or has_cffi_scrypt:
+ raise self.skipTest("non-builtin 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")
+@skipUnless(has_cffi_scrypt, "'scrypt' package not found")
class ScryptPackageTest(_CommonScryptTest):
backend = "scrypt"
def test_default_backend(self):
"""backend management -- default backend"""
+ if has_stdlib_scrypt:
+ raise self.skipTest("higher priority backend present")
scrypt_mod._set_backend("default")
self.assertEqual(scrypt_mod.backend, "scrypt")
+
+@skipUnless(has_stdlib_scrypt, "'hashlib.scrypt()' not found")
+class StdlibScryptTest(_CommonScryptTest):
+ backend = "stdlib"
+
+ def test_default_backend(self):
+ """backend management -- default backend"""
+ scrypt_mod._set_backend("default")
+ self.assertEqual(scrypt_mod.backend, "stdlib")
+
#=============================================================================
# eof
#=============================================================================
diff --git a/passlib/tests/test_handlers_scrypt.py b/passlib/tests/test_handlers_scrypt.py
index bbd3cd7..5ab6d9f 100644
--- a/passlib/tests/test_handlers_scrypt.py
+++ b/passlib/tests/test_handlers_scrypt.py
@@ -102,6 +102,7 @@ class _scrypt_test(HandlerCase):
return self.randintgauss(4, 10, 6, 1)
# create test cases for specific backends
+scrypt_stdlib_test = _scrypt_test.create_backend_case("stdlib")
scrypt_scrypt_test = _scrypt_test.create_backend_case("scrypt")
scrypt_builtin_test = _scrypt_test.create_backend_case("builtin")