Last active
July 9, 2023 17:20
-
-
Save rfl890/03cc26599a890a7ae0449d849e0e6854 to your computer and use it in GitHub Desktop.
OpenSSL AES-256-CBC encryption/decryption with HMAC
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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, ¶m_n); | |
params[3] = OSSL_PARAM_construct_uint(OSSL_KDF_PARAM_SCRYPT_R, ¶m_r); | |
params[4] = OSSL_PARAM_construct_uint(OSSL_KDF_PARAM_SCRYPT_P, ¶m_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