功能说明
该示例以 scrypt 派生口令哈希。每个口令使用独立的随机盐,派生参数与盐、摘要一起编码进单串,便于日后调参或迁移而不失兼容。校验时从编码串中还原参数与盐,重新派生并用恒定时间比较摘要,避免通过比较耗时逐字节推断。
关键代码
编码格式使用美元符号分隔算法名、三个参数、盐与摘要的十六进制,使全部派生信息随哈希一同存储,无需额外查表。校验先校验格式与算法标识,格式不符直接返回假。摘要长度由待比较值决定,使参数变更后仍可校验旧哈希。比较使用 hmac.compare_digest 而非等于运算符,杜绝时序侧信道。
使用注意
scrypt 的内存与时间开销由 N、r、p 决定,示例参数用于演示,生产应根据硬件标定到数十毫秒量级,过高会拖垮登录接口,过低则削弱抗暴破能力。盐必须每用户唯一且足够随机,复用盐会让相同口令产生相同哈希。校验失败永不透露是口令错误还是账号不存在,避免用户枚举;升级参数时应在用户下次成功登录后重哈希并回写。
代码示例
import hashlib
import hmac
import os
N = 2 ** 14
R = 8
P = 1
KEY_LEN = 32
SALT_LEN = 16
def hash_password(password: str) -> str:
salt = os.urandom(SALT_LEN)
digest = hashlib.scrypt(password.encode('utf-8'), salt=salt, n=N, r=R, p=P, dklen=KEY_LEN)
return f'scrypt${N}${R}${P}${salt.hex()}${digest.hex()}'
def verify_password(password: str, encoded: str) -> bool:
parts = encoded.split('$')
if len(parts) != 5 or parts[0] != 'scrypt':
return False
_, n, r, p, rest = parts
salt_hex, digest_hex = rest[:SALT_LEN * 2], rest[SALT_LEN * 2:]
expected = bytes.fromhex(digest_hex)
digest = hashlib.scrypt(password.encode('utf-8'), salt=bytes.fromhex(salt_hex),
n=int(n), r=int(r), p=int(p), dklen=len(expected))
return hmac.compare_digest(digest, expected)