@ruvector/edge-net 0.4.0 → 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 +269 -91
- package/p2p.js +1 -1
- package/package.json +13 -11
- 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
|
/**
|
|
@@ -95,13 +109,14 @@ export async function getFirebaseConfigAsync() {
|
|
|
95
109
|
export const DEFAULT_FIREBASE_CONFIG = null;
|
|
96
110
|
|
|
97
111
|
/**
|
|
98
|
-
* Signaling
|
|
112
|
+
* Signaling collection names in Firestore
|
|
113
|
+
* Each is a top-level collection (Firestore requires collection/document pairs)
|
|
99
114
|
*/
|
|
100
115
|
export const SIGNALING_PATHS = {
|
|
101
|
-
peers: '
|
|
102
|
-
signals: '
|
|
103
|
-
rooms: '
|
|
104
|
-
ledger: '
|
|
116
|
+
peers: 'edgenet_peers',
|
|
117
|
+
signals: 'edgenet_signals',
|
|
118
|
+
rooms: 'edgenet_rooms',
|
|
119
|
+
ledger: 'edgenet_ledger',
|
|
105
120
|
};
|
|
106
121
|
|
|
107
122
|
// ============================================
|
|
@@ -136,6 +151,11 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
136
151
|
this.db = null;
|
|
137
152
|
this.rtdb = null;
|
|
138
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
|
+
|
|
139
159
|
// State
|
|
140
160
|
this.isConnected = false;
|
|
141
161
|
this.peers = new Map();
|
|
@@ -149,44 +169,71 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
149
169
|
firebaseSignals: 0,
|
|
150
170
|
dhtSignals: 0,
|
|
151
171
|
p2pSignals: 0,
|
|
172
|
+
verifiedSignals: 0,
|
|
173
|
+
rejectedSignals: 0,
|
|
152
174
|
};
|
|
153
175
|
}
|
|
154
176
|
|
|
155
177
|
/**
|
|
156
|
-
* Initialize Firebase connection
|
|
178
|
+
* Initialize Firebase connection with WASM cryptographic security
|
|
157
179
|
*/
|
|
158
180
|
async connect() {
|
|
159
|
-
//
|
|
181
|
+
// Use built-in config if not provided
|
|
182
|
+
if (!this.config) {
|
|
183
|
+
this.config = getFirebaseConfig();
|
|
184
|
+
}
|
|
185
|
+
|
|
160
186
|
if (!this.config || !this.config.apiKey || !this.config.projectId) {
|
|
161
|
-
console.log(' ⚠️ Firebase not configured
|
|
162
|
-
console.log(' 💡 Set environment variables: FIREBASE_API_KEY, FIREBASE_PROJECT_ID');
|
|
187
|
+
console.log(' ⚠️ Firebase not configured');
|
|
163
188
|
this.emit('not-configured');
|
|
164
189
|
return false;
|
|
165
190
|
}
|
|
166
191
|
|
|
167
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
|
+
|
|
168
210
|
// Dynamic import Firebase (tree-shakeable)
|
|
169
211
|
const { initializeApp, getApps } = await import('firebase/app');
|
|
170
|
-
const { getFirestore, collection, doc, setDoc, onSnapshot, deleteDoc, query, where, orderBy, limit } = await import('firebase/firestore');
|
|
171
|
-
const { getDatabase, ref, set, onValue, onDisconnect, serverTimestamp } = await import('firebase/database');
|
|
212
|
+
const { getFirestore, collection, doc, setDoc, onSnapshot, deleteDoc, query, where, orderBy, limit, serverTimestamp } = await import('firebase/firestore');
|
|
172
213
|
|
|
173
214
|
// Store Firebase methods for later use
|
|
174
215
|
this.firebase = {
|
|
175
|
-
collection, doc, setDoc, onSnapshot, deleteDoc, query, where, orderBy, limit,
|
|
176
|
-
ref, set, onValue, onDisconnect, serverTimestamp
|
|
216
|
+
collection, doc, setDoc, onSnapshot, deleteDoc, query, where, orderBy, limit, serverTimestamp
|
|
177
217
|
};
|
|
178
218
|
|
|
179
219
|
// Initialize or reuse existing app
|
|
180
220
|
const apps = getApps();
|
|
181
221
|
this.app = apps.length ? apps[0] : initializeApp(this.config);
|
|
182
222
|
|
|
183
|
-
// Initialize Firestore
|
|
223
|
+
// Initialize Firestore
|
|
184
224
|
this.db = getFirestore(this.app);
|
|
185
225
|
|
|
186
|
-
//
|
|
187
|
-
|
|
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
|
+
}
|
|
188
235
|
|
|
189
|
-
// Register presence
|
|
236
|
+
// Register presence in Firestore
|
|
190
237
|
await this.registerPresence();
|
|
191
238
|
|
|
192
239
|
// Listen for peers
|
|
@@ -196,7 +243,7 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
196
243
|
this.subscribeToSignals();
|
|
197
244
|
|
|
198
245
|
this.isConnected = true;
|
|
199
|
-
console.log(' ✅ Firebase
|
|
246
|
+
console.log(' ✅ Firebase connected with WASM security');
|
|
200
247
|
|
|
201
248
|
this.emit('connected');
|
|
202
249
|
return true;
|
|
@@ -209,54 +256,98 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
209
256
|
}
|
|
210
257
|
|
|
211
258
|
/**
|
|
212
|
-
* Register this peer's presence in
|
|
259
|
+
* Register this peer's presence in Firestore with WASM-signed data
|
|
213
260
|
*/
|
|
214
261
|
async registerPresence() {
|
|
215
|
-
const {
|
|
262
|
+
const { doc, setDoc, serverTimestamp } = this.firebase;
|
|
216
263
|
|
|
217
|
-
const presenceRef =
|
|
264
|
+
const presenceRef = doc(this.db, SIGNALING_PATHS.peers, this.peerId);
|
|
218
265
|
|
|
219
|
-
//
|
|
220
|
-
|
|
266
|
+
// Build presence data
|
|
267
|
+
const presenceData = {
|
|
221
268
|
peerId: this.peerId,
|
|
222
269
|
room: this.room,
|
|
223
270
|
online: true,
|
|
224
271
|
lastSeen: serverTimestamp(),
|
|
225
272
|
capabilities: ['compute', 'storage', 'verify'],
|
|
226
|
-
}
|
|
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
|
+
}
|
|
227
287
|
|
|
228
|
-
//
|
|
229
|
-
|
|
288
|
+
// Set online status in Firestore
|
|
289
|
+
await setDoc(presenceRef, presenceData, { merge: true });
|
|
290
|
+
|
|
291
|
+
// Set up heartbeat to maintain presence (Firestore doesn't have onDisconnect)
|
|
292
|
+
this._heartbeatInterval = setInterval(async () => {
|
|
293
|
+
try {
|
|
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 });
|
|
301
|
+
} catch (e) {
|
|
302
|
+
// Ignore heartbeat errors
|
|
303
|
+
}
|
|
304
|
+
}, 30000);
|
|
230
305
|
|
|
231
|
-
console.log(` 📡 Registered presence: ${this.peerId.slice(0, 8)}
|
|
306
|
+
console.log(` 📡 Registered presence: ${this.peerId.slice(0, 8)}... (WASM-signed)`);
|
|
232
307
|
}
|
|
233
308
|
|
|
234
309
|
/**
|
|
235
|
-
* Subscribe to peer presence updates
|
|
310
|
+
* Subscribe to peer presence updates (using Firestore)
|
|
236
311
|
*/
|
|
237
312
|
subscribeToPeers() {
|
|
238
|
-
const {
|
|
239
|
-
|
|
240
|
-
const roomRef = ref(this.rtdb, `presence/${this.room}`);
|
|
313
|
+
const { collection, query, where, onSnapshot } = this.firebase;
|
|
241
314
|
|
|
242
|
-
|
|
243
|
-
|
|
315
|
+
// Query peers in same room that were active in last 2 minutes
|
|
316
|
+
const peersRef = collection(this.db, SIGNALING_PATHS.peers);
|
|
317
|
+
const q = query(peersRef, where('room', '==', this.room));
|
|
244
318
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
319
|
+
const unsubscribe = onSnapshot(q, (snapshot) => {
|
|
320
|
+
const now = Date.now();
|
|
321
|
+
const staleThreshold = 2 * 60 * 1000; // 2 minutes
|
|
322
|
+
|
|
323
|
+
snapshot.docChanges().forEach((change) => {
|
|
324
|
+
const data = change.doc.data();
|
|
325
|
+
const peerId = change.doc.id;
|
|
326
|
+
|
|
327
|
+
if (peerId === this.peerId) return; // Skip self
|
|
328
|
+
|
|
329
|
+
if (change.type === 'added' || change.type === 'modified') {
|
|
330
|
+
// Check if peer is still active (lastSeen within threshold)
|
|
331
|
+
const lastSeen = data.lastSeen?.toMillis?.() || 0;
|
|
332
|
+
if (now - lastSeen < staleThreshold) {
|
|
333
|
+
if (!this.peers.has(peerId)) {
|
|
334
|
+
this.peers.set(peerId, data);
|
|
335
|
+
this.emit('peer-discovered', { peerId, ...data });
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
// Peer is stale
|
|
339
|
+
if (this.peers.has(peerId)) {
|
|
340
|
+
this.peers.delete(peerId);
|
|
341
|
+
this.emit('peer-left', { peerId });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} else if (change.type === 'removed') {
|
|
345
|
+
if (this.peers.has(peerId)) {
|
|
346
|
+
this.peers.delete(peerId);
|
|
347
|
+
this.emit('peer-left', { peerId });
|
|
348
|
+
}
|
|
258
349
|
}
|
|
259
|
-
}
|
|
350
|
+
});
|
|
260
351
|
});
|
|
261
352
|
|
|
262
353
|
this.unsubscribers.push(unsubscribe);
|
|
@@ -285,7 +376,7 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
285
376
|
}
|
|
286
377
|
|
|
287
378
|
/**
|
|
288
|
-
* Handle incoming signal
|
|
379
|
+
* Handle incoming signal with WASM signature verification
|
|
289
380
|
*/
|
|
290
381
|
async handleSignal(signal, docId) {
|
|
291
382
|
this.stats.firebaseSignals++;
|
|
@@ -294,19 +385,46 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
294
385
|
const { doc, deleteDoc } = this.firebase;
|
|
295
386
|
await deleteDoc(doc(this.db, SIGNALING_PATHS.signals, docId));
|
|
296
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
|
+
|
|
297
415
|
// Emit appropriate event
|
|
298
416
|
switch (signal.type) {
|
|
299
417
|
case 'offer':
|
|
300
|
-
this.emit('offer', { from: signal.from, offer: signal.data });
|
|
418
|
+
this.emit('offer', { from: signal.from, offer: signal.data, verified: !!signal.signature });
|
|
301
419
|
break;
|
|
302
420
|
case 'answer':
|
|
303
|
-
this.emit('answer', { from: signal.from, answer: signal.data });
|
|
421
|
+
this.emit('answer', { from: signal.from, answer: signal.data, verified: !!signal.signature });
|
|
304
422
|
break;
|
|
305
423
|
case 'ice-candidate':
|
|
306
|
-
this.emit('ice-candidate', { from: signal.from, candidate: signal.data });
|
|
424
|
+
this.emit('ice-candidate', { from: signal.from, candidate: signal.data, verified: !!signal.signature });
|
|
307
425
|
break;
|
|
308
426
|
default:
|
|
309
|
-
this.emit('signal', signal);
|
|
427
|
+
this.emit('signal', { ...signal, verified: !!signal.signature });
|
|
310
428
|
}
|
|
311
429
|
}
|
|
312
430
|
|
|
@@ -332,7 +450,7 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
332
450
|
}
|
|
333
451
|
|
|
334
452
|
/**
|
|
335
|
-
* Send signal via Firebase
|
|
453
|
+
* Send signal via Firebase with WASM signature
|
|
336
454
|
*/
|
|
337
455
|
async sendSignal(toPeerId, type, data) {
|
|
338
456
|
if (!this.isConnected) {
|
|
@@ -344,14 +462,30 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
344
462
|
const signalId = `${this.peerId}-${toPeerId}-${Date.now()}`;
|
|
345
463
|
const signalRef = doc(this.db, SIGNALING_PATHS.signals, signalId);
|
|
346
464
|
|
|
347
|
-
|
|
465
|
+
const timestamp = Date.now();
|
|
466
|
+
const signalData = {
|
|
348
467
|
from: this.peerId,
|
|
349
468
|
to: toPeerId,
|
|
350
469
|
type,
|
|
351
470
|
data,
|
|
352
|
-
timestamp
|
|
471
|
+
timestamp,
|
|
353
472
|
room: this.room,
|
|
354
|
-
}
|
|
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);
|
|
355
489
|
|
|
356
490
|
return true;
|
|
357
491
|
}
|
|
@@ -370,17 +504,27 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
370
504
|
* Disconnect and cleanup
|
|
371
505
|
*/
|
|
372
506
|
async disconnect() {
|
|
507
|
+
// Stop heartbeat
|
|
508
|
+
if (this._heartbeatInterval) {
|
|
509
|
+
clearInterval(this._heartbeatInterval);
|
|
510
|
+
this._heartbeatInterval = null;
|
|
511
|
+
}
|
|
512
|
+
|
|
373
513
|
// Unsubscribe from all listeners
|
|
374
514
|
for (const unsub of this.unsubscribers) {
|
|
375
515
|
if (typeof unsub === 'function') unsub();
|
|
376
516
|
}
|
|
377
517
|
this.unsubscribers = [];
|
|
378
518
|
|
|
379
|
-
// Remove presence
|
|
380
|
-
if (this.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
519
|
+
// Remove presence from Firestore
|
|
520
|
+
if (this.db && this.firebase) {
|
|
521
|
+
try {
|
|
522
|
+
const { doc, deleteDoc } = this.firebase;
|
|
523
|
+
const presenceRef = doc(this.db, SIGNALING_PATHS.peers, this.peerId);
|
|
524
|
+
await deleteDoc(presenceRef);
|
|
525
|
+
} catch (e) {
|
|
526
|
+
// Ignore cleanup errors
|
|
527
|
+
}
|
|
384
528
|
}
|
|
385
529
|
|
|
386
530
|
this.isConnected = false;
|
|
@@ -569,6 +713,10 @@ export class HybridBootstrap extends EventEmitter {
|
|
|
569
713
|
this.peerId = options.peerId;
|
|
570
714
|
this.config = options.firebaseConfig || DEFAULT_FIREBASE_CONFIG;
|
|
571
715
|
|
|
716
|
+
// WASM Security
|
|
717
|
+
/** @type {import('./secure-access.js').SecureAccessManager|null} */
|
|
718
|
+
this.secureAccess = options.secureAccess || null;
|
|
719
|
+
|
|
572
720
|
// Components
|
|
573
721
|
this.firebase = null;
|
|
574
722
|
this.dht = null;
|
|
@@ -586,25 +734,63 @@ export class HybridBootstrap extends EventEmitter {
|
|
|
586
734
|
directConnections: 0,
|
|
587
735
|
firebaseSignals: 0,
|
|
588
736
|
p2pSignals: 0,
|
|
737
|
+
verifiedPeers: 0,
|
|
589
738
|
};
|
|
590
739
|
}
|
|
591
740
|
|
|
592
741
|
/**
|
|
593
|
-
* Start hybrid bootstrap
|
|
742
|
+
* Start hybrid bootstrap with WASM security
|
|
594
743
|
*/
|
|
595
744
|
async start(webrtc, dht) {
|
|
596
745
|
this.webrtc = webrtc;
|
|
597
746
|
this.dht = dht;
|
|
598
747
|
|
|
599
|
-
//
|
|
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
|
|
600
766
|
this.firebase = new FirebaseSignaling({
|
|
601
767
|
peerId: this.peerId,
|
|
602
768
|
firebaseConfig: this.config,
|
|
769
|
+
secureAccess: this.secureAccess,
|
|
603
770
|
});
|
|
604
771
|
|
|
605
772
|
// Wire up events
|
|
606
773
|
this.setupFirebaseEvents();
|
|
607
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
|
+
|
|
608
794
|
// Connect to Firebase
|
|
609
795
|
const connected = await this.firebase.connect();
|
|
610
796
|
|
|
@@ -665,18 +851,10 @@ export class HybridBootstrap extends EventEmitter {
|
|
|
665
851
|
if (!this.webrtc) return;
|
|
666
852
|
|
|
667
853
|
try {
|
|
668
|
-
//
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
if (this.mode === 'p2p' && this.webrtc.isConnected(peerId)) {
|
|
673
|
-
this.webrtc.sendToPeer(peerId, { type: 'offer', offer });
|
|
674
|
-
this.stats.p2pSignals++;
|
|
675
|
-
} else {
|
|
676
|
-
// Fall back to Firebase
|
|
677
|
-
await this.firebase.sendOffer(peerId, offer);
|
|
678
|
-
this.stats.firebaseSignals++;
|
|
679
|
-
}
|
|
854
|
+
// Use WebRTCPeerManager's connectToPeer method
|
|
855
|
+
// This handles offer creation and signaling internally
|
|
856
|
+
await this.webrtc.connectToPeer(peerId);
|
|
857
|
+
this.stats.directConnections++;
|
|
680
858
|
|
|
681
859
|
} catch (error) {
|
|
682
860
|
console.warn(`[HybridBootstrap] Connect to ${peerId.slice(0, 8)} failed:`, error.message);
|
package/p2p.js
CHANGED
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": [
|
|
@@ -189,14 +197,8 @@
|
|
|
189
197
|
"dependencies": {
|
|
190
198
|
"@ruvector/ruvllm": "^0.2.3",
|
|
191
199
|
"@xenova/transformers": "^2.17.2",
|
|
200
|
+
"firebase": "^10.14.1",
|
|
201
|
+
"wrtc": "^0.4.7",
|
|
192
202
|
"ws": "^8.18.3"
|
|
193
|
-
},
|
|
194
|
-
"peerDependencies": {
|
|
195
|
-
"firebase": "^10.0.0"
|
|
196
|
-
},
|
|
197
|
-
"peerDependenciesMeta": {
|
|
198
|
-
"firebase": {
|
|
199
|
-
"optional": true
|
|
200
|
-
}
|
|
201
203
|
}
|
|
202
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());
|