@ruvector/edge-net 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/firebase-signaling.js +202 -52
- package/package.json +12 -3
- package/secure-access.js +595 -0
- package/webrtc.js +34 -10
package/firebase-signaling.js
CHANGED
|
@@ -4,9 +4,15 @@
|
|
|
4
4
|
* Uses Google Firebase as bootstrap infrastructure for WebRTC signaling
|
|
5
5
|
* with migration path to full P2P DHT network.
|
|
6
6
|
*
|
|
7
|
+
* Security Model (WASM-based, no Firebase Auth needed):
|
|
8
|
+
* 1. Each node generates cryptographic identity in WASM (PiKey)
|
|
9
|
+
* 2. All messages are signed with Ed25519 keys
|
|
10
|
+
* 3. Peers verify signatures before accepting connections
|
|
11
|
+
* 4. AdaptiveSecurity provides self-learning attack detection
|
|
12
|
+
*
|
|
7
13
|
* Architecture:
|
|
8
14
|
* 1. Firebase Firestore for signaling (offer/answer/ICE)
|
|
9
|
-
* 2.
|
|
15
|
+
* 2. WASM cryptographic identity (no Firebase Auth)
|
|
10
16
|
* 3. Gradual migration to DHT as network grows
|
|
11
17
|
*
|
|
12
18
|
* @module @ruvector/edge-net/firebase-signaling
|
|
@@ -19,23 +25,32 @@ import { EventEmitter } from 'events';
|
|
|
19
25
|
// ============================================
|
|
20
26
|
|
|
21
27
|
/**
|
|
22
|
-
*
|
|
28
|
+
* Edge-Net Public Firebase Configuration
|
|
23
29
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
30
|
+
* This is the PUBLIC Firebase project for edge-net P2P network.
|
|
31
|
+
* API keys for Firebase web apps are designed to be public - security is via:
|
|
32
|
+
* 1. Firestore Security Rules (only authenticated users can write)
|
|
33
|
+
* 2. Anonymous Authentication (anyone can join, tracked by UID)
|
|
34
|
+
* 3. API restrictions in Google Cloud Console
|
|
28
35
|
*
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
* Contributors automatically join the network - no setup required!
|
|
37
|
+
*/
|
|
38
|
+
export const EDGE_NET_FIREBASE_CONFIG = {
|
|
39
|
+
apiKey: "AIzaSyAZAJhathdnKZGzBQ8iDBFG8_OQsvb2QvA",
|
|
40
|
+
projectId: "ruv-dev",
|
|
41
|
+
authDomain: "ruv-dev.firebaseapp.com",
|
|
42
|
+
storageBucket: "ruv-dev.appspot.com",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get Firebase config
|
|
33
47
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
48
|
+
* Priority:
|
|
49
|
+
* 1. Environment variables (for custom Firebase projects)
|
|
50
|
+
* 2. Built-in edge-net public config (no setup required)
|
|
36
51
|
*/
|
|
37
52
|
export function getFirebaseConfig() {
|
|
38
|
-
//
|
|
53
|
+
// Allow override via environment variables for custom projects
|
|
39
54
|
const apiKey = process.env.FIREBASE_API_KEY;
|
|
40
55
|
const projectId = process.env.FIREBASE_PROJECT_ID;
|
|
41
56
|
|
|
@@ -44,13 +59,12 @@ export function getFirebaseConfig() {
|
|
|
44
59
|
apiKey,
|
|
45
60
|
projectId,
|
|
46
61
|
authDomain: process.env.FIREBASE_AUTH_DOMAIN || `${projectId}.firebaseapp.com`,
|
|
47
|
-
databaseURL: process.env.FIREBASE_DATABASE_URL || `https://${projectId}-default-rtdb.firebaseio.com`,
|
|
48
62
|
storageBucket: process.env.FIREBASE_STORAGE_BUCKET || `${projectId}.appspot.com`,
|
|
49
63
|
};
|
|
50
64
|
}
|
|
51
65
|
|
|
52
|
-
//
|
|
53
|
-
return
|
|
66
|
+
// Use built-in public edge-net config (no setup required!)
|
|
67
|
+
return EDGE_NET_FIREBASE_CONFIG;
|
|
54
68
|
}
|
|
55
69
|
|
|
56
70
|
/**
|
|
@@ -137,6 +151,11 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
137
151
|
this.db = null;
|
|
138
152
|
this.rtdb = null;
|
|
139
153
|
|
|
154
|
+
// WASM Security (replaces Firebase Auth)
|
|
155
|
+
/** @type {import('./secure-access.js').SecureAccessManager|null} */
|
|
156
|
+
this.secureAccess = options.secureAccess || null;
|
|
157
|
+
this.verifySignatures = options.verifySignatures !== false;
|
|
158
|
+
|
|
140
159
|
// State
|
|
141
160
|
this.isConnected = false;
|
|
142
161
|
this.peers = new Map();
|
|
@@ -150,27 +169,49 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
150
169
|
firebaseSignals: 0,
|
|
151
170
|
dhtSignals: 0,
|
|
152
171
|
p2pSignals: 0,
|
|
172
|
+
verifiedSignals: 0,
|
|
173
|
+
rejectedSignals: 0,
|
|
153
174
|
};
|
|
154
175
|
}
|
|
155
176
|
|
|
156
177
|
/**
|
|
157
|
-
* Initialize Firebase connection
|
|
178
|
+
* Initialize Firebase connection with WASM cryptographic security
|
|
158
179
|
*/
|
|
159
180
|
async connect() {
|
|
160
|
-
//
|
|
181
|
+
// Use built-in config if not provided
|
|
182
|
+
if (!this.config) {
|
|
183
|
+
this.config = getFirebaseConfig();
|
|
184
|
+
}
|
|
185
|
+
|
|
161
186
|
if (!this.config || !this.config.apiKey || !this.config.projectId) {
|
|
162
|
-
console.log(' ⚠️ Firebase not configured
|
|
163
|
-
console.log(' 💡 Set environment variables: FIREBASE_API_KEY, FIREBASE_PROJECT_ID');
|
|
187
|
+
console.log(' ⚠️ Firebase not configured');
|
|
164
188
|
this.emit('not-configured');
|
|
165
189
|
return false;
|
|
166
190
|
}
|
|
167
191
|
|
|
168
192
|
try {
|
|
193
|
+
// Initialize WASM security if not provided
|
|
194
|
+
if (!this.secureAccess) {
|
|
195
|
+
try {
|
|
196
|
+
const { createSecureAccess } = await import('./secure-access.js');
|
|
197
|
+
this.secureAccess = await createSecureAccess({
|
|
198
|
+
siteId: this.room,
|
|
199
|
+
persistIdentity: true
|
|
200
|
+
});
|
|
201
|
+
// Use WASM-generated node ID if peerId not set
|
|
202
|
+
if (!this.peerId) {
|
|
203
|
+
this.peerId = this.secureAccess.getShortId();
|
|
204
|
+
}
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.log(' ⚠️ WASM security unavailable, using basic mode');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
169
210
|
// Dynamic import Firebase (tree-shakeable)
|
|
170
211
|
const { initializeApp, getApps } = await import('firebase/app');
|
|
171
212
|
const { getFirestore, collection, doc, setDoc, onSnapshot, deleteDoc, query, where, orderBy, limit, serverTimestamp } = await import('firebase/firestore');
|
|
172
213
|
|
|
173
|
-
// Store Firebase methods for later use
|
|
214
|
+
// Store Firebase methods for later use
|
|
174
215
|
this.firebase = {
|
|
175
216
|
collection, doc, setDoc, onSnapshot, deleteDoc, query, where, orderBy, limit, serverTimestamp
|
|
176
217
|
};
|
|
@@ -179,9 +220,19 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
179
220
|
const apps = getApps();
|
|
180
221
|
this.app = apps.length ? apps[0] : initializeApp(this.config);
|
|
181
222
|
|
|
182
|
-
// Initialize Firestore
|
|
223
|
+
// Initialize Firestore
|
|
183
224
|
this.db = getFirestore(this.app);
|
|
184
225
|
|
|
226
|
+
// WASM cryptographic identity (replaces Firebase Auth)
|
|
227
|
+
if (this.secureAccess) {
|
|
228
|
+
this.uid = this.secureAccess.getNodeId();
|
|
229
|
+
console.log(` 🔐 WASM crypto identity: ${this.secureAccess.getShortId()}`);
|
|
230
|
+
console.log(` 📦 Public key: ${this.secureAccess.getPublicKeyHex().slice(0, 16)}...`);
|
|
231
|
+
} else {
|
|
232
|
+
this.uid = this.peerId;
|
|
233
|
+
console.log(` ⚠️ No WASM security, using peerId: ${this.peerId?.slice(0, 8)}...`);
|
|
234
|
+
}
|
|
235
|
+
|
|
185
236
|
// Register presence in Firestore
|
|
186
237
|
await this.registerPresence();
|
|
187
238
|
|
|
@@ -192,7 +243,7 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
192
243
|
this.subscribeToSignals();
|
|
193
244
|
|
|
194
245
|
this.isConnected = true;
|
|
195
|
-
console.log(' ✅ Firebase
|
|
246
|
+
console.log(' ✅ Firebase connected with WASM security');
|
|
196
247
|
|
|
197
248
|
this.emit('connected');
|
|
198
249
|
return true;
|
|
@@ -205,32 +256,54 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
205
256
|
}
|
|
206
257
|
|
|
207
258
|
/**
|
|
208
|
-
* Register this peer's presence in Firestore
|
|
259
|
+
* Register this peer's presence in Firestore with WASM-signed data
|
|
209
260
|
*/
|
|
210
261
|
async registerPresence() {
|
|
211
262
|
const { doc, setDoc, serverTimestamp } = this.firebase;
|
|
212
263
|
|
|
213
264
|
const presenceRef = doc(this.db, SIGNALING_PATHS.peers, this.peerId);
|
|
214
265
|
|
|
215
|
-
//
|
|
216
|
-
|
|
266
|
+
// Build presence data
|
|
267
|
+
const presenceData = {
|
|
217
268
|
peerId: this.peerId,
|
|
218
269
|
room: this.room,
|
|
219
270
|
online: true,
|
|
220
271
|
lastSeen: serverTimestamp(),
|
|
221
272
|
capabilities: ['compute', 'storage', 'verify'],
|
|
222
|
-
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// Add WASM cryptographic identity if available
|
|
276
|
+
if (this.secureAccess) {
|
|
277
|
+
presenceData.publicKey = this.secureAccess.getPublicKeyHex();
|
|
278
|
+
// Sign the presence announcement
|
|
279
|
+
const signed = this.secureAccess.signMessage({
|
|
280
|
+
peerId: this.peerId,
|
|
281
|
+
room: this.room,
|
|
282
|
+
capabilities: presenceData.capabilities
|
|
283
|
+
});
|
|
284
|
+
presenceData.signature = signed.signature;
|
|
285
|
+
presenceData.signedAt = signed.timestamp;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Set online status in Firestore
|
|
289
|
+
await setDoc(presenceRef, presenceData, { merge: true });
|
|
223
290
|
|
|
224
291
|
// Set up heartbeat to maintain presence (Firestore doesn't have onDisconnect)
|
|
225
292
|
this._heartbeatInterval = setInterval(async () => {
|
|
226
293
|
try {
|
|
227
|
-
|
|
294
|
+
const heartbeat = { lastSeen: serverTimestamp() };
|
|
295
|
+
// Sign heartbeat if security available
|
|
296
|
+
if (this.secureAccess) {
|
|
297
|
+
const signed = this.secureAccess.signMessage({ heartbeat: Date.now() });
|
|
298
|
+
heartbeat.heartbeatSig = signed.signature;
|
|
299
|
+
}
|
|
300
|
+
await setDoc(presenceRef, heartbeat, { merge: true });
|
|
228
301
|
} catch (e) {
|
|
229
302
|
// Ignore heartbeat errors
|
|
230
303
|
}
|
|
231
304
|
}, 30000);
|
|
232
305
|
|
|
233
|
-
console.log(` 📡 Registered presence: ${this.peerId.slice(0, 8)}
|
|
306
|
+
console.log(` 📡 Registered presence: ${this.peerId.slice(0, 8)}... (WASM-signed)`);
|
|
234
307
|
}
|
|
235
308
|
|
|
236
309
|
/**
|
|
@@ -303,7 +376,7 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
303
376
|
}
|
|
304
377
|
|
|
305
378
|
/**
|
|
306
|
-
* Handle incoming signal
|
|
379
|
+
* Handle incoming signal with WASM signature verification
|
|
307
380
|
*/
|
|
308
381
|
async handleSignal(signal, docId) {
|
|
309
382
|
this.stats.firebaseSignals++;
|
|
@@ -312,19 +385,46 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
312
385
|
const { doc, deleteDoc } = this.firebase;
|
|
313
386
|
await deleteDoc(doc(this.db, SIGNALING_PATHS.signals, docId));
|
|
314
387
|
|
|
388
|
+
// Verify signature if WASM security is enabled
|
|
389
|
+
if (this.verifySignatures && this.secureAccess && signal.signature && signal.publicKey) {
|
|
390
|
+
const isValid = this.secureAccess.verifyMessage({
|
|
391
|
+
payload: JSON.stringify({
|
|
392
|
+
from: signal.from,
|
|
393
|
+
to: signal.to,
|
|
394
|
+
type: signal.type,
|
|
395
|
+
data: typeof signal.data === 'object' ? JSON.stringify(signal.data) : signal.data,
|
|
396
|
+
timestamp: signal.timestamp
|
|
397
|
+
}),
|
|
398
|
+
signature: signal.signature,
|
|
399
|
+
publicKey: signal.publicKey,
|
|
400
|
+
timestamp: signal.timestamp
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
if (!isValid) {
|
|
404
|
+
console.warn(` ⚠️ Invalid signature from ${signal.from?.slice(0, 8)}...`);
|
|
405
|
+
this.stats.rejectedSignals++;
|
|
406
|
+
this.emit('invalid-signature', { from: signal.from, type: signal.type });
|
|
407
|
+
return; // Reject the signal
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Register verified peer
|
|
411
|
+
this.secureAccess.registerPeer(signal.from, signal.publicKey);
|
|
412
|
+
this.stats.verifiedSignals++;
|
|
413
|
+
}
|
|
414
|
+
|
|
315
415
|
// Emit appropriate event
|
|
316
416
|
switch (signal.type) {
|
|
317
417
|
case 'offer':
|
|
318
|
-
this.emit('offer', { from: signal.from, offer: signal.data });
|
|
418
|
+
this.emit('offer', { from: signal.from, offer: signal.data, verified: !!signal.signature });
|
|
319
419
|
break;
|
|
320
420
|
case 'answer':
|
|
321
|
-
this.emit('answer', { from: signal.from, answer: signal.data });
|
|
421
|
+
this.emit('answer', { from: signal.from, answer: signal.data, verified: !!signal.signature });
|
|
322
422
|
break;
|
|
323
423
|
case 'ice-candidate':
|
|
324
|
-
this.emit('ice-candidate', { from: signal.from, candidate: signal.data });
|
|
424
|
+
this.emit('ice-candidate', { from: signal.from, candidate: signal.data, verified: !!signal.signature });
|
|
325
425
|
break;
|
|
326
426
|
default:
|
|
327
|
-
this.emit('signal', signal);
|
|
427
|
+
this.emit('signal', { ...signal, verified: !!signal.signature });
|
|
328
428
|
}
|
|
329
429
|
}
|
|
330
430
|
|
|
@@ -350,7 +450,7 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
350
450
|
}
|
|
351
451
|
|
|
352
452
|
/**
|
|
353
|
-
* Send signal via Firebase
|
|
453
|
+
* Send signal via Firebase with WASM signature
|
|
354
454
|
*/
|
|
355
455
|
async sendSignal(toPeerId, type, data) {
|
|
356
456
|
if (!this.isConnected) {
|
|
@@ -362,14 +462,30 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
362
462
|
const signalId = `${this.peerId}-${toPeerId}-${Date.now()}`;
|
|
363
463
|
const signalRef = doc(this.db, SIGNALING_PATHS.signals, signalId);
|
|
364
464
|
|
|
365
|
-
|
|
465
|
+
const timestamp = Date.now();
|
|
466
|
+
const signalData = {
|
|
366
467
|
from: this.peerId,
|
|
367
468
|
to: toPeerId,
|
|
368
469
|
type,
|
|
369
470
|
data,
|
|
370
|
-
timestamp
|
|
471
|
+
timestamp,
|
|
371
472
|
room: this.room,
|
|
372
|
-
}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// Sign the signal with WASM cryptography
|
|
476
|
+
if (this.secureAccess) {
|
|
477
|
+
const signed = this.secureAccess.signMessage({
|
|
478
|
+
from: this.peerId,
|
|
479
|
+
to: toPeerId,
|
|
480
|
+
type,
|
|
481
|
+
data: typeof data === 'object' ? JSON.stringify(data) : data,
|
|
482
|
+
timestamp
|
|
483
|
+
});
|
|
484
|
+
signalData.signature = signed.signature;
|
|
485
|
+
signalData.publicKey = signed.publicKey;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
await setDoc(signalRef, signalData);
|
|
373
489
|
|
|
374
490
|
return true;
|
|
375
491
|
}
|
|
@@ -597,6 +713,10 @@ export class HybridBootstrap extends EventEmitter {
|
|
|
597
713
|
this.peerId = options.peerId;
|
|
598
714
|
this.config = options.firebaseConfig || DEFAULT_FIREBASE_CONFIG;
|
|
599
715
|
|
|
716
|
+
// WASM Security
|
|
717
|
+
/** @type {import('./secure-access.js').SecureAccessManager|null} */
|
|
718
|
+
this.secureAccess = options.secureAccess || null;
|
|
719
|
+
|
|
600
720
|
// Components
|
|
601
721
|
this.firebase = null;
|
|
602
722
|
this.dht = null;
|
|
@@ -614,25 +734,63 @@ export class HybridBootstrap extends EventEmitter {
|
|
|
614
734
|
directConnections: 0,
|
|
615
735
|
firebaseSignals: 0,
|
|
616
736
|
p2pSignals: 0,
|
|
737
|
+
verifiedPeers: 0,
|
|
617
738
|
};
|
|
618
739
|
}
|
|
619
740
|
|
|
620
741
|
/**
|
|
621
|
-
* Start hybrid bootstrap
|
|
742
|
+
* Start hybrid bootstrap with WASM security
|
|
622
743
|
*/
|
|
623
744
|
async start(webrtc, dht) {
|
|
624
745
|
this.webrtc = webrtc;
|
|
625
746
|
this.dht = dht;
|
|
626
747
|
|
|
627
|
-
//
|
|
748
|
+
// Initialize WASM security if not provided
|
|
749
|
+
if (!this.secureAccess) {
|
|
750
|
+
try {
|
|
751
|
+
const { createSecureAccess } = await import('./secure-access.js');
|
|
752
|
+
this.secureAccess = await createSecureAccess({
|
|
753
|
+
siteId: 'edge-net',
|
|
754
|
+
persistIdentity: true
|
|
755
|
+
});
|
|
756
|
+
// Use WASM node ID if peerId not set
|
|
757
|
+
if (!this.peerId) {
|
|
758
|
+
this.peerId = this.secureAccess.getShortId();
|
|
759
|
+
}
|
|
760
|
+
} catch (err) {
|
|
761
|
+
console.log(' ⚠️ WASM security unavailable for bootstrap');
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Start with Firebase, passing WASM security
|
|
628
766
|
this.firebase = new FirebaseSignaling({
|
|
629
767
|
peerId: this.peerId,
|
|
630
768
|
firebaseConfig: this.config,
|
|
769
|
+
secureAccess: this.secureAccess,
|
|
631
770
|
});
|
|
632
771
|
|
|
633
772
|
// Wire up events
|
|
634
773
|
this.setupFirebaseEvents();
|
|
635
774
|
|
|
775
|
+
// Set up WebRTC to use Firebase for signaling
|
|
776
|
+
if (this.webrtc) {
|
|
777
|
+
this.webrtc.setExternalSignaling(async (type, toPeerId, data) => {
|
|
778
|
+
// Route signaling through Firebase
|
|
779
|
+
switch (type) {
|
|
780
|
+
case 'offer':
|
|
781
|
+
await this.firebase.sendOffer(toPeerId, data);
|
|
782
|
+
break;
|
|
783
|
+
case 'answer':
|
|
784
|
+
await this.firebase.sendAnswer(toPeerId, data);
|
|
785
|
+
break;
|
|
786
|
+
case 'ice-candidate':
|
|
787
|
+
await this.firebase.sendIceCandidate(toPeerId, data);
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
this.stats.firebaseSignals++;
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
636
794
|
// Connect to Firebase
|
|
637
795
|
const connected = await this.firebase.connect();
|
|
638
796
|
|
|
@@ -693,18 +851,10 @@ export class HybridBootstrap extends EventEmitter {
|
|
|
693
851
|
if (!this.webrtc) return;
|
|
694
852
|
|
|
695
853
|
try {
|
|
696
|
-
//
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
if (this.mode === 'p2p' && this.webrtc.isConnected(peerId)) {
|
|
701
|
-
this.webrtc.sendToPeer(peerId, { type: 'offer', offer });
|
|
702
|
-
this.stats.p2pSignals++;
|
|
703
|
-
} else {
|
|
704
|
-
// Fall back to Firebase
|
|
705
|
-
await this.firebase.sendOffer(peerId, offer);
|
|
706
|
-
this.stats.firebaseSignals++;
|
|
707
|
-
}
|
|
854
|
+
// Use WebRTCPeerManager's connectToPeer method
|
|
855
|
+
// This handles offer creation and signaling internally
|
|
856
|
+
await this.webrtc.connectToPeer(peerId);
|
|
857
|
+
this.stats.directConnections++;
|
|
708
858
|
|
|
709
859
|
} catch (error) {
|
|
710
860
|
console.warn(`[HybridBootstrap] Connect to ${peerId.slice(0, 8)} failed:`, error.message);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ruvector/edge-net",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Distributed compute intelligence network with
|
|
5
|
+
"description": "Distributed compute intelligence network with WASM cryptographic security - contribute browser compute, spawn distributed AI agents, earn credits. Features Ed25519 signing, PiKey identity, Time Crystal coordination, Neural DAG attention, P2P swarm intelligence, ONNX inference, WebRTC signaling, CRDT ledger, and multi-agent workflows.",
|
|
6
6
|
"main": "ruvector_edge_net.js",
|
|
7
7
|
"module": "ruvector_edge_net.js",
|
|
8
8
|
"types": "ruvector_edge_net.d.ts",
|
|
@@ -55,7 +55,11 @@
|
|
|
55
55
|
"firebase",
|
|
56
56
|
"gcloud",
|
|
57
57
|
"firestore",
|
|
58
|
-
"realtime-database"
|
|
58
|
+
"realtime-database",
|
|
59
|
+
"ed25519",
|
|
60
|
+
"wasm-cryptography",
|
|
61
|
+
"pikey",
|
|
62
|
+
"secure-access"
|
|
59
63
|
],
|
|
60
64
|
"author": "RuVector Team <team@ruvector.dev>",
|
|
61
65
|
"license": "MIT",
|
|
@@ -96,6 +100,7 @@
|
|
|
96
100
|
"p2p.js",
|
|
97
101
|
"firebase-signaling.js",
|
|
98
102
|
"firebase-setup.js",
|
|
103
|
+
"secure-access.js",
|
|
99
104
|
"README.md",
|
|
100
105
|
"LICENSE"
|
|
101
106
|
],
|
|
@@ -160,6 +165,9 @@
|
|
|
160
165
|
},
|
|
161
166
|
"./firebase-setup": {
|
|
162
167
|
"import": "./firebase-setup.js"
|
|
168
|
+
},
|
|
169
|
+
"./secure-access": {
|
|
170
|
+
"import": "./secure-access.js"
|
|
163
171
|
}
|
|
164
172
|
},
|
|
165
173
|
"sideEffects": [
|
|
@@ -190,6 +198,7 @@
|
|
|
190
198
|
"@ruvector/ruvllm": "^0.2.3",
|
|
191
199
|
"@xenova/transformers": "^2.17.2",
|
|
192
200
|
"firebase": "^10.14.1",
|
|
201
|
+
"wrtc": "^0.4.7",
|
|
193
202
|
"ws": "^8.18.3"
|
|
194
203
|
}
|
|
195
204
|
}
|
package/secure-access.js
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ruvector/edge-net Secure Access Layer
|
|
3
|
+
*
|
|
4
|
+
* Uses WASM cryptographic primitives for secure network access.
|
|
5
|
+
* No external authentication needed - cryptographic proof of identity.
|
|
6
|
+
*
|
|
7
|
+
* Security Model:
|
|
8
|
+
* 1. Each node generates a PiKey (Ed25519-based) in WASM
|
|
9
|
+
* 2. All messages are signed with the node's private key
|
|
10
|
+
* 3. Other nodes verify signatures with public keys
|
|
11
|
+
* 4. AdaptiveSecurity provides self-learning attack detection
|
|
12
|
+
*
|
|
13
|
+
* @module @ruvector/edge-net/secure-access
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { EventEmitter } from 'events';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Secure Access Manager
|
|
20
|
+
*
|
|
21
|
+
* Provides WASM-based cryptographic identity and message signing
|
|
22
|
+
* for secure P2P network access without external auth providers.
|
|
23
|
+
*/
|
|
24
|
+
export class SecureAccessManager extends EventEmitter {
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
super();
|
|
27
|
+
|
|
28
|
+
/** @type {import('./ruvector_edge_net').PiKey|null} */
|
|
29
|
+
this.piKey = null;
|
|
30
|
+
|
|
31
|
+
/** @type {import('./ruvector_edge_net').SessionKey|null} */
|
|
32
|
+
this.sessionKey = null;
|
|
33
|
+
|
|
34
|
+
/** @type {import('./ruvector_edge_net').WasmNodeIdentity|null} */
|
|
35
|
+
this.nodeIdentity = null;
|
|
36
|
+
|
|
37
|
+
/** @type {import('./ruvector_edge_net').AdaptiveSecurity|null} */
|
|
38
|
+
this.security = null;
|
|
39
|
+
|
|
40
|
+
/** @type {Map<string, Uint8Array>} Known peer public keys */
|
|
41
|
+
this.knownPeers = new Map();
|
|
42
|
+
|
|
43
|
+
/** @type {Map<string, number>} Peer reputation scores */
|
|
44
|
+
this.peerReputation = new Map();
|
|
45
|
+
|
|
46
|
+
this.options = {
|
|
47
|
+
siteId: options.siteId || 'edge-net',
|
|
48
|
+
sessionTTL: options.sessionTTL || 3600, // 1 hour
|
|
49
|
+
backupPassword: options.backupPassword || null,
|
|
50
|
+
persistIdentity: options.persistIdentity !== false,
|
|
51
|
+
...options
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
this.wasm = null;
|
|
55
|
+
this.initialized = false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Initialize secure access with WASM cryptography
|
|
60
|
+
*/
|
|
61
|
+
async initialize() {
|
|
62
|
+
if (this.initialized) return this;
|
|
63
|
+
|
|
64
|
+
console.log('🔐 Initializing WASM Secure Access...');
|
|
65
|
+
|
|
66
|
+
// Load WASM module
|
|
67
|
+
try {
|
|
68
|
+
// For Node.js, use the node-specific CJS module which auto-loads WASM
|
|
69
|
+
const isNode = typeof process !== 'undefined' && process.versions?.node;
|
|
70
|
+
if (isNode) {
|
|
71
|
+
// Node.js: CJS module loads WASM synchronously on import
|
|
72
|
+
this.wasm = await import('./node/ruvector_edge_net.cjs');
|
|
73
|
+
} else {
|
|
74
|
+
// Browser: Use ES module with WASM init
|
|
75
|
+
const wasmModule = await import('./ruvector_edge_net.js');
|
|
76
|
+
// Call default init to load WASM binary
|
|
77
|
+
if (wasmModule.default && typeof wasmModule.default === 'function') {
|
|
78
|
+
await wasmModule.default();
|
|
79
|
+
}
|
|
80
|
+
this.wasm = wasmModule;
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error(' ❌ WASM load error:', err.message);
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Try to restore existing identity
|
|
88
|
+
const restored = await this._tryRestoreIdentity();
|
|
89
|
+
|
|
90
|
+
if (!restored) {
|
|
91
|
+
// Generate new cryptographic identity
|
|
92
|
+
await this._generateIdentity();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Initialize adaptive security
|
|
96
|
+
this.security = new this.wasm.AdaptiveSecurity();
|
|
97
|
+
|
|
98
|
+
// Create session key for encrypted communications
|
|
99
|
+
this.sessionKey = new this.wasm.SessionKey(this.piKey, this.options.sessionTTL);
|
|
100
|
+
|
|
101
|
+
this.initialized = true;
|
|
102
|
+
|
|
103
|
+
console.log(` 🔑 Node ID: ${this.getShortId()}`);
|
|
104
|
+
console.log(` 📦 Public Key: ${this.getPublicKeyHex().slice(0, 16)}...`);
|
|
105
|
+
console.log(` ⏱️ Session expires: ${new Date(Date.now() + this.options.sessionTTL * 1000).toISOString()}`);
|
|
106
|
+
|
|
107
|
+
this.emit('initialized', {
|
|
108
|
+
nodeId: this.getNodeId(),
|
|
109
|
+
publicKey: this.getPublicKeyHex()
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Try to restore identity from localStorage or backup
|
|
117
|
+
*/
|
|
118
|
+
async _tryRestoreIdentity() {
|
|
119
|
+
if (!this.options.persistIdentity) return false;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
// Check localStorage (browser) or file (Node.js)
|
|
123
|
+
let stored = null;
|
|
124
|
+
|
|
125
|
+
if (typeof localStorage !== 'undefined') {
|
|
126
|
+
stored = localStorage.getItem('edge-net-identity');
|
|
127
|
+
} else if (typeof process !== 'undefined') {
|
|
128
|
+
const fs = await import('fs');
|
|
129
|
+
const path = await import('path');
|
|
130
|
+
const identityPath = path.join(process.cwd(), '.edge-net-identity');
|
|
131
|
+
if (fs.existsSync(identityPath)) {
|
|
132
|
+
stored = fs.readFileSync(identityPath, 'utf8');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (stored) {
|
|
137
|
+
const data = JSON.parse(stored);
|
|
138
|
+
const encrypted = new Uint8Array(data.encrypted);
|
|
139
|
+
|
|
140
|
+
// Use default password if none provided
|
|
141
|
+
const password = this.options.backupPassword || 'edge-net-default-key';
|
|
142
|
+
|
|
143
|
+
this.piKey = this.wasm.PiKey.restoreFromBackup(encrypted, password);
|
|
144
|
+
this.nodeIdentity = this.wasm.WasmNodeIdentity.fromSecretKey(
|
|
145
|
+
encrypted, // Same key derivation
|
|
146
|
+
this.options.siteId
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
console.log(' ♻️ Restored existing identity');
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.log(' ⚡ Creating new identity (no backup found)');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Generate new cryptographic identity
|
|
161
|
+
*/
|
|
162
|
+
async _generateIdentity() {
|
|
163
|
+
// Generate Pi-Key (Ed25519-based with Pi magic)
|
|
164
|
+
// Constructor takes optional genesis_seed (Uint8Array or null)
|
|
165
|
+
const genesisSeed = this.options.genesisSeed || null;
|
|
166
|
+
this.piKey = new this.wasm.PiKey(genesisSeed);
|
|
167
|
+
|
|
168
|
+
// Create node identity from same site
|
|
169
|
+
this.nodeIdentity = new this.wasm.WasmNodeIdentity(this.options.siteId);
|
|
170
|
+
|
|
171
|
+
// Persist identity if enabled
|
|
172
|
+
if (this.options.persistIdentity) {
|
|
173
|
+
await this._persistIdentity();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(' ✨ Generated new cryptographic identity');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Persist identity to storage
|
|
181
|
+
*/
|
|
182
|
+
async _persistIdentity() {
|
|
183
|
+
const password = this.options.backupPassword || 'edge-net-default-key';
|
|
184
|
+
const backup = this.piKey.createEncryptedBackup(password);
|
|
185
|
+
const data = JSON.stringify({
|
|
186
|
+
encrypted: Array.from(backup),
|
|
187
|
+
created: Date.now(),
|
|
188
|
+
siteId: this.options.siteId
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
if (typeof localStorage !== 'undefined') {
|
|
193
|
+
localStorage.setItem('edge-net-identity', data);
|
|
194
|
+
} else if (typeof process !== 'undefined') {
|
|
195
|
+
const fs = await import('fs');
|
|
196
|
+
const path = await import('path');
|
|
197
|
+
const identityPath = path.join(process.cwd(), '.edge-net-identity');
|
|
198
|
+
fs.writeFileSync(identityPath, data);
|
|
199
|
+
}
|
|
200
|
+
} catch (err) {
|
|
201
|
+
console.warn(' ⚠️ Could not persist identity:', err.message);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ============================================
|
|
206
|
+
// IDENTITY & KEYS
|
|
207
|
+
// ============================================
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get node ID (full)
|
|
211
|
+
*/
|
|
212
|
+
getNodeId() {
|
|
213
|
+
return this.piKey?.getIdentityHex() || this.nodeIdentity?.getId?.() || 'unknown';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get short node ID for display
|
|
218
|
+
*/
|
|
219
|
+
getShortId() {
|
|
220
|
+
return this.piKey?.getShortId() || this.getNodeId().slice(0, 8);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get public key as hex string
|
|
225
|
+
*/
|
|
226
|
+
getPublicKeyHex() {
|
|
227
|
+
return Array.from(this.piKey?.getPublicKey() || new Uint8Array(32))
|
|
228
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
229
|
+
.join('');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get public key as bytes
|
|
234
|
+
*/
|
|
235
|
+
getPublicKeyBytes() {
|
|
236
|
+
return this.piKey?.getPublicKey() || new Uint8Array(32);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ============================================
|
|
240
|
+
// MESSAGE SIGNING & VERIFICATION
|
|
241
|
+
// ============================================
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Sign a message/object
|
|
245
|
+
* @param {object|string|Uint8Array} message - Message to sign
|
|
246
|
+
* @returns {{ payload: string, signature: string, publicKey: string, timestamp: number }}
|
|
247
|
+
*/
|
|
248
|
+
signMessage(message) {
|
|
249
|
+
const payload = typeof message === 'string' ? message :
|
|
250
|
+
message instanceof Uint8Array ? new TextDecoder().decode(message) :
|
|
251
|
+
JSON.stringify(message);
|
|
252
|
+
|
|
253
|
+
const timestamp = Date.now();
|
|
254
|
+
const dataToSign = `${payload}|${timestamp}`;
|
|
255
|
+
const dataBytes = new TextEncoder().encode(dataToSign);
|
|
256
|
+
|
|
257
|
+
const signature = this.piKey.sign(dataBytes);
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
payload,
|
|
261
|
+
signature: Array.from(signature).map(b => b.toString(16).padStart(2, '0')).join(''),
|
|
262
|
+
publicKey: this.getPublicKeyHex(),
|
|
263
|
+
timestamp,
|
|
264
|
+
nodeId: this.getShortId()
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Verify a signed message
|
|
270
|
+
* @param {object} signed - Signed message object
|
|
271
|
+
* @returns {boolean} Whether signature is valid
|
|
272
|
+
*/
|
|
273
|
+
verifyMessage(signed) {
|
|
274
|
+
try {
|
|
275
|
+
const { payload, signature, publicKey, timestamp } = signed;
|
|
276
|
+
|
|
277
|
+
// Check timestamp (reject messages older than 5 minutes)
|
|
278
|
+
const age = Date.now() - timestamp;
|
|
279
|
+
if (age > 5 * 60 * 1000) {
|
|
280
|
+
console.warn('⚠️ Message too old:', age, 'ms');
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Convert hex strings back to bytes
|
|
285
|
+
const dataToVerify = `${payload}|${timestamp}`;
|
|
286
|
+
const dataBytes = new TextEncoder().encode(dataToVerify);
|
|
287
|
+
const sigBytes = new Uint8Array(signature.match(/.{2}/g).map(h => parseInt(h, 16)));
|
|
288
|
+
const pubKeyBytes = new Uint8Array(publicKey.match(/.{2}/g).map(h => parseInt(h, 16)));
|
|
289
|
+
|
|
290
|
+
// Verify using WASM
|
|
291
|
+
const valid = this.piKey.verify(dataBytes, sigBytes, pubKeyBytes);
|
|
292
|
+
|
|
293
|
+
// Update peer reputation based on verification
|
|
294
|
+
if (valid) {
|
|
295
|
+
this._updateReputation(signed.nodeId || publicKey.slice(0, 16), 0.01);
|
|
296
|
+
} else {
|
|
297
|
+
this._updateReputation(signed.nodeId || publicKey.slice(0, 16), -0.1);
|
|
298
|
+
this._recordSuspicious(signed.nodeId, 'invalid_signature');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return valid;
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.warn('⚠️ Signature verification error:', err.message);
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ============================================
|
|
309
|
+
// PEER MANAGEMENT
|
|
310
|
+
// ============================================
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Register a known peer's public key
|
|
314
|
+
*/
|
|
315
|
+
registerPeer(peerId, publicKey) {
|
|
316
|
+
const pubKeyBytes = typeof publicKey === 'string' ?
|
|
317
|
+
new Uint8Array(publicKey.match(/.{2}/g).map(h => parseInt(h, 16))) :
|
|
318
|
+
publicKey;
|
|
319
|
+
|
|
320
|
+
this.knownPeers.set(peerId, pubKeyBytes);
|
|
321
|
+
this.peerReputation.set(peerId, this.peerReputation.get(peerId) || 0.5);
|
|
322
|
+
|
|
323
|
+
this.emit('peer-registered', { peerId, publicKey: this.getPublicKeyHex() });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get reputation score for a peer (0-1)
|
|
328
|
+
*/
|
|
329
|
+
getPeerReputation(peerId) {
|
|
330
|
+
return this.peerReputation.get(peerId) || 0.5;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Update peer reputation
|
|
335
|
+
*/
|
|
336
|
+
_updateReputation(peerId, delta) {
|
|
337
|
+
const current = this.peerReputation.get(peerId) || 0.5;
|
|
338
|
+
const newScore = Math.max(0, Math.min(1, current + delta));
|
|
339
|
+
this.peerReputation.set(peerId, newScore);
|
|
340
|
+
|
|
341
|
+
// Emit warning if reputation drops too low
|
|
342
|
+
if (newScore < 0.2) {
|
|
343
|
+
this.emit('peer-suspicious', { peerId, reputation: newScore });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Record suspicious activity for learning
|
|
349
|
+
*/
|
|
350
|
+
_recordSuspicious(peerId, reason) {
|
|
351
|
+
if (this.security) {
|
|
352
|
+
// Record for adaptive security learning
|
|
353
|
+
const features = new Float32Array([
|
|
354
|
+
Date.now() / 1e12,
|
|
355
|
+
this.getPeerReputation(peerId),
|
|
356
|
+
reason === 'invalid_signature' ? 1 : 0,
|
|
357
|
+
reason === 'replay_attack' ? 1 : 0,
|
|
358
|
+
0, 0, 0, 0 // Padding
|
|
359
|
+
]);
|
|
360
|
+
this.security.recordAttackPattern(reason, features, 0.5);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ============================================
|
|
365
|
+
// ENCRYPTION (SESSION-BASED)
|
|
366
|
+
// ============================================
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Encrypt data for secure transmission
|
|
370
|
+
*/
|
|
371
|
+
encrypt(data) {
|
|
372
|
+
if (!this.sessionKey || this.sessionKey.isExpired()) {
|
|
373
|
+
// Refresh session key
|
|
374
|
+
this.sessionKey = new this.wasm.SessionKey(this.piKey, this.options.sessionTTL);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const dataBytes = typeof data === 'string' ?
|
|
378
|
+
new TextEncoder().encode(data) :
|
|
379
|
+
data instanceof Uint8Array ? data :
|
|
380
|
+
new TextEncoder().encode(JSON.stringify(data));
|
|
381
|
+
|
|
382
|
+
return this.sessionKey.encrypt(dataBytes);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Decrypt received data
|
|
387
|
+
*/
|
|
388
|
+
decrypt(encrypted) {
|
|
389
|
+
if (!this.sessionKey) {
|
|
390
|
+
throw new Error('No session key available');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return this.sessionKey.decrypt(encrypted);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ============================================
|
|
397
|
+
// SECURITY ANALYSIS
|
|
398
|
+
// ============================================
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Analyze request for potential attacks
|
|
402
|
+
* @returns {number} Threat score (0-1, higher = more suspicious)
|
|
403
|
+
*/
|
|
404
|
+
analyzeRequest(features) {
|
|
405
|
+
if (!this.security) return 0;
|
|
406
|
+
|
|
407
|
+
const featureArray = features instanceof Float32Array ?
|
|
408
|
+
features :
|
|
409
|
+
new Float32Array(Array.isArray(features) ? features : Object.values(features));
|
|
410
|
+
|
|
411
|
+
return this.security.detectAttack(featureArray);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Get security statistics
|
|
416
|
+
*/
|
|
417
|
+
getSecurityStats() {
|
|
418
|
+
if (!this.security) return null;
|
|
419
|
+
|
|
420
|
+
return JSON.parse(this.security.getStats());
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Export security patterns for persistence
|
|
425
|
+
*/
|
|
426
|
+
exportSecurityPatterns() {
|
|
427
|
+
if (!this.security) return null;
|
|
428
|
+
return this.security.exportPatterns();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Import previously learned security patterns
|
|
433
|
+
*/
|
|
434
|
+
importSecurityPatterns(patterns) {
|
|
435
|
+
if (!this.security) return;
|
|
436
|
+
this.security.importPatterns(patterns);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ============================================
|
|
440
|
+
// CHALLENGE-RESPONSE
|
|
441
|
+
// ============================================
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Create a challenge for peer verification
|
|
445
|
+
*/
|
|
446
|
+
createChallenge() {
|
|
447
|
+
const challenge = crypto.getRandomValues(new Uint8Array(32));
|
|
448
|
+
const timestamp = Date.now();
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
challenge: Array.from(challenge).map(b => b.toString(16).padStart(2, '0')).join(''),
|
|
452
|
+
timestamp,
|
|
453
|
+
issuer: this.getShortId()
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Respond to a challenge (proves identity)
|
|
459
|
+
*/
|
|
460
|
+
respondToChallenge(challengeData) {
|
|
461
|
+
const challengeBytes = new Uint8Array(
|
|
462
|
+
challengeData.challenge.match(/.{2}/g).map(h => parseInt(h, 16))
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
const responseData = new Uint8Array([
|
|
466
|
+
...challengeBytes,
|
|
467
|
+
...new TextEncoder().encode(`|${challengeData.timestamp}|${this.getShortId()}`)
|
|
468
|
+
]);
|
|
469
|
+
|
|
470
|
+
const signature = this.piKey.sign(responseData);
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
...challengeData,
|
|
474
|
+
response: Array.from(signature).map(b => b.toString(16).padStart(2, '0')).join(''),
|
|
475
|
+
responder: this.getShortId(),
|
|
476
|
+
publicKey: this.getPublicKeyHex()
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Verify a challenge response
|
|
482
|
+
*/
|
|
483
|
+
verifyChallengeResponse(response) {
|
|
484
|
+
try {
|
|
485
|
+
const challengeBytes = new Uint8Array(
|
|
486
|
+
response.challenge.match(/.{2}/g).map(h => parseInt(h, 16))
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
const responseData = new Uint8Array([
|
|
490
|
+
...challengeBytes,
|
|
491
|
+
...new TextEncoder().encode(`|${response.timestamp}|${response.responder}`)
|
|
492
|
+
]);
|
|
493
|
+
|
|
494
|
+
const sigBytes = new Uint8Array(
|
|
495
|
+
response.response.match(/.{2}/g).map(h => parseInt(h, 16))
|
|
496
|
+
);
|
|
497
|
+
const pubKeyBytes = new Uint8Array(
|
|
498
|
+
response.publicKey.match(/.{2}/g).map(h => parseInt(h, 16))
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
const valid = this.piKey.verify(responseData, sigBytes, pubKeyBytes);
|
|
502
|
+
|
|
503
|
+
if (valid) {
|
|
504
|
+
// Register this peer as verified
|
|
505
|
+
this.registerPeer(response.responder, response.publicKey);
|
|
506
|
+
this._updateReputation(response.responder, 0.05);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return valid;
|
|
510
|
+
} catch (err) {
|
|
511
|
+
console.warn('Challenge verification failed:', err.message);
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Clean up resources
|
|
518
|
+
*/
|
|
519
|
+
dispose() {
|
|
520
|
+
try { this.piKey?.free?.(); } catch (e) { /* already freed */ }
|
|
521
|
+
try { this.sessionKey?.free?.(); } catch (e) { /* already freed */ }
|
|
522
|
+
try { this.nodeIdentity?.free?.(); } catch (e) { /* already freed */ }
|
|
523
|
+
try { this.security?.free?.(); } catch (e) { /* already freed */ }
|
|
524
|
+
this.piKey = null;
|
|
525
|
+
this.sessionKey = null;
|
|
526
|
+
this.nodeIdentity = null;
|
|
527
|
+
this.security = null;
|
|
528
|
+
this.knownPeers.clear();
|
|
529
|
+
this.peerReputation.clear();
|
|
530
|
+
this.initialized = false;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Create a secure access manager
|
|
536
|
+
*/
|
|
537
|
+
export async function createSecureAccess(options = {}) {
|
|
538
|
+
const manager = new SecureAccessManager(options);
|
|
539
|
+
await manager.initialize();
|
|
540
|
+
return manager;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Wrap Firebase signaling with WASM security
|
|
545
|
+
*/
|
|
546
|
+
export function wrapWithSecurity(firebaseSignaling, secureAccess) {
|
|
547
|
+
const originalAnnounce = firebaseSignaling.announcePeer?.bind(firebaseSignaling);
|
|
548
|
+
const originalSendOffer = firebaseSignaling.sendOffer?.bind(firebaseSignaling);
|
|
549
|
+
const originalSendAnswer = firebaseSignaling.sendAnswer?.bind(firebaseSignaling);
|
|
550
|
+
const originalSendIceCandidate = firebaseSignaling.sendIceCandidate?.bind(firebaseSignaling);
|
|
551
|
+
|
|
552
|
+
// Wrap peer announcement with signature
|
|
553
|
+
if (originalAnnounce) {
|
|
554
|
+
firebaseSignaling.announcePeer = async (peerId, metadata = {}) => {
|
|
555
|
+
const signedMetadata = secureAccess.signMessage({
|
|
556
|
+
...metadata,
|
|
557
|
+
publicKey: secureAccess.getPublicKeyHex()
|
|
558
|
+
});
|
|
559
|
+
return originalAnnounce(peerId, signedMetadata);
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Wrap signaling messages with signatures
|
|
564
|
+
if (originalSendOffer) {
|
|
565
|
+
firebaseSignaling.sendOffer = async (toPeerId, offer) => {
|
|
566
|
+
const signed = secureAccess.signMessage({ type: 'offer', offer });
|
|
567
|
+
return originalSendOffer(toPeerId, signed);
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (originalSendAnswer) {
|
|
572
|
+
firebaseSignaling.sendAnswer = async (toPeerId, answer) => {
|
|
573
|
+
const signed = secureAccess.signMessage({ type: 'answer', answer });
|
|
574
|
+
return originalSendAnswer(toPeerId, signed);
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (originalSendIceCandidate) {
|
|
579
|
+
firebaseSignaling.sendIceCandidate = async (toPeerId, candidate) => {
|
|
580
|
+
const signed = secureAccess.signMessage({ type: 'ice', candidate });
|
|
581
|
+
return originalSendIceCandidate(toPeerId, signed);
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Add verification method
|
|
586
|
+
firebaseSignaling.verifySignedMessage = (signed) => {
|
|
587
|
+
return secureAccess.verifyMessage(signed);
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
firebaseSignaling.secureAccess = secureAccess;
|
|
591
|
+
|
|
592
|
+
return firebaseSignaling;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export default SecureAccessManager;
|
package/webrtc.js
CHANGED
|
@@ -408,6 +408,15 @@ export class WebRTCPeerManager extends EventEmitter {
|
|
|
408
408
|
failedConnections: 0,
|
|
409
409
|
messagesRouted: 0,
|
|
410
410
|
};
|
|
411
|
+
// External signaling callback (e.g., Firebase)
|
|
412
|
+
this.externalSignaling = null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Set external signaling callback for Firebase or other signaling
|
|
417
|
+
*/
|
|
418
|
+
setExternalSignaling(callback) {
|
|
419
|
+
this.externalSignaling = callback;
|
|
411
420
|
}
|
|
412
421
|
|
|
413
422
|
/**
|
|
@@ -639,13 +648,23 @@ export class WebRTCPeerManager extends EventEmitter {
|
|
|
639
648
|
|
|
640
649
|
const offer = await peerConnection.createOffer();
|
|
641
650
|
|
|
642
|
-
// Send offer via signaling
|
|
643
|
-
|
|
651
|
+
// Send offer via available signaling method
|
|
652
|
+
const signalData = {
|
|
644
653
|
type: 'offer',
|
|
645
654
|
to: peerId,
|
|
646
655
|
from: this.localIdentity.piKey,
|
|
647
656
|
offer,
|
|
648
|
-
}
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
if (this.signalingSocket?.readyState === 1) {
|
|
660
|
+
// WebSocket signaling
|
|
661
|
+
this.signalingSocket.send(JSON.stringify(signalData));
|
|
662
|
+
} else if (this.externalSignaling) {
|
|
663
|
+
// External signaling (Firebase, etc.)
|
|
664
|
+
await this.externalSignaling('offer', peerId, offer);
|
|
665
|
+
} else {
|
|
666
|
+
throw new Error('No signaling method available');
|
|
667
|
+
}
|
|
649
668
|
|
|
650
669
|
this.peers.set(peerId, peerConnection);
|
|
651
670
|
this.emit('peers-updated', this.getPeerList());
|
|
@@ -653,6 +672,7 @@ export class WebRTCPeerManager extends EventEmitter {
|
|
|
653
672
|
} catch (err) {
|
|
654
673
|
this.stats.failedConnections++;
|
|
655
674
|
console.error(`Failed to connect to ${peerId}:`, err.message);
|
|
675
|
+
throw err;
|
|
656
676
|
}
|
|
657
677
|
}
|
|
658
678
|
|
|
@@ -678,13 +698,17 @@ export class WebRTCPeerManager extends EventEmitter {
|
|
|
678
698
|
|
|
679
699
|
const answer = await peerConnection.handleOffer(offer);
|
|
680
700
|
|
|
681
|
-
// Send answer via signaling
|
|
682
|
-
this.signalingSocket
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
701
|
+
// Send answer via available signaling method
|
|
702
|
+
if (this.signalingSocket?.readyState === 1) {
|
|
703
|
+
this.signalingSocket.send(JSON.stringify({
|
|
704
|
+
type: 'answer',
|
|
705
|
+
to: from,
|
|
706
|
+
from: this.localIdentity.piKey,
|
|
707
|
+
answer,
|
|
708
|
+
}));
|
|
709
|
+
} else if (this.externalSignaling) {
|
|
710
|
+
await this.externalSignaling('answer', from, answer);
|
|
711
|
+
}
|
|
688
712
|
|
|
689
713
|
this.peers.set(from, peerConnection);
|
|
690
714
|
this.emit('peers-updated', this.getPeerList());
|