@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.
- package/deploy/.env.example +97 -0
- package/deploy/DEPLOY.md +481 -0
- package/deploy/Dockerfile +99 -0
- package/deploy/docker-compose.yml +162 -0
- package/deploy/genesis-prod.js +1536 -0
- package/deploy/health-check.js +187 -0
- package/deploy/prometheus.yml +38 -0
- package/firebase-signaling.js +242 -53
- package/package.json +19 -3
- package/real-workers.js +9 -4
- package/scheduler.js +8 -4
- package/secure-access.js +595 -0
- package/tests/distributed-workers-test.js +1609 -0
- package/tests/p2p-migration-test.js +1102 -0
- package/tests/webrtc-peer-test.js +686 -0
- package/webrtc.js +727 -50
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;
|