0
votes

I'm trying to implement AES decryption into one of my C++ program. The idea would be to use the following openSSL command line to generate the ciphered text (but to use the C++ API to decipher) :

openssl enc -aes-256-cbc -in plaintext.txt -base64 -md sha512 -pbkdf2 -pass pass:<passwd>

As the official doc is a bit too complicated I based my implementation on this tutorial to implement the decryption : https://eclipsesource.com/blogs/2017/01/17/tutorial-aes-encryption-and-decryption-with-openssl/

It does works well, but uses a deprecated key-derivation algorithm which I wanna replace with PBKDF2.

As far as I understand I should then use PKCS5_PBKDF2_HMAC() rather than the EVP_BytesToKey() suggested in the tutorial. My problem is that EVP_BytesToKey was able to derivate both key and IV from salt and password, where PKCS5_PBKDF2_HMAC only seems to derivate one at a time.

I couldn't find any more information/tutorial on how to get both key and IV, and tried several implementations, but couldn't find how the openSSL CLI generates the IV. I'd really like to avoid to write the IV in either the CLI or the payload, the implementation of the tutorial was really convenient for that.

Could someone help me ?

Thanks, best regards

1

1 Answers

1
votes

I realize the question is about a month old by now but I came across it in my search of information on doing something similar. Given the lack of answers here I went to the source for answers.

TL;DR (direct answer)

PKCS5_PBKDF2_HMAC() generates both key and IV at the same time. Although it's concatenated to one string. It's up you to split the string into the needed parts.

const EVP_CIPHER *cipher = EVP_aes_256_cbc();
int iklen = EVP_CIPHER_key_length(cipher);
int ivlen = EVP_CIPHER_iv_length(cipher);
PKCS5_PBKDF2_HMAC(pass, -1, salt, 8, iter, EVP_sha512(), iklen + ivlen, keyivpair);
memcpy(key, keyivpair, iklen);
memcpy(iv, keyivpair + iklen, ivlen);

Detailed description

Before going into specifics I feel that I should mention that I'm using C and not C++. I do however hope that the information provided is helpful even for C++.

Before anything else the string needs to be decoded from base64 in the application. After that we can move along to the key and IV generation. The openssl tool indicates that a salt is being used by starting the encrypted string with the string 'Salted__' followed by 8 bytes of salt (at least for aes-256-cbc). In addition to the salt we also need to know the length of both the key and the IV. Luckily there are API calls for this.

const EVP_CIPHER *cipher = EVP_aes_256_cbc();
int iklen = EVP_CIPHER_key_length(cipher);
int ivlen = EVP_CIPHER_iv_length(cipher);

We also need to know the number of iterations (the default in openssl 1.1.1 when using -pbkdf2 is 10000), as well as the message digest function which in this case will be EVP_sha512() (as specified by option -md sha512).

When we have all of the above it's time to call PKCS5_PBKDF2_HMAC().

PKCS5_PBKDF2_HMAC(pass, -1, salt, 8, iter, EVP_sha512(), iklen + ivlen, keyivpair);

Short info on the arguments

  1. pass is of type (const char *)
  2. password length (int), if set to -1 the length will be determined by strlen(pass)
  3. salt is of type (const unsigned char *)
  4. salt length (int)
  5. iteration count (int)
  6. message digest (const EVP_MD *), in this case returned by EVP_sha512()
  7. total length of key + iv (int)
  8. keyivpair (unsigned char *), this is where the key and IV is stored

Now we need to split the key and IV apart and store them i separate variables.

unsigned char key[EVP_MAX_KEY_LENGTH];  
unsigned char iv[EVP_MAX_IV_LENGTH];
memcpy(key, keyivpair, iklen);
memcpy(iv, keyivpair + iklen, ivlen);

And now we have a key and IV which can be used to decrypt data encrypted by the openssl tool.

PoC

To further clarify I wrote the following proof of concept (written on and for Linux).

/*               
 * PoC written by zoke                                                        
 * Compiled with gcc decrypt-poc.c -o decrypt-poc -lcrypto -ggdb3 -Wall -Wextra
 */              
