import { socket, game } from "./main.js"; import { Packet } from "./packet.js"; class RandomSession { constructor(range) { this.range = range; this.cipherTexts = {}; this.cipherKeys = {}; this.ourKey = CryptoJS.lib.WordArray.random(32).toString(); this.ourNoise = CryptoJS.lib.WordArray.random(8); this.finalValue = null; this.resolvers = []; } cipherText() { return CryptoJS.AES.encrypt(this.ourNoise, this.ourKey).toString(); } } export class Random { constructor() { this.sessions = {}; } async get(n, sessionId) { if (this.sessions[sessionId] === undefined) { this.initialiseSession(n, sessionId); } let promise; await navigator.locks.request(`random-${sessionId}`, () => { if (this.sessions[sessionId].finalValue === null) { let session = this.sessions[sessionId]; let resolver; promise = new Promise((resolve) => { resolver = resolve; }); session.resolvers.push(resolver); } else { promise = new Promise((resolve) => { resolve(this.sessions[sessionId].finalValue); }); } }); return promise; } /** * Start a cooperative random session. */ initialiseSession(n, sessionId) { let session = new RandomSession(n); if (sessionId === undefined) { sessionId = window.crypto.randomUUID(); } this.sessions[sessionId] = session; socket.emit( "message", Packet.createRandomCyphertext(sessionId, n, session.cipherText()) ); return session; } /** * Process cooperative random protocol. * * @param data Packet received */ async processCooperativeRandom(data) { // Step 0: extract relevant information from data let session = this.sessions[data.session]; const stage = data.stage; if (session === undefined) { session = this.initialiseSession(data.range, data.session); } if (stage === "CIPHERTEXT") { session.cipherTexts[data.author] = data.cipherText; if ( Object.keys(session.cipherTexts).length === Object.keys(game.players).length - 1 ) { // Step 3: release our key once all players have sent a ciphertext socket.emit( "message", Packet.createRandomKey(data.session, 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(game.players).length - 1 ) { // Lock out wait calls as they may resolve to never-ending promises. await navigator.locks.request(`random-${data.session}`, () => { let total = BigInt("0x" + session.ourNoise.toString()); for (let participant of Object.keys(session.cipherKeys)) { let decrypted = CryptoJS.AES.decrypt( session.cipherTexts[participant], session.cipherKeys[participant] ).toString(); total += BigInt("0x" + decrypted); } // Find first good block of bits to avoid modular bias let blockSize = BigInt(Math.ceil(Math.log2(session.range))); let blockMask = 2n ** blockSize - 1n; while ((total & blockMask) >= BigInt(session.range)) { total >>= blockSize; } session.finalValue = total & blockMask; this.resolve(data.session); }); } } } /** * Resolve a session by calling any callbacks associated with the session. * * @param sessionId */ resolve(sessionId) { const session = this.sessions[sessionId]; for (let resolve of session.resolvers) { resolve(session.finalValue); } } }