diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/crypto.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/crypto.js | 66 |
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, }; |