Search code examples
javascriptencryptionaescryptojsnode-crypto

Encode with 'crypto-js' and decode with 'crypto' (Node)


I am encrypting a string using Advanced Encryption Standard (AES) in the browser with 'cypto-js' and need to decrypt it on the server with Node 'crypto'.

I can encrypt / decrypt just fine with 'crypto-js' alone, however when I try to decrypt with 'crypto' (Node) using 'crypto.createDecipher' I get error messages to the effect of 'bad decrypt' or 'wrong block size' depending on what I try.

ex: using just 'crypto-js' - works fine

crypto-js

const cypherParams = CryptoJS.AES.encrypt('my message', 'passphrase')
const decrypted = CryptoJS.AES.decrypt(cypherParams, 'passphrase')
console.log(decrypted.toString(CryptoJS.enc.Utf8)) // 'my message' - works!

ex: encode with 'crypto-js' decode with 'crypto' - results in error

[client]
const cypherParams = CryptoJS.AES.encrypt('my message', 'passphrase')

[server]
const decipher = crypto.createDecipher('aes-256-cbc', 'passphrase');
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8'); 
// results in 'bad decrypt' or 'block size' error in console


console.log(decrypted); // this never executes

I have tried:

  • Changing the encryption algorithm in decrypt to 192 or other (but 'crypto-js' docs say default is '256' is passphrase is used
  • base64 encoding on the client. tried hex encoding as well

ex:

const cypherParams = CryptoJS.AES.encrypt('my message', 'passphrase')
const base64Encoded = cipherParams.toString(CryptoJS.enc.Base64)

and

const cypherParams = CryptoJS.AES.encrypt('my message', 'passphrase')
const cypherParams.ciphertext = cipherParams.toString(CryptoJS.enc.Base64)
  • I am using 'crypto.createDecipher' rather than 'crypto.createDecipheriv' because I am stuck using Node v8.12.0 on this project

I think that's it...I appreciate any help or tips!


Solution

  • If the key is passed as a string, CryptoJS.AES.encrypt() uses the OpenSSL key derivation function (KDF) EVP_BytesToKey() to derive a 32 bytes key (and a 16 bytes IV), i.e. indeed AES-256 is applied for encryption (here and here). During this process a random 8 bytes salt is generated, which ensures that each time a different key/IV pair results.
    The NodeJS method crypto.createCipher() uses the same KDF, but does not apply a salt, so that always the same key/IV pair is generated. Therefore crypto.createDecipher() does not take a salt into account either.
    Altogether, this means that the key pair generated when encrypting with CryptoJS.AES.encrypt() is different from the key pair generated when decrypting with crypto.createDecipher() and the decryption fails.

    As far as I know both methods do not offer the possibility to control whether a salt is used or not, so that the incompatibility cannot be eliminated.

    One solution would therefore be to omit the built-in KDF (which is considered weak anyway, which in turn is why crypto.createCipher()/crypto.createDecipher() are deprecated) and use a reliable KDF instead, e.g. PBKDF2 and work with the key/IV pair derived from it.
    On the CryptoJS side you have to pass key and IV as WordArray, on the NodeJS side you have to use create.createDecipheriv().
    The connection between encryption and decryption is the salt to be generated randomly during the key derivation. The salt is not secret, is usually concatenated with the ciphertext and passed to the recipient in this way.

    You mention that the version you are using is Node v8.12.0 and therefore you cannot apply crypto.createDecipheriv(). But crypto.createDecipheriv() is available since v0.1.94, so it should be available in your environment.


    Sample implementation for encryption on the client side (CryptoJS):

    // Generate random salt
    var salt16 = CryptoJS.lib.WordArray.random(16);                                     // Random 16 bytes salt
    
    // Derive key and IV via PBKDF2
    var keyIV = CryptoJS.PBKDF2("My Passphrase", salt16, {
      keySize: (32 + 16) / 4,                                                           // 12 words a 4 bytes = 48 bytes
      iterations: 1000,                                                                 // Choose a sufficiently high iteration count
      hasher: CryptoJS.algo.SHA256                                                      // Default digest is SHA-1       
    }); 
    var key32 = CryptoJS.lib.WordArray.create(keyIV.words.slice(0, 32 / 4));            // 8 words a 4 bytes = 32 bytes 
    var iv16 = CryptoJS.lib.WordArray.create(keyIV.words.slice(32 / 4, (32 + 16) / 4)); // 4 words a 4 bytes = 16 bytes 
    
    // Encrypt
    var message = 'The quick brown fox jumps over the lazy dog';
    var cipherParams = CryptoJS.AES.encrypt(message, key32, {iv:iv16});
    var ciphertext = cipherParams.ciphertext;
    
    // Concatenate salt and ciphertext
    var saltCiphertext = salt16.clone().concat(ciphertext);
    var saltCiphertextB64 = saltCiphertext.toString(CryptoJS.enc.Base64);               // This is passed to the recipient    
    
    // Outputs
    console.log("Salt:\n", salt16.toString(CryptoJS.enc.Base64).replace(/(.{56})/g,'$1\n'));
    console.log("Ciphertext:\n", ciphertext.toString(CryptoJS.enc.Base64).replace(/(.{56})/g,'$1\n'));
    console.log("Salt | Ciphertext:\n", saltCiphertextB64.replace(/(.{56})/g,'$1\n'));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>

    Sample implementation for decryption on the server side (NodeJS):

    var crypto = require('crypto');
    
    // Separate salt and ciphertext
    var saltCiphertextB64 = 'lhBp/LKhv8TxeJYnLDy/F2oaQYScOVFVLLZxd00HiRy9fYy97lX2ZjGJt+S4x+GF9X0AEjAS9k8tUDHKCz4srQ==';  // Received from client
    var saltCiphertextBuf = Buffer.from(saltCiphertextB64, 'base64');
    var saltBuf = saltCiphertextBuf.slice(0,16);
    var ciphertextBuf = saltCiphertextBuf.slice(16);
    
    // Derive key and IV via PBKDF2
    var keyIVBuf = crypto.pbkdf2Sync("My Passphrase", saltBuf, 1000, 32 + 16, 'sha256');
    var keyBuf = keyIVBuf.slice(0, 32); 
    var ivBuf = keyIVBuf.slice(32, 32 + 16);
    
    // Decrypt
    var decipher = crypto.createDecipheriv("aes-256-cbc", keyBuf, ivBuf);
    var plaintextBuf = Buffer.concat([decipher.update(ciphertextBuf), decipher.final()]);
    
    // Outputs
    console.log("Plaintext: ", plaintextBuf.toString('utf8')); // Plaintext:  The quick brown fox jumps over the lazy dog