mirror of
https://github.com/lklynet/hypermind.git
synced 2026-05-03 17:40:29 +00:00
feat(security): add PoW and Ed25519 signatures to prevent Sybil attacks
This commit is contained in:
@@ -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}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user