diff --git a/static/js/index.js b/static/js/index.js
index e9455ae..2dbb86b 100644
--- a/static/js/index.js
+++ b/static/js/index.js
@@ -12,6 +12,7 @@ const POST_GAME = 3;
let game_state = WAITING;
let socket;
+let random;
// Not totally reliable but better than nothing.
window.addEventListener("beforeunload", () => {
@@ -25,6 +26,7 @@ window.addEventListener("beforeunload", () => {
document.addEventListener("DOMContentLoaded", () => {
socket = io();
+ random = new Random();
socket.on("connect", () => {
console.log("Connected!");
@@ -38,7 +40,7 @@ document.addEventListener("DOMContentLoaded", () => {
players[ID] = new Player(ID, name);
});
- socket.on("message", (data) => {
+ socket.on("message", async (data) => {
// Ignore any messages that originate from us.
if (data.author === ID) {
return;
@@ -58,11 +60,11 @@ document.addEventListener("DOMContentLoaded", () => {
break;
case "SYNC":
- sync(data);
+ await sync(data);
break;
case "RANDOM":
- processCooperativeRandom(data);
+ random.processCooperativeRandom(data);
break;
}
});
@@ -124,15 +126,21 @@ function keepAlive(data) {
*
* @param data Packet received
*/
-function sync(data) {
+async function sync(data) {
players[data.author].name = data.name;
players[data.author].ready = data.ready;
if (allPlayersReady()) {
game_state = PRE_GAME;
// Decide turn order. "Master" begins a cooperative rng process.
+ if (ID === Array.min(...Object.keys(players))) {
+ random.coopRandom(Object.keys(players).length, "first-player");
+ }
- //
+ // Wait for value to populate
+ let player1 = await random.get("first-player");
+
+ console.log(player1);
}
}
@@ -145,106 +153,3 @@ function allPlayersReady() {
return true;
}
-
-// Track ongoing random sessions.
-const randomSessions = {};
-
-/**
- * Start a cooperative random session.
- */
-function coopRandom(n) {
- const sessionId = window.crypto.randomUUID();
-
- const noise = CryptoJS.lib.WordArray.random(8).toString();
- const key = CryptoJS.lib.WordArray.random(32).toString();
- const cipherText = CryptoJS.AES.encrypt(noise, key).toString();
-
- randomSessions[sessionId] = {
- range: n,
- cipherTexts: {},
- cipherKeys: {},
- ourKey: key,
- ourNoise: noise,
- finalValue: -1,
- };
-
- socket.emit("message", {
- type: "RANDOM",
- author: ID,
- session: sessionId,
- range: n,
- stage: "CIPHERTEXT",
- cipherText: cipherText,
- });
-}
-
-/**
- * Process cooperative random protocol.
- *
- * @param data Packet received
- */
-function processCooperativeRandom(data) {
- // Step 0: extract relevant information from data
- let session = randomSessions[data.session];
- const stage = data.stage;
-
- if (session === undefined) {
- // Step 1: generate and encrypt our random value. 8 bytes = 64 bit integer
- const noise = CryptoJS.lib.WordArray.random(8).toString();
- console.log(`our noise: ${noise}`);
-
- const key = CryptoJS.lib.WordArray.random(32).toString();
- const cipherText = CryptoJS.AES.encrypt(noise, key).toString();
-
- randomSessions[data.session] = {
- range: data.range,
- cipherTexts: {},
- cipherKeys: {},
- ourKey: key,
- ourNoise: noise,
- finalValue: -1,
- };
- session = randomSessions[data.session];
-
- // Step 2: send our random value and wait for all responses
- socket.emit("message", {
- type: "RANDOM",
- author: ID,
- session: data.session,
- stage: "CIPHERTEXT",
- range: data.range,
- cipherText: cipherText,
- });
- }
-
- 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) {
- let total = 0;
-
- for (let participant of Object.keys(session.cipherKeys)) {
- total += CryptoJS.AES.decrypt(
- session.cipherTexts[participant],
- session.cipherKeys[participant]
- );
- }
-
- session.finalValue = total % session.range;
- }
- }
-}
diff --git a/static/js/random.js b/static/js/random.js
new file mode 100644
index 0000000..6b6fd10
--- /dev/null
+++ b/static/js/random.js
@@ -0,0 +1,151 @@
+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 64-bit levels.
+ this.ourNoise = CryptoJS.lib.WordArray.random(4).toString();
+ this.finalValue = null;
+ this.resolvers = [];
+ }
+
+ cipherText() {
+ return CryptoJS.AES.encrypt(this.ourNoise, this.ourKey).toString();
+ }
+}
+
+class Random {
+ constructor() {
+ this.locked = false;
+ this.sessions = {};
+ }
+
+ get(sessionId) {
+ // Spin until lock frees.
+ while (this.locked);
+
+ if (this.sessions[sessionId].finalValue === null) {
+ let session = this.sessions[sessionId];
+ let resolver;
+ let promise = new Promise((resolve) => {
+ resolver = resolve;
+ });
+ session.resolvers.push(resolver);
+ return promise;
+ } else {
+ return new Promise((resolve) => {
+ resolve(this.sessions[sessionId].finalValue);
+ });
+ }
+ }
+
+ /**
+ * Start a cooperative random session.
+ */
+ coopRandom(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 sessionId;
+ }
+
+ /**
+ * Process cooperative random protocol.
+ *
+ * @param data Packet received
+ */
+ processCooperativeRandom(data) {
+ // Step 0: extract relevant information from data
+ let session = this.sessions[data.session];
+ const stage = data.stage;
+
+ if (session === undefined) {
+ // Step 1: generate and encrypt our random value. 8 bytes = 64 bit integer
+ const noise = CryptoJS.lib.WordArray.random(8).toString();
+ console.log(`our noise: ${noise}`);
+
+ session = new RandomSession(data.range);
+ this.sessions[data.session] = session;
+
+ // Step 2: send our random value and wait for all responses
+ socket.emit("message", {
+ type: "RANDOM",
+ author: ID,
+ session: data.session,
+ stage: "CIPHERTEXT",
+ range: data.range,
+ cipherText: session.cipherText(),
+ });
+ }
+
+ 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.
+ this.locked = true;
+
+ let total = 0;
+
+ for (let participant of Object.keys(session.cipherKeys)) {
+ total += CryptoJS.AES.decrypt(
+ session.cipherTexts[participant],
+ session.cipherKeys[participant]
+ );
+ }
+
+ session.finalValue = total % session.range;
+
+ this.resolve(data.session);
+
+ this.locked = false;
+ }
+ }
+ }
+
+ /**
+ * 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);
+ }
+ }
+}
diff --git a/templates/index.html b/templates/index.html
index 51b8e2e..1709b74 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -10,6 +10,7 @@
+