4
votes

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!

1
1) Why are you doing this? 2) Do you know that's insecure? What exactly are you achieving with this encryption? Since it's browser to browser - key will be visible, so what's the point? - Mjh
Is k hard coded or is it retrieved through asymmetric crypto? - Luke Joshua Park
@Mjh To achieve zero-knowledge-encryption, so we wouldn't have access to user's data. - MahdiPOnline
If you have the key, you have the knowledge about data. Since you have the key, this zero-knowledge principle is broken. What's wrong with SSL/TLS? - Mjh
Right, what are you encrypting the data with? User's public key? Public key encryption has a limit to how much data it can encrypt. The whole premise of your system sounds completely off - you have a file whose contents you can see. If you couldn't see the contents, you'd have no need to encrypt it. You could just deliver the mangled blob to the other person, who would decrypt it by asking the key from the owner. You have too many bad decisions here and if I knew exactly which service you were providing, I would never sign up for it. No hard feelings, there's too much of "wrong" here. - Mjh

1 Answers

1
votes

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).