1.2 哈希、MAC、KDF 与密码存储
哈希、MAC、KDF 与密码哈希的输出都可能是一串看似随机的字节,但用途不能从输出外观判断;有些接口还能配置输出长度。把 SHA-256(password) 当密码存储,或把普通 hash 当消息签名,都是把相似接口误当成相同安全语义。
普通哈希没有秘密
digest = Hash(message)密码学哈希通常追求原像抗性、第二原像抗性和碰撞抗性。它适合内容寻址、完整性指纹和签名协议的组成部分。
但任何人都能重新计算普通 hash。若攻击者能同时替换文件和 digest,SHA-256(file) 不能证明文件来自谁。MD5 与 SHA-1 已不适合碰撞安全场景;新设计通常使用 SHA-256/384、SHA-3 或协议明确规定的现代算法。
MAC 认证共享密钥下的消息
tag = HMAC(key, message)HMAC 将秘密密钥纳入认证。在算法和密钥安全的假设下,有效 tag 表明消息由持有密钥的一方认证;因为验证者也持有密钥,它不能向第三方证明究竟是哪一方生成。验证者必须持有同一密钥,所以它适合服务间 webhook、内部协议和数据库字段完整性。
验证时使用库提供的 constant-time compare,避免普通字符串比较泄露第一个不同字节的位置。还要把协议上下文编码进消息:
version || method || path || timestamp || body_digest字段需要无歧义长度或规范化编码,例如直接拼接字段 (ab,c) 与 (a,bc) 都得到 abc;即使用分隔符,也要处理字段自身包含分隔符的情况。为防重放,还要校验时间窗口和一次性 ID;合法 HMAC 不证明它是第一次到达;“检查 ID 尚未用过并记为已用”还必须是原子操作,避免并发重放同时通过。
KDF 用于派生相互隔离的子密钥
不要让一个主密钥同时用于加密、MAC、token 签名和数据库字段。HKDF 一类 KDF 可以从输入密钥材料派生用途隔离的子密钥:
K_encrypt = HKDF(master, info="atlas/v1/encryption")
K_mac = HKDF(master, info="atlas/v1/webhook-mac")info 用于 domain separation。即使底层主密钥相同,不同协议和用途也不会直接复用同一输出。HKDF 适合高熵密钥材料,不是为了把用户短密码“变安全”。
密码需要故意昂贵的专用函数
用户密码熵低,攻击者拿到数据库后可以离线猜测。通用 hash 太快,反而帮助攻击者。密码验证应使用带独立随机 salt 的 memory-hard password hashing function,例如 Argon2id:
encoded = Argon2id(password, random_salt, memory, iterations, parallelism)Salt 不需要保密,作用是让相同密码产生不同 verifier,并阻止一次预计算覆盖所有账户。每条记录应保存算法、版本、参数、salt 和输出,便于未来升级。
参数不能照抄一个永恒数字。应在生产级硬件和峰值并发下基准测试,把单次验证调到系统可承受、攻击者又足够昂贵的水平,并为登录洪泛预留容量。RFC 9106 给出 Argon2id 参考配置,但真实服务必须结合内存预算和拒绝服务风险调优。
Pepper 是额外的服务器秘密,可存放于 KMS/HSM 并在 hash 前后以受审查方案加入。它能让只泄露数据库的攻击者缺少一部分材料,但会增加轮换和灾难恢复复杂度,不能替代 salt 和合适的 KDF。
登录验证要支持渐进升级
读取记录 → 按记录参数验证
→ 若参数已落后,在本次成功登录后用新参数重算这不需要长期保存明文密码,但重算必须使用本次成功登录提交的密码;仅凭旧 verifier 不能完成一般的参数升级。写回时还要防止并发密码重置被旧登录请求覆盖。长期不登录账户可在风险事件后强制重置。
密码政策同样影响实际安全。NIST SP 800-63B-4 对遵循该标准的验证方明确禁止强制大小写、数字、符号的组合规则;应允许长密码和密码管理器粘贴,阻止常见或已泄露密码,限制在线猜测,并在有泄露证据时要求更换,而不是固定周期无故轮换。
单因素密码至少 15 个字符;作为多因素流程的一部分可以允许更短,但不得低于 8 个字符。这是 NIST 的适用场景要求,不代表所有产品都必须机械复制同一最小值;业务仍需依据身份保证等级和监管要求建模。
验证随机盐与错误密码
下面需要 argon2-cffi 包。其 PasswordHasher 高层接口负责生成随机盐并保存参数;示例使用库默认值展示行为,生产仍需做并发和内存基准。不要记录这里提交的密码,也不要把示例常量当真实凭据。
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
hasher = PasswordHasher()
password = "example passphrase for this local check"
encoded = hasher.hash(password)
assert encoded != hasher.hash(password)
assert hasher.verify(encoded, password)
try:
hasher.verify(encoded, "a different passphrase")
except VerifyMismatchError:
pass
else:
raise AssertionError("incorrect password was accepted")
if hasher.check_needs_rehash(encoded):
replacement = hasher.hash(password)若生成了 replacement,存储层还需确认记录仍是刚才验证的版本,再原子替换。损坏的编码记录、资源不足与密码不匹配是不同内部故障,应分开监控,同时避免对外泄露账户信息。
易混淆关系
| 需求 | 应考虑 | 不应直接使用 |
|---|---|---|
| 文件内容指纹 | SHA-256 等 hash | 密码验证器(用途不同,成本过高) |
| 共享密钥消息认证 | HMAC | `Hash(key |
| 从主密钥派生用途密钥 | HKDF | 截取 master key 字节 |
| 存储用户密码 | Argon2id 等密码哈希 | SHA-256、可逆加密 |
| 防止 webhook 重放 | MAC + 时间/nonce 状态 | 只有 MAC |
下一课进入公钥密码:公钥解决密钥分发和签名问题,但不会让任意数据都适合“直接用 RSA 加密”。
参考资料
- RFC 2104, HMAC
- RFC 5869, HKDF
- RFC 9106, Argon2 Memory-Hard Function
- NIST, SP 800-63B-4 Authenticator Requirements