Search code examples
google-apps-scriptencryptionopensslcryptojs

How to decrypt cCryptoGS (CryptoJS) string using OpenSSL?


Sample function in Google Apps Script using the cCryptoGS library:

function encrypt() {

  let message = "Hello world!";
  let pw = "password";

  let encrypted = cCryptoGS.CryptoJS.AES.encrypt(message, pw).toString();

  Logger.log(encrypted);

  // Sample result, changes each time due to the salt
  // U2FsdGVkX19A/TPmx/tmR9MRiKU9AQPhUYKD/lyoY/c=

};

Trying to decrypt using OpenSSL:

echo "U2FsdGVkX19A/TPmx/tmR9MRiKU9AQPhUYKD/lyoY/c=" | openssl enc -d -a -A -aes-256-cbc -iter 1 -md md5 -pass pass:'password' && echo

That command returned an error:

bad decrypt
803B3E80A67F0000:error:1C800064:Provider routines:ossl_cipher_unpadblock:bad decrypt:../providers/implementations/ciphers/ciphercommon_block.c:124:

OpenSSL version is OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022).

How could the cyphertext generated by cCryptoGS be decrypted using OpenSSL?


Solution

  • If the key material is passed as string, it is interpreted as password and CryptoJS performs an OpenSSL compatible key derivation. For this purpose, CryptoJS implements under the hood the OpenSSL proprietary key derivation function EVP_BytesToKey() with MD5 as digest. Therefore, decryption with OpenSSL is possible with the following statement:

    echo "U2FsdGVkX19A/TPmx/tmR9MRiKU9AQPhUYKD/lyoY/c=" | openssl enc -d -a -A -aes-256-cbc -md md5 -pass pass:'password'
    

    Be aware that OpenSSL originally used MD5 as default digest, but as of v1.1.0 it applies SHA-256. In contrast, CryptoJS uses MD5 exclusively. Therefore, for OpenSSL versions before v1.1.0, the -md md5 option can be omitted.


    A more secure alternative

    The OpenSSL statement posted in the comment contains the -iter 1 option. This option sets the iteration count and defines as key derivation function implicitly PBKDF2 (so it is equivalent to -iter 1 -pbkdf2, see here). This is the reason why decryption fails.

    However, PBKDF2 is more secure. The deprecated key derivation EVP_BytesToKey() is considered insecure since it uses the broken digest MD5 and an iteration count of only 1, see here. More modern OpenSSL CLI versions issue a warning accordingly:

    *** WARNING : deprecated key derivation used. Using -iter or -pbkdf2 would be better. 
    

    PBKDF2, on the other hand, is a reliable key derivation function that is also supported by CryptoJS. Therefore, PBKDF2 should be used for security reasons.

    Unfortunately, for the built-in key derivation, CryptoJS does not support out-of-the-box switching from one key derivation function to the other. The built-in key derivation internally calls OpenSSLKdf.execute(), which uses EVP_BytesToKey(). Therefore, a corresponding function must be implemented (called OpenSSLPbkdf2 in the following) that applies PBKDF2, e.g.:

    var OpenSSLPbkdf2 = {
        execute: function(password, keySize, ivSize, salt, hasher) {
            if (!salt) {
                salt = CryptoJS.lib.WordArray.random(8);
            }
            var key = CryptoJS.PBKDF2(password, salt, pbkdf2Params);
            var iv = CryptoJS.lib.WordArray.create(key.words.slice(keySize), ivSize * 4);
            key.sigBytes = keySize * 4;
            return CryptoJS.lib.CipherParams.create({ key: key, iv: iv, salt: salt });
        }
    };
    

    and which is specified in AES.encrypt()/AES.decrypt() with {kdf: OpenSSLPbkdf2}. pbkdf2Params sets the PBKDF2 parameters (digest, iteration count and keysize). The iteration count should be as high as possible while maintaining acceptable performance. The recommended digest is SHA-256 (the OpenSSL default digest as of v1.1.0).

    Example:

    var pbkdf2Params = {
        hasher: CryptoJS.algo.SHA256,
        iterations: 10000,
        keySize: (256 + 128)/32,
    };        
    
    var OpenSSLPbkdf2 = {
        execute: function(password, keySize, ivSize, salt, hasher) {
            if (!salt) {
                salt = CryptoJS.lib.WordArray.random(8);
            }
            var key = CryptoJS.PBKDF2(password, salt, pbkdf2Params);
            var iv = CryptoJS.lib.WordArray.create(key.words.slice(keySize), ivSize * 4);
            key.sigBytes = keySize * 4;
            return CryptoJS.lib.CipherParams.create({ key: key, iv: iv, salt: salt });
        }
    };
    
    var password = 'password';
    var plaintext = 'Hello world!';
    
    // Encryption
    var ciphertextParams = CryptoJS.AES.encrypt(plaintext, password, {kdf: OpenSSLPbkdf2});
    console.log(ciphertextParams.toString());
    
    // Decryption (ciphertext in OpenSSL format)
    var dec = CryptoJS.AES.decrypt(ciphertextParams.toString(), password, {kdf: OpenSSLPbkdf2});
    console.log(dec.toString(CryptoJS.enc.Utf8));
    
    // Decryption (ciphertext as CipherParams object)
    var dec = CryptoJS.AES.decrypt(ciphertextParams, password, {kdf: OpenSSLPbkdf2});
    console.log(dec.toString(CryptoJS.enc.Utf8));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>

    The data generated in this way can be decrypted with the following OpenSSL statement:

    echo "U2FsdGVkX1+K5JPuhBTtQdvUzcfvu2rbFNuBq2QTDzI=" | openssl enc -d -a -A -aes-256-cbc -iter 10000 -md sha256 -pass pass:'password'
    

    this time without warning.