Switch back to using standard BigInteger class instead of wrapper

Using a wrapper requires adding some handling code to fix race conditions,
but it does not provide advantages until we switch to TS.
This commit is contained in:
larabr 2023-10-12 11:16:55 +02:00
parent 9e1962f006
commit 4ee9deae62
6 changed files with 88 additions and 107 deletions

View File

@ -1,39 +1,20 @@
/**
* This is a vanilla JS copy of @openpgp/noble-hashes/esm/biginteger/interface.ts .
* We need to duplicate the file, instead of importing it, since in that case the BigIntegerInterface instance
* would be shared with noble-hashes, which separately calls `setImplementation()` on load, causing it to throw due to
* duplicate initialization.
* We don't use the BigIntegerInterface wrapper from noble-hashes because:
* - importing the instance results in it being shared with noble-hashes, which separately calls `setImplementation()`
* on load, causing it to throw due to duplicate initialization.
* - even duplicating the interface code here to keep a separate instance requires handing a race-conditions the first time
* `getBigInteger` is called, when the code needs to check if the implementation is set, and initialize it if not.
* Ultimately, the interface provides no advantages and it's only needed because of TS.
*/
class BigInteger {
static setImplementation(Implementation, replace = false) {
if (BigInteger.Implementation && !replace) {
throw new Error('Implementation already set');
}
BigInteger.Implementation = Implementation;
}
static new(n) {
return new BigInteger.Implementation(n);
}
}
const detectBigInt = () => typeof BigInt !== 'undefined';
export async function getBigInteger() {
if (BigInteger.Implementation) {
return BigInteger;
}
// TODOOOOO replace = true needed in case of concurrent class loading, how to fix without removing wrapper class?
if (detectBigInt()) {
// NativeBigInteger is small, so it's imported in isolation (it could also be imported at the top level)
const { default: NativeBigInteger } = await import('@openpgp/noble-hashes/esm/biginteger/native.interface');
BigInteger.setImplementation(NativeBigInteger, true);
return NativeBigInteger;
} else {
// FallbackBigInteger relies on large BN.js lib, which is also used by noble-hashes and noble-curves
const { default: FallbackBigInteger } = await import('@openpgp/noble-hashes/esm/biginteger/bn.interface');
BigInteger.setImplementation(FallbackBigInteger, true);
return FallbackBigInteger;
}
return BigInteger;
}

View File

