diff --git a/src/lib/transport/Connection.mjs b/src/lib/transport/Connection.mjs index e2b288a..2224337 100644 --- a/src/lib/transport/Connection.mjs +++ b/src/lib/transport/Connection.mjs @@ -1,5 +1,6 @@ "use strict"; +import crypto from 'crypto'; import net from 'net'; import { EventEmitter, once } from 'events'; @@ -21,7 +22,10 @@ class Connection extends EventEmitter { this.socket = socket; this.rekey_last = null; - this.rekey_interval = 30 * 60 * 1000; // 30 minutes + this.rekey_interval_base = 30 * 60 * 1000; // 30 minutes + this.rekey_interval = this.rekey_interval_base + crypto.randomInt(0, 15 * 60 * 1000); + this.rekey_in_progress = false; + this.session_key = Buffer.from(secret_join, "base64"); } @@ -54,8 +58,20 @@ class Connection extends EventEmitter { } async rekey() { - this.rekey_last = new Date(); - this.session_key = await rekey(this.socket, this.session_key); + try { + this.rekey_in_progress = true; + this.session_key = await rekey(this, this.session_key); + this.rekey_interval = this.rekey_interval_base + crypto.randomInt(0, 15 * 60 * 1000); + this.rekey_last = new Date(); + this.emit("rekey"); + } + catch(error) { + l.warn(`Error when rekeying connection ${this.address}:${this.port}: ${settings.cli.verbose ? error : error.message}, killing connection`); + await this.destroy(); + } + finally { + this.rekey_in_progress = false; + } } async destroy() { @@ -79,15 +95,25 @@ class Connection extends EventEmitter { async handle_message(msg_text) { const msg = JSON.parse(msg_text); - this.emit("message", msg); + + if(msg.event == "rekey") { + // Set and forget here + if(!this.rekey_in_progress) this.rekey(); + } + this.emit("message", msg.event, msg.message); + this.emit(`message-${msg.event}`, msg.message); } - async send(message) { - if(new Date() - this.rekey_last > this.rekey_interval) + async send(event, message) { + if(typeof event !== "string") throw new Error(`Error: Expected string for event name, but got value of type ${typeof event}.`); + + // Rekey at semi-regular intervals, but only if we're not already in the process of doing so + if(new Date() - this.rekey_last > this.rekey_interval && !this.rekey_in_progress) await this.rekey(); + // TODO: Consider anonymous TLS, with jpake for mututal authentication // TODO: Consider https://devdocs.io/node/crypto#crypto.createCipheriv() - which lets us use any openssl ciphers we like - e.g. ChaCha20-Poly1305 - let payload = JSON.stringify(message); + let payload = JSON.stringify({ event, message }); payload = encrypt_bytes(this.session_key, payload); diff --git a/src/lib/transport/rekey.mjs b/src/lib/transport/rekey.mjs index 1593822..49c558b 100644 --- a/src/lib/transport/rekey.mjs +++ b/src/lib/transport/rekey.mjs @@ -10,20 +10,25 @@ export default async function rekey(connection, secret_join) { let jpake = new JPake(secret_join); // 1: Round 1 - connection.send("jpake-rekey-r1", jpake.GetRound1Message()); + connection.send("rekey", { round: 1, content: jpake.GetRound1Message() }); // 2: Round 2 - const their_round1 = (await once(connection, "message"))[0]; - if(typeof their_round1 !== "string") return null; - const our_round2 = jpake.GetRound2Message(their_round1); - if(typeof our_round2 !== "string") return null; + let [ event, their_round1 ] = (await once(connection, "message-rekey")); - connection.send("jpake-rekey-r2", our_round2); + if(typeof their_round1 !== "object" + || event !== "rekey" + || their_round1.round !== 1 + || typeof their_round1.content !== "string") + throw new Error(`Error: Received invalid round 1 from peer`); + const our_round2 = jpake.GetRound2Message(their_round1.content); + if(typeof our_round2 !== "string") throw new Error(`Error: Failed to compute rekey round 2`); + + connection.send("rekey", { round: 2, content: our_round2 }); // 3: Compute new shared key - const their_round2 = (await once(connection, "message"))[0]; + const their_round2 = (await once(connection, "message-rekey"))[0]; if(typeof their_round2 !== "string") return null; const new_shared_key = jpake.ComputeSharedKey(their_round2); if(typeof new_shared_key !== "string") return null;