53 lines
1.6 KiB
Python
53 lines
1.6 KiB
Python
|
|
"""密码哈希与校验。
|
|||
|
|
|
|||
|
|
当前实现使用 PBKDF2-HMAC-SHA256(内置 hashlib),以避免引入额外依赖。
|
|||
|
|
存储格式:
|
|||
|
|
pbkdf2_sha256$<iterations>$<salt_b64url>$<dk_b64url>
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import base64
|
|||
|
|
import hashlib
|
|||
|
|
import hmac
|
|||
|
|
import secrets
|
|||
|
|
|
|||
|
|
|
|||
|
|
_ALGO = "pbkdf2_sha256"
|
|||
|
|
_ITERATIONS = 210_000
|
|||
|
|
_SALT_BYTES = 16
|
|||
|
|
_DKLEN = 32
|
|||
|
|
|
|||
|
|
|
|||
|
|
def hash_password(password: str) -> str:
|
|||
|
|
"""对明文密码进行哈希并返回可存储字符串。"""
|
|||
|
|
if not isinstance(password, str) or not password:
|
|||
|
|
raise ValueError("password required")
|
|||
|
|
salt = secrets.token_bytes(_SALT_BYTES)
|
|||
|
|
dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, _ITERATIONS, dklen=_DKLEN)
|
|||
|
|
salt_b64 = base64.urlsafe_b64encode(salt).decode("ascii").rstrip("=")
|
|||
|
|
dk_b64 = base64.urlsafe_b64encode(dk).decode("ascii").rstrip("=")
|
|||
|
|
return f"{_ALGO}${_ITERATIONS}${salt_b64}${dk_b64}"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def verify_password(password: str, stored_hash: str) -> bool:
|
|||
|
|
"""校验明文密码是否匹配已存储的哈希。"""
|
|||
|
|
try:
|
|||
|
|
algo, iters_s, salt_b64, dk_b64 = stored_hash.split("$", 3)
|
|||
|
|
if algo != _ALGO:
|
|||
|
|
return False
|
|||
|
|
iterations = int(iters_s)
|
|||
|
|
salt = _b64url_decode(salt_b64)
|
|||
|
|
expected = _b64url_decode(dk_b64)
|
|||
|
|
except Exception:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations, dklen=len(expected))
|
|||
|
|
return hmac.compare_digest(dk, expected)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _b64url_decode(value: str) -> bytes:
|
|||
|
|
"""解码不带 padding 的 base64url 字符串。"""
|
|||
|
|
padded = value + "=" * (-len(value) % 4)
|
|||
|
|
return base64.urlsafe_b64decode(padded.encode("ascii"))
|