Skip to content
Closed
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
51 changes: 37 additions & 14 deletions passlib/handlers/bcrypt.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
_BNULL = b"\x00"

# reference hash of "test", used in various self-checks
TEST_HASH_2A = "$2a$04$5BJqKfqMQvV7nS.yUguNcueVirQqDBGaLXSqj.rs.pZPlNR0UX/HK"
TEST_HASH_2A = f"{IDENT_2A}04$5BJqKfqMQvV7nS.yUguNcueVirQqDBGaLXSqj.rs.pZPlNR0UX/HK"


def _detect_pybcrypt():
Expand Down Expand Up @@ -147,6 +147,7 @@ class _BcryptCommon( # type: ignore[misc]
# NOTE: these are only set on the backend mixin classes
_workrounds_initialized = False
_has_2a_wraparound_bug = False
_fails_on_wraparound_bug = False
_lacks_20_support = False
_lacks_2y_support = False
_lacks_2b_support = False
Expand Down Expand Up @@ -372,7 +373,7 @@ def detect_wrap_bug(ident):
ident.encode("ascii")
+ b"04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6"
)
if verify(secret, bug_hash):
if handled_verify_wrap_error(secret, bug_hash):
return True

# if it doesn't have wraparound bug, make sure it *does* handle things
Expand All @@ -381,13 +382,28 @@ def detect_wrap_bug(ident):
ident.encode("ascii")
+ b"04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi"
)
if not verify(secret, correct_hash):
if not handled_verify_wrap_error(mixin_cls.wrap_if_fails_on_wraparound_bug(secret), correct_hash):
raise RuntimeError(
f"{backend} backend failed to verify {ident} wraparound hash"
)

return False

def handled_verify_wrap_error(secret, test_hash):
try:
return verify(secret, test_hash)
except ValueError as err:
if mixin_cls._fails_on_wraparound_bug \
and any(word in str(err) for word in ["password", str(mixin_cls.truncate_size)]):
logger.warning(
"trapped %r backend %r",
backend,
err,
exc_info=True,
)
return False
raise err

def assert_lacks_wrap_bug(ident):
if not detect_wrap_bug(ident):
return
Expand Down Expand Up @@ -415,18 +431,18 @@ def assert_lacks_wrap_bug(ident):
result = safe_verify("test", TEST_HASH_2A)
if result is NotImplemented:
# 2a support is required, and should always be present
raise RuntimeError(f"{backend} lacks support for $2a$ hashes")
raise RuntimeError(f"{backend} lacks support for {IDENT_2A} hashes")
if not result:
raise RuntimeError(f"{backend} incorrectly rejected $2a$ hash")
raise RuntimeError(f"{backend} incorrectly rejected {IDENT_2A} hash")
assert_lacks_8bit_bug(IDENT_2A)
if detect_wrap_bug(IDENT_2A):
if backend == "os_crypt":
# don't make this a warning for os crypt (e.g. openbsd);
# they'll have proper 2b implementation which will be used for new hashes.
# so even if we didn't have a workaround, this bug wouldn't be a concern.
logger.debug(
"%r backend has $2a$ bsd wraparound bug, enabling workaround",
backend,
"%r backend has %s bsd wraparound bug, enabling workaround",
backend, IDENT_2A
)
else:
# installed library has the bug -- want to let users know,
Expand All @@ -443,13 +459,13 @@ def assert_lacks_wrap_bug(ident):
# ----------------------------------------------------------------
# check for 2y support
# ----------------------------------------------------------------
test_hash_2y = TEST_HASH_2A.replace("2a", "2y")
test_hash_2y = TEST_HASH_2A.replace(IDENT_2A, IDENT_2Y)
result = safe_verify("test", test_hash_2y)
if result is NotImplemented:
mixin_cls._lacks_2y_support = True
logger.debug("%r backend lacks $2y$ support, enabling workaround", backend)
logger.debug("%r backend lacks %s support, enabling workaround", backend, IDENT_2Y)
elif not result:
raise RuntimeError(f"{backend} incorrectly rejected $2y$ hash")
raise RuntimeError(f"{backend} incorrectly rejected {IDENT_2Y} hash")
else:
# NOTE: Not using this as fallback candidate,
# lacks wide enough support across implementations.
Expand All @@ -463,13 +479,13 @@ def assert_lacks_wrap_bug(ident):
# ----------------------------------------------------------------
# check for 2b support
# ----------------------------------------------------------------
test_hash_2b = TEST_HASH_2A.replace("2a", "2b")
test_hash_2b = TEST_HASH_2A.replace(IDENT_2A, IDENT_2B)
result = safe_verify("test", test_hash_2b)
if result is NotImplemented:
mixin_cls._lacks_2b_support = True
logger.debug("%r backend lacks $2b$ support, enabling workaround", backend)
logger.debug("%r backend lacks %s support, enabling workaround", backend, IDENT_2B)
elif not result:
raise RuntimeError(f"{backend} incorrectly rejected $2b$ hash")
raise RuntimeError(f"{backend} incorrectly rejected {IDENT_2B} hash")
else:
mixin_cls._fallback_ident = IDENT_2B
assert_lacks_8bit_bug(IDENT_2B)
Expand Down Expand Up @@ -570,13 +586,19 @@ def _norm_digest_args(cls, secret, ident, new=False):
elif ident == IDENT_2X:
# NOTE: shouldn't get here.
# XXX: could check if backend does actually offer 'support'
raise RuntimeError("$2x$ hashes not currently supported by passlib")
raise RuntimeError(f"{IDENT_2X} hashes not currently supported by passlib")

else:
raise AssertionError(f"unexpected ident value: {ident!r}")

return secret, ident

@classmethod
def wrap_if_fails_on_wraparound_bug(cls, secret):
return (secret[:cls.truncate_size] if cls._fails_on_wraparound_bug
and len(secret) > cls.truncate_size
else secret)


class _NoBackend(_BcryptCommon):
"""
Expand Down Expand Up @@ -609,6 +631,7 @@ def _load_backend_mixin(mixin_cls, name, dryrun):
return False
try:
version = metadata.version("bcrypt")
mixin_cls._fails_on_wraparound_bug = version >= "5.0.0"
except Exception:
logger.warning("(trapped) error reading bcrypt version", exc_info=True)
version = "<unknown>"
Expand Down
7 changes: 7 additions & 0 deletions tests/test_handlers_bcrypt.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@ def setUp(self):
self.addCleanup(os.environ.__delitem__, key)
os.environ[key] = "true"

wrapped_correct_hashes = []
for secret, hash in self.known_correct_hashes:
wrapped_correct_hashes.append((self.handler.wrap_if_fails_on_wraparound_bug(secret), hash))
type(self).known_correct_hashes = wrapped_correct_hashes

self.handler.truncate_error = self.handler._fails_on_wraparound_bug

super().setUp()

# silence this warning, will come up a bunch during testing of old 2a hashes.
Expand Down
Loading