diff --git a/static/js/modules/crypto/main.js b/static/js/modules/crypto/main.js index a468613..c748d40 100644 --- a/static/js/modules/crypto/main.js +++ b/static/js/modules/crypto/main.js @@ -1,2 +1,4 @@ import { generate_keypair } from "./paillier.js"; -export { generate_keypair }; +import { generate_rsa_keypair } from "./rsa.js"; + +export { generate_keypair, generate_rsa_keypair }; diff --git a/static/js/modules/crypto/math.js b/static/js/modules/crypto/math.js index 55eeb48..5a34866 100644 --- a/static/js/modules/crypto/math.js +++ b/static/js/modules/crypto/math.js @@ -12,3 +12,24 @@ export function mod_exp(a, b, n) { return res; } + +export function mod_inv(a, n) { + let t = 0n; + let new_t = 1n; + let r = n; + let new_r = a; + + while (new_r !== 0n) { + let quotient = r / new_r; + t = new_t; + new_t = t - quotient * new_t; + r = new_r; + new_r = r - quotient * new_r; + } + + if (t < 0) { + t = t + n; + } + + return t; +} diff --git a/static/js/modules/crypto/rsa.js b/static/js/modules/crypto/rsa.js new file mode 100644 index 0000000..2a6f124 --- /dev/null +++ b/static/js/modules/crypto/rsa.js @@ -0,0 +1,47 @@ +import { generate_prime } from "./random_primes.js"; +import { mod_exp, mod_inv } from "./math.js"; + +let p, q, pubKey, privKey; + +class PubKey { + constructor(p, q) { + this.n = p * q; + this.e = 65537; + } + + decrypt(m) { + return mod_exp(m, this.e, this.n); + } +} + +class PrivKey { + constructor(p, q) { + this.n = p * q; + this.d = mod_inv(65537, (q - 1) * (p - 1)); + } + + encrypt(c) { + return mod_exp(c, this.d, this.n); + } +} + +export function generate_rsa_keypair() { + if (window.sessionStorage.getItem("rsa_p") === null) { + p = generate_prime(); + window.sessionStorage.setItem("rsa_p", p); + } else { + p = BigInt(window.sessionStorage.getItem("rsa_p")); + } + + if (window.sessionStorage.getItem("rsa_q") === null) { + q = generate_prime(); + window.sessionStorage.setItem("rsa_q", q); + } else { + q = BigInt(window.sessionStorage.getItem("rsa_q")); + } + + pubKey = new PubKey(p, q); + privKey = new PrivKey(p, q); + + return { pubKey, privKey }; +} diff --git a/static/js/modules/interface/barrier.js b/static/js/modules/interface/barrier.js index e0b8fee..75444cb 100644 --- a/static/js/modules/interface/barrier.js +++ b/static/js/modules/interface/barrier.js @@ -1,4 +1,4 @@ -import { socket, players } from "./main.js"; +import { socket, game } from "./main.js"; import { Packet } from "./packet.js"; /** @@ -25,7 +25,7 @@ export class Barrier { resolve(data) { this.hits.add(data.author); - if (this.hits.size === Object.keys(players).length - 1) { + if (this.hits.size === Object.keys(game.players).length - 1) { this.hits = new Set(); this.resolver(); } diff --git a/static/js/modules/interface/dom.js b/static/js/modules/interface/dom.js index a4ae5e7..76daedf 100644 --- a/static/js/modules/interface/dom.js +++ b/static/js/modules/interface/dom.js @@ -1,15 +1,4 @@ -import { - gameState, - WAITING, - PRE_GAME, - PLAYING, - us, - socket, - players, - ID, - allPlayersReady, - startPregame, -} from "./main.js"; +import { game, socket, ID } from "./main.js"; import { Region } from "./map.js"; import { Packet } from "./packet.js"; @@ -31,28 +20,18 @@ 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) { + if (game.us.isPlaying) { unlockMapDom(); } else { lockMapDom(); } - if (gameState === PRE_GAME) { + if (game.isPregame()) { document.querySelectorAll(".fortify, .attack").forEach((e) => { e.classList.add("hidden"); }); - } else if (gameState === PLAYING) { + } else if (game.isPlaying()) { document.querySelectorAll(".node button").forEach((e) => { e.classList.remove("hidden"); }); @@ -72,16 +51,23 @@ function updateMapDom() { } } +document.addEventListener("gameStateUpdate", () => { + if (!game.isWaiting()) { + document.querySelector("#ready-button").style.display = "none"; + } +}); +document.addEventListener("gameStateUpdate", updateMapDom); + function updatePlayerDom() { let list = document.querySelector("#playerList"); list.replaceChildren(); - for (let playerId of Object.keys(players).sort()) { - let player = players[playerId]; + for (let playerId of Object.keys(game.players).sort()) { + let player = game.players[playerId]; let statusSpan = document.createElement("div"); statusSpan.classList.add("status-span"); - if (gameState === WAITING) { + if (game.isWaiting()) { if (player.ready) { statusSpan.textContent = "R"; statusSpan.classList.add("ready"); @@ -110,21 +96,19 @@ function updatePlayerDom() { } } +document.addEventListener("addPlayer", updatePlayerDom); +document.addEventListener("removePlayer", updatePlayerDom); +document.addEventListener("updatePlayer", updatePlayerDom); +document.addEventListener("gameStateUpdate", updatePlayerDom); + 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) => { @@ -212,7 +196,7 @@ document.addEventListener("DOMContentLoaded", () => { }); document.querySelector("#shuffleColors").addEventListener("click", () => { - Object.values(players).forEach((player) => { + Object.values(game.players).forEach((player) => { player.resetColor(); }); updatePlayerDom(); @@ -220,11 +204,11 @@ document.addEventListener("DOMContentLoaded", () => { }); function showRemainingReinforcements() { - if (gameState === PRE_GAME) { + if (game.isPregame()) { document.querySelector( "#remaining-reinforcements" ).innerHTML = `Remaining placements: ${reinforcementsRemaining()}`; - } else if (gameState === PLAYING) { + } else if (game.isPlaying()) { document.querySelector( "#remaining-reinforcements" ).innerHTML = `Remaining placements: ${ diff --git a/static/js/modules/interface/game.js b/static/js/modules/interface/game.js index 3b8af95..93919a6 100644 --- a/static/js/modules/interface/game.js +++ b/static/js/modules/interface/game.js @@ -1,4 +1,4 @@ -import { Player } from "./player"; +import { Player } from "./player.js"; const WAITING = 0; const PRE_GAME = 1; @@ -25,18 +25,23 @@ export class Game { incrementState() { this.state += 1; + + const event = new CustomEvent("gameStateUpdate", { + detail: { newState: this.state }, + }); + document.dispatchEvent(event); } currentPlayer() { return Object.values(this.players).filter((p) => p.isPlaying)[0]; } - addPlayer(id, name, is_us) { + addPlayer(id, 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.players[id] = new Player(id, is_us); + if (is_us) { this.us = this.players[id]; } } @@ -64,7 +69,10 @@ export class Game { } setReady(id, ready) { - this.players[id].readyState = ready; + this.players[id].ready = ready; + + const event = new CustomEvent("updatePlayer"); + document.dispatchEvent(event); if (this._allPlayersReady()) { this.incrementState(); @@ -73,7 +81,7 @@ export class Game { _allPlayersReady() { for (let player of Object.values(this.players)) { - if (!player.readyState) { + if (!player.ready) { return false; } } diff --git a/static/js/modules/interface/main.js b/static/js/modules/interface/main.js index 156fca8..96557c0 100644 --- a/static/js/modules/interface/main.js +++ b/static/js/modules/interface/main.js @@ -2,17 +2,16 @@ 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"; +import { Game } from "./game.js"; +import "./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; +const paillier = generate_keypair(); +const rsa = generate_rsa_keypair(); // Not totally reliable but better than nothing. window.addEventListener("beforeunload", () => { @@ -23,15 +22,17 @@ document.addEventListener("DOMContentLoaded", () => { socket = io(); random = new Random(); barrier = new Barrier(); - keys = generate_keypair(); socket.on("connect", () => { window.console.log("Connected!"); + window.console.log(`We are: ${ID}`); socket.emit("message", Packet.createAnnounce()); - game.addPlayer(ID, name, true); + game.addPlayer(ID, true); }); socket.on("message", async (data) => { + window.console.log(data); + // todo validate signature switch (data.type) { @@ -62,10 +63,11 @@ document.addEventListener("DOMContentLoaded", () => { * * @param data Packet received */ -document.addEventListener("ANNOUNCE", (data) => { +document.addEventListener("ANNOUNCE", (ev) => { + const data = ev.detail; if (data.author === ID) return; - let is_new = game.addPlayer(data.author, data.name, false); + let is_new = game.addPlayer(data.author, false); // When a new player is seen, all announce to ensure they know all players. if (is_new) { @@ -73,7 +75,8 @@ document.addEventListener("ANNOUNCE", (data) => { } }); -document.addEventListener("DISCONNECT", (data) => { +document.addEventListener("DISCONNECT", (ev) => { + const data = ev.detail; game.removePlayer(data.author); }); @@ -82,54 +85,61 @@ document.addEventListener("DISCONNECT", (data) => { * * @param data Packet received */ -document.addEventListener("KEEPALIVE", (data) => { +document.addEventListener("KEEPALIVE", (ev) => { + const data = ev.detail; 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; - } +document.addEventListener("ACT", async (ev) => { + const data = ev.detail; 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(); + } else { + if (data.author !== game.currentPlayer().id) { + if (data.action === "DEFENSE") { + await game.players[data.author].setDefense(data.amount); } + + return; } - if (Region.allReinforcementsPlaced()) { - game.incrementState(); - } - } else { - if (await game.currentPlayer().act(data)) { - game.currentPlayer().endTurn(); + 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; +document.addEventListener("gameStateUpdate", async () => { + if (game.isPregame()) { + let firstPlayerIndex = await random.get( + Object.keys(game.players).length, + "first-player" + ); - let firstPlayerIndex = await random.get(Object.keys(players).length, "first-player"); + let firstPlayer = Object.values(game.players).sort((a, b) => + a.id < b.id ? -1 : 1 + )[firstPlayerIndex]; - let firstPlayer = Object.values(players).sort((a, b) => (a.id < b.id ? -1 : 1))[ - firstPlayerIndex - ]; - - firstPlayer.isPlaying = true; - await barrier.wait(); - updateDom(); -} + firstPlayer.isPlaying = true; + await barrier.wait(); + } +}); diff --git a/static/js/modules/interface/packet.js b/static/js/modules/interface/packet.js index 73791a8..6aa2c22 100644 --- a/static/js/modules/interface/packet.js +++ b/static/js/modules/interface/packet.js @@ -12,7 +12,6 @@ export class Packet { static createAnnounce() { return { ...this._createBase("ANNOUNCE"), - name: "", }; } diff --git a/static/js/modules/interface/player.js b/static/js/modules/interface/player.js index 666343e..c9db5ea 100644 --- a/static/js/modules/interface/player.js +++ b/static/js/modules/interface/player.js @@ -1,6 +1,5 @@ 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; @@ -34,7 +33,7 @@ export class Player { // Emit keepalive messages to inform other players we are still here window.setInterval(() => { socket.emit("message", Packet.createKeepAlive()); - }, TIMEOUT / 5); + }, TIMEOUT / 2); } } diff --git a/static/js/modules/interface/random.js b/static/js/modules/interface/random.js index 4d2013b..ae10459 100644 --- a/static/js/modules/interface/random.js +++ b/static/js/modules/interface/random.js @@ -1,4 +1,4 @@ -import { socket, ID, players } from "./main.js"; +import { socket, ID, game } from "./main.js"; class RandomSession { constructor(range) { @@ -87,7 +87,7 @@ export class Random { if ( Object.keys(session.cipherTexts).length === - Object.keys(players).length - 1 + Object.keys(game.players).length - 1 ) { // Step 3: release our key once all players have sent a ciphertext socket.emit("message", { @@ -104,7 +104,7 @@ export class Random { // Step 4: get final random value if ( Object.keys(session.cipherKeys).length === - Object.keys(players).length - 1 + 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}`, () => {