summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2013-12-26 10:50:07 -0500
committerEli Collins <elic@assurancetechnologies.com>2013-12-26 10:50:07 -0500
commit0db4ea7949997265fcaae406a26070af86d4bb65 (patch)
treed2ddbef22b700f7e97075c15e9e94b700732955e
parent1fe99e524b5d6120b2994b94c2ed9ef2cb3437ad (diff)
downloadpasslib-0db4ea7949997265fcaae406a26070af86d4bb65.tar.gz
added passlib.hash.bcrypt_sha256
* not too much trouble, and definitely needed. after considering options, decided to use sha256 + base64. * added note re: bcrypt password truncation * HasBackend mixin -- changed to use _calc_checksum_backend() as the attribute it patches, instead of _calc_checksum(). makes it easier to consolidate code common to all backends (e.g. bcrypt) * test_60_secret_size: changed hardcoded exception list to a class flag * added registry test to make sure all hashes are being tested (with a few known exceptions) * clarified names inside builtin bcrypt backend * updated changelog
-rw-r--r--CHANGES15
-rw-r--r--docs/lib/passlib.hash.bcrypt.rst15
-rw-r--r--docs/lib/passlib.hash.bcrypt_sha256.rst71
-rw-r--r--docs/lib/passlib.hash.rst3
-rw-r--r--docs/modular_crypt_format.rst3
-rw-r--r--passlib/handlers/bcrypt.py125
-rw-r--r--passlib/registry.py1
-rw-r--r--passlib/tests/test_handlers_bcrypt.py108
-rw-r--r--passlib/tests/test_handlers_django.py2
-rw-r--r--passlib/tests/test_registry.py11
-rw-r--r--passlib/tests/utils.py12
-rw-r--r--passlib/utils/_blowfish/__init__.py12
-rw-r--r--passlib/utils/_blowfish/base.py6
-rw-r--r--passlib/utils/handlers.py12
14 files changed, 355 insertions, 41 deletions
diff --git a/CHANGES b/CHANGES
index ee4eca4..ec461e5 100644
--- a/CHANGES
+++ b/CHANGES
@@ -7,14 +7,21 @@ Release History
**1.6.2** (NOT YET RELEASED)
============================
- * Updated the :attr:`~passlib.ifc.PasswordHash.default_rounds` values for all of the hashes.
+ Minor changes & compatibility fixes
- * *BCrypt*: Added support for the `bcrypt <https://pypi.python.org/pypi/bcrypt>`_
+ * Re-tuned the :attr:`~passlib.ifc.PasswordHash.default_rounds` values for all of the hashes.
+
+ * *New hash:* Added the :doc:`bcrypt_sha256 <lib/passlib.hash.bcrypt_sha256>` hash,
+ which wraps BCrypt using SHA256 in order to work around it's password size limitations
+ (:issue:`43`).
+
+ * :doc:`passlib.hash.bcrypt <lib/passlib.hash.bcrypt>`:
+ Added support for the `bcrypt <https://pypi.python.org/pypi/bcrypt>`_
library as of the possible bcrypt backends that will be used if available.
(:issue:`49`)
- * *Django*: Passlib's Django extension (:mod:`passlib.ext.django`),
- and it's related hashes and unittests, have been updated to handle
+ * :mod:`passlib.ext.django`: Passlib's Django extension
+ (and it's related hashes and unittests) have been updated to handle
some minor API changes in Django 1.5-1.6. They should now be compatible with Django 1.2 and up.
(:issue:`50`)
diff --git a/docs/lib/passlib.hash.bcrypt.rst b/docs/lib/passlib.hash.bcrypt.rst
index b94dfa8..eafe7ac 100644
--- a/docs/lib/passlib.hash.bcrypt.rst
+++ b/docs/lib/passlib.hash.bcrypt.rst
@@ -93,6 +93,21 @@ While BCrypt's basic algorithm is described in it's design document [#f1]_,
the OpenBSD implementation [#f2]_ is considered the canonical reference, even
though it differs from the design document in a few small ways.
+Security Issues
+===============
+
+.. _bcrypt-password-truncation:
+
+* Password Truncation.
+
+ While not a security issue per-se, bcrypt does have one major limitation:
+ password are truncated on the first NULL byte (if any),
+ and only the first 72 bytes of a password are hashed... all the rest are ignored.
+ Furthermore, bytes 55-72 are not fully mixed into the resulting hash (citation needed!).
+ To work around both these issues, many applications first run the password through a message
+ digest such as SHA2-256. Passlib offers the premade :doc:`passlib.hash.bcrypt_sha256`
+ to take care of this issue.
+
Deviations
==========
This implementation of bcrypt differs from others in a few ways:
diff --git a/docs/lib/passlib.hash.bcrypt_sha256.rst b/docs/lib/passlib.hash.bcrypt_sha256.rst
new file mode 100644
index 0000000..22420db
--- /dev/null
+++ b/docs/lib/passlib.hash.bcrypt_sha256.rst
@@ -0,0 +1,71 @@
+==================================================================
+:class:`passlib.hash.bcrypt_sha256` - BCrypt+SHA256
+==================================================================
+
+.. versionadded:: 1.6.2
+
+.. currentmodule:: passlib.hash
+
+BCrypt was developed to replace :class:`~passlib.hash.md5_crypt` for BSD systems.
+It uses a modified version of the Blowfish stream cipher.
+It does, however, truncate passwords to 72 bytes, and some other minor quirks
+(see :ref:`BCrypt Password Truncation <bcrypt-password-truncation>` for details).
+This class works around that issue by first running the password through SHA2-256.
+This class can be used directly as follows::
+
+ >>> from passlib.hash import bcrypt_sha256
+
+ >>> # generate new salt, encrypt password
+ >>> h = bcrypt_sha256.encrypt("password")
+ >>> h
+ '$bcrypt-sha256$2a,12$LrmaIX5x4TRtAwEfwJZa1.$2ehnw6LvuIUTM0iz4iz9hTxv21B6KFO'
+
+ >>> # the same, but with an explicit number of rounds
+ >>> bcrypt.encrypt("password", rounds=8)
+ '$bcrypt-sha256$2a,8$UE3dIZ.0I6XZtA/LdMrrle$Ag04/5zYu./12.OSqInXZnJ.WZoh1ua'
+
+ >>> # verify password
+ >>> bcrypt.verify("password", h)
+ True
+ >>> bcrypt.verify("wrong", h)
+ False
+
+.. note::
+
+ It is strongly recommended that you install
+ `bcrypt <https://pypi.python.org/pypi/bcrypt>`_
+ or `py-bcrypt <https://pypi.python.org/pypi/py-bcrypt>`_
+ when using this hash. See :doc:`passlib.hash.bcrypt` for more details.
+
+Interface
+=========
+.. autoclass:: bcrypt_sha256()
+
+Format
+======
+Bcrypt-SHA256 is compatible with the :ref:`modular-crypt-format`, and uses ``$bcrypt-sha256$`` as the identifying prefix
+for all it's strings.
+An example hash (of ``password``) is:
+
+ ``$bcrypt-sha256$2a,12$LrmaIX5x4TRtAwEfwJZa1.$2ehnw6LvuIUTM0iz4iz9hTxv21B6KFO``
+
+Bcrypt-SHA256 hashes have the format :samp:`$bcrypt-sha256${variant},{rounds}${salt}${checksum}`, where:
+
+* :samp:`{variant}` is the BCrypt variant in use (usually, as in this case, ``2a``).
+* :samp:`{rounds}` is a cost parameter, encoded as decimal integer,
+ which determines the number of iterations used via :samp:`{iterations}=2**{rounds}` (rounds is 12 in the example).
+* :samp:`{salt}` is a 22 character salt string, using the characters in the regexp range ``[./A-Za-z0-9]`` (``LrmaIX5x4TRtAwEfwJZa1.`` in the example).
+* :samp:`{checksum}` is a 31 character checksum, using the same characters as the salt (``2ehnw6LvuIUTM0iz4iz9hTxv21B6KFO`` in the example).
+
+Algorithm
+=========
+The algorithm this hash uses is as follows:
+
+* first the password is encoded to ``UTF-8`` if not already encoded.
+* then it's run through SHA2-256 to generate a 32 byte digest.
+* this is encoded using base64, resulting in a 44-byte result
+ (including the trailing padding ``=``). For the example ``"password"``,
+ the output from this stage would be ``"XohImNooBHFR0OVvjcYpJ3NgPQ1qq73WKhHvch0VQtg="``.
+* this base64 string is then passed on to the underlying bcrypt algorithm
+ as the new password to be hashed. See :doc:`passlib.hash.bcrypt` for details
+ on it's operation.
diff --git a/docs/lib/passlib.hash.rst b/docs/lib/passlib.hash.rst
index 906f301..8817f74 100644
--- a/docs/lib/passlib.hash.rst
+++ b/docs/lib/passlib.hash.rst
@@ -10,7 +10,7 @@ Overview
The :mod:`!passlib.hash` module contains all the password hash algorithms built into Passlib.
While each hash has it's own options and output format, they all share a common interface,
documented in detail in the :ref:`password-hash-api`. The following pages
-describe the common interface, and then describe each hash in detail
+describe the common interface, and then describe each hash in detail
(including it's format, underlying algorithm, and known security issues).
.. seealso:: :doc:`Quickstart Guide </new_app_quickstart>` -- advice on
@@ -123,6 +123,7 @@ they can be used compatibly along side other modular crypt format hashes.
:maxdepth: 1
passlib.hash.apr_md5_crypt
+ passlib.hash.bcrypt_sha256
passlib.hash.phpass
passlib.hash.pbkdf2_digest
passlib.hash.cta_pbkdf2_sha1
diff --git a/docs/modular_crypt_format.rst b/docs/modular_crypt_format.rst
index 55e9aa3..a0f0cc0 100644
--- a/docs/modular_crypt_format.rst
+++ b/docs/modular_crypt_format.rst
@@ -159,7 +159,7 @@ by the following operating systems and platforms:
**MacOS X** Darwin's native :func:`!crypt` provides limited functionality,
supporting only :class:`~passlib.hash.des_crypt` and
:class:`~passlib.hash.bsdi_crypt`. OS X uses a separate
- system for it's own password hashes.
+ system for it's own password hashes.
**Google App Engine** As of 2011-08-19, Google App Engine's :func:`!crypt`
implementation appears to match that of a typical Linux
@@ -181,6 +181,7 @@ These hashes can be found in various libraries and applications
Scheme Prefix Primary Use (if known)
=========================================== =================== ===========================
:class:`~passlib.hash.apr_md5_crypt` ``$apr1$`` Apache htdigest files
+ :class:`~passlib.hash.bcrypt_sha256` ``$bcrypt-sha256$`` Passlib-specific
:class:`~passlib.hash.phpass` ``$P$``, ``$H$`` PHPass-based applications
:class:`~passlib.hash.pbkdf2_sha1` ``$pbkdf2$`` Passlib-specific
:class:`~passlib.hash.pbkdf2_sha256` ``$pbkdf2-sha256$`` Passlib-specific
diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py
index e2fdd02..42f0eca 100644
--- a/passlib/handlers/bcrypt.py
+++ b/passlib/handlers/bcrypt.py
@@ -12,6 +12,8 @@ TODO:
#=============================================================================
from __future__ import with_statement, absolute_import
# core
+from base64 import b64encode
+from hashlib import sha256
import os
import re
import logging; log = logging.getLogger(__name__)
@@ -28,7 +30,7 @@ except ImportError: # pragma: no cover
# pkg
from passlib.exc import PasslibHashWarning
from passlib.utils import bcrypt64, safe_crypt, repeat_string, to_bytes, \
- classproperty, rng, getrandstr, test_crypt
+ classproperty, rng, getrandstr, test_crypt, to_unicode
from passlib.utils.compat import bytes, b, u, uascii_to_str, unicode, str_to_uascii
import passlib.utils.handlers as uh
@@ -271,6 +273,17 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
def _no_backends_msg(cls):
return "no bcrypt backends available - please install py-bcrypt"
+ def _calc_checksum(self, secret):
+ "common backend code"
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+ if _BNULL in secret:
+ # NOTE: especially important to forbid NULLs for bcrypt, since many
+ # backends (bcryptor, bcrypt) happily accept them, and then
+ # silently truncate the password at first NULL they encounter!
+ raise uh.exc.NullPasswordError(self)
+ return self._calc_checksum_backend(secret)
+
def _calc_checksum_os_crypt(self, secret):
config = self._get_config()
hash = safe_crypt(secret, config)
@@ -294,10 +307,6 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
# hash must be ascii bytes
# secret must be bytes
# returns bytes
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- if _BNULL in secret:
- raise uh.exc.NullPasswordError(self)
if self.ident == IDENT_2:
# bcrypt doesn't support $2$ hashes; but we can fake $2$ behavior
# using the $2a$ algorithm, by repeating the password until
@@ -320,10 +329,6 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
# bytes taken as-is; returns ascii bytes.
# py3: unicode secret encoded as utf-8 bytes,
# hash encoded as ascii bytes, returns ascii unicode.
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- if _BNULL in secret:
- raise uh.exc.NullPasswordError(self)
config = self._get_config()
hash = _bcrypt.hashpw(secret, config)
assert hash.startswith(config) and len(hash) == len(config)+31
@@ -334,13 +339,6 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
# py2: unicode secret/hash encoded as ascii bytes before use,
# bytes taken as-is; returns ascii bytes.
# py3: not supported
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- if _BNULL in secret:
- # NOTE: especially important to forbid NULLs for bcryptor,
- # since it happily accepts them, and then silently truncates
- # the password at first one it encounters :(
- raise uh.exc.NullPasswordError(self)
if self.ident == IDENT_2:
# bcryptor doesn't support $2$ hashes; but we can fake $2$ behavior
# using the $2a$ algorithm, by repeating the password until
@@ -355,10 +353,6 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
return str_to_uascii(hash[-31:])
def _calc_checksum_builtin(self, secret):
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- if _BNULL in secret:
- raise uh.exc.NullPasswordError(self)
chk = _builtin_bcrypt(secret, self.ident.strip("$"),
self.salt.encode("ascii"), self.rounds)
return chk.decode("ascii")
@@ -367,6 +361,97 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
# eoc
#===================================================================
+_UDOLLAR = u("$")
+
+class bcrypt_sha256(bcrypt):
+ """This class implements a composition of BCrypt+SHA256, and follows the :ref:`password-hash-api`.
+
+ It supports a fixed-length salt, and a variable number of rounds.
+
+ The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept
+ all the same optional keywords as the base :class:`bcrypt` hash.
+
+ .. versionadded:: 1.6.2
+ """
+ name = "bcrypt_sha256"
+
+ # this is locked at 2a for now.
+ ident_values = (IDENT_2A,)
+
+ # sample hash:
+ # $bcrypt-sha256$2a,6$/3OeRpbOf8/l6nPPRdZPp.$nRiyYqPobEZGdNRBWihQhiFDh1ws1tu
+ # $bcrypt-sha256$ -- prefix/identifier
+ # 2a -- bcrypt variant
+ # , -- field separator
+ # 6 -- bcrypt work factor
+ # $ -- section separator
+ # /3OeRpbOf8/l6nPPRdZPp. -- salt
+ # $ -- section separator
+ # nRiyYqPobEZGdNRBWihQhiFDh1ws1tu -- digest
+
+ # XXX: we can't use .ident attr due to bcrypt code using it.
+ # working around that via prefix.
+ prefix = u('$bcrypt-sha256$')
+
+ _hash_re = re.compile(r"""
+ ^
+ [$]bcrypt-sha256
+ [$](?P<variant>[a-z0-9]+)
+ ,(?P<rounds>\d{1,2})
+ [$](?P<salt>[^$]{22})
+ ([$](?P<digest>.{31}))?
+ $
+ """, re.X)
+
+ @classmethod
+ def identify(cls, hash):
+ hash = uh.to_unicode_for_identify(hash)
+ if not hash:
+ return False
+ return hash.startswith(cls.prefix)
+
+ @classmethod
+ def from_string(cls, hash):
+ hash = to_unicode(hash, "ascii", "hash")
+ if not hash.startswith(cls.prefix):
+ raise uh.exc.InvalidHashError(cls)
+ m = cls._hash_re.match(hash)
+ if not m:
+ raise uh.exc.MalformedHashError(cls)
+ rounds = m.group("rounds")
+ if rounds.startswith(uh._UZERO) and rounds != uh._UZERO:
+ raise uh.exc.ZeroPaddedRoundsError(cls)
+ return cls(ident=m.group("variant"),
+ rounds=int(rounds),
+ salt=m.group("salt"),
+ checksum=m.group("digest"),
+ )
+
+ def to_string(self):
+ hash = u("%s%s,%d$%s") % (self.prefix, self.ident.strip(_UDOLLAR),
+ self.rounds, self.salt)
+ if self.checksum:
+ hash = u("%s$%s") % (hash, self.checksum)
+ return uascii_to_str(hash)
+
+ def _calc_checksum(self, secret):
+ # NOTE: this bypasses bcrypt's _calc_checksum,
+ # so has to take care of all it's issues, such as secret encoding.
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+ # NOTE: can't use digest directly, since bcrypt stops at first NULL.
+ # NOTE: bcrypt doesn't fully mix entropy for bytes 55-72 of password
+ # (XXX: citation needed), so we don't want key to be > 55 bytes.
+ # thus, have to use base64 (44 bytes) rather than hex (64 bytes).
+ key = b64encode(sha256(secret).digest())
+ return self._calc_checksum_backend(key)
+
+ # patch set_backend so it modifies bcrypt class, not this one...
+ # else it would clobber our _calc_checksum() wrapper above.
+ @classmethod
+ def set_backend(cls, *args, **kwds):
+ return bcrypt.set_backend(*args, **kwds)
+
#=============================================================================
# eof
#=============================================================================
diff --git a/passlib/registry.py b/passlib/registry.py
index 5a8055c..938bc5e 100644
--- a/passlib/registry.py
+++ b/passlib/registry.py
@@ -82,6 +82,7 @@ _locations = dict(
apr_md5_crypt = "passlib.handlers.md5_crypt",
atlassian_pbkdf2_sha1 = "passlib.handlers.pbkdf2",
bcrypt = "passlib.handlers.bcrypt",
+ bcrypt_sha256 = "passlib.handlers.bcrypt",
bigcrypt = "passlib.handlers.des_crypt",
bsd_nthash = "passlib.handlers.windows",
bsdi_crypt = "passlib.handlers.des_crypt",
diff --git a/passlib/tests/test_handlers_bcrypt.py b/passlib/tests/test_handlers_bcrypt.py
index 70dbf91..a6027da 100644
--- a/passlib/tests/test_handlers_bcrypt.py
+++ b/passlib/tests/test_handlers_bcrypt.py
@@ -27,6 +27,7 @@ class _bcrypt_test(HandlerCase):
handler = hash.bcrypt
secret_size = 72
reduce_default_rounds = True
+ fuzz_salts_need_bcrypt_repair = True
known_correct_hashes = [
#
@@ -360,5 +361,112 @@ bcrypt_bcrypt_test, bcrypt_pybcrypt_test, bcrypt_bcryptor_test, bcrypt_os_crypt_
_bcrypt_test.create_backend_cases(["bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin"])
#=============================================================================
+# bcrypt
+#=============================================================================
+class _bcrypt_sha256_test(HandlerCase):
+ "base for BCrypt-SHA256 test cases"
+ handler = hash.bcrypt_sha256
+ reduce_default_rounds = True
+ forbidden_characters = None
+ fuzz_salts_need_bcrypt_repair = True
+ fallback_os_crypt_handler = hash.bcrypt
+
+ known_correct_hashes = [
+ #
+ # custom test vectors
+ #
+
+ # empty
+ ("",
+ '$bcrypt-sha256$2a,5$E/e/2AOhqM5W/KJTFQzLce$F6dYSxOdAEoJZO2eoHUZWZljW/e0TXO'),
+
+ # ascii
+ ("password",
+ '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'),
+
+ # unicode / utf8
+ (UPASS_TABLE,
+ '$bcrypt-sha256$2a,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'),
+ (UPASS_TABLE.encode("utf-8"),
+ '$bcrypt-sha256$2a,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'),
+
+ # test >72 chars is hashed correctly -- under bcrypt these hash the same.
+ # NOTE: test_60_secret_size() handles this already, this is just for overkill :)
+ (repeat_string("abc123",72),
+ '$bcrypt-sha256$2a,5$X1g1nh3g0v4h6970O68cxe$r/hyEtqJ0teqPEmfTLoZ83ciAI1Q74.'),
+ (repeat_string("abc123",72)+"qwr",
+ '$bcrypt-sha256$2a,5$X1g1nh3g0v4h6970O68cxe$021KLEif6epjot5yoxk0m8I0929ohEa'),
+ (repeat_string("abc123",72)+"xyz",
+ '$bcrypt-sha256$2a,5$X1g1nh3g0v4h6970O68cxe$7.1kgpHduMGEjvM3fX6e/QCvfn6OKja'),
+ ]
+
+ known_correct_configs =[
+ ('$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe',
+ "password", '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'),
+ ]
+
+ known_malformed_hashes = [
+ # bad char in otherwise correct hash
+ # \/
+ '$bcrypt-sha256$2a,5$5Hg1DKF!PE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
+
+ # unrecognized bcrypt variant
+ '$bcrypt-sha256$2c,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
+
+ # unsupported bcrypt variant
+ '$bcrypt-sha256$2x,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
+
+ # rounds zero-padded
+ '$bcrypt-sha256$2a,05$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
+
+ # config string w/ $ added
+ '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$',
+ ]
+
+ #===================================================================
+ # override some methods -- cloned from bcrypt
+ #===================================================================
+ def setUp(self):
+ # ensure builtin is enabled for duration of test.
+ if TEST_MODE("full") and self.backend == "builtin":
+ key = "PASSLIB_BUILTIN_BCRYPT"
+ orig = os.environ.get(key)
+ if orig:
+ self.addCleanup(os.environ.__setitem__, key, orig)
+ else:
+ self.addCleanup(os.environ.__delitem__, key)
+ os.environ[key] = "enabled"
+ super(_bcrypt_sha256_test, self).setUp()
+
+ def populate_settings(self, kwds):
+ # builtin is still just way too slow.
+ if self.backend == "builtin":
+ kwds.setdefault("rounds", 4)
+ super(_bcrypt_sha256_test, self).populate_settings(kwds)
+
+ #===================================================================
+ # override ident tests for now
+ #===================================================================
+ def test_30_HasManyIdents(self):
+ raise self.skipTest("multiple idents not supported")
+
+ def test_30_HasOneIdent(self):
+ # forbidding ident keyword, we only support "2a" for now
+ handler = self.handler
+ handler(use_defaults=True)
+ self.assertRaises(ValueError, handler, ident="$2y$", use_defaults=True)
+
+ #===================================================================
+ # fuzz testing -- cloned from bcrypt
+ #===================================================================
+ def fuzz_setting_rounds(self):
+ # decrease default rounds for fuzz testing to speed up volume.
+ return randintgauss(5, 8, 6, 1)
+
+# create test cases for specific backends
+bcrypt_sha256_bcrypt_test, bcrypt_sha256_pybcrypt_test, bcrypt_sha256_bcryptor_test, bcrypt_sha256_os_crypt_test, bcrypt_sha256_builtin_test = \
+ _bcrypt_sha256_test.create_backend_cases(["bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin"])
+
+#=============================================================================
# eof
#=============================================================================
diff --git a/passlib/tests/test_handlers_django.py b/passlib/tests/test_handlers_django.py
index 00a2f9b..2d516ae 100644
--- a/passlib/tests/test_handlers_django.py
+++ b/passlib/tests/test_handlers_django.py
@@ -270,6 +270,7 @@ class django_bcrypt_test(HandlerCase, _DjangoHelper):
handler = hash.django_bcrypt
secret_size = 72
min_django_version = (1,4)
+ fuzz_salts_need_bcrypt_repair = True
known_correct_hashes = [
#
@@ -306,6 +307,7 @@ class django_bcrypt_sha256_test(HandlerCase, _DjangoHelper):
handler = hash.django_bcrypt_sha256
min_django_version = (1,6)
forbidden_characters = None
+ fuzz_salts_need_bcrypt_repair = True
known_correct_hashes = [
#
diff --git a/passlib/tests/test_registry.py b/passlib/tests/test_registry.py
index 306e95a..27c5c5c 100644
--- a/passlib/tests/test_registry.py
+++ b/passlib/tests/test_registry.py
@@ -198,6 +198,17 @@ class RegistryTest(TestCase):
for name in list_crypt_handlers():
self.assertFalse(name.startswith("_"), "%r: " % name)
+ def test_handlers(self):
+ "verify we have tests for all handlers"
+ from passlib.registry import list_crypt_handlers
+ from passlib.tests.test_handlers import get_handler_case
+ for name in list_crypt_handlers():
+ if name.startswith("ldap_") and name[5:] in list_crypt_handlers():
+ continue
+ if name in ["roundup_plaintext"]:
+ continue
+ self.assertTrue(get_handler_case(name))
+
#=============================================================================
# eof
#=============================================================================
diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py
index f4dc811..b840aff 100644
--- a/passlib/tests/utils.py
+++ b/passlib/tests/utils.py
@@ -1152,10 +1152,12 @@ class HandlerCase(TestCase):
c3 = self.do_genconfig(salt=s1[:-1])
self.assertNotEqual(c3, c1)
- # XXX: make this a class-level flag
+ # whether salt should be passed through bcrypt repair function
+ fuzz_salts_need_bcrypt_repair = False
+
def prepare_salt(self, salt):
"prepare generated salt"
- if self.handler.name in ["bcrypt", "django_bcrypt", "django_bcrypt_sha256"]:
+ if self.fuzz_salts_need_bcrypt_repair:
from passlib.utils import bcrypt64
salt = bcrypt64.repair_unused(salt)
return salt
@@ -1958,13 +1960,17 @@ class OsCryptMixin(HandlerCase):
self._patch_safe_crypt()
super(OsCryptMixin, self).setUp()
+ # alternate handler to use for fake os_crypt,
+ # e.g. bcrypt_sha256 uses bcrypt
+ fallback_os_crypt_handler = None
+
def _patch_safe_crypt(self):
"""if crypt() doesn't support current hash alg, this patches
safe_crypt() so that it transparently uses another one of the handler's
backends, so that we can go ahead and test as much of code path
as possible.
"""
- handler = self.handler
+ handler = self.fallback_os_crypt_handler or self.handler
# resolve wrappers, since we want to return crypt compatible hash.
while hasattr(handler, "wrapped"):
handler = handler.wrapped
diff --git a/passlib/utils/_blowfish/__init__.py b/passlib/utils/_blowfish/__init__.py
index ef90ac0..16b8544 100644
--- a/passlib/utils/_blowfish/__init__.py
+++ b/passlib/utils/_blowfish/__init__.py
@@ -143,18 +143,20 @@ def raw_bcrypt(password, ident, salt, log_rounds):
engine = BlowfishEngine()
- # convert password & salt into list of 18 32-bit integers.
+ # convert password & salt into list of 18 32-bit integers (72 bytes total).
pass_words = engine.key_to_words(password)
salt_words = engine.key_to_words(salt)
+ # truncate salt_words to original 16 byte salt, or loop won't wrap
+ # correctly when passed to .eks_salted_expand()
+ salt_words16 = salt_words[:4]
+
# do EKS key schedule setup
- # NOTE: [:4] is due to salt being 16 bytes originally,
- # and the list needs to wrap properly
- engine.eks_expand(pass_words, salt_words[:4])
+ engine.eks_salted_expand(pass_words, salt_words16)
# apply password & salt keys to key schedule a bunch more times.
rounds = 1<<log_rounds
- engine.eks_rounds_expand0(pass_words, salt_words, rounds)
+ engine.eks_repeated_expand(pass_words, salt_words, rounds)
# encipher constant data, and encode to bytes as digest.
data = list(BCRYPT_CDATA)
diff --git a/passlib/utils/_blowfish/base.py b/passlib/utils/_blowfish/base.py
index 65b6da0..f62aca2 100644
--- a/passlib/utils/_blowfish/base.py
+++ b/passlib/utils/_blowfish/base.py
@@ -378,8 +378,8 @@ class BlowfishEngine(object):
#===================================================================
# eks-blowfish routines
#===================================================================
- def eks_expand(self, key_words, salt_words):
- "perform EKS version of Blowfish keyschedule setup"
+ def eks_salted_expand(self, key_words, salt_words):
+ "perform EKS' salted version of Blowfish keyschedule setup"
# NOTE: this is the same as expand(), except for the addition
# of the operations involving *salt_words*.
@@ -415,7 +415,7 @@ class BlowfishEngine(object):
box[i], box[i+1] = l,r = encipher(l,r) # next()
i += 2
- def eks_rounds_expand0(self, key_words, salt_words, rounds):
+ def eks_repeated_expand(self, key_words, salt_words, rounds):
"perform rounds stage of EKS keyschedule setup"
expand = self.expand
n = 0
diff --git a/passlib/utils/handlers.py b/passlib/utils/handlers.py
index 4385f3b..4d03b3b 100644
--- a/passlib/utils/handlers.py
+++ b/passlib/utils/handlers.py
@@ -1443,19 +1443,23 @@ class HasManyBackends(GenericHandler):
elif not cls.has_backend(name):
raise exc.MissingBackendError("%s backend not available: %r" %
(cls.name, name))
- cls._calc_checksum = getattr(cls, "_calc_checksum_" + name)
+ cls._calc_checksum_backend = getattr(cls, "_calc_checksum_" + name)
cls._backend = name
return name
- def _calc_checksum(self, secret):
- "stub for _calc_checksum(), default backend will be selected first time stub is called"
+ def _calc_checksum_backend(self, secret):
+ "stub for _calc_checksum_backend(), default backend will be selected first time stub is called"
# if we got here, no backend has been loaded; so load default backend
assert not self._backend, "set_backend() failed to replace lazy loader"
self.set_backend()
assert self._backend, "set_backend() failed to load a default backend"
# this should now invoke the backend-specific version, so call it again.
- return self._calc_checksum(secret)
+ return self._calc_checksum_backend(secret)
+
+ def _calc_checksum(self, secret):
+ "wrapper for backend, for common code"""
+ return self._calc_checksum_backend(secret)
#=============================================================================
# wrappers