UNPKG

lotus-sdk

Version:

Central repository for several classes of tools for integrating with, and building for, the Lotusia ecosystem

359 lines (358 loc) 13.3 kB
import { Hash } from '../../bitcore/crypto/hash.js'; import { Schnorr } from '../../bitcore/crypto/schnorr.js'; import { Signature } from '../../bitcore/crypto/signature.js'; import { DEFAULT_SECURITY_POLICY, } from './types.js'; export class DiscoverySecurityValidator { coordinator; reputationData = new Map(); seenAdvertisements = new Map(); rateLimitTracker = new Map(); policy; cleanupTimer; constructor(coordinator, policy = {}) { this.coordinator = coordinator; this.policy = { ...DEFAULT_SECURITY_POLICY, ...policy }; } async validateAdvertisement(advertisement, criteria) { const now = Date.now(); const result = { valid: true, securityScore: 100, details: { signatureValid: true, notExpired: true, reputationAcceptable: true, criteriaMatch: true, customValidation: true, }, }; try { if (advertisement.expiresAt <= now) { result.valid = false; result.error = 'Advertisement expired'; result.details.notExpired = false; result.securityScore -= 50; } const age = now - advertisement.createdAt; if (age > this.policy.maxAdvertisementAge) { result.valid = false; result.error = 'Advertisement too old'; result.securityScore -= 30; } if (this.policy.enableSignatureVerification) { const signatureValid = await this.validateSignature(advertisement); result.details.signatureValid = signatureValid; if (!signatureValid) { result.valid = false; result.error = 'Invalid signature'; result.securityScore -= 40; } } const reputation = this.getReputation(advertisement.peerInfo.peerId); result.details.reputationAcceptable = reputation.score >= this.policy.minReputation; if (!result.details.reputationAcceptable) { result.valid = false; result.error = 'Reputation too low'; result.securityScore -= 30; } if (this.policy.enableReplayPrevention) { const isReplay = this.checkReplayAttack(advertisement); if (isReplay) { result.valid = false; result.error = 'Replay attack detected'; result.securityScore -= 60; } } if (criteria) { result.details.criteriaMatch = this.validateCriteriaMatch(advertisement, criteria); if (!result.details.criteriaMatch) { result.valid = false; result.error = 'Criteria mismatch'; result.securityScore -= 20; } } for (const validator of this.policy.customValidators) { try { const isValid = await validator.validator(advertisement); if (!isValid) { result.details.customValidation = false; result.securityScore -= 10; } } catch (error) { result.details.customValidation = false; result.securityScore -= 15; } } result.valid = result.valid && result.securityScore >= 50; return result; } catch (error) { return { valid: false, error: `Security validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, securityScore: 0, details: { signatureValid: false, notExpired: false, reputationAcceptable: false, criteriaMatch: false, customValidation: false, }, }; } } updateReputation(peerId, success, weight = 1, reason) { const reputation = this.getReputation(peerId); const now = Date.now(); if (success) { reputation.successes += weight; reputation.score = Math.min(100, reputation.score + weight * 2); } else { reputation.failures += weight; reputation.score = Math.max(0, reputation.score - weight * 5); } reputation.lastInteraction = now; reputation.history.push({ timestamp: now, success, weight, reason, }); if (reputation.history.length > 100) { reputation.history = reputation.history.slice(-100); } this.reputationData.set(peerId, reputation); } getReputation(peerId) { let reputation = this.reputationData.get(peerId); if (!reputation) { reputation = { peerId, score: 75, successes: 0, failures: 0, lastInteraction: Date.now(), history: [], }; this.reputationData.set(peerId, reputation); } return reputation; } checkRateLimit(peerId, operation) { if (!this.policy.enableRateLimiting) { return true; } const now = Date.now(); const windowStart = now - this.policy.rateLimits.windowSizeMs; const key = `${peerId}:${operation}`; let tracker = this.rateLimitTracker.get(key); if (!tracker || tracker.windowStart < windowStart) { tracker = { count: 0, windowStart: now }; this.rateLimitTracker.set(key, tracker); } const maxOperations = operation === 'advertise' ? this.policy.rateLimits.maxAdvertisementsPerPeer : this.policy.rateLimits.maxDiscoveryQueriesPerPeer; if (tracker.count >= maxOperations) { return false; } tracker.count++; return true; } start() { if (this.cleanupTimer) { return; } this.cleanupTimer = setInterval(() => { this.cleanup(); }, 60 * 60 * 1000); } stop() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = undefined; } } getSecurityStats() { const peers = Array.from(this.reputationData.values()); const totalPeers = peers.length; const averageReputation = totalPeers > 0 ? peers.reduce((sum, p) => sum + p.score, 0) / totalPeers : 0; const lowReputationPeers = peers.filter(p => p.score < this.policy.minReputation).length; let rateLimitViolations = 0; for (const tracker of Array.from(this.rateLimitTracker.values())) { rateLimitViolations += tracker.count; } const replayAttacksPrevented = this.seenAdvertisements.size; return { totalPeers, averageReputation, lowReputationPeers, rateLimitViolations, replayAttacksPrevented, }; } async validateSignature(advertisement) { if (!advertisement.signature) { return false; } if (!advertisement.peerInfo.publicKey) { return false; } try { const messageData = this.constructSignedMessage(advertisement); const messageHash = Hash.sha256(messageData); const signature = Signature.fromBuffer(advertisement.signature); if (!signature.isSchnorr) { signature.isSchnorr = true; } return Schnorr.verify(messageHash, signature, advertisement.peerInfo.publicKey, 'big'); } catch (error) { console.error('Signature verification failed:', error); return false; } } constructSignedMessage(advertisement) { const parts = []; parts.push(Buffer.from(advertisement.peerInfo.peerId)); if (advertisement.peerInfo.multiaddrs) { parts.push(Buffer.from(JSON.stringify(advertisement.peerInfo.multiaddrs))); } parts.push(Buffer.from(advertisement.protocol)); if (advertisement.capabilities) { parts.push(Buffer.from(JSON.stringify(advertisement.capabilities))); } parts.push(Buffer.from(advertisement.createdAt.toString())); parts.push(Buffer.from(advertisement.expiresAt.toString())); if (advertisement.customCriteria) { parts.push(Buffer.from(JSON.stringify(advertisement.customCriteria))); } return Buffer.concat(parts); } checkReplayAttack(advertisement) { const key = `${advertisement.peerInfo.peerId}:${advertisement.id}`; const existingTimestamp = this.seenAdvertisements.get(key); if (existingTimestamp) { if (advertisement.createdAt <= existingTimestamp) { return true; } } this.seenAdvertisements.set(key, advertisement.createdAt); return false; } validateCriteriaMatch(advertisement, criteria) { if (advertisement.protocol !== criteria.protocol) { return false; } if (criteria.capabilities) { const hasAllCapabilities = criteria.capabilities.every(cap => advertisement.capabilities.includes(cap)); if (!hasAllCapabilities) { return false; } } if (criteria.minReputation && advertisement.reputation < criteria.minReputation) { return false; } if (criteria.location && advertisement.location) { const distance = this.calculateDistance(criteria.location.latitude, criteria.location.longitude, advertisement.location.latitude, advertisement.location.longitude); if (distance > criteria.location.radiusKm) { return false; } } return true; } calculateDistance(lat1, lon1, lat2, lon2) { const R = 6371; const dLat = this.toRadians(lat2 - lat1); const dLon = this.toRadians(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } toRadians(degrees) { return degrees * (Math.PI / 180); } cleanup() { const now = Date.now(); const maxAge = 7 * 24 * 60 * 60 * 1000; for (const [peerId, reputation] of Array.from(this.reputationData.entries())) { if (now - reputation.lastInteraction > maxAge) { this.reputationData.delete(peerId); } } for (const [key, timestamp] of Array.from(this.seenAdvertisements.entries())) { if (now - timestamp > maxAge) { this.seenAdvertisements.delete(key); } } for (const [key, tracker] of Array.from(this.rateLimitTracker.entries())) { if (now - tracker.windowStart > this.policy.rateLimits.windowSizeMs * 2) { this.rateLimitTracker.delete(key); } } } } export function createSecurityPolicy(protocol, overrides = {}) { const basePolicy = { ...DEFAULT_SECURITY_POLICY }; switch (protocol) { case 'musig2': return { ...basePolicy, minReputation: 70, maxAdvertisementAge: 30 * 60 * 1000, ...overrides, }; case 'swapsig': return { ...basePolicy, minReputation: 60, maxAdvertisementAge: 60 * 60 * 1000, ...overrides, }; default: return { ...basePolicy, ...overrides, }; } } export const musig2Validator = { name: 'musig2-validator', validator: async (advertisement) => { if (advertisement.protocol !== 'musig2') { return true; } const requiredCapabilities = [ 'musig2-signer', 'nonce-commitment', 'partial-signature', ]; return requiredCapabilities.every(cap => advertisement.capabilities.includes(cap)); }, }; export const locationValidator = { name: 'location-validator', validator: async (advertisement) => { if (!advertisement.location) { return true; } const { latitude, longitude } = advertisement.location; return (latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180); }, }; export const capabilityValidator = { name: 'capability-validator', validator: async (advertisement) => { return advertisement.capabilities.length > 0; }, };