1 引言
1.1 背景与意义
在密码存储、密钥派生等场景中,传统哈希算法(如 MD5、SHA-1)因计算成本低、易被 GPU/ASIC 暴力破解的缺陷,逐渐被淘汰。scrypt 算法由 Colin Percival 于 2009 年提出,核心优势是内存密集型设计—— 通过强制占用大量随机访问内存,大幅提高硬件破解的成本,成为替代 PBKDF2、bcrypt 的更安全方案。2016 年,scrypt 正式被 IETF 标准化为 RFC7914,进一步确立其在密码学领域的地位。
1.1.1 scrypt 的来源背景
scrypt 的诞生源于 2000 年后密码破解技术的快速演进:
- 技术痛点驱动:2000-2009 年,GPU 算力爆发式增长(如 NVIDIA GTX 系列),使得基于 “纯计算密集” 的哈希算法(如 MD5、SHA-1)可被每秒数十亿次破解;即使是早期 KDF(如 PBKDF2、bcrypt),因内存占用极低(仅 KB 级),仍可通过 ASIC 芯片并行加速破解。
- 初始应用场景:Colin Percival 在为 FreeBSD 操作系统设计密码存储方案时,发现现有算法无法抵御硬件加速破解,遂提出 “内存密集型 KDF” 概念 —— 通过让算法依赖大量随机内存访问,利用内存带宽瓶颈限制硬件并行效率,从根本上提高破解成本。
- 标准化历程:2009 年 scrypt 首次在《Stronger Key Derivation via Sequential Memory-Hard Functions》论文中发布,2012 年被纳入 Tarsnap 备份服务(Colin Percival 创办的云备份项目)验证实用性,2016 年经 IETF 审核发布 RFC7914,成为国际标准 KDF。
1.2 本文结构
本文首先解析 scrypt 算法原理与 RFC7914 规范,再深入 openHiTLS 的 scrypt 实现架构,通过代码示例拆解核心逻辑,最后探讨应用场景与安全实践,为开发者提供从理论到工程的完整参考。
2 scrypt 算法基础与 RFC7914 规范
2.1 算法定位与核心优势
scrypt 本质是密钥派生函数(KDF),核心目标是将低熵输入(如用户密码)转化为高熵密钥,同时通过 “计算 + 内存” 双重成本抵御暴力破解:
- 抗 ASIC/GPU 破解:内存密集型设计使硬件并行计算的优势失效(内存带宽瓶颈远严于计算瓶颈);
- 参数可配置:通过动态调整参数,平衡不同设备(服务器 / 手机 / 嵌入式)的安全与性能;
- 向后兼容:底层依赖 PBKDF2-HMAC-SHA256,继承成熟哈希算法的安全性。
2.1.1 与主流 KDF 算法的对比分析
scrypt 需与 PBKDF2(RFC2898)、bcrypt(1999 年提出)、Argon2(2015 年密码哈希竞赛冠军)等主流算法对比,才能更清晰体现其技术定位,核心差异如下表:
对比维度 |
scrypt (RFC7914) |
PBKDF2 (RFC2898) |
bcrypt |
Argon2 (RFC9106) |
核心设计 |
内存密集型(随机访问)+ 计算密集 |
纯计算密集(迭代哈希) |
轻量内存(固定 4KB)+ 计算 |
可配置内存 / 计算 / 并行 + 防御侧信道 |
内存占用 |
动态(如 N=16384 时约 4MB) |
极低(仅哈希上下文大小) |
固定≈4KB |
动态(支持 GB 级) |
抗破解能力 |
抗 GPU/ASIC(内存带宽限制) |
弱(易 GPU 并行加速) |
抗 CPU/GPU(但 ASIC 仍可优化) |
最优(全面抵御硬件加速) |
参数灵活性 |
3 个核心参数(N/r/p) |
1 个核心参数(迭代次数) |
2 个核心参数(成本因子 / 盐长) |
4 个核心参数(内存 / 迭代 / 并行 / 类型) |
标准化程度 |
IETF RFC7914(2016) |
IETF RFC2898(2000) |
无官方 RFC(工业标准) |
IETF RFC9106(2021) |
典型应用 |
莱特币、TLS PSK、密码存储 |
证书加密、早期 TLS |
Unix 系统密码、网站存储 |
区块链、政务加密、金融 |
缺陷 |
并行性弱(p 参数优化有限) |
无内存防御机制 |
内存固定,难适配新硬件 |
实现复杂度高,资源占用高 |
关键结论:
- scrypt 是 PBKDF2/bcrypt 的 “内存增强版”,首次解决了 “硬件加速破解” 问题;
- 与 Argon2 相比,scrypt 在并行优化、侧信道防御上稍逊,但实现更简单、资源占用更低,适合中端设备(如智能手机、嵌入式);
- 场景选择:资源受限设备选 scrypt,高端安全场景(如金融)选 Argon2, legacy 系统兼容选 PBKDF2/bcrypt。
2.2 RFC7914 核心定义
RFC7914 明确了 scrypt 的输入、输出与参数约束,是所有实现的合规依据。
2.2.1 输入输出参数
类型 |
参数名 |
描述 |
输入 |
Passphrase |
用户密码(任意长度字节流,建议 UTF-8 编码) |
输入 |
Salt |
随机盐值(至少 16 字节,RFC 建议 32 字节,避免彩虹表攻击) |
输入 |
N |
CPU / 内存成本参数(2 的幂,如 16384),值越大内存占用 / 计算量越高 |
输入 |
r |
块大小参数(通常取 8),影响每个内存块的字节数(64*r字节) |
输入 |
p |
并行化参数(通常取 1-4),控制并行处理的块数量 |
输入 |
dkLen |
输出密钥长度(需≤(2^32 - 1)*32字节,因 PBKDF2-HMAC-SHA256 输出限制) |
输出 |
DK |
最终派生密钥(长度为dkLen的字节流) |
2.2.2 关键参数约束(RFC7914 §3)
为保证安全性与可行性,RFC 强制要求参数满足:
- N ≥ 2且为 2 的幂(如 2、4、8...65536);
- r ≥ 1,p ≥ 1;
- N*r ≤ 2^30(避免内存溢出);
- p ≤ (2^32 - 1)/(128*r)(限制并行内存总占用)。
2.3 RFC7914 算法流程
scrypt 算法分三阶段,核心是阶段 2 的 SMix 内存密集处理,流程如图 1 所示:
2.3.1 阶段 1:初始密钥生成
通过 PBKDF2-HMAC-SHA256 将Passphrase和Salt转化为中间密钥DK1,公式为:
DK1 = PBKDF2-HMAC-SHA256(Passphrase, Salt, 1, 32\*p\*r)
- 迭代次数设为 1(因后续 SMix 已提供足够复杂度);
- 输出长度32*p*r字节:为后续并行 SMix 处理预留p组、每组r个 32 字节块。
2.3.2 阶段 2:SMix 内存密集型处理(核心)
SMix 是 scrypt 抗破解的关键,对DK1的每一组 32*r 字节数据执行内存密集变换,流程如下:
- 内存初始化:将 32r 字节数据拆分为r个 32 字节块,生成初始内存数组B[0..2N*r-1](总大小 = 2Nr*32 字节);
- 迭代混合:循环N次,每次对B执行 “随机访问 + 块混合”(通过 BlockMix 函数),强制内存随机读写;
- 最终提取:对混合后的B再次执行 BlockMix,输出 32*r 字节结果。
SMix 的核心是内存随机访问:每次迭代需读取前N个随机位置的块,使 GPU/ASIC 的并行计算优势因内存带宽限制而失效。
2.3.3 阶段 3:最终密钥生成
将阶段 2 输出的DK2作为新的 “盐值”,再次调用 PBKDF2-HMAC-SHA256 生成最终密钥:
DK = PBKDF2-HMAC-SHA256(Passphrase, DK2, 1, dkLen)
- 二次 PBKDF2 进一步增强密钥随机性,同时适配用户所需的dkLen长度。
3 openHiTLS 中 scrypt 的实现架构
openHiTLS 的 scrypt 模块位于crypto/scrypt/src/scrypt.c,严格遵循 RFC7914,同时优化了内存管理与并行性能,架构如图 2 所示。
3.1 openHiTLS 项目与 scrypt 模块定位
openHiTLS 是业界首个面向全场景开源密码库,核心模块包括crypto(哈希、对称加密、KDF)、tls(协议逻辑)、utils(工具函数)。scrypt 模块属于crypto下的 KDF 子模块,主要服务于:
- TLS 密钥派生(如 PSK 模式下的密钥扩展);
- 密码存储(应用层调用接口);
- 第三方应用的密钥生成(如磁盘加密)。
3.2 scrypt 模块代码结构(基于 scrypt.c)
scrypt.c的核心函数组织遵循 “入口 - 子模块 - 依赖” 的分层设计,关键函数如下表:
函数名 |
作用 |
scrypt(const uint8_t *passwd, size_t passwd_len, ...) |
算法入口:接收所有参数,调度三阶段流程 |
smix(uint8_t *B, size_t r, uint64_t N, uint8_t *V, uint8_t *XY) |
执行 SMix 内存密集处理(RFC7914 §4) |
blockmix_salsa8(const uint8_t *B, size_t r, uint8_t *Y, uint8_t *X) |
块混合函数:基于 Salsa20/8 哈希变换(RFC7914 §3.1) |
pbkdf2_hmac_sha256(const uint8_t *pass, size_t pass_len, ...) |
调用 openHiTLS 的 HMAC-SHA256 模块实现 PBKDF2(RFC2898) |
scrypt_validate_params(uint64_t N, size_t r, size_t p) |
参数校验:确保符合 RFC7914 约束 |
依赖模块:
- crypto/hmac/src/hmac.c:提供 HMAC-SHA256 实现;
- crypto/sha2/src/sha2.c:SHA256 哈希核心;
- utils/memory/src/mem.c:安全内存分配 / 释放(避免内存泄露)。
3.3 openHiTLS 实现的合规性与优化
3.3.1 RFC7914 参数校验实现
scrypt_validate_params函数严格执行 RFC 约束,代码片段如下(简化版):
int scrypt\_validate\_params(uint64\_t N, size\_t r, size\_t p) {
// 检查N是否为2的幂且≥2
if (N < 2 || (N & (N - 1)) != 0) {
return -1; // 非2的幂或过小
}
// 检查r≥1,p≥1
if (r < 1 || p < 1) {
return -1;
}
// 检查N\*r ≤ 2^30(避免内存溢出)
if ((uint64\_t)N \* r > (1ULL << 30)) {
return -1;
}
// 检查p ≤ (2^32-1)/(128\*r)
if ((uint64\_t)p > ((1ULL << 32) - 1) / (128 \* r)) {
return -1;
}
return 0;
}
所有参数需先通过校验,否则scrypt函数直接返回错误,保障合规性。
3.3.2 性能优化
- 内存复用:SMix 函数中V(内存数组)和XY(临时块)通过栈外分配(OPENSSL_malloc),避免栈溢出,且支持内存释放复用;
- 并行处理:对p组数据的 SMix 处理支持并行执行(通过pthread或系统线程池),但默认关闭(需编译时开启SCRYPT_PARALLEL);
- Salsa20/8 优化:blockmix_salsa8使用汇编优化的 Salsa20/8 变换(32 位 / 64 位平台适配),提升块混合效率。
4 openHiTLS scrypt 代码示例与解析
本节基于 openHiTLS scrypt.c,提供完整的算法调用示例,并解析核心函数逻辑。
4.1 环境准备与依赖引入
4.1.1 编译依赖
需先编译 openHiTLS 源码,依赖:
- 编译器:GCC 9.0+/Clang 11.0+;
- 依赖库:无(openHiTLS 自带所有 crypto 模块);
- 编译命令:
git clone https://gitcode.com/openHiTLS/openhitls.git
cd openhitls && mkdir build && cd build
cmake .. -DCMAKE\_INSTALL\_PREFIX=/usr/local/openhitls
make && make install
4.1.2 头文件引入
在应用代码中引入 scrypt 模块头文件:
#include "crypto/scrypt/src/scrypt.h" // openHiTLS scrypt接口
#include "crypto/sha2/src/sha2.h" // HMAC-SHA256依赖
#include "utils/memory/src/mem.h" // 内存管理
#include \<stdio.h>
#include \<string.h>
4.2 核心函数调用示例(完整流程)
以下示例实现 “用户密码→派生密钥” 的完整流程,参数符合 RFC7914 建议(服务器级安全):
int main() {
// 1. 输入参数配置(RFC7914建议:Salt≥16字节,N=16384,r=8,p=1)
const uint8\_t passwd\[] = "user\_secure\_password\_123"; // 用户密码
const size\_t passwd\_len = strlen((const char\*)passwd);
uint8\_t salt\[32]; // 32字节随机盐(实际需用安全随机数生成)
const uint64\_t N = 16384; // CPU/内存成本(2^14)
const size\_t r = 8; // 块大小
const size\_t p = 1; // 并行数
const size\_t dkLen = 32; // 输出密钥长度(32字节,适配AES-256)
uint8\_t DK\[dkLen]; // 最终派生密钥
// 2. 生成安全随机盐(关键:避免硬编码盐,此处用模拟随机数,实际需调用openHiTLS随机模块)
memset(salt, 0x12, sizeof(salt)); // 仅示例,实际替换为:rand\_bytes(salt, sizeof(salt));
// 3. 参数校验(RFC7914合规性检查)
int ret = scrypt\_validate\_params(N, r, p);
if (ret != 0) {
printf("参数非法:%d\n", ret);
return -1;
}
// 4. 调用scrypt派生密钥(核心步骤)
ret = scrypt(passwd, passwd\_len, salt, sizeof(salt), N, r, p, DK, dkLen);
if (ret != 0) {
printf("scrypt执行失败:%d\n", ret);
return -1;
}
// 5. 输出结果(十六进制打印密钥)
printf("派生密钥(%d字节):\n", dkLen);
for (size\_t i = 0; i < dkLen; i++) {
printf("%02x", DK\[i]);
}
printf("\n");
return 0;
}
编译与运行
gcc -o scrypt\_demo scrypt\_demo.c -L/usr/local/openhitls/lib -lopenhitls -I/usr/local/openhitls/include
./scrypt\_demo
输出示例:
派生密钥(32字节):
a3f2d4e5b6c7a8d9f0e1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3
4.3 关键函数深度解析
4.3.1 smix 函数实现(内存密集核心)
smix是 scrypt 的灵魂,负责执行 RFC7914 §4 的内存密集处理,代码逻辑(简化版)如下:
void smix(uint8\_t \*B, size\_t r, uint64\_t N, uint8\_t \*V, uint8\_t \*XY) {
size\_t i, j;
size\_t len = 32 \* r; // 每组数据长度(r个32字节块)
// 1. 初始化内存数组V(大小=2N\*len字节,存储N次迭代的中间结果)
memcpy(XY, B, len); // XY\[0..len-1] = B
for (i = 0; i < N; i++) {
memcpy(\&V\[i \* len], XY, len); // V\[i] = XY
blockmix\_salsa8(XY, r, XY + len, XY); // 块混合:更新XY
}
// 2. 迭代混合(随机访问V,增强内存依赖性)
for (i = 0; i < N; i++) {
// 随机选择V的索引:j = (XY最后32字节的哈希值) mod N
j = ((uint64\_t)XY\[len - 8] << 56) | ((uint64\_t)XY\[len - 7] << 48) |
((uint64\_t)XY\[len - 6] << 40) | ((uint64\_t)XY\[len - 5] << 32) |
((uint64\_t)XY\[len - 4] << 24) | ((uint64\_t)XY\[len - 3] << 16) |
((uint64\_t)XY\[len - 2] << 8) | ((uint64\_t)XY\[len - 1]);
j &= (N - 1); // 等价于j mod N(因N是2的幂)
// 异或混合:XY ^= V\[j]
for (size\_t k = 0; k < len; k++) {
XY\[k] ^= V\[j \* len + k];
}
// 再次块混合
blockmix\_salsa8(XY, r, XY + len, XY);
}
// 3. 输出结果到B
memcpy(B, XY, len);
}
核心逻辑解析:
- V数组:占用2N*len字节内存,存储N次迭代的中间块,是内存密集的关键;
- 随机索引j:通过 XY 的最后 8 字节生成,强制每次迭代随机访问V,打破硬件并行优化;
- 异或混合:将历史块(V [j])与当前块(XY)融合,增强哈希雪崩效应。
4.3.2 blockmix_salsa8 函数(块混合逻辑)
blockmix_salsa8基于 Salsa20/8 哈希变换(8 轮 Salsa20),实现块的打乱与混合,代码片段(简化版):
void blockmix\_salsa8(const uint8\_t \*B, size\_t r, uint8\_t \*Y, uint8\_t \*X) {
size\_t i;
const size\_t len = 32 \* r;
// 1. 初始化X为B的最后一个32字节块(B\[len-32..len-1])
memcpy(X, \&B\[len - 32], 32);
// 2. 对每个块执行Salsa20/8变换并混合
for (i = 0; i < r; i++) {
// X ^= B\[i\*32..(i+1)\*32-1]
for (size\_t k = 0; k < 32; k++) {
X\[k] ^= B\[i \* 32 + k];
}
// Salsa20/8变换:更新X(32字节→32字节)
salsa20\_8(X, X);
// 存储到Y的对应位置
memcpy(\&Y\[i \* 32], X, 32);
}
// 3. 重组Y为输出格式(RFC7914 §3.1)
for (i = 0; i < r; i++) {
memcpy(\&B\[i \* 32], \&Y\[(i \* 2) % r \* 32], 32);
}}
Salsa20/8 作用:是一种流密码哈希函数,8 轮变换足以提供高安全性,同时性能优于 SHA-256,适合块混合场景。
4.3.3 参数校验逻辑(RFC 合规保障)
如 3.3.1 节所示,scrypt_validate_params是算法安全的第一道防线,任何非法参数(如 N=3、p=1024)都会被拦截,避免因参数配置错误导致安全降级。
5 scrypt 的应用场景与安全考量
5.1 典型应用场景
- 密码存储:替代 bcrypt、PBKDF2,用于用户密码哈希存储(如网站后台、操作系统登录);
- 加密货币:作为工作量证明(PoW)算法(如莱特币、狗狗币),抵御 ASIC 矿机垄断;
- 密钥派生:为对称加密(AES)、磁盘加密(LUKS)生成高熵密钥;
- TLS 协议:在 TLS 1.3 PSK(预共享密钥)模式下,用于派生会话密钥。
5.2 参数选择策略(安全与性能平衡)
参数N、r、p的选择需根据设备性能动态调整,建议参考下表:
设备类型 |
N(2 的幂) |
r |
p |
内存占用(约) |
计算耗时(单线程) |
嵌入式设备(MCU) |
8192(2^13) |
4 |
1 |
8192432=1MB |
500ms-1s |
智能手机 |
16384(2^14) |
8 |
1 |
16384832=4MB |
300ms-500ms |
服务器(CPU) |
65536(2^16) |
8 |
2 |
65536832=16MB |
100ms-200ms |
原则:确保单次哈希耗时在 100ms-1s(用户无感知),同时内存占用不超过设备可用内存的 10%。
5.3 安全局限性与应对
- 资源受限设备性能不足:可降低N至 8192,r至 4,优先保证安全性;
- 未来量子计算威胁:目前量子计算对哈希函数无直接威胁,但可结合后量子密码(如格密码)进行双层保护;
- 盐值安全性:必须使用安全随机数生成盐值(如 openHiTLS 的rand_bytes函数),禁止硬编码或使用弱随机源(如时间戳)。
6 总结与展望
scrypt 算法(RFC7914)通过 “内存密集 + 计算密集” 的双重设计,成为当前最安全的密钥派生函数之一,而 openHiTLS 的实现严格遵循 RFC 规范,同时通过内存复用、汇编优化提升了工程实用性。
未来发展方向:
- 算法优化:结合更高效的内存密集结构(如 Argon2,2015 年密码哈希竞赛冠军),进一步增强抗破解能力;
- 硬件加速:针对服务器场景,开发专用硬件(如 FPGA)平衡性能与安全;
- 标准化扩展:推动 scrypt 在 TLS 1.4、区块链等场景的进一步标准化。
参考文献
- IETF RFC7914: The scrypt Password-Based Key Derivation Function. https://datatracker.ietf.org/doc/rfc7914/
- openHiTLS 官方仓库. GitCode - 全球开发者的开源社区,开源代码托管平台
- Colin Percival. Stronger Key Derivation via Sequential Memory-Hard Functions. 2009.
- IETF RFC9106: Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications. https://datatracker.ietf.org/doc/rfc9106/
- Niels Provos, David Mazières. A Future-Adaptable Password Scheme. 1999.(bcrypt 原始论文)