摘要: 密钥派生函数(KDF)是密码学安全的基石——它将人类可记忆的密码(低熵)转化为高强度的加密密钥,并抵御暴力破解和硬件加速攻击。本文详细剖析 PBKDF2、Argon2id、bcrypt、scrypt 等主流 KDF 的数学原理、安全参数和安全权衡,并介绍 HKDF 和 KDF 在密码管理器中的应用实践。
一、为什么需要密钥派生函数?
1.1 从密码到密钥的鸿沟
人类能记住的密码通常是 8-16 个字符的字符串,包含大小写字母、数字和符号。这样的密码大约有 95^8 ≈ 6.6 × 10^15 种可能——听起来很多,但对于专业级暴力破解工具来说,这个空间远远不够。
| 攻击方式 | 每秒猜测次数 | 遍历 8 位纯字母密码 | 遍历 8 位全字符密码 |
|---|---|---|---|
| CPU 单核 | ~10 万 | 几小时 | 数月 |
| GPU (RTX 4090) | ~100 亿 | <1 秒 | 几分钟 |
| 专用 ASIC (8 芯片) | ~1000 亿 | 毫秒级 | <1 秒 |
更关键的是:加密算法要求输入是一个均匀随机的 256-bit 密钥,而非人类可读的密码字符串。如果直接用密码(如 MyP@ssw0rd!)作为 AES 密钥,会产生两个问题:
- 熵不足:密码的比特熵远低于 256 位
- 非均匀分布:ASCII 字符的二进制模式有显著规律性,可直接用于统计分析
KDF 要做的就是:把低熵、非均匀的密码输入,拉伸为高熵、均匀分布的加密密钥。
1.2 KDF 的核心使命
一个好的 KDF 需要同时满足三个目标:
graph TB
subgraph KDF_三大目标["KDF 三大目标"]
A["密钥拉伸<br/>Key Stretching"] --> D["将短密码派生为<br/>固定长度密钥"]
B["计算缓慢<br/>Computational Cost"] --> E["增加暴力破解<br/>每次尝试的计算成本"]
C["加盐<br/>Salting"] --> F["相同密码 → 不同密钥<br/>防御彩虹表攻击"]
end
D --> G["输出均匀分布<br/>的密钥材料"]
E --> G
F --> G
G --> H["安全加密密钥"]
二、主流 KDF 算法详解
2.1 PBKDF2(Password-Based Key Derivation Function 2)
算法原理
PBKDF2 由 RSA 实验室在 PKCS#5 中定义(RFC 2898),核心思路是对 HMAC 进行反复迭代:
DK = PBKDF2(PRF, Password, Salt, c, dkLen)
其中:
- PRF:伪随机函数,通常为 HMAC-SHA256 或 HMAC-SHA1
- Password:用户输入的密码
- Salt:随机盐值(推荐 ≥ 16 字节)
- c:迭代次数
- dkLen:输出密钥长度
内部工作原理:
sequenceDiagram
participant PWD as Password
participant PRF as HMAC-SHA256
participant XOR as XOR 累加
participant OUT as 输出密钥块
Note over PRF: 第 1 轮迭代
PWD->>PRF: HMAC(Password, Salt OR INT(1))
PRF->>PRF: 迭代 c 次
PRF-->>XOR: U1 = HMAC_c(Salt OR 1)
Note over PRF: 第 2 轮迭代
PWD->>PRF: HMAC(Password, Salt OR INT(2))
PRF->>PRF: 迭代 c 次
PRF-->>XOR: U2 = HMAC_c(Salt OR 2)
Note over PRF: 第 N 轮迭代
PWD->>PRF: HMAC(Password, Salt OR INT(N))
PRF->>PRF: 迭代 c 次
PRF-->>XOR: UN = HMAC_c(Salt OR N)
XOR->>OUT: DK = U1 OR U2 OR ... OR UN
数学表达更清晰:
U₁ = PRF(Password, Salt ‖ INT(i))
U₂ = PRF(Password, U₁)
U₃ = PRF(Password, U₂)
⋮
U_c = PRF(Password, U_{c-1})
T_i = U₁ ⊕ U₂ ⊕ … ⊕ U_c
最终密钥 DK = T₁ ‖ T₂ ‖ … ‖ T_{dkLen/hLen}
安全参数选择
迭代次数 c 的选择至关重要——太少不安全,太多影响用户体验。以下为 2026 年的推荐值:
graph LR
subgraph 迭代次数演进["迭代次数演进"]
A["2000年: 1000次"] --> B["2010年: 10000次"]
B --> C["2016年: 10万次"]
C --> D["2020年: 30万次"]
D --> E["2026年: 60万-100万次"]
end
style E fill:#e8f5e9
| 年份 | 推荐迭代次数 | CPU 耗时(约) | 备注 |
|---|---|---|---|
| 2000 | 1,000 | <1ms | RFC 2898 初始推荐 |
| 2010 | 10,000 | ~5ms | GPU 攻击开始兴起 |
| 2016 | 100,000 | ~50ms | LastPass 泄露事件 |
| 2020 | 300,000 | ~150ms | OWASP 推荐 |
| 2026 | 600,000 | ~300ms | macOS、主流密码管理器采用 |
选择原则:在目标硬件上派生时间不超过 500ms 的前提下,尽量取最大值。
优势与局限
✅ 优势:
- 标准化成熟(RFC 2898,NIST 认可)
- 广泛支持(所有主流语言的标准库)
- 无需额外依赖,Web Crypto API 原生支持
- 输出密钥长度可调
❌ 局限:
- 顺序依赖计算:HMAC 的迭代是严格串行的——第 n 次迭代必须等第 n-1 次完成才能开始。这对防御 ASIC 攻击不利
- 非内存硬函数:PBKDF2 计算不需要大内存,GPU 和 ASIC 可以大规模并行,每个核心独立计算
- 单次计算成本可控:攻击者可以定制 ASIC,将 60 万次 HMAC 作为一个流水线单元
代码示例
async function pbkdf2(
password: string,
salt: Uint8Array,
iterations: number = 600000, // 迭代次数,2026年推荐60万次
keyLength: number = 32 // 输出密钥长度,32字节 = 256-bit
): Promise<Uint8Array> {
const keyMaterial = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
'PBKDF2',
false,
['deriveBits']
);
const key = await crypto.subtle.deriveBits(
{
name: 'PBKDF2',
salt,
iterations,
hash: 'SHA-256',
},
keyMaterial,
keyLength * 8
);
return new Uint8Array(key);
}
2.2 bcrypt
算法原理
bcrypt 由 Niels Provos 和 David Mazières 于 1999 年基于 Blowfish 密码算法设计。它的核心创新是引入了自适应成本因子:
bcrypt(password, salt, cost)
算法流程:
- 初始化 Blowfish 状态(P 数组和 S-boxes)
- 使用
cost参数决定密钥扩展轮数(2^cost 轮) - 交替执行:加密 OR 文本 + 重新派生密钥
- 输出 192-bit(24 字节,含盐值和密文)
graph TB
subgraph bcrypt_密钥扩展["bcrypt 密钥扩展"]
A["密码 + 盐值"] --> B["初始化 Blowfish<br/>P-array + S-boxes"]
B --> C["轮 1: 加密 Salt 64-bit"]
C --> D["轮 2: 用加密结果<br/>派生新子密钥"]
D --> E{"2^cost 轮?"}
E -->|"未完成"| C
E -->|"完成"| F["加密标准文本<br/>OrpheanBeholderScryDoubt"]
end
style F fill:#c8e6c9
输出格式
bcrypt 最显著的特征是其自描述输出格式:
$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
││ │ │
││ │ └─ 哈希值 (31 字符, Base64)
││ └──── 盐值 (22 字符, Base64)
│└─────── 成本因子 (10 = 2^10 = 1024 轮)
└──────── 算法版本 (2a/2b/2x/2y)
版本说明:
| 版本 | 说明 | 状态 |
|---|---|---|
$2a$ | 原始 bcrypt | 存在 Unicode null 字节 bug |
$2x$ | 修复旧版 PHP 实现 | 兼容过渡 |
$2y$ | PHP 的完整修复版 | 兼容过渡 |
$2b$ | OpenBSD 修复版 | ✅ 当前标准 |
安全参数
成本因子 cost 每增加 1,计算时间翻倍:
| Cost | 轮数 (2^cost) | 耗时(约) | 安全性 |
|---|---|---|---|
| 8 | 256 | ~50ms | 过低 |
| 10 | 1,024 | ~200ms | 基线 |
| 12 | 4,096 | ~800ms | 推荐 |
| 14 | 16,384 | ~3.2s | 高安全性 |
优势与局限
✅ 优势:
- 自描述结构:成本因子和盐值都编码在输出中,无需额外存储
- 时间经过充分检验(25+ 年)
- 内存使用量固定(约 4KB)
❌ 局限:
- 输出固定 192-bit:不能直接作为 AES-256 密钥,需配合 HKDF
- 内存用量太小:4KB 不足以抵御 GPU/ASIC 攻击
- 算法更新停滞:自 1999 年以来未显著改进
- 不支持密码短语:输入密码最大 72 字节
2.3 scrypt
算法原理
scrypt 由 Colin Percival(FreeBSD 安全官)于 2009 年设计,目标是同时消耗 CPU 和内存资源——让攻击者无法通过定制 ASIC 低成本并行破解。
DK = scrypt(P, S, N, r, p, dkLen)
参数说明:
| 参数 | 含义 | 典型值 |
|---|---|---|
| N | CPU/内存成本参数 | 16384 (2^14) |
| r | 块大小参数 | 8 |
| p | 并行化参数 | 1 |
| dkLen | 输出密钥长度 | 32 |
内存用量:Memory = 128 · N · r bytes
例如 N=16384, r=8 时,内存使用量为 128 × 16384 × 8 = 16MB。
graph TB
subgraph scrypt_三层结构["scrypt 三层结构"]
A["密码 P"] --> B["PBKDF2-HMAC-SHA256<br/>第一轮"]
C["盐值 S"] --> B
B --> D["ROMix 函数<br/>核心内存硬函数"]
subgraph ROMix_内部["ROMix 内部"]
D1["生成 X = B[0] OR B[1] OR ... OR B[N-1]"]
D2["填充大型数组 V<br/>V[i] = X after i 次迭代"]
D3["随机访问读取<br/>X = xor_chain(V, X)"]
D1 --> D2 --> D3
end
D --> E["PBKDF2-HMAC-SHA256<br/>第二轮"]
E --> F["最终密钥 DK"]
end
style D fill:#e3f2fd
内存硬函数(Memory-hard function) 指计算过程中必须使用大量内存且无法用更少内存替代的函数。其核心特点是:
- 空间换时间不可行:不能通过节省内存来换取相近的计算速度。攻击者若减少内存,计算速度会急剧下降
- 抗 ASIC/GPU:定制芯片需要内置大容量片上内存(如 64MB),成本高昂,失去并行优势
scrypt 和 Argon2id 是目前主流的内存硬 KDF。
ROMix 函数是 scrypt 的灵魂——它需要先构建一个占用大量内存的数组 V,然后在计算过程中以伪随机顺序反复访问这个数组。对于攻击者来说,这意味着:
- 不能空间换时间:如果减少内存,随机访问模式会退化,严重影响计算速度
- 不能时间换空间:ASIC 需要内置大容量片上内存,成本急剧上升
- 多路并行困难:每条并行流水线都需要自己的 16MB+ 内存
安全参数
OWASP 推荐的 scrypt 参数(2026 年):
| 安全等级 | N | r | p | 内存 | CPU 耗时 |
|---|---|---|---|---|---|
| 基础 | 16384 (2^14) | 8 | 1 | 16MB | ~250ms |
| 中等 | 32768 (2^15) | 8 | 1 | 32MB | ~500ms |
| 高 | 65536 (2^16) | 8 | 1 | 64MB | ~1s |
| 极限 | 131072 (2^17) | 8 | 4 | 128MB | ~4s |
优势与局限
✅ 优势:
- 内存硬函数,GPU/ASIC 攻击成本显著高于 PBKDF2
- 参数灵活,可独立调节 CPU 和内存成本
- RFC 7914 标准化,被各大项目采用(如 Ethereum)
❌ 局限:
- 参数配置复杂(3 个维度),容易配错
- 验证端必须使用与派生端相同的内存
- 在 Web 浏览器环境中 64MB+ 内存分配可能触发性能警告
- Node.js 原生实现不如 PBKDF2 广泛
2.4 Argon2id
算法原理
Argon2 是2015 年密码哈希竞赛(PHC)的冠军,由 Alex Biryukov、Daniel Dinu 和 Dmitry Khovratovich 设计。它有三个变体:
| 变体 | 特点 | 适用场景 |
|---|---|---|
| Argon2d | 数据依赖访问模式 | 加密货币、无侧信道风险 |
| Argon2i | 数据独立访问模式 | 密码哈希(防御侧信道) |
| Argon2id | 混合模式(先 i 后 d) | ✅ 通用推荐 |
Argon2id 的混合策略:前一半迭代使用数据独立模式(抗侧信道),后一半使用数据依赖模式(抗时间-空间权衡攻击) 。
graph TB
subgraph Argon2id_内存填充["Argon2id 内存填充"]
A["密码 + 盐值 + 参数"] --> B["初始化内存块"]
B --> C["第一轮: 数据独立<br/>索引伪随机但不依赖密码"]
C --> D["第二轮: 数据独立"]
D --> E["... 中间轮次"]
E --> F["过渡到数据依赖"]
F --> G["第 N-1 轮: 数据依赖<br/>索引由密码派生"]
G --> H["第 N 轮: 数据依赖"]
H --> I["最终哈希输出"]
end
style C fill:#e3f2fd
style D fill:#e3f2fd
style G fill:#c8e6c9
style H fill:#c8e6c9
更具体地,Argon2 在内存中构建一个 p × m 的矩阵(p = lanes, m = memory / (p · 4)),每个元素 1KB:
graph LR
subgraph "内存矩阵 (p=4 lanes)"
L1["Lane 0<br/>Block 0..m-1"]
L2["Lane 1<br/>Block 0..m-1"]
L3["Lane 2<br/>Block 0..m-1"]
L4["Lane 3<br/>Block 0..m-1"]
end
subgraph "填充规则"
R1["垂直片: 每片覆盖所有 lanes"]
R2["切片内: 引用前一个块<br/>(同一 lane 或交叉 lane)"]
R3["最终: 压缩所有 lanes → 输出"]
end
L1 --> R1
L2 --> R1
L3 --> R1
L4 --> R1
每个块使用 BLAKE2b 作为底层压缩函数,进行 G 变换:
G(a, b, c, d) → (a', b', c', d')
该变换包含:
- 两次 64-bit 加法和异或操作
- 两次 64-bit 循环移位
- 两次 64-bit 乘法和加法
安全参数
Argon2id 的参数:
| 参数 | 含义 | 推荐范围 |
|---|---|---|
time (t) | 迭代次数 | 2-5 |
mem (m) | 内存使用 (KB) | 47104-262144 |
parallelism (p) | 并行度 | 1-4 |
keyLength | 输出密钥长度 | 依应用而定 |
参数选择指导原则:
graph TB
A{"目标应用"} -->|"Web 前端"| B["t=2, m=32MB, p=1<br/>约 1-2 秒"]
A -->|"桌面应用"| C["t=3, m=64MB, p=1<br/>约 3 秒"]
A -->|"服务器/注册"| D["t=4, m=128MB, p=4<br/>约 6-8 秒"]
A -->|"高安全"| E["t=5, m=256MB, p=4<br/>约 12 秒"]
style B fill:#e3f2fd
style C fill:#c8e6c9
style D fill:#fff3e0
style E fill:#ffcdd2
优势与局限
✅ 优势:
- PHC 冠军算法,当前最先进的 KDF 设计
- 三种变体覆盖不同场景
- 内存硬 + 计算硬双重防御
- 参数灵活,可独立调节时间、内存、并行度
- 内置防御时间-空间权衡攻击(TMTO)
❌ 局限:
- 相对年轻(2015 年至今),生态支持不如 PBKDF2
- 在浏览器环境需要 WASM 支持(如 hash-wasm 库)
- 参数配置空间大,使用者容易选择不安全参数(如 mem=8KB)
- 验证端需要与派生端相同的资源
三、各算法安全对比
graph TB
subgraph ASIC_GPU_攻击难度["ASIC/GPU 攻击难度 (越高越好)"]
A["PBKDF2"] -->|"低: 纯串行 HMAC"| A1["⭐⭐"]
B["bcrypt"] -->|"中低: ~4KB 内存"| B1["⭐⭐⭐"]
C["scrypt"] -->|"中: 16-128MB"| C1["⭐⭐⭐⭐"]
D["Argon2id"] -->|"高: 内存硬 + TMTO 防御"| D1["⭐⭐⭐⭐⭐"]
end
subgraph Web_生态支持["Web 生态支持"]
A2["PBKDF2"] -->|"原生 Web Crypto API"| A3["⭐⭐⭐⭐⭐"]
B2["bcrypt"] -->|"需第三方库"| B3["⭐⭐⭐⭐"]
C2["scrypt"] -->|"Node.js crypto 支持"| C3["⭐⭐⭐"]
D2["Argon2id"] -->|"需 WASM 库"| D3["⭐⭐⭐"]
end
style A1 fill:#ffcdd2
style D1 fill:#c8e6c9
3.1 暴力破解成本对比
以下是在 RTX 4090 上的实测估算:
| 算法 | 参数 | 每秒猜测数 | 遍历 8 字符密码 |
|---|---|---|---|
| PBKDF2-SHA256 | 600k 次 | ~1,200 | ~174 年 |
| bcrypt | cost=12 | ~100 | ~2,090 年 |
| scrypt | N=16384, r=8 | ~50 | ~4,180 年 |
| Argon2id | t=3, m=64MB | ~6 | ~34,800 年 |
3.2 算法演进历程
timeline
title KDF 算法演进
1999 : bcrypt 发布
: 引入自适应成本因子
2000 : PBKDF2 标准化
: RFC 2898 (PKCS#5 v2)
2009 : scrypt 发布
: 引入内存硬函数
2013 : PBKDF2 被 NIST SP 800-132 采纳
2015 : Argon2 赢得 PHC 冠军
: 最先进的 KDF 设计
2017 : Argon2 被 RFC 9106 标准化
2020 : macOS / Linux 原生支持 Argon2
2022 : OWASP 推荐 Argon2id 为首选
2026 : Argon2id 成为主流密码管理器标准
3.3 受攻击事件回顾
| 时间 | 事件 | 教训 |
|---|---|---|
| 2015 | LastPass PBKDF2 迭代数仅 500 次 | 默认参数过低导致用户密码易破解 |
| 2016 | LinkedIn 泄露(SHA-1 无盐值) | 无 KDF + 无盐 = 灾难 |
| 2019 | Facebook 明文密码存储 | KDF 再好不如不存明文 |
| 2021 | 某知名论坛使用 md5($pass) | 经典反面教材 |
| 2023 | 23andMe 数据泄露 | 即使有 KDF,社会工程亦可绕过 |
四、HKDF:与 KDF 不同的另一类
4.1 用途区分
值得注意的是,KDF 还分为两大类,用途完全不同:
graph LR
subgraph 密码基_KDF["密码基 KDF (Password-Based)"]
A["PBKDF2"] --> D["输入: 低熵密码"]
B["bcrypt/scrypt/Argon2"] --> D
D --> E["核心: 慢 + 加盐"]
end
subgraph 密钥派生_KDF["密钥派生 KDF (Extraction-Expansion)"]
F["HKDF"] --> G["输入: 高熵密钥材料"]
G --> H["核心: 快速 + 派生多个子密钥"]
end
4.2 HKDF (RFC 5869)
HKDF 由 Hugo Krawczyk 设计,用于从一个高熵的初始密钥材料派生多个子密钥。它由两步组成:
sequenceDiagram
participant IKM as 初始密钥材料 (IKM)
participant Salt as 盐值
participant Extract as 提取阶段<br/>HMAC-Hash(salt, IKM)
participant PRK as 伪随机密钥 (PRK)
participant Expand as 扩展阶段<br/>HMAC-Hash(PRK, info OR i)
participant OKM as 输出密钥材料 (OKM)
IKM->>Extract: 输入
Salt->>Extract: 输入
Extract->>PRK: 输出固定长度 PRK
PRK->>Expand: 作为 HMAC 密钥
Expand->>OKM: 扩展为任意长度
Note over Extract: 提取: 将非均匀 IKM<br/>转化为均匀 PRK
Note over Expand: 扩展: 从 PRK 派生<br/>多个独立子密钥
HKDF 的典型应用场景:
| 场景 | 输入 | 输出 |
|---|---|---|
| TLS 1.3 | ECDHE 共享密钥 | 会话密钥 + MAC 密钥 + IV |
| SSH | 共享密钥 + 会话 ID | 加密密钥 + 认证密钥 + IV |
| WireGuard | Curve25519 共享密钥 | 各方向加密密钥 |
| 加密文件系统 | 主密钥 | 文件加密密钥 + 文件名加密密钥 |
4.3 PBKDF2 + HKDF 组合
在密码管理器中,常见组合:
graph TB
A["用户密码"] --> B["PBKDF2 或 Argon2id<br/>密码基 KDF"]
B --> C["主密钥<br/>256-bit"]
C --> D["HKDF-Expand<br/>提取阶段"]
D --> E["HKDF-Expand<br/>info='encryption'"]
D --> F["HKDF-Expand<br/>info='authentication'"]
D --> G["HKDF-Expand<br/>info='keyfile-binding'"]
style E fill:#e3f2fd
style F fill:#fff3e0
style G fill:#e8f5e9
五、如何选择 KDF?
graph TB
A{"开发环境"} -->|"浏览器前端"| B1{"目标"}
A -->|"Node.js 后端"| B2{"目标"}
A -->|"原生应用"| B3{"目标"}
B1 -->|"密码哈希"| C1["Argon2id (WASM)<br/>备选 PBKDF2"]
B1 -->|"密钥派生"| C2["HKDF-Expand"]
B2 -->|"密码存储"| D1["Argon2id<br/>t=3, m=64MB, p=4"]
B2 -->|"密钥派生"| D2["HKDF<br/>配合 PBKDF2 或 Argon2id"]
B3 -->|"通用"| E1["Argon2id<br/>t=3, m=64MB, p=1"]
B3 -->|"兼容优先"| E2["PBKDF2<br/>60 万次"]
style C1 fill:#c8e6c9
style D1 fill:#c8e6c9
style E1 fill:#c8e6c9
style E2 fill:#fff3e0
决策汇总表
| 场景 | 推荐算法 | 参数 | 理由 |
|---|---|---|---|
| Web 浏览器中哈希密码 | Argon2id (WASM) | t=2, m=32MB, p=1 | 浏览器内存受限,但需要抗 GPU |
| Web 浏览器兼容优先 | PBKDF2 | 60 万次 | Web Crypto API 原生支持 |
| 服务器用户密码存储 | Argon2id | t=3, m=64MB, p=4 | 服务器资源充裕,可并行 |
| 密码管理器桌面应用 | Argon2id / PBKDF2 双支持 | t=3, m=64MB / 60 万次 | 用户可自行权衡安全和性能 |
| 加密文件系统子密钥派生 | HKDF-Expand | SHA-256 | 输入已为高熵密钥 |
| 区块链/加密货币钱包 | Argon2d | t=3, m=64MB | 数据依赖模式更抗 TMTO |
| 嵌入式/IoT 设备 | PBKDF2 | 12.5 万次 | 资源受限,Argon2 内存太高 |
| 密码管理器密钥文件绑定 | HMAC-SHA256 | — | 第二因素与 KDF 输出绑定 |
六、常见误区与陷阱
误区 1:迭代次数越多越好
不完全正确。 过高的迭代次数导致:
- 用户体验恶劣(等待 10+ 秒)
- 验证端也承受相同负担(服务器端 CPU 成本)
- 某些应用场景(如 SSH 登录)需要服务器端验证,过高的 KDF 成本可能导致 DoS 攻击
正确的做法:在用户体验可接受的前提下(≤1 秒),取最大迭代次数。
误区 2:用过一次 KDF 就够了
graph LR
subgraph 错误_单次_KDF["错误: 单次 KDF"]
A["密码"] -->|"PBKDF2 一次"| B["固定密钥"]
B --> C["加密数据 1"]
B --> D["加密数据 2"]
B --> E["加密数据 3"]
end
subgraph 正确_KDF_分层派生["正确: KDF + 分层派生"]
F["密码"] -->|"PBKDF2"| G["主密钥"]
G -->|"HKDF-Expand info=1"| H["密钥 1"]
G -->|"HKDF-Expand info=2"| I["密钥 2"]
G -->|"HKDF-Expand info=3"| J["密钥 3"]
end
style B fill:#ffcdd2
style G fill:#c8e6c9
同一个派生密钥加密多个数据是危险的——如果某个数据泄露可通过已知明文攻击分析密钥特征。应为不同用途派生不同子密钥。
误区 3:不同的 KDF 混用
不要这样做:
- 先用 Argon2id 派生,再用 PBKDF2 处理结果
- 或用 bcrypt 的输出作为 scrypt 的输入
KDF 的输出本身已经是均匀随机的,再套一层 KDF 属于画蛇添足——不会增加安全性,反而增加验证负担。
误区 4:使用自定义的 KDF
永远不要自己发明 KDF。著名的反面教材:
❌ SHA256(SHA256(password))
❌ SHA1(password) 无盐值
❌ 3DES-ECB 加密密码
❌ md5(md5(password) + salt) 无迭代
Kerckhoffs 原则: 系统的安全性应依赖于密钥的保密性,而非算法的保密性。使用公开、标准化的 KDF,把精力花在参数选择上。
七、总结
graph TB
subgraph KDF_推荐路线["KDF 推荐路线 (2026)"]
A["首选"] --> B["Argon2id<br/>t=3, m=64MB, p=1"]
B --> C["场景不满足?"]
C -->|"生态不支持"| D["PBKDF2<br/>60 万次"]
C -->|"需要子密钥"| E["HKDF-Expand<br/>从主密钥派生"]
C -->|"遗留系统"| F["bcrypt cost=12<br/>或 scrypt N=16384"]
end
style B fill:#c8e6c9
style D fill:#e3f2fd
style E fill:#fff3e0
style F fill:#ffcdd2
| 算法 | 设计年代 | 内存硬 | GPU 抗性 | ASIC 抗性 | 生态支持 |
|---|---|---|---|---|---|
| PBKDF2 | 2000 | ❌ | ❌ 差 | ❌ 差 | ⭐⭐⭐⭐⭐ |
| bcrypt | 1999 | ⚠️ 极小 | ⚠️ 中 | ⚠️ 中 | ⭐⭐⭐⭐ |
| scrypt | 2009 | ✅ | ✅ 强 | ✅ 强 | ⭐⭐⭐ |
| Argon2id | 2015 | ✅✅ | ✅✅ 极强 | ✅✅ 极强 | ⭐⭐⭐ |
关键要点:
- PBKDF2 仍是兼容底线:Web Crypto API 原生支持,无需任何依赖,适合浏览器环境
- Argon2id 是当前最优解:内存硬 + 计算硬双重防御,PHC 冠军,所有新项目应优先考虑
- HKDF 解决不同问题:用于从一个高熵密钥派生多个子密钥,而非从低熵密码派生密钥
- 参数选择比算法选择更重要:再好的算法配上错误的参数(如小内存、少迭代)也毫无意义
- 密钥文件/第二因素永远是加分项:无论 KDF 多强,都不会增加搜索空间的难度——只是在已有风险之上叠加更多依赖,安全从来不是单点防御