AES_GCM_256 中 IV 的内部处理,用于对同一明文进行多次加密调用

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

我试图了解 OpenSSL 的 EVP_EncryptUpdate 函数的工作原理,特别是为什么它在多次调用时为相同的明文、密钥和 IV(最初提供)返回不同的密文。据我了解,每次调用后都应该更改 IV,并且该函数在内部处理 IV。问题是 IV 内部到底是如何处理的?尝试获取 IV 返回与初始化上下文的 IV 相同的 IV。

#include <openssl/ssl.h>

void print_array(char *title, unsigned char *buff, int len)
{
    printf("%s:\n", title);
    for(int i = 0; i<len; ++i)
        printf("%02x ", buff[i]);
    printf("\n");
}

void handleErrors()
{
    printf("Error in encryption/decryption\n");
}

void encrypt_EVP_aes_256_gcm_init(EVP_CIPHER_CTX **ctx, unsigned char *key, unsigned char *iv)
{
    if(!(*ctx = EVP_CIPHER_CTX_new()))
        handleErrors();
    if(1 != EVP_EncryptInit_ex(*ctx, EVP_aes_256_gcm(), NULL, key, iv))
        handleErrors();
}

void encrypt(EVP_CIPHER_CTX *ctx, unsigned char *plaintext, int plaintext_len, unsigned char *ciphertext, int *ciphertext_len)
{
    int len;
    if(1 != EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len))
        handleErrors();
    *ciphertext_len = len;
}

void test(const unsigned char* key, const unsigned char* iv, const unsigned char *data, int iters)
{
    unsigned char enc_data[32];

    int32_t cipherLength;
    EVP_CIPHER_CTX *encrypt_ctx;
    encrypt_EVP_aes_256_gcm_init(&encrypt_ctx, key, iv);

    for(int i = 0; i<iters; ++i)
    {
        print_array("iv", EVP_CIPHER_CTX_iv(encrypt_ctx), 12);    
    
        encrypt(encrypt_ctx, data, 32, enc_data, &cipherLength);

        print_array("enc data", enc_data, cipherLength);
    }
}

int main()
{
    const unsigned char* key =             "\x3c\x57\x5e\x25\x5f\x43\x41\x69\x3d\x5e\x48\x29\x72\x54\x27\x55\x3e\x29\x28\x65\x31\x34\x4a\x3e\x52\x2f\x7c\x6a\x7b\x25\x78\x52";
    const unsigned char* iv = "\x75\x71\x71\x55\x36\x33\x59\x52\x2c\x22\x74\x7d\x6f\x36\x6d\x2e";
    const unsigned char data[32] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";

    test(key, iv, data, 3);

    return 0;
}

我尝试输出IV,但似乎没有显示正在运行的IV。我只拿回提供的静脉注射。

c openssl cryptography
1个回答
0
投票

如果执行您的 C 代码,则三次迭代将产生以下密文结果:

i = 0: 0x7e81d12cb6cd69e538f709f69274f7f397c375b460cae4f6433b556bd0b0f839
i = 1: 0x97c4c7be1b67c61975fe97288599b1029984ba05633fefd942d98400d20829e1
i = 2: 0x861e3f9f54f44bce0d5afe3c9554bb503a7a675e398485693d00519111a152d9

这些发生如下:

  • 由于 C 代码不会更改 12 字节的默认 IV 长度,因此仅应用 16 字节 IV 的前 12 字节:
    0x75717155363359522c22747d
  • 根据该 IV,GCM 算法确定由 12 字节 IV 组成的初始值,其中附加了 4 个字节
    0x00000001
    0x75717155363359522c22747d00000001
    。这样确定的初始值是为以后确定GCM认证标签而保留的,并不用于加密本身。
    请注意,如果 IV 长度不是 12 字节,则会使用不同的算法来计算初始值(见下文)。
  • 对于第一个明文块和所有其他明文块的加密,初始值随着每个块而递增,即对于第一个明文,计数器的值为
    0x75717155363359522c22747d00000002
  • 第一个明文的大小为两个块,因此第二个明文的计数器值为
    0x75717155363359522c22747d00000004
  • 第二个明文的大小也是两个块,因此第三个明文的计数器的值为
    0x75717155363359522c22747d00000006
    ,依此类推。

