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/9 7 8* deal with lack of PY3-compatibile c-ext implementation 9""" 10#============================================================================= 11# imports 12#============================================================================= 13from __future__ import with_statement, absolute_import 14# core 15from base64 import b64encode 16from hashlib import sha256 17import os 18import re 19import logging; log = logging.getLogger(__name__) 20from warnings import warn 21# site 22_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# pkg 26_builtin_bcrypt = None # dynamically imported by _load_backend_builtin() 27from passlib.crypto.digest import compile_hmac 28from passlib.exc import PasslibHashWarning, PasslibSecurityWarning, PasslibSecurityError 29from 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_bytes 32from passlib.utils.binary import bcrypt64 33from passlib.utils.compat import get_unbound_method_function 34from passlib.utils.compat import u, uascii_to_str, unicode, str_to_uascii, PY3, error_from 35import passlib.utils.handlers as uh 36 37# local 38__all__ = [ 39 "bcrypt", 40] 41 42#============================================================================= 43# support funcs & constants 44#============================================================================= 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-checks 53TEST_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.3 65 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 bcrypt 77 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 None 81 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 False 89 return True 90 91#============================================================================= 92# backend mixins 93#============================================================================= 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 proper 102 are modified to prepend the correct backend-specific subclass. 103 """ 104 #=================================================================== 105 # class attrs 106 #=================================================================== 107 108 #-------------------- 109 # PasswordHash 110 #-------------------- 111 name = "bcrypt" 112 setting_kwds = ("salt", "rounds", "ident", "truncate_error") 113 114 #-------------------- 115 # GenericHandler 116 #-------------------- 117 checksum_size = 31 118 checksum_chars = bcrypt64.charmap 119 120 #-------------------- 121 # HasManyIdents 122 #-------------------- 123 default_ident = IDENT_2B 124 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 # HasSalt 130 #-------------------- 131 min_salt_size = max_salt_size = 22 132 salt_chars = bcrypt64.charmap 133 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 # HasRounds 139 #-------------------- 140 default_rounds = 12 # current passlib default 141 min_rounds = 4 # minimum from bcrypt specification 142 max_rounds = 31 # 32-bit integer limit (since real_rounds=1<<rounds) 143 rounds_cost = "log2" 144 145 #-------------------- 146 # TruncateMixin 147 #-------------------- 148 truncate_size = 72 149 150 #-------------------- 151 # custom 152 #-------------------- 153 154 # backend workaround detection flags 155 # NOTE: these are only set on the backend mixin classes 156 _workrounds_initialized = False 157 _has_2a_wraparound_bug = False 158 _lacks_20_support = False 159 _lacks_2y_support = False 160 _lacks_2b_support = False 161 _fallback_ident = IDENT_2A 162 _require_valid_utf8_bytes = False 163 164 #=================================================================== 165 # formatting 166 #=================================================================== 167 168 @classmethod 169 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 # migration 199 #=================================================================== 200 201 @classmethod 202 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 True 210 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 25 218 #=================================================================== 219 220 @classmethod 221 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 hash 227 228 @classmethod 229 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 @classmethod 236 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 be 242 # 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 salt 249 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 checksum 260 261 #=================================================================== 262 # backend configuration 263 # NOTE: backends are defined in terms of mixin classes, 264 # which are dynamically inserted into the bases of the 'bcrypt' class 265 # via the machinery in 'SubclassBackendMixin'. 266 # this lets us load in a backend-specific implementation 267 # of _calc_checksum() and similar methods. 268 #=================================================================== 269 270 # NOTE: backend config is located down in <bcrypt> class 271 272 # NOTE: set_backend() will execute the ._load_backend_mixin() 273 # of the matching mixin class, which will handle backend detection 274 275 # appended to HasManyBackends' "no backends available" error message 276 _no_backend_suggestion = " -- recommend you install one (e.g. 'pip install bcrypt')" 277 278 @classmethod 279 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 performs 283 feature detection & testing common to all backends. 284 """ 285 #---------------------------------------------------------------- 286 # setup helpers 287 #---------------------------------------------------------------- 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 True 293 294 verify = mixin_cls.verify 295 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 various 306 # errors about unrecognized version: 307 # os_crypt -- internal code below throws 308 # - PasswordValueError if there's encoding issue w/ password. 309 # - InternalBackendError if crypt fails for unknown reason 310 # (trapped below so we can debug it) 311 # pybcrypt, bcrypt -- raises ValueError 312 # bcryptor -- raises bcryptor.engine.SaltError 313 return NotImplemented 314 except uh.exc.InternalBackendError: 315 # _calc_checksum() code may also throw CryptBackendError 316 # 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 NotImplemented 321 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 url 331 """ 332 # NOTE: passlib 1.7.2 and earlier used the commented-out LATIN-1 test vector to detect 333 # this bug; but python3's crypt.crypt() only supports unicode inputs (and 334 # always encodes them as UTF8 before passing to crypt); so passlib 1.7.3 335 # switched to the UTF8-compatible test vector below. This one's bug_hash value 336 # ("$2x$...rcAS") was drawn from the same openwall source (above); and the correct 337 # hash ("$2a$...X6eu") was generated by passing the raw bytes to python2's 338 # crypt.crypt() using OpenBSD 6.7 (hash confirmed as same for $2a$ & $2b$). 339 340 # LATIN-1 test vector 341 # 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 vector 346 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 hashes 352 # generated by crypt_blowfish library. 2y/2b hashes should not have the bug 353 # (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 correct 360 # 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"*72 373 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 bug 378 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 True 382 383 # if it doesn't have wraparound bug, make sure it *does* handle things 384 # 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 False 390 391 def assert_lacks_wrap_bug(ident): 392 if not detect_wrap_bug(ident): 393 return 394 # should only see in 2a, later idents should NEVER exhibit this bug: 395 # * 2y implementations should have been free of it 396 # * 2b was what (supposedly) fixed it 397 raise RuntimeError("%s backend unexpectedly has wraparound bug for %s" % (backend, ident)) 398 399 #---------------------------------------------------------------- 400 # check for old 20 support 401 #---------------------------------------------------------------- 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 = True 406 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 support 412 #---------------------------------------------------------------- 413 result = safe_verify("test", TEST_HASH_2A) 414 if result is NotImplemented: 415 # 2a support is required, and should always be present 416 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 = True 436 437 #---------------------------------------------------------------- 438 # check for 2y support 439 #---------------------------------------------------------------- 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 = True 444 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 support 455 #---------------------------------------------------------------- 456 457 #---------------------------------------------------------------- 458 # check for 2b support 459 #---------------------------------------------------------------- 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 = True 464 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_2B 469 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 again 473 mixin_cls._workrounds_initialized = True 474 return True 475 476 #=================================================================== 477 # digest calculation 478 #=================================================================== 479 480 # _calc_checksum() defined by backends 481 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 @classmethod 490 def _norm_digest_args(cls, secret, ident, new=False): 491 # make sure secret is unicode 492 require_valid_utf8_bytes = cls._require_valid_utf8_bytes 493 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 that 502 # when _calc_digest() is actually called. 503 require_valid_utf8_bytes = False 504 505 # check max secret size 506 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 many 513 # backends (bcryptor, bcrypt) happily accept them, and then 514 # 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 past 528 # secret[71], so padding to include a full utf8 sequence 529 # 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 pass 538 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_ident 546 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_ident 552 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 algorithm 557 # 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 past 561 # secret[71], so padding to include a full utf8 sequence 562 # 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_ident 567 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, ident 578 579#----------------------------------------------------------------------- 580# stub backend 581#----------------------------------------------------------------------- 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 calculation 589 #=================================================================== 590 def _calc_checksum(self, secret): 591 self._stub_requires_backend() 592 # NOTE: have to use super() here so that we don't recursively 593 # call subclass's wrapped _calc_checksum, e.g. bcrypt_sha256._calc_checksum 594 return super(bcrypt, self)._calc_checksum(secret) 595 596 #=================================================================== 597 # eoc 598 #=================================================================== 599 600#----------------------------------------------------------------------- 601# bcrypt backend 602#----------------------------------------------------------------------- 603class _BcryptBackend(_BcryptCommon): 604 """ 605 backend which uses 'bcrypt' package 606 """ 607 608 @classmethod 609 def _load_backend_mixin(mixin_cls, name, dryrun): 610 # try to import bcrypt 611 global _bcrypt 612 if _detect_pybcrypt(): 613 # pybcrypt was installed instead 614 return False 615 try: 616 import bcrypt as _bcrypt 617 except ImportError: # pragma: no cover 618 return False 619 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 # @classmethod 632 # 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 bytes 649 # config must be ascii bytes 650 # returns ascii bytes 651 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 backend 663#----------------------------------------------------------------------- 664class _BcryptorBackend(_BcryptCommon): 665 """ 666 backend which uses 'bcryptor' package 667 """ 668 669 @classmethod 670 def _load_backend_mixin(mixin_cls, name, dryrun): 671 # try to import bcryptor 672 global _bcryptor 673 try: 674 import bcryptor as _bcryptor 675 except ImportError: # pragma: no cover 676 return False 677 678 # deprecated as of 1.7.2 679 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 supported 690 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 backend 699#----------------------------------------------------------------------- 700class _PyBcryptBackend(_BcryptCommon): 701 """ 702 backend which uses 'pybcrypt' package 703 """ 704 705 #: classwide thread lock used for pybcrypt < 0.3 706 _calc_lock = None 707 708 @classmethod 709 def _load_backend_mixin(mixin_cls, name, dryrun): 710 # try to import pybcrypt 711 global _pybcrypt 712 if not _detect_pybcrypt(): 713 # not installed, or bcrypt installed instead 714 return False 715 try: 716 import bcrypt as _pybcrypt 717 except ImportError: # pragma: no cover 718 # XXX: should we raise AssertionError here? (if get here, _detect_pybcrypt() is broken) 719 return False 720 721 # deprecated as of 1.7.2 722 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 version 727 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 version 735 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 threading 742 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 only 750 # 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_raw 768 769#----------------------------------------------------------------------- 770# os crypt backend 771#----------------------------------------------------------------------- 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 string 778 #: when truncating bytes. 779 _require_valid_utf8_bytes = not crypt_accepts_bytes 780 781 @classmethod 782 def _load_backend_mixin(mixin_cls, name, dryrun): 783 if not test_crypt("test", TEST_HASH_2A): 784 return False 785 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 bytes 802 # In detail: under py3, crypt.crypt() requires unicode inputs, which are then encoded to 803 # utf8 before passing them to os crypt() call. this is done according to the "s" format 804 # 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 here 806 # 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 error 810 # 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() expectations 834 # about what will be thrown; as well as safe_verify() above. 835 # 836 debug_only_repr = uh.exc.debug_only_repr 837 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 backend 846#----------------------------------------------------------------------- 847class _BuiltinBackend(_BcryptCommon): 848 """ 849 backend which uses passlib's pure-python implementation 850 """ 851 @classmethod 852 def _load_backend_mixin(mixin_cls, name, dryrun): 853 from passlib.utils import as_bool 854 if not as_bool(os.environ.get("PASSLIB_BUILTIN_BCRYPT")): 855 log.debug("bcrypt 'builtin' backend not enabled via $PASSLIB_BUILTIN_BCRYPT") 856 return False 857 global _builtin_bcrypt 858 from passlib.crypto._blowfish import raw_bcrypt as _builtin_bcrypt 859 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# handler 869#============================================================================= 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: str 878 :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: int 884 :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: str 891 :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.7 908 909 :type relaxed: bool 910 :param relaxed: 911 By default, providing an invalid value for one of the other 912 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.6 918 919 .. versionchanged:: 1.6 920 This class now supports ``"2y"`` hashes, and recognizes 921 (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.6 926 Added a pure-python backend. 927 928 .. versionchanged:: 1.6.3 929 930 Added support for ``"2b"`` variant. 931 932 .. versionchanged:: 1.7 933 934 Now defaults to ``"2b"`` variant. 935 """ 936 #============================================================================= 937 # backend 938 #============================================================================= 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 bases 943 # in order to load the appropriate backend. 944 945 #: list of potential backends 946 backends = ("bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin") 947 948 #: flag that this class's bases should be modified by SubclassBackendMixin 949 _backend_mixin_target = True 950 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 # eoc 963 #============================================================================= 964 965#============================================================================= 966# variants 967#============================================================================= 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() etc 976 - 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 = None 980 981 # XXX: these will be needed if any bcrypt backends directly implement this... 982 # @classmethod 983 # def hash(cls, secret, **kwds): 984 # # bypass bcrypt backend overriding this method 985 # # XXX: would wrapping bcrypt make this easier than subclassing it? 986 # return super(_BcryptCommon, cls).hash(secret, **kwds) 987 # 988 # @classmethod 989 # def verify(cls, secret, hash): 990 # # bypass bcrypt backend overriding this method 991 # return super(_BcryptCommon, cls).verify(secret, hash) 992 # 993 # @classmethod 994 # def genhash(cls, secret, hash): 995 # # bypass bcrypt backend overriding this method 996 # return super(_BcryptCommon, cls).genhash(secret, hash) 997 998 @classmethod 999 def _check_truncate_policy(cls, secret): 1000 # disable check performed by bcrypt(), since this doesn't truncate passwords. 1001 pass 1002 1003#============================================================================= 1004# bcrypt sha256 wrapper 1005#============================================================================= 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 accept 1015 all the same optional keywords as the base :class:`bcrypt` hash. 1016 1017 .. versionadded:: 1.6.2 1018 1019 .. versionchanged:: 1.7 1020 1021 Now defaults to ``"2b"`` bcrypt variant; though supports older hashes 1022 generated using the ``"2a"`` bcrypt variant. 1023 1024 .. versionchanged:: 1.7.3 1025 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 attrs 1031 #=================================================================== 1032 1033 #-------------------- 1034 # PasswordHash 1035 #-------------------- 1036 name = "bcrypt_sha256" 1037 1038 #-------------------- 1039 # GenericHandler 1040 #-------------------- 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_2B 1048 1049 #-------------------- 1050 # class specific 1051 #-------------------- 1052 1053 _supported_versions = set([1, 2]) 1054 1055 #=================================================================== 1056 # instance attrs 1057 #=================================================================== 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 = 2 1063 1064 #=================================================================== 1065 # configuration 1066 #=================================================================== 1067 1068 @classmethod 1069 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_ident 1074 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 subcls 1078 1079 #=================================================================== 1080 # formatting 1081 #=================================================================== 1082 1083 # sample hash: 1084 # $bcrypt-sha256$2a,6$/3OeRpbOf8/l6nPPRdZPp.$nRiyYqPobEZGdNRBWihQhiFDh1ws1tu 1085 # $bcrypt-sha256$ -- prefix/identifier 1086 # 2a -- bcrypt variant 1087 # , -- field separator 1088 # 6 -- bcrypt work factor 1089 # $ -- section separator 1090 # /3OeRpbOf8/l6nPPRdZPp. -- salt 1091 # $ -- section separator 1092 # nRiyYqPobEZGdNRBWihQhiFDh1ws1tu -- digest 1093 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 format 1099 _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 format 1111 _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 @classmethod 1122 def identify(cls, hash): 1123 hash = uh.to_unicode_for_identify(hash) 1124 if not hash: 1125 return False 1126 return hash.startswith(cls.prefix) 1127 1128 @classmethod 1129 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 = 1 1142 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_template 1161 else: 1162 template = self._v2_template 1163 hash = template % (self.ident.strip(_UDOLLAR), self.rounds, self.salt, self.checksum) 1164 return uascii_to_str(hash) 1165 1166 #=================================================================== 1167 # init 1168 #=================================================================== 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 # version 1177 #=================================================================== 1178 1179 @classmethod 1180 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 version 1184 1185 #=================================================================== 1186 # checksum 1187 #=================================================================== 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 password 1192 # (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_sha256 1195 # 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(), 1201 # though this could be vulnerable to a breach attach 1202 # (c.f. issue 114); which is why v2 switched to hmac wrapper. 1203 digest = sha256(secret).digest() 1204 else: 1205 # version 2 -- running secret through HMAC keyed off salt. 1206 # this prevents known secret -> sha256 password tables from being 1207 # used to test against a bcrypt_sha256 hash. 1208 # keying off salt (instead of constant string) should minimize chances of this 1209 # colliding with existing table of hmac digest lookups as well. 1210 # NOTE: salt in this case is the "bcrypt64"-encoded value, not the raw salt bytes, 1211 # to make things easier for parallel implementations of this hash -- 1212 # saving them the trouble of implementing a "bcrypt64" decoder. 1213 salt = self.salt 1214 if salt[-1] not in self.final_salt_chars: 1215 # forbidding salts with padding bits set, because bcrypt implementations 1216 # won't consistently hash them the same. since we control this format, 1217 # just prevent these from even getting used. 1218 raise ValueError("invalid salt string") 1219 digest = compile_hmac("sha256", salt.encode("ascii"))(secret) 1220 1221 # NOTE: output of b64encode() uses "+/" altchars, "=" padding chars, 1222 # and no leading/trailing whitespace. 1223 key = b64encode(digest) 1224 1225 # hand result off to normal bcrypt algorithm 1226 return super(bcrypt_sha256, self)._calc_checksum(key) 1227 1228 #=================================================================== 1229 # other 1230 #=================================================================== 1231 1232 def _calc_needs_update(self, **kwds): 1233 if self.version < type(self).version: 1234 return True 1235 return super(bcrypt_sha256, self)._calc_needs_update(**kwds) 1236 1237 #=================================================================== 1238 # eoc 1239 #=================================================================== 1240 1241#============================================================================= 1242# eof 1243#============================================================================= 1244