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}`, () => {