1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
|
From a3f920fd8f11aa543fea8d1f537d49fe0519be86 Mon Sep 17 00:00:00 2001
From: Jonathan Chapman <glitch@glitchwrks.com>
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(-)
--- a/passlib/handlers/bcrypt.py
+++ b/passlib/handlers/bcrypt.py
@@ -152,6 +152,7 @@
_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 @@
# 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 @@
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 = "<unknown>"
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 @@
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:
--- a/tests/test_handlers_bcrypt.py
+++ b/tests/test_handlers_bcrypt.py
@@ -220,9 +220,9 @@
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
|