I am working with OpenSSL and writing an AES256 encryption module. My knowledge of cybersecurity is not vast and I would like to know whether there are any obvious holes I'm leaving. The intent is for the user to construct an instance of AESEncDec
passing a key. With that instance, they may encrypt and decrypt files using that key with methods defined in the class.
My main concerns:
- In what ways could an attacker potentially compromise this program while it's running?
- Does the module's class-based nature somehow weaken its security due to having instances of the class which could be inspected by another process?
- To further test myself, I was considering adding another two layers of AES, encrypting somewhat like
Cipher = ENC(K1, (ENC(K2, (ENC(K1, Plaintext))))
. Would such a change aid security in any way?
Notes:
I am aware that the encrypt and decrypt methods can be combined into one using different EVP functions, I only learned that a bit far into the process and decided to keep the functions separate for now.
I reviewed a similar question, but the poster appeared to be using a different library, though I did note some of the responses such as adding validation functions for input as well as expanding the class to enable other sized keys. Since I had some additional questions, I made this review request.
aes_enc_dec_class.h:
#include <fstream>
#include <iostream>
#include <string>
#include <cstring>
#include <openssl/evp.h>
#include <openssl/err.h>
#include <openssl/aes.h>
#include <openssl/rand.h>
#include <sstream>
void handleErrors() {
ERR_print_errors_fp(stderr);
abort();
}
/**
* @brief An instance of AESEncDec is tied to a key, with which
* files can be encrypted or decrypted as desired.
*/
class AESEncDec {
static const int KEYLEN = 32;
static const int BLOCKSIZE = 16;
static const int ITER_COUNT = 10000;
unsigned char key[KEYLEN];
unsigned char iv[BLOCKSIZE];
public:
AESEncDec(unsigned char* keybase);
int encrypt_file(const char* path,
const char* out);
int decrypt_file(const char* path,
const char* out);
};
AESEncDec::AESEncDec(unsigned char* keybase) {
// set key from keybase
if (!(EVP_BytesToKey(EVP_aes_256_cbc(), EVP_md5(), NULL,
keybase, strlen((const char *) keybase), ITER_COUNT, key, iv))) {
fprintf(stderr, "Invalid key base.\n");
}
}
int AESEncDec::encrypt_file(const char* path, const char* out) {
// initialize/open file streams
std::ifstream plaintext_file;
std::ofstream ciphertext_file;
plaintext_file.open(path, std::ios::in | std::ios::binary);
ciphertext_file.open(out, std::ios::out | std::ios::binary | std::ios::trunc);
// ensure file is open, exit otherwise
if (!plaintext_file.is_open()) {
fprintf(stderr, "Failed to open plaintext.\n");
return -1;
}
// initialize encryption buffers
unsigned char plaintext[BLOCKSIZE];
unsigned char ciphertext[BLOCKSIZE + BLOCKSIZE]; // extra space for padding
// initialize encryption context
EVP_CIPHER_CTX *ctx;
if (!(ctx = EVP_CIPHER_CTX_new()))
handleErrors();
// reinitialize iv to avoid reuse
if (!RAND_bytes(iv, BLOCKSIZE)) {
fprintf(stderr, "Failed to initialize IV");
return -1;
}
// set cipher/key/iv
if (1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv))
handleErrors();
// for keeping track of result length
int len;
int cipherlen = 0;
int bytes_read;
// read and encrypt a block at a time, write to file
while (1) {
plaintext_file.read((char *) plaintext, BLOCKSIZE);
bytes_read = plaintext_file.gcount();
if (1 != EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, bytes_read))
handleErrors();
ciphertext_file.write((char *) ciphertext, len);
cipherlen+=len;
if (bytes_read < BLOCKSIZE) break;
}
// finalize encryption
if (1 != EVP_EncryptFinal_ex(ctx, ciphertext, &len))
handleErrors();
// write final block
ciphertext_file.write((char *) ciphertext, len);
cipherlen += bytes_read;
// clean up
ciphertext_file.close();
plaintext_file.close();
EVP_CIPHER_CTX_free(ctx);
return cipherlen;
}
int AESEncDec::decrypt_file(const char* path,
const char* out) {
// open files for reading and writing
std::ifstream ciphertext_file;
std::ofstream plaintext_file;
ciphertext_file.open(path, std::ios::in | std::ios::binary);
plaintext_file.open(out, std::ios::out | std::ios::binary | std::ios::trunc);
// if opening failed, exit
if (!ciphertext_file.is_open() || !plaintext_file.is_open()) {
fprintf(stderr, "One of the files is already open.\n");
return -1;
}
// initialize cipher context
EVP_CIPHER_CTX *ctx;
if (!(ctx = EVP_CIPHER_CTX_new()))
handleErrors();
// reinitialize iv to avoid reuse
if (!RAND_bytes(iv, BLOCKSIZE)) {
fprintf(stderr, "Failed to initialize IV");
return -1;
}
// initialize decryption
if (1 != EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv))
handleErrors();
// keeping track of length of result
int len;
int plaintext_len = 0;
// initialize cipher/plaintext buffers
unsigned char plaintext[BLOCKSIZE+BLOCKSIZE], ciphertext[BLOCKSIZE];
int bytes_read;
// go through the file one block at a time
while (1) {
ciphertext_file.read((char *) ciphertext, BLOCKSIZE);
bytes_read = ciphertext_file.gcount();
// decrypt block
if (1 != EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, bytes_read))
handleErrors();
plaintext_len += len;
plaintext_file.write((char *) plaintext, len);
if (bytes_read < BLOCKSIZE) break;
}
if (1 != EVP_DecryptFinal_ex(ctx, plaintext + len, &len))
handleErrors();
plaintext_file.write((char*) plaintext, len);
plaintext_len += len;
// clean up
EVP_CIPHER_CTX_free(ctx);
plaintext_file.close();
ciphertext_file.close();
return plaintext_len;
}
test_aes_enc_dec.cc
:
#include "aes_enc_dec_class.h"
#include <iostream>
#include <stdio.h>
#include <string>
#include <fstream>
int main (void) {
unsigned char *key = (unsigned char *) "HardcodedKey!";
AESEncDec cipher(key);
const char* test_file_path = "test.txt";
// open test file for writing, clearing it
std::ofstream test_file;
test_file.open(test_file_path, std::ios::out | std::ios::trunc);
// put a test string into the file
const char *test_string = "This is our test string!";
const int test_len = strlen(test_string);
test_file.write(test_string, test_len);
test_file.close();
// encrypt the file
int enc_result = cipher.encrypt_file("test.txt", "test.enc");
printf("Encrypted %d bytes.\n", enc_result);
// decrypt the file
int dec_result = cipher.decrypt_file("test.enc", "test.dec");
printf("Decrypted %d bytes.\n", dec_result);
// check output contents
std::ifstream result;
result.open("test.dec", std::ios::in | std::ios::binary);
char in[test_len + 1];
result.read(in, test_len);
in[test_len] = '\0';
printf("Plaintext: %s\n"
"Decrypted plaintext: %s\n", test_string, in);
// report results
int diff;
if ((diff = strcmp(test_string, in)) != 0) {
printf("Test failed: difference %d\n", diff);
return -1
} else printf("Test passed.\n");
return 0;
}
Makefile
LIBS = -lssl -lcrypto
CFLAGS = -g -o
all: clean test_AES
test_AES:
g++ $(CFLAGS) test_AES test_aes_enc_dec.cc $(LIBS)
clean:
rm -f test_AES test.txt test.enc test.dec
EVP_BytesToKey
. It's considered legacy and out of date. Use PBKDF2 instead. You don't say what version of OpenSSL you are using. In 3.0 you can use the EVP_KDF APIs for this. In older versions you can use PKCS5_PBKDF2_HMAC. You only need to do any of this if you are creating the key from a password. It's not clear that that is what you are doing. \$\endgroup\$