KDF 密钥派生函数

在现实生活中,我们更倾向于使用密码来保护自己的数据,而不是二进制的密钥。因为相比于二进制复杂的密钥,字符形式(小写字母,大写字母,数字,特殊符号等的组合)才符合人类正常的思维。

可对计算机来说这相反,现代密码学的很多算法都要求输入是一个大的数字,二进制的密钥就是这样一个大的数字。 因此显然我们需要一个将字符密码(Password)转换成密钥(Key)的函数,这就是密钥派生函数 Key Derivation Function。

直接使用SHA-一类的哈希函数加密password是不可取的,因为password通常都包含着个人的情感元素,这是容易记忆但容易受到猜测的,通常密码不会很长,在10位左右。另外,有的人为了方便,还会选择一些常见的弱密码,比如123456,admin,个人生日等。这就导致如果直接使用SHA-类的算法,许多密码将很容易被暴力破解,字典攻击,彩虹表攻击等手段猜测出来。

KDF 目前主要从如下三个维度提升 hash 碰撞难度:

  1. 时间复杂度:对应 CPU/GPU 计算资源
  2. 空间复杂度:对应 Memory 内存资源
  3. 并行维度:使用无法分解的算法,锁定只允许单线程运算

主要手段是加盐,以及多次迭代。

img

因为相比其他加密哈希算法,KDF 具有一个独特属性——计算速度很慢,而且从设计上就使其计算速度难以提升,所以 KDF 也被称作「慢哈希算法」。

目前比较著名的 KDF 算法主要有如下几个:

  1. PBKDF2:这是一个非常简单的加密 KDF 算法,目前已经不推荐使用。
  2. Bcrypt:安全性在下降,用得越来越少了。不建议使用。
  3. Scrypt:可以灵活地设定使用的内存大小,在 argon2 不可用时,可使用它。
  4. Argon2:目前最强的密码 Hash 算法,在 2015 年赢得了密码 Hash 竞赛。

Scrypt

Scrypt 是一个强大的密钥派生函数,其通过内存密集的计算方式来抵抗 GPU、ASIC、FPGA 这类密码破解硬件的攻击。

Scrypt 接收多个输入参数,进行计算后输出密钥:

1
key = Scrypt(password, salt, N, r, p, derived-key-len)

其中的参数被称为" Scrypt 配置参数",说明如下:

  • N - 迭代次数,将影响 CPU 和内存用量,例:16384 、2048 ;
  • r - 块大小,将影响 CPU 和内存用量,例:8 ;
  • p - 并行因数 (并行运行的线程数,将影响 CPU 和内存用量),通常为 1 ;
  • password - 输入的密码(推荐至少为 8 - 10 个字符);
  • salt - 安全产生的随机字节序列(最小为 64 位,推荐 128 位);
  • derived-key-len - 输出的密钥要有多少字节长,例如 32 (256 位)

Scrypt 计算过程中的每一步都会 按照强相关的顺序 访问内存,这就让内存读写性能成为了算法速度的瓶颈。

具体怎么选择参数,要取决于我们能够等待的时间和所需的安全等级(即抗破解的能力):

  • 用于交互式登录的示例参数:N=16384, r=8, p=1(RAM = 16MB)。交互式的登录一般耗时都要小于 0.5s ,所以必须快速完成计算。同样的,对于服务端而言,如果同时有很多用户登录,那么 Scrypt 的缓慢会拖慢整个系统;
  • 用于文件加密的示例参数:N=1048576, r=8, p=1(RAM = 1GB)。当要加密硬盘时,通常不会频繁解密数据(一天可能只解密 2 ~ 3次),所以你可能会愿意多等 2 ~ 3 秒作为提升安全性的代价。

在 MyEtherWallet 加密钱包应用中,默认的参数是 N=8192, r=8, p=1 。对于此类应用而言,该强度不够高,但可以通过要求用户输入又长又复杂的密码来对抗密码破解攻击。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import os
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt

salt = os.urandom(16)

# derive
kdf = Scrypt(
salt=salt,
length=32,
n=2**14,
r=8,
p=1,
)
key = kdf.derive(b"password123")

# verify
kdf = Scrypt(
salt=salt,
length=32,
n=2**14,
r=8,
p=1,
)
kdf.verify(b"password123", key)

在配置合适的前提下,Scrypt 被认为是高度安全的 KDF 函数,所以可以用在任何需要 KDF 的地方——加密钱包、文件、App 密码等场景都可以

Bcrypt

Bcrypt 也是一个 KDF ,问世时间早于 Scrypt ,对于 ASIC 、GPU 攻击的抗性相对弱一些。其虽然也可以配置迭代数,但由于对内存的压力较小,因此比较容易构建相应的硬件加速密码破解器。

在很多的应用、框架和工具中(比如 WordPress 站点的数据库),Bcrypt 加密后的密码都是和算法设置以及盐保存在一起的,体现为一个单一的字符串(字符串有着特定的格式)。这个字符串包含数个部分,以 $ 符号分割

例如:

1
2
3
$2a$07$wHirdrK4OLB0vk9r3fiseeYjQaCZ0bIeKY9qLsNep/I2nZAXbOb7m
$2a$12$UqBxs0PN/u106Fio1.FnDOhSRJztLz364AwpGemp1jt8OnJYNsr.e
$2a$12$8Ov4lfmZZbv8O5YKrXXCu.mdH9Dq9r72C5GnhVZbGNsIzTr8dSUfm