export async function downloadAndDecryptFile( url: string, password: string, onProgress?: (progress: number) => void ): Promise { const response = await fetch(url); if (!response.ok) throw new Error(`Failed to fetch: ${response.status}`); const contentLength = +(response.headers.get('Content-Length') || 0); const reader = response.body!.getReader(); const chunks: Uint8Array[] = []; let received = 0; while (true) { const { done, value } = await reader.read(); if (done) break; if (value) { chunks.push(value); received += value.length; if (contentLength && onProgress) { onProgress(received / contentLength); } } } const fullBuffer = new Uint8Array(received); let offset = 0; for (const chunk of chunks) { fullBuffer.set(chunk, offset); offset += chunk.length; } const decryptedBytes = await decryptFile(fullBuffer, password); // Create a blob and trigger a download const blob = new Blob([decryptedBytes]); const downloadUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = downloadUrl; a.download = 'decrypted_file'; // You may allow customization document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(downloadUrl); } export async function decryptFile( fileBuffer: Uint8Array, password: string ): Promise { const salt = fileBuffer.slice(0, 16); const nonce = fileBuffer.slice(16, 28); const tag = fileBuffer.slice(28, 44); const ciphertext = fileBuffer.slice(44); const enc = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey( 'raw', enc.encode(password), { name: 'PBKDF2' }, false, ['deriveKey'] ); const key = await crypto.subtle.deriveKey( { name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' }, keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['decrypt'] ); const fullCiphertext = new Uint8Array(ciphertext.length + tag.length); fullCiphertext.set(ciphertext); fullCiphertext.set(tag, ciphertext.length); let decrypted: ArrayBuffer; try { decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: nonce, tagLength: 128 }, key, fullCiphertext ); } catch { throw new Error("Incorrect password or corrupted file."); } const magic = new TextEncoder().encode("DYSON1"); const decryptedBytes = new Uint8Array(decrypted); for (let i = 0; i < magic.length; i++) { if (decryptedBytes[i] !== magic[i]) { throw new Error("Incorrect password or corrupted file."); } } return decryptedBytes.slice(magic.length); }