英文:
CryptoJS AES encryption seems to ignore IV value
问题
以下是您要翻译的部分:
"I stumbled upon the problem that the IV value that you pass to CryptoJS.AES.encrypt
function seems to be ignored. Following code:
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);
Produces following output:
This is secret text!
So it seems the function is able to decrypt the text no matter what IV value you are passing to the function.
What am I doing wrong here?"
英文:
I stumbled upon the problem that the IV value that you pass to CryptoJS.AES.encrypt
function seems to be ignored. Following code:
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);
Produces following output:
U2FsdGVkX1//3KxGM2FnVV4qR5KmWBxP/xyI1+YITvdQeXqrUBH7spIvi/Ny+7S2
This is secret text!
So it seems the function is able to decrypt the text no matter what IV value you are passing to the function.
What am I doing wrong here?
答案1
得分: 2
您可以在加密和解密时在代码中指定任何IV,因为IV根本不被使用。为什么呢?
根据密钥材料的类型,CryptoJS使用密钥(如果传递WordArray)或密码(如果传递字符串)。您传递了一个字符串,所以密钥材料被解释为密码(这就是为什么key
最好被称为password
)。在这种情况下,CryptoJS使用密钥派生函数(EVP_BytesToKey()
)来派生密钥和IV。显式传递的IV被完全忽略。这适用于加密和解密。因此,如果应用密码,不应使用IV(代码中的IV应该被移除)。
通过CryptoJS.AES.encrypt()
返回的CipherParams
对象可以获取派生的密钥和IV(参见此处):密钥可以从key
属性读取,IV可以从iv
属性读取,密文可以从ciphertext
属性读取,参见此处。
当您传递密钥而不是密码时,必须同时传递IV(否则会显示错误消息)。如果在解密过程中指定了错误的IV,这将导致CBC(默认模式)的第一个块被损坏。有关详细信息,请参阅其他答案。
如果要进行检查,您应该使用Latin1解码(或十六进制编码)而不是UTF-8解码解密后的数据,因为损坏的数据通常不兼容UTF-8(CryptoJS会显示错误消息)。
请注意,明文较短,密文仅为一个块大小的情况通常会损坏填充,从而在去填充过程中损坏数据,使数据无法显示。但是,可以通过禁用默认的PKCS#7填充来显示填充数据:padding: CryptoJS.pad.NoPadding
。
以下代码显示了使用密码(1)进行加密/解密,使用密钥和IV(2)以及在解密时使用错误IV(3)损坏CBC的第一个块:
var text = "The quick brown fox jumps over the lazy dog";
// (1) 使用密码(通过EVP_BytesToKey隐式派生密钥和IV)进行加密/解密
var password = "abc"; // 这是一个密码,不是密钥!
var iv = CryptoJS.lib.WordArray.random(16);
var encryptedOpenSSLB64 = CryptoJS.AES.encrypt(text, password, {iv: iv}).toString(); // IV被忽略(因为通过密钥派生)应该被移除!
var decrypted = CryptoJS.AES.decrypt(encryptedOpenSSLB64, password, {iv: iv}); // IV被忽略(因为通过密钥派生)应该被移除!
console.log(encryptedOpenSSLB64);
console.log(decrypted.toString(CryptoJS.enc.Utf8));
// (2) 使用密钥和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是必需的!
var decrypted = CryptoJS.AES.decrypt(ciphertextB64, key, {iv : iv}); // IV是必需的!
console.log(ciphertextB64);
console.log(decrypted.toString(CryptoJS.enc.Utf8));
// (3) 如果在解密时使用错误的IV,CBC的第一个块会损坏
var wrongIv = CryptoJS.lib.WordArray.random(16);
var decrypted = CryptoJS.AES.decrypt(ciphertextB64, key, {iv : wrongIv}); // 错误的IV会损坏CBC的第一个块
console.log(decrypted.toString(CryptoJS.enc.Latin1));
为了完整起见,在您的代码中,CipherParams
对象通过toString()
转换为Base64编码的OpenSSL格式。OpenSSL格式由Salted__
的ASCII编码开头,后跟8字节的盐和实际的密文。由于恒定的前缀Salted__
,Base64编码的数据始终以U2FsdGVkX1
开头(与您的密文一样)。
随机盐在加密时生成。根据盐和密码,使用密钥派生函数来派生密钥和IV。为了在解密时能够重建密钥和IV,需要盐。盐不是机密的,因此通常与密文连接在一起(如上面描述的OpenSSL格式)。盐也包含在CipherParams
对象的salt
属性中。
如果没有密码而是密钥,就没有盐,toString()
仅返回ciphertext
属性的Base64编码。这也在上面的代码片段中有所说明。
CryptoJS.AES.decrypt()
需要一个CipherParams
对象(参见此处)。或者(如您的代码中所示)可以传递使用toString()
创建的Base64编码,它会被隐式转换为CipherParams
对象。
英文:
You can specify any IV in your code when encrypting and decrypting, because the IV is simply not used. Why is that?
Depending on the type of the key material, CryptoJS uses a key (if a WordArray is passed) or a password (if a string is passed). You pass a string, so the key material is interpreted as password (which is why key
should better be called password
). In this case, CryptoJS uses a key derivation function (EVP_BytesToKey()
) to derive the key and the IV. The explicitly passed IV is completely ignored. This applies to both encryption and decryption. Therefore, if a password is applied, no IV should be used (and the one in the code should be removed).
The derived key and IV can be picked from the CipherParams
object returned by CryptoJS.AES.encrypt()
(s. here): The key can be read from the property key
, the IV from the property iv
and the ciphertext from the property ciphertext
, s. here.
When you pass a key instead of a password, you must also pass an IV (otherwise an error message is displayed). If you specify a wrong IV during decryption, this will result in a corrupted first block for CBC (the default mode). The details are described in the other answer.
If you want to check this, you should use a Latin1 decoding (or hex encoding) instead of a UTF-8 decoding for the decrypted data, because the corrupted data is generally not Utf-8 compatible (and CryptoJS will display an error message).
Note that short plaintexts with a ciphertext of only one block in size will generally corrupt the padding, which in turn corrupts the data during unpadding, making the data non-displayable. However, it is possible to display the padded data by disabling the default PKCS#7 padding: padding: CryptoJS.pad.NoPadding
).
The following code shows encryption/decryption with a password (1), with a key and IV (2) and the corruption of the first block for CBC when decrypting with a wrong IV (3):
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
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));
<!-- language: lang-html -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
<!-- end snippet -->
For completeness: In your code the CipherParams
object is converted to the Base64 encoded OpenSSL format with toString()
. The OpenSSL format consists of the ASCII encoding of Salted__
followed by an 8 bytes salt and the actual ciphertext. Because of the constant prefix Salted__
the Base64 encoded data always starts with U2FsdGVkX1
(like your ciphertext).
The random salt is generated during encryption. Based on salt and password, the key derivation function is used to derive key and IV. In order to be able to reconstruct key and IV during decryption, the salt is required. The salt is not secret, so it is usually concatenated with the ciphertext (as in the OpenSSL format described above). The salt is also contained in the salt
property of the CipherParams
object.
If no password but a key is applied, there is no salt and toString()
returns only the Base64 encoding of the ciphertext
property. This is also illustrated in the above code snippet.
CryptoJS.AES.decrypt()
requires a CipherParams
object (s. here). Alternatively (as in your code) the Base64 encoding created with toString()
can be passed. This is implicitly converted into a CipherParams
object.
答案2
得分: 1
CryptoJS正在使用AES-CBC。CBC不是一种经过身份验证的操作模式,因此它不提供消息的完整性和真实性。CBC只提供机密性,而且只有在正确使用时提供机密性,不允许填充攻击等攻击。
实际上,在CBC模式中,IV只在解密期间更改第一个明文块,并仅更改IV不同的那些字节。这是因为IV只是初始化向量。与AES块加密之前与明文进行XOR运算的其他“向量”是前一个密文块。最好查看维基百科上解释操作模式的页面。
请注意,以下行中:
const encryptedText = CryptoJS.AES.encrypt(text, key, { iv }).toString();
是不正确的:iv
应该是一个具名字段:
const encryptedText = CryptoJS.AES.encrypt(text, key, { iv: iv }).toString();
从问题的其余上下文来看,不清楚这是否是有意的。
英文:
CryptoJS is using AES-CBC. CBC is not an authenticated mode of operation, so it doesn't offer message integrity / authenticity. CBC only offers confidentiality, and only that while it is used correctly and doesn't allow for, for instance, padding oracle attacks.
Actually, the IV in CBC mode only changes the first block of plaintext during decryption, and only those bytes where the IV differs. So all the rest of the blocks simply decrypt as you'd expect. This is because the IV is only the initialization vector. The other "vectors" that are XOR'ed with the plaintext before AES block encryption are the previous blocks of ciphertext. Probably best to have a look at the Wikipedia page explaining the modes of operation.
Beware that the line:
const encryptedText = CryptoJS.AES.encrypt(text, key, { iv }).toString();
is not correct: iv
should be a named field:
const encryptedText = CryptoJS.AES.encrypt(text, key, { iv: iv }).toString();
It's not entirely clear if this is deliberate or not from the rest of the context in the question.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论