Oleksandr
Oleksandr

Reputation: 477

How to encrypt files with AES algorithm with CryptoJS?

I have code to encrypt files using RC4 algorithm. I was strongly advised to use a more reliable algorithm: AES. From the CryptoJS documentation, I understood that it works the same way as RC4. That is, the first argument is the string to be encrypted, and the second argument is the password string.

But simply replacing the RC4 method with AES did not help and I do not know where to look for the necessary information.

Thank you!

Here is my working (for RC4) code:

<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js" integrity="sha512-E8QSvWZ0eCLGk4km3hxSsNmGWbLtSCSUcewDQPQWZF6pEU8GlT8a5fF32wOl1i8ftdMhssTrF/OhyGWwonTcXA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

<div>
  <h1>encrypt/decrypt file</h1>
  <ol>
    <li>Set password</li>
    <li>Pick a file</li>
    <li>Download decrypted/encrypted file</li>
  </ol>
  <div>
    <input type="text" id="pass" placeholder="pass">
    <button id="encrypt">encrypt file</button>
    <button id="decrypt">decrypt file</button>
    <button id="test">test</button>
  </div>
</div>

<script>
  // support
  const download = (data, filename, type) => {
    const file = new Blob([data], {
      type: type
    });
    const a = document.createElement('a');
    const url = URL.createObjectURL(file);

    a.href = url;
    a.download = filename;
    document.body.appendChild(a);

    a.click();

    setTimeout(function() {
      document.body.removeChild(a);
      window.URL.revokeObjectURL(url);
    }, 0);
  };
  const pickAFile = (getText = true) => {
    return new Promise((resolve, reject) => {
      const input = document.createElement('input');
      input.type = 'file';
      input.onchange = (e) => {
        const file = e.target.files[0];
        const reader = new FileReader();
        if (!getText) {
          resolve(file);
        } else {
          reader.onload = (e) => resolve(e.target.result);
          reader.onerror = (e) => reject(e);
          reader.readAsText(file);
        }
      };
      input.click();
    });
  };
  const convertWordArrayToUint8Array = (wordArray) => {
    const arrayOfWords = wordArray.hasOwnProperty('words') ? wordArray.words : [];

    const length = wordArray.hasOwnProperty('sigBytes') ?
      wordArray.sigBytes :
      arrayOfWords.length * 4;

    const uInt8Array = new Uint8Array(length);
    let index = 0;
    let word;
    let i;

    for (i = 0; i < length; i++) {
      word = arrayOfWords[i];
      uInt8Array[index++] = word >> 24;
      uInt8Array[index++] = (word >> 16) & 0xff;
      uInt8Array[index++] = (word >> 8) & 0xff;
      uInt8Array[index++] = word & 0xff;
    }
    return uInt8Array;
  };
  // /support

  function app() {
    const passNode = document.querySelector('input#pass');
    const encryptNode = document.querySelector('#encrypt');
    const decryptNode = document.querySelector('#decrypt');

    encryptNode.addEventListener('click', () => {
      if (!passNode.value) return alert('Password input is empty! Aborting.');
      const pass = CryptoJS.SHA3(passNode.value);
      pickAFile(false).then((file) => {
        const reader = new FileReader();

        reader.onload = (e) => {
          const wordArray = CryptoJS.lib.WordArray.create(e.target.result);
          const encrypted = CryptoJS.RC4.encrypt(wordArray, pass).toString();
          download(encrypted, `encrypted-${file.name}`, file.type);
        };

        reader.readAsArrayBuffer(file);
      });
    });

    decryptNode.addEventListener('click', () => {
      if (!passNode.value) return alert('Password input is empty! Aborting.');
      const pass = CryptoJS.SHA3(passNode.value);
      pickAFile(false).then((file) => {
        const reader = new FileReader();

        reader.onload = (e) => {
          try {
            const decrypted = CryptoJS.RC4.decrypt(e.target.result, pass);
            const typedArray = convertWordArrayToUint8Array(decrypted);
            download(typedArray, `decrypted-${file.name}`, file.type);
          } catch (error) {
            console.log('wrong password!');
          }
        };

        reader.readAsText(file);
      });
    });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', app);
  } else {
    app();
  }
</script>

Upvotes: 2

Views: 2768

Answers (1)

Topaco
Topaco

Reputation: 49241

