直接用 OpenSSL 的 EVP_aes_256_gcm 容易解密失败,核心是 IV、auth_tag、密文三者必须严格分离且长度/顺序不可错:IV 推荐 12 字节随机唯一,auth_tag 固定 16 字节需调用 EVP_CIPHER_CTX_ctrl 获取,三者应按“长度+内容”二进制打包,禁用字符串拼接或分隔符。

为什么直接用 OpenSSL 的 EVP_aes_256_gcm 容易解密失败
核心问题不是算法调用不对,而是 GCM 模式下 IV(也叫 nonce)、auth_tag、密文三者必须严格分离且顺序/长度不可错。OpenSSL 不自动保存或校验它们,你写进文件时若混在一起、少写一个、长度没对齐(比如 IV 用了 12 字节但解密时传了 16),EVP_DecryptFinal_ex 就会返回 0,且不报具体错误。
常见错误现象:EVP_DecryptFinal_ex 返回 0,但 ERR_get_error() 拿不到有意义的错误码;或者解密后明文乱码但长度正常——大概率是 auth_tag 校验失败被静默丢弃。
- IV 必须唯一且随机,推荐 12 字节(96 bit),不是越长越好;用
RAND_bytes生成,别手写或复用 - auth_tag 固定 16 字节,必须在加密后立即用
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag)取出,不能从密文末尾“截取” - 加密前必须先设置 IV(
EVP_EncryptInit_ex第四个参数),再设置密钥;顺序反了会导致内部状态错乱
如何安全地把 IV、tag、密文打包进一个文件
不要拼接字符串或靠分隔符(比如 "|"),二进制场景下任何可打印分隔符都可能出现在密文里。标准做法是「固定头 + 原始密文」:头里放 IV 长度(1 字节)、IV 内容、tag 长度(1 字节)、tag 内容,后面紧跟密文。
示例结构(共 12+16+N 字节):[1 byte: 12][12 bytes: IV][1 byte: 16][16 bytes: auth_tag][N bytes: ciphertext]
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
- 写文件时用
fwrite顺序写入,别用std::ofstream ,后者可能触发文本模式换行转换 - 读文件时先读 1 字节得 IV 长度,再读对应字节数到 buffer,再同理读 tag 长度和 tag,最后剩余全是密文
- 如果未来要跨平台或和 Python/Java 互通,头结构必须文档化并保持一致;别依赖 OpenSSL 默认行为
解密时 EVP_CIPHER_CTX 要重置哪些状态
同一个 EVP_CIPHER_CTX* 对象不能复用做多次解密——GCM 内部有计数器和认证状态,不清除会导致第二次解密必然失败(EVP_DecryptUpdate 返回 0)。
立即学习“C++免费学习笔记(深入)”;
- 每次解密前必须调用
EVP_CIPHER_CTX_reset(ctx),不是EVP_CIPHER_CTX_cleanup(已废弃) - reset 后要重新调用
EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, key, iv),key 和 iv 都得重传 - IV 长度必须和加密时完全一致,哪怕只差 1 字节,
EVP_DecryptInit_ex也会成功返回,但后续操作全失效 - 别省略
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, tag)这一步,它必须在EVP_DecryptUpdate之前调用
C++ RAII 封装时最容易漏掉的资源释放点
OpenSSL 的 GCM 加解密需要手动管理 EVP_CIPHER_CTX* 和 RNG 状态,RAII 封装时容易只管 ctx,忽略 RAND_cleanup() 或误调 OPENSSL_cleanup()(它会干掉全局状态,影响其他模块)。
-
EVP_CIPHER_CTX_free是必须的,但要在EVP_CIPHER_CTX_reset之后再 free,否则可能内存泄漏 - 不要在析构函数里调用
OPENSSL_cleanup(),C++ 程序里通常由 main 结束前统一调一次就够了 - 如果用了
RAND_bytes,不用显式 cleanup;但若自己调了RAND_seed或RAND_load_file,才需考虑RAND_cleanup - 密钥、IV、tag 所用的 buffer 推荐用
std::vector<uint8_t></uint8_t>管理,避免裸指针生命周期错配
最麻烦的其实是错误处理粒度:OpenSSL 的 GCM 失败几乎不告诉你原因,只能靠断点确认 IV/tag 长度、调用顺序、buffer 是否重叠。这些细节一旦写错,表现就是“能加密不能解密”,而且毫无提示。


















