From cd36bdbdb17bb34516c637aec3608ac215e4e4ff Mon Sep 17 00:00:00 2001 From: Jonathan Chapman Date: Thu, 9 Oct 2025 20:51:43 -0400 Subject: [PATCH 1/5] Adding *.pyc to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index a1122a4..9361233 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ __pycache__ coverage.xml dist + +*.pyc From a3f920fd8f11aa543fea8d1f537d49fe0519be86 Mon Sep 17 00:00:00 2001 From: Jonathan Chapman Date: Thu, 9 Oct 2025 20:57:47 -0400 Subject: [PATCH 2/5] Changing from manual detection of bcrypt 5.0.0 functionality to version check in its specific backend, truncating before hashing if the backend will raise an error --- passlib/handlers/bcrypt.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index 121a12c..e8f3f22 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 version >= "5.0.0": + 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: From e5377b75789eb1d4e94265b076a38dd37ed6a7c4 Mon Sep 17 00:00:00 2001 From: Jonathan Chapman Date: Thu, 9 Oct 2025 20:58:50 -0400 Subject: [PATCH 3/5] Fixing direct call to bcrypt library in test_handlers_bcrypt, truncating secret in fuzzer to avoid raising an error, adding more verbosity when an error is raise --- tests/test_handlers_bcrypt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 From fe4f954f216ed5069c3326b000a7bab6f4b2ee11 Mon Sep 17 00:00:00 2001 From: Jonathan Chapman Date: Mon, 13 Oct 2025 11:28:38 -0400 Subject: [PATCH 4/5] Avoiding string compare for version numbers --- passlib/handlers/bcrypt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index e8f3f22..8d3195c 100644 --- a/passlib/handlers/bcrypt.py +++ b/passlib/handlers/bcrypt.py @@ -29,6 +29,8 @@ ) from passlib.utils.binary import bcrypt64 +from packaging.version import Version + _bcrypt = None # dynamically imported by _load_backend_bcrypt() __all__ = [ @@ -621,7 +623,7 @@ def _load_backend_mixin(mixin_cls, name, dryrun): try: version = metadata.version("bcrypt") - if version >= "5.0.0": + if Version(version) >= Version("5.0.0"): mixin_cls._backend_raises_on_truncate = True except Exception: logger.warning("(trapped) error reading bcrypt version", exc_info=True) From 74401de243ae8faf69105595fc03b12656c50abc Mon Sep 17 00:00:00 2001 From: Jonathan Chapman Date: Wed, 15 Oct 2025 10:06:39 -0400 Subject: [PATCH 5/5] Removing packaging dependency, comparing numeric version with split() and cast --- passlib/handlers/bcrypt.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index 8d3195c..8c225a3 100644 --- a/passlib/handlers/bcrypt.py +++ b/passlib/handlers/bcrypt.py @@ -29,8 +29,6 @@ ) from passlib.utils.binary import bcrypt64 -from packaging.version import Version - _bcrypt = None # dynamically imported by _load_backend_bcrypt() __all__ = [ @@ -623,7 +621,7 @@ def _load_backend_mixin(mixin_cls, name, dryrun): try: version = metadata.version("bcrypt") - if Version(version) >= Version("5.0.0"): + 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)