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.js66
1 files changed, 57 insertions, 9 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/crypto.js b/packages/meshbay-hub/src/meshbay_hub/static/crypto.js
index 2346fa2..d18eeae 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/crypto.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/crypto.js
@@ -268,22 +268,70 @@ function adminTranscript(op, nodePkB64, groupId, subject, nonceB64, ts) {
// ── GEK proof (HMAC-SHA256 for handshake challenge) ─────────────────────────
-async function hmacGEK(gekRaw, nonceB64, offerFp, answerFp) {
- const nonce = b64decode(nonceB64);
- const data = concatBuffers([
- nonce,
- offerFp || new Uint8Array(0),
- answerFp || new Uint8Array(0),
+// Mirrors meshbay_common/handshake.py. Every field length-prefixed and the role
+// bound in, so a client proof can never be replayed as a node proof and a missing
+// fingerprint cannot silently degrade the proof to nonce-only (L4).
+const HANDSHAKE_PREFIX = new TextEncoder().encode('meshbay:mnp:handshake:v1');
+
+function _lenPrefixed(parts) {
+ let total = 0;
+ for (const p of parts) total += 4 + p.length;
+ const out = new Uint8Array(total);
+ const view = new DataView(out.buffer);
+ let off = 0;
+ for (const p of parts) {
+ view.setUint32(off, p.length, false);
+ off += 4;
+ out.set(p, off);
+ off += p.length;
+ }
+ return out;
+}
+
+function webrtcBinding(offerFp, answerFp) {
+ if (!offerFp || !offerFp.length || !answerFp || !answerFp.length) {
+ throw new Error('Channel binding unavailable — refusing to handshake');
+ }
+ return _lenPrefixed([offerFp, answerFp]);
+}
+
+function handshakeTranscript(role, groupId, nonceClient, nonceNode, binding) {
+ const enc = new TextEncoder();
+ const body = _lenPrefixed([
+ enc.encode(role), enc.encode(groupId), nonceClient, nonceNode, binding,
]);
+ const out = new Uint8Array(HANDSHAKE_PREFIX.length + body.length);
+ out.set(HANDSHAKE_PREFIX, 0);
+ out.set(body, HANDSHAKE_PREFIX.length);
+ return out;
+}
+
+async function handshakeProof(gekRaw, role, groupId, nonceClient, nonceNode, binding) {
+ const transcript = handshakeTranscript(role, groupId, nonceClient, nonceNode, binding);
const key = await crypto.subtle.importKey(
'raw', gekRaw, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
- const sig = await crypto.subtle.sign('HMAC', key, data);
- return b64encode(new Uint8Array(sig));
+ const sig = await crypto.subtle.sign('HMAC', key, transcript);
+ return new Uint8Array(sig);
+}
+
+function constantTimeEqual(a, b) {
+ if (a.length !== b.length) return false;
+ let diff = 0;
+ for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
+ return diff === 0;
+}
+
+/** Verify the node's Ed25519 signature over the handshake transcript (C3). */
+async function verifyNodeSignature(nodePkB64, sigB64, transcript) {
+ const raw = b64decode(nodePkB64);
+ const key = await crypto.subtle.importKey('raw', raw, { name: 'Ed25519' }, false, ['verify']);
+ return crypto.subtle.verify('Ed25519', key, b64decode(sigB64), transcript);
}
// Export for use in app.js
window.MeshBayCrypto = {
importGEK, deriveChunkKey, decryptChunk, decryptChunkBin, decryptFile,
generateGEK, wrapGEK, unwrapGEK, encryptChunk, b64encode, b64decode,
- hmacGEK, adminTranscript,
+ adminTranscript, handshakeTranscript, handshakeProof, webrtcBinding,
+ verifyNodeSignature, constantTimeEqual,
};