从 Bcrypt 或 Argon2 转到 PBKDF2 的人,第一次做校验时往往会卡住:数据库里存着一串十六进制,但校验函数要求提供算法、迭代次数、输出长度和盐。这些参数当初存在哪里了?如果当初没存,现在就无法校验。这是 PBKDF2 与其他口令哈希最重要的差别。
自描述与裸摘要
口令哈希的存储格式可以分成两类。
Bcrypt 和 Argon2 属于自描述格式。它们的哈希串里包含了算法标识、参数和盐,例如 $2a$10$N9qo8uLOickgx2ZMRZoMye... 里就写明了版本、cost 和盐。校验时只需要把这个字符串和待测密码传给验证函数,函数自己会解析出所有需要的信息。
PBKDF2 属于裸摘要格式。它的输出就是一段派生出来的字节,通常用十六进制或 Base64 表示。这段字节里不包含任何参数信息——不知道用了哪个 HMAC 算法、迭代了多少次、盐是什么。
所以使用 PBKDF2 时,参数必须由调用方自己管理。忘记存参数,等于数据作废。
常见的自建存储格式
正因为裸摘要不方便,很多框架在 PBKDF2 之上自己定义了一个存储格式,把参数和哈希拼在一起。
| 格式 | 示例 |
|---|---|
| PHC 规范 | $pbkdf2-sha256$i=600000,l=32$盐$哈希 |
| Python passlib | $pbkdf2-sha256$29000$盐$哈希 |
| Django | pbkdf2_sha256$600000$盐$哈希 |
这些格式的共同思路是一致的:用分隔符把算法、参数、盐和摘要串起来,形成一条自描述的记录。校验时先按分隔符拆开,解析出参数,再重新计算并比对。
可以看到 Django 和 passlib 的格式非常接近,只是字段顺序或省略程度不同。这说明这类格式的设计空间不大,思路是趋同的。
用 PHC 格式的好处
PHC(Password Hashing Competition)提出的字符串格式是目前较通用的约定,形如:
$pbkdf2-sha256$i=600000,l=32$<盐的Base64>$<哈希的Base64>
它有几个明显的优势。
第一是自描述。所有参数都在串里,校验时不需要额外查询配置表。
第二是便于参数升级。不同用户可能有不同的迭代次数,串里各自记录,可以共存于同一张表。升级时新用户用新参数,老用户在登录时逐步更新,互不影响。
第三是可迁移。格式是公开约定的,换语言、换框架时容易对接,不像自定义格式那样需要写转换脚本。
第四是支持自动解析。只要校验函数能识别 PHC 串,就能直接读取参数,减少手工传参出错的可能。
用普通摘要格式要注意什么
如果出于历史原因只能用裸十六进制或 Base64,那么参数必须单独存储。至少需要存四项:HMAC 算法、迭代次数、输出长度、盐值。
常见的做法是在用户表里加几个字段,或者把所有参数拼成一个 JSON 存在一个字段里。前者的优点是查询方便,后者在参数变更时更灵活。
无论用哪种方式,有一条原则必须遵守:参数和哈希值要一一对应。如果系统里同时存在多组参数(比如升级期间新旧并存),那么每一条记录都必须能找到它自己的那组参数。任何「全局配置里写死一组参数」的做法,都会在升级时出问题——因为升级后就无法校验老数据了。
迁移时的一个建议
如果系统准备从裸摘要格式迁移到 PHC 格式,可以在用户登录成功时做转换:用旧参数校验通过后,用 PHC 格式重新生成一条记录并覆盖。这样存量数据会随着用户登录逐步完成格式统一,不需要一次性做数据迁移。
在这个过渡期里,校验函数需要同时支持两种格式:先判断字符串是不是以 $ 开头,是则按 PHC 解析,否则按旧格式从字段里取参数。
检查清单
- PBKDF2 的输出是裸摘要,不含算法、迭代次数、盐等参数信息
- 参数必须由调用方持久化保存,忘记存等于数据作废
- 推荐使用 PHC 格式的自描述串,把所有参数写在一条记录里
- Django、passlib 等框架有各自的自建格式,思路与 PHC 一致
- 参数与哈希值必须一一对应,不要用全局写死的一组参数
- 从裸摘要迁移到 PHC 可以在用户登录时逐步转换,校验函数需兼容两种格式