4、密钥派生函数:HKDF原理与实现、PBKDF2原理与实现、Scrypt与Argon2对比

密钥派生函数,说白了就是「从一段原始密钥材料里,变出你真正需要的密钥」。

我早年做嵌入式安全时,总觉得这步是多余的——直接拿用户密码当AES密钥不行吗?后来被现实狠狠教育了一顿。嗯,今天咱们就把这几个主流方案掰开揉碎讲清楚。

4.1 HKDF:为高熵输入而生

HKDF全称HMAC-based Key Derivation Function。它假设你的输入已经有足够的熵——比如DH协商出来的共享秘密。它不负责拉伸熵,只负责「提取」和「扩展」。

4.1.1 两步走:提取 + 扩展

HKDF分两个阶段,我习惯叫它「洗菜」和「切菜」:

  1. 提取(Extract):用HMAC把输入源打散,消除统计偏差。
  2. 扩展(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 核心逻辑框架图

下面这张图总结了四种派生函数的定位和关系:

密钥派生函数选型框架 原始密钥材料 输入熵是否足够高? 是 → HKDF 否 → 密码派生 环境支持Argon2? 是 → Argon2id 否 → Scrypt / PBKDF2 说明:HKDF用于DH协商、EAP等场景;Argon2id是密码哈希首选 Scrypt在资源受限环境可作为Argon2的替代方案

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写死在代码里,所有用户派生出的密钥都一样。这跟没加密没区别。