diff options
| author | Gab Virebent <gabriel1@virebent.art> | 2026-06-20 01:07:45 +0000 |
|---|---|---|
| committer | Gab Virebent <gabriel1@virebent.art> | 2026-06-20 01:07:45 +0000 |
| commit | aa459e3b84cc4c5d908f3781c9253848c18640c3 (patch) | |
| tree | 8454b956cc0fa027034c8291f39d58ece469e10c /static/crypto.js | |
| download | nymdrop-aa459e3b84cc4c5d908f3781c9253848c18640c3.tar.gz nymdrop-aa459e3b84cc4c5d908f3781c9253848c18640c3.tar.xz nymdrop-aa459e3b84cc4c5d908f3781c9253848c18640c3.zip | |
Initial public release: Nym-native anonymous submission system
End-to-end verified pipeline (browser -> HTTP relay -> Nym mixnet -> reader).
Client-side X25519+HKDF+AES-GCM-256, no-log blind relay, AGPL-3.0.
Diffstat (limited to 'static/crypto.js')
| -rw-r--r-- | static/crypto.js | 111 |
1 files changed, 111 insertions, 0 deletions
diff --git a/static/crypto.js b/static/crypto.js new file mode 100644 index 0000000..63b395f --- /dev/null +++ b/static/crypto.js @@ -0,0 +1,111 @@ +// 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; + } +}); |
