EVP_DecryptFinal_ex 中用于解密的最终块的正确格式是什么?

What is the correct format for final block in EVP_DecryptFinal_ex for decryption?

出于学习目的,我已经实现了一个简单的 AES-256-GCM 加密和解密。在测试我的代码时,如果我输入的字符串长度是 6 的倍数,那么我会得到正确的输出,但对于其他情况,解密后的数据会附加一些垃圾字符。

Case1:
Enter string: abcdef
Enter key: sdasdasdsa
-^%�
abcdef
6

Case2:
Enter string: abcdefghi
Enter key: sadsadsad
\h�,�[�
abcdefghi�\�
-1

现在我在 https://www.openssl.org/docs/crypto/EVP_EncryptFinal_ex.html 上看到

EVP_DecryptFinal() will return an error code if padding is enabled and the
final block is not correctly formatted.

但是由于在这种情况下默认启用填充,我猜测问题出在最终块的正确格式上。我在下面附上了我的代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>

void handleErrors()
{
    printf("Some error occured\n");
}

int encrypt(unsigned char *plaintext, int plaintext_len, unsigned char *aad,
    int aad_len, unsigned char *key, unsigned char *iv,
    unsigned char *ciphertext, unsigned char *tag)
{
    EVP_CIPHER_CTX *ctx;

    int len, ciphertext_len=0;

    /* Create and initialise the context */
    if(!(ctx = EVP_CIPHER_CTX_new()))
        handleErrors();

    /* Initialise the encryption operation. */
    if(1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL))
        handleErrors();

    /* Set IV length if default 12 bytes (96 bits) is not appropriate */
    if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 16, NULL))
        handleErrors();

    /* Initialise key and IV */
    if(1 != EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv)) handleErrors();

    /* Provide any AAD data. This can be called zero or more times as
     * required
     */
    if(1 != EVP_EncryptUpdate(ctx, NULL, &len, aad, aad_len))
        handleErrors();

    /* Provide the message to be encrypted, and obtain the encrypted output.
     * EVP_EncryptUpdate can be called multiple times if necessary
     */
    if(1 != EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len))
        handleErrors();
    ciphertext_len+= len;

    /* Finalise the encryption. Normally ciphertext bytes may be written at
     * this stage, but this does not occur in GCM mode
     */
    if(1 != EVP_EncryptFinal_ex(ctx, ciphertext + len, &len)) handleErrors();
    ciphertext_len += len;

    /* Get the tag */
    if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag))
        handleErrors();

    /* Clean up */
    EVP_CIPHER_CTX_free(ctx);

    return ciphertext_len;
}


int decrypt(unsigned char *ciphertext, int ciphertext_len, unsigned char *aad,
    int aad_len, unsigned char *tag, unsigned char *key, unsigned char *iv,
    unsigned char *plaintext)
{
    EVP_CIPHER_CTX *ctx;
    int len, plaintext_len=0, ret;

    /* Create and initialise the context */
    if(!(ctx = EVP_CIPHER_CTX_new())) 
        handleErrors();

    /* Initialise the decryption operation. */
    if(!EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL))
        handleErrors();

    /* Set IV length. Not necessary if this is 12 bytes (96 bits) */
    if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 16, NULL))
        handleErrors();

    /* Initialise key and IV */
    if(!EVP_DecryptInit_ex(ctx, NULL, NULL, key, iv)) handleErrors();

    /* Provide any AAD data. This can be called zero or more times as
     * required
     */
    if(!EVP_DecryptUpdate(ctx, NULL, &len, aad, aad_len))
        handleErrors();

    /* Provide the message to be decrypted, and obtain the plaintext output.
     * EVP_DecryptUpdate can be called multiple times if necessary
     */
    if(!EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len))
        handleErrors();
    plaintext_len+= len;

    /* Set expected tag value. Works in OpenSSL 1.0.1d and later */
    if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, tag))
        handleErrors();

    /* Finalise the decryption. A positive return value indicates success,
     * anything else is a failure - the plaintext is not trustworthy.
     */
    ret = EVP_DecryptFinal_ex(ctx, plaintext + len, &len);

    /* Clean up */
    EVP_CIPHER_CTX_free(ctx);

    if(ret > 0)
    {
        /* Success */
        plaintext_len += len;
        return plaintext_len;
    }
    else
    {
        /* Verify failed */
        return -1;
    }
}


