关于Node.JS中AES加密库“crypto-js”的问题

jgovgodb  于 2023-11-17  发布在  Node.js
关注(0)|答案(2)|浏览(202)

下面是我的示例代码

const CryptoJS = require('crypto-js')

const plaintext = "hello world"
const passphrase = "my_passphrase"

const encrypted = CryptoJS.AES.encrypt(plaintext, passphrase)
console.log("plaintext =", plaintext)
console.log("passphrase =", passphrase)
console.log("-----------------------------------------------------------")
console.log("key =", encrypted.key+'')
console.log("iv =", encrypted.iv+'')
console.log("salt =", encrypted.salt+'')
console.log("encrypted =", encrypted+'')

console.log("-----------------------------------------------------------")

var decrypted = CryptoJS.AES.decrypt(encrypted.toString(), passphrase);
console.log("decrypted =", decrypted.toString(CryptoJS.enc.Utf8))

字符串
输出结果是:

plaintext = hello world
passphrase = my_passphrase
-----------------------------------------------------------
key = 7bb8ed7c0c9ad5b714a57073068f441dfbf032173e60bf61deea2f9a5ea2ad3a
iv = 72b8e7e60fbcf1328fd1994ea2cc7f06
salt = 03a04d2d438b4cac
encrypted = U2FsdGVkX18DoE0tQ4tMrKBbK/veZm1k0vGmFxl6sow=
-----------------------------------------------------------
decrypted = hello world


据我所知,从输出中可以看到,crypto-js在内部加密过程中自动从密码短语中导出具有随机盐和随机IV值的实际密钥,然后使用导出的密钥加密明文。
我的问题是,既然salt和IV是随机生成的,那么为什么解密函数可以用唯一的密钥密码导出相同的AES密钥?会不会是salt和IV嵌入在加密数据中?如果是这样,使用或不使用salt并不重要,对吗?

ecfsfe2w

ecfsfe2w1#

