Spaces:
Sleeping
Sleeping
| """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) | |