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

使用OpenSSL EVP API解密时写入垃圾值及bad decrypt问题排查

问题分析与解决:AES-256-CBC解密归档文件的两类问题

问题背景

开发一款基于C语言+OpenSSL EVP API的归档加解密程序,实现AES-256-CBC解密时遇到两个核心问题:

  • 当文件剩余待解密字节数小于读取缓冲区大小时,输出文件会写入垃圾值;
  • 将读取缓冲区大小inLen改为block_size后,触发OpenSSL的"bad decrypt"错误,且仅在文件大小为block_size倍数且多个此类文件归档时出现。

解密核心代码

int TARIM_decrypt_aes256(FILE* infile, FILE* outfile, unsigned char* key, unsigned char* iv, unsigned long long int fileLoc, unsigned long int fileSize)
{
    if (infile == NULL || outfile == NULL || key == NULL || iv == NULL)
    {
        printf("(ERROR) decrypt_aes256: One/more parameters are NULL pointers\n");
        return 1;
    }

    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
    if (ctx == NULL)
    {
        printf("(ERROR) decrypt_aes256: Failed to generate cipher context. OpenSSL: %s\n", ERR_error_string(ERR_get_error(), NULL));
        return 1;
    }
    EVP_CIPHER* cipher = EVP_CIPHER_fetch(NULL, "AES-256-CBC", NULL);
    if (cipher == NULL)
    {
        printf("(ERROR) decrypt_aes256: Failed to fetch cipher. OpenSSL: %s\n", ERR_error_string(ERR_get_error(), NULL));

        EVP_CIPHER_CTX_free(ctx);
        return 1;
    }

    // Explicitly Enable Padding
    EVP_CIPHER_CTX_set_padding(ctx, 1);

    int num_read = 0;
    int block_size = EVP_CIPHER_block_size(cipher);
    int inLen = 1024;
    int outLen = inLen + block_size;
    unsigned char inbuffer[inLen], outbuffer[outLen]; // Allow space for additional block in outbuffer
    
    if (!EVP_CipherInit_ex2(ctx, cipher, NULL, NULL, 0, NULL))
    {
        printf("(ERROR) decrypt_aes256: Failed to Initiate Cipher. OpenSSL: %s\n", ERR_error_string(ERR_get_error(), NULL));
        
        EVP_CIPHER_free(cipher);
        EVP_CIPHER_CTX_free(ctx);
        return 1;
    }

    // Assert size of key and iv
    OPENSSL_assert(EVP_CIPHER_CTX_key_length(ctx) == 32);
    OPENSSL_assert(EVP_CIPHER_CTX_iv_length(ctx) == 16);

    // Set key and iv
    if (!EVP_CipherInit_ex2(ctx, NULL, key, iv, 0, NULL))
    {
        printf("(ERROR) decrypt_aes256: Failed to set key and iv. OpenSSL: %s\n", ERR_error_string(ERR_get_error(), NULL));
        
        EVP_CIPHER_free(cipher);
        EVP_CIPHER_CTX_free(ctx);
        return 1;
    }

    // Seek to File Location
    fseek(infile, fileLoc, SEEK_SET);

    // Loop until bytes read
    unsigned long long int b_count = 0;
    while (1)
    {
        num_read = fread(inbuffer, sizeof(unsigned char), inLen, infile);
        b_count += num_read;

        if (!EVP_CipherUpdate(ctx, outbuffer, &outLen, inbuffer, num_read))
        {
            printf("(ERROR) decrypt_aes256: Failed to pass bytes to the cipher. OpenSSL: %s\n", ERR_error_string(ERR_get_error(), NULL));
            
            EVP_CIPHER_free(cipher);
            EVP_CIPHER_CTX_free(ctx);
            return 1;
        }
        fwrite(outbuffer, sizeof(unsigned char), outLen, outfile);

        // EOF
        if (b_count >= fileSize || num_read < inLen)
        { break; }
    }

    // Cipher Final block with padding
    if (!EVP_CipherFinal_ex(ctx, outbuffer, &outLen))
    {
        printf("(ERROR) decrypt_aes256: Failed to pass bytes from final block to cipher. OpenSSL: %s\n", ERR_error_string(ERR_get_error(), NULL));

        EVP_CIPHER_free(cipher);
        EVP_CIPHER_CTX_free(ctx);
        return 1;
    }
    fwrite(outbuffer, sizeof(unsigned char), outLen, outfile);

    // Clean up
    EVP_CIPHER_free(cipher);
    EVP_CIPHER_CTX_free(ctx);
    
    return 0;
}

缓冲区修改后的错误情况

修改inLen为块大小:

int inLen = block_size;

触发错误日志:

(ERROR) decrypt_aes256: Failed to pass bytes from final block to cipher. OpenSSL: error:1C800064:Provider routines::bad decrypt

归档位置计算代码

// Get Relative Location
unsigned long long int relativeLoc = 0;
for (long long int itr = 0; itr < option_num; itr++) {
    if (block_size == 0 || fArray[itr].fsize == 0) {
        relativeLoc += fArray[itr].fsize;
    } else {
        // Adjust for padding
        relativeLoc += ((fArray[itr].fsize / block_size) + 1) * block_size;
    }
}

问题原因与解决方案

问题1:剩余字节小于缓冲区时写入垃圾值

原因:循环中未限制读取字节数,当剩余待解密数据不足缓冲区大小时,fread会读取到后续归档文件的字节(跨文件读取),这些无关字节被送入解密流程后,输出垃圾数据。

解决:每次读取时计算剩余待处理的加密数据长度,限制fread的读取量,避免跨文件读取:

// 循环逻辑修改
unsigned long long int b_count = 0;
while (b_count < fileSize)
{
    // 计算本次最大可读取的字节数,不超过剩余待处理数据
    int read_size = (fileSize - b_count) < inLen ? (fileSize - b_count) : inLen;
    num_read = fread(inbuffer, sizeof(unsigned char), read_size, infile);
    if (num_read <= 0) break;
    b_count += num_read;

    if (!EVP_CipherUpdate(ctx, outbuffer, &outLen, inbuffer, num_read))
    {
        // 错误处理
        return 1;
    }
    fwrite(outbuffer, sizeof(unsigned char), outLen, outfile);
}

问题2:改为block_size触发bad decrypt错误

原因:

  1. 当原始文件大小为block_size倍数时,加密时OpenSSL会自动添加一个完整的填充块(因为启用了padding),所以加密后文件大小是((原始大小/block_size)+1)*block_size;
  2. 解密时传入的fileSize是原始文件大小,导致未读取完整的加密数据(遗漏了填充块),EVP_CipherFinal_ex无法验证填充,触发"bad decrypt";
  3. 用block_size作为缓冲区时,最后一次读取刚好读完原始大小对应的加密数据,没有读取填充块,导致后续Final步骤失败。

解决:

  1. 解密时传入加密后的文件大小(即归档位置计算代码中算出的relativeLoc增量值),确保读取完整的加密数据(包括填充块);
  2. 解密完成后,根据原始文件大小截断输出,或者依赖EVP_CipherFinal_ex自动去掉填充(启用padding时,Final步骤会输出去掉填充后的字节,此时总输出字节数等于原始文件大小)。

关键修正点

  • 归档时记录每个文件的原始大小和加密后大小,解密时分别传入;
  • 读取加密数据时,用加密后大小作为读取终止条件;
  • 写入输出文件时,最终总写入字节数等于原始文件大小。

内容的提问来源于stack exchange,提问作者user21011675

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 03:55:13