解析OpenSSL AES-256-CBC加解密的IV异常问题
AES-256-CBC加解密异常:不同IV仍能解密的原因及修复
问题现象
- 加密时生成随机IV,解密时生成独立随机IV,未传递同一IV,但解密后原输入内容一致
- 加密前拼接16字节固定前缀,解密后前16字节为乱码,去掉后得到原文本;前缀改为20字节仍能正常解密,改为小于16字节则解密结果异常
原因分析
- OpenSSL BIO_cipher自动写入IV:你使用的
BIO_f_cipher默认会自动将加密时的IV写入密文开头,解密时BIO会自动读取密文开头的IV用于解密,你在解密函数中生成的随机IV实际上被忽略了——这就是不同IV仍能解密的核心原因。 - CBC分组特性影响:AES-CBC以16字节为一个分组加密:
- 前缀为16字节时,刚好是第一个完整分组,解密时该分组结果会和加密IV异或得到乱码,但后续分组依赖前一个密文块作为IV,解密链不受影响,去掉前16字节后原文本正常。
- 前缀为20字节时,前16字节是第一个分组,剩下4字节和原文本开头组成第二个分组,乱码仅存在于前16字节,去掉后原文本完整。
- 前缀小于16字节时,前缀和原文本前半部分组成第一个分组,解密后的乱码会覆盖原文本开头内容,导致结果异常。
修复方案
方案1:保留BIO自动处理IV的行为(推荐)
解密时不再生成随机IV,让BIO自动读取密文开头的IV:
int aes_256_cbc_decrypt(const string& text, const unsigned char* key, string& output) { if (!all_ciphers_added) { OpenSSL_add_all_ciphers(); all_ciphers_added = true; } const EVP_CIPHER* cipher = EVP_get_cipherbyname(THE_ALGORITHM); if (cipher == NULL) { return -1; } BIO* memory = BIO_new_mem_buf((void*)text.c_str(), -1); if (memory == NULL) { return -1; } BIO* base64 = BIO_new(BIO_f_base64()); if (base64 == NULL) { BIO_free(memory); return -1; } BIO_set_flags(base64, BIO_FLAGS_BASE64_NO_NL); BIO* decryptor = BIO_new(BIO_f_cipher()); if (decryptor == NULL) { BIO_free_all(base64); BIO_free(memory); return -1; } // 不再生成随机IV,传NULL让BIO自动读取密文开头的IV BIO_set_cipher(decryptor, cipher, key, NULL, 0); BIO_push(decryptor, base64); BIO_push(base64, memory); char buff[BUFF_SIZE+1]; int len; while ((len = BIO_read(decryptor, buff, BUFF_SIZE)) > 0) { buff[len] = '\0'; output += buff; } BIO_free_all(decryptor); if (len == 0) { if (output.size() > 16) { // 此时前16字节是加密时拼接的原前缀,不再是乱码 output = output.substr(16); } return 0; } else { // len < 0 output.clear(); return -1; } }
方案2:禁用BIO自动写入IV,手动传递IV
如果需要完全控制IV的存储和传递,可禁用BIO自动写入IV的功能,手动将IV和密文绑定:
// 加密函数修改BIO_set_cipher相关逻辑 int aes_256_cbc_encrypt(const string& text, const unsigned char* key, string& output) { // ... 原有代码 ... unsigned char iv[AES_CBC_IV_LENGTH]; RAND_bytes(iv, AES_CBC_IV_LENGTH); BIO_set_cipher(encryptor, cipher, key, iv, 1); // 禁用自动写入IV BIO_ctrl(encryptor, BIO_CTRL_SET_CIPHER_FLAGS, EVP_CIPH_NO_PADDING, NULL); // ... 原有代码 ... // 手动将IV写入内存BIO的开头 BIO_write(memory, iv, AES_CBC_IV_LENGTH); // 再写入加密后的数据 if (BIO_write(encryptor, data, data_len) != data_len) { BIO_free_all(encryptor); free(data); return -1; } // ... 原有代码 ... } // 解密函数需要先读取开头的IV,再设置给BIO int aes_256_cbc_decrypt(const string& text, const unsigned char* key, string& output) { // ... 原有代码 ... // 先从base64解码后的内容中读取IV unsigned char iv[AES_CBC_IV_LENGTH]; BIO_read(base64, iv, AES_CBC_IV_LENGTH); // 再设置IV到解密BIO BIO_set_cipher(decryptor, cipher, key, iv, 0); // ... 原有代码 ... }
验证说明
修复后,解密时前16字节会还原为加密时拼接的原前缀,去掉后得到原文本,且前缀长度小于16字节时的异常也会消失。
内容的提问来源于stack exchange,提问作者Miko Nahum
相关产品推荐
相关产品推荐

