4、密钥派生函数:HKDF原理与实现、PBKDF2原理与实现、Scrypt与Argon2对比
密钥派生函数,说白了就是「从一段原始密钥材料里,变出你真正需要的密钥」。
我早年做嵌入式安全时,总觉得这步是多余的——直接拿用户密码当AES密钥不行吗?后来被现实狠狠教育了一顿。嗯,今天咱们就把这几个主流方案掰开揉碎讲清楚。
4.1 HKDF:为高熵输入而生
HKDF全称HMAC-based Key Derivation Function。它假设你的输入已经有足够的熵——比如DH协商出来的共享秘密。它不负责拉伸熵,只负责「提取」和「扩展」。
4.1.1 两步走:提取 + 扩展
HKDF分两个阶段,我习惯叫它「洗菜」和「切菜」:
- 提取(Extract):用HMAC把输入源打散,消除统计偏差。
- 扩展(Expand):把打散后的伪随机密钥,拉成你需要的任意长度。
核心公式:
PRK = HMAC-Hash(salt, IKM) // 提取
OKM = HKDF-Expand(PRK, info, L) // 扩展
我在项目中遇到过一个问题:有人把salt设成空字符串。HKDF规范允许这么做,但安全强度会打折扣。我个人建议,永远给一个随机salt,哪怕只有16字节。
4.1.2 代码实现(Python)
import hashlib
import hmac
def hkdf_extract(salt, ikm, hash_mod=hashlib.sha256):
if salt is None:
salt = b'\x00' * hash_mod().digest_size
return hmac.new(salt, ikm, hash_mod).digest()
def hkdf_expand(prk, info, length, hash_mod=hashlib.sha256):
hash_len = hash_mod().digest_size
n = (length + hash_len - 1) // hash_len
okm = b''
t = b''
for i in range(1, n + 1):
t = hmac.new(prk, t + info + bytes([i]), hash_mod).digest()
okm += t
return okm[:length]
避坑指南:我曾经在info参数里塞了敏感信息,以为能增加安全性。其实info只用于上下文分离,不增加熵。别把密码放进去。
4.2 PBKDF2:老当益壮的密码拉伸器
PBKDF2跟HKDF最大的区别是——它自带「慢」属性。通过反复迭代HMAC,让暴力破解的成本指数级上升。
4.2.1 工作原理
PBKDF2的核心就是「HMAC套娃」:
DK = PBKDF2(PRF, Password, Salt, c, dkLen)
其中c是迭代次数。我2018年做项目时,c设到10万次,单次派生耗时约50ms。现在?我建议至少60万次起步。
4.2.2 实现示例
import hashlib
import hmac
import struct
def pbkdf2_hmac_sha256(password, salt, iterations, dklen):
hash_mod = hashlib.sha256
hash_len = hash_mod().digest_size
blocks = (dklen + hash_len - 1) // hash_len
dk = b''
for block in range(1, blocks + 1):
u = salt + struct.pack('>I', block)
t = b''
for i in range(iterations):
u = hmac.new(password, u, hash_mod).digest()
t = bytes(a ^ b for a, b in zip(t, u)) if t else u
dk += t
return dk[:dklen]
注意:PBKDF2对GPU和ASIC的抵抗能力较弱。如果你面对的是专业矿机级别的攻击者,PBKDF2可能不够用。
4.3 Scrypt vs Argon2:内存硬派的对决
这两个家伙的设计哲学很一致——让攻击者不光要算得快,还要占大量内存。GPU的显存是有限的,内存硬函数正好掐住这个七寸。
4.3.1 Scrypt:内存+CPU双消耗
Scrypt引入了一个大数组,反复读写。我当年在树莓派上跑Scrypt,参数设高一点,设备直接卡死。这就是它的设计目标——让并行攻击的成本高到离谱。
# 伪代码示意
def scrypt(password, salt, N, r, p, dklen):
# N: CPU/内存成本参数(必须是2的幂)
# r: 块大小参数
# p: 并行参数
B = pbkdf2_hmac_sha256(password, salt, 1, p * 128 * r)
for i in range(p):
B[i] = scrypt_romix(B[i], N, r)
return pbkdf2_hmac_sha256(password, B, 1, dklen)
4.3.2 Argon2:密码学竞赛冠军
Argon2是2015年Password Hashing Competition的胜出者。它有三个变种:
| 变种 | 特点 | 适用场景 |
|---|---|---|
| Argon2d | 数据依赖访问,抗GPU | 加密货币、区块链 |
| Argon2i | 数据独立访问,抗侧信道 | 密码哈希(服务端) |
| Argon2id | 混合模式,兼顾两者 | 我推荐默认选这个 |
参数建议(2024年):
- 内存:64MB以上
- 迭代:3次
- 并行度:4
- 输出长度:32字节
4.3.3 横向对比
| 维度 | HKDF | PBKDF2 | Scrypt | Argon2 |
|---|---|---|---|---|
| 输入熵要求 | 高 | 低 | 低 | 低 |
| 抗GPU | 弱 | 弱 | 中 | 强 |
| 抗ASIC | 弱 | 弱 | 中 | 强 |
| 内存消耗 | 极低 | 极低 | 高 | 高 |
| 标准化状态 | RFC 5869 | RFC 2898 | RFC 7914 | RFC 9106 |
| 推荐场景 | 密钥协商派生 | 遗留系统兼容 | 中等安全需求 | 新项目首选 |
我个人习惯:新项目无脑Argon2id。如果环境不支持,退而求其次用Scrypt。PBKDF2我只在兼容旧系统时用。HKDF则专门处理高熵输入——比如TLS握手后的共享密钥。
选型口诀:高熵用HKDF,密码用Argon2,兼容用PBKDF2,中间态用Scrypt。
4.4 核心逻辑框架图
下面这张图总结了四种派生函数的定位和关系:
4.5 实战建议
- HKDF:用在TLS 1.3、IPsec、WireGuard等协议中。别拿它直接处理用户密码。
- PBKDF2:如果你还在维护2015年以前的系统,用它。迭代次数设到当前硬件能承受的上限。
- Scrypt:N参数设到2^17以上,r=8,p=1。我曾经在云服务器上测过,64MB内存、1秒左右的耗时比较合理。
- Argon2:用libsodium或官方参考实现。别自己造轮子——Argon2的参数调优是个坑,我踩过。
终极警告:永远不要在派生函数里使用固定的salt。我曾经见过一个项目,salt写死在代码里,所有用户派生出的密钥都一样。这跟没加密没区别。