密码安全基石:密钥派生函数深度解析

4 阅读8分钟

摘要: 密钥派生函数(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 密钥,会产生两个问题:

  1. 熵不足:密码的比特熵远低于 256 位
  2. 非均匀分布: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 耗时(约)备注
20001,000<1msRFC 2898 初始推荐
201010,000~5msGPU 攻击开始兴起
2016100,000~50msLastPass 泄露事件
2020300,000~150msOWASP 推荐
2026600,000~300msmacOS、主流密码管理器采用

选择原则:在目标硬件上派生时间不超过 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)

算法流程:

  1. 初始化 Blowfish 状态(P 数组和 S-boxes)
  2. 使用 cost 参数决定密钥扩展轮数(2^cost 轮)
  3. 交替执行:加密 OR 文本 + 重新派生密钥
  4. 输出 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)耗时(约)安全性
8256~50ms过低
101,024~200ms基线
124,096~800ms推荐
1416,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)

参数说明:

参数含义典型值
NCPU/内存成本参数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 年):

安全等级Nrp内存CPU 耗时
基础16384 (2^14)8116MB~250ms
中等32768 (2^15)8132MB~500ms
65536 (2^16)8164MB~1s
极限131072 (2^17)84128MB~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')

该变换包含:

  1. 两次 64-bit 加法和异或操作
  2. 两次 64-bit 循环移位
  3. 两次 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-SHA256600k 次~1,200~174 年
bcryptcost=12~100~2,090 年
scryptN=16384, r=8~50~4,180 年
Argon2idt=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 受攻击事件回顾

时间事件教训
2015LastPass PBKDF2 迭代数仅 500 次默认参数过低导致用户密码易破解
2016LinkedIn 泄露(SHA-1 无盐值)无 KDF + 无盐 = 灾难
2019Facebook 明文密码存储KDF 再好不如不存明文
2021某知名论坛使用 md5($pass)经典反面教材
202323andMe 数据泄露即使有 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.3ECDHE 共享密钥会话密钥 + MAC 密钥 + IV
SSH共享密钥 + 会话 ID加密密钥 + 认证密钥 + IV
WireGuardCurve25519 共享密钥各方向加密密钥
加密文件系统主密钥文件加密密钥 + 文件名加密密钥

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 浏览器兼容优先PBKDF260 万次Web Crypto API 原生支持
服务器用户密码存储Argon2idt=3, m=64MB, p=4服务器资源充裕,可并行
密码管理器桌面应用Argon2id / PBKDF2 双支持t=3, m=64MB / 60 万次用户可自行权衡安全和性能
加密文件系统子密钥派生HKDF-ExpandSHA-256输入已为高熵密钥
区块链/加密货币钱包Argon2dt=3, m=64MB数据依赖模式更抗 TMTO
嵌入式/IoT 设备PBKDF212.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 抗性生态支持
PBKDF22000❌ 差❌ 差⭐⭐⭐⭐⭐
bcrypt1999⚠️ 极小⚠️ 中⚠️ 中⭐⭐⭐⭐
scrypt2009✅ 强✅ 强⭐⭐⭐
Argon2id2015✅✅✅✅ 极强✅✅ 极强⭐⭐⭐

关键要点:

  1. PBKDF2 仍是兼容底线:Web Crypto API 原生支持,无需任何依赖,适合浏览器环境
  2. Argon2id 是当前最优解:内存硬 + 计算硬双重防御,PHC 冠军,所有新项目应优先考虑
  3. HKDF 解决不同问题:用于从一个高熵密钥派生多个子密钥,而非从低熵密码派生密钥
  4. 参数选择比算法选择更重要:再好的算法配上错误的参数(如小内存、少迭代)也毫无意义
  5. 密钥文件/第二因素永远是加分项:无论 KDF 多强,都不会增加搜索空间的难度——只是在已有风险之上叠加更多依赖,安全从来不是单点防御

参考资源