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