diff options
Diffstat (limited to 'static/crypto.js')
| -rw-r--r-- | static/crypto.js | 49 |
1 files changed, 44 insertions, 5 deletions
diff --git a/static/crypto.js b/static/crypto.js index 63b395f..0455958 100644 --- a/static/crypto.js +++ b/static/crypto.js @@ -4,6 +4,10 @@ // 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) @@ -11,12 +15,40 @@ async function hexToBytes(hex) { 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("<unix-ts>:<nonce>") 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: "ECDH", namedCurve: "X25519" }, + { name: "X25519" }, true, ["deriveKey", "deriveBits"] ); @@ -25,14 +57,14 @@ async function encryptPayload(plaintext) { const serverKey = await crypto.subtle.importKey( "raw", serverPubBytes, - { name: "ECDH", namedCurve: "X25519" }, + { name: "X25519" }, false, [] ); - // ECDH key agreement → 32-byte shared secret. + // X25519 key agreement → 32-byte shared secret. const sharedBits = await crypto.subtle.deriveBits( - { name: "ECDH", public: serverKey }, + { name: "X25519", public: serverKey }, ephemeral.privateKey, 256 ); @@ -90,9 +122,16 @@ document.getElementById("drop-form").addEventListener("submit", async (e) => { 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" }, + headers: { + "Content-Type": "application/octet-stream", + "X-Nymdrop-Pow": powStamp, + }, body: encrypted, }); |
