使用 ECB 模式的 CryptoJS AES 加密使用相同的参数产生不同的结果

CryptoJS AES encryption with ECB mode produces different results with the same params

如本 中所述,我可以使用 ECB 模式将转换后的值反转回明文,而不仅仅是将其与另一个散列值进行比较。

但是,使用以下代码片段:

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>

我得到:

U2FsdGVkX19blKXDRXfdgXyviCrZtouB0cPcJPoR/cQ= U2FsdGVkX1+1AwWqKWntLVkh7DtiZxPDYCDNsjmc8LM= false

我是不是做错了什么?有没有办法达到预期的效果?

首先:对于相同的明文和相同的密钥,在ECB模式下总是生成相同的密文!

如果 WordArray 用作第二个参数,则 CryptoJS.AES.encrypt 使用密钥 执行 加密,生成的密文为 与预期相同 (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>

但是,如果将字符串用作第二个参数,CryptoJS.AES.encrypt 会使用密码 执行加密,生成的密文不同here)。尽管如此,解密当然returns原来的明文:

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>

解释:
在使用密码短语加密期间,会生成一个 随机 8 字节盐,从中生成实际密钥(32 字节,AES-256)。
盐旨在使彩虹表的使用不可行。 由于盐每次都是随机生成的,因此生成的密钥不同,因此密文也不同。
CryptoJS.AES.encrypt returns a CipherParams 对象,它封装了相关参数,如盐和实际密文。
toString() 将此对象转换为 OpenSSL 格式,该格式由 Salted__ 的 ASCII 编码组成,后跟 8 字节盐,后面是实际的密文,全部一起使用 Base64 编码。因此,所有密文都以 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>

详情:
CryptoJS 在派生密钥时使用 OpenSSL 功能 EVB_BytesToKey 以及摘要 MD5 和迭代计数 1,这不是很安全。更安全的方法是使用可靠的 KDF,例如 PBKDF2,然后使用生成的密钥进行后续加密。
除了安全性,需要注意的是 EVB_BytesToKey 没有实现标准,因此必须首先使用此功能在不可用的库中实施(或从 Internet 复制)。

注意:ECB 是一种不安全的模式,不应使用 (here), better is authenticated encryption like GCM. More details about CryptoJS can be found in its documentation (here)。

这可以使用 AES 和 ECB 模式确定性地加密字符串:

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);
};