Search code examples
copensslcryptography

Internal handling of IV in AES_GCM_256, for multiple encryption calls on same plaintext


I am trying to understand the working of OpenSSL's EVP_EncryptUpdate function, specifically why does it return different cipher-text for same plaintext, key and IV (initially provided), over multiple calls. As far as I understand, the IV should be changed after each call, and the function takes care of the IV internally. The Question is how exactly is the IV handled internally? Trying to fetch the IV returns the same IV as the one the context is initialised with.

#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;
}

I tried outputting the IV, but it does not seem to display the running IV. I only get back the supplied IV.


Solution

  • If your C code is executed, the following ciphertexts result for the three iterations:

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

    These come about as follows:

    • As the C code does not change the default IV length of 12 bytes, only the first 12 bytes of the 16 bytes IV are applied: 0x75717155363359522c22747d.
    • From this IV, the GCM algorithm determines an initial value consisting of the 12 bytes IV to which the 4 bytes 0x00000001 are appended: 0x75717155363359522c22747d00000001. The initial value determined in this way is reserved for the later determination of the GCM authentication tag and is not used for the encryption itself.
      Please note that if the IV length is other than 12 bytes, a different algorithm is used to calculate the initial value (see below).
    • For the encryption of the first plaintext block and all further plaintext blocks, the initial value is incremented with each block, i.e. for the first plaintext the counter has the value 0x75717155363359522c22747d00000002.
    • The first plaintext is two blocks in size, so the counter for the second plaintext has the value 0x75717155363359522c22747d00000004.
    • The second plaintext is also two blocks in size, so that the counter for the third plaintext has the value 0x75717155363359522c22747d00000006 and so on.

    I am not aware of any EVP functions that retrieve the initial value or intermediate counter values. However, as both the calculation of the initial value and the incrementation are carried out automatically, there is actually no need for this (as already mentioned in the 1st comment).

    Regarding encryption, GCM works like CTR, with the above counters corresponding to the IVs. The correctness of the counters can therefore be easily verified by encrypting with CTR and the determined counters instead of GCM and the 12 bytes IV.
    The C code can be adapted accordingly (for the sake of simplicity without exception handling):

    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); 
    

    Alternatively, this can be verified with CyberChef:

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

    both of which, as expected, correspond to the ciphertexts of the GCM encryption with the 12 bytes IV.


    Notes:

    • The added value of GCM compared to CTR is authentication, which ensures the integrity of the data. GCM uses an authentication tag for this purpose. This tag is calculated by EVP_EncryptFinal_ex(), which is why this function must be called at the end of the encryption process (see first comment). The tag must then be retrieved using EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag) (see second comment).
      Both calls are missing in the posted code, so that the tag is neither created nor retrieved. This is not a problem in this case, as the tag is not required for the demo program.
      With regular GCM encryption, however, both calls must be carried out!
    • GCM is optimized for an IV length of 12 bytes, which is why the recommended IV size of 12 bytes should be used in practice for performance and compatibility reasons.
      If for some reason a different IV size must be used, EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, NULL) can be used to specify a different length for the IV than the recommended 12 bytes. In this case, the initial value of the counter is determined differently, namely using the GHASH algorithm.
    • Here you can find the specification for GCM and especially GHASH and here an example for an EVP Authenticated Encryption and Decryption with GCM (with a 12 bytes IV or an IV of other length iv_len).