I have code that encrypts user's data using CryptoJS.AES, stores key, iv and encrypted content in different places. Also it decrypts encrypted content using stored key and iv by user demand.
I want to use Subtle Crypto browser API for encryption,which is done.
But I also want to have possibility to decrypt old data (that was ecnrypted using CryptoJS.AES) using Subtle Crypto.
old data was generated with following code
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()
};
and I've tried to decrypt it as following
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);
I receive DOMException
error without backtrace on await crypto.subtle.decrypt
plete reproduction can be found at =/src/index.js
I have code that encrypts user's data using CryptoJS.AES, stores key, iv and encrypted content in different places. Also it decrypts encrypted content using stored key and iv by user demand.
I want to use Subtle Crypto browser API for encryption,which is done.
But I also want to have possibility to decrypt old data (that was ecnrypted using CryptoJS.AES) using Subtle Crypto.
old data was generated with following code
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()
};
and I've tried to decrypt it as following
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);
I receive DOMException
error without backtrace on await crypto.subtle.decrypt
plete reproduction can be found at https://codesandbox.io/s/crypto-js-to-subtle-crypto-u0pgs?file=/src/index.js
Share edited Sep 25, 2020 at 16:34 Senid asked Sep 25, 2020 at 16:16 SenidSenid 5826 silver badges14 bronze badges 1- CryptoJS said that it uses AES-CBC with 256-bit key and PKCS7 padding, and I've used same values for crypto.subtle but decryption still fail – Senid Commented Sep 25, 2020 at 16:18
2 Answers
Reset to default 4In the CryptoJS code the key is passed as string. Therefore it is interpreted as a password, from which in bination with a randomly generated 8 bytes salt, a 32 bytes key and a 16 bytes IV are derived, see here. The proprietary (and relatively insecure) OpenSSL key derivation function EVP_BytesToKey
is used for this.
CryptoJS.AES.encrypt()
returns a CipherParams
object that encapsulates various parameters, such as the generated key and IV as WordArray
, see 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. This means that the actual ciphertext starts (after Base64 decoding) with the second block.
The following code illustrates how the ciphertext generated with CryptoJS can be decrypted with the WebCrypto 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
//
// https://stackoverflow./a/50868276
const fromHex = hexString => new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
// https://stackoverflow./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./ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
It is also possible to remove CryptoJS and have everything done by crypto.subtle. (This code, password, and salt are from another answer, which explains why the first byte is ignored.)
(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);
})();