From 2648e2467cf3cc9473bb380dd3c80d43cf38058e Mon Sep 17 00:00:00 2001 From: Jonathan Chapman Date: Wed, 8 Oct 2025 13:50:58 -0400 Subject: [PATCH 1/6] Fix bcrypt wraparound bug test for bcrypt >= 5.0.0 --- passlib/handlers/bcrypt.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index e8cf621..1ab486a 100644 --- a/passlib/handlers/bcrypt.py +++ b/passlib/handlers/bcrypt.py @@ -366,17 +366,25 @@ def detect_wrap_bug(ident): NOTE: if in future we need to deliberately create hashes which have this bug, can use something like 'hashpw(repeat_string(secret[:((1+secret) % 256) or 1]), 72)' """ - # check if it exhibits wraparound bug - secret = (b"0123456789" * 26)[:255] - bug_hash = ( - ident.encode("ascii") - + b"04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6" - ) - if verify(secret, bug_hash): - return True - # if it doesn't have wraparound bug, make sure it *does* handle things - # correctly -- or we're in some weird third case. + # 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: + 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] + + # 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. correct_hash = ( ident.encode("ascii") + b"04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi" From efa7aa227e7cc8811606c44423310b58478cb8a0 Mon Sep 17 00:00:00 2001 From: Jonathan Chapman Date: Wed, 8 Oct 2025 13:51:14 -0400 Subject: [PATCH 2/6] Unpin bcrypt < 5.0.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1b23fae..c5059e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ argon2 = [ "argon2-cffi>=18.2.0", ] bcrypt = [ - "bcrypt<5.0.0", + "bcrypt>=3.1.0", ] totp = [ "cryptography>=43.0.1", From f6d652395190e8ae55a0aed0e09faff8ae80a26c Mon Sep 17 00:00:00 2001 From: Jonathan Chapman Date: Wed, 8 Oct 2025 13:54:57 -0400 Subject: [PATCH 3/6] Fixing bcrypt backend version detection --- passlib/handlers/bcrypt.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index 1ab486a..320af86 100644 --- a/passlib/handlers/bcrypt.py +++ b/passlib/handlers/bcrypt.py @@ -615,11 +615,20 @@ def _load_backend_mixin(mixin_cls, name, dryrun): import bcrypt as _bcrypt except ImportError: # pragma: no cover return False + + # Attempt to get bcrypt backend version + version = '' + try: - version = metadata.version("bcrypt") - except Exception: - logger.warning("(trapped) error reading bcrypt version", exc_info=True) - version = "" + # "New style" (793bef 2023-11-23) version + version = _bcrypt.__version__ + except: + try: + # Old style verion + version = _bcrypt.__about__.__version__ + except: + # Can't find version, leave it as '' + log.warning("(trapped) error reading bcrypt version", exc_info=True) logger.debug("detected 'bcrypt' backend, version %r", version) return mixin_cls._finalize_backend_mixin(name, dryrun) From d7f80d37d7f16a4caa46af93cc249bd19a20a3c7 Mon Sep 17 00:00:00 2001 From: Jonathan Chapman Date: Wed, 8 Oct 2025 15:11:31 -0400 Subject: [PATCH 4/6] Fixing indent in bcrypt.py --- passlib/handlers/bcrypt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index 320af86..5e004fc 100644 --- a/passlib/handlers/bcrypt.py +++ b/passlib/handlers/bcrypt.py @@ -368,7 +368,7 @@ def detect_wrap_bug(ident): """ # Secret which will trip the wraparound bug, if present - secret = (b"0123456789"*26)[:255] + 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 From 10d51a23db86321257bbb2a89e9effb363e8ed25 Mon Sep 17 00:00:00 2001 From: Jonathan Chapman Date: Thu, 9 Oct 2025 09:04:47 -0400 Subject: [PATCH 5/6] Backing out version check change --- passlib/handlers/bcrypt.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index 5e004fc..6ff04a5 100644 --- a/passlib/handlers/bcrypt.py +++ b/passlib/handlers/bcrypt.py @@ -615,20 +615,11 @@ def _load_backend_mixin(mixin_cls, name, dryrun): import bcrypt as _bcrypt except ImportError: # pragma: no cover return False - - # Attempt to get bcrypt backend version - version = '' - try: - # "New style" (793bef 2023-11-23) version - version = _bcrypt.__version__ - except: - try: - # Old style verion - version = _bcrypt.__about__.__version__ - except: - # Can't find version, leave it as '' - log.warning("(trapped) error reading bcrypt version", exc_info=True) + version = metadata.version("bcrypt") + 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) From ea34c47c3d54271d356736d56fcf480e81af1913 Mon Sep 17 00:00:00 2001 From: Jonathan Chapman Date: Thu, 9 Oct 2025 09:07:48 -0400 Subject: [PATCH 6/6] Removing empty line --- passlib/handlers/bcrypt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index 6ff04a5..adbcc52 100644 --- a/passlib/handlers/bcrypt.py +++ b/passlib/handlers/bcrypt.py @@ -382,7 +382,7 @@ def detect_wrap_bug(ident): except ValueError: # Backend explicitly will not auto-truncate, truncate the password to 72 characters secret = secret[:72] - + # 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. correct_hash = (