我不知道有任何 EVP 函数可以检索初始值或中间计数器值。然而,由于初始值的计算和增量都是自动进行的,所以实际上没有必要这样做(正如第一条评论中已经提到的)。

关于加密,GCM 的工作原理类似于 CTR,上面的计数器对应于 IV。因此,通过使用 CTR 和确定的计数器而不是 GCM 和 12 字节 IV 进行加密,可以轻松验证计数器的正确性。
C代码可以进行相应的修改(为了简单起见,没有异常处理):

void encrypt_with_CTR(unsigned char* key, unsigned char* iv, unsigned char* data)
{
    int cipherLength;
    unsigned char ciphertext[32];
    EVP_CIPHER_CTX* encrypt_ctx = EVP_CIPHER_CTX_new();
    EVP_EncryptInit_ex(encrypt_ctx, EVP_aes_256_ctr(), NULL, key, iv);
    EVP_EncryptUpdate(encrypt_ctx, ciphertext, &cipherLength, data, 32);
    print_array((char*)"ciphertext: ", ciphertext, cipherLength);
}
...
unsigned char data[] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
unsigned char key[] = "\x3c\x57\x5e\x25\x5f\x43\x41\x69\x3d\x5e\x48\x29\x72\x54\x27\x55\x3e\x29\x28\x65\x31\x34\x4a\x3e\x52\x2f\x7c\x6a\x7b\x25\x78\x52";
unsigned char iv_1[] = "\x75\x71\x71\x55\x36\x33\x59\x52\x2c\x22\x74\x7d\x00\x00\x00\x02";
unsigned char iv_2[] = "\x75\x71\x71\x55\x36\x33\x59\x52\x2c\x22\x74\x7d\x00\x00\x00\x04";
unsigned char iv_3[] = "\x75\x71\x71\x55\x36\x33\x59\x52\x2c\x22\x74\x7d\x00\x00\x00\x06";
encrypt_with_CTR(key, iv_1, data); 
encrypt_with_CTR(key, iv_2, data); 
encrypt_with_CTR(key, iv_3, data); 

或者,可以使用 CyberChef 进行验证:

0x75717155363359522c22747d00000002
0x7e81d12cb6cd69e538f709f69274f7f397c375b460cae4f6433b556bd0b0f839
0x75717155363359522c22747d00000004
0x97c4c7be1b67c61975fe97288599b1029984ba05633fefd942d98400d20829e1
0x75717155363359522c22747d00000006
0x861e3f9f54f44bce0d5afe3c9554bb503a7a675e398485693d00519111a152d9

正如预期的那样,这两者都对应于具有 12 字节 IV 的 GCM 加密的密文。


备注:

  • 与CTR相比,GCM的附加价值是身份验证,可以确保数据的完整性。 GCM 为此目的使用身份验证标签。这个标签是通过
    EVP_EncryptFinal_ex()
    计算的,这就是为什么必须在加密过程结束时调用这个函数的原因(见第一条评论)。然后必须使用
    EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag)
    检索标签(请参阅第二条评论)。
    发布的代码中缺少这两个调用,因此既未创建也未检索标签。在这种情况下这不是问题,因为演示程序不需要该标签。
    然而,使用常规 GCM 加密,两个调用都必须执行!
  • GCM 针对 12 字节的 IV 长度进行了优化,这就是为什么出于性能和兼容性原因,在实践中应使用建议的 12 字节 IV 大小。
    如果由于某种原因必须使用不同的 IV 大小,则可以使用
    EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, NULL)
    为 IV 指定与建议的 12 字节不同的长度。在这种情况下,计数器的初始值的确定方式有所不同,即使用GHASH算法。
  • 在这里您可以找到 GCM 的规范,尤其是 GHASH 和 这里使用 GCM 的 EVP 验证加密和解密的示例(具有 12 字节 IV 或其他长度的 IV
    iv_len
    )。
© www.soinside.com 2019 - 2024. All rights reserved.