const ID = window.crypto.randomUUID(); // Timeout to consider a player disconnected const TIMEOUT = 30_000; let players = {}; const WAITING = 0; const PRE_GAME = 1; const PLAYING = 2; const POST_GAME = 3; let game_state = WAITING; let socket; // Not totally reliable but better than nothing. window.addEventListener("beforeunload", () => { socket.emit("message", { type: "DISCONNECT", id: window.crypto.randomUUID(), author: ID, name: "", }); }); document.addEventListener("DOMContentLoaded", () => { socket = io(); socket.on("connect", () => { console.log("Connected!"); socket.emit("message", { type: "ANNOUNCE", id: window.crypto.randomUUID(), author: ID, name: "", }); // Create self players[ID] = new Player(ID, name); }); socket.on("message", (data) => { // Ignore any messages that originate from us. if (data.author === ID) { return; } switch (data.type) { case "ANNOUNCE": playerConnected(data); break; case "DISCONNECT": playerDisconnected(data); break; case "KEEPALIVE": keepAlive(data); break; case "SYNC": sync(data); break; case "RANDOM": processCooperativeRandom(data); break; } }); // Emit keepalive messages to inform other players we are still here window.setInterval(() => { socket.emit("message", { type: "KEEPALIVE", id: window.crypto.randomUUID(), author: ID, }); }, TIMEOUT / 5); }); /** * Process player connect packets: these inform that a new player has joined. * * @param data Packet received */ function playerConnected(data) { // Block players from joining mid-game if (game_state !== WAITING) { return; } // When a new player is seen, all announce to ensure they know all players. if (players[data.author] === undefined) { players[data.author] = new Player(data.author, data.name); socket.emit("message", { type: "ANNOUNCE", id: window.crypto.randomUUID(), author: ID, name: "", }); players[data.author].resetTimeout(); } else { } updatePlayerDom(); } function playerDisconnected(data) { console.log("deleting player"); delete players[data.author]; updatePlayerDom(); } /** * Process keep-alive packets: these are packets that check players are still online. * * @param data Packet received */ function keepAlive(data) { players[data.author].resetTimeout(); } /** * Process sync packets: update player details like status and name. * * @param data Packet received */ function sync(data) { players[data.author].name = data.name; players[data.author].ready = data.ready; if (allPlayersReady()) { game_state = PRE_GAME; // Decide turn order. "Master" begins a cooperative rng process. // } } function allPlayersReady() { for (let player of Object.values(players)) { if (!player.ready) { return false; } } return true; } // Track ongoing random sessions. const randomSessions = {}; /** * Start a cooperative random session. */ function coopRandom(n) { const sessionId = window.crypto.randomUUID(); const noise = CryptoJS.lib.WordArray.random(8).toString(); const key = CryptoJS.lib.WordArray.random(32).toString(); const cipherText = CryptoJS.AES.encrypt(noise, key).toString(); randomSessions[sessionId] = { range: n, cipherTexts: {}, cipherKeys: {}, ourKey: key, ourNoise: noise, finalValue: -1, }; socket.emit("message", { type: "RANDOM", author: ID, session: sessionId, range: n, stage: "CIPHERTEXT", cipherText: cipherText, }); } /** * Process cooperative random protocol. * * @param data Packet received */ function processCooperativeRandom(data) { // Step 0: extract relevant information from data let session = randomSessions[data.session]; const stage = data.stage; if (session === undefined) { // Step 1: generate and encrypt our random value. 8 bytes = 64 bit integer const noise = CryptoJS.lib.WordArray.random(8).toString(); console.log(`our noise: ${noise}`); const key = CryptoJS.lib.WordArray.random(32).toString(); const cipherText = CryptoJS.AES.encrypt(noise, key).toString(); randomSessions[data.session] = { range: data.range, cipherTexts: {}, cipherKeys: {}, ourKey: key, ourNoise: noise, finalValue: -1, }; session = randomSessions[data.session]; // Step 2: send our random value and wait for all responses socket.emit("message", { type: "RANDOM", author: ID, session: data.session, stage: "CIPHERTEXT", range: data.range, cipherText: cipherText, }); } if (stage === "CIPHERTEXT") { session.cipherTexts[data.author] = data.cipherText; if (Object.keys(session.cipherTexts).length === Object.keys(players).length - 1) { // Step 3: release our key once all players have sent a ciphertext socket.emit("message", { type: "RANDOM", author: ID, session: data.session, stage: "DECRYPT", cipherKey: session.ourKey, }); } } else if (stage === "DECRYPT") { session.cipherKeys[data.author] = data.cipherKey; // Step 4: get final random value if (Object.keys(session.cipherKeys).length === Object.keys(players).length - 1) { let total = 0; for (let participant of Object.keys(session.cipherKeys)) { total += CryptoJS.AES.decrypt( session.cipherTexts[participant], session.cipherKeys[participant] ); } session.finalValue = total % session.range; } } }