如何通过 CryptoJS 加密的 Subtle Crypto 正确解密文本

how to properly decrypt text via Subtle Crypto which was encrypted via CryptoJS

我有使用 CryptoJS.AES 加密用户数据的代码,将密钥、iv 和加密内容存储在不同的地方。 它还根据用户需求使用存储的密钥和 iv 对加密内容进行解密。

我想用 Subtle Crypto 浏览器API加密,已完成

但我也希望有可能使用 Subtle Crypto 解密旧数据(使用 CryptoJS.AES 加密)。

旧数据是使用以下代码生成的

  var CryptoJS = require("crypto-js/core");
  CryptoJS.AES = require("crypto-js/aes");

  let encKey = generateRandomString();
  let aesEncrypted = CryptoJS.AES.encrypt(content, encKey);
  let encrypted = {
    key: aesEncrypted.key.toString(),
    iv: aesEncrypted.iv.toString(),
    content: aesEncrypted.toString()
  };

我试过如下解密

  let keyArrayBuffer = hexArrayToArrayBuffer(sliceArray(encrypted.key, 2));
  let decKey = await importKey(keyArrayBuffer);
  let decIv = hexArrayToArrayBuffer(sliceArray(encrypted.iv, 2));
  let encContent = stringToArrayBuffer(encrypted.content);
  let decryptedByteArray = await crypto.subtle.decrypt(
    { name: "AES-CBC", iv: decIv },
    decKey,
    encContent
  );
  let decrypted = new TextDecoder().decode(decrypted);

我在 await crypto.subtle.decrypt

上收到 DOMException 没有回溯的错误

可在 https://codesandbox.io/s/crypto-js-to-subtle-crypto-u0pgs?file=/src/index.js

找到完整的复制品

在 CryptoJS 代码中,密钥作为字符串传递。因此,它被解释为密码,从中结合随机生成的 8 字节盐,派生出 32 字节密钥和 16 字节 IV,参见 here. The proprietary (and relatively insecure) OpenSSL key derivation function EVP_BytesToKey 用于此。

CryptoJS.AES.encrypt() returns 封装各种参数的CipherParams对象,例如生成的密钥和IV为WordArray,参见here. toString() applied to the key or IV WordArray, returns the data hex encoded. toString() applied to the CipherParams object, returns the ciphertext in OpenSSL format, i.e. the first block (= the first 16 bytes) consists of the ASCII encoding of Salted__, followed by the 8 bytes salt and the actual ciphertext, all together Base64 encoded, see here。这意味着实际的密文从第二个块开始(在 Base64 解码之后)。

以下代码说明了如何使用 WebCrypto 解密使用 CryptoJS 生成的密文 API

//
// CryptoJS
//
const content = "The quick brown fox jumps over the lazy dog";
const encKey = "This is my passphrase";

const aesEncrypted = CryptoJS.AES.encrypt(content, encKey);
const encrypted = {
    key: aesEncrypted.key.toString(),
    iv: aesEncrypted.iv.toString(),
    content: aesEncrypted.toString()
};

//
// WebCrypto API
// 
// 
const fromHex = hexString => new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
// 
const fromBase64 = base64String => Uint8Array.from(atob(base64String), c => c.charCodeAt(0));
 
async function decryptDemo(){
    
    const rawKey = fromHex(encrypted.key);
    const iv = fromHex(encrypted.iv);
    const ciphertext = fromBase64(encrypted.content).slice(16);
        
    const key = await window.crypto.subtle.importKey(           
        "raw",
        rawKey,                                                 
        "AES-CBC",
        true,
        ["encrypt", "decrypt"]
    );

    const decrypted = await window.crypto.subtle.decrypt(
        {
            name: "AES-CBC",
            iv: iv
        },
        key,
        ciphertext
    );

    const decoder = new TextDecoder();
    const plaintext = decoder.decode(decrypted);
    console.log(plaintext);     
}
    
decryptDemo();
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>