【问题标题】:Encode with 'crypto-js' and decode with 'crypto' (Node)用“crypto-js”编码并用“crypto”解码(节点)
【发布时间】:2020-11-23 05:29:41
【问题描述】:

我正在使用“cypto-js”在浏览器中使用高级加密标准 (AES) 对字符串进行加密,并且需要在服务器上使用节点“crypto”对其进行解密。

我可以单独使用“crypto-js”进行加密/解密,但是当我尝试使用“crypto.createDecipher”使用“crypto”(节点)进行解密时,我会收到“错误解密”的错误消息或'错误的块大小'取决于我的尝试。

例如:只使用 'crypto-js' - 工作正常

crypto-js

const cypherParams = CryptoJS.AES.encrypt('my message', 'passphrase')
const decrypted = CryptoJS.AES.decrypt(cypherParams, 'passphrase')
console.log(decrypted.toString(CryptoJS.enc.Utf8)) // 'my message' - works!

例如:使用 'crypto-js' 编码 使用 'crypto' 解码 - 导致错误

[client]
const cypherParams = CryptoJS.AES.encrypt('my message', 'passphrase')

[server]
const decipher = crypto.createDecipher('aes-256-cbc', 'passphrase');
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8'); 
// results in 'bad decrypt' or 'block size' error in console


console.log(decrypted); // this never executes

我试过了:

  • 将解密中的加密算法更改为 192 或其他(但“crypto-js”文档说默认为“256”是使用密码短语
  • 客户端上的 base64 编码。也尝试了十六进制编码

例如:

const cypherParams = CryptoJS.AES.encrypt('my message', 'passphrase')
const base64Encoded = cipherParams.toString(CryptoJS.enc.Base64)

and

const cypherParams = CryptoJS.AES.encrypt('my message', 'passphrase')
const cypherParams.ciphertext = cipherParams.toString(CryptoJS.enc.Base64)
  • 我使用的是“crypto.createDecipher”而不是“crypto.createDecipheriv”,因为我在这个项目中使用 Node v8.12.0 时遇到了问题

我想就是这样...感谢任何帮助或提示!

【问题讨论】:

    标签: javascript encryption aes cryptojs node-crypto


    【解决方案1】:

    如果密钥作为字符串传递,CryptoJS.AES.encrypt() 使用 OpenSSL 密钥派生函数 (KDF) EVP_BytesToKey() 派生 32 字节密钥(和 16 字节 IV),即确实应用 AES-256 进行加密(herehere)。在此过程中会生成一个随机的 8 字节盐,以确保每次产生不同密钥/IV 对。
    NodeJS 方法crypto.createCipher() 使用相同的 KDF,但不应用盐,因此总是生成 same 密钥/IV 对。因此crypto.createDecipher() 也不考虑盐。
    总而言之,这意味着使用CryptoJS.AES.encrypt()加密时生成的密钥对与使用crypto.createDecipher()解密时生成的密钥对不同,并且解密失败。

    据我所知,这两种方法都无法控制是否使用盐,因此无法消除不相容性。

    因此,一种解决方案是省略内置的 KDF(无论如何都认为它很弱,这也是不推荐使用 crypto.createCipher()/crypto.createDecipher() 的原因)并改用可靠的 KDF,例如PBKDF2 并使用从它派生的密钥/IV 对。
    在 CryptoJS 方面,您必须将密钥和 IV 作为WordArray 传递,在 NodeJS 方面,您必须使用create.createDecipheriv()
    加密和解密之间的连接是在密钥推导过程中随机生成的盐。盐不是秘密的,通常与密文连接并以这种方式传递给接收者。

    您提到您使用的版本是Node v8.12.0,因此您不能申请crypto.createDecipheriv()。但是crypto.createDecipheriv() 从 v0.1.94 开始可用,因此它应该在您的环境中可用。


    客户端加密的示例实现(CryptoJS):

    // Generate random salt
    var salt16 = CryptoJS.lib.WordArray.random(16);                                     // Random 16 bytes salt
    
    // Derive key and IV via PBKDF2
    var keyIV = CryptoJS.PBKDF2("My Passphrase", salt16, {
      keySize: (32 + 16) / 4,                                                           // 12 words a 4 bytes = 48 bytes
      iterations: 1000,                                                                 // Choose a sufficiently high iteration count
      hasher: CryptoJS.algo.SHA256                                                      // Default digest is SHA-1       
    }); 
    var key32 = CryptoJS.lib.WordArray.create(keyIV.words.slice(0, 32 / 4));            // 8 words a 4 bytes = 32 bytes 
    var iv16 = CryptoJS.lib.WordArray.create(keyIV.words.slice(32 / 4, (32 + 16) / 4)); // 4 words a 4 bytes = 16 bytes 
    
    // Encrypt
    var message = 'The quick brown fox jumps over the lazy dog';
    var cipherParams = CryptoJS.AES.encrypt(message, key32, {iv:iv16});
    var ciphertext = cipherParams.ciphertext;
    
    // Concatenate salt and ciphertext
    var saltCiphertext = salt16.clone().concat(ciphertext);
    var saltCiphertextB64 = saltCiphertext.toString(CryptoJS.enc.Base64);               // This is passed to the recipient    
    
    // Outputs
    console.log("Salt:\n", salt16.toString(CryptoJS.enc.Base64).replace(/(.{56})/g,'$1\n'));
    console.log("Ciphertext:\n", ciphertext.toString(CryptoJS.enc.Base64).replace(/(.{56})/g,'$1\n'));
    console.log("Salt | Ciphertext:\n", saltCiphertextB64.replace(/(.{56})/g,'$1\n'));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>

    服务器端解密的示例实现(NodeJS):

    var crypto = require('crypto');
    
    // Separate salt and ciphertext
    var saltCiphertextB64 = 'lhBp/LKhv8TxeJYnLDy/F2oaQYScOVFVLLZxd00HiRy9fYy97lX2ZjGJt+S4x+GF9X0AEjAS9k8tUDHKCz4srQ==';  // Received from client
    var saltCiphertextBuf = Buffer.from(saltCiphertextB64, 'base64');
    var saltBuf = saltCiphertextBuf.slice(0,16);
    var ciphertextBuf = saltCiphertextBuf.slice(16);
    
    // Derive key and IV via PBKDF2
    var keyIVBuf = crypto.pbkdf2Sync("My Passphrase", saltBuf, 1000, 32 + 16, 'sha256');
    var keyBuf = keyIVBuf.slice(0, 32); 
    var ivBuf = keyIVBuf.slice(32, 32 + 16);
    
    // Decrypt
    var decipher = crypto.createDecipheriv("aes-256-cbc", keyBuf, ivBuf);
    var plaintextBuf = Buffer.concat([decipher.update(ciphertextBuf), decipher.final()]);
    
    // Outputs
    console.log("Plaintext: ", plaintextBuf.toString('utf8')); // Plaintext:  The quick brown fox jumps over the lazy dog
    

    【讨论】:

    • @jspru - 我已经用客户端和服务器端的示例实现来说明我的答案。
    • 非常感谢您的详细回答。我已经多次阅读它(以及 crypto-js 和 crypto 文档)并且一直在努力实现,但仍然有点挣扎。我将与您的示例代码进行比较并尽快回复您。再次感谢!
    • 这对我有用!问题是我没有在服务器端正确导出密钥 / iv,导致不匹配。我将继续研究您的实现,以更好地理解该方法。这是复杂的东西,但真的很有趣。感谢您提供令人难以置信的答案和帮助。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-17
    相关资源
    最近更新 更多