bigywater.tech
Java · 261

PBKDF2:密码存储为什么要慢一点

口令不能直接存,要经过「慢」的 KDF。PBKDF2 的盐、迭代次数、输出长度各有什么讲究?验证流程怎么写?这篇用 JDK 真实运行并量出耗时,还用 RFC 标准向量验证实现。

75.7 ms
PBKDF2WithHmacSHA256 60 万次迭代(单线程,10 核 macOS)
2.0 倍
要 512 位输出比 256 位输出多花的时间
RFC 6070
验证通过的标准测试向量

正确性与输入

实测 · 本机执行输出
  RFC 6070 向量 1(HMAC-SHA1,password / salt / 1 次迭代 / 20 字节)输出 0c60c80f961f0e71f3a9b524af6012062fe037a6,与标准值 0c60c80f961f0e71f3a9b524af6012062fe037a6 一致 = true
  RFC 6070 向量 3(4096 次迭代)输出 4b007901b765489abead49d926f721d065a429c1,与标准值 4b007901b765489abead49d926f721d065a429c1 一致 = true
  同样的口令、同样的盐、同样的迭代次数 → 同样的输出 = true(确定性)
  盐改 1 个 bit → 输出相同 = false
  迭代次数 10000 与 10001 → 输出相同 = false
  两个用户的口令都是 123456:各用各的盐 → 存储的哈希相同 = false;用同一个盐 → 存储的哈希相同 = true(同盐会暴露「这两个用户口令相同」)
  空口令 PBEKeySpec 构造成功
  空口令派生成功(JDK 没有拒绝空口令)
  迭代次数 0 → IllegalArgumentException: invalid iterationCount value
  盐长度 0 → IllegalArgumentException: the salt parameter must not be empty
  口令 "café" 的两种 Unicode 写法(预组合 é 与 e+组合重音符):字符串相等 = false;PBKDF2 输出相同 = false;先做 NFC 规范化后输出相同 = true
  clearPassword() 之后再用这个 PBEKeySpec 派生 → InvalidKeySpecException: Could not generate secret key

迭代次数与耗时

实测 · 本机执行输出
  PBKDF2WithHmacSHA1      10000 次:9.6 ms(104 万次/秒)  100000 次:28.6 ms(349 万次/秒)  600000 次:173.0 ms(347 万次/秒)  1000000 次:283.7 ms(353 万次/秒)
  PBKDF2WithHmacSHA256    10000 次:1.3 ms(774 万次/秒)  100000 次:12.8 ms(781 万次/秒)  600000 次:75.7 ms(793 万次/秒)  1000000 次:126.3 ms(791 万次/秒)
  PBKDF2WithHmacSHA512    10000 次:3.6 ms(276 万次/秒)  100000 次:34.8 ms(288 万次/秒)  600000 次:206.3 ms(291 万次/秒)  1000000 次:344.0 ms(291 万次/秒)
  HMAC-SHA256 的输出块是 256 位:要 256 位输出 79 ms,要 512 位输出 158 ms(2.0 倍)——输出超过一个哈希块时,防守方的工作量翻倍,而攻击者只要算第一个块就能验证猜测
  10 个线程同时各做一次 600000 次迭代:总耗时 121 ms(单个 79 ms);并行吞吐 = 单线程的 6.5 倍——服务器同时处理大量登录时,每次验证都占一个核

登录验证流程

实测 · 本机执行输出
  存储记录格式:pbkdf2-sha256$600000$ieVFEoTOHRMjqrjautf…(算法 $ 迭代次数 $ 盐 $ 哈希,迭代次数写进记录,升级时旧记录仍可验证)
  记录总长 90 字符
  口令「correct horse」→ true,耗时 92 ms(验证耗时与口令是否正确无关,都要算完整的迭代)
  口令「correct horsE」→ false,耗时 80 ms(验证耗时与口令是否正确无关,都要算完整的迭代)
  口令「」→ false,耗时 81 ms(验证耗时与口令是否正确无关,都要算完整的迭代)
  拿旧的迭代次数(100000)验证 600000 次存下的记录 → false(迭代次数必须与记录一致,不一致就算不出同一个哈希)
  按「一次验证花多久」选迭代次数: 50000:6 ms 100000:12 ms 200000:25 ms 400000:51 ms 600000:74 ms 1000000:123 ms(目标例如 300 ms,应该在生产服务器上标定,不是在开发机上)

分步演示

PBKDF2
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,macOS 10 核;耗时是单机测量
    • 没有验证:bcrypt、scrypt、Argon2(JDK 没有内置,需要第三方库)、GPU 与 ASIC 攻击者的实际破解速度、各机构对迭代次数的最新推荐值(这里只写了测到的耗时)、pepper(服务端密钥)的做法、PBKDF2 在 FIPS 模式下的限制
    京ICP备2026067502号-1京公网安备11010502063487号