caescryptojscbc-mode

Modify tiny-AES-c library to decrypt data encrypted with non-standard 128 byte AES key (CryptoJS's AES-CBC with keysize of 128 bytes)


I have some data encrypted via CryptoJS's CryptoJS.AES.encrypt(message, 'passphrase123') function. If a passphrase is supplied, CryptoJS uses the passphrase to derive an AES key and IV to encrypt the data.

You can specify the AES key size via: CryptoJS.algo.AES.keySize.

For example, CryptoJS.algo.AES.keySize = 8 would result in a key size of 32 bytes (AES256), because CryptoJS specifies the key size in multiples of words (times 4 bytes).

The data I have encrypted was encrypted with CryptoJS.algo.AES.keySize = 32, which resulted in a 128 byte AES key (not a standard AES key size, and shouldn't be allowed, but CryptoJS does not give any errors when encrypting with a non-standard AES key size, but instead encrypts the data anyways)

I have been trying to write a C program using a modified version of tiny-AES-c to be able to decrypt the data, but I'm afraid it's not as simple as changing something like "keysize = 128".

Here is my code. It takes the encrypted string (Base64 encoded), decodes it, extracts the salt and the ciphertext (prefix is "Salted__", and salt is from byte 8 to byte 16, ciphertext is from byte 16 onwards). Then it derives a 128 byte AES key and 16 byte IV using the salt and a passphrase. Then it attempts to decrypt the ciphertext using the non-standard key and IV.

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <inttypes.h>
#include "aes.h" //TinyAES library
#include "derivekey.h" //Derives a 128 byte AES key and 16 byte IV from a repeated MD5 hash of the passphrase and salt provided.

#define CBC 1
#define AES256 1 //I define AES256 for tiny-AES-c, even though it's 1024 bits, not 256 bits

static int decrypt_message(void);


int main(void)
{
    int exit;

    exit = decrypt_message();

    return exit;
}

const char b64chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

int b64invs[] = { 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58,
    59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5,
    6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
    21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28,
    29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
    43, 44, 45, 46, 47, 48, 49, 50, 51 };
    

size_t b64_get_decoded_size(const char *in)
{
    size_t len;
    size_t ret;
    size_t i;

    if (in == NULL)
        return 0;

    len = strlen(in);
    ret = len / 4 * 3;

    for (i=len; i-->0; ) {
        if (in[i] == '=') {
            ret--;
        } else {
            break;
        }
    }

    return ret;
}


int b64_decode(const char *in, unsigned char *out, size_t outlen)
{
    size_t len;
    size_t i;
    size_t j;
    int    v;

    if (in == NULL || out == NULL)
        return 0;

    len = strlen(in);
    if (outlen < b64_get_decoded_size(in) || len % 4 != 0)
        return 0;

    for (i=0, j=0; i<len; i+=4, j+=3) {
        v = b64invs[in[i]-43];
        v = (v << 6) | b64invs[in[i+1]-43];
        v = in[i+2]=='=' ? v << 6 : (v << 6) | b64invs[in[i+2]-43];
        v = in[i+3]=='=' ? v << 6 : (v << 6) | b64invs[in[i+3]-43];

        out[j] = (v >> 16) & 0xFF;
        if (in[i+2] != '=')
            out[j+1] = (v >> 8) & 0xFF;
        if (in[i+3] != '=')
            out[j+2] = v & 0xFF;
    }

    return 1;
}



static int decrypt_message(void)
{
    char       *out;
    size_t      out_len;
    char *msg = "U2FsdGVkX1Zr2Kg8jTR63TAZvbY15e5R5gmV8PonVOykoOYsnXzehHf0fq97CixhocTP9/7wJ1pQ==";
    
    out_len = b64_get_decoded_size(msg)+1;
    out = malloc(out_len);
    
    if (!b64_decode(msg, (unsigned char *)out, out_len)) {
        printf("Decode Failure\n");
        return 1;
    }

    char *cipherprefix;
    char *ciphersalt;
    char *ciphertext;
    
    cipherprefix = malloc(8); //Should be "Salted__"
    ciphersalt = malloc(8);
    ciphertext = malloc(out_len-16);

    for(int i1=0; i1<8; i1++){  
            cipherprefix[i1] = out[i1];
    }
    for(int i2=8; i2<16; i2++){
            ciphersalt[i2-8] = out[i2];
    }
    for(int i3=16; i3<strlen(out); i3++){
            ciphertext[i3-16] = out[i3];
    }

    uint8_t key;
    uint8_t iv;
    
    char* pwd = "passphrase123";
    derive_aes1024_key(pwd, ciphersalt, key, iv);
    
    struct AES_ctx ctx;
    AES_init_ctx_iv(&ctx, key, iv);
    AES_CBC_decrypt_buffer(&ctx, ciphertext, sizeof(ciphertext));
    
    printf((char*) ciphertext);

}

Now of course this fails as tiny-AES-c does not support AES keys of 128 bytes.

I tried modifying tiny-AES-c's aes.c:

/*****************************************************************************/
/* Defines:                                                                  */
/*****************************************************************************/
// The number of columns comprising a state in AES. This is a constant in AES. Value=4
#define Nb 4

#if defined(AES256) && (AES256 == 1)
    #define Nk 32 //changed from 8 (32 = 128bytes / 4)
    #define Nr 38 //changed from 14 (CryptoJS uses algorthim to determine rounds: rounds = key/32 + 6. So [1024bits / 32] + 6)
#elif defined(AES192) && (AES192 == 1)
    #define Nk 6
    #define Nr 12
#else
    #define Nk 4        // The number of 32 bit words in a key.
    #define Nr 10       // The number of rounds in AES Cipher.
#endif

I've also tried changing tiny-AES-c's aes.h:

#define AES_BLOCKLEN 16 // Block length in bytes

#if defined(AES256) && (AES256 == 1)
    #define AES_KEYLEN 128 //changed from 32
    #define AES_keyExpSize 624 // Default value is 240, I'm not sure if this is what I'm supposed to change this to
#elif defined(AES192) && (AES192 == 1)
    #define AES_KEYLEN 24
    #define AES_keyExpSize 208
#else
    #define AES_KEYLEN 16   // Key length in bytes
    #define AES_keyExpSize 176
#endif

Running my C program with the correct key and IV does not correctly decrypt the data.

Could anyone with a better understanding of this at least point me in the right direction?

Thanks a bunch!


Solution

  • In advance: The CryptoJS bug in question is #293 and it should be noted that although the AES logic is used, it will no longer be encrypted with AES if key lengths other than 16, 24 or 32 bytes are used!


    The problem actually consists of two parts, on the one hand the AES encryption logic must be modified, on the other the key derivation.

    Modification of the AES encryption/decryption logic:

    The CryptoJS bug can be ported relatively easily into the tiny-AES-c library by adding another case, AES1024, analogous to the existing AES128, AES192 and AES256 cases. The following changes are necessary for this:

    ...
    #define AES1024 1
    //#define AES128 1
    ...
    #if defined(AES1024) && (AES1024 == 1)
        #define AES_KEYLEN 128
        #define AES_keyExpSize 624
    #elif defined(AES256) && (AES256 == 1)
    ...
    
    ...
    #if defined(AES1024) && (AES1024 == 1)
        #define Nk 32
        #define Nr 38
    #elif defined(AES256) && (AES256 == 1)
    ...
    
    ... 
    #if (defined(AES256) && (AES256 == 1)) || (defined(AES1024) && (AES1024 == 1))
    ...
    
    ...
    #if defined(AES1024)
        uint8_t len = 128;
    #elif defined(AES256)
    ...
    
    ...
    #if defined(AES1024)
        printf("\nTesting AES1024\n\n");
    #elif defined(AES256)
    ...
    exit = test_decrypt_cbc();
    ...
    

    For a test, the following CryptoJS code can be used to create test data:

    function encrypt(plaintext, password) {
        CryptoJS.algo.AES.keySize = 32, CryptoJS.algo.EvpKDF.cfg.iterations = 1e4;
        return CryptoJS.AES.encrypt(plaintext, password);
    }
    
    var password = 'my password'
    var plaintext = 'The quick brown fox jumps over the lazy dog';
    var ciphertextCP = encrypt(plaintext, password);
    console.log("ct, OpenSSL format", ciphertextCP.toString());
    console.log("key", ciphertextCP.key.toString());
    console.log("salt", ciphertextCP.salt.toString());
    console.log("iv", ciphertextCP.iv.toString());
    console.log("ct", ciphertextCP.ciphertext.toString());
    

    Sample test data:

    ct, OpenSSL format:  U2FsdGVkX1/qEV9gBeaAHNSOdQrqAZZaqMuPP8ZZI70Y02Kx+D8GJyZo+ZqQ69M7wG653AIlTdh5CkwT9X5l5A==
    key:  df1776c8c0231c2401233c9908f0659a0b2a0afbda283a86f78d4b0be2a397b43c148499e8f0cd5f1cef68776e5eb5a1fa0c4e06985945c6e967bf0b5d2c03e9321f4395085679000b27a3a8614598cfca067b0804b9659febc2e2364d20aa6d4096270a726e2f9e326ec8cf405b0d52d27ffde1f27ef54ed19e22c6ab9d27a1
    salt:  ea115f6005e6801c
    iv:  6e9ed44587400cf97db58cc85ee62d4c
    ct:  d48e750aea01965aa8cb8f3fc65923bd18d362b1f83f06272668f99a90ebd33bc06eb9dc02254dd8790a4c13f57e65e4
    

    For the test, the existing function test_decrypt_cbc() has to be adapted with this test data as follows:

    static int test_decrypt_cbc(void)
    {
        uint8_t key[] = { 0xdf, 0x17, 0x76, 0xc8, 0xc0, 0x23, 0x1c, 0x24, 0x01, 0x23, 0x3c, 0x99, 0x08, 0xf0, 0x65, 0x9a,
                          0x0b, 0x2a, 0x0a, 0xfb, 0xda, 0x28, 0x3a, 0x86, 0xf7, 0x8d, 0x4b, 0x0b, 0xe2, 0xa3, 0x97, 0xb4,
                          0x3c, 0x14, 0x84, 0x99, 0xe8, 0xf0, 0xcd, 0x5f, 0x1c, 0xef, 0x68, 0x77, 0x6e, 0x5e, 0xb5, 0xa1,
                          0xfa, 0x0c, 0x4e, 0x06, 0x98, 0x59, 0x45, 0xc6, 0xe9, 0x67, 0xbf, 0x0b, 0x5d, 0x2c, 0x03, 0xe9,
                          0x32, 0x1f, 0x43, 0x95, 0x08, 0x56, 0x79, 0x00, 0x0b, 0x27, 0xa3, 0xa8, 0x61, 0x45, 0x98, 0xcf,
                          0xca, 0x06, 0x7b, 0x08, 0x04, 0xb9, 0x65, 0x9f, 0xeb, 0xc2, 0xe2, 0x36, 0x4d, 0x20, 0xaa, 0x6d,
                          0x40, 0x96, 0x27, 0x0a, 0x72, 0x6e, 0x2f, 0x9e, 0x32, 0x6e, 0xc8, 0xcf, 0x40, 0x5b, 0x0d, 0x52,
                          0xd2, 0x7f, 0xfd, 0xe1, 0xf2, 0x7e, 0xf5, 0x4e, 0xd1, 0x9e, 0x22, 0xc6, 0xab, 0x9d, 0x27, 0xa1 };
        uint8_t out[48];
    
        uint8_t iv[] = { 0x6e, 0x9e, 0xd4, 0x45, 0x87, 0x40, 0x0c, 0xf9, 0x7d, 0xb5, 0x8c, 0xc8, 0x5e, 0xe6, 0x2d, 0x4c };
        uint8_t in[] = { 0xd4, 0x8e, 0x75, 0x0a, 0xea, 0x01, 0x96, 0x5a, 0xa8, 0xcb, 0x8f, 0x3f, 0xc6, 0x59, 0x23, 0xbd, 
                         0x18, 0xd3, 0x62, 0xb1, 0xf8, 0x3f, 0x06, 0x27, 0x26, 0x68, 0xf9, 0x9a, 0x90, 0xeb, 0xd3, 0x3b, 
                         0xc0, 0x6e, 0xb9, 0xdc, 0x02, 0x25, 0x4d, 0xd8, 0x79, 0x0a, 0x4c, 0x13, 0xf5, 0x7e, 0x65, 0xe4 };
        struct AES_ctx ctx;
    
        AES_init_ctx_iv(&ctx, key, iv);
        AES_CBC_decrypt_buffer(&ctx, in, 48);
    
        printf("CBC decrypt (Utf-8): ");
        for (int i = 0; i < sizeof(in); i++) {
            printf("%c", in[i]);
        }
        printf("\n");
        printf("CBC decrypt (hex):   ");
        for (int i = 0; i < sizeof(in); i++) {
            printf("%02X", in[i]);
        }
        printf("\n");
        return(0);
    }
    

    When the code is executed, the ciphertext is decrypted correctly and the following output is produced:

    CBC decrypt (Utf-8): The quick brown fox jumps over the lazy dog
    CBC decrypt (hex):   54686520717569636B2062726F776E20666F78206A756D7073206F76657220746865206C617A7920646F670505050505
    

    Note that the CryptoJS code pads/unpads with PKCS#7, but tiny-AES-c does not, so that the padding bytes at the end (0x0505050505) are not removed during decryption. If you want this, the padding/unpadding must still be added.


    Adaptation of the key derivation:

    So far, the key derivation and the separation of salt and ciphertext from the Base64 encoded OpenSSL format (Salted__ + 8 bytes salt + ciphertext) have not yet been taken into account. CryptoJS uses the proprietary OpenSSL function EVP_BytesToKey() as key derivation function. In addition to salt and password, further input parameters are a digest (MD5 by default) and an iteration count (1 by default). Output parameters are key and IV.
    You must adapt the parameters used in the key derivation to the parameters used in the CryptoJS code, e.g. the above CryptoJS code and the test data apply the default digest MD5, but an iteration count of 10000, which differs from the default iteration count.

    For the key derivation, it would be most convenient if EVP_BytesToKey from the OpenSSL library could be used. However, this requires the algorithm associated with the key as the first parameter. If, for instance, EVP_aes_256_cbc() is used here, then only the first 32 bytes of the key are determined. I have not found an algorithm that firstly allows a 128 bytes key and secondly is supported by EVP_BytesToKey(), so that this approach can probably not be used.
    Alternatively, EVP_BytesToKey() can be customized. An implementation of EVP_BytesToKey() can be found in this SO post. The customization simply consists of removing the first parameter (const EVP_CIPHER* type), replacing EVP_CIPHER_key_length(type) with 128, EVP_CIPHER_iv_length(type) with 16 and removing the two OPENSSL_assert() calls regarding key and IV length.

    With the key derivation function adapted in this way, key and IV can then be determined from salt and password:

    #include <string>
    #include <openssl/evp.h>
    
    #define AES_KEY_SIZE 128  
    #define AES_BLOCK_SIZE 16
    #define ITERATIONS 10000
    
    ...
    
    uint8_t encrypted_data_b64[] = "U2FsdGVkX1/qEV9gBeaAHNSOdQrqAZZaqMuPP8ZZI70Y02Kx+D8GJyZo+ZqQ69M7wG653AIlTdh5CkwT9X5l5A=="; // Base64 encoding of Salted__|salt|ciphertext
    int encrypted_data_b64_len = strlen((char*)encrypted_data_b64);
    
    // Base64 decode
    int encrypted_data_len = (encrypted_data_b64_len * 3) / 4;
    uint8_t* encrypted_data = (uint8_t*)malloc(encrypted_data_len);
    encrypted_data_len = EVP_DecodeBlock(encrypted_data, encrypted_data_b64, encrypted_data_b64_len);
    while (encrypted_data_b64[--encrypted_data_b64_len] == '=') encrypted_data_len--; // consider Base64 padding
    
    // Split salt and ciphertext
    uint8_t* salt = (uint8_t*)malloc(8);
    memcpy(salt, encrypted_data + 8, 8);
    uint8_t* ciphertext = (uint8_t*)malloc(encrypted_data_len - 16);
    memcpy(ciphertext, encrypted_data + 16, encrypted_data_len - 16);
    
    // Key, IV derivation using the customized EVP_BytesToKey()
    uint8_t password[] = "my password";
    uint8_t* key = (uint8_t*)malloc(AES_KEY_SIZE);
    uint8_t* iv = (uint8_t*)malloc(AES_BLOCK_SIZE);
    EVP_BytesToKey_cstm(EVP_md5(), salt, password, strlen((char*)password), ITERATIONS, key, iv);
    for (int i = 0; i < AES_KEY_SIZE; i++) {
        printf("%02x", key[i]);
    } // df1776c8c0231c2401233c9908f0659a0b2a0afbda283a86f78d4b0be2a397b43c148499e8f0cd5f1cef68776e5eb5a1fa0c4e06985945c6e967bf0b5d2c03e9321f4395085679000b27a3a8614598cfca067b0804b9659febc2e2364d20aa6d4096270a726e2f9e326ec8cf405b0d52d27ffde1f27ef54ed19e22c6ab9d27a1
    printf("\n");
    for (int i = 0; i < AES_BLOCK_SIZE; i++) {
        printf("%02x", iv[i]);
    } // 6e9ed44587400cf97db58cc85ee62d4c
    printf("\n"); 
    

    With the key and IV determined in this way, the ciphertext can now be decrypted with the modified tiny-AES-c library as described in the first part.