EVP envelope_open()解密异常:仅能解密目录/向量中最后一个文件
问题分析与解决方案
核心问题定位
多文件解密时仅最后一个成功,其余在EVP_OpenFinal()失败,本质是解密参数不匹配或循环调用时的资源/数据复用错误:
EVP_OpenFinal()失败通常是由于解密后的明文填充校验不通过,根源是传入的加密密钥、IV或密文本身不正确。- 单个文件解密正常,说明加密逻辑和单文件解密逻辑可靠,问题出在多文件循环处理的参数传递或数据读取环节。
可能的错误点及修复
1. 多文件复用同一密钥/IV
每个加密文件必须使用独立的随机AES密钥和IV,解密时也必须对应每个文件的专属密钥(加密后)和IV。如果循环中复用了之前的密钥/IV缓冲区但未重新读取新数据,会导致解密参数不匹配。
修复:
- 加密阶段:确保每个文件都重新生成随机AES密钥和IV,再用公钥加密密钥后,和IV、密文一起写入文件。
- 解密阶段:循环处理每个文件时,重新读取当前文件的加密密钥、IV和密文,不要复用之前的缓冲区数据(或确保读取时完全覆盖旧数据)。
2. 加密数据读取长度错误
解密时需要严格按照加密时的写入顺序读取数据:先读加密后的密钥(长度为EVP_PKEY_size(priv_key)),再读16字节的IV,最后读取剩余的密文。如果读取长度计算错误(比如误算密文长度、IV读取不足16字节),会导致EVP_OpenFinal()校验失败。
修复:
- 读取加密文件时,明确各部分的长度:
// 示例:读取加密文件的结构 int encrypted_key_len = EVP_PKEY_size(priv_key); unsigned char *encrypted_key = malloc(encrypted_key_len); unsigned char iv[16]; // 读取加密密钥 if (fread(encrypted_key, 1, encrypted_key_len, in_file) != encrypted_key_len) { // 读取错误处理 } // 读取IV if (fread(iv, 1, sizeof(iv), in_file) != sizeof(iv)) { // 读取错误处理 } // 计算并读取密文长度 fseek(in_file, 0, SEEK_END); long file_size = ftell(in_file); fseek(in_file, encrypted_key_len + sizeof(iv), SEEK_SET); int ciphertext_len = file_size - encrypted_key_len - sizeof(iv); unsigned char *ciphertext = malloc(ciphertext_len); if (fread(ciphertext, 1, ciphertext_len, in_file) != ciphertext_len) { // 读取错误处理 }
3. 解密函数的错误处理过于激进
原代码中handleErrors()直接调用abort(),会导致单个文件解密失败时程序直接终止,无法处理后续文件。同时,错误信息的输出不够清晰,不利于定位问题。
修复:修改envelope_open()函数,移除abort(),改为返回错误码并保留详细错误日志:
void handleErrors(void) { ERR_print_errors_fp(stderr); } int envelope_open(EVP_PKEY *priv_key, unsigned char *ciphertext, int ciphertext_len, unsigned char *encrypted_key, int encrypted_key_len, unsigned char *iv, unsigned char **plaintext, int *plaintext_len) { EVP_CIPHER_CTX *ctx = NULL; int len = 0, ret = 0; unsigned char *tmpptxt = NULL; // 初始化输出参数 *plaintext = NULL; *plaintext_len = 0; if((ctx = EVP_CIPHER_CTX_new()) == NULL) { fprintf(stderr, "Failed to create EVP_CIPHER_CTX\n"); handleErrors(); goto err; } if ((tmpptxt = (unsigned char*)malloc(ciphertext_len)) == NULL) { fprintf(stderr, "Failed to allocate memory for plaintext\n"); goto err; } if(EVP_OpenInit(ctx, EVP_aes_256_cbc(), encrypted_key, encrypted_key_len, iv, priv_key) != 1) { fprintf(stderr, "EVP_OpenInit failed\n"); handleErrors(); goto err; } if(EVP_OpenUpdate(ctx, tmpptxt, &len, ciphertext, ciphertext_len) != 1) { fprintf(stderr, "EVP_OpenUpdate failed\n"); handleErrors(); goto err; } *plaintext_len = len; if(EVP_OpenFinal(ctx, tmpptxt + len, &len) != 1) { fprintf(stderr, "EVP_OpenFinal failed (padding verification failed, check key/IV/ciphertext)\n"); handleErrors(); goto err; } *plaintext_len += len; // 转移内存所有权给调用方 *plaintext = tmpptxt; tmpptxt = NULL; ret = 1; err: // 清理资源 if(ctx) EVP_CIPHER_CTX_free(ctx); free(tmpptxt); return ret; }
4. 内存泄漏或重复释放
原代码中虽然逻辑上不会重复释放,但在循环调用时如果调用方未正确释放*plaintext,会导致内存耗尽,间接影响后续文件的内存分配。
修复:在循环处理每个文件后,务必调用free(plaintext)释放解密后的明文内存。
验证步骤
- 加密单个文件后,手动检查加密文件的结构:确认加密密钥长度、IV长度、密文长度符合预期。
- 在循环解密时,打印每个文件的
encrypted_key_len、IV值(十六进制)、ciphertext_len,对比加密时的参数是否一致。 - 使用OpenSSL命令行工具手动解密单个失败的文件,验证是否能正常解密:
如果命令行能解密成功,说明代码中的参数传递或数据读取存在问题;如果命令行也失败,说明加密时的文件写入逻辑有问题。# 提取加密密钥、IV、密文后,用OpenSSL解密 openssl rsautl -decrypt -inkey private.key -in encrypted_key.bin -out aes_key.bin openssl enc -d -aes-256-cbc -iv <IV_HEX> -K <AES_KEY_HEX> -in ciphertext.bin -out plaintext.txt
内容的提问来源于stack exchange,提问作者Jason Smith
相关产品推荐
相关产品推荐

