from typing import List, Tuple from .crypto_constant_time_equal import constant_time_equal from .crypto_pbkdf2_sha256 import pbkdf2_sha256 from .encoding_base64_base64_decode import base64_decode from .encoding_utf8_utf8_encode import utf8_encode MALFORMED = "stored password hash is malformed" def _decode_field(text: str) -> List[int]: try: data = base64_decode(text) except ValueError: raise ValueError(MALFORMED) from None if len(data) == 0: raise ValueError(MALFORMED) return data def parse_stored_hash(stored: str) -> Tuple[int, List[int], List[int]]: """(iterations, salt, hash) of a stored pbkdf2_sha256 string. A string this code did not write is a data problem to surface, not a wrong password.""" if not isinstance(stored, str): raise TypeError("stored password hash must be a string") parts = stored.split("$") if parts[0] != "pbkdf2_sha256": raise ValueError("stored password hash is not a pbkdf2_sha256 hash") if len(parts) != 4: raise ValueError(MALFORMED) count = parts[1] if len(count) == 0 or len(count) > 10 or count[0] == "0" or any(not ("0" <= ch <= "9") for ch in count): raise ValueError(MALFORMED) return int(count), _decode_field(parts[2]), _decode_field(parts[3]) def verify_password(password: str, stored: str) -> bool: """Does the password match the stored hash? Re-derived with the stored salt and iteration count, compared in constant time.""" if not isinstance(password, str): raise TypeError("password must be a string") iterations, salt, expected = parse_stored_hash(stored) derived = pbkdf2_sha256(utf8_encode(password), salt, iterations, len(expected)) return constant_time_equal(derived, expected)