Search code examples
javascriptencryptiondeploymentaeswebcrypto-api

How to Resolve DOMException When Decrypting Files with Web Crypto API in a Streaming Manner?


I m trying to encrypt file in browser uisng webcrypto api. I m using chunking mechanism too so that i can encrypt and decrypt large files without crashing browser.

My provided code is working if i m not using chunking mechanism or the file size is less than chunk size ie 1 MB as it wont go to else part anyway

Please help me with it.

Code for encryption:

<!DOCTYPE html>
<html>
   <head>
      <title>Large File Encryption</title>
   </head>
   <body>
      <input type="file" id="fileInput" >
      <button id="encryptButton">Encrypt</button>
      <a id="downloadLink" style="display: none" download="encrypted-file.txt">Download Encrypted File</a>
      <script>
             // event listener which will listen to encrypt button
         document.getElementById("encryptButton").addEventListener("click", async () => {
             const fileInput = document.getElementById("fileInput");
             const file = fileInput.files[0];
             if (!file) {
                 alert("Please select a file.");
                 return;
             }
                
                    
             const chunkSize = 1024 * 1024; // 1 MB chunks
              
             // generating keys 
             const key = await window.crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt","decrypt"]);
         
             const keyData = await window.crypto.subtle.exportKey('jwk', key);
         
         // Create a data blob with the key data
         const keyBlob = new Blob([JSON.stringify(keyData)], { type: 'application/json' });
         
         // Create a download link for the key
         const keyUrl = URL.createObjectURL(keyBlob);
         const downloadLink = document.createElement('a');
         downloadLink.href = keyUrl;
         downloadLink.download = 'encryption-key.json';
         downloadLink.click();
         
         const iv=new Uint8Array(12);
    
         
         
         
         
             const reader = new FileReader();
              
              // chunks will be pushed in this array
             const fileData = [];
             let offset = 0;
         
             reader.onload = async function () {
                 const chunk = new Uint8Array(reader.result);
                          
                 // encrypting the chunk using AES-GCM alogorithm
                 const encryptedChunk = await window.crypto.subtle.encrypt(
                     { name: "AES-GCM", iv },
                     key,
                     chunk
                 );
                 
                 // pushing the chunk
                 fileData.push(encryptedChunk);
                 // updating offset
                 offset += chunk.length;
                            
                 // if remaining chunks are left to be read and encrypted           
                 if (offset < file.size) {
                     // calling readslice function to start reading remaining chunks
                     readSlice(offset);
                 } else {
                     // All chunks are encrypted; combine them and provide a download link.
                     const encryptedData = new Blob(fileData, { type: file.type });
                     const url = URL.createObjectURL(encryptedData);
                     const downloadLink = document.getElementById("downloadLink");
                     downloadLink.href = url;
                     downloadLink.style.display = "block";
                     console.log('finished')
                 }
             };
                    
             function readSlice(offset) {
                 // taking part of file
                 const slice = file.slice(offset, offset + chunkSize);
                 // to initiate reading the part of file
                 reader.readAsArrayBuffer(slice);
             }
         
             readSlice(0);
         });
      </script>
   </body>
</html>

 
Code for decryption

<!DOCTYPE html>
<html>
<head>
  <title>File Decryption</title>
</head>
<body>
  <h1>File Decryption</h1>
  
  <!-- Input for the encrypted file -->
  <input type="file" id="encryptedFileInput"  />
  
  <!-- Input for the encryption key (as JSON) -->
  <input type="file" id="keyInput" accept=".json" />
  
  
  
  <button id="decryptButton">Decrypt</button>
  
  <a id="downloadLink" style="display: none;">Download Decrypted File</a>
  
  <script>
    document.getElementById("decryptButton").addEventListener("click", async () => {
      // Get the encrypted file
      const encryptedFileInput = document.getElementById("encryptedFileInput");
      const encryptedFile = encryptedFileInput.files[0];
      
      // Get the encryption key 
      const keyInput = document.getElementById("keyInput");
      
      
      const keyFile = keyInput.files[0];
      
      
      if (!encryptedFile || !keyFile ) {
        alert("Please select the encrypted file and  key.");
        return;
      }
      
      // Read the key and IV as JSON
      const keyData = await keyFile.text();
   
      
      // Convert the JSON data to JavaScript objects
      const keyObj = JSON.parse(keyData);
      

     



const importAlgorithm = {
  name: "AES-GCM"
};


const keyUsage = ["encrypt","decrypt"];
            
     // importing the jwk key 
      const key = await crypto.subtle.importKey("jwk", keyObj, importAlgorithm, false, keyUsage)

      // 1 mb( chunk size while encrypting + 12 bytes(iv size in bytes) ) 
      const chunkSize= (1024*1024)+12
      
      // also tried with
      // const chunkSize= (1024*1024)
      // but no luck


       const reader = new FileReader();
            const fileData = [];
            let offset = 0;

        reader.onload = async function () {
          console.log('chunk loaded')
                const chunk = new Uint8Array(reader.result);
                const decryptedChunk = await window.crypto.subtle.decrypt(
                    { name: "AES-GCM", iv: new Uint8Array(12) },
                    key,
                    chunk
                );
                fileData.push(decryptedChunk);
                offset += chunk.length;

                if (offset < encryptedFile.size) {
                    readSlice(offset);
                } else {
                    // All chunks are decrypted; combine them and provide a download link.
                    const decryptedData = new Blob(fileData, { type: encryptedFile.type });
                    const url = URL.createObjectURL(decryptedData);
                    const downloadLink = document.getElementById("downloadLink");
                    downloadLink.href = url;
                    downloadLink.download="someting.dec"
                    downloadLink.style.display = "block";
                    downloadLink.click()
                    console.log('finished')
                }
            };

            function readSlice(offset) {
                const slice = encryptedFile.slice(offset, offset + chunkSize);
                reader.readAsArrayBuffer(slice);
            }

            readSlice(0);









      
    
    });
  </script>
</body>
</html>

For the file above 1MB(aka when chunking mechanism is used) i m getting the following error

DOMException: The operation failed for an operation-specific reason

I tried to simply decrypt without using chunks but still i m facing error. Maybe there is some issue with encryption part. I expect that just like the code works for file size less than 1 MB which is chunk size, it works too by encrypting and decrypting properly chunk by chunk.

Edit: I changed iv length from 12 bytes to 16 bytes and its now working. But one problem still exist i.e. browser crashing. Anyway to avoid that?


Solution

  • The following should work:

    const chunkSize= (1024*1024)+16 // change in decrypt.html
    
    

    I have arrived at this conclusion by running your code and observing the size difference between encrypted file and decrypted file. So I noticed that there is extra 16 bits(instead of 12) per slice in encrypted file.