72 lines
1.8 KiB
Python
72 lines
1.8 KiB
Python
"""密码哈希 (T6.1).
|
||
|
||
使用 PBKDF2-HMAC-SHA256(200k 迭代)。salt 16B hex;hash 64B hex。
|
||
不依赖 bcrypt/argon2 等第三方库,保持项目"零运行时第三方"约束。
|
||
|
||
存储格式: "<salt_hex>$<hash_hex>"(便于单字段持久化)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import hashlib
|
||
import hmac
|
||
import secrets
|
||
|
||
_PBKDF2_ALGO = "sha256"
|
||
_PBKDF2_ITERATIONS = 200_000
|
||
_SALT_BYTES = 16
|
||
_HASH_BYTES = 32
|
||
_STORED_SEPARATOR = "$"
|
||
|
||
|
||
def hash_password(plain: str) -> str:
|
||
"""对明文密码进行 PBKDF2 哈希。
|
||
|
||
Args:
|
||
plain: 明文密码
|
||
|
||
Returns:
|
||
形如 "<salt_hex>$<hash_hex>" 的字符串,可直接入库
|
||
"""
|
||
if not plain:
|
||
raise ValueError("password cannot be empty")
|
||
salt = secrets.token_bytes(_SALT_BYTES)
|
||
digest = hashlib.pbkdf2_hmac(
|
||
_PBKDF2_ALGO,
|
||
plain.encode("utf-8"),
|
||
salt,
|
||
_PBKDF2_ITERATIONS,
|
||
dklen=_HASH_BYTES,
|
||
)
|
||
return f"{salt.hex()}{_STORED_SEPARATOR}{digest.hex()}"
|
||
|
||
|
||
def verify_password(plain: str, stored: str) -> bool:
|
||
"""校验明文密码与存储哈希是否匹配。
|
||
|
||
使用恒定时间比较避免时序攻击。
|
||
|
||
Args:
|
||
plain: 明文密码
|
||
stored: 数据库存储的 "<salt>$<hash>" 字符串
|
||
|
||
Returns:
|
||
True 表示匹配
|
||
"""
|
||
if not plain or not stored or _STORED_SEPARATOR not in stored:
|
||
return False
|
||
salt_hex, hash_hex = stored.split(_STORED_SEPARATOR, 1)
|
||
try:
|
||
salt = bytes.fromhex(salt_hex)
|
||
expected = bytes.fromhex(hash_hex)
|
||
except ValueError:
|
||
return False
|
||
candidate = hashlib.pbkdf2_hmac(
|
||
_PBKDF2_ALGO,
|
||
plain.encode("utf-8"),
|
||
salt,
|
||
_PBKDF2_ITERATIONS,
|
||
dklen=_HASH_BYTES,
|
||
)
|
||
return hmac.compare_digest(candidate, expected)
|