From 81864969daf625c7164e181fd937a42ca055964e Mon Sep 17 00:00:00 2001 From: lklynet Date: Fri, 2 Jan 2026 11:05:44 -0500 Subject: [PATCH] feat(security): add PoW and Ed25519 signatures to prevent Sybil attacks --- server.js | 350 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 209 insertions(+), 141 deletions(-) diff --git a/server.js b/server.js index 3610f54..d190a83 100644 --- a/server.js +++ b/server.js @@ -1,16 +1,36 @@ -const express = require('express'); -const Hyperswarm = require('hyperswarm'); -const crypto = require('crypto'); +const express = require("express"); +const Hyperswarm = require("hyperswarm"); +const crypto = require("crypto"); const app = express(); const PORT = process.env.PORT || 3000; // --- CONFIGURATION --- -const TOPIC_NAME = 'hypermind-lklynet-v1'; -const TOPIC = crypto.createHash('sha256').update(TOPIC_NAME).digest(); +const TOPIC_NAME = "hypermind-lklynet-v1"; +const TOPIC = crypto.createHash("sha256").update(TOPIC_NAME).digest(); -const MY_ID = crypto.randomUUID(); -let mySeq = 0; +// --- SECURITY --- +// We use Ed25519 for signatures and a PoW puzzle to prevent Sybil attacks. +// Difficulty: Hash(ID + nonce) must start with '0000' +const POW_PREFIX = "0000"; + +console.log("[Security] Generating Identity & Solving PoW..."); +const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519"); +const MY_ID = publicKey.export({ type: "spki", format: "der" }).toString("hex"); +let MY_NONCE = 0; +while (true) { + const hash = crypto + .createHash("sha256") + .update(MY_ID + MY_NONCE) + .digest("hex"); + if (hash.startsWith(POW_PREFIX)) break; + MY_NONCE++; +} +console.log( + `[Security] Identity ready. ID: ${MY_ID.slice(0, 8)}... Nonce: ${MY_NONCE}` +); + +let mySeq = 0; const seenPeers = new Map(); @@ -19,152 +39,200 @@ const sseClients = new Set(); seenPeers.set(MY_ID, { seq: mySeq, lastSeen: Date.now() }); function broadcastUpdate() { - const data = JSON.stringify({ - count: seenPeers.size, - direct: swarm.connections.size, - id: MY_ID - }); - - for (const client of sseClients) { - client.write(`data: ${data}\n\n`); - } + const data = JSON.stringify({ + count: seenPeers.size, + direct: swarm.connections.size, + id: MY_ID, + }); + + for (const client of sseClients) { + client.write(`data: ${data}\n\n`); + } } const swarm = new Hyperswarm(); -swarm.on('connection', (socket) => { +swarm.on("connection", (socket) => { + const sig = crypto + .sign(null, Buffer.from(`seq:${mySeq}`), privateKey) + .toString("hex"); + const hello = JSON.stringify({ + type: "HEARTBEAT", + id: MY_ID, + seq: mySeq, + hops: 0, + nonce: MY_NONCE, + sig, + }); + socket.write(hello); + broadcastUpdate(); - const hello = JSON.stringify({ type: 'HEARTBEAT', id: MY_ID, seq: mySeq, hops: 0 }); - socket.write(hello); - broadcastUpdate(); + socket.on("data", (data) => { + try { + const msgs = data + .toString() + .split("\n") + .filter((x) => x.trim()); + for (const msgStr of msgs) { + const msg = JSON.parse(msgStr); + handleMessage(msg, socket); + } + } catch (e) { + // console.error('Invalid message', e); + } + }); - socket.on('data', (data) => { - try { - const msgs = data.toString().split('\n').filter(x => x.trim()); - for (const msgStr of msgs) { - const msg = JSON.parse(msgStr); - handleMessage(msg, socket); - } - } catch (e) { - // console.error('Invalid message', e); - } - }); + socket.on("close", () => { + if (socket.peerId && seenPeers.has(socket.peerId)) { + seenPeers.delete(socket.peerId); + } + broadcastUpdate(); + }); - socket.on('close', () => { - if (socket.peerId && seenPeers.has(socket.peerId)) { - seenPeers.delete(socket.peerId); - } - broadcastUpdate(); - }); - - socket.on('error', () => {}); + socket.on("error", () => {}); }); const discovery = swarm.join(TOPIC); discovery.flushed().then(() => { - console.log('[P2P] Joined topic:', TOPIC_NAME); + console.log("[P2P] Joined topic:", TOPIC_NAME); }); function handleMessage(msg, sourceSocket) { - if (msg.type === 'HEARTBEAT') { - const { id, seq, hops } = msg; - - if (hops === 0) { - sourceSocket.peerId = id; - } - - const now = Date.now(); - const stored = seenPeers.get(id); - - - let shouldUpdate = false; - - if (!stored) { - // New peer - shouldUpdate = true; - } else if (seq > stored.seq) { - shouldUpdate = true; - } - - if (shouldUpdate) { - const wasNew = !stored; - seenPeers.set(id, { seq, lastSeen: now }); - - if (wasNew) broadcastUpdate(); - - if (hops < 3) { - relayMessage({ ...msg, hops: hops + 1 }, sourceSocket); - } - } - } else if (msg.type === 'LEAVE') { - const { id, hops } = msg; - if (seenPeers.has(id)) { - seenPeers.delete(id); - broadcastUpdate(); - - if (hops < 3) { - relayMessage({ ...msg, hops: hops + 1 }, sourceSocket); - } - } + if (msg.type === "HEARTBEAT") { + const { id, seq, hops, nonce, sig } = msg; + + // 1. Verify PoW + if (!nonce) return; + const powHash = crypto + .createHash("sha256") + .update(id + nonce) + .digest("hex"); + if (!powHash.startsWith(POW_PREFIX)) return; // Invalid PoW + + // 2. Verify Signature + if (!sig) return; + try { + const key = crypto.createPublicKey({ + key: Buffer.from(id, "hex"), + format: "der", + type: "spki", + }); + const verified = crypto.verify( + null, + Buffer.from(`seq:${seq}`), + key, + Buffer.from(sig, "hex") + ); + if (!verified) return; // Invalid Signature + } catch (e) { + return; } + + if (hops === 0) { + sourceSocket.peerId = id; + } + + const now = Date.now(); + const stored = seenPeers.get(id); + + let shouldUpdate = false; + + if (!stored) { + // New peer + shouldUpdate = true; + } else if (seq > stored.seq) { + shouldUpdate = true; + } + + if (shouldUpdate) { + const wasNew = !stored; + seenPeers.set(id, { seq, lastSeen: now }); + + if (wasNew) broadcastUpdate(); + + if (hops < 3) { + relayMessage({ ...msg, hops: hops + 1 }, sourceSocket); + } + } + } else if (msg.type === "LEAVE") { + const { id, hops } = msg; + if (seenPeers.has(id)) { + seenPeers.delete(id); + broadcastUpdate(); + + if (hops < 3) { + relayMessage({ ...msg, hops: hops + 1 }, sourceSocket); + } + } + } } function relayMessage(msg, sourceSocket) { - const data = JSON.stringify(msg) + '\n'; - for (const socket of swarm.connections) { - if (socket !== sourceSocket) { - socket.write(data); - } + const data = JSON.stringify(msg) + "\n"; + for (const socket of swarm.connections) { + if (socket !== sourceSocket) { + socket.write(data); } + } } // Periodic Heartbeat setInterval(() => { - mySeq++; - - seenPeers.set(MY_ID, { seq: mySeq, lastSeen: Date.now() }); + mySeq++; - const heartbeat = JSON.stringify({ type: 'HEARTBEAT', id: MY_ID, seq: mySeq, hops: 0 }) + '\n'; - for (const socket of swarm.connections) { - socket.write(heartbeat); - } + seenPeers.set(MY_ID, { seq: mySeq, lastSeen: Date.now() }); - const now = Date.now(); - let changed = false; - for (const [id, data] of seenPeers) { - if (now - data.lastSeen > 2500) { - seenPeers.delete(id); - changed = true; - } + const sig = crypto + .sign(null, Buffer.from(`seq:${mySeq}`), privateKey) + .toString("hex"); + const heartbeat = + JSON.stringify({ + type: "HEARTBEAT", + id: MY_ID, + seq: mySeq, + hops: 0, + nonce: MY_NONCE, + sig, + }) + "\n"; + for (const socket of swarm.connections) { + socket.write(heartbeat); + } + + const now = Date.now(); + let changed = false; + for (const [id, data] of seenPeers) { + if (now - data.lastSeen > 2500) { + seenPeers.delete(id); + changed = true; } - - if (changed) broadcastUpdate(); - + } + + if (changed) broadcastUpdate(); }, 500); // Graceful Shutdown function handleShutdown() { - console.log('[P2P] Shutting down, sending goodbye...'); - const goodbye = JSON.stringify({ type: 'LEAVE', id: MY_ID, hops: 0 }) + '\n'; - for (const socket of swarm.connections) { - socket.write(goodbye); - } - - setTimeout(() => { - process.exit(0); - }, 500); + console.log("[P2P] Shutting down, sending goodbye..."); + const goodbye = JSON.stringify({ type: "LEAVE", id: MY_ID, hops: 0 }) + "\n"; + for (const socket of swarm.connections) { + socket.write(goodbye); + } + + setTimeout(() => { + process.exit(0); + }, 500); } -process.on('SIGINT', handleShutdown); -process.on('SIGTERM', handleShutdown); +process.on("SIGINT", handleShutdown); +process.on("SIGTERM", handleShutdown); // --- WEB SERVER --- -app.get('/', (req, res) => { - const count = seenPeers.size; - const directPeers = swarm.connections.size; - - res.send(` +app.get("/", (req, res) => { + const count = seenPeers.size; + const directPeers = swarm.connections.size; + + res.send(` @@ -324,35 +392,35 @@ app.get('/', (req, res) => { }); // SSE Endpoint -app.get('/events', (req, res) => { - res.setHeader('Content-Type', 'text/event-stream'); - res.setHeader('Cache-Control', 'no-cache'); - res.setHeader('Connection', 'keep-alive'); - res.flushHeaders(); +app.get("/events", (req, res) => { + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + res.flushHeaders(); - sseClients.add(res); + sseClients.add(res); - const data = JSON.stringify({ - count: seenPeers.size, - direct: swarm.connections.size, - id: MY_ID - }); - res.write(`data: ${data}\n\n`); + const data = JSON.stringify({ + count: seenPeers.size, + direct: swarm.connections.size, + id: MY_ID, + }); + res.write(`data: ${data}\n\n`); - req.on('close', () => { - sseClients.delete(res); - }); + req.on("close", () => { + sseClients.delete(res); + }); }); -app.get('/api/stats', (req, res) => { - res.json({ - count: seenPeers.size, - direct: swarm.connections.size, - id: MY_ID - }); +app.get("/api/stats", (req, res) => { + res.json({ + count: seenPeers.size, + direct: swarm.connections.size, + id: MY_ID, + }); }); app.listen(PORT, () => { - console.log(`Hypermind Node running on port ${PORT}`); - console.log(`ID: ${MY_ID}`); + console.log(`Hypermind Node running on port ${PORT}`); + console.log(`ID: ${MY_ID}`); });