aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/transport-pins.js
blob: c0699dc05dac46267bbc7701a9c155ba510a8f90 (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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// Which node this browser has met under which key (trust on first use), and
// the version range a node declares at the handshake.

// ── Node identity pinning (11.5.8) ───────────────────────────────────────────

const NODE_PIN_PREFIX = 'mb_nodepin_';

/**
 * The node's half of the version range, from `handshake_challenge`.
 *
 * Mirrors meshbay_common/handshake.py::check_version(). A node that declares no
 * range at all is a node that predates negotiation — that is every 0.x node, and
 * none of them can serve a sealed index or a sealed ack — so it is refused here
 * rather than left to fail later as a message that will not open.
 */
function _checkNodeVersion(reply) {
  const parse = (v) => {
    const m = /^(\d+)\.(\d+)$/.exec(String(v || ''));
    return m ? [Number(m[1]), Number(m[2])] : null;
  };
  const cmp = (a, b) => (a[0] - b[0]) || (a[1] - b[1]);
  const fail = (reason, message) => {
    const e = new Error(message);
    e.reason = reason;
    throw e;
  };

  const theirs = parse(reply.v);
  if (!theirs) {
    fail('node_version_unreadable',
         'The node did not declare a readable protocol version.');
  }
  // No declared minimum means "only what I speak" — the correct reading of a
  // node from before this field existed.
  const theirMin = parse(reply.v_min) || theirs;
  if (cmp(theirs, parse(MNP_V_MIN)) < 0) {
    fail('node_too_old',
         'This node is running an older MeshBay than this page needs. '
         + 'Its operator has to update it.');
  }
  if (cmp(theirMin, parse(MNP_V)) > 0) {
    fail('client_too_old',
         'This page is older than the node it is talking to. '
         + 'Reload to pick up the current version.');
  }
}

function _checkNodePin(nodeId, nodePk) {
  if (!nodeId || !nodePk) return;
  const key = NODE_PIN_PREFIX + nodeId;

  let pinned = null;
  try { pinned = localStorage.getItem(key); } catch { return; }

  if (pinned === null) {
    try { localStorage.setItem(key, nodePk); } catch {}
    return;
  }
  if (pinned !== nodePk) {
    throw new Error(
      'This node\'s identity key has changed. That is expected only if its ' +
      'operator reinstalled the node — otherwise someone may be impersonating ' +
      'it. Verify with the operator out of band, then clear the pin in ' +
      'Settings to accept the new key.');
  }
}

/** Forget a pinned node identity — the deliberate escape hatch for a legitimate rotation. */
function clearNodePin(nodeId) {
  try {
    if (nodeId) localStorage.removeItem(NODE_PIN_PREFIX + nodeId);
    else {
      for (const k of Object.keys(localStorage))
        if (k.startsWith(NODE_PIN_PREFIX)) localStorage.removeItem(k);
    }
  } catch {}
}

function pinnedNodeCount() {
  try {
    return Object.keys(localStorage).filter(k => k.startsWith(NODE_PIN_PREFIX)).length;
  } catch { return 0; }
}

MeshBayTransport.clearNodePin = clearNodePin;
MeshBayTransport.pinnedNodeCount = pinnedNodeCount;