鸿蒙原生应用开发-基于 OpenSSL 的国密算法使用

117 阅读10分钟

本文主要讲解如何在原生鸿蒙上使用 OpenSSL 提供的国密算法进行加解密操作。

本文的示例代码在:sm-example

接入 OpenSSL

作者已经在本文发布前实现了对 OpenSSL 的预构建以及包封装等操作,如果在没有任何需要定制化构建或者其他需求场景的情况下,可以直接使用已经封装好的包。

本文将使用 C++ 作为开发语言,来简单讲解如何使用 OpenSSL 提供的国密算法进行加解密操作。

桥接能力,将基于 node-addon-api-ohos 实现,具体使用可参考 node-addon-api-ohos

依旧是创建一个 C++ 模块的原生模块模板,我们开始尝试接入 OpenSSL。

17302004766537.jpg

创建完成之后,我们在 entry 目录下通过 ohpm 安装我们已经处理好的包。

ohpm install @ohos-rs/openssl --save-dev

17302025449503.jpg

紧接着在 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);
}
}

整体逻辑相对来比较清晰,我们可以分为以下几个步骤实现:

  1. OpenSSL 环境初始化
  2. 加载公私钥
  3. 创建和初始化加解密上下文
  4. 加密/解密
  5. 资源释放

在社区和一些渠道反馈说,该包并没有提供 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)

最终调用与执行结果如下所示:

17302721421136.jpg

PS: 这里不确保对应的加解密结果一定能被其他语言或者工具识别,因为国密算法中还涉及很多其他参数,本文实现的代码均只用于验证 OpenSSL 具备国密算法能力,不作为标准结果参考。

总结

本文简单的使用 C++ 实现了使用 OpenSSL 在鸿蒙上的国密算法的加解密过程,也简单验证了下 OpenSSL 对国密算法的支持性,同时解答了一些在社区看到的问题和疑虑,当然真正的使用还需要开发者自行根据业务做细节上的调整。

希望本文对你有所帮助 ~