feat: implement PBKDF2 algorithm with registry

This commit is contained in:
2025-11-13 13:13:15 +00:00
parent 5c4371f787
commit 95f4d7dafd
3 changed files with 90 additions and 0 deletions

View File

@@ -38,3 +38,7 @@ def get_algorithm(name: str) -> Algorithm:
def list_algorithms() -> list[str]: def list_algorithms() -> list[str]:
"""Return list of registered algorithm identifiers.""" """Return list of registered algorithm identifiers."""
return sorted(_ALGORITHMS.keys()) return sorted(_ALGORITHMS.keys())
# Import to auto-register algorithms
import pbkdf2_algorithm # noqa: E402, F401

69
pbkdf2_algorithm.py Normal file
View File

@@ -0,0 +1,69 @@
"""PBKDF2 algorithm implementation."""
from __future__ import annotations
import base64
import binascii
import hashlib
import hmac
import os
DEFAULT_SALT_BYTES = 16
DEFAULT_ITERATIONS = int(os.environ.get("PBKDF2_ITERATIONS", "200000"))
class PBKDF2Algorithm:
"""PBKDF2-HMAC-SHA256 password hashing algorithm."""
identifier = "pbkdf2"
def hash(
self,
password: str,
*,
iterations: int | None = None,
salt_bytes: int = DEFAULT_SALT_BYTES,
) -> tuple[str, str]:
"""Hash a password using PBKDF2-HMAC-SHA256."""
iterations = iterations or DEFAULT_ITERATIONS
salt = os.urandom(salt_bytes)
derived = hashlib.pbkdf2_hmac(
"sha256",
password.encode("utf-8"),
salt,
iterations,
)
return (
base64.b64encode(salt).decode("utf-8"),
base64.b64encode(derived).decode("utf-8"),
)
def verify(
self,
password: str,
salt_b64: str,
hash_b64: str,
*,
iterations: int | None = None,
) -> bool:
"""Verify a password against PBKDF2 salt and hash."""
iterations = iterations or DEFAULT_ITERATIONS
try:
salt = base64.b64decode(salt_b64, validate=True)
stored_hash = base64.b64decode(hash_b64, validate=True)
except (binascii.Error, ValueError):
return False
derived = hashlib.pbkdf2_hmac(
"sha256",
password.encode("utf-8"),
salt,
iterations,
)
return hmac.compare_digest(derived, stored_hash)
from algorithms import register_algorithm
# Auto-register when module is imported
register_algorithm(PBKDF2Algorithm())

View File

@@ -21,3 +21,20 @@ def test_get_algorithm_unknown_raises_error():
"""Verify unknown algorithm raises ValueError.""" """Verify unknown algorithm raises ValueError."""
with pytest.raises(ValueError, match="Unknown algorithm"): with pytest.raises(ValueError, match="Unknown algorithm"):
get_algorithm("unknown") get_algorithm("unknown")
def test_pbkdf2_algorithm_hash_round_trip():
"""Verify PBKDF2 algorithm can hash and verify passwords."""
algo = get_algorithm("pbkdf2")
salt, hashed = algo.hash("test password")
assert algo.verify("test password", salt, hashed)
assert not algo.verify("wrong password", salt, hashed)
def test_pbkdf2_algorithm_respects_iterations():
"""Verify PBKDF2 algorithm respects custom iterations."""
algo = get_algorithm("pbkdf2")
salt1, hash1 = algo.hash("test", iterations=100000)
salt2, hash2 = algo.hash("test", iterations=200000)
# Different iterations should produce different hashes even with same password
assert hash1 != hash2