First of all: For the same plaintext and the same key always the same ciphertext is generated in ECB mode!
If a WordArray
is used as second parameter, then CryptoJS.AES.encrypt
performs an encryption with a key and the resulting ciphertexts are identical as expected (here):
function encryptWithKey(plaintext, key){
var encrypted = CryptoJS.AES.encrypt(plaintext, key, { mode: CryptoJS.mode.ECB });
console.log("Ciphertext (Base64):\n" + encrypted.toString()); // Ciphertext
var decrypted = CryptoJS.AES.decrypt(encrypted.toString(), key, { mode: CryptoJS.mode.ECB });
console.log("Decrypted:\n" + decrypted.toString(CryptoJS.enc.Utf8)); // Plaintext
}
var key = CryptoJS.enc.Hex.parse('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f');
encryptWithKey('abc', key);
encryptWithKey('abc', key);
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
But if a string is used as the second parameter, CryptoJS.AES.encrypt
performs an encryption with a passphrase and the resulting ciphertexts are different (here). Nevertheless, the decryption of course returns the original plaintext:
function encryptWithPassphrase(plaintext, passphrase){
var encrypted = CryptoJS.AES.encrypt(plaintext, passphrase, { mode: CryptoJS.mode.ECB });
console.log("Ciphertext (OpenSSL):\n" + encrypted.toString()); // Salt and actual ciphertext in OpenSSL format
var decrypted = CryptoJS.AES.decrypt(encrypted.toString(), passphrase, { mode: CryptoJS.mode.ECB });
console.log("Decrypted:\n" + decrypted.toString(CryptoJS.enc.Utf8)); // Plaintext
}
encryptWithPassphrase('abc', '123');
encryptWithPassphrase('abc', '123');
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
Explanation:
During the encryption with a passphrase a random 8 bytes salt is generated from which together with the passphrase the actual key (32 bytes, AES-256) is generated.
The salt is intended to make the use of rainbow tables infeasible. Since the salt is generated randomly each time, the resulting keys are different and thus also the ciphertexts.
CryptoJS.AES.encrypt
returns a CipherParams
object which encapsulates the relevant parameters like salt and actual ciphertext.
toString()
converts this object into the OpenSSL format which consists of the ASCII encoding of Salted__
, followed by the 8 bytes salt, followed by the actual ciphertext, all together Base64 encoded. For this reason, all ciphertexts begin with U2FsdGVkX1
.
function encryptWithPassphraseParams(plaintext, passphrase){
var encrypted = CryptoJS.AES.encrypt(plaintext, passphrase, { mode: CryptoJS.mode.ECB });
console.log("Salt (hex):\n" + encrypted.salt); // Salt (hex)
console.log("Key (hex):\n" + encrypted.key); // Key (hex)
console.log("Ciphertext (hex):\n" + encrypted.ciphertext); // Actual ciphertext (hex)
console.log("Ciphertext (OpenSSL):\n" + encrypted.toString()); // Salt and actual ciphertext, Base64 encoded, in OpenSSL format
console.log("\n");
}
encryptWithPassphraseParams('abc', '123');
encryptWithPassphraseParams('abc', '123');
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
Details:
CryptoJS uses the OpenSSL functionality EVB_BytesToKey
with the digest MD5 and an iteration count of 1 when deriving the key, which is not very secure. More secure is the use of reliable KDFs such as PBKDF2 and the subsequent encryption with the resulting key.
Apart from security, it should be noted that EVB_BytesToKey
does not implement a standard, so this functionality must first be implemented (or copied from the Internet) in libraries where it is not available.
Note: ECB is an insecure mode and should not be used (here), better is authenticated encryption like GCM. More details about CryptoJS can be found in its documentation (here).