Search code examples
javascriptencryptioncryptographyaescryptojs

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


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

complete reproduction can be found at https://codesandbox.io/s/crypto-js-to-subtle-crypto-u0pgs?file=/src/index.js


Solution

  • In the CryptoJS code the key is passed as string. Therefore it is interpreted as a password, from which in combination 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.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>