summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2020-02-16 10:56:06 -0500
committerEli Collins <elic@assurancetechnologies.com>2020-02-16 10:56:06 -0500
commit2766485f5760489fedb2e73f8a162c83633bbbcb (patch)
tree7c62d5d273f5bfd1082ab46c6f436a9447c3bdd1
parentec62db38788b1dc0c8f78060f6119cb63bbacfbd (diff)
downloadpasslib-2766485f5760489fedb2e73f8a162c83633bbbcb.tar.gz
passlib.hash.bcrypt_sha256: now uses hmac-sha256 instead of plain sha256
(fixes issue 114)
-rw-r--r--docs/history/1.7.rst14
-rw-r--r--docs/lib/passlib.hash.bcrypt.rst2
-rw-r--r--docs/lib/passlib.hash.bcrypt_sha256.rst39
-rw-r--r--passlib/handlers/bcrypt.py156
-rw-r--r--passlib/tests/test_handlers_bcrypt.py134
5 files changed, 300 insertions, 45 deletions
diff --git a/docs/history/1.7.rst b/docs/history/1.7.rst
index 096f7ed..132a343 100644
--- a/docs/history/1.7.rst
+++ b/docs/history/1.7.rst
@@ -4,6 +4,20 @@
Passlib 1.7
===========
+**1.7.3** (NOT YET RELEASED)
+============================
+
+This release rolls up assorted bug & compatibility fixes since 1.7.2.
+
+Bugfixes
+--------
+
+* .. py:currentmodule:: passlib.hash
+
+ :class:`bcrypt_sha256`: Internal algorithm has been changed to use HMAC-SHA256 instead of
+ plain SHA256. This should strengthen the hash against brute-force attempts which bypass
+ the intermediary hash by using known-sha256-digest lookup tables (:issue:`114`).
+
* :func:`passlib.utils.safe_crypt`: Support :func:`crypt.crypt` unexpectedly
returning bytes under Python 3 (:issue:`113`).
diff --git a/docs/lib/passlib.hash.bcrypt.rst b/docs/lib/passlib.hash.bcrypt.rst
index 0d7319c..436148c 100644
--- a/docs/lib/passlib.hash.bcrypt.rst
+++ b/docs/lib/passlib.hash.bcrypt.rst
@@ -124,7 +124,7 @@ Security Issues
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`
+ digest such as (HMAC-) SHA2-256. Passlib offers the premade :doc:`passlib.hash.bcrypt_sha256`
to take care of this issue.
Deviations
diff --git a/docs/lib/passlib.hash.bcrypt_sha256.rst b/docs/lib/passlib.hash.bcrypt_sha256.rst
index 20ef5ab..a3035b1 100644
--- a/docs/lib/passlib.hash.bcrypt_sha256.rst
+++ b/docs/lib/passlib.hash.bcrypt_sha256.rst
@@ -10,7 +10,7 @@ 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 works around that issue by first running the password through HMAC-SHA2-256.
This class can be used directly as follows::
>>> from passlib.hash import bcrypt_sha256
@@ -18,11 +18,11 @@ This class can be used directly as follows::
>>> # generate new salt, hash password
>>> h = bcrypt_sha256.hash("password")
>>> h
- '$bcrypt-sha256$2a,12$LrmaIX5x4TRtAwEfwJZa1.$2ehnw6LvuIUTM0iz4iz9hTxv21B6KFO'
+ '$bcrypt-sha256$v=2,t=2b,r=12$n79VH.0Q2TMWmt3Oqt9uku$Kq4Noyk3094Y2QlB8NdRT8SvGiI4ft2'
>>> # the same, but with an explicit number of rounds
>>> bcrypt_sha256.using(rounds=13).hash("password")
- '$bcrypt-sha256$2b,13$Mant9jKTadXYyFh7xp1W5.$J8xpPZR/HxH7f1vRCNUjBI7Ev1al0hu'
+ '$bcrypt-sha256$v=2,t=2b,r=13$AmytCA45b12VeVg0YdDT3.$IZTbbJKgJlD5IJoCWhuDUqYjnJwNPlO'
>>> # verify password
>>> bcrypt_sha256.verify("password", h)
@@ -46,25 +46,38 @@ Bcrypt-SHA256 is compatible with the :ref:`modular-crypt-format`, and uses ``$bc
for all it's strings.
An example hash (of ``password``) is:
- ``$bcrypt-sha256$2a,12$LrmaIX5x4TRtAwEfwJZa1.$2ehnw6LvuIUTM0iz4iz9hTxv21B6KFO``
+ ``$bcrypt-sha256$v=2,t=2b,r=12$n79VH.0Q2TMWmt3Oqt9uku$Kq4Noyk3094Y2QlB8NdRT8SvGiI4ft2``
-Bcrypt-SHA256 hashes have the format :samp:`$bcrypt-sha256${variant},{rounds}${salt}${checksum}`, where:
+Version 1 of this format had the format :samp:`$bcrypt-sha256${type},{rounds}${salt}${digest}`.
+Passlib 1.7.3 introduced version 2 of this format, which changed the algorithm slightly (see below),
+and adjusted the format to indicate a version: :samp:`$bcrypt-sha256$v=2,t={type},r={rounds}${salt}${digest}`, where:
-* :samp:`{variant}` is the BCrypt variant in use (usually, as in this case, ``2a``).
+* :samp:`{type}` is the BCrypt variant in use (always ``2b`` under version 2; though ``2a`` was allowed under version 1).
* :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).
+* :samp:`{salt}` is a 22 character salt string, using the characters in the regexp range ``[./A-Za-z0-9]`` (``n79VH.0Q2TMWmt3Oqt9uku`` in the example).
+* :samp:`{digest}` is a 31 character digest, using the same characters as the salt (``Kq4Noyk3094Y2QlB8NdRT8SvGiI4ft2`` 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="``.
+
+* the next step is to hash the password before handing it off to bcrypt:
+
+ - Under version 2 of this algorithm (the default as of passlib 1.7.3), the password is run
+ through HMAC-SHA2-256, with the HMAC key set to the bcrypt salt (encoded as a 22 character ascii salt string).
+
+ - Under the older version 1 of this algorithm, the password was instead run through plain SHA2-256.
+
+ In either case, this generates a 32 byte digest.
+
+* this hash is then encoded using base64, resulting in a 44-byte result
+ (including the trailing padding ``=``). For the example ``"password"`` and the salt ``"n79VH.0Q2TMWmt3Oqt9uku"``,
+ the output from this stage would be ``b"7CwRr5rxo2JZcVmSDAi/2JPTkvkAdNy20Cz2LwYC0fw="`` (for version 2).
+
* 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.
+ on it's operation. For the example in the prior line, the resulting
+ bcrypt digest component would be ``"Kq4Noyk3094Y2QlB8NdRT8SvGiI4ft2"``.
diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py
index 817416b..73fbc21 100644
--- a/passlib/handlers/bcrypt.py
+++ b/passlib/handlers/bcrypt.py
@@ -24,6 +24,7 @@ _pybcrypt = None # dynamically imported by _load_backend_pybcrypt()
_bcryptor = None # dynamically imported by _load_backend_bcryptor()
# pkg
_builtin_bcrypt = None # dynamically imported by _load_backend_builtin()
+from passlib.crypto.digest import compile_hmac
from passlib.exc import PasslibHashWarning, PasslibSecurityWarning, PasslibSecurityError
from passlib.utils import safe_crypt, repeat_string, to_bytes, parse_version, \
rng, getrandstr, test_crypt, to_unicode
@@ -909,7 +910,9 @@ class _wrapped_bcrypt(bcrypt):
#=============================================================================
class bcrypt_sha256(_wrapped_bcrypt):
- """This class implements a composition of BCrypt+SHA256, and follows the :ref:`password-hash-api`.
+ """
+ This class implements a composition of BCrypt + HMAC_SHA256,
+ and follows the :ref:`password-hash-api`.
It supports a fixed-length salt, and a variable number of rounds.
@@ -920,7 +923,13 @@ class bcrypt_sha256(_wrapped_bcrypt):
.. versionchanged:: 1.7
- Now defaults to ``"2b"`` variant.
+ Now defaults to ``"2b"`` bcrypt variant; though supports older hashes
+ generated using the ``"2a"`` bcrypt variant.
+
+ .. versionchanged:: 1.7.3
+
+ For increased security, updated to use HMAC-SHA256 instead of plain SHA256.
+ Now only supports the ``"2b"`` bcrypt variant. Hash format updated to "v=2".
"""
#===================================================================
# class attrs
@@ -934,7 +943,7 @@ class bcrypt_sha256(_wrapped_bcrypt):
#--------------------
# GenericHandler
#--------------------
- # this is locked at 2a/2b for now.
+ # this is locked at 2b for now (with 2a allowed only for legacy v1 format)
ident_values = (IDENT_2A, IDENT_2B)
# clone bcrypt's ident aliases so they can be used here as well...
@@ -942,6 +951,36 @@ class bcrypt_sha256(_wrapped_bcrypt):
if item[1] in ident_values))(ident_values)
default_ident = IDENT_2B
+ #--------------------
+ # class specific
+ #--------------------
+
+ _supported_versions = {1, 2}
+
+ #===================================================================
+ # instance attrs
+ #===================================================================
+
+ #: wrapper version.
+ #: v1 -- used prior to passlib 1.7.3; performs ``bcrypt(sha256(secret), salt, cost)``
+ #: v2 -- new in passlib 1.7.3; performs `bcrypt(sha256_hmac(salt, secret), salt, cost)``
+ version = 2
+
+ #===================================================================
+ # configuration
+ #===================================================================
+
+ @classmethod
+ def using(cls, version=None, **kwds):
+ subcls = super(bcrypt_sha256, cls).using(**kwds)
+ if version is not None:
+ subcls.version = subcls._norm_version(version)
+ ident = subcls.default_ident
+ if subcls.version > 1 and ident != IDENT_2B:
+ raise ValueError("bcrypt %r hashes not allowed for version %r" %
+ (ident, subcls.version))
+ return subcls
+
#===================================================================
# formatting
#===================================================================
@@ -961,15 +1000,28 @@ class bcrypt_sha256(_wrapped_bcrypt):
# working around that via prefix.
prefix = u('$bcrypt-sha256$')
- _hash_re = re.compile(r"""
+ #: current version 2 hash format
+ _v2_hash_re = re.compile(r"""(?x)
+ ^
+ [$]bcrypt-sha256[$]
+ v=(?P<version>\d+),
+ t=(?P<type>2b),
+ r=(?P<rounds>\d{1,2})
+ [$](?P<salt>[^$]{22})
+ (?:[$](?P<digest>[^$]{31}))?
+ $
+ """)
+
+ #: old version 1 hash format
+ _v1_hash_re = re.compile(r"""(?x)
^
- [$]bcrypt-sha256
- [$](?P<variant>2[ab])
- ,(?P<rounds>\d{1,2})
+ [$]bcrypt-sha256[$]
+ (?P<type>2[ab]),
+ (?P<rounds>\d{1,2})
[$](?P<salt>[^$]{22})
- (?:[$](?P<digest>.{31}))?
+ (?:[$](?P<digest>[^$]{31}))?
$
- """, re.X)
+ """)
@classmethod
def identify(cls, hash):
@@ -983,28 +1035,62 @@ class bcrypt_sha256(_wrapped_bcrypt):
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)
+ m = cls._v2_hash_re.match(hash)
+ if m:
+ version = int(m.group("version"))
+ if version < 2:
+ raise uh.exc.MalformedHashError(cls)
+ else:
+ m = cls._v1_hash_re.match(hash)
+ if m:
+ version = 1
+ else:
+ 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"),
- )
+ return cls(
+ version=version,
+ ident=m.group("type"),
+ rounds=int(rounds),
+ salt=m.group("salt"),
+ checksum=m.group("digest"),
+ )
- _template = u("$bcrypt-sha256$%s,%d$%s$%s")
+ _v2_template = u("$bcrypt-sha256$v=2,t=%s,r=%d$%s$%s")
+ _v1_template = u("$bcrypt-sha256$%s,%d$%s$%s")
def to_string(self):
- hash = self._template % (self.ident.strip(_UDOLLAR),
- self.rounds, self.salt, self.checksum)
+ if self.version == 1:
+ template = self._v1_template
+ else:
+ template = self._v2_template
+ hash = template % (self.ident.strip(_UDOLLAR), self.rounds, self.salt, self.checksum)
return uascii_to_str(hash)
#===================================================================
+ # init
+ #===================================================================
+
+ def __init__(self, version=None, **kwds):
+ if version is not None:
+ self.version = self._norm_version(version)
+ super(bcrypt_sha256, self).__init__(**kwds)
+
+ #===================================================================
+ # version
+ #===================================================================
+
+ @classmethod
+ def _norm_version(cls, version):
+ if version not in cls._supported_versions:
+ raise ValueError("%s: unknown or unsupported version: %r" % (cls.name, version))
+ return version
+
+ #===================================================================
# checksum
#===================================================================
+
def _calc_checksum(self, secret):
# 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
@@ -1015,9 +1101,31 @@ class bcrypt_sha256(_wrapped_bcrypt):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
+ if self.version == 1:
+ # version 1 -- old version just ran secret through sha256(),
+ # though this could be vulnerable to a breach attach
+ # (c.f. issue 114); which is why v2 switched to hmac wrapper.
+ digest = sha256(secret).digest()
+ else:
+ # version 2 -- running secret through HMAC keyed off salt.
+ # this prevents known secret -> sha256 password tables from being
+ # used to test against a bcrypt_sha256 hash.
+ # keying off salt (instead of constant string) should minimize chances of this
+ # colliding with existing table of hmac digest lookups as well.
+ # NOTE: salt in this case is the "bcrypt64"-encoded value, not the raw salt bytes,
+ # to make things easier for parallel implementations of this hash --
+ # saving them the trouble of implementing a "bcrypt64" decoder.
+ salt = self.salt
+ if salt[-1] not in self.final_salt_chars:
+ # forbidding salts with padding bits set, because bcrypt implementations
+ # won't consistently hash them the same. since we control this format,
+ # just prevent these from even getting used.
+ raise ValueError("invalid salt string")
+ digest = compile_hmac("sha256", salt.encode("ascii"))(secret)
+
# NOTE: output of b64encode() uses "+/" altchars, "=" padding chars,
# and no leading/trailing whitespace.
- key = b64encode(sha256(secret).digest())
+ key = b64encode(digest)
# hand result off to normal bcrypt algorithm
return super(bcrypt_sha256, self)._calc_checksum(key)
@@ -1026,8 +1134,10 @@ class bcrypt_sha256(_wrapped_bcrypt):
# other
#===================================================================
- # XXX: have _needs_update() mark the $2a$ ones for upgrading?
- # maybe do that after we switch to hex encoding?
+ def _calc_needs_update(self, **kwds):
+ if self.version < type(self).version:
+ return True
+ return super(bcrypt_sha256, self)._calc_needs_update(**kwds)
#===================================================================
# eoc
diff --git a/passlib/tests/test_handlers_bcrypt.py b/passlib/tests/test_handlers_bcrypt.py
index 8c88935..6b6e562 100644
--- a/passlib/tests/test_handlers_bcrypt.py
+++ b/passlib/tests/test_handlers_bcrypt.py
@@ -434,9 +434,9 @@ class _bcrypt_sha256_test(HandlerCase):
has_os_crypt_fallback = True
known_correct_hashes = [
- #
- # custom test vectors
- #
+ #-------------------------------------------------------------------
+ # custom test vectors for old v1 format
+ #-------------------------------------------------------------------
# empty
("",
@@ -460,20 +460,56 @@ class _bcrypt_sha256_test(HandlerCase):
# test >72 chars is hashed correctly -- under bcrypt these hash the same.
# NOTE: test_60_truncate_size() handles this already, this is just for overkill :)
- (repeat_string("abc123",72),
+ (repeat_string("abc123", 72),
'$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$r/hyEtqJ0teqPEmfTLoZ83ciAI1Q74.'),
- (repeat_string("abc123",72)+"qwr",
+ (repeat_string("abc123", 72) + "qwr",
'$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$021KLEif6epjot5yoxk0m8I0929ohEa'),
- (repeat_string("abc123",72)+"xyz",
+ (repeat_string("abc123", 72) + "xyz",
'$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$7.1kgpHduMGEjvM3fX6e/QCvfn6OKja'),
+
+ #-------------------------------------------------------------------
+ # custom test vectors for v2 format
+ # TODO: convert to v2 format
+ #-------------------------------------------------------------------
+
+ # empty
+ ("",
+ '$bcrypt-sha256$v=2,t=2b,r=5$E/e/2AOhqM5W/KJTFQzLce$WFPIZKtDDTriqWwlmRFfHiOTeheAZWe'),
+
+ # ascii
+ ("password",
+ '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS'),
+
+ # unicode / utf8
+ (UPASS_TABLE,
+ '$bcrypt-sha256$v=2,t=2b,r=5$.US1fQ4TQS.ZTz/uJ5Kyn.$pzzgp40k8reM1CuQb03PvE0IDPQSdV6'),
+ (UPASS_TABLE.encode("utf-8"),
+ '$bcrypt-sha256$v=2,t=2b,r=5$.US1fQ4TQS.ZTz/uJ5Kyn.$pzzgp40k8reM1CuQb03PvE0IDPQSdV6'),
+
+ # test >72 chars is hashed correctly -- under bcrypt these hash the same.
+ # NOTE: test_60_truncate_size() handles this already, this is just for overkill :)
+ (repeat_string("abc123", 72),
+ '$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$zu1cloESVFIOsUIo7fCEgkdHaI9SSue'),
+ (repeat_string("abc123", 72) + "qwr",
+ '$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$CBF9csfEdW68xv3DwE6xSULXMtqEFP.'),
+ (repeat_string("abc123", 72) + "xyz",
+ '$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$zC/1UDUG2ofEXB6Onr2vvyFzfhEOS3S'),
]
known_correct_configs =[
+ # v1
('$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe',
"password", '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'),
+ # v2
+ ('$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe',
+ "password", '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS'),
]
known_malformed_hashes = [
+ #-------------------------------------------------------------------
+ # v1 format
+ #-------------------------------------------------------------------
+
# bad char in otherwise correct hash
# \/
'$bcrypt-sha256$2a,5$5Hg1DKF!PE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
@@ -489,6 +525,33 @@ class _bcrypt_sha256_test(HandlerCase):
# config string w/ $ added
'$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$',
+
+ #-------------------------------------------------------------------
+ # v2 format
+ #-------------------------------------------------------------------
+
+ # bad char in otherwise correct hash
+ # \/
+ '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKF!PE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
+
+ # unsupported version (for this format)
+ '$bcrypt-sha256$v=1,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
+
+ # unrecognized version
+ '$bcrypt-sha256$v=3,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
+
+ # unrecognized bcrypt variant
+ '$bcrypt-sha256$v=2,t=2c,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
+
+ # unsupported bcrypt variant
+ '$bcrypt-sha256$v=2,t=2a,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
+ '$bcrypt-sha256$v=2,t=2x,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
+
+ # rounds zero-padded
+ '$bcrypt-sha256$v=2,t=2b,r=05$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
+
+ # config string w/ $ added
+ '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$',
]
#===================================================================
@@ -516,11 +579,12 @@ class _bcrypt_sha256_test(HandlerCase):
#===================================================================
# override ident tests for now
#===================================================================
- def test_30_HasManyIdents(self):
+
+ def require_many_idents(self):
raise self.skipTest("multiple idents not supported")
def test_30_HasOneIdent(self):
- # forbidding ident keyword, we only support "2a" for now
+ # forbidding ident keyword, we only support "2b" for now
handler = self.handler
handler(use_defaults=True)
self.assertRaises(ValueError, handler, ident="$2y$", use_defaults=True)
@@ -528,12 +592,66 @@ class _bcrypt_sha256_test(HandlerCase):
#===================================================================
# fuzz testing -- cloned from bcrypt
#===================================================================
+
class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
def random_rounds(self):
# decrease default rounds for fuzz testing to speed up volume.
return self.randintgauss(5, 8, 6, 1)
+ def random_ident(self):
+ return "2b"
+
+ #===================================================================
+ # custom tests
+ #===================================================================
+
+ def test_using_version(self):
+ # default to v2
+ handler = self.handler
+ self.assertEqual(handler.version, 2)
+
+ # allow v1 explicitly
+ subcls = handler.using(version=1)
+ self.assertEqual(subcls.version, 1)
+
+ # forbid unknown ver
+ self.assertRaises(ValueError, handler.using, version=999)
+
+ # allow '2a' only for v1
+ subcls = handler.using(version=1, ident="2a")
+ self.assertRaises(ValueError, handler.using, ident="2a")
+
+ def test_calc_digest_v2(self):
+ """
+ test digest calc v2 matches bcrypt()
+ """
+ from passlib.hash import bcrypt
+ from passlib.crypto.digest import compile_hmac
+ from passlib.utils.binary import b64encode
+
+ # manually calc intermediary digest
+ salt = "nyKYxTAvjmy6lMDYMl11Uu"
+ secret = "test"
+ temp_digest = compile_hmac("sha256", salt.encode("ascii"))(secret.encode("ascii"))
+ temp_digest = b64encode(temp_digest).decode("ascii")
+ self.assertEqual(temp_digest, "J5TlyIDm+IcSWmKiDJm+MeICndBkFVPn4kKdJW8f+xY=")
+
+ # manually final hash from intermediary
+ # XXX: genhash() could be useful here
+ bcrypt_digest = bcrypt(ident="2b", salt=salt, rounds=12)._calc_checksum(temp_digest)
+ self.assertEqual(bcrypt_digest, "M0wE0Ov/9LXoQFCe.jRHu3MSHPF54Ta")
+ self.assertTrue(bcrypt.verify(temp_digest, "$2b$12$" + salt + bcrypt_digest))
+
+ # confirm handler outputs same thing.
+ # XXX: genhash() could be useful here
+ result = self.handler(ident="2b", salt=salt, rounds=12)._calc_checksum(secret)
+ self.assertEqual(result, bcrypt_digest)
+
+ #===================================================================
+ # eoc
+ #===================================================================
+
# create test cases for specific backends
bcrypt_sha256_bcrypt_test = _bcrypt_sha256_test.create_backend_case("bcrypt")
bcrypt_sha256_pybcrypt_test = _bcrypt_sha256_test.create_backend_case("pybcrypt")