diff options
author | Eli Collins <elic@assurancetechnologies.com> | 2020-02-16 10:56:06 -0500 |
---|---|---|
committer | Eli Collins <elic@assurancetechnologies.com> | 2020-02-16 10:56:06 -0500 |
commit | 2766485f5760489fedb2e73f8a162c83633bbbcb (patch) | |
tree | 7c62d5d273f5bfd1082ab46c6f436a9447c3bdd1 | |
parent | ec62db38788b1dc0c8f78060f6119cb63bbacfbd (diff) | |
download | passlib-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.rst | 14 | ||||
-rw-r--r-- | docs/lib/passlib.hash.bcrypt.rst | 2 | ||||
-rw-r--r-- | docs/lib/passlib.hash.bcrypt_sha256.rst | 39 | ||||
-rw-r--r-- | passlib/handlers/bcrypt.py | 156 | ||||
-rw-r--r-- | passlib/tests/test_handlers_bcrypt.py | 134 |
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") |