You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							1244 lines
						
					
					
						
							52 KiB
						
					
					
				
			
		
		
	
	
							1244 lines
						
					
					
						
							52 KiB
						
					
					
				"""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
 | 
						|
_bcrypt = None # dynamically imported by _load_backend_bcrypt()
 | 
						|
_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, \
 | 
						|
                          utf8_truncate, utf8_repeat_string, crypt_accepts_bytes
 | 
						|
from passlib.utils.binary import bcrypt64
 | 
						|
from passlib.utils.compat import get_unbound_method_function
 | 
						|
from passlib.utils.compat import u, uascii_to_str, unicode, str_to_uascii, PY3, error_from
 | 
						|
import passlib.utils.handlers as uh
 | 
						|
 | 
						|
# local
 | 
						|
__all__ = [
 | 
						|
    "bcrypt",
 | 
						|
]
 | 
						|
 | 
						|
#=============================================================================
 | 
						|
# support funcs & constants
 | 
						|
#=============================================================================
 | 
						|
IDENT_2 = u("$2$")
 | 
						|
IDENT_2A = u("$2a$")
 | 
						|
IDENT_2X = u("$2x$")
 | 
						|
IDENT_2Y = u("$2y$")
 | 
						|
IDENT_2B = u("$2b$")
 | 
						|
_BNULL = b'\x00'
 | 
						|
 | 
						|
# reference hash of "test", used in various self-checks
 | 
						|
TEST_HASH_2A = "$2a$04$5BJqKfqMQvV7nS.yUguNcueVirQqDBGaLXSqj.rs.pZPlNR0UX/HK"
 | 
						|
 | 
						|
def _detect_pybcrypt():
 | 
						|
    """
 | 
						|
    internal helper which tries to distinguish pybcrypt vs bcrypt.
 | 
						|
 | 
						|
    :returns:
 | 
						|
        True if cext-based py-bcrypt,
 | 
						|
        False if ffi-based bcrypt,
 | 
						|
        None if 'bcrypt' module not found.
 | 
						|
 | 
						|
    .. versionchanged:: 1.6.3
 | 
						|
 | 
						|
        Now assuming bcrypt installed, unless py-bcrypt explicitly detected.
 | 
						|
        Previous releases assumed py-bcrypt by default.
 | 
						|
 | 
						|
        Making this change since py-bcrypt is (apparently) unmaintained and static,
 | 
						|
        whereas bcrypt is being actively maintained, and it's internal structure may shift.
 | 
						|
    """
 | 
						|
    # NOTE: this is also used by the unittests.
 | 
						|
 | 
						|
    # check for module.
 | 
						|
    try:
 | 
						|
        import bcrypt
 | 
						|
    except ImportError:
 | 
						|
        # XXX: this is ignoring case where py-bcrypt's "bcrypt._bcrypt" C Ext fails to import;
 | 
						|
        #      would need to inspect actual ImportError message to catch that.
 | 
						|
        return None
 | 
						|
 | 
						|
    # py-bcrypt has a "._bcrypt.__version__" attribute (confirmed for v0.1 - 0.4),
 | 
						|
    # which bcrypt lacks (confirmed for v1.0 - 2.0)
 | 
						|
    # "._bcrypt" alone isn't sufficient, since bcrypt 2.0 now has that attribute.
 | 
						|
    try:
 | 
						|
        from bcrypt._bcrypt import __version__
 | 
						|
    except ImportError:
 | 
						|
        return False
 | 
						|
    return True
 | 
						|
 | 
						|
#=============================================================================
 | 
						|
# backend mixins
 | 
						|
#=============================================================================
 | 
						|
class _BcryptCommon(uh.SubclassBackendMixin, uh.TruncateMixin, uh.HasManyIdents,
 | 
						|
                    uh.HasRounds, uh.HasSalt, uh.GenericHandler):
 | 
						|
    """
 | 
						|
    Base class which implements brunt of BCrypt code.
 | 
						|
    This is then subclassed by the various backends,
 | 
						|
    to override w/ backend-specific methods.
 | 
						|
 | 
						|
    When a backend is loaded, the bases of the 'bcrypt' class proper
 | 
						|
    are modified to prepend the correct backend-specific subclass.
 | 
						|
    """
 | 
						|
    #===================================================================
 | 
						|
    # class attrs
 | 
						|
    #===================================================================
 | 
						|
 | 
						|
    #--------------------
 | 
						|
    # PasswordHash
 | 
						|
    #--------------------
 | 
						|
    name = "bcrypt"
 | 
						|
    setting_kwds = ("salt", "rounds", "ident", "truncate_error")
 | 
						|
 | 
						|
    #--------------------
 | 
						|
    # GenericHandler
 | 
						|
    #--------------------
 | 
						|
    checksum_size = 31
 | 
						|
    checksum_chars = bcrypt64.charmap
 | 
						|
 | 
						|
    #--------------------
 | 
						|
    # HasManyIdents
 | 
						|
    #--------------------
 | 
						|
    default_ident = IDENT_2B
 | 
						|
    ident_values = (IDENT_2, IDENT_2A, IDENT_2X, IDENT_2Y, IDENT_2B)
 | 
						|
    ident_aliases = {u("2"): IDENT_2, u("2a"): IDENT_2A,  u("2y"): IDENT_2Y,
 | 
						|
                     u("2b"): IDENT_2B}
 | 
						|
 | 
						|
    #--------------------
 | 
						|
    # HasSalt
 | 
						|
    #--------------------
 | 
						|
    min_salt_size = max_salt_size = 22
 | 
						|
    salt_chars = bcrypt64.charmap
 | 
						|
 | 
						|
    # NOTE: 22nd salt char must be in restricted set of ``final_salt_chars``, not full set above.
 | 
						|
    final_salt_chars = ".Oeu"  # bcrypt64._padinfo2[1]
 | 
						|
 | 
						|
    #--------------------
 | 
						|
    # 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<<rounds)
 | 
						|
    rounds_cost = "log2"
 | 
						|
 | 
						|
    #--------------------
 | 
						|
    # TruncateMixin
 | 
						|
    #--------------------
 | 
						|
    truncate_size = 72
 | 
						|
 | 
						|
    #--------------------
 | 
						|
    # custom
 | 
						|
    #--------------------
 | 
						|
 | 
						|
    # backend workaround detection flags
 | 
						|
    # NOTE: these are only set on the backend mixin classes
 | 
						|
    _workrounds_initialized = False
 | 
						|
    _has_2a_wraparound_bug = False
 | 
						|
    _lacks_20_support = False
 | 
						|
    _lacks_2y_support = False
 | 
						|
    _lacks_2b_support = False
 | 
						|
    _fallback_ident = IDENT_2A
 | 
						|
    _require_valid_utf8_bytes = False
 | 
						|
 | 
						|
    #===================================================================
 | 
						|
    # formatting
 | 
						|
    #===================================================================
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def from_string(cls, hash):
 | 
						|
        ident, tail = cls._parse_ident(hash)
 | 
						|
        if ident == IDENT_2X:
 | 
						|
            raise ValueError("crypt_blowfish's buggy '2x' hashes are not "
 | 
						|
                             "currently supported")
 | 
						|
        rounds_str, data = tail.split(u("$"))
 | 
						|
        rounds = int(rounds_str)
 | 
						|
        if rounds_str != u('%02d') % (rounds,):
 | 
						|
            raise uh.exc.MalformedHashError(cls, "malformed cost field")
 | 
						|
        salt, chk = data[:22], data[22:]
 | 
						|
        return cls(
 | 
						|
            rounds=rounds,
 | 
						|
            salt=salt,
 | 
						|
            checksum=chk or None,
 | 
						|
            ident=ident,
 | 
						|
        )
 | 
						|
 | 
						|
    def to_string(self):
 | 
						|
        hash = u("%s%02d$%s%s") % (self.ident, self.rounds, self.salt, self.checksum)
 | 
						|
        return uascii_to_str(hash)
 | 
						|
 | 
						|
    # NOTE: this should be kept separate from to_string()
 | 
						|
    #       so that bcrypt_sha256() can still use it, while overriding to_string()
 | 
						|
    def _get_config(self, ident):
 | 
						|
        """internal helper to prepare config string for backends"""
 | 
						|
        config = u("%s%02d$%s") % (ident, self.rounds, self.salt)
 | 
						|
        return uascii_to_str(config)
 | 
						|
 | 
						|
    #===================================================================
 | 
						|
    # migration
 | 
						|
    #===================================================================
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def needs_update(cls, hash, **kwds):
 | 
						|
        # NOTE: can't convert this to use _calc_needs_update() helper,
 | 
						|
        #       since _norm_hash() will correct salt padding before we can read it here.
 | 
						|
        # check for incorrect padding bits (passlib issue 25)
 | 
						|
        if isinstance(hash, bytes):
 | 
						|
            hash = hash.decode("ascii")
 | 
						|
        if hash.startswith(IDENT_2A) and hash[28] not in cls.final_salt_chars:
 | 
						|
            return True
 | 
						|
 | 
						|
        # TODO: try to detect incorrect 8bit/wraparound hashes using kwds.get("secret")
 | 
						|
 | 
						|
        # hand off to base implementation, so HasRounds can check rounds value.
 | 
						|
        return super(_BcryptCommon, cls).needs_update(hash, **kwds)
 | 
						|
 | 
						|
    #===================================================================
 | 
						|
    # specialized salt generation - fixes passlib issue 25
 | 
						|
    #===================================================================
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def normhash(cls, hash):
 | 
						|
        """helper to normalize hash, correcting any bcrypt padding bits"""
 | 
						|
        if cls.identify(hash):
 | 
						|
            return cls.from_string(hash).to_string()
 | 
						|
        else:
 | 
						|
            return hash
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _generate_salt(cls):
 | 
						|
        # generate random salt as normal,
 | 
						|
        # but repair last char so the padding bits always decode to zero.
 | 
						|
        salt = super(_BcryptCommon, cls)._generate_salt()
 | 
						|
        return bcrypt64.repair_unused(salt)
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _norm_salt(cls, salt, **kwds):
 | 
						|
        salt = super(_BcryptCommon, cls)._norm_salt(salt, **kwds)
 | 
						|
        assert salt is not None, "HasSalt didn't generate new salt!"
 | 
						|
        changed, salt = bcrypt64.check_repair_unused(salt)
 | 
						|
        if changed:
 | 
						|
            # FIXME: if salt was provided by user, this message won't be
 | 
						|
            # correct. not sure if we want to throw error, or use different warning.
 | 
						|
            warn(
 | 
						|
                "encountered a bcrypt salt with incorrectly set padding bits; "
 | 
						|
                "you may want to use bcrypt.normhash() "
 | 
						|
                "to fix this; this will be an error under Passlib 2.0",
 | 
						|
                PasslibHashWarning)
 | 
						|
        return salt
 | 
						|
 | 
						|
    def _norm_checksum(self, checksum, relaxed=False):
 | 
						|
        checksum = super(_BcryptCommon, self)._norm_checksum(checksum, relaxed=relaxed)
 | 
						|
        changed, checksum = bcrypt64.check_repair_unused(checksum)
 | 
						|
        if changed:
 | 
						|
            warn(
 | 
						|
                "encountered a bcrypt hash with incorrectly set padding bits; "
 | 
						|
                "you may want to use bcrypt.normhash() "
 | 
						|
                "to fix this; this will be an error under Passlib 2.0",
 | 
						|
                PasslibHashWarning)
 | 
						|
        return checksum
 | 
						|
 | 
						|
    #===================================================================
 | 
						|
    # backend configuration
 | 
						|
    # NOTE: backends are defined in terms of mixin classes,
 | 
						|
    #       which are dynamically inserted into the bases of the 'bcrypt' class
 | 
						|
    #       via the machinery in 'SubclassBackendMixin'.
 | 
						|
    #       this lets us load in a backend-specific implementation
 | 
						|
    #       of _calc_checksum() and similar methods.
 | 
						|
    #===================================================================
 | 
						|
 | 
						|
    # NOTE: backend config is located down in <bcrypt> class
 | 
						|
 | 
						|
    # NOTE: set_backend() will execute the ._load_backend_mixin()
 | 
						|
    #       of the matching mixin class, which will handle backend detection
 | 
						|
 | 
						|
    # appended to HasManyBackends' "no backends available" error message
 | 
						|
    _no_backend_suggestion = " -- recommend you install one (e.g. 'pip install bcrypt')"
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _finalize_backend_mixin(mixin_cls, backend, dryrun):
 | 
						|
        """
 | 
						|
        helper called by from backend mixin classes' _load_backend_mixin() --
 | 
						|
        invoked after backend imports have been loaded, and performs
 | 
						|
        feature detection & testing common to all backends.
 | 
						|
        """
 | 
						|
        #----------------------------------------------------------------
 | 
						|
        # setup helpers
 | 
						|
        #----------------------------------------------------------------
 | 
						|
        assert mixin_cls is bcrypt._backend_mixin_map[backend], \
 | 
						|
            "_configure_workarounds() invoked from wrong class"
 | 
						|
 | 
						|
        if mixin_cls._workrounds_initialized:
 | 
						|
            return True
 | 
						|
 | 
						|
        verify = mixin_cls.verify
 | 
						|
 | 
						|
        err_types = (ValueError, uh.exc.MissingBackendError)
 | 
						|
        if _bcryptor:
 | 
						|
            err_types += (_bcryptor.engine.SaltError,)
 | 
						|
 | 
						|
        def safe_verify(secret, hash):
 | 
						|
            """verify() wrapper which traps 'unknown identifier' errors"""
 | 
						|
            try:
 | 
						|
                return verify(secret, hash)
 | 
						|
            except err_types:
 | 
						|
                # backends without support for given ident will throw various
 | 
						|
                # errors about unrecognized version:
 | 
						|
                #   os_crypt -- internal code below throws
 | 
						|
                #       - PasswordValueError if there's encoding issue w/ password.
 | 
						|
                #       - InternalBackendError if crypt fails for unknown reason
 | 
						|
                #         (trapped below so we can debug it)
 | 
						|
                #   pybcrypt, bcrypt -- raises ValueError
 | 
						|
                #   bcryptor -- raises bcryptor.engine.SaltError
 | 
						|
                return NotImplemented
 | 
						|
            except uh.exc.InternalBackendError:
 | 
						|
                # _calc_checksum() code may also throw CryptBackendError
 | 
						|
                # if correct hash isn't returned (e.g. 2y hash converted to 2b,
 | 
						|
                # such as happens with bcrypt 3.0.0)
 | 
						|
                log.debug("trapped unexpected response from %r backend: verify(%r, %r):",
 | 
						|
                          backend, secret, hash, exc_info=True)
 | 
						|
                return NotImplemented
 | 
						|
 | 
						|
        def assert_lacks_8bit_bug(ident):
 | 
						|
            """
 | 
						|
            helper to check for cryptblowfish 8bit bug (fixed in 2y/2b);
 | 
						|
            even though it's not known to be present in any of passlib's backends.
 | 
						|
            this is treated as FATAL, because it can easily result in seriously malformed hashes,
 | 
						|
            and we can't correct for it ourselves.
 | 
						|
 | 
						|
            test cases from <http://cvsweb.openwall.com/cgi/cvsweb.cgi/Owl/packages/glibc/crypt_blowfish/wrapper.c.diff?r1=1.9;r2=1.10>
 | 
						|
            reference hash is the incorrectly generated $2x$ hash taken from above url
 | 
						|
            """
 | 
						|
            # NOTE: passlib 1.7.2 and earlier used the commented-out LATIN-1 test vector to detect
 | 
						|
            #       this bug; but python3's crypt.crypt() only supports unicode inputs (and
 | 
						|
            #       always encodes them as UTF8 before passing to crypt); so passlib 1.7.3
 | 
						|
            #       switched to the UTF8-compatible test vector below.  This one's bug_hash value
 | 
						|
            #       ("$2x$...rcAS") was drawn from the same openwall source (above); and the correct
 | 
						|
            #       hash ("$2a$...X6eu") was generated by passing the raw bytes to python2's
 | 
						|
            #       crypt.crypt() using OpenBSD 6.7 (hash confirmed as same for $2a$ & $2b$).
 | 
						|
 | 
						|
            # LATIN-1 test vector
 | 
						|
            # secret = b"\xA3"
 | 
						|
            # bug_hash = ident.encode("ascii") + b"05$/OK.fbVrR/bpIqNJ5ianF.CE5elHaaO4EbggVDjb8P19RukzXSM3e"
 | 
						|
            # correct_hash = ident.encode("ascii") + b"05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq"
 | 
						|
 | 
						|
            # UTF-8 test vector
 | 
						|
            secret = b"\xd1\x91"  # aka "\u0451"
 | 
						|
            bug_hash = ident.encode("ascii") + b"05$6bNw2HLQYeqHYyBfLMsv/OiwqTymGIGzFsA4hOTWebfehXHNprcAS"
 | 
						|
            correct_hash = ident.encode("ascii") + b"05$6bNw2HLQYeqHYyBfLMsv/OUcZd0LKP39b87nBw3.S2tVZSqiQX6eu"
 | 
						|
 | 
						|
            if verify(secret, bug_hash):
 | 
						|
                # NOTE: this only EVER be observed in (broken) 2a and (backward-compat) 2x hashes
 | 
						|
                #       generated by crypt_blowfish library. 2y/2b hashes should not have the bug
 | 
						|
                #       (but we check w/ them anyways).
 | 
						|
                raise PasslibSecurityError(
 | 
						|
                    "passlib.hash.bcrypt: Your installation of the %r backend is vulnerable to "
 | 
						|
                    "the crypt_blowfish 8-bit bug (CVE-2011-2483) under %r hashes, "
 | 
						|
                    "and should be upgraded or replaced with another backend" % (backend, ident))
 | 
						|
 | 
						|
            # it doesn't have wraparound bug, but make sure it *does* verify against the correct
 | 
						|
            # hash, or we're in some weird third case!
 | 
						|
            if not verify(secret, correct_hash):
 | 
						|
                raise RuntimeError("%s backend failed to verify %s 8bit hash" % (backend, ident))
 | 
						|
 | 
						|
        def detect_wrap_bug(ident):
 | 
						|
            """
 | 
						|
            check for bsd wraparound bug (fixed in 2b)
 | 
						|
            this is treated as a warning, because it's rare in the field,
 | 
						|
            and pybcrypt (as of 2015-7-21) is unpatched, but some people may be stuck with it.
 | 
						|
 | 
						|
            test cases from <http://www.openwall.com/lists/oss-security/2012/01/02/4>
 | 
						|
 | 
						|
            NOTE: reference hash is of password "0"*72
 | 
						|
 | 
						|
            NOTE: if in future we need to deliberately create hashes which have this bug,
 | 
						|
                  can use something like 'hashpw(repeat_string(secret[:((1+secret) % 256) or 1]), 72)'
 | 
						|
            """
 | 
						|
            # check if it exhibits wraparound bug
 | 
						|
            secret = (b"0123456789"*26)[:255]
 | 
						|
            bug_hash = ident.encode("ascii") + b"04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6"
 | 
						|
            if verify(secret, bug_hash):
 | 
						|
                return True
 | 
						|
 | 
						|
            # if it doesn't have wraparound bug, make sure it *does* handle things
 | 
						|
            # correctly -- or we're in some weird third case.
 | 
						|
            correct_hash = ident.encode("ascii") + b"04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi"
 | 
						|
            if not verify(secret, correct_hash):
 | 
						|
                raise RuntimeError("%s backend failed to verify %s wraparound hash" % (backend, ident))
 | 
						|
 | 
						|
            return False
 | 
						|
 | 
						|
        def assert_lacks_wrap_bug(ident):
 | 
						|
            if not detect_wrap_bug(ident):
 | 
						|
                return
 | 
						|
            # should only see in 2a, later idents should NEVER exhibit this bug:
 | 
						|
            # * 2y implementations should have been free of it
 | 
						|
            # * 2b was what (supposedly) fixed it
 | 
						|
            raise RuntimeError("%s backend unexpectedly has wraparound bug for %s" % (backend, ident))
 | 
						|
 | 
						|
        #----------------------------------------------------------------
 | 
						|
        # check for old 20 support
 | 
						|
        #----------------------------------------------------------------
 | 
						|
        test_hash_20 = b"$2$04$5BJqKfqMQvV7nS.yUguNcuRfMMOXK0xPWavM7pOzjEi5ze5T1k8/S"
 | 
						|
        result = safe_verify("test", test_hash_20)
 | 
						|
        if result is NotImplemented:
 | 
						|
            mixin_cls._lacks_20_support = True
 | 
						|
            log.debug("%r backend lacks $2$ support, enabling workaround", backend)
 | 
						|
        elif not result:
 | 
						|
            raise RuntimeError("%s incorrectly rejected $2$ hash" % backend)
 | 
						|
 | 
						|
        #----------------------------------------------------------------
 | 
						|
        # check for 2a support
 | 
						|
        #----------------------------------------------------------------
 | 
						|
        result = safe_verify("test", TEST_HASH_2A)
 | 
						|
        if result is NotImplemented:
 | 
						|
            # 2a support is required, and should always be present
 | 
						|
            raise RuntimeError("%s lacks support for $2a$ hashes" % backend)
 | 
						|
        elif not result:
 | 
						|
            raise RuntimeError("%s incorrectly rejected $2a$ hash" % backend)
 | 
						|
        else:
 | 
						|
            assert_lacks_8bit_bug(IDENT_2A)
 | 
						|
            if detect_wrap_bug(IDENT_2A):
 | 
						|
                if backend == "os_crypt":
 | 
						|
                    # don't make this a warning for os crypt (e.g. openbsd);
 | 
						|
                    # they'll have proper 2b implementation which will be used for new hashes.
 | 
						|
                    # so even if we didn't have a workaround, this bug wouldn't be a concern.
 | 
						|
                    log.debug("%r backend has $2a$ bsd wraparound bug, enabling workaround", backend)
 | 
						|
                else:
 | 
						|
                    # installed library has the bug -- want to let users know,
 | 
						|
                    # so they can upgrade it to something better (e.g. bcrypt cffi library)
 | 
						|
                    warn("passlib.hash.bcrypt: Your installation of the %r backend is vulnerable to "
 | 
						|
                         "the bsd wraparound bug, "
 | 
						|
                         "and should be upgraded or replaced with another backend "
 | 
						|
                         "(enabling workaround for now)." % backend,
 | 
						|
                         uh.exc.PasslibSecurityWarning)
 | 
						|
                mixin_cls._has_2a_wraparound_bug = True
 | 
						|
 | 
						|
        #----------------------------------------------------------------
 | 
						|
        # check for 2y support
 | 
						|
        #----------------------------------------------------------------
 | 
						|
        test_hash_2y = TEST_HASH_2A.replace("2a", "2y")
 | 
						|
        result = safe_verify("test", test_hash_2y)
 | 
						|
        if result is NotImplemented:
 | 
						|
            mixin_cls._lacks_2y_support = True
 | 
						|
            log.debug("%r backend lacks $2y$ support, enabling workaround", backend)
 | 
						|
        elif not result:
 | 
						|
            raise RuntimeError("%s incorrectly rejected $2y$ hash" % backend)
 | 
						|
        else:
 | 
						|
            # NOTE: Not using this as fallback candidate,
 | 
						|
            #       lacks wide enough support across implementations.
 | 
						|
            assert_lacks_8bit_bug(IDENT_2Y)
 | 
						|
            assert_lacks_wrap_bug(IDENT_2Y)
 | 
						|
 | 
						|
        #----------------------------------------------------------------
 | 
						|
        # TODO: check for 2x support
 | 
						|
        #----------------------------------------------------------------
 | 
						|
 | 
						|
        #----------------------------------------------------------------
 | 
						|
        # check for 2b support
 | 
						|
        #----------------------------------------------------------------
 | 
						|
        test_hash_2b = TEST_HASH_2A.replace("2a", "2b")
 | 
						|
        result = safe_verify("test", test_hash_2b)
 | 
						|
        if result is NotImplemented:
 | 
						|
            mixin_cls._lacks_2b_support = True
 | 
						|
            log.debug("%r backend lacks $2b$ support, enabling workaround", backend)
 | 
						|
        elif not result:
 | 
						|
            raise RuntimeError("%s incorrectly rejected $2b$ hash" % backend)
 | 
						|
        else:
 | 
						|
            mixin_cls._fallback_ident = IDENT_2B
 | 
						|
            assert_lacks_8bit_bug(IDENT_2B)
 | 
						|
            assert_lacks_wrap_bug(IDENT_2B)
 | 
						|
 | 
						|
        # set flag so we don't have to run this again
 | 
						|
        mixin_cls._workrounds_initialized = True
 | 
						|
        return True
 | 
						|
 | 
						|
    #===================================================================
 | 
						|
    # digest calculation
 | 
						|
    #===================================================================
 | 
						|
 | 
						|
    # _calc_checksum() defined by backends
 | 
						|
 | 
						|
    def _prepare_digest_args(self, secret):
 | 
						|
        """
 | 
						|
        common helper for backends to implement _calc_checksum().
 | 
						|
        takes in secret, returns (secret, ident) pair,
 | 
						|
        """
 | 
						|
        return self._norm_digest_args(secret, self.ident, new=self.use_defaults)
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _norm_digest_args(cls, secret, ident, new=False):
 | 
						|
        # make sure secret is unicode
 | 
						|
        require_valid_utf8_bytes = cls._require_valid_utf8_bytes
 | 
						|
        if isinstance(secret, unicode):
 | 
						|
            secret = secret.encode("utf-8")
 | 
						|
        elif require_valid_utf8_bytes:
 | 
						|
            # if backend requires utf8 bytes (os_crypt);
 | 
						|
            # make sure input actually is utf8, or don't bother enabling utf-8 specific helpers.
 | 
						|
            try:
 | 
						|
                secret.decode("utf-8")
 | 
						|
            except UnicodeDecodeError:
 | 
						|
                # XXX: could just throw PasswordValueError here, backend will just do that
 | 
						|
                #      when _calc_digest() is actually called.
 | 
						|
                require_valid_utf8_bytes = False
 | 
						|
 | 
						|
        # check max secret size
 | 
						|
        uh.validate_secret(secret)
 | 
						|
 | 
						|
        # check for truncation (during .hash() calls only)
 | 
						|
        if new:
 | 
						|
            cls._check_truncate_policy(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!
 | 
						|
        if _BNULL in secret:
 | 
						|
            raise uh.exc.NullPasswordError(cls)
 | 
						|
 | 
						|
        # TODO: figure out way to skip these tests when not needed...
 | 
						|
 | 
						|
        # protect from wraparound bug by truncating secret before handing it to the backend.
 | 
						|
        # bcrypt only uses first 72 bytes anyways.
 | 
						|
        # NOTE: not needed for 2y/2b, but might use 2a as fallback for them.
 | 
						|
        if cls._has_2a_wraparound_bug and len(secret) >= 255:
 | 
						|
            if require_valid_utf8_bytes:
 | 
						|
                # backend requires valid utf8 bytes, so truncate secret to nearest valid segment.
 | 
						|
                # want to do this in constant time to not give away info about secret.
 | 
						|
                # NOTE: this only works because bcrypt will ignore everything past
 | 
						|
                #       secret[71], so padding to include a full utf8 sequence
 | 
						|
                #       won't break anything about the final output.
 | 
						|
                secret = utf8_truncate(secret, 72)
 | 
						|
            else:
 | 
						|
                secret = secret[:72]
 | 
						|
 | 
						|
        # special case handling for variants (ordered most common first)
 | 
						|
        if ident == IDENT_2A:
 | 
						|
            # nothing needs to be done.
 | 
						|
            pass
 | 
						|
 | 
						|
        elif ident == IDENT_2B:
 | 
						|
            if cls._lacks_2b_support:
 | 
						|
                # handle $2b$ hash format even if backend is too old.
 | 
						|
                # have it generate a 2A/2Y digest, then return it as a 2B hash.
 | 
						|
                # 2a-only backend could potentially exhibit wraparound bug --
 | 
						|
                # but we work around that issue above.
 | 
						|
                ident = cls._fallback_ident
 | 
						|
 | 
						|
        elif ident == IDENT_2Y:
 | 
						|
            if cls._lacks_2y_support:
 | 
						|
                # handle $2y$ hash format (not supported by BSDs, being phased out on others)
 | 
						|
                # have it generate a 2A/2B digest, then return it as a 2Y hash.
 | 
						|
                ident = cls._fallback_ident
 | 
						|
 | 
						|
        elif ident == IDENT_2:
 | 
						|
            if cls._lacks_20_support:
 | 
						|
                # handle legacy $2$ format (not supported by most backends except BSD os_crypt)
 | 
						|
                # we can fake $2$ behavior using the 2A/2Y/2B algorithm
 | 
						|
                # by repeating the password until it's at least 72 chars in length.
 | 
						|
                if secret:
 | 
						|
                    if require_valid_utf8_bytes:
 | 
						|
                        # NOTE: this only works because bcrypt will ignore everything past
 | 
						|
                        #       secret[71], so padding to include a full utf8 sequence
 | 
						|
                        #       won't break anything about the final output.
 | 
						|
                        secret = utf8_repeat_string(secret, 72)
 | 
						|
                    else:
 | 
						|
                        secret = repeat_string(secret, 72)
 | 
						|
                ident = cls._fallback_ident
 | 
						|
 | 
						|
        elif ident == IDENT_2X:
 | 
						|
 | 
						|
            # NOTE: shouldn't get here.
 | 
						|
            # XXX: could check if backend does actually offer 'support'
 | 
						|
            raise RuntimeError("$2x$ hashes not currently supported by passlib")
 | 
						|
 | 
						|
        else:
 | 
						|
            raise AssertionError("unexpected ident value: %r" % ident)
 | 
						|
 | 
						|
        return secret, ident
 | 
						|
 | 
						|
#-----------------------------------------------------------------------
 | 
						|
# stub backend
 | 
						|
#-----------------------------------------------------------------------
 | 
						|
class _NoBackend(_BcryptCommon):
 | 
						|
    """
 | 
						|
    mixin used before any backend has been loaded.
 | 
						|
    contains stubs that force loading of one of the available backends.
 | 
						|
    """
 | 
						|
    #===================================================================
 | 
						|
    # digest calculation
 | 
						|
    #===================================================================
 | 
						|
    def _calc_checksum(self, secret):
 | 
						|
        self._stub_requires_backend()
 | 
						|
        # NOTE: have to use super() here so that we don't recursively
 | 
						|
        #       call subclass's wrapped _calc_checksum, e.g. bcrypt_sha256._calc_checksum
 | 
						|
        return super(bcrypt, self)._calc_checksum(secret)
 | 
						|
 | 
						|
    #===================================================================
 | 
						|
    # eoc
 | 
						|
    #===================================================================
 | 
						|
 | 
						|
#-----------------------------------------------------------------------
 | 
						|
# bcrypt backend
 | 
						|
#-----------------------------------------------------------------------
 | 
						|
class _BcryptBackend(_BcryptCommon):
 | 
						|
    """
 | 
						|
    backend which uses 'bcrypt' package
 | 
						|
    """
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _load_backend_mixin(mixin_cls, name, dryrun):
 | 
						|
        # try to import bcrypt
 | 
						|
        global _bcrypt
 | 
						|
        if _detect_pybcrypt():
 | 
						|
            # pybcrypt was installed instead
 | 
						|
            return False
 | 
						|
        try:
 | 
						|
            import bcrypt as _bcrypt
 | 
						|
        except ImportError: # pragma: no cover
 | 
						|
            return False
 | 
						|
        try:
 | 
						|
            version = _bcrypt.__about__.__version__
 | 
						|
        except:
 | 
						|
            log.warning("(trapped) error reading bcrypt version", exc_info=True)
 | 
						|
            version = '<unknown>'
 | 
						|
 | 
						|
        log.debug("detected 'bcrypt' backend, version %r", version)
 | 
						|
        return mixin_cls._finalize_backend_mixin(name, dryrun)
 | 
						|
 | 
						|
    # # TODO: would like to implementing verify() directly,
 | 
						|
    # #       to skip need for parsing hash strings.
 | 
						|
    # #       below method has a few edge cases where it chokes though.
 | 
						|
    # @classmethod
 | 
						|
    # def verify(cls, secret, hash):
 | 
						|
    #     if isinstance(hash, unicode):
 | 
						|
    #         hash = hash.encode("ascii")
 | 
						|
    #     ident = hash[:hash.index(b"$", 1)+1].decode("ascii")
 | 
						|
    #     if ident not in cls.ident_values:
 | 
						|
    #         raise uh.exc.InvalidHashError(cls)
 | 
						|
    #     secret, eff_ident = cls._norm_digest_args(secret, ident)
 | 
						|
    #     if eff_ident != ident:
 | 
						|
    #         # lacks support for original ident, replace w/ new one.
 | 
						|
    #         hash = eff_ident.encode("ascii") + hash[len(ident):]
 | 
						|
    #     result = _bcrypt.hashpw(secret, hash)
 | 
						|
    #     assert result.startswith(eff_ident)
 | 
						|
    #     return consteq(result, hash)
 | 
						|
 | 
						|
    def _calc_checksum(self, secret):
 | 
						|
        # bcrypt behavior:
 | 
						|
        #   secret must be bytes
 | 
						|
        #   config must be ascii bytes
 | 
						|
        #   returns ascii bytes
 | 
						|
        secret, ident = self._prepare_digest_args(secret)
 | 
						|
        config = self._get_config(ident)
 | 
						|
        if isinstance(config, unicode):
 | 
						|
            config = config.encode("ascii")
 | 
						|
        hash = _bcrypt.hashpw(secret, config)
 | 
						|
        assert isinstance(hash, bytes)
 | 
						|
        if not hash.startswith(config) or len(hash) != len(config)+31:
 | 
						|
            raise uh.exc.CryptBackendError(self, config, hash, source="`bcrypt` package")
 | 
						|
        return hash[-31:].decode("ascii")
 | 
						|
 | 
						|
#-----------------------------------------------------------------------
 | 
						|
# bcryptor backend
 | 
						|
#-----------------------------------------------------------------------
 | 
						|
class _BcryptorBackend(_BcryptCommon):
 | 
						|
    """
 | 
						|
    backend which uses 'bcryptor' package
 | 
						|
    """
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _load_backend_mixin(mixin_cls, name, dryrun):
 | 
						|
        # try to import bcryptor
 | 
						|
        global _bcryptor
 | 
						|
        try:
 | 
						|
            import bcryptor as _bcryptor
 | 
						|
        except ImportError: # pragma: no cover
 | 
						|
            return False
 | 
						|
 | 
						|
        # deprecated as of 1.7.2
 | 
						|
        if not dryrun:
 | 
						|
            warn("Support for `bcryptor` is deprecated, and will be removed in Passlib 1.8; "
 | 
						|
                 "Please use `pip install bcrypt` instead", DeprecationWarning)
 | 
						|
 | 
						|
        return mixin_cls._finalize_backend_mixin(name, dryrun)
 | 
						|
 | 
						|
    def _calc_checksum(self, secret):
 | 
						|
        # bcryptor behavior:
 | 
						|
        #   py2: unicode secret/hash encoded as ascii bytes before use,
 | 
						|
        #        bytes taken as-is; returns ascii bytes.
 | 
						|
        #   py3: not supported
 | 
						|
        secret, ident = self._prepare_digest_args(secret)
 | 
						|
        config = self._get_config(ident)
 | 
						|
        hash = _bcryptor.engine.Engine(False).hash_key(secret, config)
 | 
						|
        if not hash.startswith(config) or len(hash) != len(config) + 31:
 | 
						|
            raise uh.exc.CryptBackendError(self, config, hash, source="bcryptor library")
 | 
						|
        return str_to_uascii(hash[-31:])
 | 
						|
 | 
						|
#-----------------------------------------------------------------------
 | 
						|
# pybcrypt backend
 | 
						|
#-----------------------------------------------------------------------
 | 
						|
class _PyBcryptBackend(_BcryptCommon):
 | 
						|
    """
 | 
						|
    backend which uses 'pybcrypt' package
 | 
						|
    """
 | 
						|
 | 
						|
    #: classwide thread lock used for pybcrypt < 0.3
 | 
						|
    _calc_lock = None
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _load_backend_mixin(mixin_cls, name, dryrun):
 | 
						|
        # try to import pybcrypt
 | 
						|
        global _pybcrypt
 | 
						|
        if not _detect_pybcrypt():
 | 
						|
            # not installed, or bcrypt installed instead
 | 
						|
            return False
 | 
						|
        try:
 | 
						|
            import bcrypt as _pybcrypt
 | 
						|
        except ImportError: # pragma: no cover
 | 
						|
            # XXX: should we raise AssertionError here? (if get here, _detect_pybcrypt() is broken)
 | 
						|
            return False
 | 
						|
 | 
						|
        # deprecated as of 1.7.2
 | 
						|
        if not dryrun:
 | 
						|
            warn("Support for `py-bcrypt` is deprecated, and will be removed in Passlib 1.8; "
 | 
						|
                 "Please use `pip install bcrypt` instead", DeprecationWarning)
 | 
						|
 | 
						|
        # determine pybcrypt version
 | 
						|
        try:
 | 
						|
            version = _pybcrypt._bcrypt.__version__
 | 
						|
        except:
 | 
						|
            log.warning("(trapped) error reading pybcrypt version", exc_info=True)
 | 
						|
            version = "<unknown>"
 | 
						|
        log.debug("detected 'pybcrypt' backend, version %r", version)
 | 
						|
 | 
						|
        # return calc function based on version
 | 
						|
        vinfo = parse_version(version) or (0, 0)
 | 
						|
        if vinfo < (0, 3):
 | 
						|
            warn("py-bcrypt %s has a major security vulnerability, "
 | 
						|
                 "you should upgrade to py-bcrypt 0.3 immediately."
 | 
						|
                 % version, uh.exc.PasslibSecurityWarning)
 | 
						|
            if mixin_cls._calc_lock is None:
 | 
						|
                import threading
 | 
						|
                mixin_cls._calc_lock = threading.Lock()
 | 
						|
            mixin_cls._calc_checksum = get_unbound_method_function(mixin_cls._calc_checksum_threadsafe)
 | 
						|
 | 
						|
        return mixin_cls._finalize_backend_mixin(name, dryrun)
 | 
						|
 | 
						|
    def _calc_checksum_threadsafe(self, secret):
 | 
						|
        # as workaround for pybcrypt < 0.3's concurrency issue,
 | 
						|
        # we wrap everything in a thread lock. as long as bcrypt is only
 | 
						|
        # used through passlib, this should be safe.
 | 
						|
        with self._calc_lock:
 | 
						|
            return self._calc_checksum_raw(secret)
 | 
						|
 | 
						|
    def _calc_checksum_raw(self, secret):
 | 
						|
        # py-bcrypt behavior:
 | 
						|
        #   py2: unicode secret/hash encoded as ascii bytes before use,
 | 
						|
        #        bytes taken as-is; returns ascii bytes.
 | 
						|
        #   py3: unicode secret encoded as utf-8 bytes,
 | 
						|
        #        hash encoded as ascii bytes, returns ascii unicode.
 | 
						|
        secret, ident = self._prepare_digest_args(secret)
 | 
						|
        config = self._get_config(ident)
 | 
						|
        hash = _pybcrypt.hashpw(secret, config)
 | 
						|
        if not hash.startswith(config) or len(hash) != len(config) + 31:
 | 
						|
            raise uh.exc.CryptBackendError(self, config, hash, source="pybcrypt library")
 | 
						|
        return str_to_uascii(hash[-31:])
 | 
						|
 | 
						|
    _calc_checksum = _calc_checksum_raw
 | 
						|
 | 
						|
#-----------------------------------------------------------------------
 | 
						|
# os crypt backend
 | 
						|
#-----------------------------------------------------------------------
 | 
						|
class _OsCryptBackend(_BcryptCommon):
 | 
						|
    """
 | 
						|
    backend which uses :func:`crypt.crypt`
 | 
						|
    """
 | 
						|
 | 
						|
    #: set flag to ensure _prepare_digest_args() doesn't create invalid utf8 string
 | 
						|
    #: when truncating bytes.
 | 
						|
    _require_valid_utf8_bytes = not crypt_accepts_bytes
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _load_backend_mixin(mixin_cls, name, dryrun):
 | 
						|
        if not test_crypt("test", TEST_HASH_2A):
 | 
						|
            return False
 | 
						|
        return mixin_cls._finalize_backend_mixin(name, dryrun)
 | 
						|
 | 
						|
    def _calc_checksum(self, secret):
 | 
						|
        #
 | 
						|
        # run secret through crypt.crypt().
 | 
						|
        # if everything goes right, we'll get back a properly formed bcrypt hash.
 | 
						|
        #
 | 
						|
        secret, ident = self._prepare_digest_args(secret)
 | 
						|
        config = self._get_config(ident)
 | 
						|
        hash = safe_crypt(secret, config)
 | 
						|
        if hash is not None:
 | 
						|
            if not hash.startswith(config) or len(hash) != len(config) + 31:
 | 
						|
                raise uh.exc.CryptBackendError(self, config, hash)
 | 
						|
            return hash[-31:]
 | 
						|
 | 
						|
        #
 | 
						|
        # Check if this failed due to non-UTF8 bytes
 | 
						|
        # In detail: under py3, crypt.crypt() requires unicode inputs, which are then encoded to
 | 
						|
        # utf8 before passing them to os crypt() call.  this is done according to the "s" format
 | 
						|
        # specifier for PyArg_ParseTuple (https://docs.python.org/3/c-api/arg.html).
 | 
						|
        # There appears no way to get around that to pass raw bytes; so we just throw error here
 | 
						|
        # to let user know they need to use another backend if they want raw bytes support.
 | 
						|
        #
 | 
						|
        # XXX: maybe just let safe_crypt() throw UnicodeDecodeError under passlib 2.0,
 | 
						|
        #      and then catch it above? maybe have safe_crypt ALWAYS throw error
 | 
						|
        #      instead of returning None? (would save re-detecting what went wrong)
 | 
						|
        # XXX: isn't secret ALWAYS bytes at this point?
 | 
						|
        #
 | 
						|
        if PY3 and isinstance(secret, bytes):
 | 
						|
            try:
 | 
						|
                secret.decode("utf-8")
 | 
						|
            except UnicodeDecodeError:
 | 
						|
                raise error_from(uh.exc.PasswordValueError(
 | 
						|
                    "python3 crypt.crypt() ony supports bytes passwords using UTF8; "
 | 
						|
                    "passlib recommends running `pip install bcrypt` for general bcrypt support.",
 | 
						|
                    ), None)
 | 
						|
 | 
						|
        #
 | 
						|
        # else crypt() call failed for unknown reason.
 | 
						|
        #
 | 
						|
        # NOTE: getting here should be considered a bug in passlib --
 | 
						|
        #       if os_crypt backend detection said there's support,
 | 
						|
        #       and we've already checked all known reasons above;
 | 
						|
        #       want them to file bug so we can figure out what happened.
 | 
						|
        #       in the meantime, users can avoid this by installing bcrypt-cffi backend;
 | 
						|
        #       which won't have this (or utf8) edgecases.
 | 
						|
        #
 | 
						|
        # XXX: throw something more specific, like an "InternalBackendError"?
 | 
						|
        # NOTE: if do change this error, need to update test_81_crypt_fallback() expectations
 | 
						|
        #       about what will be thrown; as well as safe_verify() above.
 | 
						|
        #
 | 
						|
        debug_only_repr = uh.exc.debug_only_repr
 | 
						|
        raise uh.exc.InternalBackendError(
 | 
						|
            "crypt.crypt() failed for unknown reason; "
 | 
						|
            "passlib recommends running `pip install bcrypt` for general bcrypt support."
 | 
						|
            # for debugging UTs --
 | 
						|
            "(config=%s, secret=%s)" % (debug_only_repr(config), debug_only_repr(secret)),
 | 
						|
            )
 | 
						|
 | 
						|
#-----------------------------------------------------------------------
 | 
						|
# builtin backend
 | 
						|
#-----------------------------------------------------------------------
 | 
						|
class _BuiltinBackend(_BcryptCommon):
 | 
						|
    """
 | 
						|
    backend which uses passlib's pure-python implementation
 | 
						|
    """
 | 
						|
    @classmethod
 | 
						|
    def _load_backend_mixin(mixin_cls, name, dryrun):
 | 
						|
        from passlib.utils import as_bool
 | 
						|
        if not as_bool(os.environ.get("PASSLIB_BUILTIN_BCRYPT")):
 | 
						|
            log.debug("bcrypt 'builtin' backend not enabled via $PASSLIB_BUILTIN_BCRYPT")
 | 
						|
            return False
 | 
						|
        global _builtin_bcrypt
 | 
						|
        from passlib.crypto._blowfish import raw_bcrypt as _builtin_bcrypt
 | 
						|
        return mixin_cls._finalize_backend_mixin(name, dryrun)
 | 
						|
 | 
						|
    def _calc_checksum(self, secret):
 | 
						|
        secret, ident = self._prepare_digest_args(secret)
 | 
						|
        chk = _builtin_bcrypt(secret, ident[1:-1],
 | 
						|
                              self.salt.encode("ascii"), self.rounds)
 | 
						|
        return chk.decode("ascii")
 | 
						|
 | 
						|
#=============================================================================
 | 
						|
# handler
 | 
						|
#=============================================================================
 | 
						|
class bcrypt(_NoBackend, _BcryptCommon):
 | 
						|
    """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.using` method accepts 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 (``"2b"``) 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"`` - some implementations suffered from rare security flaws, replaced by 2b.
 | 
						|
        * ``"2y"`` - format specific to the *crypt_blowfish* BCrypt implementation,
 | 
						|
          identical to ``"2b"`` in all but name.
 | 
						|
        * ``"2b"`` - latest revision of the official BCrypt algorithm, current default.
 | 
						|
 | 
						|
    :param bool truncate_error:
 | 
						|
        By default, BCrypt will silently truncate passwords larger than 72 bytes.
 | 
						|
        Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash`
 | 
						|
        to raise a :exc:`~passlib.exc.PasswordTruncateError` instead.
 | 
						|
 | 
						|
        .. versionadded:: 1.7
 | 
						|
 | 
						|
    :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 <crypt-blowfish-bug>`
 | 
						|
        for details).
 | 
						|
 | 
						|
    .. versionchanged:: 1.6
 | 
						|
        Added a pure-python backend.
 | 
						|
 | 
						|
    .. versionchanged:: 1.6.3
 | 
						|
 | 
						|
        Added support for ``"2b"`` variant.
 | 
						|
 | 
						|
    .. versionchanged:: 1.7
 | 
						|
 | 
						|
        Now defaults to ``"2b"`` variant.
 | 
						|
    """
 | 
						|
    #=============================================================================
 | 
						|
    # backend
 | 
						|
    #=============================================================================
 | 
						|
 | 
						|
    # NOTE: the brunt of the bcrypt class is implemented in _BcryptCommon.
 | 
						|
    #       there are then subclass for each backend (e.g. _PyBcryptBackend),
 | 
						|
    #       these are dynamically prepended to this class's bases
 | 
						|
    #       in order to load the appropriate backend.
 | 
						|
 | 
						|
    #: list of potential backends
 | 
						|
    backends = ("bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin")
 | 
						|
 | 
						|
    #: flag that this class's bases should be modified by SubclassBackendMixin
 | 
						|
    _backend_mixin_target = True
 | 
						|
 | 
						|
    #: map of backend -> mixin class, used by _get_backend_loader()
 | 
						|
    _backend_mixin_map = {
 | 
						|
        None: _NoBackend,
 | 
						|
        "bcrypt": _BcryptBackend,
 | 
						|
        "pybcrypt": _PyBcryptBackend,
 | 
						|
        "bcryptor": _BcryptorBackend,
 | 
						|
        "os_crypt": _OsCryptBackend,
 | 
						|
        "builtin": _BuiltinBackend,
 | 
						|
    }
 | 
						|
 | 
						|
    #=============================================================================
 | 
						|
    # eoc
 | 
						|
    #=============================================================================
 | 
						|
 | 
						|
#=============================================================================
 | 
						|
# variants
 | 
						|
#=============================================================================
 | 
						|
_UDOLLAR = u("$")
 | 
						|
 | 
						|
# XXX: it might be better to have all the bcrypt variants share a common base class,
 | 
						|
#      and have the (django_)bcrypt_sha256 wrappers just proxy bcrypt instead of subclassing it.
 | 
						|
class _wrapped_bcrypt(bcrypt):
 | 
						|
    """
 | 
						|
    abstracts out some bits bcrypt_sha256 & django_bcrypt_sha256 share.
 | 
						|
    - bypass backend-loading wrappers for hash() etc
 | 
						|
    - disable truncation support, sha256 wrappers don't need it.
 | 
						|
    """
 | 
						|
    setting_kwds = tuple(elem for elem in bcrypt.setting_kwds if elem not in ["truncate_error"])
 | 
						|
    truncate_size = None
 | 
						|
 | 
						|
    # XXX: these will be needed if any bcrypt backends directly implement this...
 | 
						|
    # @classmethod
 | 
						|
    # def hash(cls, secret, **kwds):
 | 
						|
    #     # bypass bcrypt backend overriding this method
 | 
						|
    #     # XXX: would wrapping bcrypt make this easier than subclassing it?
 | 
						|
    #     return super(_BcryptCommon, cls).hash(secret, **kwds)
 | 
						|
    #
 | 
						|
    # @classmethod
 | 
						|
    # def verify(cls, secret, hash):
 | 
						|
    #     # bypass bcrypt backend overriding this method
 | 
						|
    #     return super(_BcryptCommon, cls).verify(secret, hash)
 | 
						|
    #
 | 
						|
    # @classmethod
 | 
						|
    # def genhash(cls, secret, hash):
 | 
						|
    #     # bypass bcrypt backend overriding this method
 | 
						|
    #     return super(_BcryptCommon, cls).genhash(secret, hash)
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _check_truncate_policy(cls, secret):
 | 
						|
        # disable check performed by bcrypt(), since this doesn't truncate passwords.
 | 
						|
        pass
 | 
						|
 | 
						|
#=============================================================================
 | 
						|
# bcrypt sha256 wrapper
 | 
						|
#=============================================================================
 | 
						|
 | 
						|
class bcrypt_sha256(_wrapped_bcrypt):
 | 
						|
    """
 | 
						|
    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.
 | 
						|
 | 
						|
    The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept
 | 
						|
    all the same optional keywords as the base :class:`bcrypt` hash.
 | 
						|
 | 
						|
    .. versionadded:: 1.6.2
 | 
						|
 | 
						|
    .. versionchanged:: 1.7
 | 
						|
 | 
						|
        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
 | 
						|
    #===================================================================
 | 
						|
 | 
						|
    #--------------------
 | 
						|
    # PasswordHash
 | 
						|
    #--------------------
 | 
						|
    name = "bcrypt_sha256"
 | 
						|
 | 
						|
    #--------------------
 | 
						|
    # GenericHandler
 | 
						|
    #--------------------
 | 
						|
    # 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...
 | 
						|
    ident_aliases = (lambda ident_values: dict(item for item in bcrypt.ident_aliases.items()
 | 
						|
                                               if item[1] in ident_values))(ident_values)
 | 
						|
    default_ident = IDENT_2B
 | 
						|
 | 
						|
    #--------------------
 | 
						|
    # class specific
 | 
						|
    #--------------------
 | 
						|
 | 
						|
    _supported_versions = set([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
 | 
						|
    #===================================================================
 | 
						|
 | 
						|
    # 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$')
 | 
						|
 | 
						|
    #: 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<type>2[ab]),
 | 
						|
        (?P<rounds>\d{1,2})
 | 
						|
        [$](?P<salt>[^$]{22})
 | 
						|
        (?:[$](?P<digest>[^$]{31}))?
 | 
						|
        $
 | 
						|
        """)
 | 
						|
 | 
						|
    @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._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(
 | 
						|
            version=version,
 | 
						|
            ident=m.group("type"),
 | 
						|
            rounds=int(rounds),
 | 
						|
            salt=m.group("salt"),
 | 
						|
            checksum=m.group("digest"),
 | 
						|
        )
 | 
						|
 | 
						|
    _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):
 | 
						|
        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
 | 
						|
        #       (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).
 | 
						|
        # XXX: it's later come out that 55-72 may be ok, so later revision of bcrypt_sha256
 | 
						|
        #      may switch to hex encoding, since it's simpler to implement elsewhere.
 | 
						|
        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(digest)
 | 
						|
 | 
						|
        # hand result off to normal bcrypt algorithm
 | 
						|
        return super(bcrypt_sha256, self)._calc_checksum(key)
 | 
						|
 | 
						|
    #===================================================================
 | 
						|
    # other
 | 
						|
    #===================================================================
 | 
						|
 | 
						|
    def _calc_needs_update(self, **kwds):
 | 
						|
        if self.version < type(self).version:
 | 
						|
            return True
 | 
						|
        return super(bcrypt_sha256, self)._calc_needs_update(**kwds)
 | 
						|
 | 
						|
    #===================================================================
 | 
						|
    # eoc
 | 
						|
    #===================================================================
 | 
						|
 | 
						|
#=============================================================================
 | 
						|
# eof
 | 
						|
#=============================================================================
 |