Server IP : 127.0.0.2 / Your IP : 18.117.9.230 Web Server : Apache/2.4.18 (Ubuntu) System : User : www-data ( ) PHP Version : 7.0.33-0ubuntu0.16.04.16 Disable Function : disk_free_space,disk_total_space,diskfreespace,dl,exec,fpaththru,getmyuid,getmypid,highlight_file,ignore_user_abord,leak,listen,link,opcache_get_configuration,opcache_get_status,passthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,php_uname,phpinfo,posix_ctermid,posix_getcwd,posix_getegid,posix_geteuid,posix_getgid,posix_getgrgid,posix_getgrnam,posix_getgroups,posix_getlogin,posix_getpgid,posix_getpgrp,posix_getpid,posix,_getppid,posix_getpwnam,posix_getpwuid,posix_getrlimit,posix_getsid,posix_getuid,posix_isatty,posix_kill,posix_mkfifo,posix_setegid,posix_seteuid,posix_setgid,posix_setpgid,posix_setsid,posix_setuid,posix_times,posix_ttyname,posix_uname,pclose,popen,proc_open,proc_close,proc_get_status,proc_nice,proc_terminate,shell_exec,source,show_source,system,virtual MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : ON Directory : /usr/lib/python2.7/dist-packages/passlib/handlers/ |
Upload File : |
"""passlib.bcrypt -- implementation of OpenBSD's BCrypt algorithm. TODO: * support 2x and altered-2a hashes? http://www.openwall.com/lists/oss-security/2011/06/27/9 * deal with lack of PY3-compatibile c-ext implementation """ #============================================================================= # imports #============================================================================= from __future__ import with_statement, absolute_import # core from base64 import b64encode from hashlib import sha256 import os import re import logging; log = logging.getLogger(__name__) from warnings import warn # site try: import bcrypt as _bcrypt except ImportError: # pragma: no cover _bcrypt = None try: import bcryptor as _bcryptor except ImportError: # pragma: no cover _bcryptor = None # pkg from passlib.exc import PasslibHashWarning, PasslibSecurityWarning, PasslibSecurityError from passlib.utils import bcrypt64, safe_crypt, repeat_string, to_bytes, \ classproperty, rng, getrandstr, test_crypt, to_unicode from passlib.utils.compat import bytes, b, u, uascii_to_str, unicode, str_to_uascii import passlib.utils.handlers as uh # local __all__ = [ "bcrypt", ] #============================================================================= # support funcs & constants #============================================================================= _builtin_bcrypt = None def _load_builtin(): global _builtin_bcrypt if _builtin_bcrypt is None: from passlib.utils._blowfish import raw_bcrypt as _builtin_bcrypt IDENT_2 = u("$2$") IDENT_2A = u("$2a$") IDENT_2X = u("$2x$") IDENT_2Y = u("$2y$") IDENT_2B = u("$2b$") _BNULL = b('\x00') 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: 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 #============================================================================= # handler #============================================================================= class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.GenericHandler): """This class implements the BCrypt password hash, and follows the :ref:`password-hash-api`. It supports a fixed-length salt, and a variable number of rounds. The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``. :type rounds: int :param rounds: Optional number of rounds to use. Defaults to 12, must be between 4 and 31, inclusive. This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}` -- increasing the rounds by +1 will double the amount of time taken. :type ident: str :param ident: Specifies which version of the BCrypt algorithm will be used when creating a new hash. Typically this option is not needed, as the default (``"2a"``) is usually the correct choice. If specified, it must be one of the following: * ``"2"`` - the first revision of BCrypt, which suffers from a minor security flaw and is generally not used anymore. * ``"2a"`` - some implementations suffered from a very rare security flaw. current default for compatibility purposes. * ``"2y"`` - format specific to the *crypt_blowfish* BCrypt implementation, identical to ``"2a"`` in all but name. * ``"2b"`` - latest revision of the official BCrypt algorithm (will be default in Passlib 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. """ #=================================================================== # class attrs #=================================================================== #--GenericHandler-- name = "bcrypt" setting_kwds = ("salt", "rounds", "ident") checksum_size = 31 checksum_chars = bcrypt64.charmap #--HasManyIdents-- default_ident = IDENT_2A ident_values = (IDENT_2, IDENT_2A, IDENT_2X, IDENT_2Y, IDENT_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 bcrypt64._padinfo2[1], not full charmap #--HasRounds-- default_rounds = 12 # current passlib default min_rounds = 4 # minimum from bcrypt specification max_rounds = 31 # 32-bit integer limit (since real_rounds=1<<rounds) rounds_cost = "log2" #=================================================================== # 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 or u('')) 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) #=================================================================== # specialized salt generation - fixes passlib issue 25 #=================================================================== @classmethod def _bind_needs_update(cls, **settings): return cls._needs_update @classmethod def _needs_update(cls, hash, secret): if isinstance(hash, bytes): hash = hash.decode("ascii") # check for incorrect padding bits (passlib issue 25) if hash.startswith(IDENT_2A) and hash[28] not in bcrypt64._padinfo2[1]: return True # TODO: try to detect incorrect $2x$ hashes using *secret* return False @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 def _generate_salt(self, salt_size): # generate random salt as normal, # but repair last char so the padding bits always decode to zero. salt = super(bcrypt, self)._generate_salt(salt_size) return bcrypt64.repair_unused(salt) def _norm_salt(self, salt, **kwds): salt = super(bcrypt, self)._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; see Passlib 1.5.3 changelog.", PasslibHashWarning) return salt def _norm_checksum(self, checksum): checksum = super(bcrypt, self)._norm_checksum(checksum) if not checksum: return None 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; see Passlib 1.5.3 changelog.", PasslibHashWarning) return checksum #=================================================================== # primary interface #=================================================================== backends = ("bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin") # backend workaround detection _has_wraparound_bug = False _lacks_20_support = False _lacks_2y_support = False _lacks_2b_support = False @classmethod def set_backend(cls, *a, **k): backend = super(bcrypt, cls).set_backend(*a, **k) cls._scan_backend(backend) return backend @classmethod def _scan_backend(cls, backend): """ check for known bugs & feature support once backend is loaded """ # 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> # NOTE: reference hash taken from above url, and is the incorrectly generate 2x hash. if cls.verify(u("\xA3"), "$2a$05$/OK.fbVrR/bpIqNJ5ianF.CE5elHaaO4EbggVDjb8P19RukzXSM3e"): raise PasslibSecurityError( "passlib.hash.bcrypt: Your installation of the %r backend is vulnerable to " "the crypt_blowfish 8-bit bug (CVE-2011-2483), " "and should be upgraded or replaced with another backend." % backend) # 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)' cls._has_wraparound_bug = False if cls.verify(("0123456789"*26)[:255], "$2a$04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6"): 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 " "(this warning will be fatal under passlib 1.7)" % backend) cls._has_wraparound_bug = True def _detect_lacks_variant(ident, refhash): """helper to detect if backend *lacks* support for specified bcrypt variant""" assert refhash.startswith(ident) # NOTE: can't use cls.verify() directly or we have recursion error try: result = cls.verify("test", refhash) except (ValueError, _bcryptor.engine.SaltError if _bcryptor else ValueError): # backends without support will throw various errors about unrecognized version # pybcrypt, bcrypt -- raises ValueError # bcryptor -- raises bcryptor.engine.SaltError log.debug("%r backend lacks %r support", backend, ident) return True assert result, "%r backend %r check failed" % (backend, ident) return False # check for native 2 support # NOTE: have to clear workaround first, so verify() doesn't enable it during detection. cls._lacks_20_support = False cls._lacks_20_support = _detect_lacks_variant("$2$", "$2$04$5BJqKfqMQvV7nS.yUguNcu" "RfMMOXK0xPWavM7pOzjEi5ze5T1k8/S") # TODO: check for 2x support # check for native 2y support cls._lacks_2y_support = False cls._lacks_2y_support = _detect_lacks_variant("$2y$", "$2y$04$5BJqKfqMQvV7nS.yUguNcu" "eVirQqDBGaLXSqj.rs.pZPlNR0UX/HK") # check for native 2b support cls._lacks_2b_support = False cls._lacks_2b_support = _detect_lacks_variant("$2b$", "$2b$04$5BJqKfqMQvV7nS.yUguNcu" "eVirQqDBGaLXSqj.rs.pZPlNR0UX/HK") # sanity check assert cls._lacks_2b_support or not cls._has_wraparound_bug, \ "sanity check failed: %r backend supports $2b$ but has wraparound bug" % backend @classproperty def _has_backend_bcrypt(cls): return _bcrypt is not None and not _detect_pybcrypt() @classproperty def _has_backend_pybcrypt(cls): return _bcrypt is not None and _detect_pybcrypt() @classproperty def _has_backend_bcryptor(cls): return _bcryptor is not None @classproperty def _has_backend_builtin(cls): if os.environ.get("PASSLIB_BUILTIN_BCRYPT") not in ["enable","enabled"]: return False # look at it cross-eyed, and it loads itself _load_builtin() return True @classproperty def _has_backend_os_crypt(cls): # XXX: what to do if "2" isn't supported, but "2a" is? # "2" is *very* rare, and can fake it using "2a"+repeat_string h1 = '$2$04$......................1O4gOrCYaqBG3o/4LnT2ykQUt1wbyju' h2 = '$2a$04$......................qiOQjkB8hxU8OzRhS.GhRMa4VUnkPty' return test_crypt("test",h1) and test_crypt("test", h2) @classmethod def _no_backends_msg(cls): return "no bcrypt backends available -- recommend you install one (e.g. 'pip install bcrypt')" def _calc_checksum(self, secret): """common backend code""" # make sure it's unicode if isinstance(secret, unicode): secret = secret.encode("utf-8") # 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(self) # ensure backend is loaded before workaround detection self.get_backend() # protect from wraparound bug by truncating secret before handing it to the backend. # bcrypt only uses first 72 bytes anyways. if self._has_wraparound_bug and len(secret) >= 255: secret = secret[:72] # special case handling for variants (ordered most common first) ident = self.ident if ident == IDENT_2A: # fall through and use backend w/o hacks pass elif ident == IDENT_2B: if self._lacks_2b_support: # handle $2b$ hash format even if backend is too old. # have it generate a 2A digest, then return it as a 2B hash. ident = IDENT_2A elif ident == IDENT_2Y: if self._lacks_2y_support: # handle $2y$ hash format (not supported by BSDs, being phased out on others) # have it generate a 2A digest, then return it as a 2Y hash. ident = IDENT_2A elif ident == IDENT_2: if self._lacks_20_support: # handle legacy $2$ format (not supported by most backends except BSD os_crypt) # we can fake $2$ behavior using the $2a$ algorithm # by repeating the password until it's at least 72 chars in length. if secret: secret = repeat_string(secret, 72) ident = IDENT_2A 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) # invoke backend config = self._get_config(ident) return self._calc_checksum_backend(secret, config) def _calc_checksum_os_crypt(self, secret, config): hash = safe_crypt(secret, config) if hash: assert hash.startswith(config) and len(hash) == len(config)+31 return hash[-31:] else: # NOTE: Have to raise this error because python3's crypt.crypt() only accepts unicode. # This means it can't handle any passwords that aren't either unicode # or utf-8 encoded bytes. However, hashing a password with an alternate # encoding should be a pretty rare edge case; if user needs it, they can just # install bcrypt backend. # XXX: is this the right error type to raise? # maybe have safe_crypt() not swallow UnicodeDecodeError, and have handlers # like sha256_crypt trap it if they have alternate method of handling them? raise uh.exc.MissingBackendError( "non-utf8 encoded passwords can't be handled by crypt.crypt() under python3, " "recommend running `pip install bcrypt`.", ) def _calc_checksum_bcrypt(self, secret, config): # bcrypt behavior: # hash must be ascii bytes # secret must be bytes # returns bytes if isinstance(config, unicode): config = config.encode("ascii") hash = _bcrypt.hashpw(secret, config) assert hash.startswith(config) and len(hash) == len(config)+31 assert isinstance(hash, bytes) return hash[-31:].decode("ascii") def _calc_checksum_pybcrypt(self, secret, config): # 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. hash = _bcrypt.hashpw(secret, config) assert hash.startswith(config) and len(hash) == len(config)+31 return str_to_uascii(hash[-31:]) def _calc_checksum_bcryptor(self, secret, config): # bcryptor behavior: # py2: unicode secret/hash encoded as ascii bytes before use, # bytes taken as-is; returns ascii bytes. # py3: not supported hash = _bcryptor.engine.Engine(False).hash_key(secret, config) assert hash.startswith(config) and len(hash) == len(config)+31 return str_to_uascii(hash[-31:]) def _calc_checksum_builtin(self, secret, config): chk = _builtin_bcrypt(secret, config[1:config.index("$", 1)], self.salt.encode("ascii"), self.rounds) return chk.decode("ascii") #=================================================================== # eoc #=================================================================== _UDOLLAR = u("$") class bcrypt_sha256(bcrypt): """This class implements a composition of BCrypt+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.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept all the same optional keywords as the base :class:`bcrypt` hash. .. versionadded:: 1.6.2 """ name = "bcrypt_sha256" # this is locked at 2a for now. ident_values = (IDENT_2A,) # 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$') _hash_re = re.compile(r""" ^ [$]bcrypt-sha256 [$](?P<variant>[a-z0-9]+) ,(?P<rounds>\d{1,2}) [$](?P<salt>[^$]{22}) ([$](?P<digest>.{31}))? $ """, re.X) @classmethod def identify(cls, hash): hash = uh.to_unicode_for_identify(hash) if not hash: return False return hash.startswith(cls.prefix) @classmethod def from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") if not hash.startswith(cls.prefix): raise uh.exc.InvalidHashError(cls) m = cls._hash_re.match(hash) if not m: raise uh.exc.MalformedHashError(cls) rounds = m.group("rounds") if rounds.startswith(uh._UZERO) and rounds != uh._UZERO: raise uh.exc.ZeroPaddedRoundsError(cls) return cls(ident=m.group("variant"), rounds=int(rounds), salt=m.group("salt"), checksum=m.group("digest"), ) def to_string(self): hash = u("%s%s,%d$%s") % (self.prefix, self.ident.strip(_UDOLLAR), self.rounds, self.salt) if self.checksum: hash = u("%s$%s") % (hash, self.checksum) return uascii_to_str(hash) def _calc_checksum(self, secret): # NOTE: 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") key = b64encode(sha256(secret).digest()) # hand result off to normal bcrypt algorithm return super(bcrypt_sha256, self)._calc_checksum(key) # patch set_backend so it modifies bcrypt class, not this one... # else the bcrypt.set_backend() tests will call the wrong class. @classmethod def set_backend(cls, *args, **kwds): return bcrypt.set_backend(*args, **kwds) #============================================================================= # eof #=============================================================================