typescript CryptoJS AES加密似乎忽略IV值

cu6pst1q  于 2023-02-20  发布在  TypeScript
关注(0)|答案(2)|浏览(861)

我偶然发现了一个问题,你传递给CryptoJS.AES.encrypt函数的IV值似乎被忽略了。

import CryptoJS from "crypto-js";

const text = "This is secret text!";
const key = "abc";
const iv = CryptoJS.lib.WordArray.random(2);

const encryptedText = CryptoJS.AES.encrypt(text, key, { iv }).toString();
console.log(encryptedText);
const decryptedText = CryptoJS.AES.decrypt(encryptedText, key, { iv: 'whatever' }).toString(CryptoJS.enc.Utf8);
console.log(decryptedText);

生成以下输出:

U2FsdGVkX1//3KxGM2FnVV4qR5KmWBxP/xyI1+YITvdQeXqrUBH7spIvi/Ny+7S2
This is secret text!

因此,不管您传递给函数的是什么IV值,函数似乎都能够解密文本。
我到底做错了什么?

3htmauhk

3htmauhk1#

CryptoJS使用的是AES-CBC。CBC不是一种认证的操作模式,因此它不提供消息完整性/真实性。CBC只提供机密性,而且只有在正确使用时才提供机密性,并且不允许例如填充oracle攻击。
实际上,CBC模式的IV在解密期间仅改变明文的第一块,并且只有那些IV不同的字节。所以所有其余的块如你所期望的那样简单地解密。这是因为IV仅仅是 * 初始化向量 *。在AES块加密之前与明文异或的其他“向量”是先前的密文块。也许最好看看解释操作模式的维基百科页面。
请注意,行:

const encryptedText = CryptoJS.AES.encrypt(text, key, { iv }).toString();

不正确:iv应为命名字段:

const encryptedText = CryptoJS.AES.encrypt(text, key, { iv: iv }).toString();

从这个问题的其他上下文来看,这是否是故意的还不完全清楚。

eit6fx6z

eit6fx6z2#

在加密和解密时,可以在代码中指定 * any * IV,因为IV根本不使用。为什么这样做?
根据密钥材料的类型,CryptoJS使用一个密钥(如果传递了WordArray)或密码(如果传递了字符串)。您传递了字符串,因此密钥材料被解释为密码(这就是为什么key最好被称为password)。在这种情况下,CryptoJS使用密钥派生函数(EVP_BytesToKey())来导出密钥和IV。* 显式 * 传递的IV被完全忽略。这适用于加密和解密。因此,如果应用密码,不应该使用IV(并且应该删除代码中的IV)。
派生密钥和IV可以从CryptoJS.AES.encrypt()返回的CipherParams对象中选取(s. here):这里,密钥可以从属性key读取,IV可以从属性iv读取,密文可以从属性ciphertext读取。
当您传递密钥而不是密码时,您还必须传递IV(否则会显示错误消息)。如果您在解密过程中指定了错误的IV,这将导致CBC(默认模式)的第一个块损坏。other answer中描述了详细信息。
如果您想检查这一点,您应该使用Latin1解码(或十六进制编码)而不是UTF-8解码来解码解密数据,因为损坏的数据通常不兼容UTF-8(并且CryptoJS将显示错误消息)。
请注意,短明文和只有一个块大小的密文通常会破坏填充,这反过来又会在取消填充时破坏数据,使数据无法显示。但是,可以通过禁用默认的PKCS#7填充来显示 * padded * 数据:padding: CryptoJS.pad.NoPadding)。
以下代码显示了使用密码(1)、密钥和IV(2)进行加密/解密,以及使用错误的IV解密时CBC的第一个块损坏(3):

var text = "The quick brown fox jumps over the lazy dog";

// (1) Encryption/Decryption using a password (key and IV implicitly derived via EVP_BytesToKey)
var password = "abc"; // This is a password, not a key!
var iv = CryptoJS.lib.WordArray.random(16);
var encryptedOpenSSLB64 = CryptoJS.AES.encrypt(text, password, {iv: iv}).toString(); // iv ignored (since derived via key derivation) and should be removed!
var decrypted = CryptoJS.AES.decrypt(encryptedOpenSSLB64, password, {iv: iv}); // iv ignored (since derived via key derivation) and should be removed!
console.log(encryptedOpenSSLB64);
console.log(decrypted.toString(CryptoJS.enc.Utf8));

// (2) Encryption/Decryption using a key and an IV
var key = CryptoJS.lib.WordArray.random(32);
var iv = CryptoJS.lib.WordArray.random(16);
var ciphertextB64 = CryptoJS.AES.encrypt(text, key, {iv : iv}).toString(); // iv required!
var decrypted = CryptoJS.AES.decrypt(ciphertextB64, key, {iv : iv}); // iv required!
console.log(ciphertextB64);
console.log(decrypted.toString(CryptoJS.enc.Utf8));

// (3) Corruption of the first block for CBC, if a wrong IV is applied during decryption
var wrongIv = CryptoJS.lib.WordArray.random(16);
var decrypted = CryptoJS.AES.decrypt(ciphertextB64, key, {iv : wrongIv}); // wrong IV corrupts first block for CBC
console.log(decrypted.toString(CryptoJS.enc.Latin1));
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>

完整性:在您的代码中,CipherParams对象将使用toString()转换为Base64编码的OpenSSL格式。OpenSSL格式包括Salted__的ASCII编码,后跟8字节salt和实际的密文。由于常量前缀Salted__,Base64编码的数据始终以U2FsdGVkX1开头(与您的密文类似)。
随机盐是在加密过程中生成的。基于盐和密码,使用密钥导出函数来导出密钥和IV。为了能够在解密过程中重构密钥和IV,需要盐。盐不是秘密的。因此它通常与密文连接(与上述OpenSSL格式相同)。salt还包含在CipherParams对象的salt属性中。
如果只使用密钥而不使用密码,则没有salt,toString()只返回ciphertext属性的Base64编码。
CryptoJS.AES.decrypt()需要一个CipherParams对象(在此处为s.)。或者(如代码中所示)可以传递使用toString()创建的Base64编码。这将隐式转换为CipherParams对象。

相关问题