@ruvector/edge-net 0.4.1 → 0.4.3

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.
@@ -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;