提问人:MengCheng Wei 提问时间:11/17/2023 最后编辑:MengCheng Wei 更新时间:11/17/2023 访问量:107
关于 Node.JS 中 AES 加密库“crypto-js”的问题
Question about AES encryption library "crypto-js" in Node.JS
问:
这是我的示例代码
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
据我所知,从输出中可以看出,在内部加密过程中,自动从密码短语中派生出带有随机盐和随机 IV 值的实际密钥,然后使用派生的密钥对明文进行加密。crypto-js
我的问题是,既然盐和IV是随机生成的,那么为什么解密函数只能使用密钥密码获得相同的AES密钥?会不会是加密数据中嵌入的盐和IV?如果是这样,用不用盐并不重要,对吧?
答:
是的,没错,但我将这些问题总结为两点的答案,我希望你能解释一下
第一个,在加密密码时,CryptoJS 会生成一个随机盐和初始化向量 (IV)。这些用于派生加密密钥。然后,盐、IV 和加密消息被组合成最终的密文。
在第二种情况下,在解密密码时,CryptoJS 从密文中提取 salt 和 IV,并将它们与密码一起使用来派生原始加密密钥。然后,此密钥用于将密文的其余部分解密回原始明文。
评论
EVP_BytesToKey()
CipherParams
toString()
encrypted
Salted__
U2FsdGVkX1
据我所知,从输出中可以看出,crypto-js 在内部加密过程中自动从密码中派生出带有随机盐和随机 IV 值的实际密钥,然后使用派生的密钥对明文进行加密。
这只是部分正确。当密钥材料作为字符串传递时,CryptoJS 确实使用密钥派生函数。但是,在这种情况下,加密期间只会生成一个随机的 8 字节盐,而不是 IV。IV 与密钥一起使用密钥派生函数派生,并根据使用的算法(默认为 AES-256)将密码和盐作为输入。然后应用派生的密钥和 IV(默认使用 CBC 模式和 PKCS#7 填充)执行加密。
请注意,随机盐对于安全性很重要,因为它会为每种加密生成不同的密钥/IV 对。重用密钥/IV 对将意味着或多或少的严重漏洞,具体取决于模式。
我的问题是,既然盐和IV是随机生成的,那么为什么解密函数只能使用密钥密码获得相同的AES密钥?
它不能。使用密钥派生函数时,盐必须以某种形式传递到解密端(但不是 IV,因为它是派生的)。然后,在解密过程中,使用密钥派生函数使用 salt 和 password 来重建密钥和 IV。最后,使用以这种方式派生的密钥和 IV 进行解密。
会不会是加密数据中嵌入的盐和IV?
盐可以以不同的方式传递(如前所述,IV 在派生时不会传递),例如封装在 CipherParams
对象中(包装密钥、iv、盐和密文)。
A 也是由 生成的,因此返回值可以直接传递给 。但是,如果对象仅包含盐和密文,则足以进行解密。
或者,可以从对象中提取密文和盐,并以任何所需的格式传递到解密端。
一种特殊的格式是 Base64 编码的 OpenSSL 格式,它按此顺序连接 、 salt 和密文的 ASCII 编码。Base64 编码的 OpenSSL 格式的特点是,由于前缀不变,它总是以开头。CryptoJS 直接通过对象的函数支持这种格式。CryptoJS.AES.decrypt()
CipherParams
CryptoJS.AES.encrypt()
CryptoJS.AES.decrypt()
CipherParams
CipherParams
Salted__
U2FsdGVkX1
toString()
CipherParams
以下脚本演示了这一点:
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))
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
如果是这样,用不用盐并不重要,对吧?
这个问题我不太清楚。如果你的意思是 CryptoJS 隐式处理盐,那么是的(例如,在 OpenSSL 格式中)。如果你问是否可以在密钥派生过程中禁用盐:不,这在 CryptoJS 中是不可能的(与 OpenSSL 不同)。当然,出于安全原因,也不建议禁用盐。
密钥派生函数的详细信息:
CryptoJS 应用 OpenSSL 专有密钥派生函数 EVP_BytesToKey(),
迭代计数为 1,MD5 作为摘要。结合加密数据的 OpenSSL 格式,这实现了与 OpenSSL 的兼容性(只要 MD5 用作 OpenSSL 的摘要)。
MD5 是旧版 OpenSSL 中的默认摘要。但是,从 v1.1.0 开始,OpenSSL 切换到 SHA-256 作为默认摘要。因此,只有在 OpenSSL 语句中使用 将摘要显式指定为 MD5 时,CryptoJS 才与较新的 OpenSSL 版本兼容。
注意:现在被认为是不安全的,特别是因为迭代次数为 1 并且使用了损坏的摘要 MD5。相反,对于新的实现,至少应该应用 PBKDF2(CryptoJS 和 OpenSSL 都支持)或者,如果可用,则应用 Argon2。-md
EVP_BytesToKey()
请注意,如果密钥材料作为 传递,则不执行密钥派生,但密钥材料直接用作密钥,此处为 s。然后,必须将 IV 显式指定为(对于使用一个 IV 的所有模式)。WordArray
WordArray
以下脚本显式执行内置密钥派生,并显示两个结果是等效的:
// 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 的加密模块。
评论
CryptoJS.AES.encrypt(plaintext, passphrase)
EVP_BytesToKey()
上一个:NextJS 加密错误
评论