加密与安全:OpenSSL跨平台使用、AES/RSA封装、哈希算法
说实话,加密这块内容,很多C++开发者要么直接跳过,要么用的时候从网上拷一段代码就跑。我早年也这么干过,直到有一次在Linux上编译好好的代码,到了Windows上直接崩了——嗯,就是因为OpenSSL的库链接方式不一样。从那以后,我就老老实实把跨平台加密封装这件事给捋清楚了。
今天这一章,咱们就聊聊怎么在C++里把OpenSSL用顺手,把AES、RSA和哈希算法封装成跨平台的接口。说白了,就是让你写一次加密代码,Windows、Linux、macOS都能跑。
一、OpenSSL的跨平台坑与对策
OpenSSL本身是跨平台的,但它的编译和链接方式在不同系统上差异很大。我个人习惯的做法是:不要直接链接系统自带的OpenSSL,而是自己编译一个静态库,或者用vcpkg/conan来管理。
跨平台编译的推荐方案:
- Windows:用vcpkg安装
openssl-windows,或者从源码编译(需要安装Perl和NASM) - Linux:用包管理器安装
libssl-dev(Ubuntu)或openssl-devel(CentOS) - macOS:用Homebrew安装
openssl@3,然后通过pkg-config获取编译参数
下面是我常用的CMake配置片段,可以自动处理不同平台的OpenSSL路径:
find_package(OpenSSL REQUIRED)
if(OpenSSL_FOUND)
include_directories(${OPENSSL_INCLUDE_DIR})
target_link_libraries(my_project ${OPENSSL_LIBRARIES})
endif()
# 针对macOS的特殊处理
if(APPLE)
set(OPENSSL_ROOT_DIR "/usr/local/opt/openssl@3")
endif()
二、AES对称加密封装
AES是目前最常用的对称加密算法。我一般用AES-256-GCM模式,因为它既加密又带认证,安全性比ECB/CBC高不少。
先看一个简单的AES加密封装类:
#include <openssl/evp.h>
#include <vector>
#include <string>
class AesEncryptor {
public:
// 加密:返回密文(包含IV和tag)
std::vector<unsigned char> encrypt(
const std::vector<unsigned char>& plaintext,
const std::vector<unsigned char>& key) {
// 生成随机IV(12字节)
std::vector<unsigned char> iv(12);
RAND_bytes(iv.data(), iv.size());
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr,
key.data(), iv.data());
std::vector<unsigned char> ciphertext(plaintext.size() + 16);
int len = 0, ciphertext_len = 0;
EVP_EncryptUpdate(ctx, ciphertext.data(), &len,
plaintext.data(), plaintext.size());
ciphertext_len = len;
EVP_EncryptFinal_ex(ctx, ciphertext.data() + len, &len);
ciphertext_len += len;
// 获取认证标签
std::vector<unsigned char> tag(16);
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag.data());
EVP_CIPHER_CTX_free(ctx);
// 返回格式:IV + 密文 + tag
ciphertext.resize(ciphertext_len);
ciphertext.insert(ciphertext.end(), iv.begin(), iv.end());
ciphertext.insert(ciphertext.end(), tag.begin(), tag.end());
return ciphertext;
}
// 解密:从密文中提取IV和tag
std::vector<unsigned char> decrypt(
const std::vector<unsigned char>& ciphertext,
const std::vector<unsigned char>& key) {
// 解析IV、密文、tag
auto iv_start = ciphertext.end() - 28;
auto tag_start = ciphertext.end() - 16;
std::vector<unsigned char> iv(iv_start, iv_start + 12);
std::vector<unsigned char> tag(tag_start, ciphertext.end());
std::vector<unsigned char> actual_cipher(ciphertext.begin(), iv_start);
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr,
key.data(), iv.data());
std::vector<unsigned char> plaintext(actual_cipher.size());
int len = 0, plaintext_len = 0;
EVP_DecryptUpdate(ctx, plaintext.data(), &len,
actual_cipher.data(), actual_cipher.size());
plaintext_len = len;
// 设置期望的tag
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, tag.data());
// 验证并完成解密
int ret = EVP_DecryptFinal_ex(ctx, plaintext.data() + len, &len);
if (ret <= 0) {
EVP_CIPHER_CTX_free(ctx);
throw std::runtime_error("解密失败:数据可能被篡改");
}
plaintext_len += len;
EVP_CIPHER_CTX_free(ctx);
plaintext.resize(plaintext_len);
return plaintext;
}
};
三、RSA非对称加密封装
RSA适合加密小数据(比如AES的密钥),或者做数字签名。我一般用RSA-2048,密钥长度和性能之间比较平衡。
下面是一个RSA密钥对生成和加密的封装:
#include <openssl/rsa.h>
#include <openssl/pem.h>
class RsaEncryptor {
public:
// 生成密钥对,返回PEM格式的字符串
static std::pair<std::string, std::string> generateKeyPair() {
EVP_PKEY* pkey = EVP_PKEY_new();
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr);
EVP_PKEY_keygen_init(ctx);
EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048);
EVP_PKEY_keygen(ctx, &pkey);
// 提取公钥和私钥(PEM格式)
BIO* bio_priv = BIO_new(BIO_s_mem());
BIO* bio_pub = BIO_new(BIO_s_mem());
PEM_write_bio_PrivateKey(bio_priv, pkey, nullptr, nullptr, 0, nullptr, nullptr);
PEM_write_bio_PUBKEY(bio_pub, pkey);
std::string priv_key = bioToString(bio_priv);
std::string pub_key = bioToString(bio_pub);
BIO_free_all(bio_priv);
BIO_free_all(bio_pub);
EVP_PKEY_CTX_free(ctx);
EVP_PKEY_free(pkey);
return {priv_key, pub_key};
}
// 使用公钥加密
static std::vector<unsigned char> encrypt(
const std::vector<unsigned char>& data,
const std::string& pubKeyPem) {
BIO* bio = BIO_new_mem_buf(pubKeyPem.data(), pubKeyPem.size());
EVP_PKEY* pkey = PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr);
BIO_free(bio);
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(pkey, nullptr);
EVP_PKEY_encrypt_init(ctx);
size_t outlen = 0;
EVP_PKEY_encrypt(ctx, nullptr, &outlen, data.data(), data.size());
std::vector<unsigned char> encrypted(outlen);
EVP_PKEY_encrypt(ctx, encrypted.data(), &outlen, data.data(), data.size());
encrypted.resize(outlen);
EVP_PKEY_CTX_free(ctx);
EVP_PKEY_free(pkey);
return encrypted;
}
private:
static std::string bioToString(BIO* bio) {
char* data = nullptr;
long len = BIO_get_mem_data(bio, &data);
return std::string(data, len);
}
};
四、哈希算法封装
哈希算法我常用SHA-256和SHA-3。OpenSSL的EVP接口对哈希的支持非常统一,封装起来很简洁:
#include <openssl/evp.h>
class HashUtil {
public:
// 计算SHA-256哈希
static std::vector<unsigned char> sha256(
const std::vector<unsigned char>& data) {
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr);
EVP_DigestUpdate(ctx, data.data(), data.size());
std::vector<unsigned char> hash(EVP_MAX_MD_SIZE);
unsigned int hash_len = 0;
EVP_DigestFinal_ex(ctx, hash.data(), &hash_len);
EVP_MD_CTX_free(ctx);
hash.resize(hash_len);
return hash;
}
// 计算文件的哈希(适合大文件)
static std::vector<unsigned char> sha256File(const std::string& filepath) {
FILE* fp = fopen(filepath.c_str(), "rb");
if (!fp) throw std::runtime_error("无法打开文件");
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr);
unsigned char buffer[8192];
size_t bytes_read = 0;
while ((bytes_read = fread(buffer, 1, sizeof(buffer), fp)) > 0) {
EVP_DigestUpdate(ctx, buffer, bytes_read);
}
std::vector<unsigned char> hash(EVP_MAX_MD_SIZE);
unsigned int hash_len = 0;
EVP_DigestFinal_ex(ctx, hash.data(), &hash_len);
EVP_MD_CTX_free(ctx);
fclose(fp);
hash.resize(hash_len);
return hash;
}
// 将哈希转为十六进制字符串(方便显示)
static std::string toHex(const std::vector<unsigned char>& hash) {
std::string hex;
const char* hex_chars = "0123456789abcdef";
for (auto byte : hash) {
hex += hex_chars[(byte >> 4) & 0x0F];
hex += hex_chars[byte & 0x0F];
}
return hex;
}
};
mmap 映射一个10GB的文件然后算哈希——结果内存直接爆了。用分块读取的方式,缓冲区设成8KB或16KB就够用了。
五、知识体系总览
下面这张图把本章的核心知识点串起来了,你可以看到加密、解密、签名、验证之间的关系:
六、跨平台封装的几个关键点
把上面这些封装成跨平台库,我总结了几个要点:
| 要点 | 说明 | 我的建议 |
|---|---|---|
| 内存管理 | OpenSSL大量使用C风格的内存分配,容易泄漏 | 用RAII包装所有OpenSSL对象,比如 std::unique_ptr 配合自定义删除器 |
| 错误处理 | OpenSSL的错误码很分散,不同函数返回方式不同 | 统一封装成异常,用 ERR_get_error() 获取详细错误信息 |
| 线程安全 | OpenSSL 1.1.0+ 默认线程安全,但旧版本需要自己加锁 | 强制要求OpenSSL版本 ≥ 1.1.0,否则编译时报错 |
| 密钥安全 | 私钥在内存中可能被转储 | 使用完密钥后,用 OPENSSL_cleanse() 擦除内存,不要用 memset |
🔑 核心总结:
- AES用GCM模式,自带认证,安全性高
- RSA只用来加密小数据或签名,大文件用混合加密
- 哈希用SHA-256,分块计算大文件
- 所有OpenSSL对象用RAII管理,避免内存泄漏
- 跨平台编译用CMake + vcpkg/conan,不要依赖系统自带版本
嗯,加密这块内容其实不难,但细节特别多。我当年第一次封装RSA时,忘了处理PEM格式的换行符,结果在Windows上解析公钥一直失败——折腾了两天才发现是换行符的问题。所以,写加密代码时,一定要在多个平台上做完整的单元测试,尤其是密钥的导入导出环节。