"""Stdlib-only secret hashing. Vendored from furtka/furtka/furtka/passwd.py — kept byte-for-byte identical to the source rather than reimplemented, the same "own a copy, keep it in lockstep" approach furtka-apps/scripts/vendor/furtka_manifest.py already uses for the manifest schema. Used here to hash registration/box/account bearer tokens at rest, not just user passwords. Format: ``$$`` - ``pbkdf2::`` — what we generate by default here - ``scrypt:::

`` — accepted for parity with the furtka-core copy; never produced by this module Both are implemented via ``hashlib`` which has been stdlib since 3.6. """ from __future__ import annotations import hashlib import hmac import secrets _PBKDF2_HASH = "sha256" _PBKDF2_ITERATIONS = 600_000 _SALT_LEN = 16 def hash_password(password: str) -> str: """Return a ``pbkdf2:sha256:$$`` hash of *password*. PBKDF2-SHA256 over UTF-8. 600k iterations — same as werkzeug's default in the 3.x series, roughly OWASP 2023's recommendation. """ if not isinstance(password, str): raise TypeError("password must be str") salt = secrets.token_urlsafe(_SALT_LEN)[:_SALT_LEN] dk = hashlib.pbkdf2_hmac( _PBKDF2_HASH, password.encode("utf-8"), salt.encode("utf-8"), _PBKDF2_ITERATIONS ) return f"pbkdf2:{_PBKDF2_HASH}:{_PBKDF2_ITERATIONS}${salt}${dk.hex()}" def verify_password(password: str, hashed: str) -> bool: """Constant-time verify *password* against a stored *hashed* value.""" if not isinstance(password, str) or not isinstance(hashed, str): return False try: method, salt, expected = hashed.split("$", 2) except ValueError: return False parts = method.split(":") if not parts: return False algo = parts[0] pw_bytes = password.encode("utf-8") salt_bytes = salt.encode("utf-8") try: if algo == "pbkdf2": if len(parts) < 3: return False inner_hash = parts[1] iterations = int(parts[2]) dk = hashlib.pbkdf2_hmac(inner_hash, pw_bytes, salt_bytes, iterations) elif algo == "scrypt": if len(parts) < 4: return False n = int(parts[1]) r = int(parts[2]) p = int(parts[3]) dk = hashlib.scrypt( pw_bytes, salt=salt_bytes, n=n, r=r, p=p, dklen=64, maxmem=132 * 1024 * 1024, ) else: return False except (ValueError, TypeError, OverflowError): return False return hmac.compare_digest(dk.hex(), expected)