1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
"""Password hashing with stdlib scrypt (Argon2id is not used)."""
from __future__ import annotations
import base64
import hashlib
import hmac
import secrets
_SCRYPT_N = 2**17
_SCRYPT_R = 8
_SCRYPT_P = 1
_SCRYPT_DKLEN = 64
_SCRYPT_SALT_BYTES = 16
_SCRYPT_MAXMEM = 128 * _SCRYPT_N * _SCRYPT_R * (_SCRYPT_P + 2)
_ALGORITHM = "scrypt"
def _password_material(password: str, config_salt: str) -> str:
if not config_salt:
return password
return f"{config_salt}:{password}"
def _scrypt_key(password: str, *, salt: bytes, n: int, r: int, p: int, dklen: int) -> bytes:
return hashlib.scrypt(
password.encode("utf-8"),
salt=salt,
n=n,
r=r,
p=p,
maxmem=max(128 * n * r * (p + 2), _SCRYPT_MAXMEM),
dklen=dklen,
)
def hash_password(password: str, *, config_salt: str = "") -> str:
material = _password_material(password, config_salt)
salt = secrets.token_bytes(_SCRYPT_SALT_BYTES)
key = _scrypt_key(material, salt=salt, n=_SCRYPT_N, r=_SCRYPT_R, p=_SCRYPT_P, dklen=_SCRYPT_DKLEN)
salt_b64 = base64.b64encode(salt).decode("ascii")
key_b64 = base64.b64encode(key).decode("ascii")
return f"{_ALGORITHM}${_SCRYPT_N}${_SCRYPT_R}${_SCRYPT_P}${salt_b64}${key_b64}"
def verify_password(password: str, pass_hash: str, *, config_salt: str = "") -> bool:
if not pass_hash:
return False
if config_salt and _verify_password_material(_password_material(password, config_salt), pass_hash):
return True
return _verify_password_material(password, pass_hash)
def _verify_password_material(material: str, pass_hash: str) -> bool:
if not pass_hash:
return False
parts = pass_hash.split("$")
if len(parts) != 6 or parts[0] != _ALGORITHM:
return False
try:
n = int(parts[1])
r = int(parts[2])
p = int(parts[3])
if n < _SCRYPT_N or r < _SCRYPT_R or p < _SCRYPT_P:
return False
salt = base64.b64decode(parts[4], validate=True)
expected = base64.b64decode(parts[5], validate=True)
except (ValueError, TypeError):
return False
try:
actual = _scrypt_key(material, salt=salt, n=n, r=r, p=p, dklen=len(expected))
except ValueError:
return False
return hmac.compare_digest(actual, expected)