diff --git a/README.md b/README.md index 61b895e..0fea09d 100644 --- a/README.md +++ b/README.md @@ -33,3 +33,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 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. + +### Playing the game + +1. Open the game in two separate tabs +2. Press "Ready" on both tabs once they are fully loaded diff --git a/static/js/modules/crypto/paillier.js b/static/js/modules/crypto/paillier.js index 4d1911c..caa317b 100644 --- a/static/js/modules/crypto/paillier.js +++ b/static/js/modules/crypto/paillier.js @@ -30,6 +30,10 @@ function RSTransform(g, a, p) { class Ciphertext { constructor(key, plainText, r, set) { + while (plainText < 0n) { + plainText += key.n2; + } + if (set !== undefined) { this.pubKey = key; this.plainText = plainText; @@ -76,10 +80,6 @@ class Ciphertext { this.pubKey = key; this.plainText = plainText; - while (this.plainText < 0n) { - this.plainText += key.n2; - } - this.readOnly = false; } @@ -183,6 +183,10 @@ window.Ciphertext = Ciphertext; export class ReadOnlyCiphertext { constructor(key, cipherText) { + if (typeof cipherText !== "bigint") { + throw "ReadOnlyCiphertext must take BigInt parameter"; + } + this.cipherText = cipherText; this.pubKey = key; @@ -291,13 +295,20 @@ export class PaillierPubKey { this._h_cache = []; this._hn_cache = []; - for (let i = 0n; i < BigInt(KEY_SIZE); i++) { - this._h_cache.push(mod_exp(this.h, 2n ** i, this.n)); - this._hn_cache.push(mod_exp(this.hn, 2n ** i, this.n2)); + // Browser dies on higher key sizes :P + if (KEY_SIZE <= 1024) { + for (let i = 0n; i < BigInt(KEY_SIZE); i++) { + this._h_cache.push(mod_exp(this.h, 2n ** i, this.n)); + this._hn_cache.push(mod_exp(this.hn, 2n ** i, this.n2)); + } } } h_exp(b) { + if (KEY_SIZE > 1024) { + return mod_exp(this.h, b, this.n); + } + let ctr = 1n; let i = 0; while (b !== 0n) { @@ -313,6 +324,10 @@ export class PaillierPubKey { } hn_exp(b) { + if (KEY_SIZE > 1024) { + return mod_exp(this.hn, b, this.n2); + } + let ctr = 1n; let i = 0; while (b !== 0n) { diff --git a/static/js/modules/crypto/random_primes.js b/static/js/modules/crypto/random_primes.js index 46fe7cf..04ec236 100644 --- a/static/js/modules/crypto/random_primes.js +++ b/static/js/modules/crypto/random_primes.js @@ -1,6 +1,6 @@ import { mod_exp } from "./math.js"; -export const KEY_SIZE = 512; +export const KEY_SIZE = 2048; export function cryptoRandom(bits) { if (bits === undefined) { @@ -100,7 +100,8 @@ export function generate_prime() { export function generate_safe_prime() { while (true) { let n = generate_prime(); - if (small_prime_test((n - 1n) / 2n) && miller_rabin((n - 1n) / 2n, 40)) { + // This does not generate safe primes! But it takes forever to find safe primes of size 1024, so this will do. + if (small_prime_test((n - 1n) / 2n) /* && miller_rabin((n - 1n) / 2n, 40) */) { return n; } } diff --git a/static/js/modules/interface/proofs.js b/static/js/modules/interface/proofs.js index 7c4604a..cc79428 100644 --- a/static/js/modules/interface/proofs.js +++ b/static/js/modules/interface/proofs.js @@ -1,3 +1,23 @@ +import { cryptoRandom } from "../crypto/random_primes.js"; + +function cryptoRange(upper) { + // This is ridiculous: why implement a BigInt primitive, have it behave like a number, and then _not_ offer + // mathematical operations like ilog2? Thankfully JavaScript is, for some reason, relatively fast at processing + // strings (that is relative to it processing anything else) + // + // Actual comment: subtract 1 is important as otherwise this overestimates the logarithm for powers of two. + let bitLength = BigInt((upper - 1n).toString(2).length + 1); + let mask = 2n ** bitLength - 1n; + + let r = cryptoRandom(1024); + + while ((r & mask) >= BigInt(upper)) { + r >>= bitLength; + } + + return r & mask; +} + /** * CSPRNG Fisher-Yates shuffle. * @@ -164,4 +184,118 @@ window.verifyRegions = verifyRegions; // verifyRegions(proveRegions({A:paillier.pubKey.encrypt(0n),B:paillier.pubKey.encrypt(1n),C:paillier.pubKey.encrypt(0n),D:paillier.pubKey.encrypt(0n),E:paillier.pubKey.encrypt(0n)}), paillier.pubKey) -function proveRange() {} +export function proveRange(cipherText, rangeUpper) { + if (cipherText.readOnly) { + throw "Cannot prove range of ReadOnlyCiphertext"; + } + + let key = cipherText.pubKey; + + let proofs = []; + + // Construct \omega_1, \omega_2, ciphertexts + for (let i = 0; i < ROUNDS; i++) { + let o1 = cryptoRange(rangeUpper); + let o2 = o1 - rangeUpper; + + let cs = cryptoShuffle([key.encrypt(o1), key.encrypt(o2)]); + + proofs.push({ + cs: cs, + }); + } + + let coins = getCoins(JSON.stringify(proofs)); + let verifications = []; + + for (let i = 0; i < ROUNDS; i++) { + let coin = coins[i]; + let proof = proofs[i]; + + if (coin === 1) { + // Prove that ciphertexts are valid + verifications.push({ + c1: proof.cs[0].proveNI(), + c2: proof.cs[1].proveNI(), + }); + } else { + // Prove that one of the sums is valid + if ((cipherText.plainText + proof.cs[0].plainText) % key.n2 <= rangeUpper) { + let ct = cipherText.clone(); + ct.update(proof.cs[0]); + + verifications.push({ + csIndex: 0, + proof: ct.proveNI(), + }); + } else { + let ct = cipherText.clone(); + ct.update(proof.cs[1]); + + verifications.push({ + csIndex: 1, + proof: ct.proveNI(), + }); + } + } + } + + return { + cipherText: cipherText, + rangeUpper: "0x" + rangeUpper.toString(16), + proofs: proofs, + verifications: verifications, + }; +} + +window.proveRange = proveRange; + +export function verifyRange(obj, key) { + let coins = getCoins(JSON.stringify(obj.proofs)); + let rangeUpper = BigInt(obj.rangeUpper); + + for (let i = 0; i < ROUNDS; i++) { + let coin = coins[i]; + let proof = obj.proofs[i]; + let verification = obj.verifications[i]; + + if (coin === 1) { + let c1 = new ReadOnlyCiphertext(key, BigInt(proof.cs[0])); + let c2 = new ReadOnlyCiphertext(key, BigInt(proof.cs[1])); + let o1 = c1.verifyNI(verification.c1); + let o2 = c2.verifyNI(verification.c2); + let diff; + + if (o1 < o2) { + o2 -= key.n2; + diff = o1 - o2; + } else { + o1 -= key.n2; + diff = o2 - o1; + } + + if (diff !== rangeUpper) { + return false; + } + } else { + let c = new ReadOnlyCiphertext(key, BigInt(proof.cs[verification.csIndex])); + c.update(new ReadOnlyCiphertext(key, BigInt(obj.cipherText))); + let value = c.verifyNI(verification.proof); + + if (value === null || value > rangeUpper) { + return false; + } + } + } + + return true; +} + +window.verifyRange = verifyRange; + +/** + * - We prove that the set contains |S| - 2 zeros, with the final pair summing to 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 + */ +export function proveFortify() {} diff --git a/templates/index.html b/templates/index.html index 081cb68..b28a7ac 100644 --- a/templates/index.html +++ b/templates/index.html @@ -288,13 +288,13 @@ console.log("Benching") - performance.mark("paillier-start") + performance.mark("p4-start") for (let i = 0; i < ROUNDS; i++) { proveRegions(regions) } - performance.mark("paillier-end") + performance.mark("p4-end") - console.log(`Bench done. Time per proof: ${performance.measure("paillier-duration", "paillier-start", "paillier-end").duration / ROUNDS}`) + console.log(`Bench done. Time per proof: ${performance.measure("p4-duration", "p4-start", "p4-end").duration / ROUNDS}`) } function Protocol4VerifierBench() { @@ -316,13 +316,56 @@ console.log("Benching") - performance.mark("paillier-start") + performance.mark("pv-start") for (let i = 0; i < ROUNDS; i++) { verifyRegions(proof, paillier.pubKey) } - performance.mark("paillier-end") + performance.mark("pv-end") - console.log(`Bench done. Time per verification: ${performance.measure("paillier-duration", "paillier-start", "paillier-end").duration / ROUNDS}`) + console.log(`Bench done. Time per verification: ${performance.measure("pv-duration", "pv-start", "pv-end").duration / ROUNDS}`) + } + + function RangeProofBench() { + console.log("Warming up") + + const ct = paillier.pubKey.encrypt(5n) + const ROUNDS = 20; + + for (let i = 0; i < 5; i++) { + proveRange(ct, 10n) + } + + console.log("Benching") + + performance.mark("rp-start") + for (let i = 0; i < ROUNDS; i++) { + proveRange(ct, 10n) + } + performance.mark("rp-end") + + console.log(`Bench done. Time per proof: ${performance.measure("rp-duration", "rp-start", "rp-end").duration / ROUNDS}`) + } + + function RangeVerifierBench() { + console.log("Warming up") + + const ROUNDS = 20; + const ct = paillier.pubKey.encrypt(5n) + let proof = proveRange(ct, 10n) + + for (let i = 0; i < 5; i++) { + verifyRange(proof, paillier.pubKey) + } + + console.log("Benching") + + performance.mark("rv-start") + for (let i = 0; i < ROUNDS; i++) { + verifyRange(proof, paillier.pubKey) + } + performance.mark("rv-end") + + console.log(`Bench done. Time per verification: ${performance.measure("rv-duration", "rv-start", "rv-end").duration / ROUNDS}`) } // https://gist.github.com/kawanet/352a2ed1d1656816b2bc @@ -357,6 +400,25 @@ }; } + function RangeSize() { + const ct = paillier.pubKey.encrypt(5n) + let ROUNDS = 10; + + let size = 0; + let compressedSize = 0; + + for (let x = 0; x < ROUNDS; x++) { + let s = JSON.stringify(proveRange(ct, 10n)); + size += string_to_buffer(s).byteLength; + compressedSize += LZString.compressToUint8Array(s).length; + } + + return { + size: size / ROUNDS, + compressedSize: compressedSize / ROUNDS + }; + } + function ZeroProofSize() { const ROUNDS = 100; const cipherText = paillier.pubKey.encrypt(0n) diff --git a/whitepaper/Dissertation.bib b/whitepaper/Dissertation.bib index b0359df..6045ef6 100644 --- a/whitepaper/Dissertation.bib +++ b/whitepaper/Dissertation.bib @@ -396,4 +396,18 @@ doi={10.1109/SP.2014.36}} journal={ECMA (European Association for Standardizing Information and Communication Systems), pub-ECMA: adr,}, url = {https://tc39.es/ecma262} year={1999} +} + +@inproceedings{bcdg1987, + author = {Brickell, Ernest F. and Chaum, David and Damg\r{a}rd, Ivan and Graaf, Jeroen van de}, + title = {Gradual and Verifiable Release of a Secret}, + year = {1987}, + isbn = {3540187960}, + publisher = {Springer-Verlag}, + address = {Berlin, Heidelberg}, + abstract = {Protocols are presented allowing someone with a secret discrete logarithm to release it, bit by bit, such that anyone can verify each bit's correctness as they receive it. This new notion of release of secrets generalizes and extends that of the already known exchange of secrets protocols. Consequently, the protocols presented allow exchange of secret discrete logs between any number of parties.The basic protocol solves an even more general problem than that of releasing a discrete log. Given any instance of a discrete log problem in a group with public group operation, the party who knows the solution can make public some interval I and convince anyone that the solution belongs to I , while releasing no additional information, such as any hint as to where in I the solution is.This can be used directly to release a discrete log, or to transfer it securely between different groups, i.e. prove that two instances are related such that knowledge of the solution to one implies knowledge of the solution to the other.We show how this last application can be used to implement a more efficient release protocol by transferring the given discrete log instance to a group with special properties. In this scenario, each bit of the secret can be verified by a single modular squaring, and unlike the direct use of the basic protocol, no interactive proofs are needed after the basic setup has been done.Finally, it is shown how the basic protocol can be used to release the factorization of a public composite number.}, + booktitle = {A Conference on the Theory and Applications of Cryptographic Techniques on Advances in Cryptology}, + pages = {156–166}, + numpages = {11}, + series = {CRYPTO '87} } \ No newline at end of file diff --git a/whitepaper/Dissertation.pdf b/whitepaper/Dissertation.pdf index 5a8faca..74159d9 100644 Binary files a/whitepaper/Dissertation.pdf and b/whitepaper/Dissertation.pdf differ diff --git a/whitepaper/Dissertation.tex b/whitepaper/Dissertation.tex index cb71df8..52a825c 100644 --- a/whitepaper/Dissertation.tex +++ b/whitepaper/Dissertation.tex @@ -638,11 +638,11 @@ Players should prove a number of properties of their game state to each other to (2) and (4) are both covered by the proof above. (3) is okay between two players, as it is a subcase of (2). But in the case of more players, the availability of units should be proven. One way to achieve this is with a range proof. -\cite{Boudot2000EfficientPT} demonstrates a proof that some given ciphertext lies within an interval $[-\ell, 2\ell]$, where $\ell$ is some public value. This proof can easily be manipulated into a proof that a value lies within the interval $[n, 3\ell + n]$ from the additive homomorphic property. By selecting a sufficiently high $\ell$ and appropriate $n$, this proof is appropriate for proving to other players that the number of units being used in an attack is valid. +\cite[Section~2]{bcdg1987} demonstrates a proof that some given ciphertext lies within an interval $[-\ell, 2\ell]$, where $\ell$ is some public value. This proof can easily be manipulated into a proof that a value lies within the interval $[n, 3\ell + n]$ from the additive homomorphic property. By selecting a sufficiently high $\ell$ and appropriate $n$, this proof is appropriate for proving to other players that the number of units being used in an attack is valid. \subsection{Range proof} -\cite{Boudot2000EfficientPT}'s proof is a multi-round proof more similar in structure to the graph isomorphism proof presented in \cite{10.1145/116825.116852}. We select public parameter $\ell$ to be some sufficiently high value that a player's unit count should not exceed during play: an appropriate choice may be 1000. Select $n$ as the number of units that the player is defending with, or in the case of attacking, let $n$ be the number of units that the player is attacking with plus 1 (as is required by the rules of Risk). %todo +\cite{bcdg1987}'s proof is a multi-round proof more similar in structure to the graph isomorphism proof presented in \cite{10.1145/116825.116852}. We select public parameter $\ell$ to be some sufficiently high value that a player's unit count should not exceed during play: an appropriate choice may be 1000. Select $n$ as the number of units that the player is defending with, or in the case of attacking, let $n$ be the number of units that the player is attacking with plus 1 (as is required by the rules of Risk). %todo \subsection{Cheating with negative values} @@ -680,7 +680,7 @@ This protocol has the following properties, given that the proof of zero from be Additionally, we can consider this protocol perfect zero-knowledge. \begin{proposition} - In the random oracle model, \hyperref[protocol1]{Protocol~\ref*{protocol1}} is perfect zero-knowledge. + In the random-oracle and honest-verifier models, \hyperref[protocol1]{Protocol~\ref*{protocol1}} is perfect zero-knowledge. \end{proposition} \begin{proof} @@ -699,6 +699,12 @@ 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. +\subsection{Extending \hyperref[protocol1]{Protocol~\ref*{protocol1}} for adjacency} + +Performing a "fortify" action is a more complicated variant of the "reinforce" action. We devise a modified + +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$). + \subsection{Optimising} It is preferred that these proofs can be performed with only a few communications: this issue is particularly prevalent here as this protocol requires multiple rounds to complete. The independence of each round on the next is a beneficial property, as it means the proof can be performed in parallel, so the prover transmits \textit{all} of their $\psi$'s, then the verifier transmits all of their challenges. However, still is the issue of performing proofs of zero. @@ -816,15 +822,20 @@ All measurements were taken on Brave 1.50.114 (Chromium 112.0.5615.49) 64-bit, u \begin{table} \fontsize{10pt}{10pt}\selectfont \caption{Time\parnote{$|n| = 4096$ uses a less-optimised encryption method, as the browser frequently timed out attempting to pre-compute for the more-optimised version.} to process proofs} - \begin{tabularx}{\hsize}{c *6{>{\Centering}X}} + \begin{tabularx}{\hsize}{c *8{>{\Centering}X}} \toprule - \multirow{2}{*}{Modulus size} & \multicolumn{2}{c}{Proof-of-zero non-interactive} & \multicolumn{2}{c}{\hyperref[protocol1]{Protocol~\ref*{protocol1}} with $t = 24$} & \multicolumn{2}{c}{\hyperref[protocol1]{Protocol~\ref*{protocol1}} with $t = 48$} \tabularnewline - \cmidrule(l){2-3}\cmidrule(l){4-5}\cmidrule(l){6-7} - & Prover & Verifier & Prover & Verifier & Prover & Verifier \\ + \multirow{2}{*}{Modulus size} & + \multicolumn{2}{c}{Proof-of-zero non-interactive} & + \multicolumn{2}{c}{\hyperref[protocol1]{Protocol~\ref*{protocol1}} with $t = 24$} & + \multicolumn{2}{c}{\hyperref[protocol1]{Protocol~\ref*{protocol1}} with $t = 48$} & + \multicolumn{2}{c}{BCDG Range with $t = 24$} + \tabularnewline + \cmidrule(l){2-3}\cmidrule(l){4-5}\cmidrule(l){6-7}\cmidrule(l){8-9} + & Prover & Verifier & Prover & Verifier & Prover & Verifier & Prover & Verifier \\ \midrule - $|n| = 1024$ & 10ms & 18ms & 1,420ms & 2,140ms & 2,900ms & 4,270ms \\ - $|n| = 2048$ & 44ms & 68ms & 6,390ms & 8,140ms & 13,200ms & 16,200ms \\ - $|n| = 4096$ & 225ms & 292ms & 41,500ms & 34,400ms & 83,200ms & 68,400ms \\ + $|n| = 1024$ & 10ms & 18ms & 1,420ms & 2,140ms & 2,900ms & 4,270ms & 443ms & 655ms \\ + $|n| = 2048$ & 44ms & 68ms & 6,390ms & 8,140ms & 13,200ms & 16,200ms & 1,980ms & 2,400ms \\ + $|n| = 4096$ & 225ms & 292ms & 41,500ms & 34,400ms & 83,200ms & 68,400ms & 14,300ms & 11,400ms \\ \bottomrule \end{tabularx} \parnotes @@ -834,15 +845,16 @@ All measurements were taken on Brave 1.50.114 (Chromium 112.0.5615.49) 64-bit, u \fontsize{10pt}{10pt}\selectfont \caption{Byte size\parnote{1 UTF-16 character, as used by ECMAScript \cite[Section~6.1.4]{ecma2024262}, is 2 or more bytes.} of encoded proofs} \label{table3} - \begin{tabularx}{\hsize}{c *6{>{\Centering}X}} + \begin{tabularx}{\hsize}{c *8{>{\Centering}X}} \toprule - \multirow{2}{*}{Modulus size} & \multicolumn{2}{c}{Proof-of-zero non-interactive} & \multicolumn{2}{c}{\hyperref[protocol1]{Protocol~\ref*{protocol1}} with $t = 24$} & \multicolumn{2}{c}{\hyperref[protocol1]{Protocol~\ref*{protocol1}} with $t = 48$} \tabularnewline - \cmidrule(l){2-3}\cmidrule(l){4-5}\cmidrule(l){6-7} - & JSON & with LZ-String & JSON & with LZ-String & JSON & with LZ-String \\ + \multirow{2}{*}{Modulus size} & \multicolumn{2}{c}{Proof-of-zero non-interactive} & \multicolumn{2}{c}{\hyperref[protocol1]{Protocol~\ref*{protocol1}} with $t = 24$} & \multicolumn{2}{c}{\hyperref[protocol1]{Protocol~\ref*{protocol1}} with $t = 48$} & \multicolumn{2}{c}{BCDG Range with $t = 24$} + \tabularnewline + \cmidrule(l){2-3}\cmidrule(l){4-5}\cmidrule(l){6-7}\cmidrule(l){8-9} + & JSON & with LZ-String & JSON & with LZ-String & JSON & with LZ-String & JSON & with LZ-String \\ \midrule - $|n| = 1024$ & 1,617B & 576B & 338,902B & 95,738B & 673,031B & 186,857B \\ - $|n| = 2048$ & 3,153B & 1,050B & 662,233B & 187,333B & 1,315,463B & 365,086B \\ - $|n| = 4096$ & 6,226B & 1,999B & 1,315,027B & 368,646B & 2,609,131B & 721,891B \\ + $|n| = 1024$ & 1,617B & 576B & 338,902B & 95,738B & 673,031B & 186,857B & 123,354B & 34,857B \\ + $|n| = 2048$ & 3,153B & 1,050B & 662,233B & 187,333B & 1,315,463B & 365,086B & 252,230B & 70,868B \\ + $|n| = 4096$ & 6,226B & 1,999B & 1,315,027B & 368,646B & 2,609,131B & 721,891B & 484,117B & 135,990B \\ \bottomrule \end{tabularx} \parnotes