Add methods to wait for random session to complete
This commit is contained in:
parent
0a1011f0e5
commit
b37f273b05
@ -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
151
static/js/random.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user