把口令存成 sha256(password) 看起来已经「加密」了,其实攻击者一晚上能试的猜测数多到你想象不到。这篇让脚本真的去枚举一遍,再换成故意做得慢的 pbkdf2_hmac 和 scrypt,看每次猜测到底贵了多少。
实验脚本(节选) stored = hashlib.sha256(b"482915").hexdigest() # 泄露出来的值 for i in range(1_000_000): # 6 位数字口令的全部空间 g = f"{i:06d}" if hashlib.sha256(g.encode()).hexdigest() == stored: break
库里存的: 48290cf691c41cbc99b2396d2e5313ccfba91987b384e6d8f08b951fa5045e83 纯 Python 循环暴力枚举 6 位数字: 试了 482916 次, 找到 '482915', 用时 250 ms, 速度 1,931,646 次/秒 (单核, 没有任何优化) 字典 (10 个常见口令) 命中 'letmein', 用时 9 µs
这是一个没有任何优化的 Python 循环,速度已经约 1,931,646 次/秒;用 C、多核或显卡,速度只会高几个数量级(这条我没有实测,只是指出瓶颈在猜测速度而不是算法)。口令如果落在常见口令字典里就更快:10 个常见口令的字典,命中 letmein 只用了 9 微秒。
ann: sha256 = f52fbd32b2b3b86ff88ef6c4… bob: sha256 = f52fbd32b2b3b86ff88ef6c4… cat: sha256 = d9298a10d1b0735837dc4bd8… 加盐 (每人一个随机 16 字节): ann: salt=61e434a9… sha256(salt+pw) = 6f26d3bbe49f00fd6649b1de… bob: salt=f382e363… sha256(salt+pw) = 046069d603326682b1574a26…
ann 和 bob 用了同一个口令,库里的 sha256 一模一样——攻击者不用算,一眼就知道他俩口令相同,还能用一张预先算好的「哈希 → 口令」表(彩虹表)一次查完全库。每人一个随机 16 字节盐之后,同一个口令得到完全不同的值,攻击者只能逐个用户单独猜。盐不需要保密,和哈希一起存就行。
pbkdf2_hmac 的迭代次数、scrypt 的内存参数 sha256(一次) 0.29 µs / 次 → 3,439,588 次/秒
pbkdf2_hmac 迭代 1000 0.18 ms / 次 → 5,529.6 次/秒
pbkdf2_hmac 迭代 100000 10.66 ms / 次 → 93.8 次/秒
pbkdf2_hmac 迭代 600000 64.44 ms / 次 → 15.5 次/秒
scrypt n=2^14, r=8, p=1 32.10 ms / 次 → 31.2 次/秒 (需要内存 ≈ 128*n*r = 16 MiB)
scrypt n=2^15, r=8, p=1 64.80 ms / 次 → 15.4 次/秒 (需要内存 ≈ 128*n*r = 32 MiB)
按 6 位数字空间 (100 万种) 算, 枚举全部要:
sha256 0.3 秒
pbkdf2 1000 180.8 秒
pbkdf2 100000 10,657.6 秒
pbkdf2 600000 64,440.2 秒
scrypt 16384 32,099.9 秒单次 sha256 只要 0.29 µs;pbkdf2_hmac 迭代 60 万次,一次要 64.4 ms,慢了约 221,648 倍。对正常用户,一次登录多花 64 ms 完全无感;对要枚举 100 万种可能的攻击者,单核就是 17.9 小时。scrypt 除了耗时还要占内存(表里按公式 128·n·r 估算,并非实测),让攻击者不好靠大量并行硬件压价。迭代次数应当随机器变快而调高,而不是写死。
存进库的字符串: pbkdf2_sha256$600000$17479bc64d8471a85291a6f4e8e95467$97e315… 长度 118
verify('hunter2') = True | verify('hunter3') = False
同一口令两次 hash_pw 得到相同记录? False把算法、迭代次数、盐、哈希用 $ 拼起来存一个字段,以后调高迭代次数时旧记录仍能按它自己的参数验证。同一口令两次生成的记录不同(盐不同),所以不能拿字符串比较去查「谁用了这个口令」;校验用 hmac.compare_digest(见上一篇)。
hashlib(sha256、pbkdf2_hmac、scrypt)、hmac.compare_digest