zero-knowledge file sharing
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899/// <reference lib="WebWorker" />
import { ctRange, decryptRange } from "./crypto";import { guessMime } from "./utils";
declare const self: ServiceWorkerGlobalScope;
self.addEventListener("install", () => self.skipWaiting());self.addEventListener("activate", (e) => e.waitUntil(self.clients.claim()));
interface Entry { key: CryptoKey; fileName: string; fileLen: number;}
const entries = new Map<string, Entry>();
// Parse Range header, resolving against total file size.// Returns null if no valid range, otherwise [start, end] inclusive.function parseRange(header: string | null, fileLen: number): [number, number] | null { if (!header || fileLen <= 0) return null; const m = header.match(/^bytes=(\d*)-(\d*)$/); if (!m) return null; let start: number, end: number; if (!m[1]) { start = Math.max(0, fileLen - parseInt(m[2]!)); end = fileLen - 1; } else { start = parseInt(m[1]); end = m[2] ? Math.min(parseInt(m[2]), fileLen - 1) : fileLen - 1; } return start <= end ? [start, end] : null;}
self.addEventListener("message", (event: ExtendableMessageEvent) => { const { type, id, key, fileName, fileLen } = event.data; if (type === "register-key") { entries.set(id, { key, fileName, fileLen }); event.ports[0]?.postMessage("ok"); } else if (type === "claim") { event.waitUntil(self.clients.claim()); }});
self.addEventListener("fetch", (event: FetchEvent) => { const { pathname } = new URL(event.request.url); const match = pathname.match(/^\/([^/]+)\/decrypted$/); if (!match) return;
const [, id] = match as [string, string];
event.respondWith( (async () => { const entry = entries.get(id); if (!entry) return new Response("Key not registered.", { status: 400 });
const { key, fileName, fileLen } = entry; const mime = guessMime(fileName); const range = parseRange(event.request.headers.get("Range"), fileLen); const plainStart = range?.[0] ?? 0; const plainEnd = range?.[1] ?? Math.max(fileLen - 1, 0); const responseLen = plainEnd - plainStart + 1; const headers: Record<string, string> = { "Content-Type": mime, "Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`, "Content-Length": String(responseLen), "Accept-Ranges": "bytes", };
// Fetch ciphertext (range-narrowed for partial requests, full for non-range) const fetchHeaders: Record<string, string> = {}; if (range) { const [ctStart, ctEnd] = ctRange(plainStart, plainEnd); fetchHeaders.Range = `bytes=${ctStart}-${ctEnd}`; }
const ctResp = await fetch(`/api/file/${id}`, { headers: fetchHeaders }); if (!ctResp.ok && ctResp.status !== 206) { return new Response( ctResp.status === 404 ? "File not found or expired." : "Download failed.", { status: ctResp.status }, ); }
const stream = decryptRange(ctResp.body!.getReader(), key, fileLen, plainStart, plainEnd);
if (range) { headers["Content-Range"] = `bytes ${plainStart}-${plainEnd}/${fileLen}`; return new Response(stream, { status: 206, headers }); }
return new Response(stream, { headers }); })().catch( (err: any) => new Response(err.message ?? "error", { status: err.status ?? 500 }), ), );});