// NymDrop — client-side encryption via WebCrypto API. // No external libraries. Plaintext never leaves the browser. // Server's X25519 public key — replaced at deploy time. const NYMDROP_PUBKEY_HEX = "6aa99a672c48cb8eee65ddd5c5e5f8947a8d52d39a8b8eb4e11b15c87fe49038"; async function hexToBytes(hex) { const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.substr(i, 2), 16); return bytes; } async function encryptPayload(plaintext) { const serverPubBytes = await hexToBytes(NYMDROP_PUBKEY_HEX); // Generate ephemeral X25519 keypair for this submission. const ephemeral = await crypto.subtle.generateKey( { name: "ECDH", namedCurve: "X25519" }, true, ["deriveKey", "deriveBits"] ); // Import server's X25519 public key. const serverKey = await crypto.subtle.importKey( "raw", serverPubBytes, { name: "ECDH", namedCurve: "X25519" }, false, [] ); // ECDH key agreement → 32-byte shared secret. const sharedBits = await crypto.subtle.deriveBits( { name: "ECDH", public: serverKey }, ephemeral.privateKey, 256 ); // Derive AES-GCM-256 key via HKDF-SHA-256. const hkdfKey = await crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]); const aesKey = await crypto.subtle.deriveKey( { name: "HKDF", hash: "SHA-256", salt: new Uint8Array(32), info: new TextEncoder().encode("nymdrop-v1") }, hkdfKey, { name: "AES-GCM", length: 256 }, false, ["encrypt"] ); // Encrypt plaintext. const iv = crypto.getRandomValues(new Uint8Array(12)); const ciphertext = await crypto.subtle.encrypt( { name: "AES-GCM", iv }, aesKey, new TextEncoder().encode(plaintext) ); // Export ephemeral X25519 public key (32 bytes raw). const ephemeralPubRaw = await crypto.subtle.exportKey("raw", ephemeral.publicKey); // Packet layout: ephemeral_pub(32) + iv(12) + ciphertext const packet = new Uint8Array(32 + 12 + ciphertext.byteLength); packet.set(new Uint8Array(ephemeralPubRaw), 0); packet.set(iv, 32); packet.set(new Uint8Array(ciphertext), 44); return packet; } document.getElementById("drop-form").addEventListener("submit", async (e) => { e.preventDefault(); const status = document.getElementById("status"); status.className = ""; status.textContent = "Encrypting..."; try { let plaintext = document.getElementById("message").value; const fileInput = document.getElementById("file"); if (fileInput.files.length > 0) { const fileBytes = await fileInput.files[0].arrayBuffer(); const fileB64 = btoa(String.fromCharCode(...new Uint8Array(fileBytes))); plaintext += "\n---FILE:" + fileInput.files[0].name + "---\n" + fileB64; } const encrypted = await encryptPayload(plaintext); const resp = await fetch("/submit", { method: "POST", headers: { "Content-Type": "application/octet-stream" }, body: encrypted, }); if (resp.ok) { document.getElementById("drop-form").reset(); document.getElementById("filename").textContent = ""; status.textContent = "Delivered. No record of this submission exists on the server."; } else { status.className = "error"; status.textContent = "Delivery failed. Try again."; } } catch (err) { status.className = "error"; status.textContent = "Encryption failed: " + err.message; } });