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); } } }