Riskless/static/js/modules/crypto/paillier.js
2023-04-21 15:03:15 +01:00

408 lines
10 KiB
JavaScript

import {
cryptoRandom,
generate_prime,
generate_safe_prime,
KEY_SIZE,
} from "./random_primes.js";
import { gcd, mod_exp } from "./math.js";
const PAILLIER = 0;
const JURIK = 1;
function RSTransform(g, a, p) {
let plainText = p.toString(16);
if (plainText.length % 2 !== 0) {
plainText = "0" + plainText;
}
let aStr = a.toString(16);
if (aStr.length % 2 !== 0) {
aStr = "0" + aStr;
}
let hasher = new jsSHA("SHAKE256", "HEX");
hasher.update(g.toString(16));
hasher.update(plainText);
hasher.update(aStr);
return BigInt("0x" + hasher.getHash("HEX", { outputLen: 2048 }));
}
class Ciphertext {
constructor(key, plainText, r, set) {
while (plainText < 0n) {
plainText += key.n2;
}
if (set !== undefined) {
this.pubKey = key;
this.plainText = plainText;
this.readOnly = false;
return;
}
if (r === undefined) {
// Use the optimised form using Jacobi classes
r = cryptoRandom();
// Compute g^m by binomial theorem.
let gm = (1n + key.n * plainText) % key.n2;
// Compute g^m h^r.
this.cipherText = (gm * key.hn_exp(r)) % key.n2;
// Force into range.
while (this.cipherText < 0n) {
this.cipherText += key.n2;
}
this.mode = JURIK;
this.r = key.h_exp(r);
} else {
// Use the standard form
// Compute g^m by binomial theorem.
let gm = (1n + key.n * plainText) % key.n2;
// Compute g^m r^n.
this.cipherText = (gm * mod_exp(r, key.n, key.n2)) % key.n2;
// Force into range.
while (this.cipherText < 0n) {
this.cipherText += key.n2;
}
this.mode = PAILLIER;
this.r = r;
}
this.pubKey = key;
this.plainText = plainText;
this.readOnly = false;
}
update(c) {
this.cipherText = (this.cipherText * c.cipherText) % this.pubKey.n2;
this.r = (this.r * c.r) % this.pubKey.n2;
this.plainText = (this.plainText + c.plainText) % this.pubKey.n2;
// Force into range
while (this.cipherText < 0n) {
this.cipherText += this.pubKey.n2;
}
while (this.plainText < 0n) {
this.plainText += this.pubKey.n2;
}
}
toString() {
return "0x" + this.cipherText.toString(16);
}
toJSON() {
return "0x" + this.cipherText.toString(16);
}
prove() {
return new ValueProofSessionProver(this);
}
// Construct a non-interactive proof
proveNI() {
let rp = cryptoRandom(KEY_SIZE * 2);
while (rp >= this.pubKey.n) {
rp = cryptoRandom(KEY_SIZE * 2);
}
let a = mod_exp(rp, this.pubKey.n, this.pubKey.n2);
let challenge = RSTransform(this.pubKey.g, a, this.plainText);
return {
plainText: "0x" + this.plainText.toString(16),
a: "0x" + a.toString(16),
proof:
"0x" +
(
((rp % this.pubKey.n) * mod_exp(this.r, challenge, this.pubKey.n)) %
this.pubKey.n
).toString(16),
};
}
asReadOnlyCiphertext() {
return new ReadOnlyCiphertext(this.pubKey, this.cipherText);
}
clone() {
let c = new Ciphertext(this.pubKey, this.plainText, 0, true);
c.cipherText = this.cipherText;
c.r = this.r;
c.mode = this.mode;
return c;
}
}
class ValueProofSessionProver {
constructor(cipherText) {
this.cipherText = cipherText;
this.rp = cryptoRandom(KEY_SIZE * 2);
while (this.rp >= this.cipherText.pubKey.n) {
this.rp = cryptoRandom(KEY_SIZE * 2);
}
}
get a() {
return mod_exp(this.rp, this.cipherText.pubKey.n, this.cipherText.pubKey.n2);
}
noise() {
return mod_exp(this.rp, this.cipherText.pubKey.n, this.cipherText.pubKey.n2);
}
prove(challenge) {
return (
((this.rp % this.cipherText.pubKey.n) *
mod_exp(this.cipherText.r, challenge, this.cipherText.pubKey.n)) %
this.cipherText.pubKey.n
);
}
asVerifier() {
return this.cipherText
.asReadOnlyCiphertext()
.prove(this.cipherText.plainText, this.noise());
}
}
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;
this.readOnly = true;
}
update(c) {
this.cipherText = (this.cipherText * c.cipherText) % this.pubKey.n2;
// Force into range
while (this.cipherText < 0n) {
this.cipherText += this.pubKey.n2;
}
}
prove(plainText, a) {
return new ValueProofSessionVerifier(this, plainText, a);
}
verifyNI(statement) {
let challenge = RSTransform(
this.pubKey.g,
BigInt(statement.a),
BigInt(statement.plainText)
);
let verifier = new ValueProofSessionVerifier(
this,
BigInt(statement.plainText),
BigInt(statement.a),
challenge
);
if (verifier.verify(BigInt(statement.proof))) {
return BigInt(statement.plainText);
} else {
return null;
}
}
clone() {
return new ReadOnlyCiphertext(this.pubKey, this.cipherText);
}
}
class ValueProofSessionVerifier {
constructor(cipherText, plainText, a, challenge) {
// Clone, otherwise the update below will mutate the original value
this.cipherText = cipherText.clone();
this.cipherText.update(this.cipherText.pubKey.encrypt(-1n * plainText, 1n));
if (challenge === undefined) {
// Shift the challenge down by 1 to ensure it is smaller than either prime factor.
this.challenge = cryptoRandom(KEY_SIZE) << 1n;
} else {
this.challenge = challenge;
}
this.a = a;
this.plainText = plainText;
}
verify(proof) {
// check coprimality
if (gcd(proof, this.cipherText.pubKey.n) !== 1n) return -1;
if (gcd(this.cipherText.cipherText, this.cipherText.pubKey.n) !== 1n) return -2;
if (gcd(this.a, this.cipherText.pubKey.n) !== 1n) return -3;
// check exp
return mod_exp(proof, this.cipherText.pubKey.n, this.cipherText.pubKey.n2) ===
(this.a *
mod_exp(
this.cipherText.cipherText,
this.challenge,
this.cipherText.pubKey.n2
)) %
this.cipherText.pubKey.n2
? 1
: -4;
}
}
window.ReadOnlyCiphertext = ReadOnlyCiphertext;
export class PaillierPubKey {
constructor(n, h) {
this.n = n;
if (h === undefined) {
let x = cryptoRandom(KEY_SIZE * 2);
while (x >= this.n) {
x = cryptoRandom(KEY_SIZE * 2);
}
this.h = ((-1n * x ** 2n) % this.n) + this.n;
} else {
this.h = h;
}
this.g = this.n + 1n;
this.n2 = this.n ** 2n;
this.hn = mod_exp(this.h, this.n, this.n2);
this._h_cache = [];
this._hn_cache = [];
// 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) {
if (b % 2n === 1n) {
ctr *= this._h_cache[i];
ctr %= this.n;
}
i++;
b >>= 1n;
}
return ctr;
}
hn_exp(b) {
if (KEY_SIZE > 1024) {
return mod_exp(this.hn, b, this.n2);
}
let ctr = 1n;
let i = 0;
while (b !== 0n) {
if (b % 2n === 1n) {
ctr *= this._hn_cache[i];
ctr %= this.n2;
}
i++;
b >>= 1n;
}
return ctr;
}
encrypt(m, r) {
return new Ciphertext(this, m, r);
}
toJSON() {
return {
n: "0x" + this.n.toString(16),
h: "0x" + this.h.toString(16),
};
}
static fromJSON(data) {
return new PaillierPubKey(BigInt(data.n), BigInt(data.h));
}
}
class PaillierPrivKey {
constructor(p, q) {
this.n = p * q;
// precompute square of n
this.n2 = this.n ** 2n;
this.lambda = (p - 1n) * (q - 1n);
this.mu = mod_exp(this.lambda, this.lambda - 1n, this.n);
}
decrypt(c) {
return (((mod_exp(c, this.lambda, this.n2) - 1n) / this.n) * this.mu) % this.n;
}
}
function check_gcd(primes, new_prime) {
for (let prime of primes) {
if (gcd(prime - 1n, new_prime - 1n) === 2n) {
return prime;
}
}
return null;
}
export function generate_keypair() {
let p, q, pubKey, privKey;
if (
window.sessionStorage.getItem("p") !== null &&
window.sessionStorage.getItem("q") !== null
) {
p = BigInt(window.sessionStorage.getItem("p"));
q = BigInt(window.sessionStorage.getItem("q"));
} else {
p = generate_safe_prime();
q = generate_safe_prime();
}
window.sessionStorage.setItem("p", p);
window.sessionStorage.setItem("q", q);
pubKey = new PaillierPubKey(p * q);
privKey = new PaillierPrivKey(p, q);
return { pubKey, privKey };
}
// p = a.prove(); v = p.asVerifier(); v.verify(p.prove(v.challenge));