// 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"; // Required leading zero bits for the proof of work — must match the // server's --pow-difficulty flag. const NYMDROP_POW_DIFFICULTY = 18; 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; } function leadingZeroBits(bytes) { let count = 0; for (const b of bytes) { if (b === 0) { count += 8; continue; } count += Math.clz32(b) - 24; // Math.clz32 counts over 32 bits, b is 8 bits break; } return count; } // Self-contained hashcash-style proof of work: no challenge round-trip with // the server. Finds a nonce such that SHA-256(":") has at // least `difficultyBits` leading zero bits. The server only verifies this, // it never computes it — the cost is entirely client-side. async function computeProofOfWork(difficultyBits) { const encoder = new TextEncoder(); const ts = Math.floor(Date.now() / 1000); for (let nonce = 0; ; nonce++) { const stamp = ts + ":" + nonce; const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(stamp))); if (leadingZeroBits(hash) >= difficultyBits) { return stamp; } } } async function encryptPayload(plaintext) { const serverPubBytes = await hexToBytes(NYMDROP_PUBKEY_HEX); // Generate ephemeral X25519 keypair for this submission. // Modern WebCrypto exposes X25519 as its own algorithm (secure-curves // spec), not as ECDH+namedCurve — that combination doesn't exist. const ephemeral = await crypto.subtle.generateKey( { name: "X25519" }, true, ["deriveKey", "deriveBits"] ); // Import server's X25519 public key. const serverKey = await crypto.subtle.importKey( "raw", serverPubBytes, { name: "X25519" }, false, [] ); // X25519 key agreement → 32-byte shared secret. const sharedBits = await crypto.subtle.deriveBits( { name: "X25519", 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); status.textContent = "Computing proof of work (a few seconds)..."; const powStamp = await computeProofOfWork(NYMDROP_POW_DIFFICULTY); status.textContent = "Sending..."; const resp = await fetch("/submit", { method: "POST", headers: { "Content-Type": "application/octet-stream", "X-Nymdrop-Pow": powStamp, }, 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; } });