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 | |
| 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')
| -rw-r--r-- | static/crypto.js | 111 | ||||
| -rw-r--r-- | static/index.html | 61 | ||||
| -rw-r--r-- | static/style.css | 179 |
3 files changed, 351 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; + } +}); diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..c957a01 --- /dev/null +++ b/static/index.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="referrer" content="no-referrer"> + <title>NymDrop — Secure Submission</title> + <link rel="stylesheet" href="/style.css"> +</head> +<body> +<div class="card"> + + <div class="logo"> + <div class="logo-icon">⬡</div> + <h1>NYMDROP</h1> + </div> + <p class="tagline">Anonymous document submission — encrypted in your browser, delivered over Nym mixnet.</p> + + <div class="badges"> + <span class="badge badge-green">END-TO-END ENCRYPTED</span> + <span class="badge badge-blue">NYM MIXNET</span> + <span class="badge badge-gray">NO LOGS</span> + <span class="badge badge-gray">NO METADATA</span> + </div> + + <form id="drop-form"> + <label for="message">Message</label> + <textarea id="message" placeholder="Write your message here. It will be encrypted before leaving your browser."></textarea> + + <div class="file-section"> + <label>Attachment (optional)</label> + <label class="file-label" for="file">📎 Choose file</label> + <input type="file" id="file"> + <span id="filename"></span> + </div> + + <button type="submit" class="submit-btn">🔒 Submit securely</button> + </form> + + <div id="status"></div> + + <hr class="divider"> + + <p class="notice"> + <span>Your submission is encrypted in this browser before being sent.</span> + The server never sees the content and retains no logs. + Once delivered over the Nym mixnet, no record of this submission exists on this server. + For maximum anonymity, submit from a public network. + </p> + +</div> +<div class="footer">NYMDROP · ZERO KNOWLEDGE · ZERO LOGS</div> +<script src="/crypto.js"></script> +<script> + document.getElementById('file').addEventListener('change', function() { + const name = this.files.length ? this.files[0].name : ''; + document.getElementById('filename').textContent = name; + }); +</script> +</body> +</html> diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..3b077d7 --- /dev/null +++ b/static/style.css @@ -0,0 +1,179 @@ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +body { + background: #0d1117; + color: #c9d1d9; + font-family: 'Courier New', Courier, monospace; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 2rem 1rem; +} + +.card { + background: #161b22; + border: 1px solid #21262d; + border-radius: 8px; + padding: 2.5rem 2rem; + width: 100%; + max-width: 580px; + box-shadow: 0 0 40px rgba(0,255,160,0.04); +} + +.logo { + display: flex; + align-items: center; + gap: 0.7rem; + margin-bottom: 0.5rem; +} + +.logo-icon { + width: 32px; + height: 32px; + background: linear-gradient(135deg, #00ffa0, #0080ff); + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + color: #0d1117; + font-weight: bold; +} + +h1 { + font-size: 1.4rem; + font-weight: bold; + color: #e6edf3; + letter-spacing: 0.15em; +} + +.tagline { + font-size: 0.72rem; + color: #484f58; + margin-bottom: 2rem; + letter-spacing: 0.03em; +} + +.badges { + display: flex; + gap: 0.5rem; + margin-bottom: 2rem; + flex-wrap: wrap; +} + +.badge { + font-size: 0.65rem; + padding: 0.2rem 0.6rem; + border-radius: 20px; + letter-spacing: 0.05em; + font-weight: bold; +} + +.badge-green { background: rgba(0,255,160,0.08); color: #00ffa0; border: 1px solid rgba(0,255,160,0.2); } +.badge-blue { background: rgba(0,128,255,0.08); color: #58a6ff; border: 1px solid rgba(0,128,255,0.2); } +.badge-gray { background: rgba(255,255,255,0.04); color: #8b949e; border: 1px solid #30363d; } + +label { + display: block; + font-size: 0.72rem; + color: #8b949e; + margin-bottom: 0.4rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +textarea { + width: 100%; + height: 160px; + background: #0d1117; + border: 1px solid #30363d; + border-radius: 6px; + color: #c9d1d9; + font-family: 'Courier New', Courier, monospace; + font-size: 0.88rem; + padding: 0.9rem; + resize: vertical; + margin-bottom: 1.4rem; + transition: border-color 0.2s; + outline: none; +} + +textarea:focus { border-color: #00ffa0; box-shadow: 0 0 0 2px rgba(0,255,160,0.08); } + +.file-section { margin-bottom: 1.8rem; } + +.file-label { + display: inline-block; + background: #21262d; + border: 1px solid #30363d; + border-radius: 6px; + color: #8b949e; + font-size: 0.78rem; + padding: 0.5rem 1rem; + cursor: pointer; + transition: border-color 0.2s, color 0.2s; +} + +.file-label:hover { border-color: #58a6ff; color: #58a6ff; } + +#file { display: none; } + +#filename { + font-size: 0.72rem; + color: #58a6ff; + margin-left: 0.6rem; + vertical-align: middle; +} + +.submit-btn { + width: 100%; + background: linear-gradient(135deg, #00ffa0 0%, #0080ff 100%); + border: none; + border-radius: 6px; + color: #0d1117; + font-family: 'Courier New', Courier, monospace; + font-size: 0.9rem; + font-weight: bold; + letter-spacing: 0.1em; + padding: 0.85rem; + cursor: pointer; + transition: opacity 0.2s, transform 0.1s; + text-transform: uppercase; +} + +.submit-btn:hover { opacity: 0.9; } +.submit-btn:active { transform: scale(0.99); } + +#status { + margin-top: 1rem; + font-size: 0.78rem; + min-height: 1.2rem; + text-align: center; + color: #00ffa0; +} + +#status.error { color: #f85149; } + +.divider { + border: none; + border-top: 1px solid #21262d; + margin: 2rem 0 1.2rem; +} + +.notice { + font-size: 0.68rem; + color: #3d444d; + line-height: 1.7; +} + +.notice span { color: #484f58; } + +.footer { + margin-top: 1.5rem; + font-size: 0.62rem; + color: #21262d; + text-align: center; + letter-spacing: 0.05em; +} |
