diff options
author | Eli Collins <elic@assurancetechnologies.com> | 2019-11-11 15:15:07 -0500 |
---|---|---|
committer | Eli Collins <elic@assurancetechnologies.com> | 2019-11-11 15:15:07 -0500 |
commit | f339799ff1c60dc57347f403e15371d934f3f060 (patch) | |
tree | c1f47e2fb59d67b7dc6289c129c14a4bad30ddd3 | |
parent | 190e4a35eb29619f52dd17fec66e5638dc0b2173 (diff) | |
download | passlib-f339799ff1c60dc57347f403e15371d934f3f060.tar.gz |
passlib.crypto.scrypt: add support for hashlib.scrypt() backend (fixes issue 86)
-rw-r--r-- | docs/history/1.7.rst | 5 | ||||
-rw-r--r-- | docs/lib/passlib.hash.scrypt.rst | 13 | ||||
-rw-r--r-- | passlib/crypto/scrypt/__init__.py | 75 | ||||
-rw-r--r-- | passlib/tests/test_crypto_scrypt.py | 57 | ||||
-rw-r--r-- | passlib/tests/test_handlers_scrypt.py | 1 |
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") |