口令不能直接存,要经过「慢」的 KDF。PBKDF2 的盐、迭代次数、输出长度各有什么讲究?验证流程怎么写?这篇用 JDK 真实运行并量出耗时,还用 RFC 标准向量验证实现。
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,应该在生产服务器上标定,不是在开发机上)