// The proof of work that gates sign-up and passphrase reset (the hub's // captcha.py). The hub sends the SHA-256 of `` and the page // finds the number by trying them from 0, so a person waits for nothing: it is // solved while the form is being filled in. Nothing leaves for a third party. // // On the main thread, in batches handed to SubtleCrypto, rather than in a Web // Worker: the Android shell serves the interface through its own request // interceptor, and a worker's script is a request that interceptor is not // guaranteed to see. Each batch is a turn of the event loop, so typing in the // form is not held up by it. const BATCH = 1000; function fromHex(hex) { const out = new Uint8Array(hex.length / 2); for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.substr(i * 2, 2), 16); return out; } function same(buf, want) { const got = new Uint8Array(buf); for (let i = 0; i < want.length; i++) if (got[i] !== want[i]) return false; return true; } /** * The answer to a challenge from `GET /v1/users/captcha`, as the token the * register and reset-request bodies carry — or null if no number below * `max_number` matches, which only a challenge that is not the hub's produces, * or once `signal` is aborted: the form was left before it was needed. */ export async function solve({ prefix, challenge, max_number: max, signature }, signal) { const enc = new TextEncoder(); const want = fromHex(challenge); for (let start = 0; start < max; start += BATCH) { if (signal && signal.aborted) return null; const end = Math.min(start + BATCH, max); const pending = []; for (let n = start; n < end; n++) { pending.push(crypto.subtle.digest('SHA-256', enc.encode(prefix + n))); } const digests = await Promise.all(pending); const hit = digests.findIndex(d => same(d, want)); if (hit >= 0) return `${prefix}${start + hit}.${signature}`; } return null; }