int main (void)
{
    unsigned char str[1024],key[10],ciphertext[1024+EVP_MAX_BLOCK_LENGTH],tag[100],pt[1024];
    unsigned char iv[]="1234567890abcdef";
    unsigned char aad[]="1234567890123456";
    int k;
    printf("Enter string: ");
    scanf("%s",str);
    printf("Enter key: ");
    scanf("%s",key);

    encrypt(str, strlen(str), aad, strlen(aad), key, iv, ciphertext, tag);
    printf("%s\n",ciphertext);
    k = decrypt(ciphertext, strlen(ciphertext), aad, strlen(aad), tag, key, iv, pt);
    printf("%s\n%d\n",pt,k);
}

decrypt(ciphertext, strlen(ciphertext), aad, strlen(aad), tag, key, iv, pt);

decrypt(ciphertext, strlen(ciphertext), ... 是错误的。密文中可能嵌入了 NULL,在这种情况下,它将被 t运行cated。在您的情况下,额外的字符被馈送到 decrypt 函数。很难说有多少 - 直到 strlen 碰巧 运行 变成内存中的 NULL。

您需要捕获 encryptdecrpyt 的 return 值以正确设置各种长度:

int x;
...

x = encrypt(str, strlen(str), aad, strlen(aad), key, iv, ciphertext, tag);
...

x = decrypt(ciphertext, x, aad, strlen(aad), tag, key, iv, pt);
...

您可能对 aad, strlen(aad) 有同样的问题,但我认为它还没有暴露出来。


What is the correct format for final block in EVP_DecryptFinal_ex for decryption?

回到标题问题:none。你的问题出在别处。


您可能在 decrypt 函数中使用的 unsigned char *plaintext 缓冲区发生了溢出。您没有传入长度,所以 decrypt 愉快地写入超过它的长度...

在上面的示例中,您将密钥、IV 数据定义为字符串。密钥和 IV 不应由字符串组成,因为它们不包含任何可能的字节。这是最大化安全性所必需的;目前您正在限制可能的密钥数量。密钥应由其输出与随机输出无法区分的函数生成。

您应该使用密码密钥派生函数 (PBKDF)(例如 PBKDF2)从密码创建密钥。 IV 应该是随机的并与密文一起发送。 IV 和密钥应为静态大小,IV 为 16 字节,密钥为 16、24 或 32 字节。

然而,要进行测试,您可以使用简单的数组初始化(32 字节的密钥,因为您使用的是 AES-256):

unsigned char key[32] = { 0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,
                          0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,
                          0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,
                          0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00 };
unsigned char iv[16] = { 0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,
                         0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00,  0x00 };

或者您可以分配 X 字节的内存并用一个值填充它,当然,只要您为函数提供正确的大小即可。

您需要将所有出现的 strlen() 替换为 sizeof()plaintextciphertext 除外。在前者中,你实际上是在加密一个字符串,所以 strlen 是有道理的。在后者中,您需要使用 encrypt 操作的结果。密文包含在您的加密方法返回的长度(正确)的缓冲区中。

最后,现代密码在 字节 而不是 字符 上运行,因此您需要为所有输入提供以字节为单位的大小。任何使用相同原始类型处理字节和字符的语言都存在这种问题(当然,对于 C 语言,char)。它还倾向于隐藏 encoding/decoding 问题(例如关于 UTF-8)。

如果您想处理未知长度,则必须多次调用 EVP_EncryptUpdateEVP_DecryptUpdate,记录返回的字节数(就像您现在所做的那样)。然后在输入结束时,您只需最后一次调用更新方法,然后调用 EVP_EncryptFinal_exEVP_DecryptFinal_ex。在那种情况下,您当然应该将您的方法重构为 init/update/final 部分,并为输入和输出使用一些缓冲区。