5

As mentioned in this answer, I can use ECB mode to reverse a transformed value back into plaintext and not just compare 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.com/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?

Kenny Ki
  • 3,400
  • 1
  • 25
  • 30
  • Read [the CryptoJS documentation.](https://cryptojs.gitbook.io/docs/#the-cipher-output) 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 Apr 05 '20 at 17:05
  • @Pointy what do you mean? I'm calling `toString` method on the object as mentioned in the library's [readme](https://github.com/brix/crypto-js#plain-text-encryption) – Kenny Ki Apr 06 '20 at 02:27
  • You can certainly do that, but the object details contain the answer to your question. The encryption process involves random numbers. – Pointy Apr 06 '20 at 13:36

2 Answers2

13

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).

Topaco
  • 40,594
  • 4
  • 35
  • 62
  • Very concise explanations! Now I understand it much better. Thank you – Kenny Ki Apr 08 '20 at 03:14
  • In my case I need determinism because I'm building CDN urls that should remain the same until they expire. Your first example works, unfortunately if I use var key = CryptoJS.enc.Utf8.parse(password); (instead of hex) to parse the password, it stop working. In general passwords are arbitrary strings so it should be better to parse as utf8 ... unfortunately it doesn't work :( – cancerbero May 25 '22 at 22:10
  • @cancerbero - Key derivation from a password *without* salt is insecure and not supported by modern libraries. If you have more questions, please post a new question including code and any information needed to answer. – Topaco May 26 '22 at 06:21
1

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);
};
cancerbero
  • 6,799
  • 1
  • 32
  • 24