Search code examples
javascriptencryptionopensslaescryptojs

Decrypt openssl AES 256 CBC in browser/CryptoJS


I want to decrypt a string that has been encrypted with openssl on the server like this:

openssl enc -e -aes-256-cbc -pbkdf2 -a -S 0123456789ABCDEF -A -k mypassword

Note this is done providing only a salt and password, and openssl should handle key and IV automatically. Am I too optimistic that this can happen when the browser decrypts too? If at all possible, I want to do it with only those encryption settings, or the bare minimum of increased complexity. In the browser, I'm trying to decrypt with CryptoJS like this:

import * as CryptoJS from 'crypto-js'

const encrypted = <ENCRYPTED_STRING_FROM_SERVER>
const password = 'mypassword'
const salt = '0123456789ABCDEF'
const key = CryptoJS.PBKDF2(password, salt) // Generate key

const bytes = CryptoJS.AES.decrypt(encrypted, key)
const decrypted = bytes.toString(CryptoJS.enc.Utf8)
console.log(decrypted)

But the call to CryptoJS.AES.decrypt errors with Cannot read property '0' of undefined, crypto-js/cipher-core.js:371. The docs for CryptoJS.AES.decrypt are quite thin, and any settings I am aware of to change when calling that func seem to give the same error. Thanks to anyone who can shine light!


Solution

  • In the OpenSSL statement, the iteration count and digest are not specified, so the default values 10000 and SHA256 are used. This is relevant because CryptoJS uses different default values (1 and SHA1).

    CryptoJS applies the OpenSSL format for the ciphertext, i.e. the encrypted data starts with the ASCII encoding of Salted__ followed by the salt and then the ciphertext. Therefore the beginning of the Base64 encoded ciphertext starts always with U2FsdGVkX1.

    CryptoJS uses the WordArray data type, which encapsulates an array of words. A word consists of 4 bytes.

    During decryption, ciphertext and salt must first be separated. Then, key and IV must be determined using PBKDF2. Due to the different default values, iteration count and digest must be specified explicitly. Finally it can be decrypted:

    // 1. Separate ciphertext and salt
    var encrypted = "U2FsdGVkX18BI0VniavN78vlhR6fryIan0VvUrdIr+YeLkDYhO2xyA+/oVXJj/c35swVVkCqHPh9VdRbNQG6NQ=="
    var encryptedWA = CryptoJS.enc.Base64.parse(encrypted);
    var prefixWA = CryptoJS.lib.WordArray.create(encryptedWA.words.slice(0, 8/4));                             // Salted__ prefix
    var saltWA = CryptoJS.lib.WordArray.create(encryptedWA.words.slice(8/4, 16/4));                            // 8 bytes salt: 0x0123456789ABCDEF
    var ciphertextWA = CryptoJS.lib.WordArray.create(encryptedWA.words.slice(16/4, encryptedWA.words.length)); // ciphertext        
    
    // 2. Determine key and IV using PBKDF2
    var password = 'mypassword'
    var keyIvWA = CryptoJS.PBKDF2(
        password, 
        saltWA, 
        {
            keySize: (32+16)/4,          // key and IV
            iterations: 10000,
            hasher: CryptoJS.algo.SHA256
        }
    );
    var keyWA = CryptoJS.lib.WordArray.create(keyIvWA.words.slice(0, 32/4));
    var ivWA = CryptoJS.lib.WordArray.create(keyIvWA.words.slice(32/4, (32+16)/4));
    
    // 3. Decrypt
    var decryptedWA = CryptoJS.AES.decrypt(
        {ciphertext: ciphertextWA}, 
        keyWA, 
        {iv: ivWA}
    );
    var decrypted = decryptedWA.toString(CryptoJS.enc.Utf8)
    console.log(decrypted)
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>

    More details can be found in the CryptoJS documentation.