4、密钥派生基础:KDF概念、PBKDF2算法原理、bcrypt/scrypt/Argon2对比
密钥派生,说白了就是「从密码生成密钥」的技术。你想想看,用户输入的密码通常只有8到16个字符,而AES-256需要32字节的密钥,RSA可能需要上千字节。直接拿密码当密钥用?那是给自己挖坑。
我刚开始做安全开发时,就见过有人把用户密码直接塞进AES。结果呢?密码长度不够,补零;密码有规律,密钥也有规律。攻击者拿到密文,跑个字典就把密码猜出来了。嗯,从那以后,我再也不敢跳过KDF这一步了。
4.1 什么是KDF?为什么需要它?
KDF(Key Derivation Function,密钥派生函数)的核心任务就两个:
- 拉伸:把任意长度的输入,变成固定长度的输出
- 强化:让暴力破解的成本变得极高
为什么需要强化?因为人类密码的熵太低了。一个8位大小写字母加数字的密码,理论上有62^8≈218万亿种组合。听起来很多?但GPU每秒能算几十亿次哈希,跑完所有组合也就几小时。KDF的作用就是让每次尝试都变慢——慢到攻击者等不起。
核心原则:KDF不是加密,也不是哈希。它是专门为「从低熵输入生成高熵密钥」而设计的函数。普通哈希函数(如SHA-256)太快了,不适合直接用于密码存储或密钥派生。
4.2 PBKDF2:最经典的KDF
PBKDF2(Password-Based Key Derivation Function 2)是PKCS#5标准的一部分,也是目前应用最广泛的KDF。它的核心思想很简单:把哈希函数反复跑很多遍。
4.2.1 算法原理
PBKDF2的公式长这样:
DK = PBKDF2(PRF, Password, Salt, c, dkLen)
参数含义:
- PRF:伪随机函数,通常是HMAC-SHA256
- Password:用户输入的密码
- Salt:随机盐值,至少16字节
- c:迭代次数,越大越安全
- dkLen:派生密钥的长度
具体流程:
- 将密码和盐值拼接,做第一轮HMAC
- 把上一轮的结果作为输入,再做HMAC
- 重复c次,把中间结果异或累加
- 如果需要的密钥长度超过单次输出,就增加块索引重新计算
我的经验:迭代次数不是越大越好。我在项目中遇到过把迭代次数设到1000万次的案例,结果用户登录要等5秒。一般建议:2024年的环境下,SHA-256版本的PBKDF2迭代次数不低于60万次,SHA-512版本不低于20万次。具体值要根据你的服务器性能来调。
4.2.2 代码示例
import hashlib
import os
def pbkdf2_derive(password: bytes, salt: bytes = None,
iterations: int = 600000, dklen: int = 32) -> tuple:
if salt is None:
salt = os.urandom(16)
dk = hashlib.pbkdf2_hmac(
'sha256', password, salt, iterations, dklen
)
return dk, salt
# 使用示例
password = b"my_secure_password"
salt = os.urandom(16)
key, used_salt = pbkdf2_derive(password, salt)
print(f"派生密钥: {key.hex()}")
print(f"盐值: {used_salt.hex()}")
4.3 bcrypt、scrypt、Argon2:新一代KDF
PBKDF2虽然经典,但它有个致命弱点:抗GPU攻击能力差。为什么呢?因为它只依赖CPU计算,而GPU可以并行跑成千上万个HMAC。于是,内存硬化的KDF应运而生。
4.3.1 bcrypt:加盐的Blowfish
bcrypt基于Blowfish加密算法,设计时就把「慢」作为目标。它的特点:
- 内置盐值(22个字符的Base64编码)
- 可调成本因子(cost factor),每增加1,时间翻倍
- 输出固定为60个字符的字符串
我曾经在审计一个旧系统时发现,他们用的bcrypt成本因子只有4。这意味着什么?2010年的机器都能在1毫秒内算完。我建议至少用10以上,2024年推荐12(约250ms)。
4.3.2 scrypt:内存硬化的先驱
scrypt引入了两个关键参数:
- N:CPU/内存成本参数
- r:块大小参数
- p:并行化参数
它的核心是「顺序内存困难函数」——计算过程中需要大量内存,而且访问模式是随机的。GPU想并行?内存带宽先跟上再说。
避坑指南:scrypt的参数设置很讲究。N必须是2的幂,r和p的乘积不能太大。我曾经见过有人把N设成2^20,r设成8,结果服务器内存直接爆了。推荐值:N=2^17(131072),r=8,p=1,大约占用16MB内存,计算时间约500ms。
4.3.3 Argon2:PHC冠军
Argon2是2015年密码哈希竞赛(PHC)的冠军,目前有三个变种:
- Argon2d:抗侧信道攻击,适合加密货币
- Argon2i:抗时间-内存权衡攻击,适合密码哈希
- Argon2id:混合模式,推荐使用
Argon2的参数更灵活:
- m:内存成本(KB)
- t:时间成本(迭代次数)
- p:并行度
4.4 四大KDF对比
| 特性 | PBKDF2 | bcrypt | scrypt | Argon2id |
|---|---|---|---|---|
| 设计年份 | 2000 | 1999 | 2009 | 2015 |
| 核心算法 | HMAC迭代 | Blowfish加密 | PBKDF2 + Salsa20/8 | Blake2b + 内存访问 |
| 抗GPU | 弱 | 中 | 强 | 强 |
| 抗ASIC | 弱 | 弱 | 中 | 强 |
| 内存可调 | 否 | 否 | 是 | 是 |
| 并行度可调 | 否 | 否 | 是 | 是 |
| 推荐场景 | 兼容性优先 | 传统系统 | 内存充足 | 新系统首选 |
| 2024年推荐参数 | c=600000 | cost=12 | N=2^17, r=8, p=1 | m=64MB, t=3, p=4 |
4.5 如何选择?
说实话,没有银弹。我的建议是:
- 新系统:无脑选Argon2id。它是目前最安全的KDF,参数灵活,抗各种攻击。
- 兼容旧系统:用bcrypt。几乎所有语言都有现成库,参数简单不容易配错。
- 需要FIPS认证:PBKDF2是唯一选择。虽然慢,但合规。
- 内存受限环境:scrypt或PBKDF2。Argon2id默认64MB内存,嵌入式设备扛不住。
重要提醒:无论选哪个KDF,盐值必须随机生成,长度至少16字节。盐值可以明文存储,但每个密码必须用不同的盐。我曾经见过一个系统所有用户共用同一个盐——攻击者拿到一个用户的哈希,就能批量破解所有人的密码。
4.6 KDF知识体系图
下面这张图展示了KDF的核心概念、主流算法和选择路径:
4.7 实战建议
最后,分享几个我在项目中踩过的坑:
- 盐值不要复用:每个密码独立生成盐值,哪怕用户改了密码也要重新生成。
- 参数要留余量:硬件每年都在升级,你的KDF参数也要跟着涨。我习惯每年评估一次,把迭代次数或内存成本提高20%。
- 不要自己实现:KDF的细节太多,一个字节的差异就可能导致安全漏洞。用标准库,别手写。
- 存储格式要规范:推荐使用「$算法$参数$盐值$哈希」的格式,方便以后升级算法。
我的习惯:在数据库里存密码哈希时,我会同时存一个「算法版本号」字段。这样哪天想从PBKDF2迁移到Argon2id,只需要在用户登录时检测版本号,自动升级哈希。用户无感知,安全有保障。