You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Web Crypto API流式解密大文件:DOMException及崩溃问题求助

大文件AES-GCM分块加密解密问题解决

问题描述

尝试用浏览器Web Crypto API对大文件进行1MB分块加密,避免处理大文件时浏览器崩溃。但分块加密后的文件解密时抛出异常:

DOMException: The operation failed for an operation-specific reason

即使解密不分块加密的文件也报错,推测加密环节存在问题。将IV长度从12字节改为16字节后解密功能恢复,但浏览器仍会崩溃,寻求解决办法。

原加密代码

<!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>

原解密代码

<!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>

问题根源及解决方案

核心问题分析

  1. 重复使用IV:AES-GCM要求每个加密块使用唯一的IV,原代码所有块共用同一个IV,违反GCM安全规范,直接导致解密失败。
  2. 未存储IV:解密需要和加密完全一致的IV,原代码未将IV与密文一起保存,解密时用全0IV,无法匹配。
  3. 并发内存溢出:原代码连续触发FileReader读取,导致多个加密任务并行执行,占用大量内存引发浏览器崩溃。
  4. 解密块大小错误:AES-GCM加密后会自动附加16字节认证标签,加密块实际大小是原块大小+16字节,原解密代码按1MB+12字节拆分,导致块数据错误。

修正后的加密代码

<!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.bin">Download Encrypted File</a>
    <script>
        document.getElementById("encryptButton").addEventListener("click", async () => {
            const fileInput = document.getElementById("fileInput");
            const file = fileInput.files[0];
            if (!file) {
                alert("请选择文件");
                return;
            }

            const chunkSize = 1024 * 1024; // 1MB块
            // 生成AES-GCM密钥
            const key = await window.crypto.subtle.generateKey(
                { name: "AES-GCM", length: 256 },
                true,
                ["encrypt", "decrypt"]
            );
            // 导出密钥保存
            const keyData = await window.crypto.subtle.exportKey('jwk', key);
            const keyBlob = new Blob([JSON.stringify(keyData)], { type: 'application/json' });
            const keyUrl = URL.createObjectURL(keyBlob);
            const keyLink = document.createElement('a');
            keyLink.href = keyUrl;
            keyLink.download = 'encryption-key.json';
            keyLink.click();

            const encryptedChunks = [];
            let offset = 0;

            // 串行处理每个块,避免并发内存占用过高
            while (offset < file.size) {
                const slice = file.slice(offset, offset + chunkSize);
                const chunkBuffer = await slice.arrayBuffer();
                const chunk = new Uint8Array(chunkBuffer);
                
                // 每个块生成唯一的随机IV(12字节是GCM推荐长度)
                const iv = window.crypto.getRandomValues(new Uint8Array(12));
                // 加密块,GCM默认附加16字节认证标签
                const encryptedChunk = await window.crypto.subtle.encrypt(
                    { name: "AES-GCM", iv },
                    key,
                    chunk
                );
                
                // 拼接IV和加密数据(IV在前,方便解密时读取)
                const combined = new Uint8Array(iv.length + encryptedChunk.byteLength);
                combined.set(iv, 0);
                combined.set(new Uint8Array(encryptedChunk), iv.length);
                encryptedChunks.push(combined);
                
                offset += chunk.length;
            }

            // 合并所有块并生成下载链接
            const encryptedData = new Blob(encryptedChunks, { type: 'application/octet-stream' });
            const url = URL.createObjectURL(encryptedData);
            const downloadLink = document.getElementById("downloadLink");
            downloadLink.href = url;
            downloadLink.style.display = "block";
            console.log('加密完成');
        });
    </script>
</body>
</html>

修正后的解密代码

<!DOCTYPE html>
<html>
<head>
    <title>File Decryption</title>
</head>
<body>
    <h1>文件解密</h1>
    <input type="file" id="encryptedFileInput" />
    <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 () => {
            const encryptedFile = document.getElementById("encryptedFileInput").files[0];
            const keyFile = document.getElementById("keyInput").files[0];
            if (!encryptedFile || !keyFile) {
                alert("请选择加密文件和密钥");
                return;
            }

            // 导入密钥
            const keyData = await keyFile.text();
            const keyObj = JSON.parse(keyData);
            const key = await crypto.subtle.importKey(
                "jwk",
                keyObj,
                { name: "AES-GCM" },
                false,
                ["decrypt"]
            );

            const ivLength = 12;
            const authTagLength = 16;
            const chunkSize = 1024 * 1024 + ivLength + authTagLength; // 加密块总大小:原块+IV+认证标签
            const decryptedChunks = [];
            let offset = 0;

            // 串行处理每个块
            while (offset < encryptedFile.size) {
                // 读取当前块(最后一块可能小于chunkSize)
                const endOffset = Math.min(offset + chunkSize, encryptedFile.size);
                const slice = encryptedFile.slice(offset, endOffset);
                const chunkBuffer = await slice.arrayBuffer();
                const chunk = new Uint8Array(chunkBuffer);
                
                // 提取IV(前12字节)
                const iv = chunk.slice(0, ivLength);
                // 提取加密数据(剩余部分,包含认证标签)
                const encryptedData = chunk.slice(ivLength);
                
                // 解密块
                const decryptedChunk = await window.crypto.subtle.decrypt(
                    { name: "AES-GCM", iv },
                    key,
                    encryptedData
                );
                decryptedChunks.push(decryptedChunk);
                
                offset += chunk.length;
            }

            // 合并解密后的数据并下载
            const decryptedData = new Blob(decryptedChunks, { type: encryptedFile.type });
            const url = URL.createObjectURL(decryptedData);
            const downloadLink = document.getElementById("downloadLink");
            downloadLink.href = url;
            downloadLink.download = `decrypted-${encryptedFile.name}`;
            downloadLink.style.display = "block";
            downloadLink.click();
            console.log('解密完成');
        });
    </script>
</body>
</html>

关键改进点

  • 每个加密块生成唯一随机IV,并将IV与加密数据拼接存储,保证解密时能获取对应IV。
  • 使用while循环串行处理块,避免并发加密/解密导致的内存暴涨,解决浏览器崩溃问题。
  • 正确计算加密/解密的块大小,包含IV和认证标签的长度,确保块拆分准确。
  • 使用slice.arrayBuffer()替代FileReader,代码更简洁且避免回调嵌套。

内容的提问来源于stack exchange,提问作者bulsaka

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 10:37:02