diff --git a/static/js/modules/interface/main.js b/static/js/modules/interface/main.js index f4b3f9b..53e2d3c 100644 --- a/static/js/modules/interface/main.js +++ b/static/js/modules/interface/main.js @@ -49,7 +49,10 @@ document.addEventListener("DOMContentLoaded", () => { let sig = BigInt(packet.sig); // decrypt and compare signature let dehash = sender.rsaPubKey.encrypt(sig); - let hash = BigInt("0x" + CryptoJS.SHA3(JSON.stringify(data)).toString()); + + let hasher = new jsSHA("SHA3-256", "TEXT"); + hasher.update(JSON.stringify(data)); + let hash = BigInt("0x" + hasher.getHash("HEX")); if (dehash !== hash) { window.console.error(`Signature invalid! Ignoring packet ${data.id}.`); return; diff --git a/static/js/modules/interface/packet.js b/static/js/modules/interface/packet.js index 2f238cb..213b893 100644 --- a/static/js/modules/interface/packet.js +++ b/static/js/modules/interface/packet.js @@ -12,16 +12,12 @@ export class Packet { static _sign(data) { // compute hash of data - let hash = CryptoJS.SHA3(JSON.stringify(data)); - let res = 0n; - for (let word32 of hash.words) { - // remove any sign - let bi = BigInt.asUintN(32, BigInt(word32)); - res = (res << 32n) | bi; - } + let hasher = new jsSHA("SHA3-256", "TEXT"); + hasher.update(JSON.stringify(data)); + let hash = BigInt("0x" + hasher.getHash("HEX")); // sign hash - let sig = window.rsa.privKey.decrypt(res); + let sig = window.rsa.privKey.decrypt(hash); // return new packet return { @@ -54,22 +50,23 @@ export class Packet { }); } - static createRandomCyphertext(sessionId, range, cipherText) { + static createRandomHMAC(sessionId, range, hmac, key) { return this._sign({ ...this._createBase("RANDOM"), session: sessionId, range: range, - stage: "CIPHERTEXT", - cipherText: cipherText, + stage: "COMMIT", + hmac: hmac, + key: key, }); } - static createRandomKey(sessionId, key) { + static createRandomNoise(sessionId, noise) { return this._sign({ ...this._createBase("RANDOM"), session: sessionId, - stage: "DECRYPT", - cipherKey: key, + stage: "REVEAL", + noise: noise, }); } diff --git a/static/js/modules/interface/random.js b/static/js/modules/interface/random.js index 2ed81e1..2a688b0 100644 --- a/static/js/modules/interface/random.js +++ b/static/js/modules/interface/random.js @@ -1,19 +1,24 @@ import { socket, game } from "./main.js"; import { Packet } from "./packet.js"; +import { cryptoRandom } from "../crypto/random_primes.js"; class RandomSession { constructor(range) { this.range = range; - this.cipherTexts = {}; - this.cipherKeys = {}; - this.ourKey = CryptoJS.lib.WordArray.random(32).toString(); - this.ourNoise = CryptoJS.lib.WordArray.random(8); + this.hmacs = {}; + this.keys = {}; + this.noises = {}; + this.ourKey = cryptoRandom(1024).toString(16); + this.ourNoise = cryptoRandom(64); this.finalValue = null; this.resolvers = []; } - cipherText() { - return CryptoJS.AES.encrypt(this.ourNoise, this.ourKey).toString(); + hmac() { + let hasher = new jsSHA("SHA3-256", "HEX"); + hasher.update(this.ourKey); + hasher.update(this.ourNoise.toString(16)); + return hasher.getHash("HEX"); } } @@ -58,7 +63,7 @@ export class Random { socket.emit( "message", - Packet.createRandomCyphertext(sessionId, n, session.cipherText()) + Packet.createRandomHMAC(sessionId, n, session.hmac(), session.ourKey) ); return session; @@ -78,41 +83,49 @@ export class Random { session = this.initialiseSession(data.range, data.session); } - if (stage === "CIPHERTEXT") { - session.cipherTexts[data.author] = data.cipherText; + if (stage === "COMMIT") { + session.hmacs[data.author] = data.hmac; + session.keys[data.author] = data.key; if ( - Object.keys(session.cipherTexts).length === + Object.keys(session.hmacs).length === Object.keys(game.players).length - 1 ) { // Step 3: release our key once all players have sent a ciphertext socket.emit( "message", - Packet.createRandomKey(data.session, session.ourKey) + Packet.createRandomNoise( + data.session, + "0x" + session.ourNoise.toString(16) + ) ); } - } else if (stage === "DECRYPT") { - session.cipherKeys[data.author] = data.cipherKey; + } else if (stage === "REVEAL") { + // Check HMAC + let noise = BigInt(data.noise) % 2n ** 64n; + let hasher = new jsSHA("SHA3-256", "HEX"); + hasher.update(session.keys[data.author]); + hasher.update(noise.toString(16)); + let hash = hasher.getHash("HEX"); + + if (hash === session.hmacs[data.author]) { + session.noises[data.author] = noise; + } // Step 4: get final random value if ( - Object.keys(session.cipherKeys).length === + Object.keys(session.noises).length === Object.keys(game.players).length - 1 ) { // Lock out wait calls as they may resolve to never-ending promises. await navigator.locks.request(`random-${data.session}`, () => { - let total = BigInt("0x" + session.ourNoise.toString()); + let total = session.ourNoise; - for (let participant of Object.keys(session.cipherKeys)) { - let decrypted = CryptoJS.AES.decrypt( - session.cipherTexts[participant], - session.cipherKeys[participant] - ).toString(); - - total += BigInt("0x" + decrypted); + for (let noise of Object.values(session.noises)) { + total += noise; } - // Find first good block of bits to avoid modular bias + // Find first good block of bits let blockSize = BigInt(Math.ceil(Math.log2(session.range))); let blockMask = 2n ** blockSize - 1n; diff --git a/templates/index.html b/templates/index.html index b054521..b22aba7 100644 --- a/templates/index.html +++ b/templates/index.html @@ -6,7 +6,6 @@ - diff --git a/whitepaper/Dissertation.bib b/whitepaper/Dissertation.bib index ce52d8b..565c12c 100644 --- a/whitepaper/Dissertation.bib +++ b/whitepaper/Dissertation.bib @@ -286,7 +286,7 @@ howpublished = {\url{https://zcash.readthedocs.io/en/latest/rtd_pages/basics.htm } @misc{jssha, - author = {Caligatio}, + author = {Brian Turek}, title = {{jsSHA: A JavaScript/TypeScript implementation of the complete Secure Hash Standard (SHA) family}}, year = {2022}, publisher = {GitHub}, @@ -521,4 +521,6 @@ howpublished = {\url{https://www.science.org/content/article/why-criminals-cant- author = {"Nir"}, title = {Frequently Asked Questions | Soulseek}, howpublished = {\url{http://www.soulseekqt.net/news/faq-page#t10n606}} -} \ No newline at end of file +} + +@ \ No newline at end of file diff --git a/whitepaper/Dissertation.pdf b/whitepaper/Dissertation.pdf index 6719236..5d3e7ee 100644 Binary files a/whitepaper/Dissertation.pdf and b/whitepaper/Dissertation.pdf differ diff --git a/whitepaper/Dissertation.tex b/whitepaper/Dissertation.tex index 49381a3..27970f2 100644 --- a/whitepaper/Dissertation.tex +++ b/whitepaper/Dissertation.tex @@ -83,6 +83,7 @@ We present a modern implementation of the Paillier cryptosystem for the browser, $\lambda(k)$ & Carmichael's totient function \\ $H(\dots)$ & Ideal cryptographic hash function \\ $\in_R$ & Selection at random \\ + $A \mathbin\Vert B$ & Concatenation of $A$ and $B$ \\ \bottomrule \end{tabularx} \end{table} @@ -598,17 +599,19 @@ This is achieved with a standard application of bit-commitment, and properties o \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,-3)--node [auto] {$H(k_A \mathbin\Vert N_A), k_A$}++(6,0); + \draw [<-,very thick] (0,-4)--node [auto] {$H(k_B \mathbin\Vert N_B), k_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); + \draw [->,very thick] (0,-5)--node [auto] {$N_A$}++(6,0); + \draw [<-,very thick] (0,-6)--node [auto] {$N_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$}; + \node[draw=blue!50,rectangle,thick] (ChA) at (0,-7) {Check $H(k_B \mathbin\Vert N_B)$}; + \node[draw=blue!50,rectangle,thick] (ChB) at (6,-7) {Check $H(k_A \mathbin\Vert N_A)$}; + \node[draw=blue!50,rectangle,thick] (CA) at (0,-8) {Compute $N_A + N_B$}; + \node[draw=blue!50,rectangle,thick] (CB) at (6,-8) {Compute $N_A + N_B$}; - \draw [very thick] (A)-- (NoiseA)-- (CA)-- (0,-7); - \draw [very thick] (B)-- (NoiseB)-- (CB)-- (6,-7); + \draw [very thick] (A)-- (NoiseA)-- (ChA)-- (CA)-- (0,-8); + \draw [very thick] (B)-- (NoiseB)-- (ChB)-- (CB)-- (6,-8); \end{tikzpicture} \end{center} \end{protocol} @@ -617,7 +620,7 @@ To generalise this to $n$ peers, we ensure that each peer waits to receive all e Depending on how $N_A + N_B$ is then moved into the required 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, and compute the final value as $N_A \oplus N_B$. -The encryption function used must also guarantee the integrity of decrypted ciphertexts. Otherwise, a malicious party could create a ciphertext which decrypts to multiple valid values by using different keys. +The hash function used must also be resistant to length-extension attacks for the presented protocol. In general, a hash-based message authentication code can be used. \begin{proposition} With the above considerations, the scheme shown is not manipulable by a single cheater. @@ -626,13 +629,13 @@ The encryption function used must also guarantee the integrity of decrypted ciph \begin{proof} Suppose $P_1, \dots, P_{n-1}$ are honest participants, and $P_n$ is a cheater with a desired outcome. - In step 1, each participant $P_i$ commits $E_{k_i}(N_i)$. The cheater $P_n$ commits a constructed noise $E_{k_n}(N_n)$. + In step 1, each participant $P_i$ commits $H(k_i \mathbin\Vert N_i)$. The cheater $P_n$ commits $H(k_n \mathbin\Vert N_n)$. - The encryption function $E_k$ holds the confidentiality property: that is, without $k$, $P_i$ cannot retrieve $m$ given $E_k(m)$. So $P_n$'s choice of $N_n$ cannot be directed by other commitments. + The hash function $H$ holds the preimage resistance property: that is, $P_i$ cannot find $m$ given $H(m)$. So $P_n$'s choice of $N_n$ cannot be directed by other commitments. The final value is dictated by the sum of all decrypted values. $P_n$ is therefore left in a position of choosing $N_n$ to control the outcome of $a + N_n$, where $a$ is selected uniformly at random from the abelian group $\mathbb{Z}_{2^\ell}$ for $\ell$ the agreed upon bit length. - As every element of this group is of order $2^\ell$, the distribution of $a + N_n$ is identical regardless of the choice of $N_n$. So $P_n$ maintains no control over the outcome of $a + N_n$. + As every element of this group is of order $2^\ell$, the distribution of $a + N_n$ is identical regardless of the choice of $N_n$. As $P_n$ cannot reasonably find a collision for $H(k_n \mathbin\Vert N_n)$, $P_n$ must reveal $N_n$. So $P_n$ maintains no control over the outcome of $a + N_n$. \end{proof} This extends inductively to support $n-1$ cheating participants, even if colluding. Finally, we must consider how to reduce random noise to useful values. @@ -652,6 +655,8 @@ Random values are used in two places. \begin{itemize} As this protocol must run many times during a game, we consider each operation of the protocol as a "session", each of which has a unique name that is derived from the context. A benefit of this is that the unique name can be used with the Web Locks API to prevent race conditions that may occur due to this protocol running asynchronously. +To achieve bit-commitment, we use SHA-3 \cite{FIPS202}, as implemented by jsSHA \cite{jssha}. SHA-3 is resistant to length-extension attacks, and is considered secure, so it is reasonable to assume that a malicious player will not be able to find a collision. + \section{Proof system} Players should prove a number of properties of their game state to each other to ensure fair play. These are as follows. \begin{enumerate}