#include <stdio.h>                                                            
#include <stdlib.h>                                                           
#include <string.h>                                                           
#include <openssl/conf.h>                                                     
#include <openssl/evp.h>                                                      
#include <openssl/err.h>                                                      

void bail() {    
  ERR_print_errors_fp(stderr);                                                
  exit(EXIT_FAILURE);                                                         
}                

int main(int argc, char *argv[]) {
  if(argc < 3)   
    bail();      
  unsigned char key[EVP_MAX_KEY_LENGTH];  
  unsigned char iv[EVP_MAX_IV_LENGTH];
  unsigned char salt[8]; // openssl tool uses 8 bytes for salt
  unsigned char decodeddata[256];
  unsigned char ciphertext[256];
  unsigned char plaintext[256];
  const char *pass = argv[1]; // use first argument as password (PoC only)
  unsigned char *encodeddata = (unsigned char *)argv[2]; // use second argument
  int decodeddata_len, ciphertext_len, plaintext_len, len;

  // Decode base64 string provided as second option
  EVP_ENCODE_CTX *ctx;
  if(!(ctx = EVP_ENCODE_CTX_new()))
    bail();      
  EVP_DecodeInit(ctx);
  EVP_DecodeUpdate(ctx, decodeddata, &len, encodeddata, strlen((const char*)encodeddata));
  decodeddata_len = len;
  if(!EVP_DecodeFinal(ctx, decodeddata, &len))
    bail();      
  EVP_ENCODE_CTX_free(ctx);

  // openssl tool format seems to be 'Salted__' + salt + encrypted data
  // take it apart
  memcpy(salt, decodeddata + 8, 8); // 8 bytes starting at 8th byte
  memcpy(ciphertext, decodeddata + 16, decodeddata_len - 16); // all but the 16 first bytes
  ciphertext_len = decodeddata_len - 16;

  // Get some needed information
  const EVP_CIPHER *cipher = EVP_aes_256_cbc();
  int iklen = EVP_CIPHER_key_length(cipher);
  int ivlen = EVP_CIPHER_iv_length(cipher);
  int iter = 10000; // default in openssl 1.1.1
  unsigned char keyivpair[iklen + ivlen];                                     

  // Generate the actual key IV pair                                          
  if(!PKCS5_PBKDF2_HMAC(pass, -1, salt, 8, iter, EVP_sha512(), iklen + ivlen, keyivpair))
    bail();      
  memcpy(key, keyivpair, iklen);                                              
  memcpy(iv, keyivpair + iklen, ivlen);                                       

  // Decrypt data                                                             
  EVP_CIPHER_CTX *cipherctx;                                                  
  if(!(cipherctx = EVP_CIPHER_CTX_new()))                                     
    bail();      
  if(!EVP_DecryptInit_ex(cipherctx, cipher, NULL, key, iv))                   
    bail();      
  if(!EVP_DecryptUpdate(cipherctx, plaintext, &len, ciphertext, ciphertext_len))
    bail();      
  plaintext_len = len;                                                        
  if(!EVP_DecryptFinal_ex(cipherctx, plaintext + len, &len))                  
    bail();      
  plaintext_len += len;                                                       
  EVP_CIPHER_CTX_free(cipherctx);                                             
  plaintext[plaintext_len] = '\0'; // add null termination                    

  printf("%s", plaintext);                                                    

  exit(EXIT_SUCCESS);
}

Application tested by running

$ openssl aes-256-cbc -e -a -md sha512 -pbkdf2 -pass pass:test321 <<< "Some secret data"
U2FsdGVkX19ZNjDQXX/aACg7d4OopxqvpjclkaSuybeAxOhVRIONXoCmCQaG/Vg9
$ ./decrypt-poc test321 U2FsdGVkX19ZNjDQXX/aACg7d4OopxqvpjclkaSuybeAxOhVRIONXoCmCQaG/Vg9
Some secret data

The Key/IV generation used by the command line tool is in apps/enc.c and was very helpful when figuring this out.