You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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)释放解密后的明文内存。

验证步骤

  1. 加密单个文件后,手动检查加密文件的结构:确认加密密钥长度、IV长度、密文长度符合预期。
  2. 在循环解密时,打印每个文件的encrypted_key_len、IV值(十六进制)、ciphertext_len,对比加密时的参数是否一致。
  3. 使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 23:40:26