Files
gaokao-volunteer-system/admin/password.py

72 lines
1.8 KiB
Python
Raw Normal View History

2026-06-13 14:49:58 +08:00
"""密码哈希 (T6.1).
使用 PBKDF2-HMAC-SHA256200k 迭代salt 16B hexhash 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)