62 lines
1.5 KiB
Python
62 lines
1.5 KiB
Python
"""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}"
|