diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index e8cf621..1a33b78 100644 --- a/passlib/handlers/bcrypt.py +++ b/passlib/handlers/bcrypt.py @@ -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(): @@ -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 @@ -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 @@ -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 @@ -415,9 +431,9 @@ 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": @@ -425,8 +441,8 @@ def assert_lacks_wrap_bug(ident): # 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, @@ -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. @@ -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) @@ -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): """ @@ -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 = "" diff --git a/tests/test_handlers_bcrypt.py b/tests/test_handlers_bcrypt.py index b49c1fb..55655ad 100644 --- a/tests/test_handlers_bcrypt.py +++ b/tests/test_handlers_bcrypt.py @@ -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.