Search code examples
javascriptencryptionopensslaescryptojs

how to decrypt a file that was encrypted with CryptoJs3+ library with custom iterations and key sizes, in openssl


I have a text file that was encrypted by cryptojs(and maybe some custom js function to iterate the hash of the password) and I want to decrypt it in windows via openssl.exe not with a browser(which works flawlessly when i provide it with password). it looks like cryptojs has used some custom stuff like deriving key with sha512 and iterating it 11512 times as you see the js decryptor below and then uses evpkdf iteration 484 times(i have no idea what this stuff mean). the code snippet for decrypting the file from the cryptojs is below, i need the openssl exe -cli params to do so without using that js library in a browser.

function hex2a(t) { //hex to ascii
    for (var e = t.toString(), i = "", n = 0; n < e.length && "00" !== e.substr(n, 2); n += 2) i += String.fromCharCode(parseInt(e.substr(n, 2), 16));
    return i; }

    function decoder(secret, passwrd) {
        for (var i = CryptoJS.SHA512(passwrd), n = 0; n < 11512; n++) 
i = CryptoJS.SHA512(i);
        (CryptoJS.algo.AES.keySize = 32), 
(CryptoJS.algo.EvpKDF.cfg.iterations = 1e4), 
(CryptoJS.algo.EvpKDF.cfg.keySize = 32);
        var r = CryptoJS.AES.decrypt(secret, i.toString());
        return (out = hex2a(r)), out;
    }

this is when it calls the function and returns the file if it was successfully decrypted.

var msg="base64 of the salted encrypted file via cryptojs"
function proceed() {
    var pass=document.getElementById('textfield2').value;
    a=decoder(msg, pass);
    if (a.search('{version')>-1) {
        document.getElementById('status').innerHTML="SUCCESS";
        download("result.json",a);
    }   else {
document.getElementById('status').innerHTML="FAILED";
}

I have tried this(windows openssl.exe cli 1.1) and got an error about -iter and other param. dont know how to give it key size 32 and iteration and evpkdf and hatever that js file does to decrypt.

OpenSSL.exe enc -aes-256-cbc -md md5 -d -pass pass:"simpletext" -in "tiny.bin" -out "result.txt"

Solution

  • Decryption with OpenSSL is principally not possible for several reasons:

    • The specified key size AES.keySize = 32 defines a 128 bytes key (which is not passed directly here, but derived from a password). Presumably a 32 bytes key was intended, but CryptoJS specifies key sizes in words, where a word consists of 4 bytes.
      AES is only defined for 16/24/32 bytes keys, i. e. not for the 128 bytes key used. Although the key is invalid for AES, CryptoJS processes this key due to a bug (Issue #293).
      From the key size the round number is derived directly, which results here to 38, (source code). However, AES is only defined for the round numbers 10 (AES-128), 12 (AES-192) and 14 (AES-256) (AES, Security). The generated ciphertext is therefore not AES compatible, i.e. decryption with an AES-compliant tool, in particular OpenSSL, is therefore generally impossible.
      For decryption with OpenSSL to be possible, AES.keySize needs to have one of the values 4/6/8, which corresponds to the permitted round numbers 10/12/14 or AES-128/192/256. The configuration AES.keySize = 32 is therefore a KO criterion for all AES-compliant tools like OpenSSL.

    • CryptoJS uses the OpenSSL function EVP_BytesToKey() for key derivation. However, the specified iteration count EvpKDF.cfg.iterations = 1e4 is not supported by OpenSSL for key derivation with EVP_BytesToKey(), only the value 1 (here, last part).
      More modern OpenSSL versions (from 1.1.1) have the option -iter, but this is only used in conjunction with the key derivation PBKDF2, i.e. if -iter is specified, PBKDF2 is automatically used instead of EVP_BytesToKey(), which is not compatible with CryptoJS.
      For decryption to be possible with OpenSSL, EvpKDF.cfg.iterations would need to have the value 1. The configuration EvpKDF.cfg.iterations = 1e4 is therefore a KO criterion for OpenSSL.

    • The password passed to CryptoJS is derived from the original password by multiple hashing with SHA512. As already explained in Artjom B.'s comment, this cannot be implemented with OpenSSL alone, but it would at least be possible as part of a script.

    • The EvpKDF.cfg.keySize parameter has no effect on encryption/decryption and can be ignored.

    If compatible values are used in the posted code instead of the incompatible values, e.g. CryptoJS.algo.AES.keySize = 8 and CryptoJS.algo.EvpKDF.cfg.iterations = 1, decryption with OpenSSL is possible. The following example uses the posted code with compatible values and performs encryption and decryption. The ciphertext can be decrypted with OpenSSL:

    //
    // Your code
    //
    function hex2a(t) {
        for (var e = t.toString(), i = "", n = 0; n < e.length && "00" !== e.substr(n, 2); n += 2) i += String.fromCharCode(parseInt(e.substr(n, 2), 16));
        return i; 
    }
    
    function decoder(secret, passwrd) {
        for (var i = CryptoJS.SHA512(passwrd), n = 0; n < 11512; n++) i = CryptoJS.SHA512(i);
        CryptoJS.algo.AES.keySize = 8;            // compatible with OpenSSL: 4/6/8, corresponds to AES-128/192/265
        CryptoJS.algo.EvpKDF.cfg.iterations = 1;  // compatible with OpenSSL: 1
        CryptoJS.algo.EvpKDF.cfg.keySize = 123;   // ignored
        var r = CryptoJS.AES.decrypt(secret, i.toString());
        return (out = hex2a(r)), out;
    }
    
    // 
    // The encryption counterpart
    //
    function encrypt(secret, passwrd) {
        for (var i = CryptoJS.SHA512(passwrd), n = 0; n < 11512; n++) i = CryptoJS.SHA512(i);
        CryptoJS.algo.AES.keySize = 8;
        CryptoJS.algo.EvpKDF.cfg.iterations = 1;
        CryptoJS.algo.EvpKDF.cfg.keySize = 456;
        console.log("CryptoJS input passphrase:\n", i.toString().replace(/(.{56})/g,'$1\n'));  
        var ciphertext = CryptoJS.AES.encrypt(secret, i.toString());
        return ciphertext;
    }
    
    // 
    // Encryption, decryption
    //
    var ciphertext = encrypt('The quick brown fox jumps over the lazy dog', 'my initial passphrase');
    var decryptedData = decoder(ciphertext, 'my initial passphrase');
    console.log("Ciphertext:\n", ciphertext.toString().replace(/(.{56})/g,'$1\n'));  
    console.log("Plaintext:\n", decryptedData.replace(/(.{56})/g,'$1\n'));  
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>

    with the possible output:

    CryptoJS input passphrase: 7bff3331f9dcfbb2e1c5b6cb4170689db91fe5b12258d59672fe0e9ce61780c61f3e6dc324e58cc2170c6a8010083aafe07930708ee63c0022a7e9bce784c4c5
    Ciphertext: U2FsdGVkX1/3SLTOsvc6CoWfg53rR+l//pHiWstJibl5D5OopIFWVmhUDhEpj7zBZRjA1vQiIoU2F5qR1v8NEw==
    Plaintext: The quick brown fox jumps over the lazy dog
    

    The ciphertext can be decrypted with the following OpenSSL statement (using the derived password directly):

    openssl enc -aes-256-cbc -d -a -A -in <path to ciphertext file> -md md5 -pass pass:7bff3331f9dcfbb2e1c5b6cb4170689db91fe5b12258d59672fe0e9ce61780c61f3e6dc324e58cc2170c6a8010083aafe07930708ee63c0022a7e9bce784c4c5 
    

    When using parameters different from these (e.g. AES.keySize = 32 or EvpKDF.cfg.iterations = 1e4), decryption generally fails with bad decrypt (even when using -iter 10000).