summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/crypto.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/crypto.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/crypto.js67
1 files changed, 67 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/crypto.js b/packages/meshbay-hub/src/meshbay_hub/static/crypto.js
index 21bf05d..1ddaa50 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/crypto.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/crypto.js
@@ -321,6 +321,8 @@ async function handshakeProof(gekRaw, role, groupId, nonceClient, nonceNode, bin
// wrap the group key for a key that came over the wire instead of one fetched
// from the hub's directory (H3). nonce_node ties it to this connection.
const JOIN_PREFIX = new TextEncoder().encode('meshbay:join:v1');
+const DEVICE_REQ_PREFIX = new TextEncoder().encode('meshbay:device_req:v1');
+const DEVICE_ADD_PREFIX = new TextEncoder().encode('meshbay:device_add:v1');
function joinTranscript(nodePkB64, groupId, userId, pkEdB64, pkXB64, nonceNode, ts) {
const enc = new TextEncoder();
@@ -339,6 +341,69 @@ function joinTranscript(nodePkB64, groupId, userId, pkEdB64, pkXB64, nonceNode,
return out;
}
+/**
+ * Device linking transcripts, mirroring meshbay_common/device.py.
+ *
+ * Two signatures admit a device: the new one proves it holds the keys it is
+ * presenting, and a key the node already pinned countersigns them. The hub can
+ * produce neither — it has stored no user keys since 2026-08-14 — which is what
+ * makes this safe to do without an operator.
+ */
+function deviceRequestTranscript(nodePkB64, userId, pkEdB64, pkXB64, codeHash,
+ nonceNode, ts) {
+ const enc = new TextEncoder();
+ const body = _lenPrefixed([
+ enc.encode(nodePkB64), enc.encode(userId), enc.encode(pkEdB64),
+ enc.encode(pkXB64), enc.encode(codeHash), nonceNode, enc.encode(String(ts)),
+ ]);
+ const out = new Uint8Array(DEVICE_REQ_PREFIX.length + body.length);
+ out.set(DEVICE_REQ_PREFIX, 0);
+ out.set(body, DEVICE_REQ_PREFIX.length);
+ return out;
+}
+
+function deviceAddTranscript(nodePkB64, userId, pkEdB64, pkXB64, nonceNode, ts) {
+ const enc = new TextEncoder();
+ const body = _lenPrefixed([
+ enc.encode(nodePkB64), enc.encode(userId), enc.encode(pkEdB64),
+ enc.encode(pkXB64), nonceNode, enc.encode(String(ts)),
+ ]);
+ const out = new Uint8Array(DEVICE_ADD_PREFIX.length + body.length);
+ out.set(DEVICE_ADD_PREFIX, 0);
+ out.set(body, DEVICE_ADD_PREFIX.length);
+ return out;
+}
+
+/**
+ * sha256(code ‖ pk_ed ‖ pk_x), hex — the lookup key for a pending request.
+ *
+ * The keys go in with the code, so the hash identifies *this device asking with
+ * this code* rather than *this code*. That is what stops the node answering an
+ * approver with a substituted key: the approver recomputes this from what they
+ * typed and what they were handed, and a substitution finds nothing. Nothing
+ * here rests on a human comparing digits.
+ */
+async function deviceCodeHash(code, pkEdB64, pkXB64) {
+ const enc = new TextEncoder();
+ const payload = enc.encode([code, pkEdB64, pkXB64].join('\x1f'));
+ const digest = await crypto.subtle.digest('SHA-256', payload);
+ return Array.from(new Uint8Array(digest))
+ .map(b => b.toString(16).padStart(2, '0')).join('');
+}
+
+/** Crockford folding, mirroring roster.normalize_code. */
+function normalizeCode(code) {
+ let out = '';
+ for (const ch of code.toUpperCase()) {
+ if (ch === '-' || ch === ' ' || ch === '\t') continue;
+ if (ch === 'I' || ch === 'L') out += '1';
+ else if (ch === 'O') out += '0';
+ else if (ch === 'U') out += 'V';
+ else out += ch;
+ }
+ return out;
+}
+
function constantTimeEqual(a, b) {
if (a.length !== b.length) return false;
let diff = 0;
@@ -359,4 +424,6 @@ window.MeshBayCrypto = {
generateGEK, wrapGEK, unwrapGEK, encryptChunk, b64encode, b64decode,
adminTranscript, handshakeTranscript, handshakeProof, webrtcBinding,
joinTranscript, verifyNodeSignature, constantTimeEqual,
+ deviceRequestTranscript, deviceAddTranscript, deviceCodeHash,
+ normalizeCode,
};