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
			| 
								 
											3 years ago
										 
									 | 
							
								"""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
							 | 
						||
| 
								 | 
							
								#=============================================================================
							 |