As mentioned in this answer, I can use ECB mode to reverse a transformed value back into plaintext and not just pare it to another hashed value.
However, with the below code snippet:
const x = CryptoJS.AES.encrypt('abc', '123', { mode: CryptoJS.mode.ECB }).toString()
const y = CryptoJS.AES.encrypt('abc', '123', { mode: CryptoJS.mode.ECB }).toString()
console.log(x, y, x === y)
<script src=".0.0/crypto-js.min.js"></script>
As mentioned in this answer, I can use ECB mode to reverse a transformed value back into plaintext and not just pare it to another hashed value.
However, with the below code snippet:
const x = CryptoJS.AES.encrypt('abc', '123', { mode: CryptoJS.mode.ECB }).toString()
const y = CryptoJS.AES.encrypt('abc', '123', { mode: CryptoJS.mode.ECB }).toString()
console.log(x, y, x === y)
<script src="https://cdnjs.cloudflare./ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
I get:
U2FsdGVkX19blKXDRXfdgXyviCrZtouB0cPcJPoR/cQ= U2FsdGVkX1+1AwWqKWntLVkh7DtiZxPDYCDNsjmc8LM= false
Am I doing something wrong? Is there a way to achieve the intended results?
Share Improve this question asked Apr 5, 2020 at 16:47 Kenny KiKenny Ki 3,4302 gold badges27 silver badges30 bronze badges 3- Read the CryptoJS documentation. The result of the encryption is an object. You're just converting the whole object to a string, which doesn't really make sense. – Pointy Commented Apr 5, 2020 at 17:05
-
@Pointy what do you mean? I'm calling
toString
method on the object as mentioned in the library's readme – Kenny Ki Commented Apr 6, 2020 at 2:27 - You can certainly do that, but the object details contain the answer to your question. The encryption process involves random numbers. – Pointy Commented Apr 6, 2020 at 13:36
2 Answers
Reset to default 14First 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./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./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./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).
This works using AES and ECB mode to encrypt strings deterministically:
const encrypt = (text: string, key: string) => {
const hash = CryptoJS.SHA256(key);
const ciphertext = CryptoJS.AES.encrypt(text, hash, {
mode: CryptoJS.mode.ECB,
});
return ciphertext.toString();
};
const decrypt = (ciphertext: string, key: string) => {
const hash = CryptoJS.SHA256(key);
const bytes = CryptoJS.AES.decrypt(ciphertext, hash, {
mode: CryptoJS.mode.ECB,
});
return bytes.toString(CryptoJS.enc.Utf8);
};