paillier integration sort of

This commit is contained in:
jude 2023-03-17 10:42:11 +00:00
parent a6961e1900
commit 2d72cdd87b
11 changed files with 244 additions and 92 deletions

View File

@ -37,3 +37,5 @@ export function mod_inv(a, n) {
return t; return t;
} }
window.mod_exp = mod_exp;

View File

@ -1,25 +1,60 @@
import { random2048, generate_prime } from "./random_primes.js"; import { random2048, generate_prime } from "./random_primes.js";
import { mod_exp } from "./math.js"; import { mod_exp } from "./math.js";
export class PaillierPubKey { class Cyphertext {
constructor(n) { constructor(key, plainText) {
this.n = n;
// this.g = this.n + 1n;
}
encrypt(m) {
// Compute g^m r^n mod n^2 // Compute g^m r^n mod n^2
let r = random2048(); let r = random2048();
// Resample to avoid modulo bias. // Resample to avoid modulo bias.
while (r >= this.n) { while (r >= key.n) {
r = random2048(); r = random2048();
} }
// Compute g^m by binomial theorem. // 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 // 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() { toJSON() {

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

View File

@ -135,14 +135,14 @@ document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".claim").forEach((el) => document.querySelectorAll(".claim").forEach((el) =>
el.addEventListener("click", (ev) => { el.addEventListener("click", (ev) => {
let region = ev.target.closest(".node").dataset.name; let region = ev.target.closest(".node").dataset.name;
socket.emit("message", Packet.createRegionClaim(region)); game.us.sendClaim(region);
}) })
); );
document.querySelectorAll(".reinforce").forEach((el) => document.querySelectorAll(".reinforce").forEach((el) =>
el.addEventListener("click", (ev) => { el.addEventListener("click", (ev) => {
let region = ev.target.closest(".node").dataset.name; 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()) { if (game.isPregame()) {
document.querySelector( document.querySelector(
"#remaining-reinforcements" "#remaining-reinforcements"
).innerHTML = `<span>Remaining placements: ${Region.reinforcementsRemaining()}</span>`; ).innerHTML = `<span>Remaining placements: ${game.reinforcementsRemaining()}</span>`;
} else if (game.isPlaying()) { } else if (game.isPlaying()) {
document.querySelector( document.querySelector(
"#remaining-reinforcements" "#remaining-reinforcements"

View File

@ -1,5 +1,8 @@
import { Player } from "./player.js"; import { Player } from "./player.js";
// In standard Risk, this is 5
const _REINFORCEMENT_MULTIPLIER = 1;
const WAITING = 0; const WAITING = 0;
const PRE_GAME = 1; const PRE_GAME = 1;
const PLAYING = 2; const PLAYING = 2;
@ -9,6 +12,8 @@ export class Game {
this.us = null; this.us = null;
this.players = {}; this.players = {};
this.state = WAITING; this.state = WAITING;
this.allPlaced = false;
} }
isWaiting() { isWaiting() {
@ -91,4 +96,33 @@ export class Game {
} }
return true; 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;
}
}
} }

View File

@ -121,6 +121,11 @@ document.addEventListener("ACT", async (ev) => {
if (game.isWaiting()) { if (game.isWaiting()) {
game.setReady(data.author, data.ready); game.setReady(data.author, data.ready);
} else { } else {
// Throw out our own packets
if (data.author === game.us) {
return;
}
if (data.author !== game.currentPlayer().id) { if (data.author !== game.currentPlayer().id) {
if (data.action === "DEFENSE") { if (data.action === "DEFENSE") {
await game.players[data.author].setDefense(data.amount); await game.players[data.author].setDefense(data.amount);
@ -136,13 +141,13 @@ document.addEventListener("ACT", async (ev) => {
// Increment to next player. // Increment to next player.
game.currentPlayer().endTurn(); game.currentPlayer().endTurn();
} }
} else if (!Region.allReinforcementsPlaced()) { } else if (!game.allReinforcementsPlaced()) {
if (game.currentPlayer().reinforce(data)) { if (game.currentPlayer().reinforce(data)) {
game.currentPlayer().endTurn(); game.currentPlayer().endTurn();
} }
} }
if (Region.allReinforcementsPlaced()) { if (game.allReinforcementsPlaced()) {
game.incrementState(); game.incrementState();
} }
} else { } else {

View File

@ -1,10 +1,3 @@
import { game } from "./main.js";
let allPlaced = false;
// In standard Risk, this is 5
const _REINFORCEMENT_MULTIPLIER = 1;
const REGIONS = {}; const REGIONS = {};
class Continent { class Continent {
@ -15,16 +8,16 @@ class Continent {
} }
class Strength { class Strength {
constructor(cyphertext) { constructor(cipherText) {
this.cyphertext = cyphertext; this.cipherText = cipherText;
this.assumedStrength = null; this.assumedStrength = null;
} }
update(amount) { update(cipherText) {
if (this.cyphertext === null) { if (this.cipherText === null) {
this.cyphertext = amount; this.cipherText = cipherText;
} else { } else {
this.cyphertext *= amount; this.cipherText.update(cipherText);
} }
this.assumedStrength = null; 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) { static getRegion(name) {
return REGIONS[name]; return REGIONS[name];
} }
@ -95,25 +54,25 @@ export class Region {
return Object.values(REGIONS); return Object.values(REGIONS);
} }
claim(player) { claim(player, cipherText) {
this.owner = player; this.owner = player;
this.strength.update(player.paillierPubKey.encrypt(1n)); this.strength.update(cipherText);
this.strength.assumedStrength = 1; this.strength.assumedStrength = 1;
} }
reinforce(amount) { reinforce(cipherText) {
this.strength.update(amount); this.strength.update(cipherText);
} }
isClaimed() { isClaimed() {
return this.strength.cyphertext !== null; return this.strength.cipherText !== null;
} }
displayStrength() { displayStrength() {
if (this.owner === null) { if (this.owner === null) {
return ""; return "";
} else if (this.owner === game.us) { } else if (!this.strength.cipherText.readOnly) {
return window.paillier.privKey.decrypt(this.strength.cyphertext).toString(); return this.strength.cipherText.plainText;
} else if (this.strength.assumedStrength !== null) { } else if (this.strength.assumedStrength !== null) {
return `${this.strength.assumedStrength}`; return `${this.strength.assumedStrength}`;
} else { } else {

View File

@ -77,29 +77,18 @@ export class Packet {
return this._sign(this._createBase("BARRIER")); return this._sign(this._createBase("BARRIER"));
} }
static createRegionClaim(region) { static createRegionClaim(region, text) {
return this._sign({ return this._sign({
...this._createBase("ACT"), ...this._createBase("ACT"),
region: region, region: region,
cipherText: text,
}); });
} }
static createReinforce(region) { static createReinforce(regionCyphertexts) {
// 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);
}
}
return this._sign({ return this._sign({
...this._createBase("ACT"), ...this._createBase("ACT"),
regions: regions, regions: regionCyphertexts,
}); });
} }

View File

@ -1,7 +1,7 @@
import { Packet } from "./packet.js"; import { Packet } from "./packet.js";
import { socket, game, random } from "./main.js"; import { socket, game, random } from "./main.js";
import { RsaPubKey } from "../crypto/rsa.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 { Region } from "./map.js";
import { showDefenseDom } from "./dom.js"; import { showDefenseDom } from "./dom.js";
@ -16,9 +16,13 @@ let totalDice = 0;
export class Player { export class Player {
constructor(id, local, rsa_key, paillier_key) { constructor(id, local, rsa_key, paillier_key) {
// Game state
this.totalStrength = 0;
this.ready = false;
// Protocol state
this.timeout = null; this.timeout = null;
this.id = id; this.id = id;
this.ready = false;
this.rsaPubKey = RsaPubKey.fromJSON(rsa_key); this.rsaPubKey = RsaPubKey.fromJSON(rsa_key);
this.paillierPubKey = PaillierPubKey.fromJSON(paillier_key); this.paillierPubKey = PaillierPubKey.fromJSON(paillier_key);
this.lastPacket = 0; this.lastPacket = 0;
@ -82,7 +86,13 @@ export class Player {
let region = Region.getRegion(data.region); let region = Region.getRegion(data.region);
if (region.owner === null) { if (region.owner === null) {
region.claim(this); region.claim(
this,
new ReadOnlyCyphertext(this.paillierPubKey, data.cipherText)
);
this.totalStrength += 1;
return true; return true;
} else { } else {
return false; return false;
@ -105,14 +115,48 @@ export class Player {
} }
} }
this.totalStrength += 1;
return true; 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. * Process a generic action packet representing a player's move.
* *
* @param data Data received via socket * @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) { async act(data) {
if (this.turnPhase === PHASE_REINFORCE) { if (this.turnPhase === PHASE_REINFORCE) {

Binary file not shown.

View File

@ -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] (P) at (0,0) {Prover};
\node[draw,rectangle] (V) at (6,0) {Verifier}; \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); \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^*$}; \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$}; \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,-7)--node [auto] {$e$}++(6,0);
\draw [->,very thick] (0,-8)--node [auto] {$z = r^*r^e$}++(6,0); \draw [->,very thick] (0,-8)--node [auto] {$z = r^*r^e \mod n$}++(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$}; \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) {}; \node[draw=none] (term) at (0,-9) {};
\fill (term) circle [radius=2pt]; \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) % 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} \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} 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}$. 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} \bibliography{Dissertation}
\end{document} \end{document}