我有使用 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
找到在 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>
也可以删除 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);
})();