Riskless/static/js/random.js
2023-02-11 14:59:24 +00:00

140 lines
4.2 KiB
JavaScript

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 53-bit levels.
this.ourNoise = CryptoJS.lib.WordArray.random(4);
this.finalValue = null;
this.resolvers = [];
}
cipherText() {
return CryptoJS.AES.encrypt(this.ourNoise, this.ourKey).toString();
}
}
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", {
type: "RANDOM",
author: ID,
session: sessionId,
range: n,
stage: "CIPHERTEXT",
cipherText: 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(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.
await navigator.locks.request(`random-${data.session}`, () => {
let total = parseInt(session.ourNoise, 16);
for (let participant of Object.keys(session.cipherKeys)) {
let decrypted = CryptoJS.AES.decrypt(
session.cipherTexts[participant],
session.cipherKeys[participant]
).toString();
total += parseInt(decrypted, 16);
}
session.finalValue = total % session.range;
this.resolve(data.session);
});
}
}
}
/**
* 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);
}
}
}