This commit is contained in:
jude 2023-04-24 14:20:44 +01:00
parent 88cf76f815
commit f4020aadec
8 changed files with 265 additions and 112 deletions

View File

@ -32,7 +32,8 @@ selecting 512 if you want the program to run in less than 3-5 business days.
Key generation will still take some time. The browser may tell you the tab is not Key generation will still take some time. The browser may tell you the tab is not
responding, but just wait and it'll be fine eventually. If not, short-circuit the responding, but just wait and it'll be fine eventually. If not, short-circuit the
`generate_safe_prime` function in `random_primes.js` to just return a normal prime. `generate_safe_prime` function in `random_primes.js` to just return a normal prime
(there is a comment on the conditional that needs to be removed).
### Playing the game ### Playing the game

View File

@ -100,8 +100,11 @@ export function generate_prime() {
export function generate_safe_prime() { export function generate_safe_prime() {
while (true) { while (true) {
let n = generate_prime(); let n = generate_prime();
if (small_prime_test((n - 1n) / 2n) && miller_rabin((n - 1n) / 2n, 40)) { if (small_prime_test((n - 1n) / 2n)) {
// Remove this conditional if you want it to run fast! It (probably) won't break it
// if (miller_rabin((n - 1n) / 2n, 40)) {
return n; return n;
// }
} }
} }
} }

View File

@ -203,10 +203,14 @@ document.addEventListener("DOMContentLoaded", () => {
let amount = modal.querySelector(".amount").value; let amount = modal.querySelector(".amount").value;
let endRegion = modal.querySelector(".target").value; let endRegion = modal.querySelector(".target").value;
socket.emit( if (action === "ATTACK") {
"message", socket.emit(
Packet.createAction(action, startRegion, endRegion, amount) "message",
); Packet.createAction(action, startRegion, endRegion, amount)
);
} else {
game.us.sendFortify(startRegion, endRegion, amount);
}
}); });
document document

View File