据我所知,从输出中,你可以看到crypto-js在内部加密过程中自动从密码短语中导出具有随机盐和随机IV值的实际密钥,然后使用导出的密钥加密明文。
这只是部分正确。当密钥材料作为字符串传递时,CryptoJS确实使用了密钥派生函数。然而,在这种情况下,在加密期间仅生成 random 8字节盐,而不是IV。根据所使用的算法,使用密钥导出函数并使用密码和盐作为输入来沿着密钥导出IV然后应用导出的密钥和IV(默认使用CBC模式和PKCS#7填充)执行加密。
请注意,random salt对于安全性很重要,因为它为每个加密生成不同的密钥/IV对。密钥/IV对的重用将意味着或多或少的严重漏洞,具体取决于模式。
我的问题是,既然盐和IV是随机生成的,那么为什么解密函数可以用唯一的密钥密码来导出相同的AES密钥?
当使用密钥导出函数时,salt * 必须 * 以某种形式传递给解密端(但不是IV,因为它是导出的)。然后,在使用密钥导出函数解密期间,使用salt和密码来重构密钥和IV。最后,使用以这种方式导出的密钥和IV执行解密。
会不会是加密数据里的盐和IV?
salt可以通过不同的方式传递给CryptoJS.AES.decrypt()(如前所述,IV在派生时不会传递),例如封装在CipherParams对象中( Package 密钥,iv,salt和密文)。
一个CipherParams也是由CryptoJS.AES.encrypt()生成的,所以返回值可以 * 直接 * 传递给CryptoJS.AES.decrypt()。但是,如果CipherParams对象只包含salt和密文,就足够解密了。
或者,可以从CipherParams对象中提取密文和盐,并以任何所需的格式传递到解密端。
一种特殊的格式是Base64编码的OpenSSL格式,它将Salted__的ASCII编码,salt和密文按此顺序连接在一起。Base64编码的OpenSSL格式的特点是它总是以U2FsdGVkX1开头,因为前缀是常量。CryptoJS通过CipherParams对象的toString()函数直接支持这种格式。
下面的脚本演示了这一点:

var password = "test passphrase"
var plaintext = "The quick brown fox jumps over the lazy dog"

// test 1: pass CipherParams object directly from encrypt() to decrypt()
var dataCP = CryptoJS.AES.encrypt(plaintext, password)
console.log("test 1:", CryptoJS.AES.decrypt(dataCP, password).toString(CryptoJS.enc.Utf8))

// test 2: pass a fresh CipherParams object to decrypt() that contains only salt and ciphertext
var dataCP_onlySaltAndCiphertext = CryptoJS.lib.CipherParams.create({ salt: dataCP.salt, ciphertext: dataCP.ciphertext })
console.log("test 2:", CryptoJS.AES.decrypt(dataCP_onlySaltAndCiphertext, password).toString(CryptoJS.enc.Utf8))
console.log("       ", CryptoJS.AES.decrypt({ salt: dataCP.salt, ciphertext: dataCP.ciphertext }, password).toString(CryptoJS.enc.Utf8)) // or more compact

// test 3: pass data to decrypt() in Base64 encoded OpenSSL format using toString()
var dataOpenSSL = dataCP.toString()
console.log("test 3:", CryptoJS.AES.decrypt(dataOpenSSL, password).toString(CryptoJS.enc.Utf8))

// test 4: pass data to decrypt() in explicitly generated Base64 encoded OpenSSL format 
var dataOpenSSL_explicit = CryptoJS.enc.Utf8.parse("Salted__").concat(dataCP.salt).concat(dataCP.ciphertext).toString(CryptoJS.enc.Base64)
console.log("test 4:", CryptoJS.AES.decrypt(dataOpenSSL_explicit, password).toString(CryptoJS.enc.Utf8))

个字符
如果是的话,用不用盐都没关系,对吧?
这个问题我不是很清楚。如果你的意思是CryptoJS处理盐 * 隐式 *,那么是的(例如以OpenSSL格式)。如果你问在密钥派生过程中是否可以禁用盐:不,这在CryptoJS中是不可能的(不像OpenSSL)。当然,出于安全原因,也不建议禁用盐。
关于密钥派生函数的详细信息:
CryptoJS应用OpenSSL专有密钥派生函数EVP_BytesToKey(),迭代次数为1,MD5作为摘要。结合加密数据的OpenSSL格式,这实现了与OpenSSL的兼容性(只要MD5用作OpenSSL的摘要)。
MD5是旧版本OpenSSL中的默认摘要。然而,从v1.1.0开始,OpenSSL切换到SHA-256作为默认摘要。因此,CryptoJS仅在OpenSSL语句中使用-md显式指定摘要为MD5时才与新版本的OpenSSL兼容。
请注意:EVP_BytesToKey()现在被认为是不安全的,特别是因为迭代计数为1并且使用了损坏的摘要MD5。相反,对于新的实现,至少应该应用PBKDF 2(CryptoJS和OpenSSL都支持),或者,如果可用的话,Argon 2。
请注意,如果密钥材料作为WordArray传递,则不执行密钥推导,但密钥材料 * 直接 * 用作密钥,s。然后,IV * 必须 * 显式指定为WordArray(对于使用IV的所有模式)。
以下脚本执行内置密钥推导 * 显式 *,并显示两个结果是等效的:

// 1. Encrypt with built-in key derivation
var password = "test passphrase"
var plaintext = "The quick brown fox jumps over the lazy dog"
var encryptedCP = CryptoJS.AES.encrypt(plaintext, password) // keymaterial is a string => encryption with key derivation
var saltWA = encryptedCP.salt
console.log("salt", saltWA.toString())
console.log("ciphertext, OpenSSL format, built-in", encryptedCP.toString())

// 2. Encrypt with explicit key derivation
// - Generate 32 bytes key key and 16 bytes IV using EVP_BytesToKey using password and salt from above
var keySize = 8; // key size for AES-256: 8 words (a 4 bytes) = 32 bytes
var ivSize = 4;  // iv size for AES: 4 words (a 4 bytes) = 16 bytes
var keyIvWA = CryptoJS.EvpKDF(password, saltWA, {keySize: keySize + ivSize, iterations: 1, hasher: CryptoJS.algo.MD5})
var keyWA = CryptoJS.lib.WordArray.create(keyIvWA.words.slice(0, keySize), keySize * 4)
var ivWA = CryptoJS.lib.WordArray.create(keyIvWA.words.slice(keySize), ivSize * 4)
// - Encrypt with AES-256 in CBC mode (default) and PKCS#7 padding (default) without built-in key derivation
var encryptedCP = CryptoJS.AES.encrypt(plaintext, keyWA, {iv: ivWA}) // keymaterial is a WordArray => encryption without key derivation
var encryptedCPOpenSSL = CryptoJS.enc.Utf8.parse("Salted__").concat(saltWA).concat(encryptedCP.ciphertext).toString(CryptoJS.enc.Base64)
console.log("ciphertext, OpenSSL format, explicit", encryptedCPOpenSSL)

// 3. Decrypt
var decrypted = CryptoJS.AES.decrypt(encryptedCPOpenSSL, password)
console.log(decrypted.toString(CryptoJS.enc.Utf8))
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>

的字符串
请注意,CryptoJS的开发最近已经停止,库也不再维护。推荐的替代方案是WebCrypto或NodeJS的crypto模块。

vdgimpew

vdgimpew2#

是的,对,但是我将这些问题归纳为两点,我希望你能解释一下
第一个,当加密密码时,CryptoJS生成一个随机盐和一个随机向量(IV)。这些用于导出加密密钥。然后将盐,IV和加密的消息组合成最终的密文。
在第二种情况下,当解密密码时,CryptoJS从密文中提取salt和IV,并将它们与密码一起沿着来导出原始加密密钥。然后使用该密钥将密文的其余部分解密为原始明文。

相关问题