39 lines
1.2 KiB
Python
39 lines
1.2 KiB
Python
"""Password hashing with scrypt from the standard library (no extra dependencies).
|
|
|
|
Stored format: scrypt$<n>$<r>$<p>$<salt, base64>$<hash, base64>. 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)
|