zero-knowledge file sharing
at main 89 lines 2.9 kB view raw
1// AES-256-GCM helpers using Web Crypto API 2// Each upload gets a unique key, so a fixed zero IV is safe. 3// Filename + file body are packed into a single blob before encryption, 4// so only one (key, IV) pair is ever used. 5 6const IV = new Uint8Array(12); // 12 zero bytes 7const PADDING_BLOCK = 4096; 8 9export async function generateKey() { 10 const key = await crypto.subtle.generateKey( 11 { name: "AES-GCM", length: 256 }, 12 true, 13 ["encrypt", "decrypt"], 14 ); 15 const raw = await crypto.subtle.exportKey("raw", key); 16 return { 17 key, 18 encoded: new Uint8Array(raw).toBase64({ 19 alphabet: "base64url", 20 omitPadding: true, 21 }), 22 }; 23} 24 25export async function importKey(encoded: string) { 26 const raw = Uint8Array.fromBase64(encoded, { alphabet: "base64url" }); 27 return crypto.subtle.importKey("raw", raw, { name: "AES-GCM" }, false, [ 28 "encrypt", 29 "decrypt", 30 ]); 31} 32 33// Pack: [u16 filenameLen][u64 fileLen][filename][file][zero padding to 4K boundary] 34// Then encrypt the whole thing as one AES-GCM ciphertext. 35export async function encrypt( 36 fileName: string, 37 fileBuffer: ArrayBuffer, 38 key: CryptoKey, 39) { 40 const nameBytes = new TextEncoder().encode(fileName); 41 if (nameBytes.length > 0xffff) throw new Error("Filename too long"); 42 43 const headerSize = 2 + 8; // u16 + u64 44 const payloadSize = headerSize + nameBytes.length + fileBuffer.byteLength; 45 const paddedSize = Math.ceil(payloadSize / PADDING_BLOCK) * PADDING_BLOCK; 46 47 const buf = new ArrayBuffer(paddedSize); 48 const view = new DataView(buf); 49 const bytes = new Uint8Array(buf); 50 51 // Header 52 view.setUint16(0, nameBytes.length, false); // big-endian 53 // u64 file length — DataView has no setUint64, use two u32s 54 const fileLen = fileBuffer.byteLength; 55 view.setUint32(2, Math.floor(fileLen / 0x100000000), false); // high 32 56 view.setUint32(6, fileLen >>> 0, false); // low 32 57 58 // Filename + file body 59 bytes.set(nameBytes, headerSize); 60 bytes.set(new Uint8Array(fileBuffer), headerSize + nameBytes.length); 61 62 const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv: IV }, key, buf); 63 return new Uint8Array(ct); 64} 65 66// Decrypt and unpack — returns { fileName, fileData } 67export async function decrypt( 68 ciphertext: Uint8Array<ArrayBuffer>, 69 key: CryptoKey, 70) { 71 const plain = await crypto.subtle.decrypt( 72 { name: "AES-GCM", iv: IV }, 73 key, 74 ciphertext, 75 ); 76 77 const view = new DataView(plain); 78 const nameLen = view.getUint16(0, false); 79 const fileLenHi = view.getUint32(2, false); 80 const fileLenLo = view.getUint32(6, false); 81 const fileLen = fileLenHi * 0x100000000 + fileLenLo; 82 83 const headerSize = 2 + 8; 84 const nameBytes = new Uint8Array(plain, headerSize, nameLen); 85 const fileName = new TextDecoder().decode(nameBytes); 86 const fileData = new Uint8Array(plain, headerSize + nameLen, fileLen); 87 88 return { fileName, fileData }; 89}