paillier integration sort of
This commit is contained in:
parent
a6961e1900
commit
2d72cdd87b
@ -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) {
|
||||
|
Binary file not shown.
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user