"""passlib.bcrypt -- implementation of OpenBSD's BCrypt algorithm. TODO: * support 2x and altered-2a hashes? http://www.openwall.com/lists/oss-security/2011/06/27/9 * deal with lack of PY3-compatibile c-ext implementation """ #============================================================================= # imports #============================================================================= 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__) from warnings import warn # site try: import bcrypt as _bcrypt except ImportError: # pragma: no cover _bcrypt = None try: from bcryptor.engine import Engine as bcryptor_engine except ImportError: # pragma: no cover bcryptor_engine = None # pkg from passlib.exc import PasslibHashWarning from passlib.utils import bcrypt64, safe_crypt, repeat_string, to_bytes, \ 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 # local __all__ = [ "bcrypt", ] #============================================================================= # support funcs & constants #============================================================================= _builtin_bcrypt = None def _load_builtin(): global _builtin_bcrypt if _builtin_bcrypt is None: from passlib.utils._blowfish import raw_bcrypt as _builtin_bcrypt IDENT_2 = u("$2$") IDENT_2A = u("$2a$") IDENT_2X = u("$2x$") IDENT_2Y = u("$2y$") _BNULL = b('\x00') #============================================================================= # handler #============================================================================= class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.GenericHandler): """This class implements the BCrypt password hash, 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 the following optional keywords: :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``. :type rounds: int :param rounds: Optional number of rounds to use. Defaults to 12, must be between 4 and 31, inclusive. This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}` -- increasing the rounds by +1 will double the amount of time taken. :type ident: str :param ident: Specifies which version of the BCrypt algorithm will be used when creating a new hash. Typically this option is not needed, as the default (``"2a"``) is usually the correct choice. If specified, it must be one of the following: * ``"2"`` - the first revision of BCrypt, which suffers from a minor security flaw and is generally not used anymore. * ``"2a"`` - latest revision of the official BCrypt algorithm, and the current default. * ``"2y"`` - format specific to the *crypt_blowfish* BCrypt implementation, identical to ``"2a"`` in all but name. :type relaxed: bool :param relaxed: By default, providing an invalid value for one of the other keywords will result in a :exc:`ValueError`. If ``relaxed=True``, and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` will be issued instead. Correctable errors include ``rounds`` that are too small or too large, and ``salt`` strings that are too long. .. versionadded:: 1.6 .. versionchanged:: 1.6 This class now supports ``"2y"`` hashes, and recognizes (but does not support) the broken ``"2x"`` hashes. (see the :ref:`crypt_blowfish bug ` for details). .. versionchanged:: 1.6 Added a pure-python backend. """ #=================================================================== # class attrs #=================================================================== #--GenericHandler-- name = "bcrypt" setting_kwds = ("salt", "rounds", "ident") checksum_size = 31 checksum_chars = bcrypt64.charmap #--HasManyIdents-- default_ident = IDENT_2A ident_values = (IDENT_2, IDENT_2A, IDENT_2X, IDENT_2Y) ident_aliases = {u("2"): IDENT_2, u("2a"): IDENT_2A, u("2y"): IDENT_2Y} #--HasSalt-- min_salt_size = max_salt_size = 22 salt_chars = bcrypt64.charmap # NOTE: 22nd salt char must be in bcrypt64._padinfo2[1], not full charmap #--HasRounds-- default_rounds = 12 # current passlib default min_rounds = 4 # minimum from bcrypt specification max_rounds = 31 # 32-bit integer limit (since real_rounds=1<[a-z0-9]+) ,(?P\d{1,2}) [$](?P[^$]{22}) ([$](?P.{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 #=============================================================================