diff --git a/static/js/modules/interface/dom.js b/static/js/modules/interface/dom.js index 76daedf..457c95e 100644 --- a/static/js/modules/interface/dom.js +++ b/static/js/modules/interface/dom.js @@ -3,12 +3,12 @@ import { Region } from "./map.js"; import { Packet } from "./packet.js"; export function unlockMapDom() { - Object.values(REGIONS).forEach((region) => { - if (!allRegionsClaimed() && region.owner === null) { + Object.values(Region.getAllRegions()).forEach((region) => { + if (!Region.allRegionsClaimed() && region.owner === null) { document .querySelector(`.node[data-name=${region.name}] .actions`) .classList.remove("hidden"); - } else if (region.owner === us) { + } else if (region.owner === game.us) { document .querySelector(`.node[data-name=${region.name}] .actions`) .classList.remove("hidden"); @@ -36,7 +36,7 @@ function updateMapDom() { e.classList.remove("hidden"); }); - if (us.isPlaying) { + if (game.us.isPlaying) { document.querySelector("#end-turn").classList.remove("hidden"); } else { document.querySelector("#end-turn").classList.add("hidden"); @@ -49,6 +49,8 @@ function updateMapDom() { element.style.backgroundColor = region.owner === null ? "white" : region.owner.getColor(); } + + showRemainingReinforcements(); } document.addEventListener("gameStateUpdate", () => { @@ -57,6 +59,8 @@ document.addEventListener("gameStateUpdate", () => { } }); document.addEventListener("gameStateUpdate", updateMapDom); +document.addEventListener("playerChange", updateMapDom); +document.addEventListener("turnProgress", updateMapDom); function updatePlayerDom() { let list = document.querySelector("#playerList"); @@ -99,7 +103,7 @@ function updatePlayerDom() { document.addEventListener("addPlayer", updatePlayerDom); document.addEventListener("removePlayer", updatePlayerDom); document.addEventListener("updatePlayer", updatePlayerDom); -document.addEventListener("gameStateUpdate", updatePlayerDom); +document.addEventListener("playerChange", updatePlayerDom); document.addEventListener("DOMContentLoaded", () => { document.querySelector("#ready-button").addEventListener("click", async (ev) => { @@ -207,17 +211,18 @@ function showRemainingReinforcements() { if (game.isPregame()) { document.querySelector( "#remaining-reinforcements" - ).innerHTML = `Remaining placements: ${reinforcementsRemaining()}`; + ).innerHTML = `Remaining placements: ${Region.reinforcementsRemaining()}`; } else if (game.isPlaying()) { document.querySelector( "#remaining-reinforcements" ).innerHTML = `Remaining placements: ${ - currentPlayer().reinforcementsAvailable - currentPlayer().reinforcementsPlaced + game.currentPlayer().reinforcementsAvailable - + game.currentPlayer().reinforcementsPlaced }`; } } -function showDefenseDom(region) { +export function showDefenseDom(region) { const modal = document.querySelector("#defense-modal"); modal.querySelector("span").textContent = region; modal.classList.remove("hidden"); diff --git a/static/js/modules/interface/game.js b/static/js/modules/interface/game.js index 3357022..5791874 100644 --- a/static/js/modules/interface/game.js +++ b/static/js/modules/interface/game.js @@ -32,6 +32,10 @@ export class Game { document.dispatchEvent(event); } + playerCount() { + return Object.values(this.players).length; + } + currentPlayer() { return Object.values(this.players).filter((p) => p.isPlaying)[0]; } diff --git a/static/js/modules/interface/main.js b/static/js/modules/interface/main.js index 7a79a07..352c9e5 100644 --- a/static/js/modules/interface/main.js +++ b/static/js/modules/interface/main.js @@ -3,12 +3,13 @@ import { Random } from "./random.js"; import { Barrier } from "./barrier.js"; import { Packet } from "./packet.js"; import { Game } from "./game.js"; +import { Region } from "./map.js"; import "./dom.js"; export const ID = window.crypto.randomUUID(); export const game = new Game(); export let socket; -let random; +export let random; let barrier; window.paillier = generate_keypair(); window.rsa = generate_rsa_keypair(); @@ -42,8 +43,8 @@ document.addEventListener("DOMContentLoaded", () => { } else { let sig = BigInt(packet.sig); // decrypt and compare signature - let dehash = sender.rsaPubKey.encrypt(sig).toString(16); - let hash = CryptoJS.SHA3(JSON.stringify(data)).toString(); + let dehash = sender.rsaPubKey.encrypt(sig); + let hash = BigInt("0x" + CryptoJS.SHA3(JSON.stringify(data)).toString()); if (dehash !== hash) { window.console.error(`Signature invalid! Ignoring packet ${data.id}.`); return; @@ -138,6 +139,9 @@ document.addEventListener("ACT", async (ev) => { } else { if (await game.currentPlayer().act(data)) { game.currentPlayer().endTurn(); + } else { + const event = new CustomEvent("turnProgress"); + document.dispatchEvent(event); } } } @@ -156,5 +160,8 @@ document.addEventListener("gameStateUpdate", async () => { firstPlayer.isPlaying = true; await barrier.wait(); + + const event = new CustomEvent("playerChange"); + document.dispatchEvent(event); } }); diff --git a/static/js/modules/interface/map.js b/static/js/modules/interface/map.js index 0c67e15..dfafd01 100644 --- a/static/js/modules/interface/map.js +++ b/static/js/modules/interface/map.js @@ -1,9 +1,11 @@ +import { game } from "./main.js"; + let allPlaced = false; // In standard Risk, this is 5 const _REINFORCEMENT_MULTIPLIER = 1; -export const REGIONS = {}; +const REGIONS = {}; class Continent { constructor(name) { @@ -39,11 +41,10 @@ export class Region { return 0; } else { let totalStrength = Object.values(REGIONS) - .filter((region) => region.owner === us) + .filter((region) => region.owner === game.us) .reduce((counter, region) => counter + region.strength, 0); - let numPlayers = Object.values(players).length; - return _REINFORCEMENT_MULTIPLIER * (10 - numPlayers) - totalStrength; + return _REINFORCEMENT_MULTIPLIER * (10 - game.playerCount()) - totalStrength; } } @@ -55,11 +56,12 @@ export class Region { (counter, region) => counter + region.strength, 0 ); - let numPlayers = Object.values(players).length; allPlaced = totalStrength >= - numPlayers * _REINFORCEMENT_MULTIPLIER * (10 - numPlayers); + game.playerCount() * + _REINFORCEMENT_MULTIPLIER * + (10 - game.playerCount()); return allPlaced; } } diff --git a/static/js/modules/interface/packet.js b/static/js/modules/interface/packet.js index 8fa4734..0f355c0 100644 --- a/static/js/modules/interface/packet.js +++ b/static/js/modules/interface/packet.js @@ -52,15 +52,34 @@ export class Packet { }); } + static createRandomCyphertext(sessionId, range, cipherText) { + return this._sign({ + ...this._createBase("RANDOM"), + session: sessionId, + range: range, + stage: "CIPHERTEXT", + cipherText: cipherText, + }); + } + + static createRandomKey(sessionId, key) { + return this._sign({ + ...this._createBase("RANDOM"), + session: sessionId, + stage: "DECRYPT", + cipherKey: key, + }); + } + static createBarrierSignal() { return this._sign(this._createBase("BARRIER")); } static createRegionClaim(region) { - return { + return this._sign({ ...this._createBase("ACT"), region: region, - }; + }); } static createAction(action, startRegion, endRegion, amount) { diff --git a/static/js/modules/interface/player.js b/static/js/modules/interface/player.js index 01d4dbc..988d489 100644 --- a/static/js/modules/interface/player.js +++ b/static/js/modules/interface/player.js @@ -1,6 +1,8 @@ import { Packet } from "./packet.js"; -import { socket } from "./main.js"; +import { socket, game, random } from "./main.js"; import { RsaPubKey } from "../crypto/rsa.js"; +import { Region } from "./map.js"; +import { showDefenseDom } from "./dom.js"; // Timeout to consider a player disconnected const TIMEOUT = 30_000; @@ -100,9 +102,6 @@ export class Player { * @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)) { @@ -158,7 +157,7 @@ export class Player { } // If we're the defender, we need to send a packet to state our defense. - if (defender.owner === us) { + if (defender.owner === game.us) { showDefenseDom(defender.name); } @@ -223,8 +222,6 @@ export class Player { // Reset the promises in case they attack again. defender.owner.defenderPromise = null; defender.owner.defenderAmount = null; - - updateDom(); } async setDefense(amount) { @@ -284,8 +281,9 @@ export class Player { return Math.min( 3, Math.floor( - Object.values(REGIONS).filter((region) => region.owner === this).length / - 3 + Object.values(Region.getAllRegions()).filter( + (region) => region.owner === this + ).length / 3 ) ); } @@ -307,10 +305,14 @@ export class Player { endTurn() { this.isPlaying = false; this.nextPlayer().startTurn(); + + const event = new CustomEvent("playerChange"); + document.dispatchEvent(event); } nextPlayer() { - let sorted = Object.values(players).sort((a, b) => (a.id < b.id ? -1 : 1)); + // todo move this to :class:Game + let sorted = Object.values(game.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]; diff --git a/static/js/modules/interface/random.js b/static/js/modules/interface/random.js index ae10459..6b11331 100644 --- a/static/js/modules/interface/random.js +++ b/static/js/modules/interface/random.js @@ -1,4 +1,5 @@ -import { socket, ID, game } from "./main.js"; +import { socket, game } from "./main.js"; +import { Packet } from "./packet.js"; class RandomSession { constructor(range) { @@ -56,14 +57,10 @@ export class Random { this.sessions[sessionId] = session; - socket.emit("message", { - type: "RANDOM", - author: ID, - session: sessionId, - range: n, - stage: "CIPHERTEXT", - cipherText: session.cipherText(), - }); + socket.emit( + "message", + Packet.createRandomCyphertext(sessionId, n, session.cipherText()) + ); return session; } @@ -90,13 +87,10 @@ export class Random { Object.keys(game.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, - }); + socket.emit( + "message", + Packet.createRandomKey(data.session, session.ourKey) + ); } } else if (stage === "DECRYPT") { session.cipherKeys[data.author] = data.cipherKey; diff --git a/whitepaper/Dissertation.pdf b/whitepaper/Dissertation.pdf index f2c2f02..d466b40 100644 Binary files a/whitepaper/Dissertation.pdf and b/whitepaper/Dissertation.pdf differ diff --git a/whitepaper/Dissertation.tex b/whitepaper/Dissertation.tex index c23d1e9..9fe30ae 100644 --- a/whitepaper/Dissertation.tex +++ b/whitepaper/Dissertation.tex @@ -259,10 +259,14 @@ In particular, the final point allows for the use of purely JSON messages, which \subsection{Message structure} -Messages are given a fixed structure to make processing simpler. Each JSON message holds an \texttt{author} field, being the sender's ID, and an \texttt{action}, which at a high level dictates how each client should process the message. +Messages are given a fixed structure to make processing simpler. Each JSON message holds an \texttt{author} field, being the sender's ID; a message ID to prevent replay attacks and associate related messages; and an \texttt{action}, which at a high level dictates how each client should process the message. + +The action more specifically is one of \texttt{ANNOUNCE}, \texttt{DISCONNECT}, \texttt{KEEPALIVE}, \texttt{RANDOM}, and \texttt{ACT}. The first three of these are used for managing the network by ensuring peers are aware of each other and know the state of the network. \texttt{RANDOM} is designated to be used by the shared-random-value subprotocol defined later. \texttt{ACT} is used by players to submit actions for their turn during gameplay. Each message is also signed to verify the author. This is a standard application of RSA. A hash of the message is taken, then encrypted with the private key. This can be verified with the public key. +RSA keys are accepted by peers on a first-seen basis. + \subsection{Paillier} Paillier requires the calculation of two large primes for the generation of public and private key pairs. ECMAScript typically stores integers as floating point numbers, giving precision up to $2^{53}$. This is clearly inappropriate for the generation of sufficiently large primes. @@ -362,7 +366,7 @@ Then, a proof for the following homologous problem can be trivially constructed: \subsection{Application to domain} -Players should prove a number of properties of their game state to each other to ensure fair play. These are as follows. \begin{itemize} +Players should prove a number of properties of their game state to each other to ensure fair play. These are as follows. \begin{enumerate} \item The number of reinforcements placed during the first stage of a turn. \item The number of units on a region neighbouring another player. @@ -372,10 +376,11 @@ Players should prove a number of properties of their game state to each other to \item The number of units available for an attack/defence. \item The number of units moved when fortifying. -\end{itemize} +\end{enumerate} -Of these, the bottom two are, more specifically, range proofs. %todo is this grammar right? -The top three can be addressed with the protocol we have provided however. +(4) and (5) can be generalised further as range proofs. + +For (1), we propose the following communication sequence. The player submits pairs $(R, c_R)$ for each region they control, where $R$ is the region and $c_R$ is a cyphertext encoding the number of reinforcements to add to the region (which may be 0). Each player computes $c_{R_1} \cdot \ldots \cdot c_{R_n}$. \bibliography{Dissertation}