javascriptnode.jsencryptioncryptographycryptojs

crypto-js output is different from node crypto


We are trying to match the crypto and crypto-js output. Our requirement is to encrypt string in browser but crypto will not support browser side encryption. So, we are trying to match the output using crypto-js. Each time crypto-js produces different output.

const crypto = require('crypto');
const CryptoJS = require('crypto-js');

const payload = {
    name: 'John Doe'
}

// Node Package
function encryptNode(text) {
    const cipher = crypto.createCipher('aes-256-cbc', 'devOps');
    return cipher.update(Buffer.from(JSON.stringify(text)), 'utf8', 'hex') + cipher.final('hex');
}
console.log(encryptNode(payload));


// Browser Package
function encryptBrowser(text) {
    const ciphertext = CryptoJS.AES.encrypt(JSON.stringify(text), 'devOps', { mode: CryptoJS.mode.CBC });
    return ciphertext.toString(CryptoJS.format.Hex);
}
console.log(encryptBrowser(payload));

Output:

crypto(Expected): dfe03c7e825e9943aa6ec61deb4a8a73fdba0016a13c59c628ce025f39d44c7c

crypto-js: 4e5453abe7bd53d67d88aa4f040356c649fe0101366d05ce4c7d625cfd052cdc


Solution

  • crypto.createCipher and CryptoJS.AES.encrypt utilize the functionality of the OpenSSL-function EVP_BytesToKey, which expects a password, an optional 8-byte salt, a digest and an iteration count, and generates a key and an IV from these data. Both functions use the MD5 digest and an iteration count of 1 as fixed parameters.

    crypto.createCipher doesn't use a salt, so that the same key and IV are generated each time and thus the same ciphertext (assuming the same plaintext).

    In contrast, CryptoJS.AES.encrypt generates a random salt each time (here and here), so that a different key and IV are generated each time, and thus a different ciphertext (even for an identical plaintext). For decryption (beside the password) the salt is needed, which isn't secret and can be passed together with the ciphertext (e.g. in a CipherParams-object to CryptoJS.AES.decrypt).

    The posted code snippet therefore behaves as expected: The ciphertext created with crypto.createCipher doesn't change, the ciphertext created with CryptoJS.AES.encrypt changes each time.

    EVP_BytesToKey is weak and shouldn't be used for security reasons (at least when using MD5 and an iteration count of 1). The same applies to crypto.createCipher (even more so because of the missing salt), which is deprecated anyway, and that overloaded variant of CryptoJS.AES.encrypt, which uses EVP_BytesToKey. Both libraries provide additional or overloaded methods to which a key and IV can be passed directly (which can be derived from a passphrase beforehand using a secure procedure if required).