从 OpenSSL 1.0.2b 迁移到 3.3.0 后,由于填充,AES-128-ECB 解密“失败”

问题描述 投票:0回答:1

我有一个测试程序,在链接到 OpenSSL 1.0.2b 时可以工作,但在链接到 OpenSSL 3.0.0 和 OpenSSL 3.3.0 时失败。我认为问题出在加密方面,因为解密以编程方式成功(不返回错误),但明文仍然包含填充。

如果输入长度可被块大小整除,则将具有预期值的填充块添加到恢复的明文中(但在解密后不会被剥离)。明文是一个比预期长的完整块,还有一个可预测的额外块。预期值始终是填充字节的计数,因此对于完整的填充块,您将获得 16 个字节的 0x10 (16)。

例如:明文以偶数块结尾:

...                      0x01 0x02 0x03

恢复的明文结尾为:

...                      0x01 0x02 0x03

0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10

0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10

如果输入长度不能被块大小整除,则明文的倒数第二个块将用最终明文填充(例如,如果有 1 个字节不能被块大小整除并且该字节设置为 0x31,您会看到整个 0x31 块),然后是另一个具有预期值的填充块(解密后仍然没有被删除)。明文比预期长超过一个块(但少于两个完整块),并且在不知道原始明文长度的情况下是不可预测的。

例如:明文以不完整的块结尾:

0x01

恢复的明文以填充该块加上正常的填充块结束:

0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01

0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01

0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10

0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10

我尝试过显式启用填充、显式禁用填充以及预取密码上下文,但似乎没有什么区别。禁用填充后,我在最后没有得到 0x10 的完整块,但当输入大小不能被块大小整除时,我仍然得到剩余的填充。

密钥为 16 字节(128 位),输入和输出缓冲区的大小至少比必要的大 2 个块。

我不敢相信我是唯一一个看到这一点的人,除非每个人都能够摆脱 ECB(不过,我需要解密已经存在的文件)。

这是加密:

// Encrypt data_length bytes of data into enc_data using key
EVP_CIPHER_CTX* ctx{EVP_CIPHER_CTX_new()};
if ( !ctx ) { 
    return -1;
}

EVP_CIPHER* cipher{EVP_CIPHER_fetch(nullptr, "AES-128-ECB", nullptr)};
if ( !cipher ) {
    return -1;
}

// Explicitly enable padding - doesn't seem to matter
int padding{1};
OSSL_PARAM params[2];
params[0] = OSSL_PARAM_construct_int(OSSL_CIPHER_PARAM_PADDING,&padding);
params[0] = OSSL_PARAM_construct_end();

if ( EVP_EncryptInit_ex2(ctx,cipher,key,nullptr,params) != 1 ) {
    return -1;
}

int input_ptr{0};
int output_ptr{0};
// Encrypt until the output_ptr meets or exceeds the input data_length
while ( output_ptr < data_length ) {
    int this_enc_len{0};
    if ( EVP_EncryptUpdate(ctx,reinterpret_cast<unsigned char*>(enc_data) + output_ptr,&this_enc_len,reinterpret_cast<unsigned char*>(data) + input_ptr, data_length - input_ptr) != 1 ) {
        return -1;
    }
    input_ptr += this_enc_len;
    output_ptr += this_enc_len;
}

int final_enc_len{0};
if ( EVP_EncryptFinal_ex(ctx,reintrepret_cast<unsigned char*>(enc_data) + output_ptr,&final_enc_len) != 1 ) {
    return -1;
}
output_ptr += final_enc_len;

// Cleanup omitted for brevity
return output_ptr;

无论显式启用还是禁用,始终会添加填充。可以被块大小整除的输入大小不会被填充;否则它们将被填充到偶数 16 字节长度。

解密类似:

// Decrypt data_length bytes of data into dec_data using key
EVP_CIPHER_CTX* ctx{EVP_CIPHER_CTX_new()};
if ( !ctx ) { 
    return -1;
}

EVP_CIPHER* cipher{EVP_CIPHER_fetch(nullptr, "AES-128-ECB", nullptr)};
if ( !cipher ) {
    return -1;
}

// Explicitly enable padding - doesn't seem to matter here either
int padding{1};
OSSL_PARAM params[2];
params[0] = OSSL_PARAM_construct_int(OSSL_CIPHER_PARAM_PADDING,&padding);
params[0] = OSSL_PARAM_construct_end();

if ( EVP_DecryptInit_ex2(ctx,cipher,key,nullptr,params) != 1 ) {
    return -1;
}

int input_ptr{0};
int output_ptr{0};
while ( output_ptr < data_length ) {
    int this_enc_len{0};
    if ( EVP_DecryptUpdate(ctx,reinterpret_cast<unsigned char*>(dec_data) + output_ptr,&this_enc_len,reinterpret_cast<unsigned char*>(data) + input_ptr, data_length - input_ptr) != 1 ) {
        return -1;
    }
    input_ptr += this_enc_len;
    output_ptr += this_enc_len;
}

int final_enc_len{0};
if ( EVP_DecryptFinal_ex(ctx,reinterpret_cast<unsigned char*>(dec_data) + output_ptr,&final_enc_len) != 1 ) {
    return -1;
}
output_ptr += final_enc_len;

// Cleanup again omitted for brevity
return output_ptr;

还有其他人看过这个吗?您想出解决方案或解决方法吗?

c++ encryption openssl padding
1个回答
0
投票

不,这在语义上是不正确的。您可以使用 ECB + 填充进行加密,直到将所有输入字节传递给该方法。您绝对应该将循环与创建的输出大小分离。最后的调用将添加填充,因此您可能会陷入该循环。在解密过程中,您会遇到同样的问题,但更糟糕的是,输出大小(即明文大小)永远不会超过更大的密文大小。最后,填充应添加 1 到 n 个字节,其中 n 是以字节为单位的块大小(即 AES 的 1 到 16)。

让我们创建一个心理图片(它跳过一些明显的技巧来加速多块加密)。发生的情况是,您在“更新”期间将字节传递到缓冲区,如果缓冲区中有一个完整的块,那么它将被加密。当所有字节都已放入缓冲区时,缓冲区中就有 0 到 n - 1 个字节。现在,填充已添加到缓冲区中,并且在调用“final”方法时对最后一个块进行加密。 这意味着在调用final之前,最后的字节可能永远不会被输出。因此,即使密文总是大于明文(由于块边界和填充),“更新”方法可能永远不会“看到”这种情况,因为缓冲区仍然保存着最终块的数据。

同样,在解密过程中,您将在更新过程中将所有数据传递到缓冲区中。如果缓冲区已满,则不能直接返回,因为数据很可能是填充,不应该返回。这就是为什么您只会在调用“final”期间取回最终数据,因为这是解密例程知道它已被赋予带有填充的最终块的唯一方法。太有趣了,你必须在解密过程中缓冲更多。

TL;DR:只需关注放入算法实现中的字节,并在可能的情况下为最后一个数据块调用“最终”方法。或者用零输入字节调用它,因为实现将简单地从缓冲区检索最终块的数据。

© www.soinside.com 2019 - 2024. All rights reserved.