diff --git a/static/js/index.js b/static/js/index.js index e9455ae..2dbb86b 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -12,6 +12,7 @@ const POST_GAME = 3; let game_state = WAITING; let socket; +let random; // Not totally reliable but better than nothing. window.addEventListener("beforeunload", () => { @@ -25,6 +26,7 @@ window.addEventListener("beforeunload", () => { document.addEventListener("DOMContentLoaded", () => { socket = io(); + random = new Random(); socket.on("connect", () => { console.log("Connected!"); @@ -38,7 +40,7 @@ document.addEventListener("DOMContentLoaded", () => { players[ID] = new Player(ID, name); }); - socket.on("message", (data) => { + socket.on("message", async (data) => { // Ignore any messages that originate from us. if (data.author === ID) { return; @@ -58,11 +60,11 @@ document.addEventListener("DOMContentLoaded", () => { break; case "SYNC": - sync(data); + await sync(data); break; case "RANDOM": - processCooperativeRandom(data); + random.processCooperativeRandom(data); break; } }); @@ -124,15 +126,21 @@ function keepAlive(data) { * * @param data Packet received */ -function sync(data) { +async 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. + if (ID === Array.min(...Object.keys(players))) { + random.coopRandom(Object.keys(players).length, "first-player"); + } - // + // Wait for value to populate + let player1 = await random.get("first-player"); + + console.log(player1); } } @@ -145,106 +153,3 @@ function allPlayersReady() { 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; - } - } -} diff --git a/static/js/random.js b/static/js/random.js new file mode 100644 index 0000000..6b6fd10 --- /dev/null +++ b/static/js/random.js @@ -0,0 +1,151 @@ +class RandomSession { + constructor(range) { + this.range = range; + this.cipherTexts = {}; + this.cipherKeys = {}; + this.ourKey = CryptoJS.lib.WordArray.random(32).toString(); + // 32-bit as JavaScript does funny stuff at 64-bit levels. + this.ourNoise = CryptoJS.lib.WordArray.random(4).toString(); + this.finalValue = null; + this.resolvers = []; + } + + cipherText() { + return CryptoJS.AES.encrypt(this.ourNoise, this.ourKey).toString(); + } +} + +class Random { + constructor() { + this.locked = false; + this.sessions = {}; + } + + get(sessionId) { + // Spin until lock frees. + while (this.locked); + + if (this.sessions[sessionId].finalValue === null) { + let session = this.sessions[sessionId]; + let resolver; + let promise = new Promise((resolve) => { + resolver = resolve; + }); + session.resolvers.push(resolver); + return promise; + } else { + return new Promise((resolve) => { + resolve(this.sessions[sessionId].finalValue); + }); + } + } + + /** + * Start a cooperative random session. + */ + coopRandom(n, sessionId) { + let session = new RandomSession(n); + if (sessionId === undefined) { + sessionId = window.crypto.randomUUID(); + } + + this.sessions[sessionId] = session; + + socket.emit("message", { + type: "RANDOM", + author: ID, + session: sessionId, + range: n, + stage: "CIPHERTEXT", + cipherText: session.cipherText(), + }); + + return sessionId; + } + + /** + * Process cooperative random protocol. + * + * @param data Packet received + */ + processCooperativeRandom(data) { + // Step 0: extract relevant information from data + let session = this.sessions[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}`); + + session = new RandomSession(data.range); + this.sessions[data.session] = 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: session.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 + ) { + // Lock out wait calls as they may resolve to never-ending promises. + this.locked = true; + + 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; + + this.resolve(data.session); + + this.locked = false; + } + } + } + + /** + * Resolve a session by calling any callbacks associated with the session and then deleting it. + * + * @param sessionId + */ + resolve(sessionId) { + const session = this.sessions[sessionId]; + for (let resolve of session.resolvers) { + resolve(session.finalValue); + } + } +} diff --git a/templates/index.html b/templates/index.html index 51b8e2e..1709b74 100644 --- a/templates/index.html +++ b/templates/index.html @@ -10,6 +10,7 @@ +