'CryptoJS - Decrypt an encrypted file

I'm trying to write an application to do end-to-end encryption for files with JS in browser. However I don't seem to be able to get all files decrypted correctly.

TL;DR As it's impractical to encrypt files bigger than 1MB as a whole, I'm trying to encrypt them chunk by chunk. After doing so I try to write the encrypted words (resulted from CryptoJS's WordArray) into a blob. As for decryption I read the files and split them to chunks according to map generated while encrypting the chunks and try to decrypt them. The problem is decrypted result is 0 bits!

I guess I'm not reading the chunks while decrypting correctly. Please take a look at the code below for the function getBlob (writing data to the blob) and the last part of decryptFile for reading chunks.

More explanation

I'm using CryptoJS AES with default settings.

Right now my code looks like this:

function encryptFile (file, options, resolve, reject) {
  if (!options.encrypt) {
    return resolve(file)
  }
  if (!options.processor || !options.context) {
    return reject('No encryption method.')
  }

  function encryptBlob (file, optStart, optEnd) {
    const start = optStart || 0
    let stop = optEnd || CHUNK_SIZE
    if (stop > file.size - 1) {
      stop = file.size
    }

    const blob = file.slice(start, stop)
    const fileReader = new FileReader()

    fileReader.onloadend = function () {
      if (this.readyState !== FileReader.DONE) return

      const index = Math.ceil(optStart / CHUNK_SIZE)
      const result = CryptoJS.lib.WordArray.create(this.result)
      encryptedFile[index] = encrypt(result)

      chunksResolved++
      if (chunksResolved === count) {
        const {sigBytes, sigBytesMap, words} = getCipherInfo(encryptedFile)
        const blob = getBlob(sigBytes, words)

        resolve(blob, Object.keys(sigBytesMap))
      }
    }
    fileReader.readAsArrayBuffer(blob)
  }

  let chunksResolved = 0
  const encryptedFile = []
  const CHUNK_SIZE = 1024*1024
  const count = Math.ceil(file.size / CHUNK_SIZE)
  const encrypt = value => options.processor.call(
    options.context, value, 'file',
    (v, k) => CryptoJS.AES.encrypt(v, k))

  for (let start = 0; (start + CHUNK_SIZE) / CHUNK_SIZE <= count; start+= CHUNK_SIZE) {
    encryptBlob(file, start, start + CHUNK_SIZE - 1)
  }
}

As you can see I'm trying to read the file chunk by chunk (each chunk is 1MB or fileSize % 1MB) as ArrayBuffer, converting it to WordArray for CryptoJS to understand and encrypt it.

After encrypting all the chunks I try to write each word they have to a blob (using a code I found in CryptoJS's issues in Google Code, mentioned below) and I guess here is what goes wrong. I also generated a map for where encrypted chunks end so I can later use it to get the chunks out of the binary file for decryption.

And here's how I decrypt the files:

function decryptFile (file, sigBytesMap, filename, options, resolve, reject) {
  if (!options.decrypt) {
    return resolve(file)
  }
  if (!options.processor || !options.context) {
    return reject('No decryption method.')
  }

  function decryptBlob (file, index, start, stop) {
    const blob = file.slice(start, stop)
    const fileReader = new FileReader()

    fileReader.onloadend = function () {
      if (this.readyState !== FileReader.DONE) return

      const result = CryptoJS.lib.WordArray.create(this.result)
      decryptedFile[index] = decrypt(result)

      chunksResolved++
      if (chunksResolved === count) {
        const {sigBytes, words} = getCipherInfo(decryptedFile)
        const finalFile = getBlob(sigBytes, words)

        resolve(finalFile, filename)
      }
    }
    fileReader.readAsArrayBuffer(blob)
  }

  let chunksResolved = 0
  const count = sigBytesMap.length
  const decryptedFile = []
  const decrypt = value => options.processor.call(
    options.context, value, 'file',
    (v, k) => CryptoJS.AES.decrypt(v, k))

  for (let i = 0; i < count; i++) {
    decryptBlob(file, i, parseInt(sigBytesMap[i - 1]) || 0, parseInt(sigBytesMap[i]) - 1)
  }
}

Decryption is exactly like the encryption but doesn't work. Although chunks are not 1MB anymore, they are limited to sigBytes mentioned in the map. There is no result for the decryption! sigBytes: 0.

Here's the code for generating a blob and getting sigbytesMap:

function getCipherInfo (ciphers) {
  const sigBytesMap = []
  const sigBytes = ciphers.reduce((tmp, cipher) => {
    tmp += cipher.sigBytes || cipher.ciphertext.sigBytes
    sigBytesMap.push(tmp)
    return tmp
  }, 0)

  const words = ciphers.reduce((tmp, cipher) => {
    return tmp.concat(cipher.words || cipher.ciphertext.words)
  }, [])

  return {sigBytes, sigBytesMap, words}
}

function getBlob (sigBytes, words) {
  const bytes = new Uint8Array(sigBytes)
  for (var i = 0; i < sigBytes; i++) {
    const byte = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff
    bytes[i] = byte
  }

  return new Blob([ new Uint8Array(bytes) ])
}

I'm guessing the issue is the method I'm using to read the encrypted chunks. Or maybe writing them!

I should also mention that previously I was doing something different for encryption. I was stringifying each WordArray I got as the result for CryptoJS.AES.encrypt using the toString method with the default encoding (which I believe is CryptoJS.enc.Hex) but some files didn't decrypt correctly. It didn't have anything to do with the size of the original file, rather than their types. Again, I'm guessing!



Solution 1:[1]

Turns out the problem was the WordArray returned by CryptoJS.AES.decrypt(value, key) has 4 extra words as padding which should not be included in the final result. CryptoJS tries unpadding the result but only changes sigBytes accordingly and doesn't change words. So when decrypting, before writing chunks to file pop those extra words. 4 words for full chunks and 3 for smaller ones (last chunk).

Solution 2:[2]

check this issue

import CryptoJS from "crypto-js";

async function encryptBlobToBlob(blob: Blob, secret: string): Promise<Blob> {
    const wordArray = CryptoJS.lib.WordArray.create(await blob.arrayBuffer());
    const result = CryptoJS.AES.encrypt(wordArray, secret);
    return new Blob([result.toString()]);
}
export async function decryptBlobToBlob(blob: Blob, secret: string): Promise<Blob> {
    const decryptedRaw = CryptoJS.AES.decrypt(await blob.text(), secret);
    return new Blob([wordArrayToByteArray(decryptedRaw)]);
}

function wordToByteArray(word, length) {
    const ba = [];
    const xFF = 0xff;
    if (length > 0) ba.push(word >>> 24);
    if (length > 1) ba.push((word >>> 16) & xFF);
    if (length > 2) ba.push((word >>> 8) & xFF);
    if (length > 3) ba.push(word & xFF);

    return ba;
}

function wordArrayToByteArray({ words, sigBytes }: { sigBytes: number; words: number[] }) {
    const result = [];
    let bytes;
    let i = 0;
    while (sigBytes > 0) {
        bytes = wordToByteArray(words[i], Math.min(4, sigBytes));
        sigBytes -= bytes.length;
        result.push(bytes);
        i++;
    }
    return new Uint8Array(result.flat());
}

async function main() {
    const secret = "bbbb";
    const blob = new Blob(["1".repeat(1e3)]);
    const encryptedBlob = await encryptBlobToBlob(blob, secret);
    console.log("enrypted blob size", encryptedBlob.size);
    const decryptedBlob = await decryptBlobToBlob(encryptedBlob, secret);
    console.log("decryptedBlob", decryptedBlob);
    console.log(await decryptedBlob.text());
}
main();

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 MahdiPOnline
Solution 2