diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/transport.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/transport.js | 178 |
1 files changed, 160 insertions, 18 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index 1dc1ded..ca9c60e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -16,6 +16,16 @@ * transport.close(); */ +async function _pkFromSk(skPkcs8B64) { + const raw = Uint8Array.from(atob(skPkcs8B64), c => c.charCodeAt(0)); + const sk = await crypto.subtle.importKey('pkcs8', raw, { name: 'X25519' }, true, ['deriveBits']); + const jwk = await crypto.subtle.exportKey('jwk', sk); + const b64url = jwk.x; + const b64 = b64url.replace(/-/g, '+').replace(/_/g, '/'); + const pad = b64.length % 4; + return pad ? b64 + '='.repeat(4 - pad) : b64; +} + class MeshBayTransport { constructor(hubUrl, accessToken) { this._hubUrl = hubUrl; @@ -30,6 +40,7 @@ class MeshBayTransport { this._onStreamInit = null; this._onStreamData = null; this._onStreamEnd = null; + this._onIndexSync = null; } get connected() { return this._connected; } @@ -38,8 +49,15 @@ class MeshBayTransport { set onStreamInit(fn) { this._onStreamInit = fn; } set onStreamData(fn) { this._onStreamData = fn; } set onStreamEnd(fn) { this._onStreamEnd = fn; } + set onIndexSync(fn) { this._onIndexSync = fn; } + + get sessionKeys() { return this._sessionKeys; } - async connect(nodeId, jwtToken, groupId) { + async connect(nodeId, jwtToken, groupId, gekRaw, sessionKeys, bundleKey, username) { + this._gekRaw = gekRaw || null; + this._sessionKeys = sessionKeys || null; + this._bundleKey = bundleKey || null; + this._username = username || null; this._pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }], }); @@ -106,22 +124,94 @@ class MeshBayTransport { } const answer = await resp.json(); + this._rawAnswerSdp = answer.sdp; await this._pc.setRemoteDescription({ type: 'answer', sdp: answer.sdp }); await channelReady; - const ack = await this._sendAndWait({ + const reply = await this._sendAndWait({ type: 'handshake', v: '0.1', token: jwtToken, group_id: groupId || '', }); - if (ack.type !== 'handshake_ack') { - throw new Error('MNP handshake rejected: ' + (ack.detail || JSON.stringify(ack))); + if (reply.type === 'handshake_challenge') { + if (!window.MeshBayCrypto) { + throw new Error('Node requires GEK proof but no crypto available'); + } + + // Recover session keys from node if not available locally (P2P keypair bundle) + if (!this._sessionKeys && this._bundleKey && window.MeshBayKeys) { + const kpResp = await this._sendAndWait({ + type: 'keypair_bundle_fetch', v: '0.1', + }); + if (kpResp.type === 'keypair_bundle_resp' && kpResp.found) { + const keys = await window.MeshBayKeys.decryptBundleWithKey( + kpResp.bundle_enc, this._bundleKey); + const pkXB64 = await _pkFromSk(keys.skX); + this._sessionKeys = { skXB64: keys.skX, skEdB64: keys.skEd, pkXB64 }; + } + } + + // Fetch wrapped GEK bundle from node (P2P only — hub never touches crypto) + if (!gekRaw && this._sessionKeys) { + const bundleResp = await this._sendAndWait({ + type: 'gek_bundle_fetch', v: '0.1', + }); + if (bundleResp.type === 'gek_bundle_resp' && bundleResp.found) { + const skXRaw = Uint8Array.from(atob(this._sessionKeys.skXB64), c => c.charCodeAt(0)); + const myPkX = Uint8Array.from(atob(this._sessionKeys.pkXB64), c => c.charCodeAt(0)); + try { + gekRaw = await window.MeshBayCrypto.unwrapGEK(bundleResp, skXRaw, myPkX); + this._gekRaw = gekRaw; + } catch (e) { + console.warn('[MeshBay] GEK unwrap failed with local keys, trying node keypair bundle'); + if (this._bundleKey && window.MeshBayKeys) { + const kpResp = await this._sendAndWait({ + type: 'keypair_bundle_fetch', v: '0.1', + }); + if (kpResp.type === 'keypair_bundle_resp' && kpResp.found) { + const keys = await window.MeshBayKeys.decryptBundleWithKey( + kpResp.bundle_enc, this._bundleKey); + const pkXB64 = await _pkFromSk(keys.skX); + this._sessionKeys = { skXB64: keys.skX, skEdB64: keys.skEd, pkXB64 }; + const skXRaw2 = Uint8Array.from(atob(keys.skX), c => c.charCodeAt(0)); + const myPkX2 = Uint8Array.from(atob(pkXB64), c => c.charCodeAt(0)); + gekRaw = await window.MeshBayCrypto.unwrapGEK(bundleResp, skXRaw2, myPkX2); + this._gekRaw = gekRaw; + } + } + } + } + } + + if (!gekRaw) { + throw new Error('Node requires GEK proof but no GEK available'); + } + + let proof = ''; + if (gekRaw) { + const offerFp = _extractDtlsFingerprint(this._pc.localDescription.sdp); + const answerFp = _extractDtlsFingerprint(this._rawAnswerSdp); + proof = await window.MeshBayCrypto.hmacGEK(gekRaw, reply.nonce, offerFp, answerFp); + } + const ack = await this._sendAndWait({ + type: 'handshake_response', + v: '0.1', + proof, + }); + if (ack.type !== 'handshake_ack') { + throw new Error('GEK proof rejected: ' + (ack.detail || JSON.stringify(ack))); + } + return ack; } - return ack; + if (reply.type !== 'handshake_ack') { + throw new Error('MNP handshake rejected: ' + (reply.detail || JSON.stringify(reply))); + } + + return reply; } async fetchIndex() { @@ -130,12 +220,6 @@ class MeshBayTransport { return msg; } - async fetchGEK() { - const msg = await this._sendAndWait({ type: 'gek_req', v: '0.1' }); - if (msg.type === 'error') throw new Error(msg.detail); - return msg.gek_b64; - } - async fetchChunk(fileId, chunkIndex) { const msg = await this._sendAndWait({ type: 'file_req', @@ -182,13 +266,25 @@ class MeshBayTransport { return msg; } - async deleteFile(fileId) { + async deleteFile(fileId, signFn) { const msg = await this._sendAndWait({ type: 'file_delete', v: '0.1', file_id: fileId, }); if (msg.type === 'error') throw new Error(msg.detail); + if (msg.type === 'admin_challenge') { + if (!signFn) throw new Error('Admin challenge received but no signing key available'); + const signature = await signFn(msg.challenge); + const ack = await this._sendAndWait({ + type: 'admin_response', + v: '0.1', + file_id: fileId, + signature, + }); + if (ack.type === 'error') throw new Error(ack.detail); + return ack; + } return msg; } @@ -208,6 +304,32 @@ class MeshBayTransport { return msg; } + async storeGekBundle(userId, groupId, bundle) { + const msg = await this._sendAndWait({ + type: 'gek_bundle_store', + v: '0.1', + user_id: userId, + group_id: groupId, + pk_eph_b64: bundle.pk_eph_b64, + nonce_b64: bundle.nonce_b64, + wrapped_b64: bundle.wrapped_b64, + }); + if (msg.type === 'error') throw new Error(msg.detail); + return msg; + } + + async storeKeypairBundle(bundleEnc) { + const msg = await this._sendAndWait({ + type: 'keypair_bundle_store', + v: '0.1', + bundle_enc: bundleEnc, + }); + if (msg.type === 'error') throw new Error(msg.detail); + return msg; + } + + get gekRaw() { return this._gekRaw; } + close() { if (this._channel) this._channel.close(); if (this._pc) this._pc.close(); @@ -226,6 +348,7 @@ class MeshBayTransport { reject(new Error('Response timeout')); }, 30000); this._pending.set(id, { + _reqType: obj.type, resolve: (msg) => { clearTimeout(timeout); this._pending.delete(id); resolve(msg); }, reject: (err) => { clearTimeout(timeout); this._pending.delete(id); reject(err); }, }); @@ -269,16 +392,25 @@ class MeshBayTransport { this._onChat(msg); return; } - if (msg.type === 'stream_init' && this._onStreamInit) { - this._onStreamInit(msg); + if (msg.type === 'stream_init') { + if (this._onStreamInit) this._onStreamInit(msg); + return; + } + if (msg.type === 'stream_data') { + if (this._onStreamData) this._onStreamData(msg); return; } - if (msg.type === 'stream_data' && this._onStreamData) { - this._onStreamData(msg); + if (msg.type === 'stream_end') { + if (this._onStreamEnd) this._onStreamEnd(msg); return; } - if (msg.type === 'stream_end' && this._onStreamEnd) { - this._onStreamEnd(msg); + + if (msg.type === 'index_sync' && msg.entries) { + if (this._onIndexSync) this._onIndexSync(msg); + const oldest = this._pending.entries().next(); + if (!oldest.done && oldest.value[1]._reqType === 'index_sync') { + oldest.value[1].resolve(msg); + } return; } @@ -485,5 +617,15 @@ function _b64decode(b64) { return bytes; } +function _extractDtlsFingerprint(sdp) { + const match = sdp.match(/a=fingerprint:sha-256 ([0-9A-Fa-f:]+)/); + if (!match) return new Uint8Array(0); + const hex = match[1].replace(/:/g, ''); + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) + bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); + return bytes; +} + // Export window.MeshBayTransport = MeshBayTransport; |