summaryrefslogtreecommitdiffstats
path: root/static/crypto.js
diff options
context:
space:
mode:
authorGab Virebent <gabriel1@virebent.art>2026-06-20 01:07:45 +0000
committerGab Virebent <gabriel1@virebent.art>2026-06-20 01:07:45 +0000
commitaa459e3b84cc4c5d908f3781c9253848c18640c3 (patch)
tree8454b956cc0fa027034c8291f39d58ece469e10c /static/crypto.js
downloadnymdrop-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.js111
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;
+ }
+});