156 lines
4.7 KiB
JavaScript
156 lines
4.7 KiB
JavaScript
import { socket, game } from "./main.js";
|
|
import { Packet } from "./packet.js";
|
|
import { cryptoRandom } from "../crypto/random_primes.js";
|
|
|
|
class RandomSession {
|
|
constructor(range) {
|
|
this.range = range;
|
|
this.hmacs = {};
|
|
this.keys = {};
|
|
this.noises = {};
|
|
this.ourKey = cryptoRandom(1024).toString(16);
|
|
this.ourNoise = cryptoRandom(64);
|
|
this.finalValue = null;
|
|
this.resolvers = [];
|
|
}
|
|
|
|
hmac() {
|
|
let hasher = new jsSHA("SHA3-256", "HEX");
|
|
hasher.update(this.ourKey);
|
|
hasher.update(this.ourNoise.toString(16));
|
|
return hasher.getHash("HEX");
|
|
}
|
|
}
|
|
|
|
export class Random {
|
|
constructor() {
|
|
this.sessions = {};
|
|
}
|
|
|
|
async get(n, sessionId) {
|
|
if (this.sessions[sessionId] === undefined) {
|
|
this.initialiseSession(n, sessionId);
|
|
}
|
|
|
|
let promise;
|
|
await navigator.locks.request(`random-${sessionId}`, () => {
|
|
if (this.sessions[sessionId].finalValue === null) {
|
|
let session = this.sessions[sessionId];
|
|
let resolver;
|
|
promise = new Promise((resolve) => {
|
|
resolver = resolve;
|
|
});
|
|
session.resolvers.push(resolver);
|
|
} else {
|
|
promise = new Promise((resolve) => {
|
|
resolve(this.sessions[sessionId].finalValue);
|
|
});
|
|
}
|
|
});
|
|
return promise;
|
|
}
|
|
|
|
/**
|
|
* Start a cooperative random session.
|
|
*/
|
|
initialiseSession(n, sessionId) {
|
|
let session = new RandomSession(n);
|
|
if (sessionId === undefined) {
|
|
sessionId = window.crypto.randomUUID();
|
|
}
|
|
|
|
this.sessions[sessionId] = session;
|
|
|
|
socket.emit(
|
|
"message",
|
|
Packet.createRandomHMAC(sessionId, n, session.hmac(), session.ourKey)
|
|
);
|
|
|
|
return session;
|
|
}
|
|
|
|
/**
|
|
* Process cooperative random protocol.
|
|
*
|
|
* @param data Packet received
|
|
*/
|
|
async processCooperativeRandom(data) {
|
|
// Step 0: extract relevant information from data
|
|
let session = this.sessions[data.session];
|
|
const stage = data.stage;
|
|
|
|
if (session === undefined) {
|
|
session = this.initialiseSession(data.range, data.session);
|
|
}
|
|
|
|
if (stage === "COMMIT") {
|
|
session.hmacs[data.author] = data.hmac;
|
|
session.keys[data.author] = data.key;
|
|
|
|
if (
|
|
Object.keys(session.hmacs).length ===
|
|
Object.keys(game.players).length - 1
|
|
) {
|
|
// Step 3: release our key once all players have sent a ciphertext
|
|
socket.emit(
|
|
"message",
|
|
Packet.createRandomNoise(
|
|
data.session,
|
|
"0x" + session.ourNoise.toString(16)
|
|
)
|
|
);
|
|
}
|
|
} else if (stage === "REVEAL") {
|
|
// Check HMAC
|
|
let noise = BigInt(data.noise) % 2n ** 64n;
|
|
let hasher = new jsSHA("SHA3-256", "HEX");
|
|
hasher.update(session.keys[data.author]);
|
|
hasher.update(noise.toString(16));
|
|
let hash = hasher.getHash("HEX");
|
|
|
|
if (hash === session.hmacs[data.author]) {
|
|
session.noises[data.author] = noise;
|
|
}
|
|
|
|
// Step 4: get final random value
|
|
if (
|
|
Object.keys(session.noises).length ===
|
|
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}`, () => {
|
|
let total = session.ourNoise;
|
|
|
|
for (let noise of Object.values(session.noises)) {
|
|
total += noise;
|
|
}
|
|
|
|
// Find first good block of bits
|
|
let blockSize = BigInt(Math.ceil(Math.log2(session.range)));
|
|
let blockMask = 2n ** blockSize - 1n;
|
|
|
|
while ((total & blockMask) >= BigInt(session.range)) {
|
|
total >>= blockSize;
|
|
}
|
|
|
|
session.finalValue = total & blockMask;
|
|
|
|
this.resolve(data.session);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve a session by calling any callbacks associated with the session.
|
|
*
|
|
* @param sessionId
|
|
*/
|
|
resolve(sessionId) {
|
|
const session = this.sessions[sessionId];
|
|
for (let resolve of session.resolvers) {
|
|
resolve(session.finalValue);
|
|
}
|
|
}
|
|
}
|