@ -103,6 +103,14 @@ export class Packet {
}); });
} }
static createFortify(fortify, verification) {
return this._sign({
...this._createBase("ACT"),
fortify: fortify,
verification: verification,
});
}
static createDefense(amount) { static createDefense(amount) {
return this._sign({ return this._sign({
...this._createBase("ACT"), ...this._createBase("ACT"),

View File

@ -4,7 +4,7 @@ import { RsaPubKey } from "../crypto/rsa.js";
import { PaillierPubKey, ReadOnlyCiphertext } from "../crypto/paillier.js"; import { PaillierPubKey, ReadOnlyCiphertext } from "../crypto/paillier.js";
import { Region } from "./map.js"; import { Region } from "./map.js";
import { showDefenseDom } from "./dom.js"; import { showDefenseDom } from "./dom.js";
import { proveRange, proveRegions, verifyRegions } from "./proofs.js"; import { proveFortify, proveRange, proveRegions, verifyRegions } from "./proofs.js";
// Timeout to consider a player disconnected // Timeout to consider a player disconnected
const TIMEOUT = 30_000; const TIMEOUT = 30_000;
@ -178,6 +178,45 @@ export class Player {
} }
} }
/**
* Process an action which is to attack another region.
*
* @param data Data received via socket
*/
fortify(data) {
let sender = Region.getRegion(data.startRegion);
let receiver = Region.getRegion(data.endRegion);
let strength = parseInt(data.strength);
if (
sender.owner === this &&
receiver.owner === this &&
sender.strength > strength &&
strength > 0
) {
receiver.reinforce(strength);
sender.strength -= strength;
return true;
} else {
return false;
}
}
sendFortify(startRegion, endRegion, amount) {
let fortify = {
[startRegion]: new Ciphertext(this.paillierPubKey, BigInt(-amount)),
[endRegion]: new Ciphertext(this.paillierPubKey, BigInt(amount)),
};
for (let r of this.getRegions()) {
if (!fortify.hasOwnProperty(r)) {
fortify[r] = new Ciphertext(this.paillierPubKey, 0n);
}
}
socket.emit("message", Packet.createFortify(fortify, proveFortify(fortify)));
}
/** /**
* Process a generic action packet representing a player's move. * Process a generic action packet representing a player's move.
* *
@ -213,7 +252,11 @@ export class Player {
this.turnPhase = PHASE_FORTIFY; this.turnPhase = PHASE_FORTIFY;
if (data.action === "FORTIFY") { if (data.action === "FORTIFY") {
return this.fortify(data); if (this === game.us) {
return;
} else {
return this.fortify(data);
}
} }
} }
return false; return false;
@ -352,30 +395,6 @@ export class Player {
return promise; return promise;
} }
/**
* Process an action which is to attack another region.
*
* @param data Data received via socket
*/
fortify(data) {
let sender = Region.getRegion(data.startRegion);
let receiver = Region.getRegion(data.endRegion);
let strength = parseInt(data.strength);
if (
sender.owner === this &&
receiver.owner === this &&
sender.strength > strength &&
strength > 0
) {
receiver.reinforce(strength);
sender.strength -= strength;
return true;
} else {
return false;
}
}
/** /**
* Calculate the number of additional reinforcements to gain on this turn. * Calculate the number of additional reinforcements to gain on this turn.
*/ */

View File

@ -295,7 +295,8 @@ export function verifyRange(obj, key) {
window.verifyRange = verifyRange; window.verifyRange = verifyRange;
/** /**
* - We prove that the set contains |S| - 2 zeros, with the final pair summing to zero * - We prove that the set contains |S| - 2 zeros, with the final pair summing to zero and sums with the original
* set are zero.
* - We also attach some form of adjacency guarantee: that is, we prove the sums on all adjacent pairs are zero * - We also attach some form of adjacency guarantee: that is, we prove the sums on all adjacent pairs are zero
* - We also attach a range proof for the new region values * - We also attach a range proof for the new region values
*/ */
@ -311,11 +312,13 @@ export function proveFortify(fortify) {
psiMap[regionNames[i]] = psi[i]; psiMap[regionNames[i]] = psi[i];
} }
let newRegions = structuredClone(fortify); let newRegions = { regions: {} };
// Rearrange keys // Rearrange keys
for (let r of regionNames) { for (let r of regionNames) {
newRegions[psiMap[r]] = fortify[r].pubKey.encrypt(fortify[r].plainText); newRegions.regions[psiMap[r]] = fortify[r].pubKey.encrypt(
-fortify[r].plainText
);
} }
let edges = []; let edges = [];
@ -363,8 +366,12 @@ export function proveFortify(fortify) {
for (let i = 0; i < ROUNDS; i++) { for (let i = 0; i < ROUNDS; i++) {
let coin = coins[i]; let coin = coins[i];
let proof = proofs[i]; let proof = proofs[i].regions;
let input = privateInputs[i]; let input = privateInputs[i];
let psiMap = {};
for (let i = 0; i < regionNames.length; i++) {
psiMap[regionNames[i]] = input.psi[i];
}
if (coin === 1) { if (coin === 1) {
// Show |S| - 2 zeroes // Show |S| - 2 zeroes
@ -389,17 +396,29 @@ export function proveFortify(fortify) {
// Show pair is joined by edge // Show pair is joined by edge
let pairName = pair.sort(); let pairName = pair.sort();
console.log(input);
verification.pairEdgeSalt = input.edges.find( verification.pairEdgeSalt = input.edges.find(
(e) => e.edge[0] === pairName[0] && e.edge[1] === pairName[1] (e) => e.edge[0] === pairName[0] && e.edge[1] === pairName[1]
); ).salt;
verifications.push(verification); verifications.push(verification);
} else { } else {
// Show isomorphism // Show isomorphism
let edges = {};
for (let e of input.edges) {
edges[e.edge[0] + e.edge[1]] = e.salt;
}
let zeroProofs = {};
for (let r of regionNames) {
let c = proof[psiMap[r]].clone();
c.update(fortify[r]);
zeroProofs[r] = c.proveNI();
}
verifications.push({ verifications.push({
psi: input.psi, psi: input.psi,
salts: input.edges.map((e) => e.salt), salts: edges,
zeroProofs: zeroProofs,
}); });
} }
} }
@ -415,4 +434,116 @@ export function proveFortify(fortify) {
window.proveFortify = proveFortify; window.proveFortify = proveFortify;
// proveRegions({A:paillier.pubKey.encrypt(0n),B:paillier.pubKey.encrypt(3n),C:paillier.pubKey.encrypt(-3n),D:paillier.pubKey.encrypt(0n),E:paillier.pubKey.encrypt(0n)}) export function verifyFortify(obj, key) {
let coins = getCoins(JSON.stringify(obj.proofs));
let fortify = obj.fortify;
let regionNames = Object.keys(fortify).sort();
for (let i = 0; i < ROUNDS; i++) {
let coin = coins[i];
let proof = obj.proofs[i];
let verification = obj.verifications[i];
if (coin === 1) {
// Check |S| - 2 zeroes
if (regionNames.length - 2 !== Object.keys(verification.regions).length) {
return false;
}
let regions = new Set(regionNames);
for (let r of Object.keys(verification.regions)) {
let c = new ReadOnlyCiphertext(key, BigInt(proof.regions[r]));
let p = c.verifyNI(verification.regions[r]);
if (p !== 0n) {
return false;
}
regions.delete(r);
}
// Check remaining pair sums to zero
let pair = [...regions.values()].sort();
let c = new ReadOnlyCiphertext(key, BigInt(proof.regions[pair[0]]));
c.update(proof.regions[pair[1]]);
let p = c.verifyNI(verification.pairCipherText);
if (p !== 0n) {
return false;
}
// Check edge is okay
let hasher = new jsSHA("SHA3-256", "TEXT");
hasher.update(pair[0]);
hasher.update(pair[1]);
hasher.update(verification.pairEdgeSalt.toString(16));
let hash = hasher.getHash("HEX");
if (!proof.edges.includes(hash)) {
return false;
}
} else {
// Check isomorphism
let psi = verification.psi;
let psiMap = {};
for (let i = 0; i < regionNames.length; i++) {
psiMap[regionNames[i]] = psi[i];
}
let salts = verification.salts;
// Psi matches?
if (psi.split("").sort().join("") !== regionNames.join("")) {
return false;
}
// Ciphertexts match?
for (let r of regionNames) {
let c = new ReadOnlyCiphertext(key, BigInt(proof.regions[r]));
c.update(fortify[psiMap[r]]);
let p = c.verifyNI(verification.zeroProofs[r]);
if (p !== 0n) {
console.log(p);
return false;
}
}
// Edges match?
let proverEdges = new Set(proof.edges);
for (let r of regionNames) {
let psiRegion = psiMap[r];
for (let n of Region.getRegion(r).neighbours) {
let psiNeighbour = psiMap[n.name];
if (psiNeighbour > psiRegion) {
let edgeSalt = salts[psiRegion + psiNeighbour];
let hasher = new jsSHA("SHA3-256", "TEXT");
hasher.update(psiRegion);
hasher.update(psiNeighbour);
hasher.update(edgeSalt.toString(16));
let hash = hasher.getHash("HEX");
if (proverEdges.has(hash)) {
proverEdges.delete(hash);
} else {
return false;
}
}
}
}
// Check it is not over-specified
if (proverEdges.size !== 0) {
return false;
}
}
}
return true;
}
window.verifyFortify = verifyFortify;
// proveFortify({A:paillier.pubKey.encrypt(0n),B:paillier.pubKey.encrypt(3n),C:paillier.pubKey.encrypt(-3n),D:paillier.pubKey.encrypt(0n),E:paillier.pubKey.encrypt(0n)})

Binary file not shown.

View File

@ -169,11 +169,11 @@ Zero-knowledge proofs are particularly applicable to the presented problem. They
\item The proof presented can only be trusted by the verifier, and not by other parties. \item The proof presented can only be trusted by the verifier, and not by other parties.
\end{itemize} \end{itemize}
We can further formalise the general description of a zero-knowledge proof. The common formalisation of the concept of a zero-knowledge proof system for a language $L$ is We can further formalise the general description of a zero-knowledge proof. The common formalisation of the concept of a zero-knowledge proof system for a language $L$ is
\begin{itemize} \begin{itemize}
\item For every $x \in L$, the verifier will accept $x$ following interaction with a prover. \item For every $x \in L$, the verifier will accept $x$ following interaction with a prover.
\item For some polynomial $p$ and any $x \notin S$, the verifier will reject $x$ with probability at least $\frac{1}{p(|x|)}$. \item For some polynomial $p$ and any $x \notin S$, the verifier will reject $x$ with probability at least $\frac{1}{p(|x|)}$.
\item A verifier can produce a simulator $S$ such that for all $x \in L$, the outputs of $S(x)$ are indistinguishable from a transcript of the proving steps taken with the prover on $x$. \item A verifier can produce a simulator $S$ such that for all $x \in L$, the outputs of $S(x)$ are indistinguishable from a transcript of the proving steps taken with the prover on $x$.
\end{itemize} \end{itemize}
The final point describes a proof as being \textit{computationally zero-knowledge}. Some stronger conditions exist, which describe the distributions of the outputs of the simulator versus the distributions of the outputs of interaction with the prover. \begin{itemize} The final point describes a proof as being \textit{computationally zero-knowledge}. Some stronger conditions exist, which describe the distributions of the outputs of the simulator versus the distributions of the outputs of interaction with the prover. \begin{itemize}
@ -212,22 +212,6 @@ A typical example for zero-knowledge proofs is graph isomorphism \cite{10.1145/1
Identifying Risk as a graph therefore enables us to construct isomorphisms as part of the proof protocol. For example, when a player wishes to commit to a movement, it is important to prove that the initial node and the new node are adjacent. This can be proven by communicating isomorphic graphs, and constructing challenges based on the edges of the original graph. Identifying Risk as a graph therefore enables us to construct isomorphisms as part of the proof protocol. For example, when a player wishes to commit to a movement, it is important to prove that the initial node and the new node are adjacent. This can be proven by communicating isomorphic graphs, and constructing challenges based on the edges of the original graph.
\subsubsection{Adjacency proofs}
Proving adjacency of two nodes is akin to proving isomorphism of two graphs. A protocol using challenges could be constructed as follows: \begin{enumerate}
\item The prover commits a new edge between two nodes.
\item The prover constructs an isomorphic graph to the game, and encrypts the edges.
\item The verified challenges either: \begin{itemize}
\item That the graphs are isomorphic.
\item That the new edge is valid.
\end{itemize}
\item The prover sends a total decryption key for the graph's nodes, to prove isomorphism to the game board; or a decryption key for the new edge to the isomorphism, to prove adjacency.
\end{enumerate}
These challenges restrict the ability for the prover to cheat: if the two nodes they are committing to are not adjacent, either the prover will need to commit an invalid isomorphism (detected by challenge 1), or lie about the edge they have committed (detected by challenge 2).
Selection between two challenges is the ideal number of challenges to use, as the probability of cheating being detected is $\frac{1}{2}$. Using more challenge options (e.g, $n$) means the likelihood of the prover cheating a single challenge reduces to $\frac{1}{n}$. This would require much larger numbers of communications to then convince the verifier to the same level of certainty.
\subsubsection{Cheating with negative values} \subsubsection{Cheating with negative values}
Zerocash is a ledger system that uses zero-knowledge proofs to ensure consistency and prevent cheating. Ledgers are the main existing use case of zero-knowledge proofs, and there are some limited similarities between ledgers and Risk in how they wish to obscure values of tokens within the system. Zerocash is a ledger system that uses zero-knowledge proofs to ensure consistency and prevent cheating. Ledgers are the main existing use case of zero-knowledge proofs, and there are some limited similarities between ledgers and Risk in how they wish to obscure values of tokens within the system.
@ -244,16 +228,16 @@ A similar issue appears in the proposed system: a cheating player could update t
\subsubsection{Additive homomorphic cryptosystems} \subsubsection{Additive homomorphic cryptosystems}
Some cryptosystems admit an additive homomorphic property: that is, given the public key and two encrypted values $\sigma_1 = E(m_1), \sigma_2 = E(m_2)$, the value $\sigma_1 + \sigma_2 = E(m_1 + m_2)$ is the ciphertext of the underlying operation. Some cryptosystems admit an additive homomorphic property: that is, given the public key and two encrypted values $\sigma_1 = E(m_1), \sigma_2 = E(m_2)$, the value $\sigma_1 + \sigma_2 = E(m_1 + m_2)$ is the ciphertext of the underlying operation.
The Paillier cryptosystem, which is based on composite residuosity classes express the additive homomorphic property \cite{paillier1999public}. This is due to the structure of ciphertexts in the Paillier cryptosystem. A public key is of structure $(n, g)$, where $n$ is the product of two large primes and $g$ is a generator of $\mathbb{Z}^*_n$. Under the public key, the encryption $c$ of a message $m$ is computed as \begin{align*} The Paillier cryptosystem, which is based on composite residuosity classes express the additive homomorphic property \cite{paillier1999public}. This is due to the structure of ciphertexts in the Paillier cryptosystem. A public key is of structure $(n, g)$, where $n$ is the product of two large primes and $g$ is a generator of $\mathbb{Z}^*_n$. Under the public key, the encryption $c$ of a message $m$ is computed as \begin{align*}
c = g^mr^n \mod n^2 c = g^mr^n \mod n^2
\end{align*} \end{align*}
for some random $r \in \mathbb{Z}^*_{n^2}$. for some random $r \in \mathbb{Z}^*_{n^2}$.
The Paillier cryptosystem has disadvantages in its time and space complexity compared to other public-key cryptosystems such as RSA. In space complexity, Paillier ciphertexts are twice the size of their corresponding plaintext, as for a modulus $n$, ciphertexts are computed modulo $n^2$ for a message in range up to $n$. This cost can be reduced by employing some form of compression on the resulting ciphertexts. The Paillier cryptosystem has disadvantages in its time and space complexity compared to other public-key cryptosystems such as RSA. In space complexity, Paillier ciphertexts are twice the size of their corresponding plaintext, as for a modulus $n$, ciphertexts are computed modulo $n^2$ for a message in range up to $n$. This cost can be reduced by employing some form of compression on the resulting ciphertexts.
The main concern is the issue of time complexity of Paillier. Theoretic results based on the number of multiplications performed indicate that Paillier can be 1,000 times slower than RSA encryption. Many optimisations have been presented of the Paillier cryptosystem. The main concern is the issue of time complexity of Paillier. Theoretic results based on the number of multiplications performed indicate that Paillier can be 1,000 times slower than RSA encryption. Many optimisations have been presented of the Paillier cryptosystem.
The first is in the selection of public parameter $g$. The original paper suggests a choice of $g = 2$, however the choice of $g = 1 + n$ is very common, as the exponentiation $g^m = 1 + mn$ by binomial theorem. The first is in the selection of public parameter $g$. The original paper suggests a choice of $g = 2$, however the choice of $g = 1 + n$ is very common, as the exponentiation $g^m = 1 + mn$ by binomial theorem.
@ -264,7 +248,7 @@ for some random $r \in \mathbb{Z}^*_{n}$.
The optimisation comes in two parts: firstly, the mantissa is smaller, resulting in faster multiplications. Secondly, by taking $h_n = h^n \mod n^2$, we find the following equivalence: \begin{align*} The optimisation comes in two parts: firstly, the mantissa is smaller, resulting in faster multiplications. Secondly, by taking $h_n = h^n \mod n^2$, we find the following equivalence: \begin{align*}
(h^r \mod n)^n \mod n^2 = h_n^r \mod n^2 (h^r \mod n)^n \mod n^2 = h_n^r \mod n^2
\end{align*} \end{align*}
Exponentials of the fixed base $h_n$ can then be pre-computed to speed up exponentiation by arbitrary $r$. Exponentials of the fixed base $h_n$ can then be pre-computed to speed up exponentiation by arbitrary $r$.
@ -306,95 +290,97 @@ Despite this approach being centralised, it does emulate a fully peer-to-peer en
In particular, the final point allows for the use of purely JSON messages, which are readily parsed and processed by the client-side JavaScript. In particular, the final point allows for the use of purely JSON messages, which are readily parsed and processed by the client-side JavaScript.
The game is broken down into three main stages, each of which handles events in a different way. These are shown below. The game is broken down into three main stages, each of which handles events in a different way. These are shown below.
\begin{landscape}\begin{tikzpicture}[every node/.style={anchor=north west}] \begin{landscape}\begin{tikzpicture}[every node/.style={anchor=north west}]
% Create outlines % Create outlines
\node[ \node[
rectangle, rectangle,
dotted, dotted,
draw, draw,
minimum width=0.5\hsize-4pt, minimum width=0.5\hsize-4pt,
minimum height=0.5\textheight-4pt, minimum height=0.5\textheight-4pt,
align=right, align=right,
] at (0, 0.5\textheight) {}; ] at (0, 0.5\textheight) {};
\node[anchor=south west] at (0, 2pt) {Setup}; \node[anchor=south west] at (0, 2pt) {Setup};
\node[ \node[
rectangle, rectangle,
dotted, dotted,
draw, draw,
minimum width=0.5\hsize-4pt, minimum width=0.5\hsize-4pt,
minimum height=0.5\textheight-4pt, minimum height=0.5\textheight-4pt,
align=right align=right
] at (0, -2pt) {}; ] at (0, -2pt) {};
\node[anchor=south west] at (0, -0.5\textheight) {Pre-game}; \node[anchor=south west] at (0, -0.5\textheight) {Pre-game};
\node[ \node[
rectangle, rectangle,
dotted, dotted,
draw, draw,
minimum width=0.5\hsize-4pt, minimum width=0.5\hsize-4pt,
minimum height=\textheight-2pt, minimum height=\textheight-2pt,
align=right, align=right,
] at (0.5\hsize+2pt, 0.5\textheight) {}; ] at (0.5\hsize+2pt, 0.5\textheight) {};
% This node doesnt position correctly...... % This node doesnt position correctly......
\node[anchor=south west] at (0.5\hsize+4pt, -0.5\textheight+2pt) {Game}; \node[anchor=south west] at (0.5\hsize+4pt, -0.5\textheight+2pt) {Game};
% Player connect handling % Player connect handling
\node[draw=blue!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Connect) at (56pt, 0.5\textheight-4pt) {Player connects}; \node[draw=blue!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Connect) at (56pt, 0.5\textheight-4pt) {Player connects};
\node[draw=black!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Addplayer) at (56pt, 0.5\textheight-36pt) {Add player $P_i$}; \node[draw=black!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Addplayer) at (56pt, 0.5\textheight-36pt) {Add player $P_i$};
\draw[very thick,->] (Connect) -- (Addplayer); \draw[very thick,->] (Connect) -- (Addplayer);
\node[draw=green!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Announce) at (56pt, 0.5\textheight-68pt) {Announce self}; \node[draw=green!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Announce) at (56pt, 0.5\textheight-68pt) {Announce self};
\draw[very thick,->] (Addplayer) -- (Announce); \draw[very thick,->] (Addplayer) -- (Announce);
\node[draw=black!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Starttimer) at (56pt, 0.5\textheight-100pt) {Start timer $T_i$}; \node[draw=black!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Starttimer) at (56pt, 0.5\textheight-100pt) {Start timer $T_i$};
\draw[very thick,->] (Announce) -- (Starttimer); \draw[very thick,->] (Announce) -- (Starttimer);
% Player disconnect handling % Player disconnect handling
\node[draw=blue!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Disconnect) at (170pt, 0.5\textheight-4pt) {Player disconnects}; \node[draw=blue!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Disconnect) at (170pt, 0.5\textheight-4pt) {Player disconnects};
\node[draw=red!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Removeplayer) at (170pt, 0.5\textheight-36pt) {Remove player $P_i$}; \node[draw=red!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Removeplayer) at (170pt, 0.5\textheight-36pt) {Remove player $P_i$};
\draw[very thick,dashed,->] (Starttimer)-- (170pt, 0.5\textheight-109.5pt)-- node[right] {Timer expires} ++(Removeplayer); \draw[very thick,dashed,->] (Starttimer)-- (170pt, 0.5\textheight-109.5pt)-- node[right] {Timer expires} ++(Removeplayer);
\draw[very thick,->] (Disconnect) -- (Removeplayer); \draw[very thick,->] (Disconnect) -- (Removeplayer);
% Player keep-alive handling % Player keep-alive handling
\node[draw=blue!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Keepalive) at (290pt, 0.5\textheight-4pt) {Player keep-alives}; \node[draw=blue!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Keepalive) at (290pt, 0.5\textheight-4pt) {Player keep-alives};
\node[draw=black!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Resettimer) at (290pt, 0.5\textheight-36pt) {Reset timer $T_i$}; \node[draw=black!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Resettimer) at (290pt, 0.5\textheight-36pt) {Reset timer $T_i$};
\draw[very thick,->] (Keepalive) -- (Resettimer); \draw[very thick,->] (Keepalive) -- (Resettimer);
% Player ready handling % Player ready handling
\node[draw=blue!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Ready) at (170pt, 80pt) {Player becomes ready}; \node[draw=blue!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Ready) at (170pt, 80pt) {Player becomes ready};
\node[draw=black!50,rectangle,fill=white,very thick,rounded corners=0.1mm,anchor=north] (MoveStage1) at (170pt, 10pt) {Update game stage}; \node[draw=black!50,rectangle,fill=white,very thick,rounded corners=0.1mm,anchor=north] (MoveStage1) at (170pt, 10pt) {Update game stage};
\node[draw=green!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Random1) at (170pt, -22pt) {Decide first player}; \node[draw=green!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Random1) at (170pt, -22pt) {Decide first player};
\draw[very thick,dashed,->] (Ready)-- node[right] {All players ready} ++(MoveStage1); \draw[very thick,dashed,->] (Ready)-- node[right] {All players ready} ++(MoveStage1);
\draw[very thick,->] (MoveStage1) -- (Random1); \draw[very thick,->] (MoveStage1) -- (Random1);
% Player connect handling % Player connect handling
\node[draw=blue!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Act1) at (56pt, -50pt) {Player acts}; \node[draw=blue!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Act1) at (56pt, -50pt) {Player acts};
\end{tikzpicture}\end{landscape} \end{tikzpicture}\end{landscape}
\subsection{Message structure} \section{Message structure}
Messages are given a fixed structure to make processing simpler. Each JSON message holds an \texttt{author} field, being the sender's ID; a message ID to prevent replay attacks and associate related messages; and an \texttt{action}, which at a high level dictates how each client should process the message. Each JSON message holds an \texttt{author} field, being the sender's ID; a message ID to associate related messages; a timestamp to prevent replay attacks; and an \texttt{action}, which at a high level dictates how each client should process the message.
The action more specifically is one of \texttt{ANNOUNCE}, \texttt{DISCONNECT}, \texttt{KEEPALIVE}, \texttt{RANDOM}, \texttt{PROOF}, and \texttt{ACT}. The first three of these are used for managing the network by ensuring peers are aware of each other and know the state of the network. \texttt{RANDOM} and \texttt{PROOF} are designated to be used by sub-protocols defined later on. \texttt{ACT} is used by players to submit actions for their turn during gameplay. The "action" is one of \texttt{ANNOUNCE}, \texttt{DISCONNECT}, \texttt{KEEPALIVE}, \texttt{RANDOM}, \texttt{PROOF}, and \texttt{ACT}. The first three of these are used for managing the network by ensuring peers are aware of each other and know the state of the network. \texttt{ANNOUNCE} is transmitted upon a player joining to ensure the new player is aware of all other players. The \texttt{ANNOUNCE} message contains the player's encryption keys and the player's ID.
Each message is also signed to verify the author. This is a standard application of RSA. A hash of the message is taken, then encrypted with the private key. This can be verified with the public key. \texttt{RANDOM} and \texttt{PROOF} are designated to be used by sub-protocols defined later on. \texttt{ACT} is used by players to submit actions for their turn during gameplay.
Each message is also signed to verify the author. This is a standard application of RSA. A SHA-3 hash of the message is taken, then encrypted with the private key. This can be verified with the public key.
Players trust RSA keys on a trust-on-first-use (TOFU) basis. TOFU is the same protocol as used by Gemini \cite{projectgemini}. The main issue with TOFU is that if a malicious party intercepts the first communication, they may substitute the RSA credentials transmitted by the intended party, resulting in a man-in-the-middle attack. Players trust RSA keys on a trust-on-first-use (TOFU) basis. TOFU is the same protocol as used by Gemini \cite{projectgemini}. The main issue with TOFU is that if a malicious party intercepts the first communication, they may substitute the RSA credentials transmitted by the intended party, resulting in a man-in-the-middle attack.
@ -445,7 +431,7 @@ Besides reducing the number of operations to perform exponentiation, exponentiat
In Jurik's form, we also need to compute $h$, a generator of the Jacobi subgroup, and impose restrictions on $p, q$. In particular, it is required that $p \equiv q \equiv 3 \mod 4$, $\gcd(p-1, q-1) = 2$, and that $p-1, q-1$ consist of large factors except for 2. One method to guarantee this is to use safe primes, which are primes of form $2p+1$ for $p$ prime. In Jurik's form, we also need to compute $h$, a generator of the Jacobi subgroup, and impose restrictions on $p, q$. In particular, it is required that $p \equiv q \equiv 3 \mod 4$, $\gcd(p-1, q-1) = 2$, and that $p-1, q-1$ consist of large factors except for 2. One method to guarantee this is to use safe primes, which are primes of form $2p+1$ for $p$ prime.
\begin{proposition} \begin{proposition}
Safe primes are of form $p \equiv 3 \mod 4$ For $p > 5$ a safe prime, $p \equiv 3 \mod 4$
\end{proposition} \end{proposition}
\begin{proof} \begin{proof}
@ -457,7 +443,7 @@ In Jurik's form, we also need to compute $h$, a generator of the Jacobi subgroup
\end{proof} \end{proof}
\begin{proposition} \begin{proposition}
Safe primes $p \neq q$ satisfy $\gcd(p - 1, q - 1) = 2$ For safe primes $p \neq q$ with $p, q > 5$, $\gcd(p - 1, q - 1) = 2$
\end{proposition} \end{proposition}
\begin{proof} \begin{proof}
@ -486,7 +472,7 @@ To achieve a better speed-up, pre-computation of the fixed base $h^n \bmod n$ is
\Function{FixedBaseExp}{$b$} \Function{FixedBaseExp}{$b$}
\State $index \gets 0$ \State $index \gets 0$
\State $counter \gets 1$ \State $counter \gets 1$
\While{$b \neq 0$} \While{$b \neq 0$}
\If {$b \equiv 1 \mod 2$} \If {$b \equiv 1 \mod 2$}
\State $ctr \gets ctr \times h[i]$ \State $ctr \gets ctr \times h[i]$
@ -498,18 +484,17 @@ To achieve a better speed-up, pre-computation of the fixed base $h^n \bmod n$ is
\EndFunction \EndFunction
\end{algorithmic} \end{algorithmic}
%todo got up to here
\subsection{Private key} \subsection{Private key}
The private key is the value of the Carmichael function $\lambda = \lambda(n)$, defined as the exponent of the group $\mathbb{Z}^*_n$. From the Chinese remainder theorem, $\lambda(n) = \lambda(pq)$ can be computed as $\lcm(\lambda(p), \lambda(q))$. From Carmichael's theorem, this is equivalent to $\lcm(\phi(p), \phi(q))$. Hence, from the definition of $\phi$, and as $p, q$ are equal length, $\lambda = (p - 1)(q - 1) = \phi(n)$. The private key is the value of the Carmichael function $\lambda = \lambda(n)$, defined as the exponent of the group $\mathbb{Z}^*_n$. From the Chinese remainder theorem, $\lambda(n) = \lambda(pq)$ can be computed as $\lcm(\lambda(p), \lambda(q))$. From Carmichael's theorem, this is equivalent to $\lcm(\phi(p), \phi(q))$. Hence, from the definition of $\phi$, and as $p, q$ are equal length, $\lambda = (p - 1)(q - 1) = \phi(n)$.
We are also interested in the ability to compute $\mu = \lambda^{-1} \mod n$ as part of decryption. Fortunately, this is easy, as from Euler's theorem, $\lambda^{\phi(n)} \equiv 1 \mod n$, and so we propose $\mu = \lambda^{\phi(n) - 1} \mod n$. As $\phi(n)$ is easily computable with knowledge of $p, q$, we get ${\mu = \lambda^{(p - 1)(q - 1)} \mod n}$, a relatively straight-forward computation. We also need to compute $\mu = \lambda^{-1} \mod n$ as part of decryption. Fortunately, this is easy, as from Euler's theorem, $\lambda^{\phi(n)} \equiv 1 \mod n$, and so we propose $\mu = \lambda^{\phi(n) - 1} \mod n$. As $\phi(n)$ is easily computable with knowledge of $p, q$, we get ${\mu = \lambda^{(p - 1)(q - 1)} \mod n}$, a relatively straight-forward computation.
\subsection{Decryption} \subsection{Decryption}
Let $c$ be the ciphertext. The corresponding plaintext is computed as \begin{align*} Let $c$ be the ciphertext. The corresponding plaintext is computed as \begin{align*}
m = L(c^\lambda \mod n^2) \cdot \mu \mod n, m = L(c^\lambda \mod n^2) \cdot \mu \mod n,
\end{align*} where $L(x) = \frac{x - 1}{n}$. This operation can be optimised by applying Chinese remainder theorem. However, in the application presented, decryption is not used and is only useful as a debugging measure. So this need not be optimised. \end{align*} where $L(x) = \frac{x - 1}{n}$. This operation can be optimised by applying Chinese remainder theorem. However, in the application presented, decryption is not used and is only useful as a debugging measure. So this optimisation is not applied.
\subsection{Implementation details} \subsection{Implementation details}
@ -551,7 +536,7 @@ This is achieved through bit-commitment and properties of $\mathbb{Z}_n$. The pr
To generalise this to $n$ peers, we ensure that each peer waits to receive all encrypted noises before transmitting their decryption key. To generalise this to $n$ peers, we ensure that each peer waits to receive all encrypted noises before transmitting their decryption key.
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 probability of the final result being a 1 are significantly higher than the probability of a 0 or a 2. This is a typical example of modular bias. To avoid this problem, peers should agree beforehand on the number of bits to transmit. Addition of noise will then operate modulo $2^\ell$, where $\ell$ is the agreed-upon number of bits. 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 an 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 probability of the final result being a 1 is significantly higher than the probability of a 0 or a 2. This is a typical example of modular bias. To avoid this problem, peers should agree beforehand on the number of bits to transmit. To combine noises, then use the XOR operation.
The encryption function used must also guarantee the integrity of decrypted ciphertexts to prevent a malicious party creating a ciphertext which decrypts to multiple valid values through using different keys. The encryption function used must also guarantee the integrity of decrypted ciphertexts to prevent a malicious party creating a ciphertext which decrypts to multiple valid values through using different keys.
@ -573,7 +558,9 @@ The encryption function used must also guarantee the integrity of decrypted ciph
This extends inductively to support $n-1$ cheating participants, even if colluding. Finally, we must consider how to reduce random noise to useful values. This extends inductively to support $n-1$ cheating participants, even if colluding. Finally, we must consider how to reduce random noise to useful values.
\subsection{Avoiding modular bias} \subsection{Modular bias}
A common approach is to take the modulus of the random noise. This causes modular bias to appear however, where some values are less likely to be generated.
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 probability 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}$. 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 probability 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}$.
@ -711,9 +698,9 @@ Additionally, we can consider this protocol perfect zero-knowledge.
In reality, as we are using Jurik's form of Paillier, the best we can hope for is computational zero-knowledge, as Jurik's form relies upon the computational indistinguishability of the sequence generated by powers of $h$ to random powers. In reality, as we are using Jurik's form of Paillier, the best we can hope for is computational zero-knowledge, as Jurik's form relies upon the computational indistinguishability of the sequence generated by powers of $h$ to random powers.
\subsection{Extending \hyperref[protocol1]{Protocol~\ref*{protocol1}} for adjacency} \subsection{Proving fortifications}
Performing a "fortify" action is a more complicated variant of the "reinforce" action. We devise a modified Performing a "fortify" action is distinct to the "reinforce" action and requires its own verification.
Firstly, the set being proven on changes form to $k, -k, 0, \dots, 0$, for a movement of $k$ units from one region to another. The challenges then change form to be proving that either these sum to zero, or that all but two are zero, with the remaining pair summing to zero (this appropriately hides the true value of $k$). Firstly, the set being proven on changes form to $k, -k, 0, \dots, 0$, for a movement of $k$ units from one region to another. The challenges then change form to be proving that either these sum to zero, or that all but two are zero, with the remaining pair summing to zero (this appropriately hides the true value of $k$).
@ -815,7 +802,7 @@ The other proofs do not translate so trivially to this structure however. In fac
All measurements were taken on Brave 1.50.114 (Chromium 112.0.5615.49) 64-bit, using a Ryzen 5 3600 CPU: a consumer CPU from 2019. Absolute timings are extremely dependent on the browser engine: for example Firefox 111.0.1 was typically 4 times slower than the results shown. All measurements were taken on Brave 1.50.114 (Chromium 112.0.5615.49) 64-bit, using a Ryzen 5 3600 CPU: a consumer CPU from 2019. Absolute timings are extremely dependent on the browser engine: for example Firefox 111.0.1 was typically 4 times slower than the results shown.
\begin{landscape} \begin{landscape}
\begin{table} \begin{table}
\fontsize{10pt}{10pt}\selectfont \fontsize{10pt}{10pt}\selectfont
\caption{Time to encrypt} \caption{Time to encrypt}