diff --git a/package-lock.json b/package-lock.json index b1ceb5e7..8d0c7a99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@typescript-eslint/parser": "^7.8.0", "argon2id": "^1.0.1", "benchmark": "^2.1.4", + "bn.js": "^5.2.1", "c8": "^8.0.1", "chai": "^4.4.1", "chai-as-promised": "^7.1.1", @@ -2141,6 +2142,12 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "dev": true }, + "node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dev": true + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -9783,6 +9790,12 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "dev": true }, + "bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dev": true + }, "body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", diff --git a/package.json b/package.json index 2f5d1bac..4d0968ba 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "@typescript-eslint/parser": "^7.8.0", "argon2id": "^1.0.1", "benchmark": "^2.1.4", + "bn.js": "^5.2.1", "c8": "^8.0.1", "chai": "^4.4.1", "chai-as-promised": "^7.1.1", diff --git a/rollup.config.js b/rollup.config.js index 870c0c26..87050ee8 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -64,7 +64,9 @@ export default Object.assign([ resolve({ browser: true }), - typescript(), + typescript({ + compilerOptions: { outDir: './dist/tmp-ts' } // to avoid js files being overwritten + }), commonjs({ ignore: nodeBuiltinModules.concat(nodeDependencies) }), @@ -89,7 +91,9 @@ export default Object.assign([ resolve({ exportConditions: ['node'] // needed for resolution of noble-curves import of '@noble/crypto' in Node 16 and 18 }), - typescript(), + typescript({ + compilerOptions: { outDir: './dist/tmp-ts' } + }), commonjs(), replace({ 'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}` @@ -109,7 +113,9 @@ export default Object.assign([ resolve({ browser: true }), - typescript(), + typescript({ + compilerOptions: { outDir: './dist/lightweight/tmp-ts' } + }), commonjs({ ignore: nodeBuiltinModules.concat(nodeDependencies) }), @@ -136,7 +142,9 @@ export default Object.assign([ resolve({ browser: true }), - typescript(), + typescript({ + compilerOptions: { outDir: './test/lib/tmp-ts' } + }), commonjs({ ignore: nodeBuiltinModules.concat(nodeDependencies), requireReturnsDefault: 'preferred' diff --git a/src/biginteger.ts b/src/biginteger.ts deleted file mode 100644 index 6b9c0adc..00000000 --- a/src/biginteger.ts +++ /dev/null @@ -1,499 +0,0 @@ -/** - * @fileoverview - * BigInteger implementation of basic operations - * that wraps the native BigInt library. - * Operations are not constant time, - * but we try and limit timing leakage where we can - */ -export default class BigInteger { - private value: bigint; - - /** - * Get a BigInteger (input must be big endian for strings and arrays) - * @param {Number|String|Uint8Array} n - Value to convert - * @throws {Error} on null or undefined input - */ - constructor(n: Uint8Array | string | number | bigint) { - if (n === undefined) { - throw new Error('Invalid BigInteger input'); - } - - if (n instanceof Uint8Array) { - const bytes = n; - const hexAlphabet = '0123456789ABCDEF'; - let s = ''; - bytes.forEach(v => { - s += hexAlphabet[v >> 4] + hexAlphabet[v & 15]; - }); - this.value = BigInt('0x0' + s); - } else { - this.value = BigInt(n); - } - } - - clone() { - return new BigInteger(this.value); - } - - /** - * BigInteger increment in place - */ - iinc() { - this.value++; - return this; - } - - /** - * BigInteger increment - * @returns {BigInteger} this + 1. - */ - inc() { - return this.clone().iinc(); - } - - /** - * BigInteger decrement in place - */ - idec() { - this.value--; - return this; - } - - /** - * BigInteger decrement - * @returns {BigInteger} this - 1. - */ - dec() { - return this.clone().idec(); - } - - /** - * BigInteger addition in place - * @param {BigInteger} x - Value to add - */ - iadd(x: BigInteger) { - this.value += x.value; - return this; - } - - /** - * BigInteger addition - * @param {BigInteger} x - Value to add - * @returns {BigInteger} this + x. - */ - add(x: BigInteger) { - return this.clone().iadd(x); - } - - /** - * BigInteger subtraction in place - * @param {BigInteger} x - Value to subtract - */ - isub(x: BigInteger) { - this.value -= x.value; - return this; - } - - /** - * BigInteger subtraction - * @param {BigInteger} x - Value to subtract - * @returns {BigInteger} this - x. - */ - sub(x: BigInteger) { - return this.clone().isub(x); - } - - /** - * BigInteger multiplication in place - * @param {BigInteger} x - Value to multiply - */ - imul(x: BigInteger) { - this.value *= x.value; - return this; - } - - /** - * BigInteger multiplication - * @param {BigInteger} x - Value to multiply - * @returns {BigInteger} this * x. - */ - mul(x: BigInteger) { - return this.clone().imul(x); - } - - /** - * Compute value modulo m, in place - * @param {BigInteger} m - Modulo - */ - imod(m: BigInteger) { - this.value %= m.value; - if (this.isNegative()) { - this.iadd(m); - } - return this; - } - - /** - * Compute value modulo m - * @param {BigInteger} m - Modulo - * @returns {BigInteger} this mod m. - */ - mod(m: BigInteger) { - return this.clone().imod(m); - } - - /** - * Compute modular exponentiation using square and multiply - * @param {BigInteger} e - Exponent - * @param {BigInteger} n - Modulo - * @returns {BigInteger} this ** e mod n. - */ - modExp(e: BigInteger, n: BigInteger) { - if (n.isZero()) throw Error('Modulo cannot be zero'); - if (n.isOne()) return new BigInteger(0); - if (e.isNegative()) throw Error('Unsopported negative exponent'); - - let exp = e.value; - let x = this.value; - - x %= n.value; - let r = BigInt(1); - while (exp > BigInt(0)) { - const lsb = exp & BigInt(1); - exp >>= BigInt(1); // e / 2 - // Always compute multiplication step, to reduce timing leakage - const rx = (r * x) % n.value; - // Update r only if lsb is 1 (odd exponent) - r = lsb ? rx : r; - x = (x * x) % n.value; // Square - } - return new BigInteger(r); - } - - /** - * Compute the inverse of this value modulo n - * Note: this and and n must be relatively prime - * @param {BigInteger} n - Modulo - * @returns {BigInteger} x such that this*x = 1 mod n - * @throws {Error} if the inverse does not exist - */ - modInv(n: BigInteger) { - const { gcd, x } = this._egcd(n); - if (!gcd.isOne()) { - throw new Error('Inverse does not exist'); - } - return x.add(n).mod(n); - } - - /** - * BigInteger division, in place - * @param {BigInteger} n - Value to divide - */ - idiv(n: BigInteger) { - this.value /= n.value; - return this; - } - - /** - * BigInteger division - * @param {BigInteger} n - Value to divide - * @returns {BigInteger} this divded by n. - */ - div(n: BigInteger) { - return this.clone().idiv(n); - } - - /** - * Extended Eucleadian algorithm (http://anh.cs.luc.edu/331/notes/xgcd.pdf) - * Given a = this and b, compute (x, y) such that ax + by = gdc(a, b). - * Negative numbers are also supported. - * @param {BigInteger} b - Second operand - * @returns {{ gcd, x, y: BigInteger }} - */ - private _egcd(bInput: BigInteger) { - let x = BigInt(0); - let y = BigInt(1); - let xPrev = BigInt(1); - let yPrev = BigInt(0); - - // Deal with negative numbers: run algo over absolute values, - // and "move" the sign to the returned x and/or y. - // See https://math.stackexchange.com/questions/37806/extended-euclidean-algorithm-with-negative-numbers - let a = this.abs().value; - let b = bInput.abs().value; - const aNegated = this.isNegative(); - const bNegated = bInput.isNegative(); - - while (b !== BigInt(0)) { - const q = a / b; - let tmp = x; - x = xPrev - q * x; - xPrev = tmp; - - tmp = y; - y = yPrev - q * y; - yPrev = tmp; - - tmp = b; - b = a % b; - a = tmp; - } - - return { - x: new BigInteger(aNegated ? -xPrev : xPrev), - y: new BigInteger(bNegated ? -yPrev : yPrev), - gcd: new BigInteger(a) - }; - } - - /** - * Compute greatest common divisor between this and n - * @param {BigInteger} b - Operand - * @returns {BigInteger} gcd - */ - gcd(bInput: BigInteger) { - let a = this.value; - let b = bInput.value; - while (b !== BigInt(0)) { - const tmp = b; - b = a % b; - a = tmp; - } - return new BigInteger(a); - } - - /** - * Shift this to the left by x, in place - * @param {BigInteger} x - Shift value - */ - ileftShift(x: BigInteger) { - this.value <<= x.value; - return this; - } - - /** - * Shift this to the left by x - * @param {BigInteger} x - Shift value - * @returns {BigInteger} this << x. - */ - leftShift(x: BigInteger) { - return this.clone().ileftShift(x); - } - - /** - * Shift this to the right by x, in place - * @param {BigInteger} x - Shift value - */ - irightShift(x: BigInteger) { - this.value >>= x.value; - return this; - } - - /** - * Shift this to the right by x - * @param {BigInteger} x - Shift value - * @returns {BigInteger} this >> x. - */ - rightShift(x: BigInteger) { - return this.clone().irightShift(x); - } - - ixor(x: BigInteger) { - this.value ^= x.value; - return this; - } - - xor(x: BigInteger) { - return this.clone().ixor(x); - } - - ibitwiseAnd(x: BigInteger) { - this.value &= x.value; - return this; - } - - bitwiseAnd(x: BigInteger) { - return this.clone().ibitwiseAnd(x); - } - - ibitwiseOr(x: BigInteger) { - this.value |= x.value; - return this; - } - - /** - * Whether this value is equal to x - * @param {BigInteger} x - * @returns {Boolean} - */ - equal(x: BigInteger) { - return this.value === x.value; - } - - /** - * Whether this value is less than x - * @param {BigInteger} x - * @returns {Boolean} - */ - lt(x: BigInteger) { - return this.value < x.value; - } - - /** - * Whether this value is less than or equal to x - * @param {BigInteger} x - * @returns {Boolean} - */ - lte(x: BigInteger) { - return this.value <= x.value; - } - - /** - * Whether this value is greater than x - * @param {BigInteger} x - * @returns {Boolean} - */ - gt(x: BigInteger) { - return this.value > x.value; - } - - /** - * Whether this value is greater than or equal to x - * @param {BigInteger} x - * @returns {Boolean} - */ - gte(x: BigInteger) { - return this.value >= x.value; - } - - isZero() { - return this.value === BigInt(0); - } - - isOne() { - return this.value === BigInt(1); - } - - isNegative() { - return this.value < BigInt(0); - } - - isEven() { - return !(this.value & BigInt(1)); - } - - abs() { - const res = this.clone(); - if (this.isNegative()) { - res.value = -res.value; - } - return res; - } - - negate() { - const res = this.clone(); - res.value = -res.value; - return res; - } - - /** - * Get this value as a string - * @returns {String} this value. - */ - toString() { - return this.value.toString(); - } - - /** - * Get this value as an exact Number (max 53 bits) - * Fails if this value is too large - * @returns {Number} - */ - toNumber() { - const number = Number(this.value); - if (number > Number.MAX_SAFE_INTEGER) { - // We throw and error to conform with the bn.js implementation - throw new Error('Number can only safely store up to 53 bits'); - } - return number; - } - - /** - * Get value of i-th bit - * @param {Number} i - Bit index - * @returns {Number} Bit value. - */ - getBit(i: number) { - const bit = (this.value >> BigInt(i)) & BigInt(1); - return bit === BigInt(0) ? 0 : 1; - } - - /** - * Compute bit length - * @returns {Number} Bit length. - */ - bitLength() { - const zero = new BigInteger(0); - const one = new BigInteger(1); - const negOne = new BigInteger(-1); - - // -1n >> -1n is -1n - // 1n >> 1n is 0n - const target = this.isNegative() ? negOne : zero; - let bitlen = 1; - const tmp = this.clone(); - while (!tmp.irightShift(one).equal(target)) { - bitlen++; - } - return bitlen; - } - - /** - * Compute byte length - * @returns {Number} Byte length. - */ - byteLength() { - const zero = new BigInteger(0); - const negOne = new BigInteger(-1); - - const target = this.isNegative() ? negOne : zero; - const eight = new BigInteger(8); - let len = 1; - const tmp = this.clone(); - while (!tmp.irightShift(eight).equal(target)) { - len++; - } - return len; - } - - /** - * Get Uint8Array representation of this number - * @param {String} endian - Endianess of output array (defaults to 'be') - * @param {Number} length - Of output array - * @returns {Uint8Array} - */ - toUint8Array(endian = 'be', length: number) { - // we get and parse the hex string (https://coolaj86.com/articles/convert-js-bigints-to-typedarrays/) - // this is faster than shift+mod iterations - let hex = this.value.toString(16); - if (hex.length % 2 === 1) { - hex = '0' + hex; - } - - const rawLength = hex.length / 2; - const bytes = new Uint8Array(length || rawLength); - // parse hex - const offset = length ? length - rawLength : 0; - let i = 0; - while (i < rawLength) { - bytes[i + offset] = parseInt(hex.slice(2 * i, 2 * i + 2), 16); - i++; - } - - if (endian !== 'be') { - bytes.reverse(); - } - - return bytes; - } -} diff --git a/src/crypto/biginteger.ts b/src/crypto/biginteger.ts new file mode 100644 index 00000000..ba8c82c4 --- /dev/null +++ b/src/crypto/biginteger.ts @@ -0,0 +1,216 @@ +// Operations are not constant time, but we try and limit timing leakage where we can + +const _0n = BigInt(0); +const _1n = BigInt(1); + +export function uint8ArrayToBigInt(bytes: Uint8Array) { + const hexAlphabet = '0123456789ABCDEF'; + let s = ''; + bytes.forEach(v => { + s += hexAlphabet[v >> 4] + hexAlphabet[v & 15]; + }); + return BigInt('0x0' + s); +} + +export function mod(a: bigint, m: bigint) { + const reduced = a % m; + return reduced < _0n ? reduced + m : reduced; +} + +/** + * Compute modular exponentiation using square and multiply + * @param {BigInt} a - Base + * @param {BigInt} e - Exponent + * @param {BigInt} n - Modulo + * @returns {BigInt} b ** e mod n. + */ +export function modExp(b: bigint, e: bigint, n: bigint) { + if (n === _0n) throw Error('Modulo cannot be zero'); + if (n === _1n) return BigInt(0); + if (e < _0n) throw Error('Unsopported negative exponent'); + + let exp = e; + let x = b; + + x %= n; + let r = BigInt(1); + while (exp > _0n) { + const lsb = exp & _1n; + exp >>= _1n; // e / 2 + // Always compute multiplication step, to reduce timing leakage + const rx = (r * x) % n; + // Update r only if lsb is 1 (odd exponent) + r = lsb ? rx : r; + x = (x * x) % n; // Square + } + return r; +} + + +function abs(x: bigint) { + return x >= _0n ? x : -x; +} + +/** + * Extended Eucleadian algorithm (http://anh.cs.luc.edu/331/notes/xgcd.pdf) + * Given a and b, compute (x, y) such that ax + by = gdc(a, b). + * Negative numbers are also supported. + * @param {BigInt} a - First operand + * @param {BigInt} b - Second operand + * @returns {{ gcd, x, y: bigint }} + */ +function _egcd(aInput: bigint, bInput: bigint) { + let x = BigInt(0); + let y = BigInt(1); + let xPrev = BigInt(1); + let yPrev = BigInt(0); + + // Deal with negative numbers: run algo over absolute values, + // and "move" the sign to the returned x and/or y. + // See https://math.stackexchange.com/questions/37806/extended-euclidean-algorithm-with-negative-numbers + let a = abs(aInput); + let b = abs(bInput); + const aNegated = aInput < _0n; + const bNegated = bInput < _0n; + + while (b !== _0n) { + const q = a / b; + let tmp = x; + x = xPrev - q * x; + xPrev = tmp; + + tmp = y; + y = yPrev - q * y; + yPrev = tmp; + + tmp = b; + b = a % b; + a = tmp; + } + + return { + x: aNegated ? -xPrev : xPrev, + y: bNegated ? -yPrev : yPrev, + gcd: a + }; +} + +/** + * Compute the inverse of `a` modulo `n` + * Note: `a` and and `n` must be relatively prime + * @param {BigInt} a + * @param {BigInt} n - Modulo + * @returns {BigInt} x such that a*x = 1 mod n + * @throws {Error} if the inverse does not exist + */ +export function modInv(a: bigint, n: bigint) { + const { gcd, x } = _egcd(a, n); + if (gcd !== _1n) { + throw new Error('Inverse does not exist'); + } + return mod(x + n, n); +} + +/** + * Compute greatest common divisor between this and n + * @param {BigInt} aInput - Operand + * @param {BigInt} bInput - Operand + * @returns {BigInt} gcd + */ +export function gcd(aInput: bigint, bInput: bigint) { + let a = aInput; + let b = bInput; + while (b !== _0n) { + const tmp = b; + b = a % b; + a = tmp; + } + return a; +} + +/** + * Get this value as an exact Number (max 53 bits) + * Fails if this value is too large + * @returns {Number} + */ +export function bigIntToNumber(x: bigint) { + const number = Number(x); + if (number > Number.MAX_SAFE_INTEGER) { + // We throw and error to conform with the bn.js implementation + throw new Error('Number can only safely store up to 53 bits'); + } + return number; +} + +/** + * Get value of i-th bit + * @param {BigInt} x + * @param {Number} i - Bit index + * @returns {Number} Bit value. + */ +export function getBit(x:bigint, i: number) { + const bit = (x >> BigInt(i)) & _1n; + return bit === _0n ? 0 : 1; +} + +/** + * Compute bit length + */ +export function bitLength(x: bigint) { + // -1n >> -1n is -1n + // 1n >> 1n is 0n + const target = x < _0n ? BigInt(-1) : _0n; + let bitlen = 1; + let tmp = x; + // eslint-disable-next-line no-cond-assign + while ((tmp >>= _1n) !== target) { + bitlen++; + } + return bitlen; +} + +/** + * Compute byte length + */ +export function byteLength(x: bigint) { + const target = x < _0n ? BigInt(-1) : _0n; + const _8n = BigInt(8); + let len = 1; + let tmp = x; + // eslint-disable-next-line no-cond-assign + while ((tmp >>= _8n) !== target) { + len++; + } + return len; +} + +/** + * Get Uint8Array representation of this number + * @param {String} endian - Endianess of output array (defaults to 'be') + * @param {Number} length - Of output array + * @returns {Uint8Array} + */ +export function bigIntToUint8Array(x: bigint, endian = 'be', length: number) { + // we get and parse the hex string (https://coolaj86.com/articles/convert-js-bigints-to-typedarrays/) + // this is faster than shift+mod iterations + let hex = x.toString(16); + if (hex.length % 2 === 1) { + hex = '0' + hex; + } + + const rawLength = hex.length / 2; + const bytes = new Uint8Array(length || rawLength); + // parse hex + const offset = length ? length - rawLength : 0; + let i = 0; + while (i < rawLength) { + bytes[i + offset] = parseInt(hex.slice(2 * i, 2 * i + 2), 16); + i++; + } + + if (endian !== 'be') { + bytes.reverse(); + } + + return bytes; +} diff --git a/src/crypto/pkcs1.js b/src/crypto/pkcs1.js index 229081e8..d888017d 100644 --- a/src/crypto/pkcs1.js +++ b/src/crypto/pkcs1.js @@ -132,7 +132,7 @@ export function emeDecode(encoded, randomPayload) { * @param {Integer} emLen - Intended length in octets of the encoded message * @returns {Uint8Array} Encoded message. */ -export async function emsaEncode(algo, hashed, emLen) { +export function emsaEncode(algo, hashed, emLen) { let i; if (hashed.length !== hash.getHashByteLength(algo)) { throw new Error('Invalid hash length'); diff --git a/src/crypto/public_key/dsa.js b/src/crypto/public_key/dsa.js index 80da4328..c01e3185 100644 --- a/src/crypto/public_key/dsa.js +++ b/src/crypto/public_key/dsa.js @@ -22,7 +22,7 @@ import { getRandomBigInteger } from '../random'; import util from '../../util'; import { isProbablePrime } from './prime'; -import BigInteger from '../../biginteger'; +import { bigIntToUint8Array, bitLength, byteLength, mod, modExp, modInv, uint8ArrayToBigInt } from '../biginteger'; /* TODO regarding the hash function, read: @@ -30,6 +30,9 @@ import BigInteger from '../../biginteger'; https://tools.ietf.org/html/rfc4880#section-14 */ +const _0n = BigInt(0); +const _1n = BigInt(1); + /** * DSA Sign function * @param {Integer} hashAlgo @@ -42,24 +45,24 @@ import BigInteger from '../../biginteger'; * @async */ export async function sign(hashAlgo, hashed, g, p, q, x) { - const one = new BigInteger(1); - p = new BigInteger(p); - q = new BigInteger(q); - g = new BigInteger(g); - x = new BigInteger(x); + const _0n = BigInt(0); + p = uint8ArrayToBigInt(p); + q = uint8ArrayToBigInt(q); + g = uint8ArrayToBigInt(g); + x = uint8ArrayToBigInt(x); let k; let r; let s; let t; - g = g.mod(p); - x = x.mod(q); + g = mod(g, p); + x = mod(x, q); // If the output size of the chosen hash is larger than the number of // bits of q, the hash result is truncated to fit by taking the number // 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 = new BigInteger(hashed.subarray(0, q.byteLength())).mod(q); + const h = mod(uint8ArrayToBigInt(hashed.subarray(0, byteLength(q))), 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 @@ -67,22 +70,22 @@ export async function sign(hashAlgo, hashed, g, p, q, x) { // or s = 0 if signatures are generated properly. while (true) { // See Appendix B here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf - k = await getRandomBigInteger(one, q); // returns in [1, q-1] - r = g.modExp(k, p).imod(q); // (g**k mod p) mod q - if (r.isZero()) { + k = getRandomBigInteger(_1n, q); // returns in [1, q-1] + r = mod(modExp(g, k, p), q); // (g**k mod p) mod q + if (r === _0n) { continue; } - const xr = x.mul(r).imod(q); - t = h.add(xr).imod(q); // H(m) + x*r mod q - s = k.modInv(q).imul(t).imod(q); // k**-1 * (H(m) + x*r) mod q - if (s.isZero()) { + const xr = mod(x * r, q); + t = mod(h + xr, q); // H(m) + x*r mod q + s = mod(modInv(k, q) * t, q); // k**-1 * (H(m) + x*r) mod q + if (s === _0n) { continue; } break; } return { - r: r.toUint8Array('be', q.byteLength()), - s: s.toUint8Array('be', q.byteLength()) + r: bigIntToUint8Array(r, 'be', byteLength(p)), + s: bigIntToUint8Array(s, 'be', byteLength(p)) }; } @@ -100,35 +103,34 @@ export async function sign(hashAlgo, hashed, g, p, q, x) { * @async */ export async function verify(hashAlgo, r, s, hashed, g, p, q, y) { - const zero = new BigInteger(0); - r = new BigInteger(r); - s = new BigInteger(s); + r = uint8ArrayToBigInt(r); + s = uint8ArrayToBigInt(s); - p = new BigInteger(p); - q = new BigInteger(q); - g = new BigInteger(g); - y = new BigInteger(y); + p = uint8ArrayToBigInt(p); + q = uint8ArrayToBigInt(q); + g = uint8ArrayToBigInt(g); + y = uint8ArrayToBigInt(y); - if (r.lte(zero) || r.gte(q) || - s.lte(zero) || s.gte(q)) { + if (r <= _0n || r >= q || + s <= _0n || s >= q) { util.printDebug('invalid DSA Signature'); return false; } - const h = new BigInteger(hashed.subarray(0, q.byteLength())).imod(q); - const w = s.modInv(q); // s**-1 mod q - if (w.isZero()) { + const h = mod(uint8ArrayToBigInt(hashed.subarray(0, byteLength(q))), q); + const w = modInv(s, q); // s**-1 mod q + if (w === _0n) { util.printDebug('invalid DSA Signature'); return false; } - g = g.mod(p); - y = y.mod(p); - const u1 = h.mul(w).imod(q); // H(m) * w mod q - const u2 = r.mul(w).imod(q); // r * w mod q - const t1 = g.modExp(u1, p); // g**u1 mod p - const t2 = y.modExp(u2, p); // y**u2 mod p - const v = t1.mul(t2).imod(p).imod(q); // (g**u1 * y**u2 mod p) mod q - return v.equal(r); + g = mod(g, p); + y = mod(y, p); + const u1 = mod(h * w, q); // H(m) * w mod q + const u2 = mod(r * w, q); // r * w mod q + const t1 = modExp(g, u1, p); // g**u1 mod p + const t2 = modExp(y, u2, p); // y**u2 mod p + const v = mod(mod(t1 * t2, p), q); // (g**u1 * y**u2 mod p) mod q + return v === r; } /** @@ -142,20 +144,19 @@ export async function verify(hashAlgo, r, s, hashed, g, p, q, y) { * @async */ export async function validateParams(p, q, g, y, x) { - p = new BigInteger(p); - q = new BigInteger(q); - g = new BigInteger(g); - y = new BigInteger(y); - const one = new BigInteger(1); + p = uint8ArrayToBigInt(p); + q = uint8ArrayToBigInt(q); + g = uint8ArrayToBigInt(g); + y = uint8ArrayToBigInt(y); // Check that 1 < g < p - if (g.lte(one) || g.gte(p)) { + if (g <= _1n || g >= p) { return false; } /** * Check that subgroup order q divides p-1 */ - if (!p.dec().mod(q).isZero()) { + if (mod(p - _1n, q) !== _0n) { return false; } @@ -163,16 +164,16 @@ export async function validateParams(p, q, g, y, x) { * g has order q * Check that g ** q = 1 mod p */ - if (!g.modExp(q, p).isOne()) { + if (modExp(g, q, p) !== _1n) { return false; } /** * Check q is large and probably prime (we mainly want to avoid small factors) */ - const qSize = new BigInteger(q.bitLength()); - const n150 = new BigInteger(150); - if (qSize.lt(n150) || !(await isProbablePrime(q, null, 32))) { + const qSize = BigInt(bitLength(q)); + const _150n = BigInt(150); + if (qSize < _150n || !isProbablePrime(q, null, 32)) { return false; } @@ -182,11 +183,11 @@ export async function validateParams(p, q, g, y, x) { * * Blinded exponentiation computes g**{rq + x} to compare to y */ - 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))) { + x = uint8ArrayToBigInt(x); + const _2n = BigInt(2); + const r = getRandomBigInteger(_2n << (qSize - _1n), _2n << qSize); // draw r of same size as q + const rqx = q * r + x; + if (y !== modExp(g, rqx, p)) { return false; } diff --git a/src/crypto/public_key/elgamal.js b/src/crypto/public_key/elgamal.js index a64b9681..6bd4641e 100644 --- a/src/crypto/public_key/elgamal.js +++ b/src/crypto/public_key/elgamal.js @@ -21,7 +21,9 @@ */ import { getRandomBigInteger } from '../random'; import { emeEncode, emeDecode } from '../pkcs1'; -import BigInteger from '../../biginteger'; +import { bigIntToUint8Array, bitLength, byteLength, mod, modExp, modInv, uint8ArrayToBigInt } from '../biginteger'; + +const _1n = BigInt(1); /** * ElGamal Encryption function @@ -34,19 +36,19 @@ import BigInteger from '../../biginteger'; * @async */ export async function encrypt(data, p, g, y) { - p = new BigInteger(p); - g = new BigInteger(g); - y = new BigInteger(y); + p = uint8ArrayToBigInt(p); + g = uint8ArrayToBigInt(g); + y = uint8ArrayToBigInt(y); - const padded = emeEncode(data, p.byteLength()); - const m = new BigInteger(padded); + const padded = emeEncode(data, byteLength(p)); + const m = uint8ArrayToBigInt(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(new BigInteger(1), p.dec()); + const k = getRandomBigInteger(_1n, p - _1n); return { - c1: g.modExp(k, p).toUint8Array(), - c2: y.modExp(k, p).imul(m).imod(p).toUint8Array() + c1: bigIntToUint8Array(modExp(g, k, p)), + c2: bigIntToUint8Array(mod(modExp(y, k, p) * m, p)) }; } @@ -63,13 +65,13 @@ export async function encrypt(data, p, g, y) { * @async */ export async function decrypt(c1, c2, p, x, randomPayload) { - c1 = new BigInteger(c1); - c2 = new BigInteger(c2); - p = new BigInteger(p); - x = new BigInteger(x); + c1 = uint8ArrayToBigInt(c1); + c2 = uint8ArrayToBigInt(c2); + p = uint8ArrayToBigInt(p); + x = uint8ArrayToBigInt(x); - const padded = c1.modExp(x, p).modInv(p).imul(c2).imod(p); - return emeDecode(padded.toUint8Array('be', p.byteLength()), randomPayload); + const padded = mod(modInv(modExp(c1, x, p), p) * c2, p); + return emeDecode(bigIntToUint8Array(padded, 'be', byteLength(p)), randomPayload); } /** @@ -82,20 +84,19 @@ export async function decrypt(c1, c2, p, x, randomPayload) { * @async */ export async function validateParams(p, g, y, x) { - p = new BigInteger(p); - g = new BigInteger(g); - y = new BigInteger(y); + p = uint8ArrayToBigInt(p); + g = uint8ArrayToBigInt(g); + y = uint8ArrayToBigInt(y); - const one = new BigInteger(1); // Check that 1 < g < p - if (g.lte(one) || g.gte(p)) { + if (g <= _1n || g >= p) { return false; } // Expect p-1 to be large - const pSize = new BigInteger(p.bitLength()); - const n1023 = new BigInteger(1023); - if (pSize.lt(n1023)) { + const pSize = BigInt(bitLength(p)); + const _1023n = BigInt(1023); + if (pSize < _1023n) { return false; } @@ -103,7 +104,7 @@ export async function validateParams(p, g, y, x) { * g should have order p-1 * Check that g ** (p-1) = 1 mod p */ - if (!g.modExp(p.dec(), p).isOne()) { + if (modExp(g, p - _1n, p) !== _1n) { return false; } @@ -114,14 +115,15 @@ 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 = 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()) { + let i = BigInt(1); + const _2n = BigInt(2); + const threshold = _2n << BigInt(17); // we want order > threshold + while (i < threshold) { + res = mod(res * g, p); + if (res === _1n) { return false; } - i.iinc(); + i++; } /** @@ -130,11 +132,10 @@ export async function validateParams(p, g, y, x) { * * Blinded exponentiation computes g**{r(p-1) + x} to compare to y */ - 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))) { + x = uint8ArrayToBigInt(x); + const r = getRandomBigInteger(_2n << (pSize - _1n), _2n << pSize); // draw r of same size as p-1 + const rqx = (p - _1n) * r + x; + if (y !== modExp(g, rqx, p)) { return false; } diff --git a/src/crypto/public_key/elliptic/ecdsa.js b/src/crypto/public_key/elliptic/ecdsa.js index b7db3a85..76fe73d1 100644 --- a/src/crypto/public_key/elliptic/ecdsa.js +++ b/src/crypto/public_key/elliptic/ecdsa.js @@ -25,7 +25,7 @@ import util from '../../../util'; import { getRandomBytes } from '../../random'; import hash from '../../hash'; import { CurveWithOID, webCurves, privateToJWK, rawPublicToJWK, validateStandardParams, nodeCurves } from './oid_curves'; -import BigInteger from '../../../biginteger'; +import { bigIntToUint8Array } from '../../biginteger'; const webCrypto = util.getWebCrypto(); const nodeCrypto = util.getNodeCrypto(); @@ -73,8 +73,8 @@ export async function sign(oid, hashAlgo, message, publicKey, privateKey, hashed // lowS: non-canonical sig: https://stackoverflow.com/questions/74338846/ecdsa-signature-verification-mismatch const signature = nobleCurve.sign(hashed, privateKey, { lowS: false }); return { - r: new BigInteger(signature.r).toUint8Array('be', curve.payloadSize), - s: new BigInteger(signature.s).toUint8Array('be', curve.payloadSize) + r: bigIntToUint8Array(signature.r, 'be', curve.payloadSize), + s: bigIntToUint8Array(signature.s, 'be', curve.payloadSize) }; } diff --git a/src/crypto/public_key/prime.js b/src/crypto/public_key/prime.ts similarity index 79% rename from src/crypto/public_key/prime.js rename to src/crypto/public_key/prime.ts index 854f02ba..b892d666 100644 --- a/src/crypto/public_key/prime.js +++ b/src/crypto/public_key/prime.ts @@ -19,21 +19,20 @@ * @fileoverview Algorithms for probabilistic random prime generation * @module crypto/public_key/prime */ -import BigInteger from '../../biginteger'; +import { bigIntToNumber, bitLength, gcd, getBit, mod, modExp } from '../biginteger'; import { getRandomBigInteger } from '../random'; +const _1n = BigInt(1); + /** * Generate a probably prime random number - * @param {Integer} bits - Bit length of the prime - * @param {BigInteger} e - Optional RSA exponent to check against the prime - * @param {Integer} k - Optional number of iterations of Miller-Rabin test - * @returns BigInteger - * @async + * @param bits - Bit length of the prime + * @param e - Optional RSA exponent to check against the prime + * @param k - Optional number of iterations of Miller-Rabin test */ -export async function randomProbablePrime(bits, e, k) { - const one = new BigInteger(1); - const min = one.leftShift(new BigInteger(bits - 1)); - const thirty = new BigInteger(30); +export function randomProbablePrime(bits: number, e: bigint, k: number) { + const _30n = BigInt(30); + const min = _1n << BigInt(bits - 1); /* * 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 @@ -42,40 +41,38 @@ export async function randomProbablePrime(bits, e, k) { */ const adds = [1, 6, 5, 4, 3, 2, 1, 4, 3, 2, 1, 2, 1, 4, 3, 2, 1, 2, 1, 4, 3, 2, 1, 6, 5, 4, 3, 2, 1, 2]; - const n = await getRandomBigInteger(min, min.leftShift(one)); - let i = n.mod(thirty).toNumber(); + let n = getRandomBigInteger(min, min << _1n); + let i = bigIntToNumber(mod(n, _30n)); do { - n.iadd(new BigInteger(adds[i])); + n += BigInt(adds[i]); i = (i + adds[i]) % adds.length; // If reached the maximum, go back to the minimum. - if (n.bitLength() > bits) { - n.imod(min.leftShift(one)).iadd(min); - i = n.mod(thirty).toNumber(); + if (bitLength(n) > bits) { + n = mod(n, min << _1n); n += min; + i = bigIntToNumber(mod(n, _30n)); } - } while (!await isProbablePrime(n, e, k)); + } while (!isProbablePrime(n, e, k)); return n; } /** * Probabilistic primality testing - * @param {BigInteger} n - Number to test - * @param {BigInteger} e - Optional RSA exponent to check against the prime - * @param {Integer} k - Optional number of iterations of Miller-Rabin test - * @returns {boolean} - * @async + * @param n - Number to test + * @param e - Optional RSA exponent to check against the prime + * @param k - Optional number of iterations of Miller-Rabin test */ -export async function isProbablePrime(n, e, k) { - if (e && !n.dec().gcd(e).isOne()) { +export function isProbablePrime(n: bigint, e: bigint, k: number) { + if (e && gcd(n - _1n, e) !== _1n) { return false; } - if (!await divisionTest(n)) { + if (!divisionTest(n)) { return false; } - if (!await fermat(n)) { + if (!fermat(n)) { return false; } - if (!await millerRabin(n, k)) { + if (!millerRabin(n, k)) { return false; } // TODO implement the Lucas test @@ -86,19 +83,16 @@ export async function isProbablePrime(n, e, k) { /** * Tests whether n is probably prime or not using Fermat's test with b = 2. * Fails if b^(n-1) mod n != 1. - * @param {BigInteger} n - Number to test - * @param {BigInteger} b - Optional Fermat test base - * @returns {boolean} + * @param n - Number to test + * @param b - Optional Fermat test base */ -export async function fermat(n, b) { - b = b || new BigInteger(2); - return b.modExp(n.dec(), n).isOne(); +export function fermat(n: bigint, b = BigInt(2)) { + return modExp(b, n - _1n, n) === _1n; } -export async function divisionTest(n) { - return smallPrimes.every(m => { - return n.mod(new BigInteger(m)) !== 0; - }); +export function divisionTest(n: bigint) { + const _0n = BigInt(0); + return smallPrimes.every(m => mod(n, m) !== _0n); } // https://github.com/gpg/libgcrypt/blob/master/cipher/primegen.c @@ -182,7 +176,7 @@ const smallPrimes = [ 4801, 4813, 4817, 4831, 4861, 4871, 4877, 4889, 4903, 4909, 4919, 4931, 4933, 4937, 4943, 4951, 4957, 4967, 4969, 4973, 4987, 4993, 4999 -]; +].map(n => BigInt(n)); // Miller-Rabin - Miller Rabin algorithm for primality test @@ -217,42 +211,42 @@ const smallPrimes = [ /** * Tests whether n is probably prime or not using the Miller-Rabin test. * See HAC Remark 4.28. - * @param {BigInteger} n - Number to test - * @param {Integer} k - Optional number of iterations of Miller-Rabin test - * @param {Function} rand - Optional function to generate potential witnesses + * @param n - Number to test + * @param k - Optional number of iterations of Miller-Rabin test + * @param rand - Optional function to generate potential witnesses * @returns {boolean} * @async */ -export async function millerRabin(n, k, rand) { - const len = n.bitLength(); +export function millerRabin(n: bigint, k: number, rand?: () => bigint) { + const len = bitLength(n); if (!k) { k = Math.max(1, (len / 48) | 0); } - const n1 = n.dec(); // n - 1 + const n1 = n - _1n; // n - 1 // Find d and s, (n - 1) = (2 ^ s) * d; let s = 0; - while (!n1.getBit(s)) { s++; } - const d = n.rightShift(new BigInteger(s)); + while (!getBit(n1, s)) { s++; } + const d = n >> BigInt(s); for (; k > 0; k--) { - const a = rand ? rand() : await getRandomBigInteger(new BigInteger(2), n1); + const a = rand ? rand() : getRandomBigInteger(BigInt(2), n1); - let x = a.modExp(d, n); - if (x.isOne() || x.equal(n1)) { + let x = modExp(a, d, n); + if (x === _1n || x === n1) { continue; } let i; for (i = 1; i < s; i++) { - x = x.mul(x).mod(n); + x = mod(x * x, n); - if (x.isOne()) { + if (x === _1n) { return false; } - if (x.equal(n1)) { + if (x === n1) { break; } } diff --git a/src/crypto/public_key/rsa.js b/src/crypto/public_key/rsa.js index e8c3adff..5c6027ca 100644 --- a/src/crypto/public_key/rsa.js +++ b/src/crypto/public_key/rsa.js @@ -25,10 +25,11 @@ import util from '../../util'; import { uint8ArrayToB64, b64ToUint8Array } from '../../encoding/base64'; import { emsaEncode, emeEncode, emeDecode } from '../pkcs1'; import enums from '../../enums'; -import BigInteger from '../../biginteger'; +import { bigIntToNumber, bigIntToUint8Array, bitLength, byteLength, mod, modExp, modInv, uint8ArrayToBigInt } from '../biginteger'; const webCrypto = util.getWebCrypto(); const nodeCrypto = util.getNodeCrypto(); +const _1n = BigInt(1); /** Create signature * @param {module:enums.hash} hashAlgo - Hash algorithm @@ -142,14 +143,14 @@ export async function decrypt(data, n, e, d, p, q, u, randomPayload) { * @async */ export async function generate(bits, e) { - e = new BigInteger(e); + e = BigInt(e); // Native RSA keygen using Web Crypto if (util.getWebCrypto()) { const keyGenOpt = { name: 'RSASSA-PKCS1-v1_5', modulusLength: bits, // the specified keysize in bits - publicExponent: e.toUint8Array(), // take three bytes (max 65537) for exponent + publicExponent: bigIntToUint8Array(e), // take three bytes (max 65537) for exponent hash: { name: 'SHA-1' // not required for actual RSA keys, but for crypto api 'sign' and 'verify' } @@ -164,7 +165,7 @@ export async function generate(bits, e) { } else if (util.getNodeCrypto()) { const opts = { modulusLength: bits, - publicExponent: e.toNumber(), + publicExponent: bigIntToNumber(e), publicKeyEncoding: { type: 'pkcs1', format: 'jwk' }, privateKeyEncoding: { type: 'pkcs1', format: 'jwk' } }; @@ -187,26 +188,26 @@ export async function generate(bits, e) { let q; let n; do { - q = await randomProbablePrime(bits - (bits >> 1), e, 40); - p = await randomProbablePrime(bits >> 1, e, 40); - n = p.mul(q); - } while (n.bitLength() !== bits); + q = randomProbablePrime(bits - (bits >> 1), e, 40); + p = randomProbablePrime(bits >> 1, e, 40); + n = p * q; + } while (bitLength(n) !== bits); - const phi = p.dec().imul(q.dec()); + const phi = (p - _1n) * (q - _1n); - if (q.lt(p)) { + if (q < p) { [p, q] = [q, p]; } return { - n: n.toUint8Array(), - e: e.toUint8Array(), - d: e.modInv(phi).toUint8Array(), - p: p.toUint8Array(), - q: q.toUint8Array(), + n: bigIntToUint8Array(n), + e: bigIntToUint8Array(e), + d: bigIntToUint8Array(modInv(e, phi)), + p: bigIntToUint8Array(p), + q: bigIntToUint8Array(q), // dp: d.mod(p.subn(1)), // dq: d.mod(q.subn(1)), - u: p.modInv(q).toUint8Array() + u: bigIntToUint8Array(modInv(p, q)) }; } @@ -222,24 +223,24 @@ export async function generate(bits, e) { * @async */ export async function validateParams(n, e, d, p, q, u) { - n = new BigInteger(n); - p = new BigInteger(p); - q = new BigInteger(q); + n = uint8ArrayToBigInt(n); + p = uint8ArrayToBigInt(p); + q = uint8ArrayToBigInt(q); // expect pq = n - if (!p.mul(q).equal(n)) { + if ((p * q) !== n) { return false; } - const two = new BigInteger(2); + const _2n = BigInt(2); // expect p*u = 1 mod q - u = new BigInteger(u); - if (!p.mul(u).mod(q).isOne()) { + u = uint8ArrayToBigInt(u); + if (mod(p * u, q) !== BigInt(1)) { return false; } - e = new BigInteger(e); - d = new BigInteger(d); + e = uint8ArrayToBigInt(e); + d = uint8ArrayToBigInt(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)] @@ -247,11 +248,11 @@ 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 = 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); + const nSizeOver3 = BigInt(Math.floor(bitLength(n) / 3)); + const r = getRandomBigInteger(_2n, _2n << nSizeOver3); // r in [ 2, 2^{|n|/3} ) < p and q + const rde = r * d * e; - const areInverses = rde.mod(p.dec()).equal(r) && rde.mod(q.dec()).equal(r); + const areInverses = mod(rde, p - _1n) === r && mod(rde, q - _1n) === r; if (!areInverses) { return false; } @@ -260,13 +261,13 @@ export async function validateParams(n, e, d, p, q, u) { } async function bnSign(hashAlgo, n, d, hashed) { - n = new BigInteger(n); - const m = new BigInteger(await emsaEncode(hashAlgo, hashed, n.byteLength())); - d = new BigInteger(d); - if (m.gte(n)) { + n = uint8ArrayToBigInt(n); + const m = uint8ArrayToBigInt(emsaEncode(hashAlgo, hashed, byteLength(n))); + d = uint8ArrayToBigInt(d); + if (m >= n) { throw new Error('Message size cannot exceed modulus size'); } - return m.modExp(d, n).toUint8Array('be', n.byteLength()); + return bigIntToUint8Array(modExp(m, d, n), 'be', byteLength(n)); } async function webSign(hashName, data, n, e, d, p, q, u) { @@ -296,14 +297,14 @@ async function nodeSign(hashAlgo, data, n, e, d, p, q, u) { } async function bnVerify(hashAlgo, s, n, e, hashed) { - n = new BigInteger(n); - s = new BigInteger(s); - e = new BigInteger(e); - if (s.gte(n)) { + n = uint8ArrayToBigInt(n); + s = uint8ArrayToBigInt(s); + e = uint8ArrayToBigInt(e); + if (s >= n) { throw new Error('Signature size cannot exceed modulus size'); } - const EM1 = s.modExp(e, n).toUint8Array('be', n.byteLength()); - const EM2 = await emsaEncode(hashAlgo, hashed, n.byteLength()); + const EM1 = bigIntToUint8Array(modExp(s, e, n), 'be', byteLength(n)); + const EM2 = emsaEncode(hashAlgo, hashed, byteLength(n)); return util.equalsUint8Array(EM1, EM2); } @@ -339,13 +340,13 @@ async function nodeEncrypt(data, n, e) { } async function bnEncrypt(data, n, e) { - n = new BigInteger(n); - data = new BigInteger(emeEncode(data, n.byteLength())); - e = new BigInteger(e); - if (data.gte(n)) { + n = uint8ArrayToBigInt(n); + data = uint8ArrayToBigInt(emeEncode(data, byteLength(n))); + e = uint8ArrayToBigInt(e); + if (data >= n) { throw new Error('Message size cannot exceed modulus size'); } - return data.modExp(e, n).toUint8Array('be', n.byteLength()); + return bigIntToUint8Array(modExp(data, e, n), 'be', byteLength(n)); } async function nodeDecrypt(data, n, e, d, p, q, u) { @@ -360,34 +361,32 @@ async function nodeDecrypt(data, n, e, d, p, q, u) { } async function bnDecrypt(data, n, e, d, p, q, u, randomPayload) { - 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)) { + data = uint8ArrayToBigInt(data); + n = uint8ArrayToBigInt(n); + e = uint8ArrayToBigInt(e); + d = uint8ArrayToBigInt(d); + p = uint8ArrayToBigInt(p); + q = uint8ArrayToBigInt(q); + u = uint8ArrayToBigInt(u); + if (data >= 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 dq = mod(d, q - _1n); // d mod (q-1) + const dp = mod(d, p - _1n); // d mod (p-1) - const unblinder = (await getRandomBigInteger(new BigInteger(2), n)).mod(n); - const blinder = unblinder.modInv(n).modExp(e, n); - data = data.mul(blinder).mod(n); + const unblinder = getRandomBigInteger(BigInt(2), n); + const blinder = modExp(modInv(unblinder, n), e, n); + data = mod(data * blinder, n); + const mp = modExp(data, dp, p); // data**{d mod (q-1)} mod p + const mq = modExp(data, dq, q); // data**{d mod (p-1)} mod q + const h = mod(u * (mq - mp), q); // u * (mq-mp) mod q (operands already < q) - const mp = data.modExp(dp, p); // data**{d mod (q-1)} mod p - const mq = data.modExp(dq, q); // data**{d mod (p-1)} mod q - const h = u.mul(mq.sub(mp)).mod(q); // u * (mq-mp) mod q (operands already < q) + let result = h * p + mp; // result < n due to relations above - let result = h.mul(p).add(mp); // result < n due to relations above + result = mod(result * unblinder, n); - result = result.mul(unblinder).mod(n); - - - return emeDecode(result.toUint8Array('be', n.byteLength()), randomPayload); + return emeDecode(bigIntToUint8Array(result, 'be', byteLength(n)), randomPayload); } /** Convert Openpgp private key params to jwk key according to @@ -401,14 +400,14 @@ async function bnDecrypt(data, n, e, d, p, q, u, randomPayload) { * @param {Uint8Array} u */ async function privateToJWK(n, e, d, p, q, u) { - const pNum = new BigInteger(p); - const qNum = new BigInteger(q); - const dNum = new BigInteger(d); + const pNum = uint8ArrayToBigInt(p); + const qNum = uint8ArrayToBigInt(q); + const dNum = uint8ArrayToBigInt(d); - let dq = dNum.mod(qNum.dec()); // d mod (q-1) - let dp = dNum.mod(pNum.dec()); // d mod (p-1) - dp = dp.toUint8Array(); - dq = dq.toUint8Array(); + let dq = mod(dNum, qNum - _1n); // d mod (q-1) + let dp = mod(dNum, pNum - _1n); // d mod (p-1) + dp = bigIntToUint8Array(dp); + dq = bigIntToUint8Array(dq); return { kty: 'RSA', n: uint8ArrayToB64(n, true), @@ -444,7 +443,7 @@ function publicToJWK(n, e) { function jwkToPrivate(jwk, e) { return { n: b64ToUint8Array(jwk.n), - e: e.toUint8Array(), + e: bigIntToUint8Array(e), d: b64ToUint8Array(jwk.d), // switch p and q p: b64ToUint8Array(jwk.q), diff --git a/src/crypto/random.js b/src/crypto/random.js index 4b038587..c7e53169 100644 --- a/src/crypto/random.js +++ b/src/crypto/random.js @@ -21,7 +21,7 @@ * @fileoverview Provides tools for retrieving secure randomness from browsers or Node.js * @module crypto/random */ -import BigInteger from '../biginteger'; +import { byteLength, mod, uint8ArrayToBigInt } from './biginteger'; import util from '../util'; const nodeCrypto = util.getNodeCrypto(); @@ -45,23 +45,23 @@ export function getRandomBytes(length) { } /** - * Create a secure random BigInteger that is greater than or equal to min and less than max. - * @param {module:BigInteger} min - Lower bound, included - * @param {module:BigInteger} max - Upper bound, excluded - * @returns {Promise} Random BigInteger. + * Create a secure random BigInt that is greater than or equal to min and less than max. + * @param {bigint} min - Lower bound, included + * @param {bigint} max - Upper bound, excluded + * @returns {bigint} Random BigInt. * @async */ -export async function getRandomBigInteger(min, max) { - if (max.lt(min)) { +export function getRandomBigInteger(min, max) { + if (max < min) { throw new Error('Illegal parameter value: max <= min'); } - const modulus = max.sub(min); - const bytes = modulus.byteLength(); + const modulus = max - min; + const bytes = byteLength(modulus); // 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 = new BigInteger(getRandomBytes(bytes + 8)); - return r.mod(modulus).add(min); + const r = uint8ArrayToBigInt(getRandomBytes(bytes + 8)); + return mod(r, modulus) + min; } diff --git a/test/crypto/biginteger.js b/test/crypto/biginteger.js new file mode 100644 index 00000000..aeabd943 --- /dev/null +++ b/test/crypto/biginteger.js @@ -0,0 +1,96 @@ +import { expect } from 'chai'; + +import BN from 'bn.js'; +import { bigIntToUint8Array, bitLength, byteLength, gcd, getBit, modExp, modInv } from '../../src/crypto/biginteger'; +import { getRandomBytes } from '../../src/crypto/random'; + +async function getRandomBN(min, max) { + if (max.cmp(min) <= 0) { + throw new Error('Illegal parameter value: max <= min'); + } + + const modulus = max.sub(min); + const bytes = modulus.byteLength(); + const r = new BN(getRandomBytes(bytes + 8)); + return r.mod(modulus).add(min); +} + + +export default () => describe('BigInt', () => { + it('bitLength is correct', function() { + const n = BigInt(127); + const expected = 7; + expect(bitLength(n)).to.equal(expected); + expect(bitLength(n + BigInt(1))).to.equal(expected + 1); + }); + + it('byteLength is correct', function() { + const n = BigInt(65535); + const expected = 2; + expect(byteLength(n)).to.equal(expected); + expect(byteLength(n + BigInt(1))).to.equal(expected + 1); + }); + + it('toUint8Array is correct', function() { + const nString = '417653931840771530406225971293556769925351769207235721650257629558293828796031115397206059067934284452829611906818956352854418342467914729341523414945427019410284762464062112274326172407819051167058569790660930309496043254270888417520676082271432948852231332576271876251597199882908964994070268531832274431027'; + const n = BigInt(nString); + const paddedSize = Number(byteLength(n)) + 1; + // big endian, unpadded + let expected = new BN(nString).toArrayLike(Uint8Array); + expect(bigIntToUint8Array(n)).to.deep.equal(expected); + // big endian, padded + expected = new BN(nString).toArrayLike(Uint8Array, 'be', paddedSize); + expect(bigIntToUint8Array(n, 'be', paddedSize)).to.deep.equal(expected); + // little endian, unpadded + expected = new BN(nString).toArrayLike(Uint8Array, 'le'); + expect(bigIntToUint8Array(n, 'le')).to.deep.equal(expected); + //little endian, padded + expected = new BN(nString).toArrayLike(Uint8Array, 'le', paddedSize); + expect(bigIntToUint8Array(n, 'le', paddedSize)).to.deep.equal(expected); + }); + + it('modExp is correct (large values)', function() { + const stringX = '417653931840771530406225971293556769925351769207235721650257629558293828796031115397206059067934284452829611906818956352854418342467914729341523414945427019410284762464062112274326172407819051167058569790660930309496043254270888417520676082271432948852231332576271876251597199882908964994070268531832274431027'; + const stringE = '21139356010872569239159922781526379521587348169074209285187910481667533072168468011617194695181255483288792585413365359733692097084373249198758148704369207793873998901870577262254971784191473102265830193058813215898765238784670469696574407580179153118937858890572095234316482449291777882525949871374961971753'; + const stringN = '129189808515414783602892982235788912674846062846614219472827821758734760420002631653235573915244294540972376140705505703576175711417114803419704967903726436285518767606681184247119430411311152556442947708732584954518890222684529678365388350886907287414896703685680210648760841628375425909680236584021041565183'; + const x = BigInt(stringX); + const e = BigInt(stringE); + const n = BigInt(stringN); + + const got = modExp(x, e, n); + const expected = new BN(stringX).toRed(BN.red(new BN(stringN))).redPow(new BN(stringE)); + // different formats, it's easier to compare strings + expect(got.toString(), expected.toString()); + }); + + it('gcd is correct', async function() { + const aBN = await getRandomBN(new BN(2), new BN(200)); + const bBN = await getRandomBN(new BN(2), new BN(200)); + if (aBN.isEven()) aBN.iaddn(1); + const a = BigInt(aBN.toString()); + const b = BigInt(bBN.toString()); + const expected = aBN.gcd(bBN); + expect(gcd(a, b).toString()).to.equal(expected.toString()); + }); + + it('modular inversion is correct', async function() { + const moduloBN = new BN(229); // this is a prime + const baseBN = await getRandomBN(new BN(2), moduloBN); + const a = BigInt(baseBN.toString()); + const n = BigInt(moduloBN.toString()); + const expected = baseBN.invm(moduloBN); + expect(modInv(a, n).toString()).to.equal(expected.toString()); + // test negative operand + const expectedNegated = baseBN.neg().invm(moduloBN); + expect(modInv(-a, n).toString()).to.equal(expectedNegated.toString()); + expect(() => modInv(a * n, n)).to.throw(/Inverse does not exist/); + }); + + it('getBit is correct', async function() { + const i = 5; + const nBN = await getRandomBN(new BN(2), new BN(200)); + const n = BigInt(nBN.toString()); + const expected = nBN.testn(5) ? 1 : 0; + expect(getBit(n, i)).to.equal(expected); + }); +}); diff --git a/test/crypto/index.js b/test/crypto/index.js index 86f45445..a4c73ae5 100644 --- a/test/crypto/index.js +++ b/test/crypto/index.js @@ -1,3 +1,4 @@ +import testBigInteger from './biginteger'; import testCipher from './cipher'; import testHash from './hash'; import testCrypto from './crypto'; @@ -14,6 +15,7 @@ import testRSA from './rsa'; import testValidate from './validate'; export default () => describe('Crypto', function () { + testBigInteger(); testCipher(); testHash(); testCrypto(); diff --git a/test/crypto/validate.js b/test/crypto/validate.js index 3731ea9e..8ed63089 100644 --- a/test/crypto/validate.js +++ b/test/crypto/validate.js @@ -3,7 +3,7 @@ import chaiAsPromised from 'chai-as-promised'; // eslint-disable-line import/new chaiUse(chaiAsPromised); import openpgp from '../initOpenpgp.js'; -import BigInteger from '../../src/biginteger.ts'; +import { bigIntToUint8Array, modExp, uint8ArrayToBigInt } from '../../src/crypto/biginteger.ts'; const armoredDSAKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- @@ -390,11 +390,12 @@ export default () => { const keyPacket = await cloneKeyPacket(egKey); const { p, g } = keyPacket.publicParams; - const pBN = new BigInteger(p); - const gBN = new BigInteger(g); + const _1n = BigInt(1); + const pBN = uint8ArrayToBigInt(p); + const gBN = uint8ArrayToBigInt(g); // g**(p-1)/2 has order 2 - const gOrd2 = gBN.modExp(pBN.dec().irightShift(new BigInteger(1)), pBN); - keyPacket.publicParams.g = gOrd2.toUint8Array(); + const gOrd2 = modExp(gBN, (pBN - _1n) >> _1n, pBN); + keyPacket.publicParams.g = bigIntToUint8Array(gOrd2); await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid'); }); }); diff --git a/tsconfig.json b/tsconfig.json index 6471d348..1b8fdcb0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,6 @@ "allowJs": true, "paths": { "openpgp": [ "." ] - }, + }, } }