"""Password hashing with scrypt from the standard library (no extra dependencies). Stored format: scrypt$$$

$$. The parameters are stored with the hash, so they can be raised later without invalidating existing passwords. """ import base64 import hashlib import hmac import secrets _N, _R, _P = 2**14, 8, 1 _KEY_LENGTH = 32 def _derive(password: str, salt: bytes, n: int, r: int, p: int) -> bytes: return hashlib.scrypt( password.encode(), salt=salt, n=n, r=r, p=p, dklen=_KEY_LENGTH, maxmem=128 * n * r * 2, ) def hash_password(password: str) -> str: salt = secrets.token_bytes(16) key = _derive(password, salt, _N, _R, _P) encode = lambda value: base64.b64encode(value).decode() # noqa: E731 return f"scrypt${_N}${_R}${_P}${encode(salt)}${encode(key)}" def verify_password(password: str, stored: str) -> bool: try: scheme, n, r, p, salt, key = stored.split("$") if scheme != "scrypt": return False expected = base64.b64decode(key) actual = _derive(password, base64.b64decode(salt), int(n), int(r), int(p)) except ValueError: return False return hmac.compare_digest(actual, expected)