diff options
Diffstat (limited to 'passlib/handlers/bcrypt.py')
-rw-r--r-- | passlib/handlers/bcrypt.py | 156 |
1 files changed, 133 insertions, 23 deletions
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 |