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 don't use the BigIntegerInterface wrapper from noble-hashes because:
* We need to duplicate the file, instead of importing it, since in that case the BigIntegerInterface instance * - importing the instance results in it being shared with noble-hashes, which separately calls `setImplementation()`
* would be shared with noble-hashes, which separately calls `setImplementation()` on load, causing it to throw due to * on load, causing it to throw due to duplicate initialization.
* 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'; const detectBigInt = () => typeof BigInt !== 'undefined';
export async function getBigInteger() { 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()) { if (detectBigInt()) {
// NativeBigInteger is small, so it's imported in isolation (it could also be imported at the top level) // 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'); const { default: NativeBigInteger } = await import('@openpgp/noble-hashes/esm/biginteger/native.interface');
BigInteger.setImplementation(NativeBigInteger, true); return NativeBigInteger;
} else { } else {
// FallbackBigInteger relies on large BN.js lib, which is also used by noble-hashes and noble-curves // 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'); 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) { export async function sign(hashAlgo, hashed, g, p, q, x) {
const BigInteger = await util.getBigInteger(); const BigInteger = await util.getBigInteger();
const one = BigInteger.new(1); const one = new BigInteger(1);
p = BigInteger.new(p); p = new BigInteger(p);
q = BigInteger.new(q); q = new BigInteger(q);
g = BigInteger.new(g); g = new BigInteger(g);
x = BigInteger.new(x); x = new BigInteger(x);
let k; let k;
let r; 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 // of leftmost bits equal to the number of bits of q. This (possibly
// truncated) hash function result is treated as a number and used // truncated) hash function result is treated as a number and used
// directly in the DSA signature algorithm. // 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: // FIPS-186-4, section 4.6:
// The values of r and s shall be checked to determine if r = 0 or s = 0. // 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 // 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) { export async function verify(hashAlgo, r, s, hashed, g, p, q, y) {
const BigInteger = await util.getBigInteger(); const BigInteger = await util.getBigInteger();
const zero = BigInteger.new(0); const zero = new BigInteger(0);
r = BigInteger.new(r); r = new BigInteger(r);
s = BigInteger.new(s); s = new BigInteger(s);
p = BigInteger.new(p); p = new BigInteger(p);
q = BigInteger.new(q); q = new BigInteger(q);
g = BigInteger.new(g); g = new BigInteger(g);
y = BigInteger.new(y); y = new BigInteger(y);
if (r.lte(zero) || r.gte(q) || if (r.lte(zero) || r.gte(q) ||
s.lte(zero) || s.gte(q)) { s.lte(zero) || s.gte(q)) {
util.printDebug('invalid DSA Signature'); util.printDebug('invalid DSA Signature');
return false; 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 const w = s.modInv(q); // s**-1 mod q
if (w.isZero()) { if (w.isZero()) {
util.printDebug('invalid DSA Signature'); 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) { export async function validateParams(p, q, g, y, x) {
const BigInteger = await util.getBigInteger(); const BigInteger = await util.getBigInteger();
p = BigInteger.new(p); p = new BigInteger(p);
q = BigInteger.new(q); q = new BigInteger(q);
g = BigInteger.new(g); g = new BigInteger(g);
y = BigInteger.new(y); y = new BigInteger(y);
const one = BigInteger.new(1); const one = new BigInteger(1);
// Check that 1 < g < p // Check that 1 < g < p
if (g.lte(one) || g.gte(p)) { if (g.lte(one) || g.gte(p)) {
return false; 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) * Check q is large and probably prime (we mainly want to avoid small factors)
*/ */
const qSize = BigInteger.new(q.bitLength()); const qSize = new BigInteger(q.bitLength());
const n150 = BigInteger.new(150); const n150 = new BigInteger(150);
if (qSize.lt(n150) || !(await isProbablePrime(q, null, 32))) { if (qSize.lt(n150) || !(await isProbablePrime(q, null, 32))) {
return false; 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 * Blinded exponentiation computes g**{rq + x} to compare to y
*/ */
x = BigInteger.new(x); x = new BigInteger(x);
const two = BigInteger.new(2); const two = new BigInteger(2);
const r = await getRandomBigInteger(two.leftShift(qSize.dec()), two.leftShift(qSize)); // draw r of same size as q 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); const rqx = q.mul(r).add(x);
if (!y.equal(g.modExp(rqx, p))) { 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) { export async function encrypt(data, p, g, y) {
const BigInteger = await util.getBigInteger(); const BigInteger = await util.getBigInteger();
p = BigInteger.new(p); p = new BigInteger(p);
g = BigInteger.new(g); g = new BigInteger(g);
y = BigInteger.new(y); y = new BigInteger(y);
const padded = emeEncode(data, p.byteLength()); 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* // 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] // 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 { return {
c1: g.modExp(k, p).toUint8Array(), c1: g.modExp(k, p).toUint8Array(),
c2: y.modExp(k, p).imul(m).imod(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) { export async function decrypt(c1, c2, p, x, randomPayload) {
const BigInteger = await util.getBigInteger(); const BigInteger = await util.getBigInteger();
c1 = BigInteger.new(c1); c1 = new BigInteger(c1);
c2 = BigInteger.new(c2); c2 = new BigInteger(c2);
p = BigInteger.new(p); p = new BigInteger(p);
x = BigInteger.new(x); x = new BigInteger(x);
const padded = c1.modExp(x, p).modInv(p).imul(c2).imod(p); const padded = c1.modExp(x, p).modInv(p).imul(c2).imod(p);
return emeDecode(padded.toUint8Array('be', p.byteLength()), randomPayload); 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) { export async function validateParams(p, g, y, x) {
const BigInteger = await util.getBigInteger(); const BigInteger = await util.getBigInteger();
p = BigInteger.new(p); p = new BigInteger(p);
g = BigInteger.new(g); g = new BigInteger(g);
y = BigInteger.new(y); y = new BigInteger(y);
const one = BigInteger.new(1); const one = new BigInteger(1);
// Check that 1 < g < p // Check that 1 < g < p
if (g.lte(one) || g.gte(p)) { if (g.lte(one) || g.gte(p)) {
return false; return false;
} }
// Expect p-1 to be large // Expect p-1 to be large
const pSize = BigInteger.new(p.bitLength()); const pSize = new BigInteger(p.bitLength());
const n1023 = BigInteger.new(1023); const n1023 = new BigInteger(1023);
if (pSize.lt(n1023)) { if (pSize.lt(n1023)) {
return false; 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 * We just check g**i != 1 for all i up to a threshold
*/ */
let res = g; let res = g;
const i = BigInteger.new(1); const i = new BigInteger(1);
const threshold = BigInteger.new(2).leftShift(BigInteger.new(17)); // we want order > threshold const threshold = new BigInteger(2).leftShift(new BigInteger(17)); // we want order > threshold
while (i.lt(threshold)) { while (i.lt(threshold)) {
res = res.mul(g).imod(p); res = res.mul(g).imod(p);
if (res.isOne()) { 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 * Blinded exponentiation computes g**{r(p-1) + x} to compare to y
*/ */
x = BigInteger.new(x); x = new BigInteger(x);
const two = BigInteger.new(2); 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 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); const rqx = p.dec().imul(r).iadd(x);
if (!y.equal(g.modExp(rqx, p))) { if (!y.equal(g.modExp(rqx, p))) {

View File

@ -33,9 +33,9 @@ import { getRandomBigInteger } from '../random';
export async function randomProbablePrime(bits, e, k) { export async function randomProbablePrime(bits, e, k) {
const BigInteger = await util.getBigInteger(); const BigInteger = await util.getBigInteger();
const one = BigInteger.new(1); const one = new BigInteger(1);
const min = one.leftShift(BigInteger.new(bits - 1)); const min = one.leftShift(new BigInteger(bits - 1));
const thirty = BigInteger.new(30); const thirty = new BigInteger(30);
/* /*
* We can avoid any multiples of 3 and 5 by looking at n mod 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 * 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(); let i = n.mod(thirty).toNumber();
do { do {
n.iadd(BigInteger.new(adds[i])); n.iadd(new BigInteger(adds[i]));
i = (i + adds[i]) % adds.length; i = (i + adds[i]) % adds.length;
// If reached the maximum, go back to the minimum. // If reached the maximum, go back to the minimum.
if (n.bitLength() > bits) { if (n.bitLength() > bits) {
@ -95,7 +95,7 @@ export async function isProbablePrime(n, e, k) {
export async function fermat(n, b) { export async function fermat(n, b) {
const BigInteger = await util.getBigInteger(); const BigInteger = await util.getBigInteger();
b = b || BigInteger.new(2); b = b || new BigInteger(2);
return b.modExp(n.dec(), n).isOne(); return b.modExp(n.dec(), n).isOne();
} }
@ -103,7 +103,7 @@ export async function divisionTest(n) {
const BigInteger = await util.getBigInteger(); const BigInteger = await util.getBigInteger();
return smallPrimes.every(m => { 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; // Find d and s, (n - 1) = (2 ^ s) * d;
let s = 0; let s = 0;
while (!n1.getBit(s)) { s++; } while (!n1.getBit(s)) { s++; }
const d = n.rightShift(BigInteger.new(s)); const d = n.rightShift(new BigInteger(s));
for (; k > 0; k--) { 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); let x = a.modExp(d, n);
if (x.isOne() || x.equal(n1)) { 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) { export async function generate(bits, e) {
const BigInteger = await util.getBigInteger(); const BigInteger = await util.getBigInteger();
e = BigInteger.new(e); e = new BigInteger(e);
// Native RSA keygen using Web Crypto // Native RSA keygen using Web Crypto
if (util.getWebCrypto()) { if (util.getWebCrypto()) {
@ -265,24 +265,24 @@ export async function generate(bits, e) {
export async function validateParams(n, e, d, p, q, u) { export async function validateParams(n, e, d, p, q, u) {
const BigInteger = await util.getBigInteger(); const BigInteger = await util.getBigInteger();
n = BigInteger.new(n); n = new BigInteger(n);
p = BigInteger.new(p); p = new BigInteger(p);
q = BigInteger.new(q); q = new BigInteger(q);
// expect pq = n // expect pq = n
if (!p.mul(q).equal(n)) { if (!p.mul(q).equal(n)) {
return false; return false;
} }
const two = BigInteger.new(2); const two = new BigInteger(2);
// expect p*u = 1 mod q // expect p*u = 1 mod q
u = BigInteger.new(u); u = new BigInteger(u);
if (!p.mul(u).mod(q).isOne()) { if (!p.mul(u).mod(q).isOne()) {
return false; return false;
} }
e = BigInteger.new(e); e = new BigInteger(e);
d = BigInteger.new(d); d = new BigInteger(d);
/** /**
* In RSA pkcs#1 the exponents (d, e) are inverses modulo lcm(p-1, q-1) * 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)] * 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) * 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 r = await getRandomBigInteger(two, two.leftShift(nSizeOver3)); // r in [ 2, 2^{|n|/3} ) < p and q
const rde = r.mul(d).mul(e); 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) { async function bnSign(hashAlgo, n, d, hashed) {
const BigInteger = await util.getBigInteger(); const BigInteger = await util.getBigInteger();
n = BigInteger.new(n); n = new BigInteger(n);
const m = BigInteger.new(await emsaEncode(hashAlgo, hashed, n.byteLength())); const m = new BigInteger(await emsaEncode(hashAlgo, hashed, n.byteLength()));
d = BigInteger.new(d); d = new BigInteger(d);
if (m.gte(n)) { if (m.gte(n)) {
throw new Error('Message size cannot exceed modulus size'); 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) { async function bnVerify(hashAlgo, s, n, e, hashed) {
const BigInteger = await util.getBigInteger(); const BigInteger = await util.getBigInteger();
n = BigInteger.new(n); n = new BigInteger(n);
s = BigInteger.new(s); s = new BigInteger(s);
e = BigInteger.new(e); e = new BigInteger(e);
if (s.gte(n)) { if (s.gte(n)) {
throw new Error('Signature size cannot exceed modulus size'); 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) { async function bnEncrypt(data, n, e) {
const BigInteger = await util.getBigInteger(); const BigInteger = await util.getBigInteger();
n = BigInteger.new(n); n = new BigInteger(n);
data = BigInteger.new(emeEncode(data, n.byteLength())); data = new BigInteger(emeEncode(data, n.byteLength()));
e = BigInteger.new(e); e = new BigInteger(e);
if (data.gte(n)) { if (data.gte(n)) {
throw new Error('Message size cannot exceed modulus size'); 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) { async function bnDecrypt(data, n, e, d, p, q, u, randomPayload) {
const BigInteger = await util.getBigInteger(); const BigInteger = await util.getBigInteger();
data = BigInteger.new(data); data = new BigInteger(data);
n = BigInteger.new(n); n = new BigInteger(n);
e = BigInteger.new(e); e = new BigInteger(e);
d = BigInteger.new(d); d = new BigInteger(d);
p = BigInteger.new(p); p = new BigInteger(p);
q = BigInteger.new(q); q = new BigInteger(q);
u = BigInteger.new(u); u = new BigInteger(u);
if (data.gte(n)) { if (data.gte(n)) {
throw new Error('Data too large.'); throw new Error('Data too large.');
} }
const dq = d.mod(q.dec()); // d mod (q-1) const dq = d.mod(q.dec()); // d mod (q-1)
const dp = d.mod(p.dec()); // d mod (p-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); const blinder = unblinder.modInv(n).modExp(e, n);
data = data.mul(blinder).mod(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) { async function privateToJWK(n, e, d, p, q, u) {
const BigInteger = await util.getBigInteger(); const BigInteger = await util.getBigInteger();
const pNum = BigInteger.new(p); const pNum = new BigInteger(p);
const qNum = BigInteger.new(q); const qNum = new BigInteger(q);
const dNum = BigInteger.new(d); const dNum = new BigInteger(d);
let dq = dNum.mod(qNum.dec()); // d mod (q-1) let dq = dNum.mod(qNum.dec()); // d mod (q-1)
let dp = dNum.mod(pNum.dec()); // d mod (p-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. // 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. // 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 // 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); return r.mod(modulus).add(min);
} }