Add methods to wait for random session to complete

This commit is contained in:
jude 2023-01-31 14:19:56 +00:00
parent 0a1011f0e5
commit b37f273b05
3 changed files with 165 additions and 108 deletions

View File

@ -12,6 +12,7 @@ const POST_GAME = 3;
let game_state = WAITING; let game_state = WAITING;
let socket; let socket;
let random;
// Not totally reliable but better than nothing. // Not totally reliable but better than nothing.
window.addEventListener("beforeunload", () => { window.addEventListener("beforeunload", () => {
@ -25,6 +26,7 @@ window.addEventListener("beforeunload", () => {
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
socket = io(); socket = io();
random = new Random();
socket.on("connect", () => { socket.on("connect", () => {
console.log("Connected!"); console.log("Connected!");
@ -38,7 +40,7 @@ document.addEventListener("DOMContentLoaded", () => {
players[ID] = new Player(ID, name); players[ID] = new Player(ID, name);
}); });
socket.on("message", (data) => { socket.on("message", async (data) => {
// Ignore any messages that originate from us. // Ignore any messages that originate from us.
if (data.author === ID) { if (data.author === ID) {
return; return;
@ -58,11 +60,11 @@ document.addEventListener("DOMContentLoaded", () => {
break; break;
case "SYNC": case "SYNC":
sync(data); await sync(data);
break; break;
case "RANDOM": case "RANDOM":
processCooperativeRandom(data); random.processCooperativeRandom(data);
break; break;
} }
}); });
@ -124,15 +126,21 @@ function keepAlive(data) {
* *
* @param data Packet received * @param data Packet received
*/ */
function sync(data) { async function sync(data) {
players[data.author].name = data.name; players[data.author].name = data.name;
players[data.author].ready = data.ready; players[data.author].ready = data.ready;
if (allPlayersReady()) { if (allPlayersReady()) {
game_state = PRE_GAME; game_state = PRE_GAME;
// Decide turn order. "Master" begins a cooperative rng process. // 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; 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;
}
}
}

151
static/js/random.js Normal file
View File

@ -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);
}
}
}

View File

@ -10,6 +10,7 @@
<script src="{{ url_for('static', filename='js/index.js') }}"></script> <script src="{{ url_for('static', filename='js/index.js') }}"></script>
<script src="{{ url_for('static', filename='js/player.js') }}"></script> <script src="{{ url_for('static', filename='js/player.js') }}"></script>
<script src="{{ url_for('static', filename='js/dom.js') }}"></script> <script src="{{ url_for('static', filename='js/dom.js') }}"></script>
<script src="{{ url_for('static', filename='js/random.js') }}"></script>
</head> </head>
<body> <body>