143 lines
4.4 KiB
JavaScript
143 lines
4.4 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
}
|