codekingpro/portable-devtools
114k
1"""passlib.bcrypt -- implementation of OpenBSD's BCrypt algorithm.2 3TODO:4 5* support 2x and altered-2a hashes?6 http://www.openwall.com/lists/oss-security/2011/06/27/97 8* deal with lack of PY3-compatibile c-ext implementation9"""10#=============================================================================11# imports12#=============================================================================13from __future__ import with_statement, absolute_import14# core15from base64 import b64encode16from hashlib import sha25617import os18import re19import logging; log = logging.getLogger(__name__)20from warnings import warn21# site22_bcrypt = None # dynamically imported by _load_backend_bcrypt()23_pybcrypt = None # dynamically imported by _load_backend_pybcrypt()24_bcryptor = None # dynamically imported by _load_backend_bcryptor()25# pkg26_builtin_bcrypt = None # dynamically imported by _load_backend_builtin()27from passlib.crypto.digest import compile_hmac28from passlib.exc import PasslibHashWarning, PasslibSecurityWarning, PasslibSecurityError29from passlib.utils import safe_crypt, repeat_string, to_bytes, parse_version, \30 rng, getrandstr, test_crypt, to_unicode, \31 utf8_truncate, utf8_repeat_string, crypt_accepts_bytes32from passlib.utils.binary import bcrypt6433from passlib.utils.compat import get_unbound_method_function34from passlib.utils.compat import u, uascii_to_str, unicode, str_to_uascii, PY3, error_from35import passlib.utils.handlers as uh36 37# local38__all__ = [39 "bcrypt",40]41 42#=============================================================================43# support funcs & constants44#=============================================================================45IDENT_2 = u("$2$")46IDENT_2A = u("$2a$")47IDENT_2X = u("$2x$")48IDENT_2Y = u("$2y$")49IDENT_2B = u("$2b$")50_BNULL = b'\x00'51 52# reference hash of "test", used in various self-checks53TEST_HASH_2A = "$2a$04$5BJqKfqMQvV7nS.yUguNcueVirQqDBGaLXSqj.rs.pZPlNR0UX/HK"54 55def _detect_pybcrypt():56 """57 internal helper which tries to distinguish pybcrypt vs bcrypt.58 59 :returns:60 True if cext-based py-bcrypt,61 False if ffi-based bcrypt,62 None if 'bcrypt' module not found.63 64 .. versionchanged:: 1.6.365 66 Now assuming bcrypt installed, unless py-bcrypt explicitly detected.67 Previous releases assumed py-bcrypt by default.68 69 Making this change since py-bcrypt is (apparently) unmaintained and static,70 whereas bcrypt is being actively maintained, and it's internal structure may shift.71 """72 # NOTE: this is also used by the unittests.73 74 # check for module.75 try:76 import bcrypt77 except ImportError:78 # XXX: this is ignoring case where py-bcrypt's "bcrypt._bcrypt" C Ext fails to import;79 # would need to inspect actual ImportError message to catch that.80 return None81 82 # py-bcrypt has a "._bcrypt.__version__" attribute (confirmed for v0.1 - 0.4),83 # which bcrypt lacks (confirmed for v1.0 - 2.0)84 # "._bcrypt" alone isn't sufficient, since bcrypt 2.0 now has that attribute.85 try:86 from bcrypt._bcrypt import __version__87 except ImportError:88 return False89 return True90 91#=============================================================================92# backend mixins93#=============================================================================94class _BcryptCommon(uh.SubclassBackendMixin, uh.TruncateMixin, uh.HasManyIdents,95 uh.HasRounds, uh.HasSalt, uh.GenericHandler):96 """97 Base class which implements brunt of BCrypt code.98 This is then subclassed by the various backends,99 to override w/ backend-specific methods.100 101 When a backend is loaded, the bases of the 'bcrypt' class proper102 are modified to prepend the correct backend-specific subclass.103 """104 #===================================================================105 # class attrs106 #===================================================================107 108 #--------------------109 # PasswordHash110 #--------------------111 name = "bcrypt"112 setting_kwds = ("salt", "rounds", "ident", "truncate_error")113 114 #--------------------115 # GenericHandler116 #--------------------117 checksum_size = 31118 checksum_chars = bcrypt64.charmap119 120 #--------------------121 # HasManyIdents122 #--------------------123 default_ident = IDENT_2B124 ident_values = (IDENT_2, IDENT_2A, IDENT_2X, IDENT_2Y, IDENT_2B)125 ident_aliases = {u("2"): IDENT_2, u("2a"): IDENT_2A, u("2y"): IDENT_2Y,126 u("2b"): IDENT_2B}127 128 #--------------------129 # HasSalt130 #--------------------131 min_salt_size = max_salt_size = 22132 salt_chars = bcrypt64.charmap133 134 # NOTE: 22nd salt char must be in restricted set of ``final_salt_chars``, not full set above.135 final_salt_chars = ".Oeu" # bcrypt64._padinfo2[1]136 137 #--------------------138 # HasRounds139 #--------------------140 default_rounds = 12 # current passlib default141 min_rounds = 4 # minimum from bcrypt specification142 max_rounds = 31 # 32-bit integer limit (since real_rounds=1<<rounds)143 rounds_cost = "log2"144 145 #--------------------146 # TruncateMixin147 #--------------------148 truncate_size = 72149 150 #--------------------151 # custom152 #--------------------153 154 # backend workaround detection flags155 # NOTE: these are only set on the backend mixin classes156 _workrounds_initialized = False157 _has_2a_wraparound_bug = False158 _lacks_20_support = False159 _lacks_2y_support = False160 _lacks_2b_support = False161 _fallback_ident = IDENT_2A162 _require_valid_utf8_bytes = False163 164 #===================================================================165 # formatting166 #===================================================================167 168 @classmethod169 def from_string(cls, hash):170 ident, tail = cls._parse_ident(hash)171 if ident == IDENT_2X:172 raise ValueError("crypt_blowfish's buggy '2x' hashes are not "173 "currently supported")174 rounds_str, data = tail.split(u("$"))175 rounds = int(rounds_str)176 if rounds_str != u('%02d') % (rounds,):177 raise uh.exc.MalformedHashError(cls, "malformed cost field")178 salt, chk = data[:22], data[22:]179 return cls(180 rounds=rounds,181 salt=salt,182 checksum=chk or None,183 ident=ident,184 )185 186 def to_string(self):187 hash = u("%s%02d$%s%s") % (self.ident, self.rounds, self.salt, self.checksum)188 return uascii_to_str(hash)189 190 # NOTE: this should be kept separate from to_string()191 # so that bcrypt_sha256() can still use it, while overriding to_string()192 def _get_config(self, ident):193 """internal helper to prepare config string for backends"""194 config = u("%s%02d$%s") % (ident, self.rounds, self.salt)195 return uascii_to_str(config)196 197 #===================================================================198 # migration199 #===================================================================200 201 @classmethod202 def needs_update(cls, hash, **kwds):203 # NOTE: can't convert this to use _calc_needs_update() helper,204 # since _norm_hash() will correct salt padding before we can read it here.205 # check for incorrect padding bits (passlib issue 25)206 if isinstance(hash, bytes):207 hash = hash.decode("ascii")208 if hash.startswith(IDENT_2A) and hash[28] not in cls.final_salt_chars:209 return True210 211 # TODO: try to detect incorrect 8bit/wraparound hashes using kwds.get("secret")212 213 # hand off to base implementation, so HasRounds can check rounds value.214 return super(_BcryptCommon, cls).needs_update(hash, **kwds)215 216 #===================================================================217 # specialized salt generation - fixes passlib issue 25218 #===================================================================219 220 @classmethod221 def normhash(cls, hash):222 """helper to normalize hash, correcting any bcrypt padding bits"""223 if cls.identify(hash):224 return cls.from_string(hash).to_string()225 else:226 return hash227 228 @classmethod229 def _generate_salt(cls):230 # generate random salt as normal,231 # but repair last char so the padding bits always decode to zero.232 salt = super(_BcryptCommon, cls)._generate_salt()233 return bcrypt64.repair_unused(salt)234 235 @classmethod236 def _norm_salt(cls, salt, **kwds):237 salt = super(_BcryptCommon, cls)._norm_salt(salt, **kwds)238 assert salt is not None, "HasSalt didn't generate new salt!"239 changed, salt = bcrypt64.check_repair_unused(salt)240 if changed:241 # FIXME: if salt was provided by user, this message won't be242 # correct. not sure if we want to throw error, or use different warning.243 warn(244 "encountered a bcrypt salt with incorrectly set padding bits; "245 "you may want to use bcrypt.normhash() "246 "to fix this; this will be an error under Passlib 2.0",247 PasslibHashWarning)248 return salt249 250 def _norm_checksum(self, checksum, relaxed=False):251 checksum = super(_BcryptCommon, self)._norm_checksum(checksum, relaxed=relaxed)252 changed, checksum = bcrypt64.check_repair_unused(checksum)253 if changed:254 warn(255 "encountered a bcrypt hash with incorrectly set padding bits; "256 "you may want to use bcrypt.normhash() "257 "to fix this; this will be an error under Passlib 2.0",258 PasslibHashWarning)259 return checksum260 261 #===================================================================262 # backend configuration263 # NOTE: backends are defined in terms of mixin classes,264 # which are dynamically inserted into the bases of the 'bcrypt' class265 # via the machinery in 'SubclassBackendMixin'.266 # this lets us load in a backend-specific implementation267 # of _calc_checksum() and similar methods.268 #===================================================================269 270 # NOTE: backend config is located down in <bcrypt> class271 272 # NOTE: set_backend() will execute the ._load_backend_mixin()273 # of the matching mixin class, which will handle backend detection274 275 # appended to HasManyBackends' "no backends available" error message276 _no_backend_suggestion = " -- recommend you install one (e.g. 'pip install bcrypt')"277 278 @classmethod279 def _finalize_backend_mixin(mixin_cls, backend, dryrun):280 """281 helper called by from backend mixin classes' _load_backend_mixin() --282 invoked after backend imports have been loaded, and performs283 feature detection & testing common to all backends.284 """285 #----------------------------------------------------------------286 # setup helpers287 #----------------------------------------------------------------288 assert mixin_cls is bcrypt._backend_mixin_map[backend], \289 "_configure_workarounds() invoked from wrong class"290 291 if mixin_cls._workrounds_initialized:292 return True293 294 verify = mixin_cls.verify295 296 err_types = (ValueError, uh.exc.MissingBackendError)297 if _bcryptor:298 err_types += (_bcryptor.engine.SaltError,)299 300 def safe_verify(secret, hash):301 """verify() wrapper which traps 'unknown identifier' errors"""302 try:303 return verify(secret, hash)304 except err_types:305 # backends without support for given ident will throw various306 # errors about unrecognized version:307 # os_crypt -- internal code below throws308 # - PasswordValueError if there's encoding issue w/ password.309 # - InternalBackendError if crypt fails for unknown reason310 # (trapped below so we can debug it)311 # pybcrypt, bcrypt -- raises ValueError312 # bcryptor -- raises bcryptor.engine.SaltError313 return NotImplemented314 except uh.exc.InternalBackendError:315 # _calc_checksum() code may also throw CryptBackendError316 # if correct hash isn't returned (e.g. 2y hash converted to 2b,317 # such as happens with bcrypt 3.0.0)318 log.debug("trapped unexpected response from %r backend: verify(%r, %r):",319 backend, secret, hash, exc_info=True)320 return NotImplemented321 322 def assert_lacks_8bit_bug(ident):323 """324 helper to check for cryptblowfish 8bit bug (fixed in 2y/2b);325 even though it's not known to be present in any of passlib's backends.326 this is treated as FATAL, because it can easily result in seriously malformed hashes,327 and we can't correct for it ourselves.328 329 test cases from <http://cvsweb.openwall.com/cgi/cvsweb.cgi/Owl/packages/glibc/crypt_blowfish/wrapper.c.diff?r1=1.9;r2=1.10>330 reference hash is the incorrectly generated $2x$ hash taken from above url331 """332 # NOTE: passlib 1.7.2 and earlier used the commented-out LATIN-1 test vector to detect333 # this bug; but python3's crypt.crypt() only supports unicode inputs (and334 # always encodes them as UTF8 before passing to crypt); so passlib 1.7.3335 # switched to the UTF8-compatible test vector below. This one's bug_hash value336 # ("$2x$...rcAS") was drawn from the same openwall source (above); and the correct337 # hash ("$2a$...X6eu") was generated by passing the raw bytes to python2's338 # crypt.crypt() using OpenBSD 6.7 (hash confirmed as same for $2a$ & $2b$).339 340 # LATIN-1 test vector341 # secret = b"\xA3"342 # bug_hash = ident.encode("ascii") + b"05$/OK.fbVrR/bpIqNJ5ianF.CE5elHaaO4EbggVDjb8P19RukzXSM3e"343 # correct_hash = ident.encode("ascii") + b"05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq"344 345 # UTF-8 test vector346 secret = b"\xd1\x91" # aka "\u0451"347 bug_hash = ident.encode("ascii") + b"05$6bNw2HLQYeqHYyBfLMsv/OiwqTymGIGzFsA4hOTWebfehXHNprcAS"348 correct_hash = ident.encode("ascii") + b"05$6bNw2HLQYeqHYyBfLMsv/OUcZd0LKP39b87nBw3.S2tVZSqiQX6eu"349 350 if verify(secret, bug_hash):351 # NOTE: this only EVER be observed in (broken) 2a and (backward-compat) 2x hashes352 # generated by crypt_blowfish library. 2y/2b hashes should not have the bug353 # (but we check w/ them anyways).354 raise PasslibSecurityError(355 "passlib.hash.bcrypt: Your installation of the %r backend is vulnerable to "356 "the crypt_blowfish 8-bit bug (CVE-2011-2483) under %r hashes, "357 "and should be upgraded or replaced with another backend" % (backend, ident))358 359 # it doesn't have wraparound bug, but make sure it *does* verify against the correct360 # hash, or we're in some weird third case!361 if not verify(secret, correct_hash):362 raise RuntimeError("%s backend failed to verify %s 8bit hash" % (backend, ident))363 364 def detect_wrap_bug(ident):365 """366 check for bsd wraparound bug (fixed in 2b)367 this is treated as a warning, because it's rare in the field,368 and pybcrypt (as of 2015-7-21) is unpatched, but some people may be stuck with it.369 370 test cases from <http://www.openwall.com/lists/oss-security/2012/01/02/4>371 372 NOTE: reference hash is of password "0"*72373 374 NOTE: if in future we need to deliberately create hashes which have this bug,375 can use something like 'hashpw(repeat_string(secret[:((1+secret) % 256) or 1]), 72)'376 """377 # check if it exhibits wraparound bug378 secret = (b"0123456789"*26)[:255]379 bug_hash = ident.encode("ascii") + b"04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6"380 if verify(secret, bug_hash):381 return True382 383 # if it doesn't have wraparound bug, make sure it *does* handle things384 # correctly -- or we're in some weird third case.385 correct_hash = ident.encode("ascii") + b"04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi"386 if not verify(secret, correct_hash):387 raise RuntimeError("%s backend failed to verify %s wraparound hash" % (backend, ident))388 389 return False390 391 def assert_lacks_wrap_bug(ident):392 if not detect_wrap_bug(ident):393 return394 # should only see in 2a, later idents should NEVER exhibit this bug:395 # * 2y implementations should have been free of it396 # * 2b was what (supposedly) fixed it397 raise RuntimeError("%s backend unexpectedly has wraparound bug for %s" % (backend, ident))398 399 #----------------------------------------------------------------400 # check for old 20 support401 #----------------------------------------------------------------402 test_hash_20 = b"$2$04$5BJqKfqMQvV7nS.yUguNcuRfMMOXK0xPWavM7pOzjEi5ze5T1k8/S"403 result = safe_verify("test", test_hash_20)404 if result is NotImplemented:405 mixin_cls._lacks_20_support = True406 log.debug("%r backend lacks $2$ support, enabling workaround", backend)407 elif not result:408 raise RuntimeError("%s incorrectly rejected $2$ hash" % backend)409 410 #----------------------------------------------------------------411 # check for 2a support412 #----------------------------------------------------------------413 result = safe_verify("test", TEST_HASH_2A)414 if result is NotImplemented:415 # 2a support is required, and should always be present416 raise RuntimeError("%s lacks support for $2a$ hashes" % backend)417 elif not result:418 raise RuntimeError("%s incorrectly rejected $2a$ hash" % backend)419 else:420 assert_lacks_8bit_bug(IDENT_2A)421 if detect_wrap_bug(IDENT_2A):422 if backend == "os_crypt":423 # don't make this a warning for os crypt (e.g. openbsd);424 # they'll have proper 2b implementation which will be used for new hashes.425 # so even if we didn't have a workaround, this bug wouldn't be a concern.426 log.debug("%r backend has $2a$ bsd wraparound bug, enabling workaround", backend)427 else:428 # installed library has the bug -- want to let users know,429 # so they can upgrade it to something better (e.g. bcrypt cffi library)430 warn("passlib.hash.bcrypt: Your installation of the %r backend is vulnerable to "431 "the bsd wraparound bug, "432 "and should be upgraded or replaced with another backend "433 "(enabling workaround for now)." % backend,434 uh.exc.PasslibSecurityWarning)435 mixin_cls._has_2a_wraparound_bug = True436 437 #----------------------------------------------------------------438 # check for 2y support439 #----------------------------------------------------------------440 test_hash_2y = TEST_HASH_2A.replace("2a", "2y")441 result = safe_verify("test", test_hash_2y)442 if result is NotImplemented:443 mixin_cls._lacks_2y_support = True444 log.debug("%r backend lacks $2y$ support, enabling workaround", backend)445 elif not result:446 raise RuntimeError("%s incorrectly rejected $2y$ hash" % backend)447 else:448 # NOTE: Not using this as fallback candidate,449 # lacks wide enough support across implementations.450 assert_lacks_8bit_bug(IDENT_2Y)451 assert_lacks_wrap_bug(IDENT_2Y)452 453 #----------------------------------------------------------------454 # TODO: check for 2x support455 #----------------------------------------------------------------456 457 #----------------------------------------------------------------458 # check for 2b support459 #----------------------------------------------------------------460 test_hash_2b = TEST_HASH_2A.replace("2a", "2b")461 result = safe_verify("test", test_hash_2b)462 if result is NotImplemented:463 mixin_cls._lacks_2b_support = True464 log.debug("%r backend lacks $2b$ support, enabling workaround", backend)465 elif not result:466 raise RuntimeError("%s incorrectly rejected $2b$ hash" % backend)467 else:468 mixin_cls._fallback_ident = IDENT_2B469 assert_lacks_8bit_bug(IDENT_2B)470 assert_lacks_wrap_bug(IDENT_2B)471 472 # set flag so we don't have to run this again473 mixin_cls._workrounds_initialized = True474 return True475 476 #===================================================================477 # digest calculation478 #===================================================================479 480 # _calc_checksum() defined by backends481 482 def _prepare_digest_args(self, secret):483 """484 common helper for backends to implement _calc_checksum().485 takes in secret, returns (secret, ident) pair,486 """487 return self._norm_digest_args(secret, self.ident, new=self.use_defaults)488 489 @classmethod490 def _norm_digest_args(cls, secret, ident, new=False):491 # make sure secret is unicode492 require_valid_utf8_bytes = cls._require_valid_utf8_bytes493 if isinstance(secret, unicode):494 secret = secret.encode("utf-8")495 elif require_valid_utf8_bytes:496 # if backend requires utf8 bytes (os_crypt);497 # make sure input actually is utf8, or don't bother enabling utf-8 specific helpers.498 try:499 secret.decode("utf-8")500 except UnicodeDecodeError:501 # XXX: could just throw PasswordValueError here, backend will just do that502 # when _calc_digest() is actually called.503 require_valid_utf8_bytes = False504 505 # check max secret size506 uh.validate_secret(secret)507 508 # check for truncation (during .hash() calls only)509 if new:510 cls._check_truncate_policy(secret)511 512 # NOTE: especially important to forbid NULLs for bcrypt, since many513 # backends (bcryptor, bcrypt) happily accept them, and then514 # silently truncate the password at first NULL they encounter!515 if _BNULL in secret:516 raise uh.exc.NullPasswordError(cls)517 518 # TODO: figure out way to skip these tests when not needed...519 520 # protect from wraparound bug by truncating secret before handing it to the backend.521 # bcrypt only uses first 72 bytes anyways.522 # NOTE: not needed for 2y/2b, but might use 2a as fallback for them.523 if cls._has_2a_wraparound_bug and len(secret) >= 255:524 if require_valid_utf8_bytes:525 # backend requires valid utf8 bytes, so truncate secret to nearest valid segment.526 # want to do this in constant time to not give away info about secret.527 # NOTE: this only works because bcrypt will ignore everything past528 # secret[71], so padding to include a full utf8 sequence529 # won't break anything about the final output.530 secret = utf8_truncate(secret, 72)531 else:532 secret = secret[:72]533 534 # special case handling for variants (ordered most common first)535 if ident == IDENT_2A:536 # nothing needs to be done.537 pass538 539 elif ident == IDENT_2B:540 if cls._lacks_2b_support:541 # handle $2b$ hash format even if backend is too old.542 # have it generate a 2A/2Y digest, then return it as a 2B hash.543 # 2a-only backend could potentially exhibit wraparound bug --544 # but we work around that issue above.545 ident = cls._fallback_ident546 547 elif ident == IDENT_2Y:548 if cls._lacks_2y_support:549 # handle $2y$ hash format (not supported by BSDs, being phased out on others)550 # have it generate a 2A/2B digest, then return it as a 2Y hash.551 ident = cls._fallback_ident552 553 elif ident == IDENT_2:554 if cls._lacks_20_support:555 # handle legacy $2$ format (not supported by most backends except BSD os_crypt)556 # we can fake $2$ behavior using the 2A/2Y/2B algorithm557 # by repeating the password until it's at least 72 chars in length.558 if secret:559 if require_valid_utf8_bytes:560 # NOTE: this only works because bcrypt will ignore everything past561 # secret[71], so padding to include a full utf8 sequence562 # won't break anything about the final output.563 secret = utf8_repeat_string(secret, 72)564 else:565 secret = repeat_string(secret, 72)566 ident = cls._fallback_ident567 568 elif ident == IDENT_2X:569 570 # NOTE: shouldn't get here.571 # XXX: could check if backend does actually offer 'support'572 raise RuntimeError("$2x$ hashes not currently supported by passlib")573 574 else:575 raise AssertionError("unexpected ident value: %r" % ident)576 577 return secret, ident578 579#-----------------------------------------------------------------------580# stub backend581#-----------------------------------------------------------------------582class _NoBackend(_BcryptCommon):583 """584 mixin used before any backend has been loaded.585 contains stubs that force loading of one of the available backends.586 """587 #===================================================================588 # digest calculation589 #===================================================================590 def _calc_checksum(self, secret):591 self._stub_requires_backend()592 # NOTE: have to use super() here so that we don't recursively593 # call subclass's wrapped _calc_checksum, e.g. bcrypt_sha256._calc_checksum594 return super(bcrypt, self)._calc_checksum(secret)595 596 #===================================================================597 # eoc598 #===================================================================599 600#-----------------------------------------------------------------------601# bcrypt backend602#-----------------------------------------------------------------------603class _BcryptBackend(_BcryptCommon):604 """605 backend which uses 'bcrypt' package606 """607 608 @classmethod609 def _load_backend_mixin(mixin_cls, name, dryrun):610 # try to import bcrypt611 global _bcrypt612 if _detect_pybcrypt():613 # pybcrypt was installed instead614 return False615 try:616 import bcrypt as _bcrypt617 except ImportError: # pragma: no cover618 return False619 try:620 version = _bcrypt.__about__.__version__621 except:622 log.warning("(trapped) error reading bcrypt version", exc_info=True)623 version = '<unknown>'624 625 log.debug("detected 'bcrypt' backend, version %r", version)626 return mixin_cls._finalize_backend_mixin(name, dryrun)627 628 # # TODO: would like to implementing verify() directly,629 # # to skip need for parsing hash strings.630 # # below method has a few edge cases where it chokes though.631 # @classmethod632 # def verify(cls, secret, hash):633 # if isinstance(hash, unicode):634 # hash = hash.encode("ascii")635 # ident = hash[:hash.index(b"$", 1)+1].decode("ascii")636 # if ident not in cls.ident_values:637 # raise uh.exc.InvalidHashError(cls)638 # secret, eff_ident = cls._norm_digest_args(secret, ident)639 # if eff_ident != ident:640 # # lacks support for original ident, replace w/ new one.641 # hash = eff_ident.encode("ascii") + hash[len(ident):]642 # result = _bcrypt.hashpw(secret, hash)643 # assert result.startswith(eff_ident)644 # return consteq(result, hash)645 646 def _calc_checksum(self, secret):647 # bcrypt behavior:648 # secret must be bytes649 # config must be ascii bytes650 # returns ascii bytes651 secret, ident = self._prepare_digest_args(secret)652 config = self._get_config(ident)653 if isinstance(config, unicode):654 config = config.encode("ascii")655 hash = _bcrypt.hashpw(secret, config)656 assert isinstance(hash, bytes)657 if not hash.startswith(config) or len(hash) != len(config)+31:658 raise uh.exc.CryptBackendError(self, config, hash, source="`bcrypt` package")659 return hash[-31:].decode("ascii")660 661#-----------------------------------------------------------------------662# bcryptor backend663#-----------------------------------------------------------------------664class _BcryptorBackend(_BcryptCommon):665 """666 backend which uses 'bcryptor' package667 """668 669 @classmethod670 def _load_backend_mixin(mixin_cls, name, dryrun):671 # try to import bcryptor672 global _bcryptor673 try:674 import bcryptor as _bcryptor675 except ImportError: # pragma: no cover676 return False677 678 # deprecated as of 1.7.2679 if not dryrun:680 warn("Support for `bcryptor` is deprecated, and will be removed in Passlib 1.8; "681 "Please use `pip install bcrypt` instead", DeprecationWarning)682 683 return mixin_cls._finalize_backend_mixin(name, dryrun)684 685 def _calc_checksum(self, secret):686 # bcryptor behavior:687 # py2: unicode secret/hash encoded as ascii bytes before use,688 # bytes taken as-is; returns ascii bytes.689 # py3: not supported690 secret, ident = self._prepare_digest_args(secret)691 config = self._get_config(ident)692 hash = _bcryptor.engine.Engine(False).hash_key(secret, config)693 if not hash.startswith(config) or len(hash) != len(config) + 31:694 raise uh.exc.CryptBackendError(self, config, hash, source="bcryptor library")695 return str_to_uascii(hash[-31:])696 697#-----------------------------------------------------------------------698# pybcrypt backend699#-----------------------------------------------------------------------700class _PyBcryptBackend(_BcryptCommon):701 """702 backend which uses 'pybcrypt' package703 """704 705 #: classwide thread lock used for pybcrypt < 0.3706 _calc_lock = None707 708 @classmethod709 def _load_backend_mixin(mixin_cls, name, dryrun):710 # try to import pybcrypt711 global _pybcrypt712 if not _detect_pybcrypt():713 # not installed, or bcrypt installed instead714 return False715 try:716 import bcrypt as _pybcrypt717 except ImportError: # pragma: no cover718 # XXX: should we raise AssertionError here? (if get here, _detect_pybcrypt() is broken)719 return False720 721 # deprecated as of 1.7.2722 if not dryrun:723 warn("Support for `py-bcrypt` is deprecated, and will be removed in Passlib 1.8; "724 "Please use `pip install bcrypt` instead", DeprecationWarning)725 726 # determine pybcrypt version727 try:728 version = _pybcrypt._bcrypt.__version__729 except:730 log.warning("(trapped) error reading pybcrypt version", exc_info=True)731 version = "<unknown>"732 log.debug("detected 'pybcrypt' backend, version %r", version)733 734 # return calc function based on version735 vinfo = parse_version(version) or (0, 0)736 if vinfo < (0, 3):737 warn("py-bcrypt %s has a major security vulnerability, "738 "you should upgrade to py-bcrypt 0.3 immediately."739 % version, uh.exc.PasslibSecurityWarning)740 if mixin_cls._calc_lock is None:741 import threading742 mixin_cls._calc_lock = threading.Lock()743 mixin_cls._calc_checksum = get_unbound_method_function(mixin_cls._calc_checksum_threadsafe)744 745 return mixin_cls._finalize_backend_mixin(name, dryrun)746 747 def _calc_checksum_threadsafe(self, secret):748 # as workaround for pybcrypt < 0.3's concurrency issue,749 # we wrap everything in a thread lock. as long as bcrypt is only750 # used through passlib, this should be safe.751 with self._calc_lock:752 return self._calc_checksum_raw(secret)753 754 def _calc_checksum_raw(self, secret):755 # py-bcrypt behavior:756 # py2: unicode secret/hash encoded as ascii bytes before use,757 # bytes taken as-is; returns ascii bytes.758 # py3: unicode secret encoded as utf-8 bytes,759 # hash encoded as ascii bytes, returns ascii unicode.760 secret, ident = self._prepare_digest_args(secret)761 config = self._get_config(ident)762 hash = _pybcrypt.hashpw(secret, config)763 if not hash.startswith(config) or len(hash) != len(config) + 31:764 raise uh.exc.CryptBackendError(self, config, hash, source="pybcrypt library")765 return str_to_uascii(hash[-31:])766 767 _calc_checksum = _calc_checksum_raw768 769#-----------------------------------------------------------------------770# os crypt backend771#-----------------------------------------------------------------------772class _OsCryptBackend(_BcryptCommon):773 """774 backend which uses :func:`crypt.crypt`775 """776 777 #: set flag to ensure _prepare_digest_args() doesn't create invalid utf8 string778 #: when truncating bytes.779 _require_valid_utf8_bytes = not crypt_accepts_bytes780 781 @classmethod782 def _load_backend_mixin(mixin_cls, name, dryrun):783 if not test_crypt("test", TEST_HASH_2A):784 return False785 return mixin_cls._finalize_backend_mixin(name, dryrun)786 787 def _calc_checksum(self, secret):788 #789 # run secret through crypt.crypt().790 # if everything goes right, we'll get back a properly formed bcrypt hash.791 #792 secret, ident = self._prepare_digest_args(secret)793 config = self._get_config(ident)794 hash = safe_crypt(secret, config)795 if hash is not None:796 if not hash.startswith(config) or len(hash) != len(config) + 31:797 raise uh.exc.CryptBackendError(self, config, hash)798 return hash[-31:]799 800 #801 # Check if this failed due to non-UTF8 bytes802 # In detail: under py3, crypt.crypt() requires unicode inputs, which are then encoded to803 # utf8 before passing them to os crypt() call. this is done according to the "s" format804 # specifier for PyArg_ParseTuple (https://docs.python.org/3/c-api/arg.html).805 # There appears no way to get around that to pass raw bytes; so we just throw error here806 # to let user know they need to use another backend if they want raw bytes support.807 #808 # XXX: maybe just let safe_crypt() throw UnicodeDecodeError under passlib 2.0,809 # and then catch it above? maybe have safe_crypt ALWAYS throw error810 # instead of returning None? (would save re-detecting what went wrong)811 # XXX: isn't secret ALWAYS bytes at this point?812 #813 if PY3 and isinstance(secret, bytes):814 try:815 secret.decode("utf-8")816 except UnicodeDecodeError:817 raise error_from(uh.exc.PasswordValueError(818 "python3 crypt.crypt() ony supports bytes passwords using UTF8; "819 "passlib recommends running `pip install bcrypt` for general bcrypt support.",820 ), None)821 822 #823 # else crypt() call failed for unknown reason.824 #825 # NOTE: getting here should be considered a bug in passlib --826 # if os_crypt backend detection said there's support,827 # and we've already checked all known reasons above;828 # want them to file bug so we can figure out what happened.829 # in the meantime, users can avoid this by installing bcrypt-cffi backend;830 # which won't have this (or utf8) edgecases.831 #832 # XXX: throw something more specific, like an "InternalBackendError"?833 # NOTE: if do change this error, need to update test_81_crypt_fallback() expectations834 # about what will be thrown; as well as safe_verify() above.835 #836 debug_only_repr = uh.exc.debug_only_repr837 raise uh.exc.InternalBackendError(838 "crypt.crypt() failed for unknown reason; "839 "passlib recommends running `pip install bcrypt` for general bcrypt support."840 # for debugging UTs --841 "(config=%s, secret=%s)" % (debug_only_repr(config), debug_only_repr(secret)),842 )843 844#-----------------------------------------------------------------------845# builtin backend846#-----------------------------------------------------------------------847class _BuiltinBackend(_BcryptCommon):848 """849 backend which uses passlib's pure-python implementation850 """851 @classmethod852 def _load_backend_mixin(mixin_cls, name, dryrun):853 from passlib.utils import as_bool854 if not as_bool(os.environ.get("PASSLIB_BUILTIN_BCRYPT")):855 log.debug("bcrypt 'builtin' backend not enabled via $PASSLIB_BUILTIN_BCRYPT")856 return False857 global _builtin_bcrypt858 from passlib.crypto._blowfish import raw_bcrypt as _builtin_bcrypt859 return mixin_cls._finalize_backend_mixin(name, dryrun)860 861 def _calc_checksum(self, secret):862 secret, ident = self._prepare_digest_args(secret)863 chk = _builtin_bcrypt(secret, ident[1:-1],864 self.salt.encode("ascii"), self.rounds)865 return chk.decode("ascii")866 867#=============================================================================868# handler869#=============================================================================870class bcrypt(_NoBackend, _BcryptCommon):871 """This class implements the BCrypt password hash, and follows the :ref:`password-hash-api`.872 873 It supports a fixed-length salt, and a variable number of rounds.874 875 The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:876 877 :type salt: str878 :param salt:879 Optional salt string.880 If not specified, one will be autogenerated (this is recommended).881 If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``.882 883 :type rounds: int884 :param rounds:885 Optional number of rounds to use.886 Defaults to 12, must be between 4 and 31, inclusive.887 This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}`888 -- increasing the rounds by +1 will double the amount of time taken.889 890 :type ident: str891 :param ident:892 Specifies which version of the BCrypt algorithm will be used when creating a new hash.893 Typically this option is not needed, as the default (``"2b"``) is usually the correct choice.894 If specified, it must be one of the following:895 896 * ``"2"`` - the first revision of BCrypt, which suffers from a minor security flaw and is generally not used anymore.897 * ``"2a"`` - some implementations suffered from rare security flaws, replaced by 2b.898 * ``"2y"`` - format specific to the *crypt_blowfish* BCrypt implementation,899 identical to ``"2b"`` in all but name.900 * ``"2b"`` - latest revision of the official BCrypt algorithm, current default.901 902 :param bool truncate_error:903 By default, BCrypt will silently truncate passwords larger than 72 bytes.904 Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash`905 to raise a :exc:`~passlib.exc.PasswordTruncateError` instead.906 907 .. versionadded:: 1.7908 909 :type relaxed: bool910 :param relaxed:911 By default, providing an invalid value for one of the other912 keywords will result in a :exc:`ValueError`. If ``relaxed=True``,913 and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`914 will be issued instead. Correctable errors include ``rounds``915 that are too small or too large, and ``salt`` strings that are too long.916 917 .. versionadded:: 1.6918 919 .. versionchanged:: 1.6920 This class now supports ``"2y"`` hashes, and recognizes921 (but does not support) the broken ``"2x"`` hashes.922 (see the :ref:`crypt_blowfish bug <crypt-blowfish-bug>`923 for details).924 925 .. versionchanged:: 1.6926 Added a pure-python backend.927 928 .. versionchanged:: 1.6.3929 930 Added support for ``"2b"`` variant.931 932 .. versionchanged:: 1.7933 934 Now defaults to ``"2b"`` variant.935 """936 #=============================================================================937 # backend938 #=============================================================================939 940 # NOTE: the brunt of the bcrypt class is implemented in _BcryptCommon.941 # there are then subclass for each backend (e.g. _PyBcryptBackend),942 # these are dynamically prepended to this class's bases943 # in order to load the appropriate backend.944 945 #: list of potential backends946 backends = ("bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin")947 948 #: flag that this class's bases should be modified by SubclassBackendMixin949 _backend_mixin_target = True950 951 #: map of backend -> mixin class, used by _get_backend_loader()952 _backend_mixin_map = {953 None: _NoBackend,954 "bcrypt": _BcryptBackend,955 "pybcrypt": _PyBcryptBackend,956 "bcryptor": _BcryptorBackend,957 "os_crypt": _OsCryptBackend,958 "builtin": _BuiltinBackend,959 }960 961 #=============================================================================962 # eoc963 #=============================================================================964 965#=============================================================================966# variants967#=============================================================================968_UDOLLAR = u("$")969 970# XXX: it might be better to have all the bcrypt variants share a common base class,971# and have the (django_)bcrypt_sha256 wrappers just proxy bcrypt instead of subclassing it.972class _wrapped_bcrypt(bcrypt):973 """974 abstracts out some bits bcrypt_sha256 & django_bcrypt_sha256 share.975 - bypass backend-loading wrappers for hash() etc976 - disable truncation support, sha256 wrappers don't need it.977 """978 setting_kwds = tuple(elem for elem in bcrypt.setting_kwds if elem not in ["truncate_error"])979 truncate_size = None980 981 # XXX: these will be needed if any bcrypt backends directly implement this...982 # @classmethod983 # def hash(cls, secret, **kwds):984 # # bypass bcrypt backend overriding this method985 # # XXX: would wrapping bcrypt make this easier than subclassing it?986 # return super(_BcryptCommon, cls).hash(secret, **kwds)987 #988 # @classmethod989 # def verify(cls, secret, hash):990 # # bypass bcrypt backend overriding this method991 # return super(_BcryptCommon, cls).verify(secret, hash)992 #993 # @classmethod994 # def genhash(cls, secret, hash):995 # # bypass bcrypt backend overriding this method996 # return super(_BcryptCommon, cls).genhash(secret, hash)997 998 @classmethod999 def _check_truncate_policy(cls, secret):1000 # disable check performed by bcrypt(), since this doesn't truncate passwords.1001 pass1002 1003#=============================================================================1004# bcrypt sha256 wrapper1005#=============================================================================1006 1007class bcrypt_sha256(_wrapped_bcrypt):1008 """1009 This class implements a composition of BCrypt + HMAC_SHA256,1010 and follows the :ref:`password-hash-api`.1011 1012 It supports a fixed-length salt, and a variable number of rounds.1013 1014 The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept1015 all the same optional keywords as the base :class:`bcrypt` hash.1016 1017 .. versionadded:: 1.6.21018 1019 .. versionchanged:: 1.71020 1021 Now defaults to ``"2b"`` bcrypt variant; though supports older hashes1022 generated using the ``"2a"`` bcrypt variant.1023 1024 .. versionchanged:: 1.7.31025 1026 For increased security, updated to use HMAC-SHA256 instead of plain SHA256.1027 Now only supports the ``"2b"`` bcrypt variant. Hash format updated to "v=2".1028 """1029 #===================================================================1030 # class attrs1031 #===================================================================1032 1033 #--------------------1034 # PasswordHash1035 #--------------------1036 name = "bcrypt_sha256"1037 1038 #--------------------1039 # GenericHandler1040 #--------------------1041 # this is locked at 2b for now (with 2a allowed only for legacy v1 format)1042 ident_values = (IDENT_2A, IDENT_2B)1043 1044 # clone bcrypt's ident aliases so they can be used here as well...1045 ident_aliases = (lambda ident_values: dict(item for item in bcrypt.ident_aliases.items()1046 if item[1] in ident_values))(ident_values)1047 default_ident = IDENT_2B1048 1049 #--------------------1050 # class specific1051 #--------------------1052 1053 _supported_versions = set([1, 2])1054 1055 #===================================================================1056 # instance attrs1057 #===================================================================1058 1059 #: wrapper version.1060 #: v1 -- used prior to passlib 1.7.3; performs ``bcrypt(sha256(secret), salt, cost)``1061 #: v2 -- new in passlib 1.7.3; performs `bcrypt(sha256_hmac(salt, secret), salt, cost)``1062 version = 21063 1064 #===================================================================1065 # configuration1066 #===================================================================1067 1068 @classmethod1069 def using(cls, version=None, **kwds):1070 subcls = super(bcrypt_sha256, cls).using(**kwds)1071 if version is not None:1072 subcls.version = subcls._norm_version(version)1073 ident = subcls.default_ident1074 if subcls.version > 1 and ident != IDENT_2B:1075 raise ValueError("bcrypt %r hashes not allowed for version %r" %1076 (ident, subcls.version))1077 return subcls1078 1079 #===================================================================1080 # formatting1081 #===================================================================1082 1083 # sample hash:1084 # $bcrypt-sha256$2a,6$/3OeRpbOf8/l6nPPRdZPp.$nRiyYqPobEZGdNRBWihQhiFDh1ws1tu1085 # $bcrypt-sha256$ -- prefix/identifier1086 # 2a -- bcrypt variant1087 # , -- field separator1088 # 6 -- bcrypt work factor1089 # $ -- section separator1090 # /3OeRpbOf8/l6nPPRdZPp. -- salt1091 # $ -- section separator1092 # nRiyYqPobEZGdNRBWihQhiFDh1ws1tu -- digest1093 1094 # XXX: we can't use .ident attr due to bcrypt code using it.1095 # working around that via prefix.1096 prefix = u('$bcrypt-sha256$')1097 1098 #: current version 2 hash format1099 _v2_hash_re = re.compile(r"""(?x)1100 ^1101 [$]bcrypt-sha256[$]1102 v=(?P<version>\d+),1103 t=(?P<type>2b),1104 r=(?P<rounds>\d{1,2})1105 [$](?P<salt>[^$]{22})1106 (?:[$](?P<digest>[^$]{31}))?1107 $1108 """)1109 1110 #: old version 1 hash format1111 _v1_hash_re = re.compile(r"""(?x)1112 ^1113 [$]bcrypt-sha256[$]1114 (?P<type>2[ab]),1115 (?P<rounds>\d{1,2})1116 [$](?P<salt>[^$]{22})1117 (?:[$](?P<digest>[^$]{31}))?1118 $1119 """)1120 1121 @classmethod1122 def identify(cls, hash):1123 hash = uh.to_unicode_for_identify(hash)1124 if not hash:1125 return False1126 return hash.startswith(cls.prefix)1127 1128 @classmethod1129 def from_string(cls, hash):1130 hash = to_unicode(hash, "ascii", "hash")1131 if not hash.startswith(cls.prefix):1132 raise uh.exc.InvalidHashError(cls)1133 m = cls._v2_hash_re.match(hash)1134 if m:1135 version = int(m.group("version"))1136 if version < 2:1137 raise uh.exc.MalformedHashError(cls)1138 else:1139 m = cls._v1_hash_re.match(hash)1140 if m:1141 version = 11142 else:1143 raise uh.exc.MalformedHashError(cls)1144 rounds = m.group("rounds")1145 if rounds.startswith(uh._UZERO) and rounds != uh._UZERO:1146 raise uh.exc.ZeroPaddedRoundsError(cls)1147 return cls(1148 version=version,1149 ident=m.group("type"),1150 rounds=int(rounds),1151 salt=m.group("salt"),1152 checksum=m.group("digest"),1153 )1154 1155 _v2_template = u("$bcrypt-sha256$v=2,t=%s,r=%d$%s$%s")1156 _v1_template = u("$bcrypt-sha256$%s,%d$%s$%s")1157 1158 def to_string(self):1159 if self.version == 1:1160 template = self._v1_template1161 else:1162 template = self._v2_template1163 hash = template % (self.ident.strip(_UDOLLAR), self.rounds, self.salt, self.checksum)1164 return uascii_to_str(hash)1165 1166 #===================================================================1167 # init1168 #===================================================================1169 1170 def __init__(self, version=None, **kwds):1171 if version is not None:1172 self.version = self._norm_version(version)1173 super(bcrypt_sha256, self).__init__(**kwds)1174 1175 #===================================================================1176 # version1177 #===================================================================1178 1179 @classmethod1180 def _norm_version(cls, version):1181 if version not in cls._supported_versions:1182 raise ValueError("%s: unknown or unsupported version: %r" % (cls.name, version))1183 return version1184 1185 #===================================================================1186 # checksum1187 #===================================================================1188 1189 def _calc_checksum(self, secret):1190 # NOTE: can't use digest directly, since bcrypt stops at first NULL.1191 # NOTE: bcrypt doesn't fully mix entropy for bytes 55-72 of password1192 # (XXX: citation needed), so we don't want key to be > 55 bytes.1193 # thus, have to use base64 (44 bytes) rather than hex (64 bytes).1194 # XXX: it's later come out that 55-72 may be ok, so later revision of bcrypt_sha2561195 # may switch to hex encoding, since it's simpler to implement elsewhere.1196 if isinstance(secret, unicode):1197 secret = secret.encode("utf-8")1198 1199 if self.version == 1:1200 # version 1 -- old version just ran secret through sha256(),