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>
问题根源及解决方案
核心问题分析
- 重复使用IV:AES-GCM要求每个加密块使用唯一的IV,原代码所有块共用同一个IV,违反GCM安全规范,直接导致解密失败。
- 未存储IV:解密需要和加密完全一致的IV,原代码未将IV与密文一起保存,解密时用全0IV,无法匹配。
- 并发内存溢出:原代码连续触发FileReader读取,导致多个加密任务并行执行,占用大量内存引发浏览器崩溃。
- 解密块大小错误: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
相关产品推荐
相关产品推荐

