基于 OpenSSL 开发鸿蒙加密库:一次惊心动魄的 Bug 排查之旅
最近在为 HarmonyOS 开发一个加密库,需要使用 OpenSSL 来实现 Blowfish 等经典加密算法。相同的数据,每次加密的结果都不一样!🤯经过一段时间的排查,终于找到了问题的根源。这次踩坑经历让我深刻理解了 OpenSSL EVP API 的一些"潜规则",特此分享给各位,希望能帮大家避免同样的坑。// 1. 获取参数// 2. 初始化 OpenSSL// 加载 providers
基于 OpenSSL 开发鸿蒙加密库:一次惊心动魄的 Bug 排查之旅
前言
最近在为 HarmonyOS 开发一个加密库,需要使用 OpenSSL 来实现 Blowfish 等经典加密算法。本以为是个很简单的任务,结果却遇到了一个让我抓狂的问题:相同的数据,每次加密的结果都不一样! 🤯
经过一段时间的排查,终于找到了问题的根源。这次踩坑经历让我深刻理解了 OpenSSL EVP API 的一些"潜规则",特此分享给各位,希望能帮大家避免同样的坑。
开发环境准备
在开始之前,强烈推荐收藏这些官方资源:
第一章:OpenSSL 编译篇 - 环境搭建之路
1.1 为什么选择 tpc_c_cplusplus?
华为官方提供的 tpc_c_cplusplus 仓库包含了大量常用 C/C++ 库的编译脚本,对于 HarmonyOS 的适配已经做得很完善了。我们直接使用它来编译 OpenSSL,可以省去大量的踩坑时间。
1.2 编译脚本的修改要点
问题 1:默认脚本编译的是旧版本
官方脚本默认编译 OpenSSL_1_1_1u,但我需要使用 OpenSSL 3.5.0 的新特性,所以需要修改脚本中的版本号:
# 找到脚本中的 pkgver 变量,修改为:
pkgver=openssl-3.5.0
问题 2:需要使用 Blowfish 等 Legacy 算法
OpenSSL 3.x 将一些老旧的算法(如 Blowfish、DES、MD5 等)归入了 legacy provider,默认不启用。如果需要使用这些算法,必须在编译时开启:
# 在编译参数中添加
--enable-legacy
⚠️ 重要提示:默认脚本会生成静态库(no-shared),如果需要动态库,请删除该参数。
问题 3:Legacy Provider 的路径陷阱
这是一个巨坑!编译时,OpenSSL 会将 legacy.so 的路径硬编码到库文件中。这个路径是编译环境的路径,而不是目标设备的路径。
问题现象:
运行时报错:Failed to load legacy provider
原因:OpenSSL 尝试从 /home/builder/openssl/lib/ossl-modules/legacy.so 加载
但实际设备上根本没有这个路径!
解决方案:
在编译参数中添加 no-module 选项,将 legacy 直接编译进静态库:
# 完整的编译参数示例
./Configure \
--prefix=/path/to/install \
no-shared \
enable-legacy \
no-module \
...其他参数
这样,legacy 算法就会内嵌到 libcrypto.a 中,不需要额外的 .so 文件。
1.3 编译产物
编译完成后,你会得到:
libssl.a- SSL/TLS 协议实现libcrypto.a- 加密算法实现(包含 legacy)- 头文件目录
将这些文件拷贝到你的 HarmonyOS 项目中即可使用。
第二章:使用篇 - 惊心动魄的 Bug 排查
2.1 问题浮现:加密结果不一致
一切准备就绪后,我写了一个简单的 Blowfish 加密测试:
// JavaScript 测试代码
const key = buffer.from("test1234").buffer;
const iv = buffer.from("test1234").buffer;
const input = buffer.from("test1234").buffer;
// 连续加密 3 次相同的数据
for (let i = 0; i < 3; i++) {
let result = Crypto.Blowfish(input, key, iv, 0);
console.log(`第 ${i+1} 次:`, buffer.from(result).toString("base64"));
}
预期结果:三次输出应该完全一样
实际结果:💥
第 1 次: dr/Y4j4Of6o=
第 2 次: ZeMJJaEJcu0= ❌ 完全不一样!
第 3 次: wsjcVOSUg9E= ❌ 又变了!
看到这个结果,我的第一反应是:难道 Blowfish 内部有随机化?但这显然不合理,加密算法应该是确定性的啊!
2.2 排查过程:一步步缩小范围
第一步:检查输入数据
我首先怀疑是 JavaScript 端的数据有问题,于是在 C++ 端打印了所有输入参数:
LOGI("key(hex): %s", keyHex.c_str()); // 7465737431323334 ✅
LOGI("iv(hex): %s", ivHex.c_str()); // 7465737431323334 ✅
LOGI("input(hex): %s", inputHex.c_str()); // 7465737431323334 ✅
输入完全一致,排除数据问题。
第二步:检查 IV 是否被修改
CBC 模式会修改内部的 IV 状态,我怀疑 OpenSSL 可能修改了传入的 IV 缓冲区:
// 在加密前后打印 IV
LOGI("iv before: %s", ivHex.c_str());
// ... 执行加密 ...
LOGI("iv after: %s", ivHex.c_str());
结果:IV 完全没有被修改!排除 IV 污染。
第三步:自检测试 - 真相大白
我在初始化时添加了一个自检函数,直接用 OpenSSL API 连续加密 3 次:
// 在 globalInit 中添加自检
for (int i = 0; i < 3; i++) {
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
EVP_EncryptInit_ex(ctx, EVP_bf_cbc(), nullptr, nullptr, nullptr);
EVP_EncryptInit_ex(ctx, nullptr, nullptr, test_key, test_iv);
// ... 执行加密 ...
LOGI("Self-test iteration %d: %s", i+1, hex);
EVP_CIPHER_CTX_free(ctx);
}
第一次自检结果:
Self-test iteration 1: 7cceb15d1bdcc1b9 ❌
Self-test iteration 2: a5d2d8bd8d3cdbba ❌ 不一致
Self-test iteration 3: 942e789dbfc867e8 ❌ 不一致
问题复现了!说明是 OpenSSL 使用方式的问题,与 JavaScript 无关。
2.3 真凶现身:Blowfish 的密钥长度陷阱
经过仔细查阅 OpenSSL 文档和源码,我发现了问题的根源:
Blowfish 的特殊性
Blowfish 是一个可变密钥长度的算法:
- 支持密钥长度:1 到 56 字节(非常灵活)
- 块大小:固定 8 字节
- IV 长度:固定 8 字节
而 AES 等现代算法的密钥长度是固定的(AES-128 就是 16 字节,AES-256 就是 32 字节)。
问题代码分析
我的原始代码:
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
// 第一步:设置 cipher
EVP_EncryptInit_ex(ctx, EVP_bf_cbc(), nullptr, nullptr, nullptr);
// ❌ 问题:直接设置 8 字节的 key,但没有告诉 OpenSSL 密钥长度
EVP_EncryptInit_ex(ctx, nullptr, nullptr, key, iv);
问题根源:
- 第一次调用
EVP_EncryptInit_ex时,OpenSSL 使用 Blowfish 的默认密钥长度 16 字节 - Context 内部预期接收 16 字节的密钥
- 第二次调用时传入了 8 字节密钥,长度不匹配
- OpenSSL 不知道该怎么处理,就会产生未定义行为:
- 可能用内存中的随机数据填充剩余的 8 字节
- 可能用之前的状态数据
- 每次执行时使用的数据都不一样,导致输出不一致
正确的做法
关键:显式设置密钥长度!
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
// 第一步:设置 cipher(不设置 key/IV)
EVP_EncryptInit_ex(ctx, EVP_bf_cbc(), nullptr, nullptr, nullptr);
// ✅ 第二步:显式设置密钥长度(关键的一步!)
EVP_CIPHER_CTX_set_key_length(ctx, 8); // 告诉 OpenSSL:我要用 8 字节密钥
// 第三步:设置 key 和 IV
EVP_EncryptInit_ex(ctx, nullptr, nullptr, key, iv);
// 第四步:设置填充模式
EVP_CIPHER_CTX_set_padding(ctx, 0);
// 第五步:执行加密
EVP_EncryptUpdate(ctx, out, &outl, input, inl);
EVP_EncryptFinal_ex(ctx, out + outl, &outl);
2.4 修复验证:完美!
添加 EVP_CIPHER_CTX_set_key_length 后,重新编译运行:
Self-test iteration 1: 76bfd8e23e0e7faa ✅
Self-test iteration 2: 76bfd8e23e0e7faa ✅ 一致!
Self-test iteration 3: 76bfd8e23e0e7faa ✅ 一致!
用户测试:
第 1 次: dr/Y4j4Of6o= ✅
第 2 次: dr/Y4j4Of6o= ✅ 完美一致
第 3 次: dr/Y4j4Of6o= ✅ 完美一致
问题彻底解决!🎉
2.5 额外发现:Provider 的正确加载方式
在排查过程中,我还发现了 OpenSSL 3.x 的 Provider 加载最佳实践:
❌ 错误做法
bool globalInit() {
// 每次都加载,但不保存指针
if (!OSSL_PROVIDER_load(nullptr, "legacy")) {
LOGE("Failed to load legacy");
}
}
问题:Provider 可能被意外卸载,导致算法不可用。
✅ 正确做法
// 使用静态变量保存 provider 指针
static OSSL_PROVIDER *default_provider = nullptr;
static OSSL_PROVIDER *legacy_provider = nullptr;
static std::once_flag init_flag;
bool globalInit() {
std::call_once(init_flag, []() {
// 先加载 default,再加载 legacy
default_provider = OSSL_PROVIDER_load(nullptr, "default");
legacy_provider = OSSL_PROVIDER_load(nullptr, "legacy");
if (!legacy_provider) {
throw std::runtime_error("Failed to load legacy provider");
}
LOGI("OpenSSL providers loaded successfully");
});
return true;
}
要点:
- 使用
static变量保存 provider 指针,确保生命周期 - 使用
std::call_once确保只初始化一次 - 同时加载
default和legacyprovider
第三章:经验总结与最佳实践
3.1 完整的 Blowfish 加密代码模板
napi_value Encrypt(napi_env env, napi_callback_info info) {
// 1. 获取参数
size_t argc = 5;
napi_value args[5] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
// 2. 初始化 OpenSSL
globalInit(); // 加载 providers
// 3. 创建 context
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx) {
napi_throw_error(env, nullptr, "Failed to create context");
return nullptr;
}
// 4. 解析参数
std::string algName = get_string(env, args[0]);
size_t keyLen = 0, ivLen = 0;
unsigned char* keyPtr = get_arraybuffer(env, args[1], &keyLen);
unsigned char* ivPtr = get_arraybuffer(env, args[2], &ivLen);
int padding = get_int32(env, args[3]);
size_t inLen = 0;
unsigned char* inputPtr = get_arraybuffer(env, args[4], &inLen);
// 5. 复制数据(确保内存安全)
unsigned char* key = new unsigned char[keyLen];
unsigned char* iv = new unsigned char[ivLen];
unsigned char* input = new unsigned char[inLen];
memcpy_s(key, keyLen, keyPtr, keyLen);
memcpy_s(iv, ivLen, ivPtr, ivLen);
memcpy_s(input, inLen, inputPtr, inLen);
// 6. 设置 cipher(重点步骤)
int ret = EVP_EncryptInit_ex(ctx, EVP_bf_cbc(), nullptr, nullptr, nullptr);
if (ret != 1) goto error;
// ⭐ 7. 设置密钥长度(Blowfish 必需!)
ret = EVP_CIPHER_CTX_set_key_length(ctx, keyLen);
if (ret != 1) {
LOGW("Failed to set key length, using default");
}
// 8. 设置 key 和 IV
ret = EVP_EncryptInit_ex(ctx, nullptr, nullptr, key, iv);
if (ret != 1) goto error;
// 9. 设置填充模式
EVP_CIPHER_CTX_set_padding(ctx, padding);
// 10. 验证块大小(无填充时必须对齐)
int blockSize = EVP_CIPHER_CTX_block_size(ctx);
if (padding == 0 && (inLen % blockSize != 0)) {
napi_throw_error(env, nullptr, "Input length must be multiple of block size");
goto error;
}
// 11. 分配输出缓冲区
size_t maxOutLen = inLen + blockSize;
unsigned char* out = new unsigned char[maxOutLen];
memset(out, 0, maxOutLen);
// 12. 执行加密
int outl = 0, cipherLen = 0;
ret = EVP_EncryptUpdate(ctx, out, &outl, input, inLen);
if (ret != 1) goto error;
cipherLen += outl;
ret = EVP_EncryptFinal_ex(ctx, out + outl, &outl);
if (ret != 1) goto error;
cipherLen += outl;
// 13. 清理资源
EVP_CIPHER_CTX_free(ctx);
delete[] key;
delete[] iv;
delete[] input;
// 14. 创建返回值
napi_value arraybuffer;
void* buffer_data = nullptr;
napi_create_arraybuffer(env, cipherLen, &buffer_data, &arraybuffer);
memcpy_s(buffer_data, cipherLen, out, cipherLen);
delete[] out;
return arraybuffer;
error:
// 错误处理:清理所有资源
EVP_CIPHER_CTX_free(ctx);
delete[] key;
delete[] iv;
delete[] input;
napi_throw_error(env, nullptr, "Encryption failed");
return nullptr;
}
3.2 EVP API 标准调用顺序
对于可变密钥长度算法(Blowfish、RC2、CAST5 等):
1. EVP_CIPHER_CTX_new()
2. EVP_EncryptInit_ex(ctx, cipher, engine, NULL, NULL)
3. ⭐ EVP_CIPHER_CTX_set_key_length(ctx, key_len) ← 必需!
4. EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv)
5. EVP_CIPHER_CTX_set_padding(ctx, padding)
6. EVP_EncryptUpdate() / EVP_EncryptFinal_ex()
7. EVP_CIPHER_CTX_free()
对于固定密钥长度算法(AES、DES 等):
1. EVP_CIPHER_CTX_new()
2. EVP_EncryptInit_ex(ctx, cipher, engine, key, iv) ← 可以一次设置
3. EVP_CIPHER_CTX_set_padding(ctx, padding)
4. EVP_EncryptUpdate() / EVP_EncryptFinal_ex()
5. EVP_CIPHER_CTX_free()
3.3 需要特别注意的算法
以下算法支持可变密钥长度,必须显式设置密钥长度:
| 算法 | 密钥长度范围 | OpenSSL 默认长度 |
|---|---|---|
| Blowfish | 1-56 字节 | 16 字节 (128 位) |
| RC2 | 1-128 字节 | 16 字节 (128 位) |
| RC4 | 1-256 字节 | 16 字节 (128 位) |
| CAST5 | 5-16 字节 | 16 字节 (128 位) |
总结
这次排查经历让我学到了:
- OpenSSL 文档虽然详细,但很多"潜规则"需要实践才能发现 📖
- 可变密钥长度算法需要额外小心,必须显式设置密钥长度 🔑
- 不要想当然,即使是"简单"的加密操作也可能有坑 ⚠️
希望这篇文章能帮助到正在开发 HarmonyOS/openssl的同学,少走弯路!
如果你也遇到了类似的问题,欢迎在评论区交流~ 🙌
参考资料
项目地址:crypto-openharmony
更多推荐



所有评论(0)