feat(security): add PoW and Ed25519 signatures to prevent Sybil attacks

This commit is contained in:
lklynet
2026-01-02 11:05:44 -05:00
parent 149298016e
commit 81864969da
+209 -141
View File
@@ -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(`
<!DOCTYPE html>
<html>
<head>
@@ -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}`);
});