Mid refactor to stop polluting the namespace so much

This commit is contained in:
jude
2023-03-03 17:34:15 +00:00
parent 7d9531f0d9
commit cf0c9135e1
15 changed files with 349 additions and 292 deletions

View File

@ -0,0 +1,2 @@
import { generate_keypair } from "./paillier.js";
export { generate_keypair };

View File

@ -0,0 +1,14 @@
export function mod_exp(a, b, n) {
let res = 1n;
while (b > 0n) {
if (b % 2n === 1n) {
res = (res * a) % n;
}
b >>= 1n;
a = (a * a) % n;
}
return res;
}

View File

@ -0,0 +1,61 @@
import { random2048, generate_prime } from "./random_primes.js";
import { mod_exp } from "./math.js";
let p, q, pubKey, privKey;
class PubKey {
constructor(p, q) {
this.n = p * q;
// this.g = this.n + 1n;
}
encrypt(m) {
// Compute g^m r^n mod n^2
let r = random2048();
// Resample to avoid modulo bias.
while (r >= this.n) {
r = random2048();
}
// Compute g^m by binomial theorem.
let gm = (1n + this.n * m) % this.n ** 2n;
// Compute g^m r^n from crt
return (gm * mod_exp(r, this.n, this.n ** 2n)) % this.n ** 2n;
}
}
class PrivKey {
constructor(p, q) {
this.n = p * q;
this.lambda = (p - 1n) * (q - 1n);
this.mu = mod_exp(this.lambda, this.lambda - 1n, this.n);
}
decrypt(c) {
return (
(((mod_exp(c, this.lambda, this.n ** 2n) - 1n) / this.n) * this.mu) % this.n
);
}
}
export function generate_keypair() {
if (window.sessionStorage.getItem("p") === null) {
p = generate_prime();
window.sessionStorage.setItem("p", p);
} else {
p = BigInt(window.sessionStorage.getItem("p"));
}
if (window.sessionStorage.getItem("q") === null) {
q = generate_prime();
window.sessionStorage.setItem("q", q);
} else {
q = BigInt(window.sessionStorage.getItem("q"));
}
pubKey = new PubKey(p, q);
privKey = new PrivKey(p, q);
return { pubKey, privKey };
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
import { socket, players } from "./main.js";
import { Packet } from "./packet.js";
/**
* Typical barrier type.
*
* Block all clients until everyone has hit the barrier.
*/
export class Barrier {
constructor() {
let resolver;
this.promise = new Promise((resolve) => {
resolver = resolve;
});
this.resolver = resolver;
this.hits = new Set();
}
wait() {
socket.emit("message", Packet.createBarrierSignal());
return this.promise;
}
resolve(data) {
this.hits.add(data.author);
if (this.hits.size === Object.keys(players).length - 1) {
this.hits = new Set();
this.resolver();
}
}
}

View File

@ -0,0 +1,240 @@
import {
gameState,
WAITING,
PRE_GAME,
PLAYING,
us,
socket,
players,
ID,
allPlayersReady,
startPregame,
} from "./main.js";
import { Region } from "./map.js";
import { Packet } from "./packet.js";
export function unlockMapDom() {
Object.values(REGIONS).forEach((region) => {
if (!allRegionsClaimed() && region.owner === null) {
document
.querySelector(`.node[data-name=${region.name}] .actions`)
.classList.remove("hidden");
} else if (region.owner === us) {
document
.querySelector(`.node[data-name=${region.name}] .actions`)
.classList.remove("hidden");
}
});
}
export function lockMapDom() {
document.querySelectorAll(".actions").forEach((e) => e.classList.add("hidden"));
}
export function updateDom() {
if (gameState !== WAITING) {
document.querySelector("#ready-button").style.display = "none";
}
updatePlayerDom();
showRemainingReinforcements();
updateMapDom();
}
function updateMapDom() {
if (us.isPlaying) {
unlockMapDom();
} else {
lockMapDom();
}
if (gameState === PRE_GAME) {
document.querySelectorAll(".fortify, .attack").forEach((e) => {
e.classList.add("hidden");
});
} else if (gameState === PLAYING) {
document.querySelectorAll(".node button").forEach((e) => {
e.classList.remove("hidden");
});
if (us.isPlaying) {
document.querySelector("#end-turn").classList.remove("hidden");
} else {
document.querySelector("#end-turn").classList.add("hidden");
}
}
for (let region of Region.getAllRegions()) {
const element = document.querySelector(`.node[data-name=${region.name}]`);
element.querySelector(".strength").textContent = region.strength || "";
element.style.backgroundColor =
region.owner === null ? "white" : region.owner.getColor();
}
}
function updatePlayerDom() {
let list = document.querySelector("#playerList");
list.replaceChildren();
for (let playerId of Object.keys(players).sort()) {
let player = players[playerId];
let statusSpan = document.createElement("div");
statusSpan.classList.add("status-span");
if (gameState === WAITING) {
if (player.ready) {
statusSpan.textContent = "R";
statusSpan.classList.add("ready");
} else {
statusSpan.textContent = "N";
statusSpan.classList.add("not-ready");
}
} else {
if (player.isPlaying) {
statusSpan.textContent = "P";
}
}
let idSpan = document.createElement("span");
idSpan.style.color = player.getColor();
if (playerId === ID) {
idSpan.textContent = `${playerId} (you)`;
} else {
idSpan.textContent = playerId;
}
let newDom = document.createElement("li");
newDom.appendChild(statusSpan);
newDom.appendChild(idSpan);
list.appendChild(newDom);
}
}
document.addEventListener("DOMContentLoaded", () => {
document.querySelector("#ready-button").addEventListener("click", async (ev) => {
let nowReady = ev.target.textContent === "Not ready";
us.ready = nowReady;
ev.target.classList.toggle("active");
ev.target.textContent = nowReady ? "Ready" : "Not ready";
socket.emit("message", Packet.createSetReady(nowReady));
updatePlayerDom();
if (allPlayersReady()) {
await startPregame();
}
});
document.querySelector("#end-turn").addEventListener("click", async (ev) => {
socket.emit("message", Packet.createEndTurn());
});
document.querySelectorAll(".reinforce").forEach((el) =>
el.addEventListener("click", (ev) => {
let region = ev.target.closest(".node").dataset.name;
socket.emit("message", Packet.createRegionClaim(region));
})
);
document.querySelectorAll(".attack").forEach((el) =>
el.addEventListener("click", (ev) => {
let modal = document.querySelector("#modal");
let region = ev.target.closest(".node").dataset["name"];
modal.dataset["region"] = ev.target.closest(".node").dataset.name;
modal.dataset["action"] = "ATTACK";
let selector = modal.querySelector(".target");
selector.replaceChildren("");
for (let neighbour of Region.getRegion(region).neighbours) {
let opt = document.createElement("option");
opt.value = neighbour.name;
opt.textContent = neighbour.name;
selector.appendChild(opt);
}
modal.classList.remove("hidden");
})
);
document.querySelectorAll(".fortify").forEach((el) =>
el.addEventListener("click", (ev) => {
let modal = document.querySelector("#modal");
let region = ev.target.closest(".node").dataset["name"];
modal.dataset["region"] = region;
modal.dataset["action"] = "FORTIFY";
let selector = modal.querySelector(".target");
selector.replaceChildren("");
for (let neighbour of Region.getRegion(region).neighbours) {
let opt = document.createElement("option");
opt.value = neighbour.name;
opt.textContent = neighbour.name;
selector.appendChild(opt);
}
modal.classList.remove("hidden");
})
);
document.querySelector("#modal .submit-modal").addEventListener("click", () => {
let modal = document.querySelector("#modal");
let startRegion = modal.dataset["region"];
let action = modal.dataset["action"];
let amount = modal.querySelector(".amount").value;
let endRegion = modal.querySelector(".target").value;
socket.emit(
"message",
Packet.createAction(action, startRegion, endRegion, amount)
);
});
document
.querySelector("#defense-modal .submit-modal")
.addEventListener("click", () => {
let modal = document.querySelector("#defense-modal");
let amount = modal.querySelector(".amount").value;
socket.emit("message", Packet.createDefense(amount));
});
document.querySelectorAll(".submit-modal, .cancel-modal").forEach((el) => {
el.addEventListener("click", () => {
el.closest(".modal").classList.add("hidden");
});
});
document.querySelector("#shuffleColors").addEventListener("click", () => {
Object.values(players).forEach((player) => {
player.resetColor();
});
updatePlayerDom();
});
});
function showRemainingReinforcements() {
if (gameState === PRE_GAME) {
document.querySelector(
"#remaining-reinforcements"
).innerHTML = `<span>Remaining placements: ${reinforcementsRemaining()}</span>`;
} else if (gameState === PLAYING) {
document.querySelector(
"#remaining-reinforcements"
).innerHTML = `<span>Remaining placements: ${
currentPlayer().reinforcementsAvailable - currentPlayer().reinforcementsPlaced
}</span>`;
}
}
function showDefenseDom(region) {
const modal = document.querySelector("#defense-modal");
modal.querySelector("span").textContent = region;
modal.classList.remove("hidden");
}

View File

@ -0,0 +1,82 @@
import { Player } from "./player";
const WAITING = 0;
const PRE_GAME = 1;
const PLAYING = 2;
export class Game {
constructor() {
this.us = null;
this.players = {};
this.state = WAITING;
}
isWaiting() {
return this.state === WAITING;
}
isPregame() {
return this.state === PRE_GAME;
}
isPlaying() {
return this.state === PLAYING;
}
incrementState() {
this.state += 1;
}
currentPlayer() {
return Object.values(this.players).filter((p) => p.isPlaying)[0];
}
addPlayer(id, name, is_us) {
let is_new = this.players[id] === undefined;
if (this.isWaiting()) {
this.players[id] = new Player(id, name, is_us);
if (is_us === true) {
this.us = this.players[id];
}
}
if (is_new) {
const event = new CustomEvent("addPlayer");
document.dispatchEvent(event);
}
return is_new;
}
removePlayer(id) {
if (this.players[id] !== undefined) {
const event = new CustomEvent("removePlayer");
document.dispatchEvent(event);
delete this.players[id];
}
}
keepAlive(id) {
if (id !== this.us.id) {
this.players[id].resetTimeout(this);
}
}
setReady(id, ready) {
this.players[id].readyState = ready;
if (this._allPlayersReady()) {
this.incrementState();
}
}
_allPlayersReady() {
for (let player of Object.values(this.players)) {
if (!player.readyState) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,135 @@
import { generate_keypair } from "../crypto/main.js";
import { Random } from "./random.js";
import { Barrier } from "./barrier.js";
import { Packet } from "./packet.js";
import { updateDom } from "./dom.js";
export const ID = window.crypto.randomUUID();
export let us = null;
export const game = new Game();
export let socket;
let random;
let barrier;
let keys;
// Not totally reliable but better than nothing.
window.addEventListener("beforeunload", () => {
socket.emit("message", Packet.createDisconnect());
});
document.addEventListener("DOMContentLoaded", () => {
socket = io();
random = new Random();
barrier = new Barrier();
keys = generate_keypair();
socket.on("connect", () => {
window.console.log("Connected!");
socket.emit("message", Packet.createAnnounce());
game.addPlayer(ID, name, true);
});
socket.on("message", async (data) => {
// todo validate signature
switch (data.type) {
case "RANDOM":
if (data.author === ID) {
return;
}
await random.processCooperativeRandom(data);
break;
case "BARRIER":
if (data.author === ID) {
return;
}
barrier.resolve(data);
break;
default:
const event = new CustomEvent(data.type, { detail: data });
document.dispatchEvent(event);
break;
}
});
});
/**
* Process player connect packets: these inform that a new player has joined.
*
* @param data Packet received
*/
document.addEventListener("ANNOUNCE", (data) => {
if (data.author === ID) return;
let is_new = game.addPlayer(data.author, data.name, false);
// When a new player is seen, all announce to ensure they know all players.
if (is_new) {
socket.emit("message", Packet.createAnnounce());
}
});
document.addEventListener("DISCONNECT", (data) => {
game.removePlayer(data.author);
});
/**
* Process keep-alive packets: these are packets that check players are still online.
*
* @param data Packet received
*/
document.addEventListener("KEEPALIVE", (data) => {
game.keepAlive(data.author);
});
document.addEventListener("ACT", async (data) => {
if (data.author !== game.currentPlayer().id) {
if (data.action === "DEFENSE") {
await game.players[data.author].setDefense(data.amount);
}
return;
}
if (game.isWaiting()) {
game.setReady(data.author, data.ready);
} else if (game.isPregame()) {
if (!Region.allRegionsClaimed()) {
// Claim a region in the pregame.
if (game.currentPlayer().claim(data)) {
// Increment to next player.
game.currentPlayer().endTurn();
}
} else if (!Region.allReinforcementsPlaced()) {
if (game.currentPlayer().reinforce(data)) {
game.currentPlayer().endTurn();
}
}
if (Region.allReinforcementsPlaced()) {
game.incrementState();
}
} else {
if (await game.currentPlayer().act(data)) {
game.currentPlayer().endTurn();
}
}
});
export async function startPregame() {
gameState = PRE_GAME;
let firstPlayerIndex = await random.get(Object.keys(players).length, "first-player");
let firstPlayer = Object.values(players).sort((a, b) => (a.id < b.id ? -1 : 1))[
firstPlayerIndex
];
firstPlayer.isPlaying = true;
await barrier.wait();
updateDom();
}

View File

@ -0,0 +1,113 @@
let allPlaced = false;
// In standard Risk, this is 5
const _REINFORCEMENT_MULTIPLIER = 1;
export const REGIONS = {};
class Continent {
constructor(name) {
this.name = name;
this.yield = 0;
}
}
export class Region {
constructor(name, continent) {
this.name = name;
this.owner = null;
this.strength = 0;
this.neighbours = new Set();
this.continent = continent;
REGIONS[name] = this;
}
static setNeighbours(region1, region2) {
region1.neighbours.add(region2);
region2.neighbours.add(region1);
}
static allRegionsClaimed() {
return (
Object.values(REGIONS).find((region) => region.owner === null) === undefined
);
}
static reinforcementsRemaining() {
if (allPlaced) {
return 0;
} else {
let totalStrength = Object.values(REGIONS)
.filter((region) => region.owner === us)
.reduce((counter, region) => counter + region.strength, 0);
let numPlayers = Object.values(players).length;
return _REINFORCEMENT_MULTIPLIER * (10 - numPlayers) - totalStrength;
}
}
static allReinforcementsPlaced() {
if (allPlaced) {
return true;
} else {
let totalStrength = Object.values(REGIONS).reduce(
(counter, region) => counter + region.strength,
0
);
let numPlayers = Object.values(players).length;
allPlaced =
totalStrength >=
numPlayers * _REINFORCEMENT_MULTIPLIER * (10 - numPlayers);
return allPlaced;
}
}
static getRegion(name) {
return REGIONS[name];
}
static getAllRegions() {
return Object.values(REGIONS);
}
claim(player) {
this.owner = player;
this.strength = 1;
}
reinforce(amount) {
this.strength += amount;
}
}
const EAST = new Continent("East");
const WEST = new Continent("West");
const A = new Region("A", EAST);
const B = new Region("B", EAST);
const C = new Region("C", EAST);
const D = new Region("D", EAST);
const J = new Region("J", EAST);
const F = new Region("F", WEST);
const G = new Region("G", WEST);
const H = new Region("H", WEST);
const I = new Region("I", WEST);
const E = new Region("E", WEST);
Region.setNeighbours(A, B);
Region.setNeighbours(A, C);
Region.setNeighbours(B, C);
Region.setNeighbours(B, J);
Region.setNeighbours(C, D);
Region.setNeighbours(C, F);
Region.setNeighbours(E, J);
Region.setNeighbours(E, I);
Region.setNeighbours(E, H);
Region.setNeighbours(F, J);
Region.setNeighbours(F, G);
Region.setNeighbours(G, H);
Region.setNeighbours(G, I);
Region.setNeighbours(H, I);

View File

@ -0,0 +1,70 @@
import { ID } from "./main.js";
export class Packet {
static _createBase(name) {
return {
type: name,
id: window.crypto.randomUUID(),
author: ID,
};
}
static createAnnounce() {
return {
...this._createBase("ANNOUNCE"),
name: "",
};
}
static createDisconnect() {
return this._createBase("DISCONNECT");
}
static createKeepAlive() {
return this._createBase("KEEPALIVE");
}
static createSetReady(nowReady) {
return {
...this._createBase("ACT"),
action: "READY",
ready: nowReady,
};
}
static createBarrierSignal() {
return this._createBase("BARRIER");
}
static createRegionClaim(region) {
return {
...this._createBase("ACT"),
region: region,
};
}
static createAction(action, startRegion, endRegion, amount) {
return {
...this._createBase("ACT"),
startRegion: startRegion,
endRegion: endRegion,
strength: amount,
action: action,
};
}
static createDefense(amount) {
return {
...this._createBase("ACT"),
action: "DEFENSE",
amount: amount,
};
}
static createEndTurn() {
return {
...this._createBase("ACT"),
action: "END",
};
}
}

View File

@ -0,0 +1,317 @@
import { Packet } from "./packet.js";
import { socket } from "./main.js";
import { updateDom } from "./dom.js";
// Timeout to consider a player disconnected
const TIMEOUT = 30_000;
const PHASE_REINFORCE = 1;
const PHASE_ATTACK = 2;
const PHASE_FORTIFY = 3;
let totalDice = 0;
export class Player {
constructor(id, local) {
this.timeout = null;
this.id = id;
this.ready = false;
// Data which is reset every turn
this.isPlaying = false;
this.reinforcementsPlaced = 0;
this.reinforcementsAvailable = 0;
this.offenderRegion = null;
this.turnPhase = PHASE_REINFORCE;
// Data for defending
this.defenderPromise = null;
this.defenderAmount = null;
this.resetColor();
if (local) {
// Emit keepalive messages to inform other players we are still here
window.setInterval(() => {
socket.emit("message", Packet.createKeepAlive());
}, TIMEOUT / 5);
}
}
resetTimeout(game) {
if (this.timeout !== null) {
window.clearTimeout(this.timeout);
}
this.timeout = window.setTimeout(() => {
game.removePlayer(this);
}, TIMEOUT);
}
/**
* Get our color as used on the board.
*/
getColor() {
return this.color;
}
resetColor() {
let randomColor = Math.random() * 360;
this.color = `hsl(${randomColor} 57% 50%)`;
}
/**
* Claim a region of the map.
*
* @param data Data received via socket.
*/
claim(data) {
let region = Region.getRegion(data.region);
if (region.owner === null) {
region.claim(this);
return true;
} else {
return false;
}
}
/**
* Reinforce a region of the map.
*
* @param data Data received via socket.
*/
reinforce(data) {
let region = Region.getRegion(data.region);
if (region.owner === this) {
region.reinforce(1);
return true;
} else {
return false;
}
}
/**
* Process a generic action packet representing a player's move.
*
* @param data Data received via socket
* @returns {boolean} Whether this player's turn has ended or not.
*/
async act(data) {
console.log(`player: ${this.id}`);
console.log(data);
if (this.turnPhase === PHASE_REINFORCE) {
if (data.region !== undefined) {
if (this.reinforce(data)) {
this.reinforcementsPlaced += 1;
}
if (this.reinforcementsPlaced === this.reinforcementsAvailable) {
this.turnPhase = PHASE_ATTACK;
}
}
return false;
} else {
// End turn prematurely.
if (data.action === "END") {
return true;
}
if (this.turnPhase === PHASE_ATTACK && data.action === "ATTACK") {
await this.attack(data);
return false;
}
this.turnPhase = PHASE_FORTIFY;
if (data.action === "FORTIFY") {
return this.fortify(data);
}
}
return false;
}
/**
* Process an action which is to attack another region.
*
* @param data Data received via socket
*/
async attack(data) {
let offender = Region.getRegion(data.startRegion);
let defender = Region.getRegion(data.endRegion);
let offenderStrength = parseInt(data.strength);
// Basic validation on game state
if (
offender.owner !== this ||
defender.owner === this ||
offenderStrength > 3 ||
offenderStrength <= 0 ||
offenderStrength >= offender.strength ||
(this.offenderRegion !== null && this.offenderRegion !== offender)
) {
return false;
}
// If we're the defender, we need to send a packet to state our defense.
if (defender.owner === us) {
showDefenseDom(defender.name);
}
// Grab the defense amount from
let defenderStrength = 0;
while (
defenderStrength <= 0 ||
defenderStrength > Math.min(2, defender.strength)
) {
defenderStrength = await defender.owner.getDefense();
}
/* How do Risk attacks work?
- Offender signs 1-3 armies, defender signs 1-2 armies
- Both roll respective dice
- Compare pairs of highest die in the rolls to remove armies.
*/
if (this.offenderRegion === null) {
this.offenderRegion = offender;
}
let offenderRolls = [];
let defenderRolls = [];
// Get random values
for (let i = 0; i < offenderStrength; i++) {
offenderRolls.push(await random.get(6, `dice-${totalDice}`));
totalDice += 1;
}
for (let i = 0; i < defenderStrength; i++) {
defenderRolls.push(await random.get(6, `dice-${totalDice}`));
totalDice += 1;
}
console.log(`attacker rolls: ${offenderRolls}`);
console.log(`defender rolls: ${defenderRolls}`);
offenderRolls.sort();
defenderRolls.sort();
// Compare and settle.
while (offenderRolls.length * defenderRolls.length > 0) {
let offenderResult = offenderRolls.pop();
let defenderResult = defenderRolls.pop();
if (offenderResult > defenderResult) {
defender.strength -= 1;
} else {
offender.strength -= 1;
}
if (defender.strength === 0) {
defender.strength = offenderRolls.length + 1;
offender.strength -= offenderRolls.length + 1;
defender.owner = this;
break;
}
}
// Reset the promises in case they attack again.
defender.owner.defenderPromise = null;
defender.owner.defenderAmount = null;
updateDom();
}
async setDefense(amount) {
await navigator.locks.request("defender", () => {
this.defenderAmount = amount;
if (this.defenderPromise !== null) {
this.defenderPromise(amount);
}
});
}
async getDefense() {
let promise, resolver;
await navigator.locks.request("defender", () => {
if (this.defenderAmount === null) {
promise = new Promise((resolve) => {
resolver = resolve;
});
this.defenderPromise = resolver;
} else {
promise = new Promise((resolve) => {
resolve(this.defenderAmount);
});
}
});
return promise;
}
/**
* Process an action which is to attack another region.
*
* @param data Data received via socket
*/
fortify(data) {
let sender = Region.getRegion(data.startRegion);
let receiver = Region.getRegion(data.endRegion);
let strength = parseInt(data.strength);
if (
sender.owner === this &&
receiver.owner === this &&
sender.strength > strength &&
strength > 0
) {
receiver.reinforce(strength);
sender.strength -= strength;
return true;
} else {
return false;
}
}
/**
* Calculate the number of additional reinforcements to gain on this turn.
*/
additionalReinforcements() {
return Math.min(
3,
Math.floor(
Object.values(REGIONS).filter((region) => region.owner === this).length /
3
)
);
}
/**
* Start a player's turn.
*/
startTurn() {
this.isPlaying = true;
this.reinforcementsPlaced = 0;
this.reinforcementsAvailable = this.additionalReinforcements();
this.offenderRegion = null;
this.turnPhase = PHASE_REINFORCE;
}
/**
* End player's turn
*/
endTurn() {
this.isPlaying = false;
this.nextPlayer().startTurn();
}
nextPlayer() {
let sorted = Object.values(players).sort((a, b) => (a.id < b.id ? -1 : 1));
let ourIndex = sorted.findIndex((player) => player.id === this.id);
return sorted[(ourIndex + 1) % sorted.length];
}
}

View File

@ -0,0 +1,141 @@
import { socket, ID, players } from "./main.js";
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();
}
}
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", {
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);
}
}
}