Skip to content

Instantly share code, notes, and snippets.

@rfl890
Last active July 9, 2023 17:20
Show Gist options
  • Save rfl890/03cc26599a890a7ae0449d849e0e6854 to your computer and use it in GitHub Desktop.
Save rfl890/03cc26599a890a7ae0449d849e0e6854 to your computer and use it in GitHub Desktop.
OpenSSL AES-256-CBC encryption/decryption with HMAC
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <string.h>
#include <immintrin.h>
#include <openssl/evp.h>
#include <openssl/err.h>
#include <openssl/kdf.h>
#include <openssl/rand.h>
#include <openssl/core_names.h>
#ifdef _WIN32
#pragma comment(lib, "crypt32")
#pragma comment(lib, "ws2_32")
#pragma comment(lib, "user32")
#pragma comment(lib, "advapi32")
#endif
typedef struct
{
unsigned char *bytes;
size_t length;
} byte_array_result;
#define calc_ctext_length(plen) plen + (16 - (plen % 16))
static void dumphex(unsigned char *bytes, int n)
{
for (int i = 0; i < n; i++)
{
printf("%02x ", bytes[i]);
}
printf("\n");
}
/**
* @brief Computes the HMAC SHA3-256 signature of a message
*
* @param msg The message.
* @param msgLen The length (in bytes) of the message.
* @param key The key to use.
* @param keyLen The length (in bytes) of the key
* @param out The buffer to fill with the signature.
* @param outsize The size of the buffer. This should always be 32.
*/
static int hmac(const unsigned char *msg, size_t msgLen, const unsigned char *key, size_t keyLen, unsigned char *out, size_t outsize)
{
EVP_MAC *mac;
EVP_MAC_CTX *ctx;
OSSL_PARAM params[2];
size_t len = 0;
params[0] = OSSL_PARAM_construct_utf8_string("digest", "SHA3-256", 0);
params[1] = OSSL_PARAM_construct_end();
mac = EVP_MAC_fetch(NULL, "HMAC", NULL);
if (mac == NULL)
{
return 0;
}
ctx = EVP_MAC_CTX_new(mac);
if (ctx == NULL)
{
EVP_MAC_free(mac);
return 0;
}
if (!EVP_MAC_init(ctx, key, keyLen, params))
{
EVP_MAC_free(mac);
EVP_MAC_CTX_free(ctx);
return 0;
}
if (!EVP_MAC_update(ctx, msg, msgLen))
{
EVP_MAC_free(mac);
EVP_MAC_CTX_free(ctx);
return 0;
}
if (!EVP_MAC_final(ctx, out, &len, outsize))
{
EVP_MAC_free(mac);
EVP_MAC_CTX_free(ctx);
return 0;
}
EVP_MAC_free(mac);
EVP_MAC_CTX_free(ctx);
return 1;
}
/**
* @brief Derives a key from a password.
*
* @param password A string.
* @param passwordSize The size of the string minus the null terminator.
* @param saltOut A buffer to fill with the generated salt. This should always be 32 bytes.
* @param derivedOut A buffer to fill with the generated key. This should always be 32 bytes.
*/
static int deriveKey(const unsigned char *password, size_t passwordSize, unsigned char *saltOut, unsigned char *derivedOut, unsigned char *saltIn)
{
// Variables
EVP_KDF *kdf;
EVP_KDF_CTX *ctx;
OSSL_PARAM params[6];
// Source: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#scrypt
unsigned int param_n = 131072;
unsigned int param_r = 8;
unsigned int param_p = 1;
unsigned char salt[32];
int genRandomSalt = 0;
if (saltIn != NULL)
{
memcpy(salt, saltIn, 32);
}
else
{
genRandomSalt = 1;
}
if (genRandomSalt == 1)
{
if (!RAND_bytes(salt, 32))
{
return 0;
}
}
params[0] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_PASSWORD, password, passwordSize);
params[1] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, salt, 32);
params[2] = OSSL_PARAM_construct_uint(OSSL_KDF_PARAM_SCRYPT_N, &param_n);
params[3] = OSSL_PARAM_construct_uint(OSSL_KDF_PARAM_SCRYPT_R, &param_r);
params[4] = OSSL_PARAM_construct_uint(OSSL_KDF_PARAM_SCRYPT_P, &param_p);
params[5] = OSSL_PARAM_construct_end();
kdf = EVP_KDF_fetch(NULL, OSSL_KDF_NAME_SCRYPT, NULL);
if (kdf == NULL)
{
return 0;
}
ctx = EVP_KDF_CTX_new(kdf);
if (ctx == NULL)
{
EVP_KDF_free(kdf);
OPENSSL_free(salt);
return 0;
}
if (!EVP_KDF_derive(ctx, derivedOut, 32, params))
{
EVP_KDF_free(kdf);
EVP_KDF_CTX_free(ctx);
OPENSSL_free(salt);
return 0;
}
EVP_KDF_free(kdf);
EVP_KDF_CTX_free(ctx);
if (saltOut != NULL)
{
memcpy(saltOut, salt, 32);
}
if (genRandomSalt == 1)
{
OPENSSL_cleanse(salt, 32);
}
return 1;
}
/**
* @brief Derives an encryption key and an HMAC key from a master key using HKDF.
*
* @param masterKey The master key.
* @param salt The salt to use.
* @param encryptionKey The buffer to fill with the encryption key. This should always be 32 bytes.
* @param hmacKey The buffer to fill with the HMAC key. This should always be 32 bytes.
*/
static int deriveKeys(unsigned char *masterKey, unsigned char *salt, unsigned char *encryptionKey, unsigned char *hmacKey)
{
EVP_KDF *kdf;
EVP_KDF_CTX *ctx;
OSSL_PARAM params_hmac[5];
OSSL_PARAM params_enc[5];
const char *info_hmacKey = "OpenSSL3.1|HMAC Key";
const char *info_encKey = "OpenSSL3.1|Encryption Key";
params_hmac[0] = OSSL_PARAM_construct_utf8_string("digest", "SHA3-256", 0);
params_hmac[1] = OSSL_PARAM_construct_octet_string("salt", salt, 32);
params_hmac[2] = OSSL_PARAM_construct_octet_string("key", masterKey, 32);
params_hmac[3] = OSSL_PARAM_construct_octet_string("info", info_hmacKey, strlen(info_hmacKey));
params_hmac[4] = OSSL_PARAM_construct_end();
params_enc[0] = OSSL_PARAM_construct_utf8_string("digest", "SHA3-256", 0);
params_enc[1] = OSSL_PARAM_construct_octet_string("salt", salt, 32);
params_enc[2] = OSSL_PARAM_construct_octet_string("key", masterKey, 32);
params_enc[3] = OSSL_PARAM_construct_octet_string("info", info_encKey, strlen(info_encKey));
params_enc[4] = OSSL_PARAM_construct_end();
kdf = EVP_KDF_fetch(NULL, "HKDF", NULL);
if (kdf == NULL)
{
return 0;
}
ctx = EVP_KDF_CTX_new(kdf);
if (ctx == NULL)
{
EVP_KDF_free(kdf);
return 0;
}
if (!EVP_KDF_derive(ctx, hmacKey, 32, params_hmac))
{
EVP_KDF_free(kdf);
EVP_KDF_CTX_free(ctx);
return 0;
}
if (!EVP_KDF_derive(ctx, encryptionKey, 32, params_enc))
{
EVP_KDF_free(kdf);
EVP_KDF_CTX_free(ctx);
return 0;
}
EVP_KDF_free(kdf);
EVP_KDF_CTX_free(ctx);
return 1;
}
/**
* @brief Encrypts a plaintext with AES-256-CBC using PKCS#7 padding.
*
* @param plaintext The plaintext to encrypt.
* @param plaintextLen The length of the plaintext. This should exclude the null terminator if it is a string.
* @param key The encryption key. This should always be 32 bytes.
* @param iv The IV to use during encryption. This should always be 16 bytes
* @param ciphertext The buffer to fill with the ciphertext. You can calculate the length of this via the calc_ctext_length macro.
*/
static int encrypt_raw(const unsigned char *plaintext, size_t plaintextLen, unsigned char *key, unsigned char *iv, unsigned char *ciphertext)
{
EVP_CIPHER_CTX *ctx;
int len = 0;
ctx = EVP_CIPHER_CTX_new();
if (!ctx)
{
return 0;
}
if (!EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv))
{
EVP_CIPHER_CTX_free(ctx);
return 0;
}
if (!EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintextLen))
{
EVP_CIPHER_CTX_free(ctx);
return 0;
}
if (!EVP_EncryptFinal_ex(ctx, ciphertext + len, &len))
{
EVP_CIPHER_CTX_free(ctx);
return 0;
}
EVP_CIPHER_CTX_free(ctx);
return 1;
}
/**
* @brief Decrypts a plaintext with AES-256-CBC using PKCS#7 padding.
*
* @param ciphertext The ciphertext to decrypt.
* @param ciphertextLen The length of the ciphertext.
* @param key The key to use during decryption.
* @param iv The iv to use during decryption.
* @param plaintext The output buffer for the plaintext.
* @return int The size of the plaintext in bytes.
*/
static int decrypt_raw(const unsigned char *ciphertext, size_t ciphertextLen, unsigned char *key, unsigned char *iv, unsigned char *plaintext)
{
EVP_CIPHER_CTX *ctx;
int len = 0;
int plaintext_len = 0;
ctx = EVP_CIPHER_CTX_new();
if (!ctx)
{
return -1;
}
if (!EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv))
{
EVP_CIPHER_CTX_free(ctx);
return -1;
}
if (!EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertextLen))
{
EVP_CIPHER_CTX_free(ctx);
return -1;
}
plaintext_len = len;
if (!EVP_DecryptFinal_ex(ctx, plaintext + len, &len))
{
EVP_CIPHER_CTX_free(ctx);
return -1;
}
EVP_CIPHER_CTX_free(ctx);
plaintext_len += len;
return plaintext_len;
}
// Only exposed function(s)
/**
* @brief Encrypts some plaintext with a password.
*
* @param plaintext The plaintext to encrypt.
* @param plaintextLength The length of the plaintext (in bytes).
* @param password A null-terminated string to encrypt the plaintext with.
* @return byte_array_result A struct representing the returned result. Contains a pointer to the ciphertext and a size_t representing its length.
*/
byte_array_result encrypt(const char *plaintext, size_t plaintextLength, const char *password)
{
unsigned char *uPlaintext = (unsigned char *)plaintext;
// Derive the master key
unsigned char masterKey[32];
unsigned char salt[32];
if (deriveKey(password, strlen(password), salt, masterKey, NULL) != 1) {
byte_array_result errResult;
errResult.bytes = NULL;
errResult.length = 0;
return errResult;
}
// Derive HMAC and encryption keys from the master key
unsigned char encryptionKey[32];
unsigned char hmacKey[32];
if (deriveKeys(masterKey, salt, encryptionKey, hmacKey) != 1) {
byte_array_result errResult;
errResult.bytes = NULL;
errResult.length = 0;
return errResult;
}
// Generate a random 16-byte IV
unsigned char iv[16];
if (!RAND_bytes(iv, 16))
{
byte_array_result errResult;
errResult.bytes = NULL;
errResult.length = 0;
return errResult;
}
// Calculate length of final ciphertext.
// We assume the caller is calculating this the same way, and
// the output buffer is this size.
const size_t ciphertextLengthRaw = calc_ctext_length(plaintextLength);
const size_t finalCiphertextLength = (ciphertextLengthRaw) + /* Ciphertext */
16 + /* IV */
32 + /* Salt */
32; /* Signatue */
unsigned char *finalCiphertext = calloc(finalCiphertextLength, sizeof(unsigned char));
if (finalCiphertext == NULL) {
byte_array_result errResult;
errResult.bytes = NULL;
errResult.length = 0;
return errResult;
}
// Fills the first (ciphertextLengthRaw) bytes with the ciphertext.
if (encrypt_raw(uPlaintext, plaintextLength, encryptionKey, iv, finalCiphertext) != 1) {
byte_array_result errResult;
errResult.bytes = NULL;
errResult.length = 0;
free(finalCiphertext);
return errResult;
}
// Append the IV and salt to the ciphertext.
memcpy(finalCiphertext + ciphertextLengthRaw, iv, 16);
memcpy(finalCiphertext + ciphertextLengthRaw + 16, salt, 32);
// Calculate HMAC on (ciphertext || iv || salt). This will be our signature/authentication tag.
unsigned char signature[32];
if (hmac(finalCiphertext, (finalCiphertextLength - 32), hmacKey, 32, signature, 32) != 1) {
byte_array_result errResult;
errResult.bytes = NULL;
errResult.length = 0;
free(finalCiphertext);
return errResult;
}
// Append the HMAC to the ciphertext.
memcpy(finalCiphertext + ciphertextLengthRaw + 16 + 32, signature, 32);
// Cleanse all our memory. I'm not sure if this is needed, but I'm doing it just to be safe.
OPENSSL_cleanse(masterKey, 32);
OPENSSL_cleanse(salt, 32);
OPENSSL_cleanse(encryptionKey, 32);
OPENSSL_cleanse(hmacKey, 32);
OPENSSL_cleanse(iv, 16);
OPENSSL_cleanse(signature, 32);
// Construct and return our byte_array_result
byte_array_result result;
result.bytes = finalCiphertext;
result.length = finalCiphertextLength;
return result;
}
/**
* @brief Decrypts some ciphertext with a password.
*
* @param ciphertext The ciphertext to decrypt.
* @param ciphertextLength The length of the ciphertext (in bytes).
* @param password A null-terminated string (the password) to decrypt the ciphertext with.
* @return byte_array_result The decrypted result.
*/
byte_array_result decrypt(const char *ciphertext, size_t ciphertextLength, const char *password)
{
const unsigned char *uCiphertext = (const unsigned char *)ciphertext;
// Obtain the IV, salt and signature from the ciphertext.
const unsigned char *iv = (uCiphertext + (ciphertextLength - 80));
const unsigned char *salt = (uCiphertext + (ciphertextLength - 64));
const unsigned char *signature = (uCiphertext + (ciphertextLength - 32));
// Derive the master key from the password.
unsigned char masterKey[32];
if (deriveKey(password, strlen(password), NULL, masterKey, salt) != 1) {
byte_array_result errResult;
errResult.bytes = NULL;
errResult.length = 0;
return errResult;
}
// Derive encryption and HMAC keys from the master keys
unsigned char encryptionKey[32];
unsigned char hmacKey[32];
if (deriveKeys(masterKey, salt, encryptionKey, hmacKey) != 1) {
byte_array_result errResult;
errResult.bytes = NULL;
errResult.length = 0;
return errResult;
}
// Calculate HMAC on our ciphertext.
unsigned char ourSignature[32];
if (hmac(uCiphertext, ciphertextLength - 32, hmacKey, 32, ourSignature, 32) != 1) {
byte_array_result errResult;
errResult.bytes = NULL;
errResult.length = 0;
return errResult;
}
// Verify our HMACs.
int result = CRYPTO_memcmp(signature, ourSignature, 32);
if (result != 0)
{
byte_array_result errResult;
errResult.bytes = NULL;
errResult.length = 0;
return errResult;
}
// Proceed with decryption.
unsigned char *plaintext = calloc(ciphertextLength, sizeof(unsigned char));
if (plaintext == NULL) {
byte_array_result errResult;
errResult.bytes = NULL;
errResult.length = 0;
return errResult;
}
int bytes = decrypt_raw(uCiphertext, ciphertextLength - 80, encryptionKey, iv, plaintext);
if (bytes == -1) {
byte_array_result errResult;
errResult.bytes = NULL;
errResult.length = 0;
free(plaintext);
return errResult;
}
int remainder = ciphertextLength - bytes;
OPENSSL_cleanse(plaintext + bytes, remainder);
plaintext = OPENSSL_realloc(plaintext, bytes);
// Cleanse all our variables, just in case.
OPENSSL_cleanse(masterKey, 32);
OPENSSL_cleanse(encryptionKey, 32);
OPENSSL_cleanse(hmacKey, 32);
OPENSSL_cleanse(ourSignature, 32);
// Construct and return our byte_array_result result.
byte_array_result tResult;
tResult.bytes = plaintext;
tResult.length = bytes;
return tResult;
}
int main(void) {
const char *plaintext = "Hello";
byte_array_result encrypted = encrypt(plaintext, strlen(plaintext), "pa55w0rd!");
OPENSSL_assert(encrypted.bytes != NULL);
byte_array_result decrypted = decrypt(encrypted.bytes, encrypted.length, "pa55w0rd!");
OPENSSL_assert(decrypted.bytes != NULL);
for (size_t i = 0; i < decrypted.length; i++) {
printf("%c", decrypted.bytes[i]);
}
printf("%c", '\n');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment