Search code examples
javascriptencryptioncryptographycryptojs

Decrypting CryptoJS AES data (with passphrase) in Forge.js


So I have a piece of code which encrypts a string with a passphrase. It uses the CryptoJS AES encrypt function (CryptoJS.AES.encrypt) and looks like this...

CryptoJS.AES.encrypt(data, password).toString();

Going forward, I don't want to be using CryptoJS, as it's officially deprecated/not maintained, and I would instead like to use Forge.js. I've attempted to read through the Forge.js docs on GitHub to find a solution, but haven't been able to find anything which uses passphrases instead of manually creating the key & IV.

I've taken a look at the CryptoJS archive at https://code.google.com/archive/p/crypto-js/ and it seems that if the encrypt function is passed a string as the second argument (key) it's used as a passphrase to derive a key and IV. But it doesn't detail how it does this.

It seems that base64 decoding the result gives a string that starts with Salted__ then a comma and then the encrypted blob of binary text, and I'm unsure even how I would pass the "salt" through to Forge.

How would I go about decrypting this blob of data using Forge.js only?


Solution

  • CryptoJS supports OpenSSL's EVP_BytesToKey function, which derives a key and IV from a freshly generated salt and password with one round of MD5. There is an example on the forge documentation page:

    Using forge in node.js to match openssl's "enc" command line tool (Note: OpenSSL "enc" uses a non-standard file format with a custom key derivation function and a fixed iteration count of 1, which some consider less secure than alternatives such as OpenPGP/GnuPG):

    var forge = require('node-forge');
    var fs = require('fs');
    
    // openssl enc -des3 -in input.txt -out input.enc
    function encrypt(password) {
      var input = fs.readFileSync('input.txt', {encoding: 'binary'});
    
      // 3DES key and IV sizes
      var keySize = 24;
      var ivSize = 8;
    
      // get derived bytes
      // Notes:
      // 1. If using an alternative hash (eg: "-md sha1") pass
      //   "forge.md.sha1.create()" as the final parameter.
      // 2. If using "-nosalt", set salt to null.
      var salt = forge.random.getBytesSync(8);
      // var md = forge.md.sha1.create(); // "-md sha1"
      var derivedBytes = forge.pbe.opensslDeriveBytes(
        password, salt, keySize + ivSize/*, md*/);
      var buffer = forge.util.createBuffer(derivedBytes);
      var key = buffer.getBytes(keySize);
      var iv = buffer.getBytes(ivSize);
    
      var cipher = forge.cipher.createCipher('3DES-CBC', key);
      cipher.start({iv: iv});
      cipher.update(forge.util.createBuffer(input, 'binary'));
      cipher.finish();
    
      var output = forge.util.createBuffer();
    
      // if using a salt, prepend this to the output:
      if(salt !== null) {
        output.putBytes('Salted__'); // (add to match openssl tool output)
        output.putBytes(salt);
      }
      output.putBuffer(cipher.output);
    
      fs.writeFileSync('input.enc', output.getBytes(), {encoding: 'binary'});
    }
    
    // openssl enc -d -des3 -in input.enc -out input.dec.txt
    function decrypt(password) {
      var input = fs.readFileSync('input.enc', {encoding: 'binary'});
    
      // parse salt from input
      input = forge.util.createBuffer(input, 'binary');
      // skip "Salted__" (if known to be present)
      input.getBytes('Salted__'.length);
      // read 8-byte salt
      var salt = input.getBytes(8);
    
      // Note: if using "-nosalt", skip above parsing and use
      // var salt = null;
    
      // 3DES key and IV sizes
      var keySize = 24;
      var ivSize = 8;
    
      var derivedBytes = forge.pbe.opensslDeriveBytes(
        password, salt, keySize + ivSize);
      var buffer = forge.util.createBuffer(derivedBytes);
      var key = buffer.getBytes(keySize);
      var iv = buffer.getBytes(ivSize);
    
      var decipher = forge.cipher.createDecipher('3DES-CBC', key);
      decipher.start({iv: iv});
      decipher.update(input);
      var result = decipher.finish(); // check 'result' for true/false
    
      fs.writeFileSync(
        'input.dec.txt', decipher.output.getBytes(), {encoding: 'binary'});
    }
    

    This example is shown for Triple DES, but it works in the same way for AES. You just have to change the ivSize to 16.