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

问题描述 投票:0回答:2

我有使用 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);

我收到

DOMException
错误,但没有在
await crypto.subtle.decrypt

上进行回溯

完整的复制可以在https://codesandbox.io/s/crypto-js-to-subtle-crypto-u0pgs?file=/src/index.js

找到
javascript encryption cryptography aes cryptojs
2个回答
4
投票

在 CryptoJS 代码中,密钥作为字符串传递。因此,它被解释为密码,并结合随机生成的 8 字节盐,派生出 32 字节密钥和 16 字节 IV,请参见here。为此使用专有(且相对不安全)的 OpenSSL 密钥派生函数

EVP_BytesToKey

CryptoJS.AES.encrypt()
返回一个
CipherParams
对象,封装了各种参数,例如生成的key和IV为
WordArray
,参见here
toString()
应用于密钥或 IV
WordArray
,返回十六进制编码的数据。
toString()
应用于
CipherParams
对象,返回OpenSSL格式的密文,即第一个块(=前16个字节)由
Salted__
的ASCII编码组成,后面是8个字节的salt和实际的密文,所有内容都经过 Base64 编码,请参阅here。这意味着实际的密文(在 Base64 解码后)从第二个块开始。

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

//
// 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
// 
// https://stackoverflow.com/a/50868276
const fromHex = hexString => new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
// https://stackoverflow.com/a/41106346
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>


0
投票

也可以删除 CryptoJS 并让 crypto.subtle 完成所有工作。 (此代码、密码和盐来自另一个答案,这解释了为什么第一个字节被忽略。)

(async function subtle() {
    const encryptedDataB64 = "EAAAACMtkB64He4p/MSI+yF2A2rJWhxpssG6b48Z01JrfbErvQ1r6Gi0esgCmdrBaFxOPFF1+AUsyrUUl5FQ4Nk0dSU=";
    const passString = 'D2s1d_5$_t0t3||y_4c3$0m3!1!1!!';
    const saltString = 'o6805542kcM7c5';
    const encoder = new TextEncoder();
    const passHex = encoder.encode(passString);
    const saltHex = encoder.encode(saltString);
    const passKey = await crypto.subtle.importKey('raw',passHex,'PBKDF2',false,['deriveKey']);
    const pbkdfParams = {
        'name': 'PBKDF2',
        'hash': 'SHA-1',
        'salt': saltHex,
        'iterations': 1000
    };
    const passCrypto = await crypto.subtle.deriveKey(pbkdfParams,passKey,{'name':'AES-CBC','length':256},false,['decrypt']);
    const encryptedData = Uint8Array.from(atob(encryptedDataB64), c => c.charCodeAt(0));
    const iv = encryptedData.slice(4,20);
    const ciphertext = encryptedData.slice(20);
    const decrypted = await crypto.subtle.decrypt({'name':'AES-CBC','iv':iv},passCrypto,ciphertext);
    const decryptedText = new TextDecoder().decode(decrypted);
    console.log(decryptedText);
})();

© www.soinside.com 2019 - 2024. All rights reserved.