import { cryptoRandom } from "../crypto/random_primes.js"; import { Region } from "./map.js"; const ROUNDS = 12; 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. * * Only works on lists up to 255 elements. */ function cryptoShuffle(l) { let out = []; for (let i = l.length - 1; i > 0; i--) { let value = new Uint8Array([0]); crypto.getRandomValues(value); while (value[0] > i) { crypto.getRandomValues(value); } let v = l.splice(value[0], 1); out.push(v[0]); } out.push(l[0]); return out; } /** * R-S transform. * * Uses the hash of the proof content to produce verifier coins */ function getCoins(text) { // Construct verifier coins let hasher = new jsSHA("SHA3-256", "TEXT"); hasher.update(text); let hash = hasher.getHash("UINT8ARRAY"); let verifierCoins = []; for (let i = 0; i < ROUNDS / 8; i++) { let v = hash[i]; for (let j = 0; j < 8; j++) { verifierCoins.push(v & 1); v >>= 1; } } return verifierCoins; } window.cryptoShuffle = cryptoShuffle; export function proveRegions(regions) { // Construct prover coins let proofs = []; let privateInputs = []; let regionNames = Object.keys(regions).sort(); for (let x = 0; x < ROUNDS; x++) { let psi = cryptoShuffle(structuredClone(regionNames)).join(""); let newRegions = structuredClone(regions); // Rearrange keys for (let index = 0; index < regionNames.length; index++) { newRegions[regionNames[index]] = regions[psi[index]].pubKey.encrypt( regions[psi[index]].plainText * -1n ); } proofs.push(newRegions); privateInputs.push(psi); } let verifierCoins = getCoins(JSON.stringify(proofs)); let verifications = []; // Construct prover proofs for (let i = 0; i < ROUNDS; i++) { let coin = verifierCoins[i]; let proof = proofs[i]; let privateInput = privateInputs[i]; if (coin === 1) { // Reveal bijection and proof for zero let zeroProofs = {}; for (let i = 0; i < regionNames.length; i++) { let name = regionNames[i]; let psiName = privateInput[i]; let c = proof[name].clone(); c.update(regions[psiName]); zeroProofs[name] = c.proveNI(); } let ver = { psi: privateInput, zeroProofs: zeroProofs, }; verifications.push(ver); } else { // Reveal proof for plaintext let valueProofs = {}; for (let name of regionNames) { valueProofs[name] = proof[name].proveNI(); } verifications.push({ valueProofs: valueProofs }); } } return { regions: regions, proofs: proofs, verifications: verifications, }; } window.proveRegions = proveRegions; export function verifyRegions(obj, key) { let verifierCoins = getCoins(JSON.stringify(obj.proofs)); let regions = obj.regions; let regionNames = Object.keys(regions).sort(); for (let i = 0; i < ROUNDS; i++) { let proof = obj.proofs[i]; let verification = obj.verifications[i]; if (verifierCoins[i] === 1) { for (let regionName of regionNames) { // Undo psi let originalRegion = proof[regionName]; // Compute product let c = new ReadOnlyCiphertext(key, BigInt(regions[regionName])); c.update(new ReadOnlyCiphertext(key, BigInt(originalRegion))); // Check ciphertext is zero let plaintext = c.verifyNI(verification.zeroProofs[regionName]); if (plaintext !== 0n) { return false; } } } else { let foundOne = false; for (let name of Object.keys(verification.valueProofs)) { let ciphertext = new ReadOnlyCiphertext(key, BigInt(proof[name])); let plaintext = ciphertext.verifyNI(verification.valueProofs[name]); if (plaintext === null) { return false; } else if (plaintext === 1n) { if (foundOne) { return false; } else { foundOne = true; } } } } } return true; } 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) /** * BCDG Range proof */ 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; export function proveBitLength(cipherText) { if (cipherText.readOnly) { throw "Cannot prove readonly ciphertext"; } let key = cipherText.pubKey; // Compute decomposition let bitCommitments = []; let m = cipherText.plainText; let prod = cipherText.clone(); let e = 1n; while (m !== 0n) { let bit = m & 0b1n; let cBit = key.encrypt(-bit); bitCommitments.push(cBit); let cBit2 = cBit.clone(); cBit2.mul(e); prod.update(cBit2); e <<= 1n; m >>= 1n; } // Pad out while (bitCommitments.length < 8) { let c = key.encrypt(0n); bitCommitments.push(c); let c2 = c.clone(); c2.mul(e); prod.update(c2); e <<= 1n; } let bitProofs = []; for (let bitCommitment of bitCommitments) { let p = proveOneOfTwo(bitCommitment); bitProofs.push(p); } return { cipherText: cipherText, bitCommitments: bitCommitments, prodProof: prod.proveNI(), bitProofs: bitProofs, }; } window.proveBitLength = proveBitLength; export function verifyBitLength(obj, key) { // Check product is fine let prod = new ReadOnlyCiphertext(key, BigInt(obj.cipherText)); let m = 1n; for (let bit of obj.bitCommitments) { let cBit = new ReadOnlyCiphertext(key, BigInt(bit)); cBit.mul(m); prod.update(cBit); m >>= 1n; } let p = prod.verifyNI(obj.prodProof); if (p !== 0n) { return null; } for (let proof of obj.bitProofs) { let r = verifyOneOfTwo(proof, key); if (!r) { return null; } } return obj.bitCommitments.length; } window.verifyBitLength = verifyBitLength; /** * Prove that a ciphertext is either a 0 or a -1 */ function proveOneOfTwo(cipherText) { let key = cipherText.pubKey; let proofs = []; for (let x = 0; x < ROUNDS; x++) { proofs.push({ cs: cryptoShuffle([key.encrypt(0n), key.encrypt(1n)]), }); } let verifications = []; let coins = getCoins(JSON.stringify(proofs)); for (let x = 0; x < ROUNDS; x++) { let coin = coins[x]; let proof = proofs[x]; if (coin === 1) { verifications.push({ cProofs: proof.cs.map((p) => p.proveNI()), }); } else { let c1Index; if (cipherText.plainText === paillier.pubKey.n2 - 1n) { c1Index = proof.cs.findIndex((c) => c.plainText === 1n); } else { c1Index = proof.cs.findIndex((c) => c.plainText === 0n); } let c1 = proof.cs[c1Index].clone(); c1.update(cipherText); verifications.push({ csIndex: c1Index, zeroProof: c1.proveNI(), }); } } return { cipherText: cipherText, proofs: proofs, verifications: verifications, }; } window.proveOneOfTwo = proveOneOfTwo; function verifyOneOfTwo(obj, key) { let coins = getCoins(JSON.stringify(obj.proofs)); for (let x = 0; x < ROUNDS; x++) { let coin = coins[x]; let proof = obj.proofs[x]; let verification = obj.verifications[x]; if (coin === 1) { let c1 = new ReadOnlyCiphertext(key, BigInt(proof.cs[0])); let p1 = c1.verifyNI(verification.cProofs[0]); let c2 = new ReadOnlyCiphertext(key, BigInt(proof.cs[1])); let p2 = c2.verifyNI(verification.cProofs[1]); if (!(p1 === 0n && p2 === 1n) && !(p2 === 0n && p1 === 1n)) { return false; } } else { let c = new ReadOnlyCiphertext(key, BigInt(proof.cs[verification.csIndex])); c.update(obj.cipherText); let p = c.verifyNI(verification.zeroProof); if (p !== 0n) { return false; } } } return true; } window.verifyOneOfTwo = verifyOneOfTwo; /** * - 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 a range proof for the new region values */ export function proveFortify(fortify) { let proofs = []; let privateInputs = []; let regionNames = Object.keys(fortify).sort(); for (let x = 0; x < ROUNDS; x++) { let psi = cryptoShuffle(structuredClone(regionNames)).join(""); let psiMap = {}; for (let i = 0; i < regionNames.length; i++) { psiMap[regionNames[i]] = psi[i]; } let newRegions = { regions: {} }; // Rearrange keys for (let r of regionNames) { newRegions.regions[psiMap[r]] = fortify[r].pubKey.encrypt( -fortify[r].plainText ); } let edges = []; let proofEdges = []; // Attach edges for (let i = 0; i < regionNames.length; i++) { let region = regionNames[i]; let psiRegion = psi[i]; for (let n of Region.getRegion(region).neighbours) { if (regionNames.includes(n.name)) { let psiNeighbour = psiMap[n.name]; if (psiNeighbour > psiRegion) { let salt = cryptoRandom(128); let hasher = new jsSHA("SHA3-256", "TEXT"); hasher.update(psiRegion); hasher.update(psiNeighbour); hasher.update(salt.toString(16)); let hash = hasher.getHash("HEX"); edges.push({ hash: hash, salt: salt, edge: [psiRegion, psiNeighbour], }); proofEdges.push(hash); } } } } newRegions.edges = cryptoShuffle(proofEdges); proofs.push(newRegions); privateInputs.push({ psi: psi, edges: edges, }); } let coins = getCoins(JSON.stringify(proofs)); let verifications = []; for (let i = 0; i < ROUNDS; i++) { let coin = coins[i]; let proof = proofs[i].regions; let input = privateInputs[i]; let psiMap = {}; for (let i = 0; i < regionNames.length; i++) { psiMap[regionNames[i]] = input.psi[i]; } if (coin === 1) { // Show |S| - 2 zeroes let verification = { regions: {}, }; let pairCipherText = null; let pair = []; for (let r of regionNames) { if (proof[r].plainText === 0n) { verification.regions[r] = proof[r].proveNI(); } else if (pairCipherText === null) { pairCipherText = proof[r].clone(); pair.push(r); } else { pairCipherText.update(proof[r]); verification.pairCipherText = pairCipherText.proveNI(); pair.push(r); } } // Show pair is joined by edge let pairName = pair.sort(); verification.pairEdgeSalt = "0x" + input.edges .find((e) => e.edge[0] === pairName[0] && e.edge[1] === pairName[1]) .salt.toString(16); verifications.push(verification); } else { // Show isomorphism let edges = {}; for (let e of input.edges) { edges[e.edge[0] + e.edge[1]] = "0x" + e.salt.toString(16); } let zeroProofs = {}; for (let r of regionNames) { let c = proof[psiMap[r]].clone(); c.update(fortify[r]); zeroProofs[r] = c.proveNI(); } verifications.push({ psi: input.psi, salts: edges, zeroProofs: zeroProofs, }); } } return { fortify: fortify, proofs: proofs, verifications: verifications, }; } window.proveFortify = proveFortify; 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(new ReadOnlyCiphertext(key, BigInt(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(BigInt(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(new ReadOnlyCiphertext(key, BigInt(fortify[psiMap[r]]))); let p = c.verifyNI(verification.zeroProofs[r]); if (p !== 0n) { 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(BigInt(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)})