"""Password hashing (scrypt from the standard library) and token helpers.""" import base64 import hashlib import hmac import secrets PREFIX = "$scrypt$" N, R, P = 2 ** 15, 8, 1 def _b64(b): return base64.b64encode(b).decode().rstrip("=") def _unb64(s): return base64.b64decode(s + "=" * (-len(s) % 4)) def hash_password(password): salt = secrets.token_bytes(16) dk = hashlib.scrypt(password.encode(), salt=salt, n=N, r=R, p=P, maxmem=64 * 1024 * 1024, dklen=32) return f"{PREFIX}n={N},r={R},p={P}${_b64(salt)}${_b64(dk)}" def is_hashed(value): return value.startswith(PREFIX) # Used for unknown users so a login attempt costs the same either way. _DUMMY = None def verify_password(password, stored): global _DUMMY if not stored or not is_hashed(stored): if _DUMMY is None: _DUMMY = hash_password("dummy") stored, ok = _DUMMY, False else: ok = True try: params, salt, dk = stored[len(PREFIX):].split("$") p = dict(kv.split("=") for kv in params.split(",")) calc = hashlib.scrypt(password.encode(), salt=_unb64(salt), n=int(p["n"]), r=int(p["r"]), p=int(p["p"]), maxmem=64 * 1024 * 1024, dklen=len(_unb64(dk))) except (ValueError, KeyError): return False return hmac.compare_digest(calc, _unb64(dk)) and ok def token(): return secrets.token_urlsafe(32) def code_hash(tok, code): return hashlib.sha256(f"{tok}:{code}".encode()).hexdigest() def new_code(): return f"{secrets.randbelow(10 ** 6):06d}"