aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/captcha.js
blob: bc174a00a3434665762b6df1ebea428f7eaa5f3d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// The proof of work that gates sign-up and passphrase reset (the hub's
// captcha.py). The hub sends the SHA-256 of `<prefix><number>` 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;
}