本文主要讲解如何在原生鸿蒙上使用 OpenSSL 提供的国密算法进行加解密操作。
本文的示例代码在:sm-example
接入 OpenSSL
作者已经在本文发布前实现了对 OpenSSL 的预构建以及包封装等操作,如果在没有任何需要定制化构建或者其他需求场景的情况下,可以直接使用已经封装好的包。
本文将使用 C++ 作为开发语言,来简单讲解如何使用 OpenSSL 提供的国密算法进行加解密操作。
桥接能力,将基于 node-addon-api-ohos 实现,具体使用可参考 node-addon-api-ohos
依旧是创建一个 C++ 模块的原生模块模板,我们开始尝试接入 OpenSSL。
创建完成之后,我们在 entry
目录下通过 ohpm 安装我们已经处理好的包。
ohpm install @ohos-rs/openssl --save-dev
紧接着在 entry
模块的 CMakeLists.txt
文件中新增如下内容以供我们的项目接入 OpenSSL。
# the minimum version of CMake.
cmake_minimum_required(VERSION 3.5.0)
project(sm)
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
set(OPENSSL_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules/@ohos-rs/openssl)
set(CMAKE_MODULE_PATH ${OPENSSL_ROOT_PATH})
find_package(OpenSSL REQUIRED)
if(DEFINED PACKAGE_FIND_FILE)
include(${PACKAGE_FIND_FILE})
endif()
include_directories(${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include)
add_library(entry SHARED napi_init.cpp)
target_link_libraries(entry PUBLIC libace_napi.z.so OpenSSL::Crypto OpenSSL::SSL)
与我们之前使用 node-addon-api 类似 OPENSSL_ROOT_PATH
为固定定义变量,不可修改。
SM2
非对称加密,与 RSA 算法类似,具体的加解密过程这里不过多赘述,我们直接看如何在鸿蒙上基于 OpenSSL 实现。
这里已经假定你已经有了 SM2 所需的公私钥。
// sm2.h
#include "napi.h"
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include <openssl/bio.h>
#include <openssl/buffer.h>
#include <string>
#include <vector>
#include <iostream>
#include <memory>
namespace sm2 {
#pragma once
const char *SM2_PUBLIC_KEY = R"(
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEWr91LPdgTLpIH/rzXQQw/qmJKkli
5nPj0xNfyE87kXdP2KakBHBSf8JGHOnyXcdnvNnNrc33jQseeIuTmPuJpQ==
-----END PUBLIC KEY-----
)";
#pragma once
const char *SM2_PRIVATE_KEY = R"(
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBG0wawIBAQQgZBIiMFLLF/+OmksS
+dlFpWxBgsJpj0NLon+bKN5Yz2ihRANCAARav3Us92BMukgf+vNdBDD+qYkqSWLm
c+PTE1/ITzuRd0/YpqQEcFJ/wkYc6fJdx2e82c2tzfeNCx54i5OY+4ml
-----END PRIVATE KEY-----
)";
// 自定义智能指针删除器
struct BIODeleter {
void operator()(BIO *bio) { BIO_free_all(bio); }
};
struct EVP_PKEY_CTXDeleter {
void operator()(EVP_PKEY_CTX *ctx) { EVP_PKEY_CTX_free(ctx); }
};
struct EVP_PKEYDeleter {
void operator()(EVP_PKEY *key) { EVP_PKEY_free(key); }
};
// Base64 编码函数
std::string Base64Encode(const std::vector<unsigned char> &binary_data) {
BIO *bio = nullptr;
BIO *b64 = nullptr;
BUF_MEM *bufferPtr = nullptr;
b64 = BIO_new(BIO_f_base64());
bio = BIO_new(BIO_s_mem());
bio = BIO_push(b64, bio);
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); // 不添加换行符
BIO_write(bio, binary_data.data(), binary_data.size());
BIO_flush(bio);
BIO_get_mem_ptr(bio, &bufferPtr);
std::string result(bufferPtr->data, bufferPtr->length);
BIO_free_all(bio);
return result;
}
// Base64 解码函数
std::vector<unsigned char> Base64Decode(const std::string &base64_data) {
BIO *bio = nullptr;
BIO *b64 = nullptr;
std::vector<unsigned char> result(base64_data.size());
b64 = BIO_new(BIO_f_base64());
bio = BIO_new_mem_buf(base64_data.c_str(), -1);
bio = BIO_push(b64, bio);
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); // 不添加换行符
int decoded_size = BIO_read(bio, result.data(), base64_data.size());
BIO_free_all(bio);
if (decoded_size > 0) {
result.resize(decoded_size);
return result;
}
return std::vector<unsigned char>();
}
// 错误处理函数
void PrintOpenSSLError() {
unsigned long err;
while ((err = ERR_get_error()) != 0) {
char err_buf[256];
ERR_error_string_n(err, err_buf, sizeof(err_buf));
std::cerr << "OpenSSL Error: " << err_buf << std::endl;
}
}
// 从字符串加载密钥
std::unique_ptr<EVP_PKEY, EVP_PKEYDeleter> LoadKeyFromString(const std::string &keyStr, bool isPublic) {
BIO *bio = BIO_new_mem_buf(keyStr.c_str(), -1);
if (!bio) {
std::cerr << "Error creating BIO" << std::endl;
return nullptr;
}
EVP_PKEY *key = nullptr;
if (isPublic) {
key = PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr);
} else {
key = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
}
BIO_free_all(bio);
if (!key) {
std::cerr << "Error reading key from string" << std::endl;
PrintOpenSSLError();
return nullptr;
}
return std::unique_ptr<EVP_PKEY, EVP_PKEYDeleter>(key);
}
// 加密函数
bool Encrypt(const std::string &plaintext, EVP_PKEY *publicKey, std::string &base64Ciphertext) {
std::unique_ptr<EVP_PKEY_CTX, EVP_PKEY_CTXDeleter> ctx(EVP_PKEY_CTX_new(publicKey, nullptr));
if (!ctx) {
std::cerr << "Error creating encryption context" << std::endl;
PrintOpenSSLError();
return false;
}
if (EVP_PKEY_encrypt_init(ctx.get()) <= 0) {
std::cerr << "Error initializing encryption" << std::endl;
PrintOpenSSLError();
return false;
}
size_t outlen;
if (EVP_PKEY_encrypt(ctx.get(), nullptr, &outlen, reinterpret_cast<const unsigned char *>(plaintext.c_str()),
plaintext.length()) <= 0) {
std::cerr << "Error determining ciphertext length" << std::endl;
PrintOpenSSLError();
return false;
}
std::vector<unsigned char> ciphertext(outlen);
if (EVP_PKEY_encrypt(ctx.get(), ciphertext.data(), &outlen,
reinterpret_cast<const unsigned char *>(plaintext.c_str()), plaintext.length()) <= 0) {
std::cerr << "Error during encryption" << std::endl;
PrintOpenSSLError();
return false;
}
ciphertext.resize(outlen);
base64Ciphertext = Base64Encode(ciphertext);
return true;
}
// 解密函数
bool Decrypt(const std::string &base64Ciphertext, EVP_PKEY *privateKey, std::string &plaintext) {
std::vector<unsigned char> ciphertext = Base64Decode(base64Ciphertext);
if (ciphertext.empty()) {
std::cerr << "Error decoding base64 ciphertext" << std::endl;
return false;
}
std::unique_ptr<EVP_PKEY_CTX, EVP_PKEY_CTXDeleter> ctx(EVP_PKEY_CTX_new(privateKey, nullptr));
if (!ctx) {
std::cerr << "Error creating decryption context" << std::endl;
PrintOpenSSLError();
return false;
}
if (EVP_PKEY_decrypt_init(ctx.get()) <= 0) {
std::cerr << "Error initializing decryption" << std::endl;
PrintOpenSSLError();
return false;
}
size_t outlen;
if (EVP_PKEY_decrypt(ctx.get(), nullptr, &outlen, ciphertext.data(), ciphertext.size()) <= 0) {
std::cerr << "Error determining plaintext length" << std::endl;
PrintOpenSSLError();
return false;
}
std::vector<unsigned char> out(outlen);
if (EVP_PKEY_decrypt(ctx.get(), out.data(), &outlen, ciphertext.data(), ciphertext.size()) <= 0) {
std::cerr << "Error during decryption" << std::endl;
PrintOpenSSLError();
return false;
}
plaintext = std::string(reinterpret_cast<char *>(out.data()), outlen);
return true;
}
Napi::Value SM2_ENCRYPT(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
OpenSSL_add_all_algorithms();
ERR_load_crypto_strings();
// 从字符串加载密钥
auto publicKey = LoadKeyFromString(SM2_PUBLIC_KEY, true);
auto privateKey = LoadKeyFromString(SM2_PRIVATE_KEY, false);
if (!publicKey || !privateKey) {
PrintOpenSSLError();
return env.Null();
}
// 测试加密和解密
const std::string originalText = info[0].As<Napi::String>();
std::string base64Ciphertext;
Encrypt(originalText, publicKey.get(), base64Ciphertext);
return Napi::String::New(env, base64Ciphertext);
}
Napi::Value SM2_DECRYPT(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
OpenSSL_add_all_algorithms();
ERR_load_crypto_strings();
// 从字符串加载密钥
auto publicKey = LoadKeyFromString(SM2_PUBLIC_KEY, true);
auto privateKey = LoadKeyFromString(SM2_PRIVATE_KEY, false);
if (!publicKey || !privateKey) {
PrintOpenSSLError();
return env.Null();
}
const std::string originalText = info[0].As<Napi::String>();
std::string decryptedText;
Decrypt(originalText, privateKey.get(), decryptedText);
return Napi::String::New(env, decryptedText);
}
}
整体逻辑相对来比较清晰,我们可以分为以下几个步骤实现:
- OpenSSL 环境初始化
- 加载公私钥
- 创建和初始化加解密上下文
- 加密/解密
- 资源释放
在社区和一些渠道反馈说,该包并没有提供 SM 相关的能力?
实际上 OpenSSL 在 1.1.1 的版本中就已经开始提供了,但是为什么有人说没有对应的能力呢?因为在 OpenSSL 的实现中,一般情况下我们都不是直接使用某个具体的算法头文件,而是使用 include/openssl
下面的标准接口来实现。
- BIO_*
- PEM_*
- EVP_*
等系列标准接口来实现各种类型的加解密。比如在后续的 SM3 中会使用 EVP_sm3
来获取 SM3 算法,SM4 中则会使用 EVP_sm4_cbc
来获取 SM4 算法。
在一些比较旧的社区示例中依旧使用了类似于 crypto/sm2.h
openssl/sm2.h
等头文件,实际上根据 OpenSSL 在 issue 中的 一些 回答 也不建议如此使用。
同时官方目前的构建系统,在最终的输出产物中也不会输出跟具体加解密算法相关的头文件,这一系列操作导致了部分同学误以为 OpenSSL 不支持国密算法。
SM3
SM3 算法则是哈希散列算法,简而言之就是类似于 MD5 的算法。这里同样给出一个示例代码来简单演示在鸿蒙系统上如何使用 OpenSSL 实现 SM3 算法。
#include <openssl/evp.h>
#include <openssl/err.h>
#include <string>
#include <vector>
#include <iostream>
#include <iomanip>
#include <sstream>
#include <memory>
#include "napi.h"
#include "sm2.h"
namespace sm3 {
struct EVP_MD_CTXDeleter {
void operator()(EVP_MD_CTX *ctx) { EVP_MD_CTX_free(ctx); }
};
// 计算 SM3 哈希值
std::vector<unsigned char> CalculateSM3(const std::string &input) {
// 创建消息摘要上下文
std::unique_ptr<EVP_MD_CTX, EVP_MD_CTXDeleter> mdctx(EVP_MD_CTX_new());
if (!mdctx) {
std::cerr << "Error creating EVP_MD_CTX" << std::endl;
sm2::PrintOpenSSLError();
return std::vector<unsigned char>();
}
// 获取 SM3 算法
const EVP_MD *md = EVP_sm3();
if (!md) {
std::cerr << "Error getting SM3 algorithm" << std::endl;
sm2::PrintOpenSSLError();
return std::vector<unsigned char>();
}
// 初始化消息摘要上下文
if (EVP_DigestInit_ex(mdctx.get(), md, nullptr) != 1) {
std::cerr << "Error initializing digest" << std::endl;
sm2::PrintOpenSSLError();
return std::vector<unsigned char>();
}
// 更新消息摘要
if (EVP_DigestUpdate(mdctx.get(), input.c_str(), input.length()) != 1) {
std::cerr << "Error updating digest" << std::endl;
sm2::PrintOpenSSLError();
return std::vector<unsigned char>();
}
// 获取摘要结果
std::vector<unsigned char> hash(EVP_MD_size(md));
unsigned int hash_len;
if (EVP_DigestFinal_ex(mdctx.get(), hash.data(), &hash_len) != 1) {
std::cerr << "Error finalizing digest" << std::endl;
sm2::PrintOpenSSLError();
return std::vector<unsigned char>();
}
hash.resize(hash_len);
return hash;
}
// 将字节数组转换为十六进制字符串
std::string BytesToHexString(const std::vector<unsigned char> &bytes) {
std::stringstream ss;
ss << std::hex << std::setfill('0');
for (unsigned char byte : bytes) {
ss << std::setw(2) << static_cast<int>(byte);
}
return ss.str();
}
// 将十六进制字符串转换为字节数组
std::vector<unsigned char> HexStringToBytes(const std::string &hex) {
std::vector<unsigned char> bytes;
for (size_t i = 0; i < hex.length(); i += 2) {
std::string byteString = hex.substr(i, 2);
unsigned char byte = static_cast<unsigned char>(std::stoi(byteString, nullptr, 16));
bytes.push_back(byte);
}
return bytes;
}
Napi::Value SM3_ENCRYPT(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
std::string string1 = info[0].As<Napi::String>();
std::vector<unsigned char> hash = CalculateSM3(string1);
std::string hashHex = BytesToHexString(hash);
return Napi::String::New(env, hashHex);
}
}
SM4
SM4 是对称算法,类似于 AES/DES 等算法。这里依旧给出一个具体的示例。
#include <openssl/evp.h>
#include <openssl/err.h>
#include <openssl/rand.h>
#include <string>
#include <vector>
#include <iostream>
#include <iomanip>
#include <memory>
#include <stdexcept>
#include <sstream>
#include "napi.h"
namespace sm4 {
class SM4Cipher {
private:
// 常量定义
static const int KEY_SIZE = 16; // SM4 密钥长度 (128 bits)
static const int IV_SIZE = 16; // IV 长度 (128 bits)
static const int BLOCK_SIZE = 16; // 块大小 (128 bits)
std::vector<unsigned char> key;
std::vector<unsigned char> iv;
// 错误处理函数
static void handleOpenSSLErrors() {
unsigned long err;
while ((err = ERR_get_error()) != 0) {
char err_buf[256];
ERR_error_string_n(err, err_buf, sizeof(err_buf));
std::cerr << "OpenSSL Error: " << err_buf << std::endl;
}
}
// 字节数组转十六进制字符串
static std::string bytesToHex(const std::vector<unsigned char> &bytes) {
std::stringstream ss;
ss << std::hex << std::setfill('0');
for (unsigned char byte : bytes) {
ss << std::setw(2) << static_cast<int>(byte);
}
return ss.str();
}
// 十六进制字符串转字节数组
static std::vector<unsigned char> hexToBytes(const std::string &hex) {
std::vector<unsigned char> bytes;
for (size_t i = 0; i < hex.length(); i += 2) {
std::string byteString = hex.substr(i, 2);
unsigned char byte = static_cast<unsigned char>(std::stoi(byteString, nullptr, 16));
bytes.push_back(byte);
}
return bytes;
}
public:
// 构造函数:使用指定的密钥和 IV
SM4Cipher(const std::vector<unsigned char> &key_data, const std::vector<unsigned char> &iv_data) {
if (key_data.size() != KEY_SIZE) {
throw std::runtime_error("Invalid key size");
}
if (iv_data.size() != IV_SIZE) {
throw std::runtime_error("Invalid IV size");
}
key = key_data;
iv = iv_data;
}
// 构造函数:随机生成密钥和 IV
SM4Cipher() {
key.resize(KEY_SIZE);
iv.resize(IV_SIZE);
if (RAND_bytes(key.data(), KEY_SIZE) != 1) {
handleOpenSSLErrors();
throw std::runtime_error("Failed to generate random key");
}
if (RAND_bytes(iv.data(), IV_SIZE) != 1) {
handleOpenSSLErrors();
throw std::runtime_error("Failed to generate random IV");
}
}
// 获取密钥和 IV
std::string getKeyHex() const { return bytesToHex(key); }
std::string getIVHex() const { return bytesToHex(iv); }
// 加密函数
std::vector<unsigned char> encrypt(const std::vector<unsigned char> &plaintext) {
// 创建并配置加密上下文
std::unique_ptr<EVP_CIPHER_CTX, decltype(&EVP_CIPHER_CTX_free)> ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free);
if (!ctx) {
handleOpenSSLErrors();
throw std::runtime_error("Failed to create cipher context");
}
// 初始化加密操作
if (EVP_EncryptInit_ex(ctx.get(), EVP_sm4_cbc(), nullptr, key.data(), iv.data()) != 1) {
handleOpenSSLErrors();
throw std::runtime_error("Failed to initialize encryption");
}
// 分配输出缓冲区
std::vector<unsigned char> ciphertext(plaintext.size() + BLOCK_SIZE);
int outlen1 = 0;
int outlen2 = 0;
// 加密数据
if (EVP_EncryptUpdate(ctx.get(), ciphertext.data(), &outlen1, plaintext.data(), plaintext.size()) != 1) {
handleOpenSSLErrors();
throw std::runtime_error("Failed to encrypt data");
}
// 完成加密操作
if (EVP_EncryptFinal_ex(ctx.get(), ciphertext.data() + outlen1, &outlen2) != 1) {
handleOpenSSLErrors();
throw std::runtime_error("Failed to finalize encryption");
}
// 调整输出大小
ciphertext.resize(outlen1 + outlen2);
return ciphertext;
}
// 解密函数
std::vector<unsigned char> decrypt(const std::vector<unsigned char> &ciphertext) {
// 创建并配置解密上下文
std::unique_ptr<EVP_CIPHER_CTX, decltype(&EVP_CIPHER_CTX_free)> ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free);
if (!ctx) {
handleOpenSSLErrors();
throw std::runtime_error("Failed to create cipher context");
}
// 初始化解密操作
if (EVP_DecryptInit_ex(ctx.get(), EVP_sm4_cbc(), nullptr, key.data(), iv.data()) != 1) {
handleOpenSSLErrors();
throw std::runtime_error("Failed to initialize decryption");
}
// 分配输出缓冲区
std::vector<unsigned char> plaintext(ciphertext.size());
int outlen1 = 0;
int outlen2 = 0;
// 解密数据
if (EVP_DecryptUpdate(ctx.get(), plaintext.data(), &outlen1, ciphertext.data(), ciphertext.size()) != 1) {
handleOpenSSLErrors();
throw std::runtime_error("Failed to decrypt data");
}
// 完成解密操作
if (EVP_DecryptFinal_ex(ctx.get(), plaintext.data() + outlen1, &outlen2) != 1) {
handleOpenSSLErrors();
throw std::runtime_error("Failed to finalize decryption");
}
// 调整输出大小
plaintext.resize(outlen1 + outlen2);
return plaintext;
}
// 字符串加密便捷方法
std::string encryptString(const std::string &plaintext) {
std::vector<unsigned char> input(plaintext.begin(), plaintext.end());
auto encrypted = encrypt(input);
return bytesToHex(encrypted);
}
// 字符串解密便捷方法
std::string decryptString(const std::string &hexCiphertext) {
auto ciphertext = hexToBytes(hexCiphertext);
auto decrypted = decrypt(ciphertext);
return std::string(decrypted.begin(), decrypted.end());
}
};
SM4Cipher globalCipher;
Napi::Value SM4_ENCRYPT(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
std::string text = info[0].As<Napi::String>();
std::string ciper = globalCipher.encryptString(text);
return Napi::String::New(env, ciper);
}
Napi::Value SM4_DECRYPT(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
std::string text = info[0].As<Napi::String>();
std::string ciper = globalCipher.decryptString(text);
return Napi::String::New(env, ciper);
}
}
使用
以上就是关于 SM2/SM3/SM4 国密算法的具体简单实现,接下来我们只需要在入口文件中将我们定义的函数导出即可。
#include "sm2.h"
#include "sm3.h"
#include "sm4.h"
// 初始化模块
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "sm2Encrypt"), Napi::Function::New(env, sm2::SM2_ENCRYPT));
exports.Set(Napi::String::New(env, "sm2Decrypt"), Napi::Function::New(env, sm2::SM2_DECRYPT));
exports.Set(Napi::String::New(env, "sm3Encrypt"), Napi::Function::New(env, sm3::SM3_ENCRYPT));
exports.Set(Napi::String::New(env, "sm4Encrypt"), Napi::Function::New(env, sm4::SM4_ENCRYPT));
exports.Set(Napi::String::New(env, "sm4Decrypt"), Napi::Function::New(env, sm4::SM4_DECRYPT));
return exports;
}
NODE_API_MODULE(entry, Init)
最终调用与执行结果如下所示:
PS: 这里不确保对应的加解密结果一定能被其他语言或者工具识别,因为国密算法中还涉及很多其他参数,本文实现的代码均只用于验证 OpenSSL 具备国密算法能力,不作为标准结果参考。
总结
本文简单的使用 C++ 实现了使用 OpenSSL 在鸿蒙上的国密算法的加解密过程,也简单验证了下 OpenSSL 对国密算法的支持性,同时解答了一些在社区看到的问题和疑虑,当然真正的使用还需要开发者自行根据业务做细节上的调整。
希望本文对你有所帮助 ~