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:
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 think that's it...I appreciate any help or tips!
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