feat: implement PBKDF2 algorithm with registry
This commit is contained in:
@@ -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
69
pbkdf2_algorithm.py
Normal 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())
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user