diff --git a/algorithms.py b/algorithms.py index c8f4b27..d77e450 100644 --- a/algorithms.py +++ b/algorithms.py @@ -43,3 +43,4 @@ def list_algorithms() -> list[str]: # Import to auto-register algorithms import pbkdf2_algorithm # noqa: E402, F401 import argon2_algorithm # noqa: E402, F401 +import bcrypt_algorithm # noqa: E402, F401 diff --git a/bcrypt_algorithm.py b/bcrypt_algorithm.py new file mode 100644 index 0000000..b4e5ffc --- /dev/null +++ b/bcrypt_algorithm.py @@ -0,0 +1,48 @@ +"""bcrypt algorithm implementation.""" + +from __future__ import annotations + +import base64 +import binascii + +import bcrypt as bcrypt_lib + + +class BcryptAlgorithm: + """bcrypt password hashing algorithm.""" + + identifier = "bcrypt" + + def hash(self, password: str, **kwargs) -> tuple[str, str]: + """Hash a password using bcrypt. + + Note: bcrypt generates its own salt internally and returns + a complete hash string that includes the salt. + We return empty string for salt_b64 and the full hash as hash_b64. + """ + hashed = bcrypt_lib.hashpw(password.encode("utf-8"), bcrypt_lib.gensalt()) + # Return empty salt since bcrypt embeds salt in hash + return ("", base64.b64encode(hashed).decode("utf-8")) + + def verify( + self, + password: str, + salt_b64: str, + hash_b64: str, + **kwargs, + ) -> bool: + """Verify a password against bcrypt hash. + + Note: salt_b64 is ignored since bcrypt embeds salt in the hash. + """ + try: + hashed = base64.b64decode(hash_b64, validate=True) + return bcrypt_lib.checkpw(password.encode("utf-8"), hashed) + except (binascii.Error, ValueError): + return False + + +from algorithms import register_algorithm + +# Auto-register when module is imported +register_algorithm(BcryptAlgorithm()) diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py index e8c5363..f76e3b7 100644 --- a/tests/test_algorithms.py +++ b/tests/test_algorithms.py @@ -52,3 +52,17 @@ def test_argon2_algorithm_identifier(): """Verify Argon2 algorithm has correct identifier.""" algo = get_algorithm("argon2") assert algo.identifier == "argon2" + + +def test_bcrypt_algorithm_hash_round_trip(): + """Verify bcrypt algorithm can hash and verify passwords.""" + algo = get_algorithm("bcrypt") + salt, hashed = algo.hash("test password") + assert algo.verify("test password", salt, hashed) + assert not algo.verify("wrong password", salt, hashed) + + +def test_bcrypt_algorithm_identifier(): + """Verify bcrypt algorithm has correct identifier.""" + algo = get_algorithm("bcrypt") + assert algo.identifier == "bcrypt"