@ -43,11 +43,11 @@ import { isProbablePrime } from './prime';
export async function sign(hashAlgo, hashed, g, p, q, x) {
const BigInteger = await util.getBigInteger();
const one = BigInteger.new(1);
p = BigInteger.new(p);
q = BigInteger.new(q);
g = BigInteger.new(g);
x = BigInteger.new(x);
const one = new BigInteger(1);
p = new BigInteger(p);
q = new BigInteger(q);
g = new BigInteger(g);
x = new BigInteger(x);
let k;
let r;
@ -60,7 +60,7 @@ export async function sign(hashAlgo, hashed, g, p, q, x) {
// of leftmost bits equal to the number of bits of q. This (possibly
// truncated) hash function result is treated as a number and used
// directly in the DSA signature algorithm.
const h = BigInteger.new(hashed.subarray(0, q.byteLength())).mod(q);
const h = new BigInteger(hashed.subarray(0, q.byteLength())).mod(q);
// FIPS-186-4, section 4.6:
// The values of r and s shall be checked to determine if r = 0 or s = 0.
// If either r = 0 or s = 0, a new value of k shall be generated, and the
@ -103,21 +103,21 @@ export async function sign(hashAlgo, hashed, g, p, q, x) {
export async function verify(hashAlgo, r, s, hashed, g, p, q, y) {
const BigInteger = await util.getBigInteger();
const zero = BigInteger.new(0);
r = BigInteger.new(r);
s = BigInteger.new(s);
const zero = new BigInteger(0);
r = new BigInteger(r);
s = new BigInteger(s);
p = BigInteger.new(p);
q = BigInteger.new(q);
g = BigInteger.new(g);
y = BigInteger.new(y);
p = new BigInteger(p);
q = new BigInteger(q);
g = new BigInteger(g);
y = new BigInteger(y);
if (r.lte(zero) || r.gte(q) ||
s.lte(zero) || s.gte(q)) {
util.printDebug('invalid DSA Signature');
return false;
}
const h = BigInteger.new(hashed.subarray(0, q.byteLength())).imod(q);
const h = new BigInteger(hashed.subarray(0, q.byteLength())).imod(q);
const w = s.modInv(q); // s**-1 mod q
if (w.isZero()) {
util.printDebug('invalid DSA Signature');
@ -147,11 +147,11 @@ export async function verify(hashAlgo, r, s, hashed, g, p, q, y) {
export async function validateParams(p, q, g, y, x) {
const BigInteger = await util.getBigInteger();
p = BigInteger.new(p);
q = BigInteger.new(q);
g = BigInteger.new(g);
y = BigInteger.new(y);
const one = BigInteger.new(1);
p = new BigInteger(p);
q = new BigInteger(q);
g = new BigInteger(g);
y = new BigInteger(y);
const one = new BigInteger(1);
// Check that 1 < g < p
if (g.lte(one) || g.gte(p)) {
return false;
@ -175,8 +175,8 @@ export async function validateParams(p, q, g, y, x) {
/**
* Check q is large and probably prime (we mainly want to avoid small factors)
*/
const qSize = BigInteger.new(q.bitLength());
const n150 = BigInteger.new(150);
const qSize = new BigInteger(q.bitLength());
const n150 = new BigInteger(150);
if (qSize.lt(n150) || !(await isProbablePrime(q, null, 32))) {
return false;
}
@ -187,8 +187,8 @@ export async function validateParams(p, q, g, y, x) {
*
* Blinded exponentiation computes g**{rq + x} to compare to y
*/
x = BigInteger.new(x);
const two = BigInteger.new(2);
x = new BigInteger(x);
const two = new BigInteger(2);
const r = await getRandomBigInteger(two.leftShift(qSize.dec()), two.leftShift(qSize)); // draw r of same size as q
const rqx = q.mul(r).add(x);
if (!y.equal(g.modExp(rqx, p))) {

View File

@ -36,16 +36,16 @@ import util from '../../util';
export async function encrypt(data, p, g, y) {
const BigInteger = await util.getBigInteger();
p = BigInteger.new(p);
g = BigInteger.new(g);
y = BigInteger.new(y);
p = new BigInteger(p);
g = new BigInteger(g);
y = new BigInteger(y);
const padded = emeEncode(data, p.byteLength());
const m = BigInteger.new(padded);
const m = new BigInteger(padded);
// OpenPGP uses a "special" version of ElGamal where g is generator of the full group Z/pZ*
// hence g has order p-1, and to avoid that k = 0 mod p-1, we need to pick k in [1, p-2]
const k = await getRandomBigInteger(BigInteger.new(1), p.dec());
const k = await getRandomBigInteger(new BigInteger(1), p.dec());
return {
c1: g.modExp(k, p).toUint8Array(),
c2: y.modExp(k, p).imul(m).imod(p).toUint8Array()
@ -67,10 +67,10 @@ export async function encrypt(data, p, g, y) {
export async function decrypt(c1, c2, p, x, randomPayload) {
const BigInteger = await util.getBigInteger();
c1 = BigInteger.new(c1);
c2 = BigInteger.new(c2);
p = BigInteger.new(p);
x = BigInteger.new(x);
c1 = new BigInteger(c1);
c2 = new BigInteger(c2);
p = new BigInteger(p);
x = new BigInteger(x);
const padded = c1.modExp(x, p).modInv(p).imul(c2).imod(p);
return emeDecode(padded.toUint8Array('be', p.byteLength()), randomPayload);
@ -88,19 +88,19 @@ export async function decrypt(c1, c2, p, x, randomPayload) {
export async function validateParams(p, g, y, x) {
const BigInteger = await util.getBigInteger();
p = BigInteger.new(p);
g = BigInteger.new(g);
y = BigInteger.new(y);
p = new BigInteger(p);
g = new BigInteger(g);
y = new BigInteger(y);
const one = BigInteger.new(1);
const one = new BigInteger(1);
// Check that 1 < g < p
if (g.lte(one) || g.gte(p)) {
return false;
}
// Expect p-1 to be large
const pSize = BigInteger.new(p.bitLength());
const n1023 = BigInteger.new(1023);
const pSize = new BigInteger(p.bitLength());
const n1023 = new BigInteger(1023);
if (pSize.lt(n1023)) {
return false;
}
@ -120,8 +120,8 @@ export async function validateParams(p, g, y, x) {
* We just check g**i != 1 for all i up to a threshold
*/
let res = g;
const i = BigInteger.new(1);
const threshold = BigInteger.new(2).leftShift(BigInteger.new(17)); // we want order > threshold
const i = new BigInteger(1);
const threshold = new BigInteger(2).leftShift(new BigInteger(17)); // we want order > threshold
while (i.lt(threshold)) {
res = res.mul(g).imod(p);
if (res.isOne()) {
@ -136,8 +136,8 @@ export async function validateParams(p, g, y, x) {
*
* Blinded exponentiation computes g**{r(p-1) + x} to compare to y
*/
x = BigInteger.new(x);
const two = BigInteger.new(2);
x = new BigInteger(x);
const two = new BigInteger(2);
const r = await getRandomBigInteger(two.leftShift(pSize.dec()), two.leftShift(pSize)); // draw r of same size as p-1
const rqx = p.dec().imul(r).iadd(x);
if (!y.equal(g.modExp(rqx, p))) {

View File

@ -33,9 +33,9 @@ import { getRandomBigInteger } from '../random';
export async function randomProbablePrime(bits, e, k) {
const BigInteger = await util.getBigInteger();
const one = BigInteger.new(1);
const min = one.leftShift(BigInteger.new(bits - 1));
const thirty = BigInteger.new(30);
const one = new BigInteger(1);
const min = one.leftShift(new BigInteger(bits - 1));
const thirty = new BigInteger(30);
/*
* We can avoid any multiples of 3 and 5 by looking at n mod 30
* n mod 30 = 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
@ -48,7 +48,7 @@ export async function randomProbablePrime(bits, e, k) {
let i = n.mod(thirty).toNumber();
do {
n.iadd(BigInteger.new(adds[i]));
n.iadd(new BigInteger(adds[i]));
i = (i + adds[i]) % adds.length;
// If reached the maximum, go back to the minimum.
if (n.bitLength() > bits) {
@ -95,7 +95,7 @@ export async function isProbablePrime(n, e, k) {
export async function fermat(n, b) {
const BigInteger = await util.getBigInteger();
b = b || BigInteger.new(2);
b = b || new BigInteger(2);
return b.modExp(n.dec(), n).isOne();
}
@ -103,7 +103,7 @@ export async function divisionTest(n) {
const BigInteger = await util.getBigInteger();
return smallPrimes.every(m => {
return n.mod(BigInteger.new(m)) !== 0;
return n.mod(new BigInteger(m)) !== 0;
});
}
@ -243,10 +243,10 @@ export async function millerRabin(n, k, rand) {
// Find d and s, (n - 1) = (2 ^ s) * d;
let s = 0;
while (!n1.getBit(s)) { s++; }
const d = n.rightShift(BigInteger.new(s));
const d = n.rightShift(new BigInteger(s));
for (; k > 0; k--) {
const a = rand ? rand() : await getRandomBigInteger(BigInteger.new(2), n1);
const a = rand ? rand() : await getRandomBigInteger(new BigInteger(2), n1);
let x = a.modExp(d, n);
if (x.isOne() || x.equal(n1)) {

View File

@ -160,7 +160,7 @@ export async function decrypt(data, n, e, d, p, q, u, randomPayload) {
export async function generate(bits, e) {
const BigInteger = await util.getBigInteger();
e = BigInteger.new(e);
e = new BigInteger(e);
// Native RSA keygen using Web Crypto
if (util.getWebCrypto()) {
@ -265,24 +265,24 @@ export async function generate(bits, e) {
export async function validateParams(n, e, d, p, q, u) {
const BigInteger = await util.getBigInteger();
n = BigInteger.new(n);
p = BigInteger.new(p);
q = BigInteger.new(q);
n = new BigInteger(n);
p = new BigInteger(p);
q = new BigInteger(q);
// expect pq = n
if (!p.mul(q).equal(n)) {
return false;
}
const two = BigInteger.new(2);
const two = new BigInteger(2);
// expect p*u = 1 mod q
u = BigInteger.new(u);
u = new BigInteger(u);
if (!p.mul(u).mod(q).isOne()) {
return false;
}
e = BigInteger.new(e);
d = BigInteger.new(d);
e = new BigInteger(e);
d = new BigInteger(d);
/**
* In RSA pkcs#1 the exponents (d, e) are inverses modulo lcm(p-1, q-1)
* We check that [de = 1 mod (p-1)] and [de = 1 mod (q-1)]
@ -290,7 +290,7 @@ export async function validateParams(n, e, d, p, q, u) {
*
* We blind the multiplication with r, and check that rde = r mod lcm(p-1, q-1)
*/
const nSizeOver3 = BigInteger.new(Math.floor(n.bitLength() / 3));
const nSizeOver3 = new BigInteger(Math.floor(n.bitLength() / 3));
const r = await getRandomBigInteger(two, two.leftShift(nSizeOver3)); // r in [ 2, 2^{|n|/3} ) < p and q
const rde = r.mul(d).mul(e);
@ -305,9 +305,9 @@ export async function validateParams(n, e, d, p, q, u) {
async function bnSign(hashAlgo, n, d, hashed) {
const BigInteger = await util.getBigInteger();
n = BigInteger.new(n);
const m = BigInteger.new(await emsaEncode(hashAlgo, hashed, n.byteLength()));
d = BigInteger.new(d);
n = new BigInteger(n);
const m = new BigInteger(await emsaEncode(hashAlgo, hashed, n.byteLength()));
d = new BigInteger(d);
if (m.gte(n)) {
throw new Error('Message size cannot exceed modulus size');
}
@ -367,9 +367,9 @@ async function nodeSign(hashAlgo, data, n, e, d, p, q, u) {
async function bnVerify(hashAlgo, s, n, e, hashed) {
const BigInteger = await util.getBigInteger();
n = BigInteger.new(n);
s = BigInteger.new(s);
e = BigInteger.new(e);
n = new BigInteger(n);
s = new BigInteger(s);
e = new BigInteger(e);
if (s.gte(n)) {
throw new Error('Signature size cannot exceed modulus size');
}
@ -436,9 +436,9 @@ async function nodeEncrypt(data, n, e) {
async function bnEncrypt(data, n, e) {
const BigInteger = await util.getBigInteger();
n = BigInteger.new(n);
data = BigInteger.new(emeEncode(data, n.byteLength()));
e = BigInteger.new(e);
n = new BigInteger(n);
data = new BigInteger(emeEncode(data, n.byteLength()));
e = new BigInteger(e);
if (data.gte(n)) {
throw new Error('Message size cannot exceed modulus size');
}
@ -489,20 +489,20 @@ async function nodeDecrypt(data, n, e, d, p, q, u, randomPayload) {
async function bnDecrypt(data, n, e, d, p, q, u, randomPayload) {
const BigInteger = await util.getBigInteger();
data = BigInteger.new(data);
n = BigInteger.new(n);
e = BigInteger.new(e);
d = BigInteger.new(d);
p = BigInteger.new(p);
q = BigInteger.new(q);
u = BigInteger.new(u);
data = new BigInteger(data);
n = new BigInteger(n);
e = new BigInteger(e);
d = new BigInteger(d);
p = new BigInteger(p);
q = new BigInteger(q);
u = new BigInteger(u);
if (data.gte(n)) {
throw new Error('Data too large.');
}
const dq = d.mod(q.dec()); // d mod (q-1)
const dp = d.mod(p.dec()); // d mod (p-1)
const unblinder = (await getRandomBigInteger(BigInteger.new(2), n)).mod(n);
const unblinder = (await getRandomBigInteger(new BigInteger(2), n)).mod(n);
const blinder = unblinder.modInv(n).modExp(e, n);
data = data.mul(blinder).mod(n);
@ -532,9 +532,9 @@ async function bnDecrypt(data, n, e, d, p, q, u, randomPayload) {
async function privateToJWK(n, e, d, p, q, u) {
const BigInteger = await util.getBigInteger();
const pNum = BigInteger.new(p);
const qNum = BigInteger.new(q);
const dNum = BigInteger.new(d);
const pNum = new BigInteger(p);
const qNum = new BigInteger(q);
const dNum = new BigInteger(d);
let dq = dNum.mod(qNum.dec()); // d mod (q-1)
let dp = dNum.mod(pNum.dec()); // d mod (p-1)

View File

@ -63,6 +63,6 @@ export async function getRandomBigInteger(min, max) {
// Using a while loop is necessary to avoid bias introduced by the mod operation.
// However, we request 64 extra random bits so that the bias is negligible.
// Section B.1.1 here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf
const r = BigInteger.new(await getRandomBytes(bytes + 8));
const r = new BigInteger(getRandomBytes(bytes + 8));
return r.mod(modulus).add(min);
}