Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ __pycache__
coverage.xml

dist

*.pyc
24 changes: 16 additions & 8 deletions passlib/handlers/bcrypt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 = "<unknown>"

logger.debug("detected 'bcrypt' backend, version %r", version)

return mixin_cls._finalize_backend_mixin(name, dryrun)

# # TODO: would like to implementing verify() directly,
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions tests/test_handlers_bcrypt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading