paillier integration sort of
This commit is contained in:
@ -37,3 +37,5 @@ export function mod_inv(a, n) {
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
window.mod_exp = mod_exp;
|
||||
|
@ -1,25 +1,60 @@
|
||||
import { random2048, generate_prime } from "./random_primes.js";
|
||||
import { mod_exp } from "./math.js";
|
||||
|
||||
export class PaillierPubKey {
|
||||
constructor(n) {
|
||||
this.n = n;
|
||||
// this.g = this.n + 1n;
|
||||
}
|
||||
|
||||
encrypt(m) {
|
||||
class Cyphertext {
|
||||
constructor(key, plainText) {
|
||||
// Compute g^m r^n mod n^2
|
||||
let r = random2048();
|
||||
|
||||
// Resample to avoid modulo bias.
|
||||
while (r >= this.n) {
|
||||
while (r >= key.n) {
|
||||
r = random2048();
|
||||
}
|
||||
|
||||
// Compute g^m by binomial theorem.
|
||||
let gm = (1n + this.n * m) % this.n ** 2n;
|
||||
let gm = (1n + key.n * plainText) % key.n ** 2n;
|
||||
|
||||
// Compute g^m r^n from crt
|
||||
return (gm * mod_exp(r, this.n, this.n ** 2n)) % this.n ** 2n;
|
||||
this.cyphertext = (gm * mod_exp(r, key.n, key.n ** 2n)) % key.n ** 2n;
|
||||
this.r = r;
|
||||
this.key = key;
|
||||
this.plainText = plainText;
|
||||
|
||||
this.readOnly = false;
|
||||
}
|
||||
|
||||
update(c) {
|
||||
this.cyphertext *= c.cyphertext;
|
||||
this.r *= c.r;
|
||||
this.plainText += c.plainText;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return "0x" + this.cyphertext.toString(16);
|
||||
}
|
||||
}
|
||||
|
||||
export class ReadOnlyCyphertext {
|
||||
constructor(key, cyphertext) {
|
||||
this.cyphertext = cyphertext;
|
||||
this.key = key;
|
||||
|
||||
this.readOnly = true;
|
||||
}
|
||||
|
||||
update(c) {
|
||||
this.cyphertext *= c.cyphertext;
|
||||
}
|
||||
}
|
||||
|
||||
export class PaillierPubKey {
|
||||
constructor(n) {
|
||||
this.n = n;
|
||||
this.g = this.n + 1n;
|
||||
}
|
||||
|
||||
encrypt(m) {
|
||||
return new Cyphertext(this, m);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
|
28
static/js/modules/crypto/paillier_proof.js
Normal file
28
static/js/modules/crypto/paillier_proof.js
Normal file
@ -0,0 +1,28 @@
|
||||
import { random2048 } from "./random_primes.js";
|
||||
import { mod_exp } from "./math";
|
||||
|
||||
class PlaintextVerifier {
|
||||
constructor(cyphertext, value, pub_key) {
|
||||
this.proving =
|
||||
(cyphertext * mod_exp(pub_key.g, value, pub_key.n ** 2)) % pub_key.n ** 2;
|
||||
this.challenge = random2048();
|
||||
}
|
||||
|
||||
verify(response) {}
|
||||
}
|
||||
|
||||
class PlaintextProver {
|
||||
constructor(cyphertext, pub_key, priv_key) {
|
||||
this.value = priv_key.decrypt(cyphertext.text);
|
||||
this.mixin = random2048();
|
||||
|
||||
this.pubKey = pub_key;
|
||||
}
|
||||
|
||||
handleChallenge(challenge) {
|
||||
return (
|
||||
(this.mixin * mod_exp(cyphertext.mixin, challenge, this.pubKey.n)) %
|
||||
this.pubKey.n
|
||||
);
|
||||
}
|
||||
}
|
@ -135,14 +135,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
document.querySelectorAll(".claim").forEach((el) =>
|
||||
el.addEventListener("click", (ev) => {
|
||||
let region = ev.target.closest(".node").dataset.name;
|
||||
socket.emit("message", Packet.createRegionClaim(region));
|
||||
game.us.sendClaim(region);
|
||||
})
|
||||
);
|
||||
|
||||
document.querySelectorAll(".reinforce").forEach((el) =>
|
||||
el.addEventListener("click", (ev) => {
|
||||
let region = ev.target.closest(".node").dataset.name;
|
||||
socket.emit("message", Packet.createReinforce(region));
|
||||
game.us.sendReinforce(region);
|
||||
})
|
||||
);
|
||||
|
||||
@ -231,7 +231,7 @@ function showRemainingReinforcements() {
|
||||
if (game.isPregame()) {
|
||||
document.querySelector(
|
||||
"#remaining-reinforcements"
|
||||
).innerHTML = `<span>Remaining placements: ${Region.reinforcementsRemaining()}</span>`;
|
||||
).innerHTML = `<span>Remaining placements: ${game.reinforcementsRemaining()}</span>`;
|
||||
} else if (game.isPlaying()) {
|
||||
document.querySelector(
|
||||
"#remaining-reinforcements"
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { Player } from "./player.js";
|
||||
|
||||
// In standard Risk, this is 5
|
||||
const _REINFORCEMENT_MULTIPLIER = 1;
|
||||
|
||||
const WAITING = 0;
|
||||
const PRE_GAME = 1;
|
||||
const PLAYING = 2;
|
||||
@ -9,6 +12,8 @@ export class Game {
|
||||
this.us = null;
|
||||
this.players = {};
|
||||
this.state = WAITING;
|
||||
|
||||
this.allPlaced = false;
|
||||
}
|
||||
|
||||
isWaiting() {
|
||||
@ -91,4 +96,33 @@ export class Game {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
reinforcementsRemaining() {
|
||||
if (this.allPlaced) {
|
||||
return 0;
|
||||
} else {
|
||||
return (
|
||||
_REINFORCEMENT_MULTIPLIER * (10 - this.playerCount()) -
|
||||
this.us.totalStrength
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
allReinforcementsPlaced() {
|
||||
if (this.allPlaced) {
|
||||
return true;
|
||||
} else {
|
||||
let totalStrength = Object.values(this.players).reduce(
|
||||
(counter, player) => counter + player.totalStrength,
|
||||
0
|
||||
);
|
||||
|
||||
this.allPlaced =
|
||||
totalStrength >=
|
||||
this.playerCount() *
|
||||
_REINFORCEMENT_MULTIPLIER *
|
||||
(10 - this.playerCount());
|
||||
return this.allPlaced;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,6 +121,11 @@ document.addEventListener("ACT", async (ev) => {
|
||||
if (game.isWaiting()) {
|
||||
game.setReady(data.author, data.ready);
|
||||
} else {
|
||||
// Throw out our own packets
|
||||
if (data.author === game.us) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.author !== game.currentPlayer().id) {
|
||||
if (data.action === "DEFENSE") {
|
||||
await game.players[data.author].setDefense(data.amount);
|
||||
@ -136,13 +141,13 @@ document.addEventListener("ACT", async (ev) => {
|
||||
// Increment to next player.
|
||||
game.currentPlayer().endTurn();
|
||||
}
|
||||
} else if (!Region.allReinforcementsPlaced()) {
|
||||
} else if (!game.allReinforcementsPlaced()) {
|
||||
if (game.currentPlayer().reinforce(data)) {
|
||||
game.currentPlayer().endTurn();
|
||||
}
|
||||
}
|
||||
|
||||
if (Region.allReinforcementsPlaced()) {
|
||||
if (game.allReinforcementsPlaced()) {
|
||||
game.incrementState();
|
||||
}
|
||||
} else {
|
||||
|
@ -1,10 +1,3 @@
|
||||
import { game } from "./main.js";
|
||||
|
||||
let allPlaced = false;
|
||||
|
||||
// In standard Risk, this is 5
|
||||
const _REINFORCEMENT_MULTIPLIER = 1;
|
||||
|
||||
const REGIONS = {};
|
||||
|
||||
class Continent {
|
||||
@ -15,16 +8,16 @@ class Continent {
|
||||
}
|
||||
|
||||
class Strength {
|
||||
constructor(cyphertext) {
|
||||
this.cyphertext = cyphertext;
|
||||
constructor(cipherText) {
|
||||
this.cipherText = cipherText;
|
||||
this.assumedStrength = null;
|
||||
}
|
||||
|
||||
update(amount) {
|
||||
if (this.cyphertext === null) {
|
||||
this.cyphertext = amount;
|
||||
update(cipherText) {
|
||||
if (this.cipherText === null) {
|
||||
this.cipherText = cipherText;
|
||||
} else {
|
||||
this.cyphertext *= amount;
|
||||
this.cipherText.update(cipherText);
|
||||
}
|
||||
|
||||
this.assumedStrength = null;
|
||||
@ -53,40 +46,6 @@ export class Region {
|
||||
);
|
||||
}
|
||||
|
||||
// todo fix
|
||||
static reinforcementsRemaining() {
|
||||
if (allPlaced) {
|
||||
return 0;
|
||||
} else {
|
||||
let totalStrength = Object.values(REGIONS)
|
||||
.filter((region) => region.owner === game.us)
|
||||
.reduce(
|
||||
(counter, region) => counter + region.strength.assumedStrength,
|
||||
0
|
||||
);
|
||||
|
||||
return _REINFORCEMENT_MULTIPLIER * (10 - game.playerCount()) - totalStrength;
|
||||
}
|
||||
}
|
||||
|
||||
static allReinforcementsPlaced() {
|
||||
if (allPlaced) {
|
||||
return true;
|
||||
} else {
|
||||
let totalStrength = Object.values(REGIONS).reduce(
|
||||
(counter, region) => counter + region.strength.assumedStrength,
|
||||
0
|
||||
);
|
||||
|
||||
allPlaced =
|
||||
totalStrength >=
|
||||
game.playerCount() *
|
||||
_REINFORCEMENT_MULTIPLIER *
|
||||
(10 - game.playerCount());
|
||||
return allPlaced;
|
||||
}
|
||||
}
|
||||
|
||||
static getRegion(name) {
|
||||
return REGIONS[name];
|
||||
}
|
||||
@ -95,25 +54,25 @@ export class Region {
|
||||
return Object.values(REGIONS);
|
||||
}
|
||||
|
||||
claim(player) {
|
||||
claim(player, cipherText) {
|
||||
this.owner = player;
|
||||
this.strength.update(player.paillierPubKey.encrypt(1n));
|
||||
this.strength.update(cipherText);
|
||||
this.strength.assumedStrength = 1;
|
||||
}
|
||||
|
||||
reinforce(amount) {
|
||||
this.strength.update(amount);
|
||||
reinforce(cipherText) {
|
||||
this.strength.update(cipherText);
|
||||
}
|
||||
|
||||
isClaimed() {
|
||||
return this.strength.cyphertext !== null;
|
||||
return this.strength.cipherText !== null;
|
||||
}
|
||||
|
||||
displayStrength() {
|
||||
if (this.owner === null) {
|
||||
return "";
|
||||
} else if (this.owner === game.us) {
|
||||
return window.paillier.privKey.decrypt(this.strength.cyphertext).toString();
|
||||
} else if (!this.strength.cipherText.readOnly) {
|
||||
return this.strength.cipherText.plainText;
|
||||
} else if (this.strength.assumedStrength !== null) {
|
||||
return `${this.strength.assumedStrength}`;
|
||||
} else {
|
||||
|
@ -77,29 +77,18 @@ export class Packet {
|
||||
return this._sign(this._createBase("BARRIER"));
|
||||
}
|
||||
|
||||
static createRegionClaim(region) {
|
||||
static createRegionClaim(region, text) {
|
||||
return this._sign({
|
||||
...this._createBase("ACT"),
|
||||
region: region,
|
||||
cipherText: text,
|
||||
});
|
||||
}
|
||||
|
||||
static createReinforce(region) {
|
||||
// todo cache some of these, possibly by pregenerating cyphertexts in a web worker
|
||||
let regions = {};
|
||||
for (let ourRegion of game.us.getRegions()) {
|
||||
if (ourRegion.name !== region) {
|
||||
regions[ourRegion.name] =
|
||||
"0x" + game.us.paillierPubKey.encrypt(0n).toString(16);
|
||||
} else {
|
||||
regions[ourRegion.name] =
|
||||
"0x" + game.us.paillierPubKey.encrypt(1n).toString(16);
|
||||
}
|
||||
}
|
||||
|
||||
static createReinforce(regionCyphertexts) {
|
||||
return this._sign({
|
||||
...this._createBase("ACT"),
|
||||
regions: regions,
|
||||
regions: regionCyphertexts,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Packet } from "./packet.js";
|
||||
import { socket, game, random } from "./main.js";
|
||||
import { RsaPubKey } from "../crypto/rsa.js";
|
||||
import { PaillierPubKey } from "../crypto/paillier.js";
|
||||
import { PaillierPubKey, ReadOnlyCyphertext } from "../crypto/paillier.js";
|
||||
import { Region } from "./map.js";
|
||||
import { showDefenseDom } from "./dom.js";
|
||||
|
||||
@ -16,9 +16,13 @@ let totalDice = 0;
|
||||
|
||||
export class Player {
|
||||
constructor(id, local, rsa_key, paillier_key) {
|
||||
// Game state
|
||||
this.totalStrength = 0;
|
||||
this.ready = false;
|
||||
|
||||
// Protocol state
|
||||
this.timeout = null;
|
||||
this.id = id;
|
||||
this.ready = false;
|
||||
this.rsaPubKey = RsaPubKey.fromJSON(rsa_key);
|
||||
this.paillierPubKey = PaillierPubKey.fromJSON(paillier_key);
|
||||
this.lastPacket = 0;
|
||||
@ -82,7 +86,13 @@ export class Player {
|
||||
let region = Region.getRegion(data.region);
|
||||
|
||||
if (region.owner === null) {
|
||||
region.claim(this);
|
||||
region.claim(
|
||||
this,
|
||||
new ReadOnlyCyphertext(this.paillierPubKey, data.cipherText)
|
||||
);
|
||||
|
||||
this.totalStrength += 1;
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@ -105,14 +115,48 @@ export class Player {
|
||||
}
|
||||
}
|
||||
|
||||
this.totalStrength += 1;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
sendClaim(region) {
|
||||
let cipherText = this.paillierPubKey.encrypt(1n);
|
||||
Region.getRegion(region).claim(this, cipherText);
|
||||
|
||||
socket.emit("message", Packet.createRegionClaim(region, cipherText.toString()));
|
||||
|
||||
this.totalStrength += 1;
|
||||
|
||||
this.endTurn();
|
||||
}
|
||||
|
||||
sendReinforce(region) {
|
||||
let regions = {};
|
||||
for (let ourRegion of this.getRegions()) {
|
||||
let cipherText;
|
||||
if (ourRegion.name !== region) {
|
||||
cipherText = this.paillierPubKey.encrypt(0n);
|
||||
} else {
|
||||
cipherText = this.paillierPubKey.encrypt(1n);
|
||||
}
|
||||
|
||||
regions[ourRegion.name] = cipherText.toString();
|
||||
ourRegion.reinforce(cipherText);
|
||||
}
|
||||
|
||||
socket.emit("message", Packet.createReinforce(regions));
|
||||
|
||||
this.totalStrength += 1;
|
||||
|
||||
this.endTurn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a generic action packet representing a player's move.
|
||||
*
|
||||
* @param data Data received via socket
|
||||
* @returns {boolean} Whether this player's turn has ended or not.
|
||||
* @returns {boolean} Whether this player's turn should now end or not.
|
||||
*/
|
||||
async act(data) {
|
||||
if (this.turnPhase === PHASE_REINFORCE) {
|
||||
|
Reference in New Issue
Block a user