diff --git a/.gitignore b/.gitignore index a1122a4..9361233 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ __pycache__ coverage.xml dist + +*.pyc diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index 121a12c..8c225a3 100644 --- a/passlib/handlers/bcrypt.py +++ b/passlib/handlers/bcrypt.py @@ -152,6 +152,7 @@ class _BcryptCommon( # type: ignore[misc] _lacks_2b_support = False _fallback_ident = IDENT_2A _require_valid_utf8_bytes = False + _backend_raises_on_truncate = False @classmethod def from_string(cls, hash): @@ -370,21 +371,20 @@ def detect_wrap_bug(ident): # Secret which will trip the wraparound bug, if present secret = (b"0123456789" * 26)[:255] - # Python bcrypt >= 5.0.0 will raise an exception on passwords greater than 72 characters, - # whereas earlier versions without the wraparound bug silently truncated the input to 72 - # characters. See if the exception is generated. - try: + if not mixin_cls._backend_raises_on_truncate: + # Backend accepts more than 72 characters, test for the wraparound bug bug_hash = ( ident.encode("ascii") + b"04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6" ) - # If we get here, the backend auto-truncates, test for wraparound bug if verify(secret, bug_hash): return True - except ValueError: - # Backend explicitly will not auto-truncate, truncate the password to 72 characters - secret = secret[:72] + else: + # Backend won't accept more than 72 characters, truncate the secret + # + # n.b. this should preclude the wraparound bug existing anyway + secret = secret[:mixin_cls.truncate_size] # Check to make sure that the backend still hashes correctly; if not, we're in a failure case # not related to the original wraparound bug or bcrypt >= 5.0.0 input length restriction. @@ -620,11 +620,15 @@ def _load_backend_mixin(mixin_cls, name, dryrun): return False try: version = metadata.version("bcrypt") + + if int(version.split(".")[0]) >= 5: + mixin_cls._backend_raises_on_truncate = True except Exception: logger.warning("(trapped) error reading bcrypt version", exc_info=True) version = "" logger.debug("detected 'bcrypt' backend, version %r", version) + return mixin_cls._finalize_backend_mixin(name, dryrun) # # TODO: would like to implementing verify() directly, @@ -654,6 +658,10 @@ def _calc_checksum(self, secret): config = self._get_config(ident) if isinstance(config, str): config = config.encode("ascii") + + if self._backend_raises_on_truncate: + secret = secret[:72] + hash = _bcrypt.hashpw(secret, config) assert isinstance(hash, bytes) if not hash.startswith(config) or len(hash) != len(config) + 31: diff --git a/tests/test_handlers_bcrypt.py b/tests/test_handlers_bcrypt.py index b49c1fb..a9134e8 100644 --- a/tests/test_handlers_bcrypt.py +++ b/tests/test_handlers_bcrypt.py @@ -220,9 +220,9 @@ def check_bcrypt(secret, hash): hash = IDENT_2B + hash[4:] hash = to_bytes(hash) try: - return bcrypt.hashpw(secret, hash) == hash - except ValueError: - raise ValueError(f"bcrypt rejected hash: {hash!r} (secret={secret!r})") + return bcrypt.hashpw(secret[:72], hash) == hash + except ValueError as e: + raise ValueError(f"bcrypt rejected hash: {hash!r} (secret={secret!r}) with message: {str(e)}") return check_bcrypt