UNPKG

melq

Version:

Quantum-secure chat network with ML-KEM-768 encryption and host-based architecture

325 lines (276 loc) 8.63 kB
import WebSocket from 'ws'; import crypto from 'crypto'; import { MLKEM } from '../crypto/mlkem.js'; import { AESCrypto } from '../crypto/aes.js'; export class P2PNode { constructor(coordinatorUrl) { this.nodeId = this.generateNodeId(); this.coordinatorUrl = coordinatorUrl; this.mlkem = new MLKEM(); this.aesCrypto = new AESCrypto(); this.keyPair = null; // Will be generated async this.peerKeys = new Map(); // nodeId -> shared secret this.chats = new Map(); // chatId -> chat info this.messageHandlers = new Map(); this.ws = null; this.cliInterface = null; this.heartbeatInterval = null; } generateNodeId() { return `node_${crypto.randomBytes(8).toString('hex')}`; } async connect() { // Generate genuine ML-KEM-768 keypair first console.log('Generating ML-KEM-768 keypair...'); this.keyPair = await this.mlkem.generateKeyPair(); console.log('✓ Post-quantum cryptographic keys generated'); return new Promise((resolve, reject) => { this.ws = new WebSocket(this.coordinatorUrl); this.ws.on('open', () => { console.log(`Connected to coordinator as ${this.nodeId}`); this.register(); this.startHeartbeat(); resolve(); }); this.ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); this.handleMessage(message); } catch (error) { console.error('Failed to parse message:', error); } }); this.ws.on('close', (code, reason) => { console.log(`Disconnected from coordinator (code: ${code}, reason: ${reason})`); this.stopHeartbeat(); if (this.cliInterface) this.cliInterface.rl.prompt(); }); this.ws.on('error', (error) => { console.error('Connection error:', error); if (this.cliInterface) this.cliInterface.rl.prompt(); reject(error); }); }); } register() { this.send({ type: 'register', nodeId: this.nodeId, publicKey: this.keyPair.publicKey, address: `ws://localhost:${Math.floor(Math.random() * 10000) + 8000}` }); } send(message) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(message)); } } handleMessage(message) { switch (message.type) { case 'registered': console.log(`Registered successfully as ${message.nodeId}`); this.getChats(); if (this.cliInterface) this.cliInterface.rl.prompt(); break; case 'node_list': this.handleNodeList(message.nodes); break; case 'chat_created': console.log(`Chat created: ${message.chatName} (ID: ${message.chatId})`); this.chats.set(message.chatId, { id: message.chatId, name: message.chatName, participants: [this.nodeId] }); if (this.cliInterface) this.cliInterface.rl.prompt(); break; case 'chat_available': console.log(`New chat available: ${message.chatName}`); this.chats.set(message.chatId, { id: message.chatId, name: message.chatName, participants: [message.creator] }); if (this.cliInterface) this.cliInterface.rl.prompt(); break; case 'user_joined': if (this.chats.has(message.chatId)) { console.log(`User ${message.nodeId} joined chat`); } break; case 'encrypted_message': this.handleEncryptedMessage(message); break; case 'key_exchange_request': this.handleKeyExchangeRequest(message).catch(console.error); break; case 'key_exchange_response': this.handleKeyExchangeResponse(message); break; case 'chat_list': this.handleChatList(message.chats); break; case 'pong': // Heartbeat response - no action needed break; default: console.log(`Unknown message type: ${message.type}`, message); } } handleNodeList(nodes) { console.log(`Discovered ${nodes.length} nodes`); nodes.forEach(async node => { if (!this.peerKeys.has(node.nodeId)) { await this.initiateKeyExchange(node); } }); if (this.cliInterface) this.cliInterface.rl.prompt(); } async initiateKeyExchange(peerNode) { const { ciphertext, sharedSecret } = await this.mlkem.encapsulate(peerNode.publicKey); this.peerKeys.set(peerNode.nodeId, sharedSecret); this.send({ type: 'relay_message', targetNodeId: peerNode.nodeId, fromNodeId: this.nodeId, messageType: 'key_exchange_request', ciphertext: ciphertext }); } async handleKeyExchangeRequest(message) { const sharedSecret = await this.mlkem.decapsulate(message.ciphertext, this.keyPair.privateKey); this.peerKeys.set(message.fromNodeId, sharedSecret); this.send({ type: 'relay_message', targetNodeId: message.fromNodeId, fromNodeId: this.nodeId, messageType: 'key_exchange_response', acknowledged: true }); } handleKeyExchangeResponse(message) { console.log(`Key exchange completed with ${message.fromNodeId}`); } handleRelayedMessage(message) { switch (message.messageType) { case 'key_exchange_request': this.handleKeyExchangeRequest({ fromNodeId: message.fromNodeId, ciphertext: message.ciphertext }).catch(console.error); break; case 'key_exchange_response': this.handleKeyExchangeResponse({ fromNodeId: message.fromNodeId, acknowledged: message.acknowledged }); break; default: if (message.encryptedData) { this.handleEncryptedMessage({ fromNodeId: message.fromNodeId, chatId: message.chatId, encryptedData: message.encryptedData }); } } } async sendMessage(chatId, messageText, targetNodeId) { if (!this.peerKeys.has(targetNodeId)) { console.error('No shared key with target node'); return; } const sharedSecret = this.peerKeys.get(targetNodeId); const encryptionKey = this.aesCrypto.deriveKeyFromSharedSecret(sharedSecret); const messageData = { chatId, text: messageText, timestamp: Date.now(), fromNodeId: this.nodeId }; const encryptedData = this.aesCrypto.encrypt(JSON.stringify(messageData), encryptionKey); this.send({ type: 'relay_message', targetNodeId, fromNodeId: this.nodeId, chatId, encryptedData }); } handleEncryptedMessage(message) { try { if (!this.peerKeys.has(message.fromNodeId)) { console.error('Received message from unknown node'); return; } const sharedSecret = this.peerKeys.get(message.fromNodeId); const decryptionKey = this.aesCrypto.deriveKeyFromSharedSecret(sharedSecret); const decryptedText = this.aesCrypto.decrypt(message.encryptedData, decryptionKey); const messageData = JSON.parse(decryptedText); if (this.messageHandlers.has('message')) { this.messageHandlers.get('message')(messageData); } } catch (error) { console.error('Failed to decrypt message:', error); } } onMessage(handler) { this.messageHandlers.set('message', handler); } createChat(chatName) { this.send({ type: 'create_chat', nodeId: this.nodeId, chatName }); } joinChat(chatId) { this.send({ type: 'join_chat', nodeId: this.nodeId, chatId }); } discoverNodes() { this.send({ type: 'discover_nodes', nodeId: this.nodeId }); } getChats() { this.send({ type: 'get_chats', nodeId: this.nodeId }); } handleChatList(chats) { chats.forEach(chat => { if (!this.chats.has(chat.chatId)) { this.chats.set(chat.chatId, { id: chat.chatId, name: chat.chatName, participants: chat.participants }); } }); } startHeartbeat() { this.heartbeatInterval = setInterval(() => { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.send({ type: 'ping', nodeId: this.nodeId }); } }, 30000); // Send ping every 30 seconds } stopHeartbeat() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; } } disconnect() { this.stopHeartbeat(); if (this.ws) { this.ws.close(); } } }