在本章中,我们将学习如何从密码(passwords)或短语(passphrases)中导出对称加密密钥(symmetric encryption keys)。正如我们在【第2章“对称加密和解密”】中所了解到的,对称加密算法不使用密码加密,而是使用加密密钥加密。因此,为了使用密码进行加密,必须从密码中导出加密密钥,然后用于加密。
本章概述了OpenSSL支持的密钥派生函数。在本章的实践部分,我们将学习如何使用命令行和C代码从密码中导出加密密钥。
第五章 从密码中推导加密密钥技术要求了解密码和加密密钥之间的区别什么是密钥推导函数?OpenSSL支持的密钥派生函数概述在命令行上从密码中获取密钥以编程方式从密码中获取密钥实现kdf程序运行kdf程序总结
本章将包含可以在命令行上运行的命令以及可以构建和运行的C源代码。对于命令行命令,您将需要带有OpenSSL动态库的 openssl
命令行工具。要构建C代码,您需要OpenSSL动态或静态库、库头、C编译器和链接器。
我们将在本章中实施一个示例程序,以练习我们正在学习的内容。该程序的完整源代码可以在这里找到:
https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/tree/main/Chapter05
对称加密密钥和密码有什么区别?对称加密密钥是加密算法直接使用的秘密比特数组。通常,加密算法需要特定长度的加密密钥,例如256位。一些不太流行的密码允许可变长度的加密密钥,但这是一个例外,而不是一个规则。加密密钥对人类不太友好:它看起来像随机数据(通常是随机数据),读写时间很长,而且不可能记住,除非你是超人。
相反,密码或短语通常更人性化。许多密码和短语都是人类可读的。尤其是在电影中,密码总是简短、简单、易读的。难怪人们更喜欢8个字符的密码或4个单词的密码而不是256位密钥,流行的加密软件提供了用密码而不是原始加密密钥加密数据的可能性。
虽然密码比加密密钥更人性化,但它不能直接由加密算法使用。我们必须首先从密码中导出密钥,然后使用导出的密钥进行加密。但我们该怎么做呢?借助密钥推导函数!
密钥推导函数(Key Derivation Function,KDF)是一种从其他秘密材料(如密码、密码短语、另一个共享密钥或非对称私钥和公钥的组合)中推导出所需比特长度的密钥的函数。另一种秘密材料也称为输入密钥材料(Input Key Material,IKM),而产生的秘密密钥也称为输出密钥材料(Output Key Material,OKM)。IKM和OKM的长度通常不同。KDF通常在底层使用加密哈希函数或块密码操作。
基于密码的密钥推导函数(Password-Based Key Derivation Function,PBKDF)是一种KDF,旨在从低熵IKM(如密码,passwords)中生成密钥。这些密钥可以用作对称加密密钥。PBKDF的另一个流行应用是密码散列。PBKDF提供了比单独的加密哈希函数更抗暴力破解的密码哈希。
一些密钥推导函数用于安全网络协议中的密钥交换。这些KDF不够安全,无法从低熵IKM中导出密钥。因此,在本章中,我们不会回顾此类知识发现框架。我们将重点介绍从密码中派生密钥。
典型的KDF采用以下参数:
并非每个KDF都支持所有参数。每个KDF都支持IKM和OKM长度。每个好的PBKDF都应该支持抗盐和蛮力参数。一些KDF允许您参数化PRF并添加信息。
在所有提到的参数中,只有IKM参数(密码)被认为是秘密的。其他参数不是秘密的,它们要么以明文形式存储,要么由使用KDF的解决方案(如身份验证模块或加密软件)隐含。
安全的PBKDF具有以下属性:
让我们多谈谈蛮力抵抗。重要的是要理解,典型的密码比随机生成的密钥具有少得多的熵。在良好的随机比特生成器的情况下,随机生成的密钥具有与其长度一样多的熵比特,通常为128或256比特。
密码不同。目前,大多数网站要求密码长度至少为8个字符。一些网站已经开始要求至少12个字符。许多网站要求大小写拉丁字母、数字和符号的混合。这给了我们大约80个可能的字符。让我们假设密码中包含的所有字符的概率是均匀分布的。然后,可以使用以下公式计算密码的熵:
Entropy = log2(ncharplen)
此处:
根据该公式,8个字符密码的熵约为51位。如果我们将密码长度增加到12个字符,熵将大约为76位。
重要的是要明白,PBKDF不能给你比它的输入更多的熵。如果PBKDF从包含76位熵的密码中输出256位密钥,并且盐是公开的,则输出密钥仍将只有76位熵。
正如我们所看到的,一个典型的密码没有太多的熵。因此,PBKDF不能仅依靠密码熵来有效抵抗暴力攻击。基于密码的KDF也需要大量的计算资源。许多PBKDF通过运行底层PRF的大量迭代来解决这个问题。同时,PBKDF不应太慢,因为密钥推导通常在与人交互时使用,例如在验证输入的密码时,因此PBKDF最好仍能在几分之一秒内完成。
正如我们所看到的,安全级别或安全强度不一定等于熵比特的数量。您可以通过多次迭代底层PRF来提高PBKDF的安全强度。但应该注意的是,安全强度不是线性增长的,而是对数增长的,具体取决于迭代次数。例如,要将安全强度增加8位,您只需要256(28)次迭代,但要将安全性强度增加16位,您需要65536(216)次迭代。
然而,最近,在快速图形处理单元(Graphical Processing Units,GPU)、专用集成电路(Application-Specific Integration Circuits,ASICs)或现场可编程门阵列(Field-Programmable Gate Arrays,FPGAs)上使用重并行化破解密码变得很流行,这些单元恰好具有强大的处理能力,可以进行简单的计算操作。因此,对于一个好的PBKDF来说,计算密集型是不够的。安全的PBKDF还应该使用大量的内存,这意味着密码破解的并行化将受到可用内存的限制,并且无法利用完整的GPU/ASIC/FPGA处理能力。
OpenSSL支持哪些PBKDF?我们将在下一节中了解。
OpenSSL 3.0支持多种密钥派生函数,但其中只有两种适用于从密码派生密钥,即 scrypt 和 PBKDF2。
PBKDF2是一种流行的PBKDF,由PKCS #5标准描述和推荐。它使用HMAC函数,如HMAC-SHA-256,作为底层PRF。PBKDF2支持可调的迭代次数,可以使其计算密集型,但不是内存密集型。2021年,开放Web应用程序安全项目(Open Web Application Security Project,OWASP)建议使用HMAC-SHA-256 PRF对PBKDF2进行310000次迭代。
Scrypt是OpenSSL 3.0中PBKDF的最佳选择。Scrypt是一种PBKDF,不仅计算密集,而且内存密集。Scrypt在引擎盖下使用PBKDF2和HMAC-SHA-256。Scrypt使您能够调整计算量、内存使用和并行性。2021年,OWASP为Scrypt推荐了以下抗暴力参数:N=65536,r=8,p=1。虽然Scrypt可用于生成对称加密密钥,但它也是现代GNU/Linux系统上的用户密码哈希方法之一。Scrypt的另一个应用是在一些加密货币中作为工作量证明算法,如莱特币(Litecoin)和狗狗币(Dogecoin)。
单步KDF和HKDF不适合作为PBKDF,因为它们不是计算密集型的。它们也不采用抗暴力参数。
ANSI X9.42、ANSI X9.63和TLS1 PRF不是计算密集型的,需要TLS特定的参数。
SSH KDF也不是计算密集型的,需要SSH特定的参数。
Scrypt是我们作为PBKDF的最佳选择。让我们在命令行上试试吧!
可以使用 openssl kdf
子命令在命令行上从密码生成加密密钥。这是OpenSSL 3.0中添加的一个子命令。您可以在 openssl-kdf
手册页上阅读其文档:
xxxxxxxxxx
$ man openssl-kdf
在导出密钥之前,让我们生成128位盐:
xxxxxxxxxx
$ openssl rand -hex 16
cf0e0acf943629ecffea41c87bab94d4
现在我们可以推导出适用于对称加密的256位密钥。让我们使用Scrypt KDF。OWASP建议使用暴力破解设置和密码 SuperPa$$w0rd
:
xxxxxxxxxx
$ openssl kdf \
-keylen 32 \
-kdfopt 'pass:SuperPa$$w0rd' \
-kdfopt hexsalt:cf0e0acf943629ecffea41c87bab94d4 \
-kdfopt n:65536 -kdfopt r:8 -kdfopt p:1 \
SCRYPT
D0:3D:31:A1:A2:2A:F6:68:99:B3:02:22:60:3B:D7:21:5B:15:5B:80:2B:85:33:36:E6:3B:AB:F9:EE:8F:FE:C7
请注意,包含密码的命令行参数用单引号括起来。这是为了避免命令解释器对 $
字符的特殊解释。在密码中传递特殊字符的另一种方法是使用 -kdfopt hexpass:
选项而不是 -kdfopt pass:
传递十六进制编码的密码。
我们已经学习了如何在命令行上从密码生成加密密钥。现在,让我们学习如何用C代码以编程方式完成它。
我们将实现kdf程序,该程序将从密码中导出密钥。
我们的密钥推导程序将接受两个命令行参数:
我们不会采用N、r和抗暴力破解的Scrypt(brute-force-resistant Scrypt)参数,因为我们想简化我们的示例程序及其使用。相反,我们将使用OWASP推荐的设置。
OpenSSL 3.0提供了以下用于密钥推导的API:
有关 EVP_KDF API及其与 Scrypt 的使用的文档可以在以下手册页中找到:
xxxxxxxxxx
$ man EVP_KDF
$ man EVP_KDF-SCRYPT
像往常一样,让我们制定一个高级实施计划:
现在,让我们在代码中实现这些想法。
按照以下步骤按照我们的计划实施 kdf
计划:
首先,我们必须从OpenSSL的默认算法提供程序中获取 Scrypt 密钥推导算法描述:
xxxxxxxxxx
EVP_KDF* kdf = EVP_KDF_fetch(
NULL, OSSL_KDF_NAME_SCRYPT, NULL);
接下来,我们必须创建密钥推导参数:
xxxxxxxxxx
uint64_t scrypt_n = 65536;
uint32_t scrypt_r = 8;
uint32_t scrypt_p = 1;
OSSL_PARAM params[] = {
OSSL_PARAM_construct_octet_string(
OSSL_KDF_PARAM_PASSWORD,
(char*)password, strlen(password)),
OSSL_PARAM_construct_octet_string(
OSSL_KDF_PARAM_SALT, (char*)salt, salt_length),
OSSL_PARAM_construct_uint64(
OSSL_KDF_PARAM_SCRYPT_N, &scrypt_n),
OSSL_PARAM_construct_uint32(
OSSL_KDF_PARAM_SCRYPT_R, &scrypt_r),
OSSL_PARAM_construct_uint32(
OSSL_KDF_PARAM_SCRYPT_P, &scrypt_p),
OSSL_PARAM_construct_end()
};
不幸的是,OSSL_MAM_struct_*()
函数只接受非常量指针,因此我们必须强制转换 password
和 salt
指针,并将 scrypt_n
、scrypt_r
和 scrypt_p
变量创建为非常量。
接下来,我们必须创建密钥派生上下文:
xxxxxxxxxx
EVP_KDF_CTX* ctx = EVP_KDF_CTX_new(kdf);
现在所有的部分都已就绪,是时候推导出密钥了:
xxxxxxxxxx
EVP_KDF_derive(ctx, key, KEY_LENGTH, params);
现在我们不再需要 EVP_KDF
和 EVP_KDF_CTX
对象,让我们释放它们并避免内存泄漏:
xxxxxxxxxx
EVP_KDF_CTX_free(ctx);
EVP_KDF_free(kdf);
钥匙已经生产出来了。让我们以与 openssl kdf
子命令相同的格式打印它:
xxxxxxxxxx
for (size_t i = 0; i < KEY_LENGTH; ++i) {
if (i != 0)
printf(":");
printf("%02X", key[i]);
}
printf("\n");
我们的 kdf 程序的完整源代码可以在GitHub上找到 kdf.c 文件:
让我们运行我们的密钥推导程序,看看它是否有效:
xxxxxxxxxx
$ ./kdf 'SuperPa$$w0rd' cf0e0acf943629ecffea41c87bab94d4
D0:3D:31:A1:A2:2A:F6:68:99:B3:02:22:60:3B:D7:21:5B:15:5B:80:2B:85:33:36:E6:3B:AB:F9:EE:8F:FE:C7
正如我们所看到的,我们的小型 kdf 程序导出的加密密钥与上一节中 openssl kdf
子命令生成的密钥相同,这表明我们的程序工作正常。
在本章中,我们学习了从密码中导出加密密钥的概念。然后我们了解了什么是密钥推导函数,以及良好的PBKDF的要求是什么。在理论部分,我们回顾了OpenSSL支持的密钥推导函数,并推荐了使用哪种KDF进行基于密码的密钥推导。
在实践部分,我们学习了如何从命令行上的密码中导出对称加密密钥。然后我们还学习了如何在C代码中以编程方式导出相同的键。我们比较了这两种方法得到的密钥,并满意地确认了这两个方法产生了相同的密钥。
在下一章中,我们将开始学习非对称密码学。