Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
595
vendor/ruvector/examples/edge-net/pkg/secure-access.js
vendored
Normal file
595
vendor/ruvector/examples/edge-net/pkg/secure-access.js
vendored
Normal file
@@ -0,0 +1,595 @@
|
||||
/**
|
||||
* @ruvector/edge-net Secure Access Layer
|
||||
*
|
||||
* Uses WASM cryptographic primitives for secure network access.
|
||||
* No external authentication needed - cryptographic proof of identity.
|
||||
*
|
||||
* Security Model:
|
||||
* 1. Each node generates a PiKey (Ed25519-based) in WASM
|
||||
* 2. All messages are signed with the node's private key
|
||||
* 3. Other nodes verify signatures with public keys
|
||||
* 4. AdaptiveSecurity provides self-learning attack detection
|
||||
*
|
||||
* @module @ruvector/edge-net/secure-access
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
/**
|
||||
* Secure Access Manager
|
||||
*
|
||||
* Provides WASM-based cryptographic identity and message signing
|
||||
* for secure P2P network access without external auth providers.
|
||||
*/
|
||||
export class SecureAccessManager extends EventEmitter {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
|
||||
/** @type {import('./ruvector_edge_net').PiKey|null} */
|
||||
this.piKey = null;
|
||||
|
||||
/** @type {import('./ruvector_edge_net').SessionKey|null} */
|
||||
this.sessionKey = null;
|
||||
|
||||
/** @type {import('./ruvector_edge_net').WasmNodeIdentity|null} */
|
||||
this.nodeIdentity = null;
|
||||
|
||||
/** @type {import('./ruvector_edge_net').AdaptiveSecurity|null} */
|
||||
this.security = null;
|
||||
|
||||
/** @type {Map<string, Uint8Array>} Known peer public keys */
|
||||
this.knownPeers = new Map();
|
||||
|
||||
/** @type {Map<string, number>} Peer reputation scores */
|
||||
this.peerReputation = new Map();
|
||||
|
||||
this.options = {
|
||||
siteId: options.siteId || 'edge-net',
|
||||
sessionTTL: options.sessionTTL || 3600, // 1 hour
|
||||
backupPassword: options.backupPassword || null,
|
||||
persistIdentity: options.persistIdentity !== false,
|
||||
...options
|
||||
};
|
||||
|
||||
this.wasm = null;
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize secure access with WASM cryptography
|
||||
*/
|
||||
async initialize() {
|
||||
if (this.initialized) return this;
|
||||
|
||||
console.log('🔐 Initializing WASM Secure Access...');
|
||||
|
||||
// Load WASM module
|
||||
try {
|
||||
// For Node.js, use the node-specific CJS module which auto-loads WASM
|
||||
const isNode = typeof process !== 'undefined' && process.versions?.node;
|
||||
if (isNode) {
|
||||
// Node.js: CJS module loads WASM synchronously on import
|
||||
this.wasm = await import('./node/ruvector_edge_net.cjs');
|
||||
} else {
|
||||
// Browser: Use ES module with WASM init
|
||||
const wasmModule = await import('./ruvector_edge_net.js');
|
||||
// Call default init to load WASM binary
|
||||
if (wasmModule.default && typeof wasmModule.default === 'function') {
|
||||
await wasmModule.default();
|
||||
}
|
||||
this.wasm = wasmModule;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(' ❌ WASM load error:', err.message);
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Try to restore existing identity
|
||||
const restored = await this._tryRestoreIdentity();
|
||||
|
||||
if (!restored) {
|
||||
// Generate new cryptographic identity
|
||||
await this._generateIdentity();
|
||||
}
|
||||
|
||||
// Initialize adaptive security
|
||||
this.security = new this.wasm.AdaptiveSecurity();
|
||||
|
||||
// Create session key for encrypted communications
|
||||
this.sessionKey = new this.wasm.SessionKey(this.piKey, this.options.sessionTTL);
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
console.log(` 🔑 Node ID: ${this.getShortId()}`);
|
||||
console.log(` 📦 Public Key: ${this.getPublicKeyHex().slice(0, 16)}...`);
|
||||
console.log(` ⏱️ Session expires: ${new Date(Date.now() + this.options.sessionTTL * 1000).toISOString()}`);
|
||||
|
||||
this.emit('initialized', {
|
||||
nodeId: this.getNodeId(),
|
||||
publicKey: this.getPublicKeyHex()
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to restore identity from localStorage or backup
|
||||
*/
|
||||
async _tryRestoreIdentity() {
|
||||
if (!this.options.persistIdentity) return false;
|
||||
|
||||
try {
|
||||
// Check localStorage (browser) or file (Node.js)
|
||||
let stored = null;
|
||||
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
stored = localStorage.getItem('edge-net-identity');
|
||||
} else if (typeof process !== 'undefined') {
|
||||
const fs = await import('fs');
|
||||
const path = await import('path');
|
||||
const identityPath = path.join(process.cwd(), '.edge-net-identity');
|
||||
if (fs.existsSync(identityPath)) {
|
||||
stored = fs.readFileSync(identityPath, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
if (stored) {
|
||||
const data = JSON.parse(stored);
|
||||
const encrypted = new Uint8Array(data.encrypted);
|
||||
|
||||
// Use default password if none provided
|
||||
const password = this.options.backupPassword || 'edge-net-default-key';
|
||||
|
||||
this.piKey = this.wasm.PiKey.restoreFromBackup(encrypted, password);
|
||||
this.nodeIdentity = this.wasm.WasmNodeIdentity.fromSecretKey(
|
||||
encrypted, // Same key derivation
|
||||
this.options.siteId
|
||||
);
|
||||
|
||||
console.log(' ♻️ Restored existing identity');
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(' ⚡ Creating new identity (no backup found)');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate new cryptographic identity
|
||||
*/
|
||||
async _generateIdentity() {
|
||||
// Generate Pi-Key (Ed25519-based with Pi magic)
|
||||
// Constructor takes optional genesis_seed (Uint8Array or null)
|
||||
const genesisSeed = this.options.genesisSeed || null;
|
||||
this.piKey = new this.wasm.PiKey(genesisSeed);
|
||||
|
||||
// Create node identity from same site
|
||||
this.nodeIdentity = new this.wasm.WasmNodeIdentity(this.options.siteId);
|
||||
|
||||
// Persist identity if enabled
|
||||
if (this.options.persistIdentity) {
|
||||
await this._persistIdentity();
|
||||
}
|
||||
|
||||
console.log(' ✨ Generated new cryptographic identity');
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist identity to storage
|
||||
*/
|
||||
async _persistIdentity() {
|
||||
const password = this.options.backupPassword || 'edge-net-default-key';
|
||||
const backup = this.piKey.createEncryptedBackup(password);
|
||||
const data = JSON.stringify({
|
||||
encrypted: Array.from(backup),
|
||||
created: Date.now(),
|
||||
siteId: this.options.siteId
|
||||
});
|
||||
|
||||
try {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('edge-net-identity', data);
|
||||
} else if (typeof process !== 'undefined') {
|
||||
const fs = await import('fs');
|
||||
const path = await import('path');
|
||||
const identityPath = path.join(process.cwd(), '.edge-net-identity');
|
||||
fs.writeFileSync(identityPath, data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(' ⚠️ Could not persist identity:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// IDENTITY & KEYS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Get node ID (full)
|
||||
*/
|
||||
getNodeId() {
|
||||
return this.piKey?.getIdentityHex() || this.nodeIdentity?.getId?.() || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get short node ID for display
|
||||
*/
|
||||
getShortId() {
|
||||
return this.piKey?.getShortId() || this.getNodeId().slice(0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public key as hex string
|
||||
*/
|
||||
getPublicKeyHex() {
|
||||
return Array.from(this.piKey?.getPublicKey() || new Uint8Array(32))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public key as bytes
|
||||
*/
|
||||
getPublicKeyBytes() {
|
||||
return this.piKey?.getPublicKey() || new Uint8Array(32);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MESSAGE SIGNING & VERIFICATION
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Sign a message/object
|
||||
* @param {object|string|Uint8Array} message - Message to sign
|
||||
* @returns {{ payload: string, signature: string, publicKey: string, timestamp: number }}
|
||||
*/
|
||||
signMessage(message) {
|
||||
const payload = typeof message === 'string' ? message :
|
||||
message instanceof Uint8Array ? new TextDecoder().decode(message) :
|
||||
JSON.stringify(message);
|
||||
|
||||
const timestamp = Date.now();
|
||||
const dataToSign = `${payload}|${timestamp}`;
|
||||
const dataBytes = new TextEncoder().encode(dataToSign);
|
||||
|
||||
const signature = this.piKey.sign(dataBytes);
|
||||
|
||||
return {
|
||||
payload,
|
||||
signature: Array.from(signature).map(b => b.toString(16).padStart(2, '0')).join(''),
|
||||
publicKey: this.getPublicKeyHex(),
|
||||
timestamp,
|
||||
nodeId: this.getShortId()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a signed message
|
||||
* @param {object} signed - Signed message object
|
||||
* @returns {boolean} Whether signature is valid
|
||||
*/
|
||||
verifyMessage(signed) {
|
||||
try {
|
||||
const { payload, signature, publicKey, timestamp } = signed;
|
||||
|
||||
// Check timestamp (reject messages older than 5 minutes)
|
||||
const age = Date.now() - timestamp;
|
||||
if (age > 5 * 60 * 1000) {
|
||||
console.warn('⚠️ Message too old:', age, 'ms');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert hex strings back to bytes
|
||||
const dataToVerify = `${payload}|${timestamp}`;
|
||||
const dataBytes = new TextEncoder().encode(dataToVerify);
|
||||
const sigBytes = new Uint8Array(signature.match(/.{2}/g).map(h => parseInt(h, 16)));
|
||||
const pubKeyBytes = new Uint8Array(publicKey.match(/.{2}/g).map(h => parseInt(h, 16)));
|
||||
|
||||
// Verify using WASM
|
||||
const valid = this.piKey.verify(dataBytes, sigBytes, pubKeyBytes);
|
||||
|
||||
// Update peer reputation based on verification
|
||||
if (valid) {
|
||||
this._updateReputation(signed.nodeId || publicKey.slice(0, 16), 0.01);
|
||||
} else {
|
||||
this._updateReputation(signed.nodeId || publicKey.slice(0, 16), -0.1);
|
||||
this._recordSuspicious(signed.nodeId, 'invalid_signature');
|
||||
}
|
||||
|
||||
return valid;
|
||||
} catch (err) {
|
||||
console.warn('⚠️ Signature verification error:', err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PEER MANAGEMENT
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Register a known peer's public key
|
||||
*/
|
||||
registerPeer(peerId, publicKey) {
|
||||
const pubKeyBytes = typeof publicKey === 'string' ?
|
||||
new Uint8Array(publicKey.match(/.{2}/g).map(h => parseInt(h, 16))) :
|
||||
publicKey;
|
||||
|
||||
this.knownPeers.set(peerId, pubKeyBytes);
|
||||
this.peerReputation.set(peerId, this.peerReputation.get(peerId) || 0.5);
|
||||
|
||||
this.emit('peer-registered', { peerId, publicKey: this.getPublicKeyHex() });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reputation score for a peer (0-1)
|
||||
*/
|
||||
getPeerReputation(peerId) {
|
||||
return this.peerReputation.get(peerId) || 0.5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update peer reputation
|
||||
*/
|
||||
_updateReputation(peerId, delta) {
|
||||
const current = this.peerReputation.get(peerId) || 0.5;
|
||||
const newScore = Math.max(0, Math.min(1, current + delta));
|
||||
this.peerReputation.set(peerId, newScore);
|
||||
|
||||
// Emit warning if reputation drops too low
|
||||
if (newScore < 0.2) {
|
||||
this.emit('peer-suspicious', { peerId, reputation: newScore });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record suspicious activity for learning
|
||||
*/
|
||||
_recordSuspicious(peerId, reason) {
|
||||
if (this.security) {
|
||||
// Record for adaptive security learning
|
||||
const features = new Float32Array([
|
||||
Date.now() / 1e12,
|
||||
this.getPeerReputation(peerId),
|
||||
reason === 'invalid_signature' ? 1 : 0,
|
||||
reason === 'replay_attack' ? 1 : 0,
|
||||
0, 0, 0, 0 // Padding
|
||||
]);
|
||||
this.security.recordAttackPattern(reason, features, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ENCRYPTION (SESSION-BASED)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Encrypt data for secure transmission
|
||||
*/
|
||||
encrypt(data) {
|
||||
if (!this.sessionKey || this.sessionKey.isExpired()) {
|
||||
// Refresh session key
|
||||
this.sessionKey = new this.wasm.SessionKey(this.piKey, this.options.sessionTTL);
|
||||
}
|
||||
|
||||
const dataBytes = typeof data === 'string' ?
|
||||
new TextEncoder().encode(data) :
|
||||
data instanceof Uint8Array ? data :
|
||||
new TextEncoder().encode(JSON.stringify(data));
|
||||
|
||||
return this.sessionKey.encrypt(dataBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt received data
|
||||
*/
|
||||
decrypt(encrypted) {
|
||||
if (!this.sessionKey) {
|
||||
throw new Error('No session key available');
|
||||
}
|
||||
|
||||
return this.sessionKey.decrypt(encrypted);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SECURITY ANALYSIS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Analyze request for potential attacks
|
||||
* @returns {number} Threat score (0-1, higher = more suspicious)
|
||||
*/
|
||||
analyzeRequest(features) {
|
||||
if (!this.security) return 0;
|
||||
|
||||
const featureArray = features instanceof Float32Array ?
|
||||
features :
|
||||
new Float32Array(Array.isArray(features) ? features : Object.values(features));
|
||||
|
||||
return this.security.detectAttack(featureArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get security statistics
|
||||
*/
|
||||
getSecurityStats() {
|
||||
if (!this.security) return null;
|
||||
|
||||
return JSON.parse(this.security.getStats());
|
||||
}
|
||||
|
||||
/**
|
||||
* Export security patterns for persistence
|
||||
*/
|
||||
exportSecurityPatterns() {
|
||||
if (!this.security) return null;
|
||||
return this.security.exportPatterns();
|
||||
}
|
||||
|
||||
/**
|
||||
* Import previously learned security patterns
|
||||
*/
|
||||
importSecurityPatterns(patterns) {
|
||||
if (!this.security) return;
|
||||
this.security.importPatterns(patterns);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CHALLENGE-RESPONSE
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Create a challenge for peer verification
|
||||
*/
|
||||
createChallenge() {
|
||||
const challenge = crypto.getRandomValues(new Uint8Array(32));
|
||||
const timestamp = Date.now();
|
||||
|
||||
return {
|
||||
challenge: Array.from(challenge).map(b => b.toString(16).padStart(2, '0')).join(''),
|
||||
timestamp,
|
||||
issuer: this.getShortId()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to a challenge (proves identity)
|
||||
*/
|
||||
respondToChallenge(challengeData) {
|
||||
const challengeBytes = new Uint8Array(
|
||||
challengeData.challenge.match(/.{2}/g).map(h => parseInt(h, 16))
|
||||
);
|
||||
|
||||
const responseData = new Uint8Array([
|
||||
...challengeBytes,
|
||||
...new TextEncoder().encode(`|${challengeData.timestamp}|${this.getShortId()}`)
|
||||
]);
|
||||
|
||||
const signature = this.piKey.sign(responseData);
|
||||
|
||||
return {
|
||||
...challengeData,
|
||||
response: Array.from(signature).map(b => b.toString(16).padStart(2, '0')).join(''),
|
||||
responder: this.getShortId(),
|
||||
publicKey: this.getPublicKeyHex()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a challenge response
|
||||
*/
|
||||
verifyChallengeResponse(response) {
|
||||
try {
|
||||
const challengeBytes = new Uint8Array(
|
||||
response.challenge.match(/.{2}/g).map(h => parseInt(h, 16))
|
||||
);
|
||||
|
||||
const responseData = new Uint8Array([
|
||||
...challengeBytes,
|
||||
...new TextEncoder().encode(`|${response.timestamp}|${response.responder}`)
|
||||
]);
|
||||
|
||||
const sigBytes = new Uint8Array(
|
||||
response.response.match(/.{2}/g).map(h => parseInt(h, 16))
|
||||
);
|
||||
const pubKeyBytes = new Uint8Array(
|
||||
response.publicKey.match(/.{2}/g).map(h => parseInt(h, 16))
|
||||
);
|
||||
|
||||
const valid = this.piKey.verify(responseData, sigBytes, pubKeyBytes);
|
||||
|
||||
if (valid) {
|
||||
// Register this peer as verified
|
||||
this.registerPeer(response.responder, response.publicKey);
|
||||
this._updateReputation(response.responder, 0.05);
|
||||
}
|
||||
|
||||
return valid;
|
||||
} catch (err) {
|
||||
console.warn('Challenge verification failed:', err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
dispose() {
|
||||
try { this.piKey?.free?.(); } catch (e) { /* already freed */ }
|
||||
try { this.sessionKey?.free?.(); } catch (e) { /* already freed */ }
|
||||
try { this.nodeIdentity?.free?.(); } catch (e) { /* already freed */ }
|
||||
try { this.security?.free?.(); } catch (e) { /* already freed */ }
|
||||
this.piKey = null;
|
||||
this.sessionKey = null;
|
||||
this.nodeIdentity = null;
|
||||
this.security = null;
|
||||
this.knownPeers.clear();
|
||||
this.peerReputation.clear();
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a secure access manager
|
||||
*/
|
||||
export async function createSecureAccess(options = {}) {
|
||||
const manager = new SecureAccessManager(options);
|
||||
await manager.initialize();
|
||||
return manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap Firebase signaling with WASM security
|
||||
*/
|
||||
export function wrapWithSecurity(firebaseSignaling, secureAccess) {
|
||||
const originalAnnounce = firebaseSignaling.announcePeer?.bind(firebaseSignaling);
|
||||
const originalSendOffer = firebaseSignaling.sendOffer?.bind(firebaseSignaling);
|
||||
const originalSendAnswer = firebaseSignaling.sendAnswer?.bind(firebaseSignaling);
|
||||
const originalSendIceCandidate = firebaseSignaling.sendIceCandidate?.bind(firebaseSignaling);
|
||||
|
||||
// Wrap peer announcement with signature
|
||||
if (originalAnnounce) {
|
||||
firebaseSignaling.announcePeer = async (peerId, metadata = {}) => {
|
||||
const signedMetadata = secureAccess.signMessage({
|
||||
...metadata,
|
||||
publicKey: secureAccess.getPublicKeyHex()
|
||||
});
|
||||
return originalAnnounce(peerId, signedMetadata);
|
||||
};
|
||||
}
|
||||
|
||||
// Wrap signaling messages with signatures
|
||||
if (originalSendOffer) {
|
||||
firebaseSignaling.sendOffer = async (toPeerId, offer) => {
|
||||
const signed = secureAccess.signMessage({ type: 'offer', offer });
|
||||
return originalSendOffer(toPeerId, signed);
|
||||
};
|
||||
}
|
||||
|
||||
if (originalSendAnswer) {
|
||||
firebaseSignaling.sendAnswer = async (toPeerId, answer) => {
|
||||
const signed = secureAccess.signMessage({ type: 'answer', answer });
|
||||
return originalSendAnswer(toPeerId, signed);
|
||||
};
|
||||
}
|
||||
|
||||
if (originalSendIceCandidate) {
|
||||
firebaseSignaling.sendIceCandidate = async (toPeerId, candidate) => {
|
||||
const signed = secureAccess.signMessage({ type: 'ice', candidate });
|
||||
return originalSendIceCandidate(toPeerId, signed);
|
||||
};
|
||||
}
|
||||
|
||||
// Add verification method
|
||||
firebaseSignaling.verifySignedMessage = (signed) => {
|
||||
return secureAccess.verifyMessage(signed);
|
||||
};
|
||||
|
||||
firebaseSignaling.secureAccess = secureAccess;
|
||||
|
||||
return firebaseSignaling;
|
||||
}
|
||||
|
||||
export default SecureAccessManager;
|
||||
Reference in New Issue
Block a user