Using OpenSSL cryptographic library, I need to encrypt multiple messages using the same key. Using AES-128-CBC as an example.
After getting an EVP_CIPHER_CTX, the standard scenario is:
EVP_EncryptInit_ex()EVP_CIPHER_CTX_set_padding()(no padding in my case, always a multiple of 16 bytes)EVP_EncryptUpdate()EVP_EncryptFinal_ex()
If we do this, it works but it is horribly inefficient because calling EVP_EncryptInit_ex() for each message recomputes the intermediate keys (in the case of AES) each time, even though the key does not change. With other crypto libraries, you can compute the intermediate keys only once and use the same key context for all messages as long as you use the same key.
With OpenSSL, it does not work in the general case. In practice, it works with AES-128-ECB because there is no IV and no chaining, but it does not work with AES-128-CBC. It seems that EVP_EncryptFinal_ex() is not really "final" and the last cipher block remains as IV. See a sample test program below.
How would you reset the EVP_CIPHER_CTX without clearing the intermediate keys, using the same key, and ideally set a new IV?
Not doing this is awfully slow, 12 times slower. Using the same test program as base, looping 1 million times on the encrypt operation below, 32 bytes message, AES-128-CBC, takes 16 ms when the key is scheduled once before the loop (but does not work as expected) and 194 ms when we reschedule it in each iteration. The CPU is an Intel Core i7-13700H.
There must be something obvious I missed. I cannot find anything online about this.
Currently, I use AES-128-ECB on each block and do the chaining by hand. It is much faster than using AES-128-CBC and reschedule the key for each message. Not really ideal.
OpenSSL version is 3.0.10 on Ubuntu 23.10.
Sample source code to test:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <openssl/evp.h>
#include <openssl/err.h>
// AES-128-CBC test vector (no padding):
static const uint8_t key[] = {0x2A, 0x98, 0xF5, 0xF7, 0x79, 0x99, 0x15, 0x24, 0x0B, 0xBA, 0xAA, 0x42, 0x2A, 0x6B, 0x4C, 0x8E};
static const uint8_t iv[] = {0x4A, 0x28, 0x80, 0xC6, 0x44, 0x97, 0x9C, 0x8F, 0xD2, 0xBB, 0xF8, 0xD0, 0xCA, 0xB2, 0x10, 0x5F};
static const uint8_t pt[] = {0xB7, 0x3D, 0x23, 0xC6, 0x3B, 0x13, 0xE4, 0x3A, 0x31, 0x15, 0x13, 0x42, 0x75, 0xB7, 0xAC, 0xB8,
0x18, 0xE0, 0x90, 0xFA, 0x57, 0x62, 0xA3, 0xF6, 0xE7, 0x09, 0x6B, 0xAB, 0x20, 0x97, 0x71, 0xA2};
static const uint8_t ct[] = {0xD5, 0xED, 0xF0, 0xE1, 0xBA, 0x3E, 0x2D, 0x33, 0xDB, 0xF0, 0xDA, 0xC0, 0xA7, 0x34, 0x72, 0xD6,
0x69, 0x4F, 0x62, 0x35, 0xC9, 0xBA, 0xE9, 0xB4, 0x82, 0x22, 0x17, 0xF5, 0xE3, 0xA8, 0x8B, 0xEC};
static void check(int cond)
{
if (!cond) {
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
}
int main()
{
ERR_load_crypto_strings();
OpenSSL_add_all_algorithms();
EVP_CIPHER* algo = EVP_CIPHER_fetch(NULL, "AES-128-CBC", NULL);
check(algo != NULL);
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
check(algo != NULL);
uint8_t data[sizeof(ct)];
int len, final;
for (int i = 1; i < 4; i++) {
// Doesn't work if called only once, before the loop.
check(EVP_EncryptInit_ex(ctx, algo, NULL, key, iv) == 1);
check(EVP_CIPHER_CTX_set_padding(ctx, 0) == 1);
check(EVP_EncryptUpdate(ctx, data, &len, pt, sizeof(pt)) == 1);
check(EVP_EncryptFinal_ex(ctx, data + len, &final) == 1);
printf("encrypt #%d: %s\n", i, len + final == sizeof(ct) && memcmp(data, ct, sizeof(ct)) == 0 ? "passed" : "FAILED");
}
EVP_CIPHER_CTX_free(ctx);
EVP_CIPHER_free(algo);
EVP_cleanup();
ERR_free_strings();
}