aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/transport.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/transport.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js178
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;