"""Hash and verify the single application password. PBKDF2, random salts, and constant-time comparison are provided by stdlib. """ import base64 import hashlib import hmac import os ALGORITHM = "pbkdf2_sha256" DEFAULT_ITERATIONS = 260_000 MIN_ITERATIONS = 200_000 def hash_password(password: str, iterations: int = DEFAULT_ITERATIONS) -> str: """Return a salted PBKDF2 password hash in the configured wire format.""" if iterations < MIN_ITERATIONS: raise ValueError(f"iterations must be at least {MIN_ITERATIONS}") salt = os.urandom(16) digest = hashlib.pbkdf2_hmac( "sha256", password.encode("utf-8"), salt, iterations, ) salt_b64 = base64.b64encode(salt).decode("ascii") digest_b64 = base64.b64encode(digest).decode("ascii") return f"{ALGORITHM}${iterations}${salt_b64}${digest_b64}" def verify_password(password: str, encoded: str) -> bool: """Verify a password without leaking comparison timing.""" try: algorithm, raw_iterations, salt_b64, expected_b64 = encoded.split("$", 3) iterations = int(raw_iterations) if algorithm != ALGORITHM or iterations < MIN_ITERATIONS: return False salt = base64.b64decode(salt_b64, validate=True) expected = base64.b64decode(expected_b64, validate=True) except (ValueError, TypeError): return False actual = hashlib.pbkdf2_hmac( "sha256", password.encode("utf-8"), salt, iterations, ) return hmac.compare_digest(actual, expected)