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
JavaScript
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;
},
};