第4章 密钥派生:基于口令的密钥派生函数(PBKDF2/Argon2)

密钥派生函数,说白了就是把你的口令变成一串固定长度的密钥。我做过不少安全审计,发现很多开发者直接把口令当密钥用——嗯,这是个大坑。你想想看,口令通常只有8~12个字符,而AES-256需要32字节随机密钥,直接拿口令去加密,那攻击者随便猜都能撞出来。

正确的做法是:通过一个专门的函数,把低熵的口令转换成高熵的密钥。这个过程就叫密钥派生(Key Derivation)。本章我们就聊聊最常用的两个家族:PBKDF2和Argon2。

4.1 为什么不能直接用口令?

先看一个简单的对比:

属性口令AES密钥
长度通常 6~20 字节固定 16/32 字节
熵低(约 2~4 bit/字符)高(128~256 bit)
格式可打印ASCII二进制随机
抗暴力破解弱(每秒可猜数万次)强(枚举空间天文数字)

所以密钥派生函数要干三件事:

  • 拉伸:把短口令变成长密钥。
  • 加盐:每个用户用不同的随机盐,防止预计算攻击。
  • 慢速:让暴力破解变得极慢,增加攻击成本。
小提示:盐(salt)是随机生成的,通常16~32字节。每次派生都要用不同的盐。我见过有人把盐写死在代码里——那跟没用盐没区别。

4.2 PBKDF2:经典但正在退场

PBKDF2(Password-Based Key Derivation Function 2)是PKCS#5标准的一部分。它的核心思想是重复迭代HMAC。你可能觉得简单,但它在移动设备上性能还行。

参数看下表:

参数说明典型值
PRF伪随机函数,常用HMAC-SHA256HMAC-SHA256
Salt随机盐,防彩虹表16 字节
c迭代次数100000~600000
dkLen派生密钥长度(字节)32(AES-256)

选迭代次数是个学问。我记得2018年有个项目,用户登录响应慢得不行,一查迭代次数设了1000万——那是在树莓派上跑的。后来我建议降到50万,并且用HMAC-SHA256,效果好了不少。

代码实现很简单(Python标准库支持):

import hashlib, binascii

dk = hashlib.pbkdf2_hmac(
    'sha256',
    b'my_password',
    b'salt_1234567890abcdef',
    600000,
    dklen=32
)
print(binascii.hexlify(dk))
警告:PBKDF2对GPU抗性差。攻击者可以用显卡并行计算,迭代次数打折扣。我在实验室测过,一块RTX 4090每秒能算几十万次PBKDF2-HMAC-SHA256。所以现在业界普遍认为它不够安全了。

4.3 Argon2:现代密码学的答案

Argon2是2015年密码学竞赛的胜出者,专门设计来抵抗GPU和ASIC。它有三种变体:

  • Argon2d:抗侧信道攻击弱,但速度稍快,适合加密货币。
  • Argon2i:抗侧信道强,适合密钥派生。
  • Argon2id:混合模式,既抗侧信道又抗暴力破解。推荐通用方案。

我用Argon2id最多。它的参数要这样配:

参数作用推荐值
time_cost迭代次数2~3
memory_cost内存消耗(KiB)65536(64 MiB)
parallelism并行线程数1~4
salt_len盐长度(字节)16
tag_len输出密钥长度32

你可能好奇为什么memory_cost要设这么高。举个例子,Argon2在计算时会持续占用64 MiB内存,这意味着攻击者想并行跑,显存/内存很容易被填满。我曾经测试过,同样的计算量,Argon2比PBKDF2慢10倍以上——这正是我们想要的。

Python用起来也很直观:

from argon2 import PasswordHasher

ph = PasswordHasher(
    time_cost=3,
    memory_cost=65536,
    parallelism=4,
    hash_len=32,
    salt_len=16
)

hash_str = ph.hash("my_password")
# 验证
ph.verify(hash_str, "my_password")
核心要点:Argon2的memory_cost才是真正的杀手锏。它让GPU和FPGA的并行优势瞬间消失。如果你只能选一个参数来调,优先调高内存。

4.4 一张图理解全部流程

下面这张SVG图展示了两条路径:PBKDF2和Argon2各自如何从口令到密钥。我故意把两个流程并列,这样你一眼就能看出差异。

密钥派生流程对比 PBKDF2 Argon2 口令 + 随机盐 HMAC-SHA256 迭代 c 次 拉伸至 dkLen 字节 内存填充 + 多轮混合 输出指定长度密钥 PBKDF2 派生密钥 Argon2 派生密钥 PBKDF2 缺点 - 只有CPU密集,无内存密集 - GPU/ASIC加速严重 Argon2 优点 - 内存密集 + CPU密集 - 抗GPU/ASIC效果显著

4.5 我踩过的坑和总结

讲两个真实教训。

第一个坑:迭代次数设太低。 我曾经接手过一个旧系统,PBKDF2迭代次数只有1000。攻击者用普通笔记本,每秒能算20万次。我改到60万后,登录时间从0.1秒变成了3秒——但安全提升了600倍,值。

第二个坑:盐重用。 有个项目为了省事,所有用户用一个全局盐。结果一个用户字典泄露,所有用户的派生密钥都能预计算。嗯,这相当于没加盐。以后记住:每个用户的盐必须唯一且随机。

避坑指南:千万别自己造轮子去实现PBKDF2或Argon2。标准库已经封装好了,直接用。我曾经见过有人用HMAC+循环自己写了一个派生函数,结果发现迭代次数溢出导致死循环——用户登录卡死了。

最后说一句:如果你在2025年还在用PBKDF2做新项目,建议赶紧升级到Argon2id。如果历史遗留系统没法换,把迭代次数调到60万以上,至少能挡一挡。