When converting from RC4 to AES, consider the following:

  • AES defines key sizes of 16 (AES-128), 24 (AES-192), and 32 bytes (AES-256). The larger the key, the more secure (although nowadays all variants are considered secure). In the following a 32 bytes key is used.

    The SHA3 CryptoJS implementation you apply (actually Keccak, see Hashing/SHA-3) has an output size of 64 bytes by default and therefore cannot be used directly as an AES key. Therefore I apply SHA-256 with an output size of 32 bytes for simplicity. You could also use SHA-3 and only take e.g. the first 32 bytes.

    However, the key derivation via a digest is a vulnerability. More secure is the use of a key derivation like Argon2 or PBKDF2. The latter is supported by CryptoJS and I therefore recommend to switch to PBKDF2. Since I'm focusing here on converting RC4 to AES, I'll leave that change to you.

  • AES needs in CBC mode (which is used by default by CryptoJS; default padding is PKCS#7 btw) a random IV (its length is equal to the blocksize, so 16 bytes for AES). This must not be static for security reasons, but must be randomly generated (with a CSPRNG) for each encryption.

    The IV is not secret and is needed for decryption. Therefore, it is usually concatenated with the ciphertext: IV|ciphertext.

    Before decryption, the IV and ciphertext are separated based on the known length of the IV.

A possible switch from RC4 to AES that takes the above points into account is (see also the comments in the code for an explanation of the changes):

<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js" integrity="sha512-E8QSvWZ0eCLGk4km3hxSsNmGWbLtSCSUcewDQPQWZF6pEU8GlT8a5fF32wOl1i8ftdMhssTrF/OhyGWwonTcXA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

<div>
  <h1>encrypt/decrypt file</h1>
  <ol>
    <li>Set password</li>
    <li>Pick a file</li>
    <li>Download decrypted/encrypted file</li>
  </ol>
  <div>
    <input type="text" id="pass" placeholder="pass">
    <button id="encrypt">encrypt file</button>
    <button id="decrypt">decrypt file</button>
    <button id="test">test</button>
  </div>
</div>

<script>
  // support
  const download = (data, filename, type) => {
    const file = new Blob([data], {
      type: type
    });
    const a = document.createElement('a');
    const url = URL.createObjectURL(file);

    a.href = url;
    a.download = filename;
    document.body.appendChild(a);

    a.click();

    setTimeout(function() {
      document.body.removeChild(a);
      window.URL.revokeObjectURL(url);
    }, 0);
  };
  const pickAFile = (getText = true) => {
    return new Promise((resolve, reject) => {
      const input = document.createElement('input');
      input.type = 'file';
      input.onchange = (e) => {
        const file = e.target.files[0];
        const reader = new FileReader();
        if (!getText) {
          resolve(file);
        } else {
          reader.onload = (e) => resolve(e.target.result);
          reader.onerror = (e) => reject(e);
          reader.readAsText(file);
        }
      };
      input.click();
    });
  };
  const convertWordArrayToUint8Array = (wordArray) => {
    const arrayOfWords = wordArray.hasOwnProperty('words') ? wordArray.words : [];

    const length = wordArray.hasOwnProperty('sigBytes') ?
      wordArray.sigBytes :
      arrayOfWords.length * 4;

    const uInt8Array = new Uint8Array(length);
    let index = 0;
    let word;
    let i;

    for (i = 0; i < length; i++) {
      word = arrayOfWords[i];
      uInt8Array[index++] = word >> 24;
      uInt8Array[index++] = (word >> 16) & 0xff;
      uInt8Array[index++] = (word >> 8) & 0xff;
      uInt8Array[index++] = word & 0xff;
    }
    return uInt8Array;
  };
  // /support

  function app() {
    const passNode = document.querySelector('input#pass');
    const encryptNode = document.querySelector('#encrypt');
    const decryptNode = document.querySelector('#decrypt');

    encryptNode.addEventListener('click', () => {
      if (!passNode.value) return alert('Password input is empty! Aborting.');
      const key = CryptoJS.SHA256(passNode.value); // Fix 1: Derive 32 bytes key
      pickAFile(false).then((file) => {
        const reader = new FileReader();

        reader.onload = (e) => {
          const iv = CryptoJS.lib.WordArray.random(16); // Fix 2: Create random 16 bytes IV
          const wordArray = CryptoJS.lib.WordArray.create(e.target.result);
          const encrypted = CryptoJS.AES.encrypt(wordArray, key, {iv: iv}); // Fix 3: Encrypt with AES using the above key and IV
          const ivCiphertext = iv.clone().concat(encrypted.ciphertext).toString(CryptoJS.enc.Base64); // Fix 4: Concatenate IV and ciphertext
         download(ivCiphertext, `encrypted-${file.name}`, file.type);
        };

        reader.readAsArrayBuffer(file);
      });
    });

    decryptNode.addEventListener('click', () => {
      if (!passNode.value) return alert('Password input is empty! Aborting.');
      const key = CryptoJS.SHA256(passNode.value); // Fix 5: Derive 32 bytes key
      pickAFile(false).then((file) => {
        const reader = new FileReader();

        reader.onload = (e) => {
          try {
            const ivCiphertext = CryptoJS.enc.Base64.parse(e.target.result); // Fix 6: Separate IV and ciphertext
            const iv = CryptoJS.lib.WordArray.create(ivCiphertext.words.slice(0, 4)); 
            const ciphertext = CryptoJS.lib.WordArray.create(ivCiphertext.words.slice(4)); 
            const decrypted = CryptoJS.AES.decrypt({ciphertext: ciphertext}, key, {iv: iv}); // Fix 7: Decrypt
            const typedArray = convertWordArrayToUint8Array(decrypted);
            download(typedArray, `decrypted-${file.name}`, file.type);
          } catch (error) {
            console.log('wrong password!');
          }
        };

        reader.readAsText(file);
      });
    });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', app);
  } else {
    app();
  }
</script>

Note that the code can still be optimized (this applies also to the RC4 variant): During encryption, the data (i.e. the concatenated IV and ciphertext) is Base64 encoded. Base64 is a binary-to-text encoding which increases the data size by about 33%. It is used when arbitrary binary data is to be represented as text.
However, since the data is stored as a file here, such a conversion is not really necessary (of course, there may be reasons that are not apparent from the post). Instead, the raw (i.e. the non-Base64 encoded data) can be stored, which would reduce the file size accordingly.

Upvotes: 2

Related Questions