Files

596 lines
19 KiB
JavaScript

/**
* @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;