diff --git a/static/js/modules/crypto/math.js b/static/js/modules/crypto/math.js index 5f33303..a0c762b 100644 --- a/static/js/modules/crypto/math.js +++ b/static/js/modules/crypto/math.js @@ -37,3 +37,5 @@ export function mod_inv(a, n) { return t; } + +window.mod_exp = mod_exp; diff --git a/static/js/modules/crypto/paillier.js b/static/js/modules/crypto/paillier.js index 41ab72d..e406ea6 100644 --- a/static/js/modules/crypto/paillier.js +++ b/static/js/modules/crypto/paillier.js @@ -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() { diff --git a/static/js/modules/crypto/paillier_proof.js b/static/js/modules/crypto/paillier_proof.js new file mode 100644 index 0000000..6d69872 --- /dev/null +++ b/static/js/modules/crypto/paillier_proof.js @@ -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 + ); + } +} diff --git a/static/js/modules/interface/dom.js b/static/js/modules/interface/dom.js index 74e6899..4d041e6 100644 --- a/static/js/modules/interface/dom.js +++ b/static/js/modules/interface/dom.js @@ -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 = `Remaining placements: ${Region.reinforcementsRemaining()}`; + ).innerHTML = `Remaining placements: ${game.reinforcementsRemaining()}`; } else if (game.isPlaying()) { document.querySelector( "#remaining-reinforcements" diff --git a/static/js/modules/interface/game.js b/static/js/modules/interface/game.js index b4a9b91..30ad7fe 100644 --- a/static/js/modules/interface/game.js +++ b/static/js/modules/interface/game.js @@ -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; + } + } } diff --git a/static/js/modules/interface/main.js b/static/js/modules/interface/main.js index 008342f..5c9a499 100644 --- a/static/js/modules/interface/main.js +++ b/static/js/modules/interface/main.js @@ -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 { diff --git a/static/js/modules/interface/map.js b/static/js/modules/interface/map.js index f079b5d..87d7ac7 100644 --- a/static/js/modules/interface/map.js +++ b/static/js/modules/interface/map.js @@ -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 { diff --git a/static/js/modules/interface/packet.js b/static/js/modules/interface/packet.js index f06886c..53396aa 100644 --- a/static/js/modules/interface/packet.js +++ b/static/js/modules/interface/packet.js @@ -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, }); } diff --git a/static/js/modules/interface/player.js b/static/js/modules/interface/player.js index 6ce97c3..0cabdcb 100644 --- a/static/js/modules/interface/player.js +++ b/static/js/modules/interface/player.js @@ -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) { diff --git a/whitepaper/Dissertation.pdf b/whitepaper/Dissertation.pdf index d466b40..a19ed28 100644 Binary files a/whitepaper/Dissertation.pdf and b/whitepaper/Dissertation.pdf differ diff --git a/whitepaper/Dissertation.tex b/whitepaper/Dissertation.tex index 9fe30ae..4da9071 100644 --- a/whitepaper/Dissertation.tex +++ b/whitepaper/Dissertation.tex @@ -340,17 +340,17 @@ The proof system presented is an interactive proof for a given cyphertext $c$ be \node[draw,rectangle] (P) at (0,0) {Prover}; \node[draw,rectangle] (V) at (6,0) {Verifier}; - \node[draw=blue!50,rectangle,thick] (v) at (0,-2) {$r \in \mathbb{Z}_n^*$ with $c = r^n$ (cyphertext)}; + \node[draw=blue!50,rectangle,thick,text width=5cm] (v) at (0,-1.5) {$r \in \mathbb{Z}_n^*$ with $c = r^n \mod n^2$}; \draw [->,very thick] (0,-3)--node [auto] {$c$}++(6,0); \node[draw=blue!50,rectangle,thick] (r) at (0,-4) {Choose random $r^* \in \mathbb{Z}_n^*$}; - \draw [->,very thick] (0,-5)--node [auto] {$a = (r^*)^n$}++(6,0); + \draw [->,very thick] (0,-5)--node [auto] {$a = (r^*)^n \mod n^2$}++(6,0); \node[draw=blue!50,rectangle,thick] (e) at (6,-6) {Choose random $e$}; \draw [<-,very thick] (0,-7)--node [auto] {$e$}++(6,0); - \draw [->,very thick] (0,-8)--node [auto] {$z = r^*r^e$}++(6,0); - \node[draw=blue!50,rectangle,thick,text width=6cm] (verify) at (6,-9) {Verify $z, c, a$ coprime to $n$\\ Verify $r^z \equiv ac^e \mod n^2$}; + \draw [->,very thick] (0,-8)--node [auto] {$z = r^*r^e \mod n$}++(6,0); + \node[draw=blue!50,rectangle,thick,text width=5cm] (verify) at (6,-9) {Verify $z, c, a$ coprime to $n$\\ Verify $z^n \equiv ac^e \mod n^2$}; \node[draw=none] (term) at (0,-9) {}; \fill (term) circle [radius=2pt]; @@ -364,6 +364,10 @@ Then, a proof for the following homologous problem can be trivially constructed: % Furthermore, the above protocol can be made non-interactive using the Fiat-Shamir heuristic \citep{fiatshamir}. (this contradicts the lit review) +\subsection{Recovering $r$ given $c$} + +The proof requires that the prover can perform new calculations with $r$ given a cyphertext $c = g^mr^n \mod n^2$. For ease of programming, + \subsection{Application to domain} Players should prove a number of properties of their game state to each other to ensure fair play. These are as follows. \begin{enumerate} @@ -382,6 +386,58 @@ Players should prove a number of properties of their game state to each other to For (1), we propose the following communication sequence. The player submits pairs $(R, c_R)$ for each region they control, where $R$ is the region and $c_R$ is a cyphertext encoding the number of reinforcements to add to the region (which may be 0). Each player computes $c_{R_1} \cdot \ldots \cdot c_{R_n}$. +\subsection{Shared random values} + +A large part of Risk involves random behaviour dictated by rolling some number of dice. To achieve this, some fair protocol must be used to generate random values consistently across each peer without any peer being able to manipulate the outcomes. + +This is achieved through bit-commitment and properties of $\mathbb{Z}_n$. The protocol for two peers is as follows, and generalises to $n$ peers trivially. + +\begin{center} + \begin{tikzpicture}[ + every node/.append style={very thick,rounded corners=0.1mm} + ] + + \node[draw,rectangle] (A) at (0,0) {Peer A}; + + \node[draw,rectangle] (B) at (6,0) {Peer B}; + + \node[draw=blue!50,rectangle,thick,text width=4cm] (NoiseA) at (0,-1.5) {Generate random noise $N_A$, random key $k_A$}; + \node[draw=blue!50,rectangle,thick,text width=4cm] (NoiseB) at (6,-1.5) {Generate random noise $N_B$, random key $k_B$}; + + \draw [->,very thick] (0,-3)--node [auto] {$E_{k_A}(N_A)$}++(6,0); + \draw [<-,very thick] (0,-4)--node [auto] {$E_{k_B}(N_B)$}++(6,0); + + \draw [->,very thick] (0,-5)--node [auto] {$k_A$}++(6,0); + \draw [<-,very thick] (0,-6)--node [auto] {$k_B$}++(6,0); + + \node[draw=blue!50,rectangle,thick] (CA) at (0,-7) {Compute $N_A + N_B$}; + \node[draw=blue!50,rectangle,thick] (CB) at (6,-7) {Compute $N_A + N_B$}; + + \draw [very thick] (A)-- (NoiseA)-- (CA)-- (0,-7); + \draw [very thick] (B)-- (NoiseB)-- (CB)-- (6,-7); + \end{tikzpicture} +\end{center} + +Depending on how $N_A + N_B$ is then turned into a random value within a range, this system may be manipulated by an attacker who has some knowledge of how participants are generating their noise. As a basic example, suppose a random value within range is generated by taking $N_A + N_B \mod 3$, and participants are producing 2-bit noises. An attacker could submit a 3-bit noise with the most-significant bit set, in which case the odds of getting a 1 are significantly higher than the odds of a 0 or a 2. To avoid this problem, peers should agree beforehand on the number of bits to transmit, and truncate any values in the final stage that exceed this limit. + +The encryption function used must also guarantee the integrity of decrypted cyphertexts to prevent a malicious party creating a cyphertext which decrypts to multiple valid values through using different keys. + +\begin{proposition} + The scheme shown is not manipulable by a single cheater. +\end{proposition} + +\begin{proof} + Suppose $P_1, \dots, P_{n-1}$ are honest participants, and $P_n$ is a cheater with desired outcome $O$. + + The encryption function $E_k$ holds the confidentiality property: that is, without $k$, $P_i$ cannot retrieve $m$ given $E_k(m)$. + + Each participant $P_i$ commits $N_i$. Then, the final value is $N_1 + \dots + N_{n-1} + N_n$. +\end{proof} + +\subsection{Avoiding modular bias} + +The typical way to avoid modular bias is by resampling. To avoid excessive communication, resampling can be performed within the bit sequence by partitioning into blocks of $n$ bits and taking blocks until one falls within range. This is appropriate in the presented use case as random values need only be up to 6, so the likelihood of consuming over 63 bits of noise when resampling for a value in the range 0 to 5 is $\left(\frac{1}{4}\right)^{21} \approx 2.3 \times 10^{-13}$. + \bibliography{Dissertation} \end{document}