From 8854b097b4baad4427408072aede7e224075f96e Mon Sep 17 00:00:00 2001 From: larabr <7375870+larabr@users.noreply.github.com> Date: Thu, 3 Sep 2020 16:02:59 +0200 Subject: [PATCH] Use native BigInt when available instead of bn.js (#1119) In the lightweight build, lazily load bn.js only when necessary. Also, use Uint8Arrays instead of strings in PKCS1 padding functions, and check that the leading zero is present when decoding EME-PKCS1 padding. --- .eslintrc.js | 3 +- src/biginteger/bn.interface.js | 324 +++++++++++++ src/biginteger/index.js | 14 + src/biginteger/native.interface.js | 441 ++++++++++++++++++ src/crypto/crypto.js | 31 +- src/crypto/pkcs1.js | 94 ++-- src/crypto/pkcs5.js | 37 +- src/crypto/public_key/dsa.js | 114 ++--- src/crypto/public_key/elgamal.js | 80 ++-- src/crypto/public_key/elliptic/curves.js | 9 +- src/crypto/public_key/elliptic/ecdh.js | 8 +- src/crypto/public_key/elliptic/ecdsa.js | 4 +- src/crypto/public_key/prime.js | 81 ++-- src/crypto/public_key/rsa.js | 195 ++++---- src/crypto/random.js | 19 +- src/crypto/signature.js | 20 +- .../public_key_encrypted_session_key.js | 20 +- src/type/mpi.js | 16 +- src/util.js | 27 ++ test/crypto/crypto.js | 20 +- test/crypto/pkcs5.js | 38 +- test/crypto/rsa.js | 24 +- test/general/biginteger.js | 168 +++++++ test/general/index.js | 1 + test/general/key.js | 6 +- test/general/openpgp.js | 3 +- test/general/packet.js | 8 +- 27 files changed, 1373 insertions(+), 432 deletions(-) create mode 100644 src/biginteger/bn.interface.js create mode 100644 src/biginteger/index.js create mode 100644 src/biginteger/native.interface.js create mode 100644 test/general/biginteger.js diff --git a/.eslintrc.js b/.eslintrc.js index c92e05ab..0b93f95e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -33,7 +33,8 @@ module.exports = { "postMessage": true, "resolves": true, "rejects": true, - "TransformStream": true + "TransformStream": true, + "BigInt": true }, "rules": { diff --git a/src/biginteger/bn.interface.js b/src/biginteger/bn.interface.js new file mode 100644 index 00000000..2185d0b6 --- /dev/null +++ b/src/biginteger/bn.interface.js @@ -0,0 +1,324 @@ +import BN from 'bn.js'; + +/** + * BigInteger implementation of basic operations + * Wrapper of bn.js library (wwww.github.com/indutny/bn.js) + */ +export default class BigInteger { + /** + * Get a BigInteger (input must be big endian for strings and arrays) + * @param {Number|String|Uint8Array} n value to convert + * @throws {Error} on undefined input + */ + constructor(n) { + if (n === undefined) { + throw new Error('Invalid BigInteger input'); + } + + this.value = new BN(n); + } + + clone() { + const clone = new BigInteger(null); + this.value.copy(clone.value); + return clone; + } + + /** + * BigInteger increment in place + */ + iinc() { + this.value.iadd(one.value); + return this; + } + + /** + * BigInteger increment + * @returns {BigInteger} this + 1 + */ + inc() { + return this.clone().iinc(); + } + + /** + * BigInteger decrement in place + */ + idec() { + this.value.isub(one.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) { + this.value.iadd(x.value); + return this; + } + + /** + * BigInteger addition + * @param {BigInteger} x value to add + * @returns {BigInteger} this + x + */ + add(x) { + return this.clone().iadd(x); + } + + /** + * BigInteger subtraction in place + * @param {BigInteger} x value to subtract + */ + isub(x) { + this.value.isub(x.value); + return this; + } + + /** + * BigInteger subtraction + * @param {BigInteger} x value to subtract + * @returns {BigInteger} this - x + */ + sub(x) { + return this.clone().isub(x); + } + + /** + * BigInteger multiplication in place + * @param {BigInteger} x value to multiply + */ + imul(x) { + this.value.imul(x.value); + return this; + } + + /** + * BigInteger multiplication + * @param {BigInteger} x value to multiply + * @returns {BigInteger} this * x + */ + mul(x) { + return this.clone().imul(x); + } + + /** + * Compute value modulo m, in place + * @param {BigInteger} m modulo + */ + imod(m) { + this.value = this.value.umod(m.value); + return this; + } + + /** + * Compute value modulo m + * @param {BigInteger} m modulo + * @returns {BigInteger} this mod m + */ + mod(m) { + return this.clone().imod(m); + } + + /** + * Compute modular exponentiation + * Much faster than this.exp(e).mod(n) + * @param {BigInteger} e exponent + * @param {BigInteger} n modulo + * @returns {BigInteger} this ** e mod n + */ + modExp(e, n) { + // We use either Montgomery or normal reduction context + // Montgomery requires coprime n and R (montogmery multiplier) + // bn.js picks R as power of 2, so n must be odd + const nred = n.isEven() ? BN.red(n.value) : BN.mont(n.value); + const x = this.clone(); + x.value = x.value.toRed(nred).redPow(e.value).fromRed(); + return x; + } + + /** + * Compute the inverse of this value modulo n + * Note: this and and n must be relatively prime + * @param {BigInteger} n modulo + * @return {BigInteger} x such that this*x = 1 mod n + */ + modInv(n) { + return new BigInteger(this.value.invm(n.value)); + } + + /** + * Compute greatest common divisor between this and n + * @param {BigInteger} n operand + * @return {BigInteger} gcd + */ + gcd(n) { + return new BigInteger(this.value.gcd(n.value)); + } + + /** + * Shift this to the left by x, in place + * @param {BigInteger} x shift value + */ + ileftShift(x) { + this.value.ishln(x.value.toNumber()); + return this; + } + + /** + * Shift this to the left by x + * @param {BigInteger} x shift value + * @returns {BigInteger} this << x + */ + leftShift(x) { + return this.clone().ileftShift(x); + } + + /** + * Shift this to the right by x, in place + * @param {BigInteger} x shift value + */ + irightShift(x) { + this.value.ishrn(x.value.toNumber()); + return this; + } + + /** + * Shift this to the right by x + * @param {BigInteger} x shift value + * @returns {BigInteger} this >> x + */ + rightShift(x) { + return this.clone().irightShift(x); + } + + /** + * Whether this value is equal to x + * @param {BigInteger} x + * @returns {Boolean} + */ + equal(x) { + return this.value.eq(x.value); + } + + /** + * Whether this value is less than x + * @param {BigInteger} x + * @returns {Boolean} + */ + lt(x) { + return this.value.lt(x.value); + } + + /** + * Whether this value is less than or equal to x + * @param {BigInteger} x + * @returns {Boolean} + */ + lte(x) { + return this.value.lte(x.value); + } + + /** + * Whether this value is greater than x + * @param {BigInteger} x + * @returns {Boolean} + */ + gt(x) { + return this.value.gt(x.value); + } + + /** + * Whether this value is greater than or equal to x + * @param {BigInteger} x + * @returns {Boolean} + */ + gte(x) { + return this.value.gte(x.value); + } + + isZero() { + return this.value.isZero(); + } + + isOne() { + return this.equal(one); + } + + isNegative() { + return this.value.isNeg(); + } + + isEven() { + return this.value.isEven(); + } + + abs() { + const res = this.clone(); + res.value = res.value.abs(); + 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 + * @return {Number} + */ + toNumber() { + return this.value.toNumber(); + } + + /** + * Get value of i-th bit + * @param {Number} i bit index + * @returns {Number} bit value + */ + getBit(i) { + return this.value.testn(i) ? 1 : 0; + } + + /** + * Compute bit length + * @returns {Number} bit length + */ + bitLength() { + return this.value.bitLength(); + } + + /** + * Compute byte length + * @returns {Number} byte length + */ + byteLength() { + return this.value.byteLength(); + } + + /** + * Get Uint8Array representation of this number + * @param {String} endian endianess of output array (defaults to 'be') + * @param {Number} length of output array + * @return {Uint8Array} + */ + toUint8Array(endian = 'be', length) { + return this.value.toArrayLike(Uint8Array, endian, length); + } +} + +const one = new BigInteger(1); diff --git a/src/biginteger/index.js b/src/biginteger/index.js new file mode 100644 index 00000000..2cd1d54a --- /dev/null +++ b/src/biginteger/index.js @@ -0,0 +1,14 @@ +import util from '../util'; +import BigInteger from './native.interface'; + +async function getBigInteger() { + if (util.detectBigInt()) { + return BigInteger; + } else { + const { default: BigInteger } = await import('./bn.interface'); + return BigInteger; + } +} + +// eslint-disable-next-line import/prefer-default-export +export { getBigInteger }; diff --git a/src/biginteger/native.interface.js b/src/biginteger/native.interface.js new file mode 100644 index 00000000..bafc05dd --- /dev/null +++ b/src/biginteger/native.interface.js @@ -0,0 +1,441 @@ +/* eslint-disable new-cap */ + +/** + * 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 { + /** + * 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) { + if (n === undefined) { + throw new Error('Invalid BigInteger input'); + } + + if (n instanceof Uint8Array) { + const bytes = n; + const hex = new Array(bytes.length); + for (let i = 0; i < bytes.length; i++) { + const hexByte = bytes[i].toString(16); + hex[i] = (bytes[i] <= 0xF) ? ('0' + hexByte) : hexByte; + } + this.value = BigInt('0x0' + hex.join('')); + } 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) { + this.value += x.value; + return this; + } + + /** + * BigInteger addition + * @param {BigInteger} x value to add + * @returns {BigInteger} this + x + */ + add(x) { + return this.clone().iadd(x); + } + + /** + * BigInteger subtraction in place + * @param {BigInteger} x value to subtract + */ + isub(x) { + this.value -= x.value; + return this; + } + + /** + * BigInteger subtraction + * @param {BigInteger} x value to subtract + * @returns {BigInteger} this - x + */ + sub(x) { + return this.clone().isub(x); + } + + /** + * BigInteger multiplication in place + * @param {BigInteger} x value to multiply + */ + imul(x) { + this.value *= x.value; + return this; + } + + /** + * BigInteger multiplication + * @param {BigInteger} x value to multiply + * @returns {BigInteger} this * x + */ + mul(x) { + return this.clone().imul(x); + } + + /** + * Compute value modulo m, in place + * @param {BigInteger} m modulo + */ + imod(m) { + 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) { + 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, n) { + 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 + * @return {BigInteger} x such that this*x = 1 mod n + */ + modInv(n) { + return this._egcd(n).x.add(n).mod(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) + * @param {BigInteger} b second operand + * @returns { gcd, x, y: BigInteger } + */ + _egcd(b) { + let x = BigInt(0); + let y = BigInt(1); + let xPrev = BigInt(1); + let yPrev = BigInt(0); + + let a = this.value; + b = b.value; + + 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(xPrev), + y: new BigInteger(yPrev), + gcd: new BigInteger(a) + }; + } + + /** + * Compute greatest common divisor between this and n + * @param {BigInteger} b operand + * @return {BigInteger} gcd + */ + gcd(b) { + let a = this.value; + b = b.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) { + this.value <<= x.value; + return this; + } + + /** + * Shift this to the left by x + * @param {BigInteger} x shift value + * @returns {BigInteger} this << x + */ + leftShift(x) { + return this.clone().ileftShift(x); + } + + /** + * Shift this to the right by x, in place + * @param {BigInteger} x shift value + */ + irightShift(x) { + this.value >>= x.value; + return this; + } + + /** + * Shift this to the right by x + * @param {BigInteger} x shift value + * @returns {BigInteger} this >> x + */ + rightShift(x) { + return this.clone().irightShift(x); + } + + /** + * Whether this value is equal to x + * @param {BigInteger} x + * @returns {Boolean} + */ + equal(x) { + return this.value === x.value; + } + + /** + * Whether this value is less than x + * @param {BigInteger} x + * @returns {Boolean} + */ + lt(x) { + return this.value < x.value; + } + + /** + * Whether this value is less than or equal to x + * @param {BigInteger} x + * @returns {Boolean} + */ + lte(x) { + return this.value <= x.value; + } + + /** + * Whether this value is greater than x + * @param {BigInteger} x + * @returns {Boolean} + */ + gt(x) { + return this.value > x.value; + } + + /** + * Whether this value is greater than or equal to x + * @param {BigInteger} x + * @returns {Boolean} + */ + gte(x) { + 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; + } + + /** + * 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 + * @return {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) { + 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 + * @return {Uint8Array} + */ + toUint8Array(endian = 'be', length) { + // 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/crypto.js b/src/crypto/crypto.js index cbfd5e28..9626a693 100644 --- a/src/crypto/crypto.js +++ b/src/crypto/crypto.js @@ -61,8 +61,8 @@ export default { * @param {Array} pub_params Algorithm-specific public key parameters - * @param {String} data Data to be encrypted - * @param {String} fingerprint Recipient fingerprint + * @param {Uint8Array} data Data to be encrypted + * @param {Uint8Array} fingerprint Recipient fingerprint * @returns {Array} encrypted session key parameters * @async @@ -72,7 +72,6 @@ export default { switch (algo) { case enums.publicKey.rsaEncrypt: case enums.publicKey.rsaEncryptSign: { - data = util.strToUint8Array(data); const n = pub_params[0].toUint8Array(); const e = pub_params[1].toUint8Array(); const res = await publicKey.rsa.encrypt(data, n, e); @@ -80,10 +79,10 @@ export default { } case enums.publicKey.elgamal: { data = new type_mpi(await pkcs1.eme.encode(data, pub_params[0].byteLength())); - const m = data.toBN(); - const p = pub_params[0].toBN(); - const g = pub_params[1].toBN(); - const y = pub_params[2].toBN(); + const m = await data.toBigInteger(); + const p = await pub_params[0].toBigInteger(); + const g = await pub_params[1].toBigInteger(); + const y = await pub_params[2].toBigInteger(); const res = await publicKey.elgamal.encrypt(m, p, g, y); return constructParams(types, [res.c1, res.c2]); } @@ -129,14 +128,12 @@ export default { return publicKey.rsa.decrypt(c, n, e, d, p, q, u); } case enums.publicKey.elgamal: { - const c1 = data_params[0].toBN(); - const c2 = data_params[1].toBN(); - const p = key_params[0].toBN(); - const x = key_params[3].toBN(); - const result = new type_mpi(await publicKey.elgamal.decrypt(c1, c2, p, x)); // MPI and BN.js discard any leading zeros - return pkcs1.eme.decode( - util.Uint8Array_to_str(result.toUint8Array('be', p.byteLength())) // re-introduce leading zeros - ); + const c1 = await data_params[0].toBigInteger(); + const c2 = await data_params[1].toBigInteger(); + const p = await key_params[0].toBigInteger(); + const x = await key_params[3].toBigInteger(); + const result = new type_mpi(await publicKey.elgamal.decrypt(c1, c2, p, x)); + return pkcs1.eme.decode(result.toUint8Array('be', p.byteLength())); } case enums.publicKey.ecdh: { const oid = key_params[0]; @@ -147,7 +144,7 @@ export default { const d = key_params[3].toUint8Array(); const result = new type_mpi(await publicKey.elliptic.ecdh.decrypt( oid, kdfParams, V, C, Q, d, fingerprint)); - return pkcs5.decode(result.toString()); + return pkcs5.decode(result.toUint8Array()); } default: throw new Error('Invalid public key encryption algorithm.'); @@ -271,7 +268,7 @@ export default { case enums.publicKey.rsaEncrypt: case enums.publicKey.rsaEncryptSign: case enums.publicKey.rsaSign: { - return publicKey.rsa.generate(bits, "10001").then(function(keyObject) { + return publicKey.rsa.generate(bits, 65537).then(function(keyObject) { return constructParams( types, [keyObject.n, keyObject.e, keyObject.d, keyObject.p, keyObject.q, keyObject.u] ); diff --git a/src/crypto/pkcs1.js b/src/crypto/pkcs1.js index a7f50a39..03dc400a 100644 --- a/src/crypto/pkcs1.js +++ b/src/crypto/pkcs1.js @@ -22,13 +22,11 @@ * @see PublicKeyEncryptedSessionKeyPacket * @requires crypto/random * @requires crypto/hash - * @requires util * @module crypto/pkcs1 */ import random from './random'; import hash from './hash'; -import util from '../util'; /** @namespace */ const eme = {}; @@ -56,17 +54,18 @@ hash_headers[11] = [0x30, 0x2d, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, /** * Create padding with secure random data * @private - * @param {Integer} length Length of the padding in bytes - * @returns {String} Padding as string + * @param {Integer} length Length of the padding in bytes + * @returns {Uint8Array} Random padding * @async */ async function getPkcs1Padding(length) { - let result = ''; - while (result.length < length) { - const randomBytes = await random.getRandomBytes(length - result.length); + const result = new Uint8Array(length); + let count = 0; + while (count < length) { + const randomBytes = await random.getRandomBytes(length - count); for (let i = 0; i < randomBytes.length; i++) { if (randomBytes[i] !== 0) { - result += String.fromCharCode(randomBytes[i]); + result[count++] = randomBytes[i]; } } } @@ -76,46 +75,46 @@ async function getPkcs1Padding(length) { /** * Create a EME-PKCS1-v1_5 padded message * @see {@link https://tools.ietf.org/html/rfc4880#section-13.1.1|RFC 4880 13.1.1} - * @param {String} M message to be encoded - * @param {Integer} k the length in octets of the key modulus - * @returns {Promise} EME-PKCS1 padded message + * @param {Uint8Array} message message to be encoded + * @param {Integer} keyLength the length in octets of the key modulus + * @returns {Promise} EME-PKCS1 padded message * @async */ -eme.encode = async function(M, k) { - const mLen = M.length; +eme.encode = async function(message, keyLength) { + const mLength = message.length; // length checking - if (mLen > k - 11) { + if (mLength > keyLength - 11) { throw new Error('Message too long'); } // Generate an octet string PS of length k - mLen - 3 consisting of // pseudo-randomly generated nonzero octets - const PS = await getPkcs1Padding(k - mLen - 3); + const PS = await getPkcs1Padding(keyLength - mLength - 3); // Concatenate PS, the message M, and other padding to form an // encoded message EM of length k octets as EM = 0x00 || 0x02 || PS || 0x00 || M. - return String.fromCharCode(0) + - String.fromCharCode(2) + - PS + - String.fromCharCode(0) + - M; + const encoded = new Uint8Array(keyLength); + // 0x00 byte + encoded[1] = 2; + encoded.set(PS, 2); + // 0x00 bytes + encoded.set(message, keyLength - mLength); + return encoded; }; /** * Decode a EME-PKCS1-v1_5 padded message * @see {@link https://tools.ietf.org/html/rfc4880#section-13.1.2|RFC 4880 13.1.2} - * @param {String} EM encoded message, an octet string - * @returns {String} message, an octet string + * @param {Uint8Array} encoded encoded message bytes + * @returns {Uint8Array} message */ -eme.decode = function(EM) { - const firstOct = EM.charCodeAt(0); - const secondOct = EM.charCodeAt(1); +eme.decode = function(encoded) { let i = 2; - while (EM.charCodeAt(i) !== 0 && i < EM.length) { + while (encoded[i] !== 0 && i < encoded.length) { i++; } const psLen = i - 2; - const separator = EM.charCodeAt(i++); - if (firstOct === 0 && secondOct === 2 && psLen >= 8 && separator === 0) { - return EM.substr(i); + const separator = encoded[i++]; + if (encoded[0] === 0 && encoded[1] === 2 && psLen >= 8 && separator === 0) { + return encoded.subarray(i); } throw new Error('Decryption error'); }; @@ -126,41 +125,36 @@ eme.decode = function(EM) { * @param {Integer} algo Hash algorithm type used * @param {Uint8Array} hashed message to be encoded * @param {Integer} emLen intended length in octets of the encoded message - * @returns {String} encoded message + * @returns {Uint8Array} encoded message */ emsa.encode = async function(algo, hashed, emLen) { let i; - const H = util.uint8ArrayToStr(hashed); - if (H.length !== hash.getHashByteLength(algo)) { + if (hashed.length !== hash.getHashByteLength(algo)) { throw new Error('Invalid hash length'); } // produce an ASN.1 DER value for the hash function used. // Let T be the full hash prefix - let T = ''; + const hashPrefix = new Uint8Array(hash_headers[algo].length); for (i = 0; i < hash_headers[algo].length; i++) { - T += String.fromCharCode(hash_headers[algo][i]); + hashPrefix[i] = hash_headers[algo][i]; } - // add hash value to prefix - T += H; - // and let tLen be the length in octets of T - const tLen = T.length; + // and let tLen be the length in octets prefix and hashed data + const tLen = hashPrefix.length + hashed.length; if (emLen < tLen + 11) { throw new Error('Intended encoded message length too short'); } // an octet string PS consisting of emLen - tLen - 3 octets with hexadecimal value 0xFF // The length of PS will be at least 8 octets - let PS = ''; - for (i = 0; i < (emLen - tLen - 3); i++) { - PS += String.fromCharCode(0xff); - } - // Concatenate PS, the hash prefix T, and other padding to form the - // encoded message EM as EM = 0x00 || 0x01 || PS || 0x00 || T. - const EM = String.fromCharCode(0x00) + - String.fromCharCode(0x01) + - PS + - String.fromCharCode(0x00) + - T; - return util.strToHex(EM); + const PS = new Uint8Array(emLen - tLen - 3).fill(0xff); + + // Concatenate PS, the hash prefix, hashed data, and other padding to form the + // encoded message EM as EM = 0x00 || 0x01 || PS || 0x00 || prefix || hashed + const EM = new Uint8Array(emLen); + EM[1] = 0x01; + EM.set(PS, 2); + EM.set(hashPrefix, emLen - tLen); + EM.set(hashed, emLen - hashed.length); + return EM; }; export default { eme, emsa }; diff --git a/src/crypto/pkcs5.js b/src/crypto/pkcs5.js index 4f826b74..bfa5c0df 100644 --- a/src/crypto/pkcs5.js +++ b/src/crypto/pkcs5.js @@ -15,6 +15,8 @@ // License along with this library; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +import util from '../util'; + /** * @fileoverview Functions to add and remove PKCS5 padding * @see PublicKeyEncryptedSessionKeyPacket @@ -22,30 +24,31 @@ */ /** - * Add pkcs5 padding to a text. - * @param {String} msg Text to add padding - * @returns {String} Text with padding added + * Add pkcs5 padding to a message + * @param {Uint8Array} message message to pad + * @returns {Uint8Array} padded message */ -function encode(msg) { - const c = 8 - (msg.length % 8); - const padding = String.fromCharCode(c).repeat(c); - return msg + padding; +function encode(message) { + const c = 8 - (message.length % 8); + const padded = new Uint8Array(message.length + c).fill(c); + padded.set(message); + return padded; } /** - * Remove pkcs5 padding from a string. - * @param {String} msg Text to remove padding from - * @returns {String} Text with padding removed + * Remove pkcs5 padding from a message + * @param {Uint8Array} message message to remove padding from + * @returns {Uint8Array} message without padding */ -function decode(msg) { - const len = msg.length; +function decode(message) { + const len = message.length; if (len > 0) { - const c = msg.charCodeAt(len - 1); + const c = message[len - 1]; if (c >= 1) { - const provided = msg.substr(len - c); - const computed = String.fromCharCode(c).repeat(c); - if (provided === computed) { - return msg.substr(0, len - c); + const provided = message.subarray(len - c); + const computed = new Uint8Array(c).fill(c); + if (util.equalsUint8Array(provided, computed)) { + return message.subarray(0, len - c); } } } diff --git a/src/crypto/public_key/dsa.js b/src/crypto/public_key/dsa.js index 49fb19c0..1a712a82 100644 --- a/src/crypto/public_key/dsa.js +++ b/src/crypto/public_key/dsa.js @@ -17,20 +17,14 @@ /** * @fileoverview A Digital signature algorithm implementation - * @requires bn.js * @requires crypto/random * @requires util * @module crypto/public_key/dsa */ - -import BN from 'bn.js'; import random from '../random'; import util from '../../util'; import prime from './prime'; -const one = new BN(1); -const zero = new BN(0); - /* TODO regarding the hash function, read: https://tools.ietf.org/html/rfc4880#section-13.6 @@ -42,28 +36,28 @@ export default { * DSA Sign function * @param {Integer} hash_algo * @param {Uint8Array} hashed - * @param {BN} g - * @param {BN} p - * @param {BN} q - * @param {BN} x - * @returns {{ r: BN, s: BN }} + * @param {BigInteger} g + * @param {BigInteger} p + * @param {BigInteger} q + * @param {BigInteger} x + * @returns {{ r: BigInteger, s: BigInteger }} * @async */ sign: async function(hash_algo, hashed, g, p, q, x) { + const BigInteger = await util.getBigInteger(); + const one = new BigInteger(1); let k; let r; let s; let t; - const redp = new BN.red(p); - const redq = new BN.red(q); - const gred = g.toRed(redp); - const xred = x.toRed(redq); + g = g.mod(p); + x = x.mod(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 BN(hashed.subarray(0, q.byteLength())).toRed(redq); + 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 @@ -71,57 +65,62 @@ export default { // 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 random.getRandomBN(one, q); // returns in [1, q-1] - r = gred.redPow(k).fromRed().toRed(redq); // (g**k mod p) mod q - if (zero.cmp(r) === 0) { + k = await random.getRandomBigInteger(one, q); // returns in [1, q-1] + r = g.modExp(k, p).imod(q); // (g**k mod p) mod q + if (r.isZero()) { continue; } - t = h.redAdd(xred.redMul(r)); // H(m) + x*r mod q - s = k.toRed(redq).redInvm().redMul(t); // k**-1 * (H(m) + x*r) mod q - if (zero.cmp(s) === 0) { + 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()) { continue; } break; } return { - r: r.toArrayLike(Uint8Array, 'be', q.byteLength()), - s: s.toArrayLike(Uint8Array, 'be', q.byteLength()) + r: r.toUint8Array('be', q.byteLength()), + s: s.toUint8Array('be', q.byteLength()) }; }, /** * DSA Verify function * @param {Integer} hash_algo - * @param {BN} r - * @param {BN} s + * @param {BigInteger} r + * @param {BigInteger} s * @param {Uint8Array} hashed - * @param {BN} g - * @param {BN} p - * @param {BN} q - * @param {BN} y + * @param {BigInteger} g + * @param {BigInteger} p + * @param {BigInteger} q + * @param {BigInteger} y * @returns {boolean} * @async */ verify: async function(hash_algo, r, s, hashed, g, p, q, y) { - if (zero.ucmp(r) >= 0 || r.ucmp(q) >= 0 || - zero.ucmp(s) >= 0 || s.ucmp(q) >= 0) { + const BigInteger = await util.getBigInteger(); + const zero = new BigInteger(0); + + if (r.lte(zero) || r.gte(q) || + s.lte(zero) || s.gte(q)) { util.printDebug("invalid DSA Signature"); return null; } - const redp = new BN.red(p); - const redq = new BN.red(q); - const h = new BN(hashed.subarray(0, q.byteLength())); - const w = s.toRed(redq).redInvm(); // s**-1 mod q - if (zero.cmp(w) === 0) { + 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"); return null; } - const u1 = h.toRed(redq).redMul(w); // H(m) * w mod q - const u2 = r.toRed(redq).redMul(w); // r * w mod q - const t1 = g.toRed(redp).redPow(u1.fromRed()); // g**u1 mod p - const t2 = y.toRed(redp).redPow(u2.fromRed()); // y**u2 mod p - const v = t1.redMul(t2).fromRed().mod(q); // (g**u1 * y**u2 mod p) mod q - return v.cmp(r) === 0; + + 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); }, /** @@ -135,11 +134,12 @@ export default { * @async */ validateParams: async function (p, q, g, y, x) { - p = new BN(p); - q = new BN(q); - g = new BN(g); - y = new BN(y); - const one = new BN(1); + const BigInteger = await util.getBigInteger(); + 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; @@ -148,25 +148,24 @@ export default { /** * Check that subgroup order q divides p-1 */ - if (!p.sub(one).mod(q).isZero()) { + if (!p.dec().mod(q).isZero()) { return false; } - const pred = new BN.red(p); - const gModP = g.toRed(pred); /** * g has order q * Check that g ** q = 1 mod p */ - if (!gModP.redPow(q).eq(one)) { + if (!g.modExp(q, p).isOne()) { return false; } /** * Check q is large and probably prime (we mainly want to avoid small factors) */ - const qSize = q.bitLength(); - if (qSize < 150 || !(await prime.isProbablePrime(q, null, 32))) { + const qSize = new BigInteger(q.bitLength()); + const n150 = new BigInteger(150); + if (qSize.lt(n150) || !(await prime.isProbablePrime(q, null, 32))) { return false; } @@ -176,10 +175,11 @@ export default { * * Blinded exponentiation computes g**{rq + x} to compare to y */ - x = new BN(x); - const r = await random.getRandomBN(new BN(2).shln(qSize - 1), new BN(2).shln(qSize)); // draw r of same size as q + x = new BigInteger(x); + const two = new BigInteger(2); + const r = await random.getRandomBigInteger(two.leftShift(qSize.dec()), two.leftShift(qSize)); // draw r of same size as q const rqx = q.mul(r).add(x); - if (!y.eq(gModP.redPow(rqx))) { + if (!y.equal(g.modExp(rqx, p))) { return false; } diff --git a/src/crypto/public_key/elgamal.js b/src/crypto/public_key/elgamal.js index 0e10f032..30c7cc0a 100644 --- a/src/crypto/public_key/elgamal.js +++ b/src/crypto/public_key/elgamal.js @@ -17,52 +17,45 @@ /** * @fileoverview ElGamal implementation - * @requires bn.js * @requires crypto/random + * @requires util * @module crypto/public_key/elgamal */ -import BN from 'bn.js'; +import util from '../../util'; import random from '../random'; export default { /** * ElGamal Encryption function - * @param {BN} m - * @param {BN} p - * @param {BN} g - * @param {BN} y - * @returns {{ c1: BN, c2: BN }} + * @param {BigInteger} m + * @param {BigInteger} p + * @param {BigInteger} g + * @param {BigInteger} y + * @returns {{ c1: BigInteger, c2: BigInteger }} * @async */ encrypt: async function(m, p, g, y) { - const redp = new BN.red(p); - const mred = m.toRed(redp); - const gred = g.toRed(redp); - const yred = y.toRed(redp); - // 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 random.getRandomBN(new BN(1), p.subn(1)); + const BigInteger = await util.getBigInteger(); + // See Section 11.5 here: https://crypto.stanford.edu/~dabo/cryptobook/BonehShoup_0_4.pdf + const k = await random.getRandomBigInteger(new BigInteger(0), p); // returns in [0, p-1] return { - c1: gred.redPow(k).fromRed(), - c2: yred.redPow(k).redMul(mred).fromRed() + c1: g.modExp(k, p), + c2: y.modExp(k, p).imul(m).imod(p) }; }, /** * ElGamal Encryption function - * @param {BN} c1 - * @param {BN} c2 - * @param {BN} p - * @param {BN} x - * @returns BN + * @param {BigInteger} c1 + * @param {BigInteger} c2 + * @param {BigInteger} p + * @param {BigInteger} x + * @returns BigInteger * @async */ decrypt: async function(c1, c2, p, x) { - const redp = new BN.red(p); - const c1red = c1.toRed(redp); - const c2red = c2.toRed(redp); - return c1red.redPow(x).redInvm().redMul(c2red).fromRed(); + return c1.modExp(x, p).modInv(p).imul(c2).imod(p); }, /** @@ -75,29 +68,29 @@ export default { * @async */ validateParams: async function (p, g, y, x) { - p = new BN(p); - g = new BN(g); - y = new BN(y); + const BigInteger = await util.getBigInteger(); + p = new BigInteger(p); + g = new BigInteger(g); + y = new BigInteger(y); - const one = new BN(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 = p.subn(1).bitLength(); - if (pSize < 1023) { + const pSize = new BigInteger(p.bitLength()); + const n1023 = new BigInteger(1023); + if (pSize.lt(n1023)) { return false; } - const pred = new BN.red(p); - const gModP = g.toRed(pred); /** * g should have order p-1 * Check that g ** (p-1) = 1 mod p */ - if (!gModP.redPow(p.subn(1)).eq(one)) { + if (!g.modExp(p.dec(), p).isOne()) { return false; } @@ -108,14 +101,14 @@ export default { * We just check g**i != 1 for all i up to a threshold */ let res = g; - const i = new BN(1); - const threshold = new BN(2).shln(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).mod(p); - if (res.eqn(1)) { + res = res.mul(g).imod(p); + if (res.isOne()) { return false; } - i.iaddn(1); + i.iinc(); } /** @@ -124,10 +117,11 @@ export default { * * Blinded exponentiation computes g**{r(p-1) + x} to compare to y */ - x = new BN(x); - const r = await random.getRandomBN(new BN(2).shln(pSize - 1), new BN(2).shln(pSize)); // draw r of same size as p-1 - const rqx = p.subn(1).mul(r).add(x); - if (!y.eq(gModP.redPow(rqx))) { + x = new BigInteger(x); + const two = new BigInteger(2); + const r = await random.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))) { return false; } diff --git a/src/crypto/public_key/elliptic/curves.js b/src/crypto/public_key/elliptic/curves.js index 1f92dbc9..40fc2cc4 100644 --- a/src/crypto/public_key/elliptic/curves.js +++ b/src/crypto/public_key/elliptic/curves.js @@ -17,7 +17,6 @@ /** * @fileoverview Wrapper of an instance of an Elliptic Curve - * @requires bn.js * @requires tweetnacl * @requires crypto/public_key/elliptic/key * @requires crypto/random @@ -28,7 +27,6 @@ * @module crypto/public_key/elliptic/curve */ -import BN from 'bn.js'; import nacl from 'tweetnacl/nacl-fast-light.js'; import random from '../../random'; import enums from '../../../enums'; @@ -212,12 +210,14 @@ class Curve { } async function generate(curve) { + const BigInteger = await util.getBigInteger(); + curve = new Curve(curve); const keyPair = await curve.genKeyPair(); return { oid: curve.oid, - Q: new BN(keyPair.publicKey), - d: new BN(keyPair.privateKey), + Q: new BigInteger(keyPair.publicKey), + d: new BigInteger(keyPair.privateKey), hash: curve.hash, cipher: curve.cipher }; @@ -281,7 +281,6 @@ async function validateStandardParams(algo, oid, Q, d) { * Re-derive public point Q' = dG from private key * Expect Q == Q' */ - d = new BN(d); const dG = keyFromPrivate(curve, d).getPublic(); if (!dG.eq(Q)) { return false; diff --git a/src/crypto/public_key/elliptic/ecdh.js b/src/crypto/public_key/elliptic/ecdh.js index 73703795..990d318d 100644 --- a/src/crypto/public_key/elliptic/ecdh.js +++ b/src/crypto/public_key/elliptic/ecdh.js @@ -17,7 +17,6 @@ /** * @fileoverview Key encryption and decryption for RFC 6637 ECDH - * @requires bn.js * @requires tweetnacl * @requires crypto/public_key/elliptic/curve * @requires crypto/aes_kw @@ -30,7 +29,6 @@ * @module crypto/public_key/elliptic/ecdh */ -import BN from 'bn.js'; import nacl from 'tweetnacl/nacl-fast-light.js'; import Curve, { jwkToRawPublic, rawPublicToJwk, privateToJwk, validateStandardParams } from './curves'; import aes_kw from '../../aes_kw'; @@ -216,10 +214,12 @@ async function genPrivateEphemeralKey(curve, V, Q, d) { * @param {Uint8Array} Q Recipient public key * @param {Uint8Array} d Recipient private key * @param {Uint8Array} fingerprint Recipient fingerprint - * @returns {Promise} Value derived from session key + * @returns {Promise} Value derived from session key * @async */ async function decrypt(oid, kdfParams, V, C, Q, d, fingerprint) { + const BigInteger = await util.getBigInteger(); + const curve = new Curve(oid); const { sharedKey } = await genPrivateEphemeralKey(curve, V, Q, d); const param = buildEcdhParam(enums.publicKey.ecdh, oid, kdfParams, fingerprint); @@ -229,7 +229,7 @@ async function decrypt(oid, kdfParams, V, C, Q, d, fingerprint) { try { // Work around old go crypto bug and old OpenPGP.js bug, respectively. const Z = await kdf(kdfParams.hash, sharedKey, cipher[cipher_algo].keySize, param, i === 1, i === 2); - return new BN(aes_kw.unwrap(Z, C)); + return new BigInteger(aes_kw.unwrap(Z, C)); } catch (e) { err = e; } diff --git a/src/crypto/public_key/elliptic/ecdsa.js b/src/crypto/public_key/elliptic/ecdsa.js index eac5699c..845450d9 100644 --- a/src/crypto/public_key/elliptic/ecdsa.js +++ b/src/crypto/public_key/elliptic/ecdsa.js @@ -17,7 +17,6 @@ /** * @fileoverview Implementation of ECDSA following RFC6637 for Openpgpjs - * @requires bn.js * @requires web-stream-tools * @requires enums * @requires util @@ -25,7 +24,6 @@ * @module crypto/public_key/elliptic/ecdsa */ -import BN from 'bn.js'; import enums from '../../../enums'; import util from '../../../util'; import random from '../../random'; @@ -283,6 +281,8 @@ async function nodeSign(curve, hash_algo, message, keyPair) { } async function nodeVerify(curve, hash_algo, { r, s }, message, publicKey) { + const { default: BN } = await import('bn.js'); + const verify = nodeCrypto.createVerify(enums.read(enums.hash, hash_algo)); verify.write(message); verify.end(); diff --git a/src/crypto/public_key/prime.js b/src/crypto/public_key/prime.js index 1e657236..595e0793 100644 --- a/src/crypto/public_key/prime.js +++ b/src/crypto/public_key/prime.js @@ -17,12 +17,11 @@ /** * @fileoverview Algorithms for probabilistic random prime generation - * @requires bn.js * @requires crypto/random * @module crypto/public_key/prime */ -import BN from 'bn.js'; +import util from '../../util'; import random from '../random'; export default { @@ -32,14 +31,16 @@ export default { /** * Probabilistic random number generator * @param {Integer} bits Bit length of the prime - * @param {BN} e Optional RSA exponent to check against 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 BN + * @returns BigInteger * @async */ async function randomProbablePrime(bits, e, k) { - const min = new BN(1).shln(bits - 1); - const thirty = new BN(30); + const BigInteger = await util.getBigInteger(); + 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,15 +49,15 @@ 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]; - let n = await random.getRandomBN(min, min.shln(1)); + const n = await random.getRandomBigInteger(min, min.leftShift(one)); let i = n.mod(thirty).toNumber(); do { - n.iaddn(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) { - n = n.mod(min.shln(1)).iadd(min); + n.imod(min.leftShift(one)).iadd(min); i = n.mod(thirty).toNumber(); } } while (!await isProbablePrime(n, e, k)); @@ -65,20 +66,20 @@ async function randomProbablePrime(bits, e, k) { /** * Probabilistic primality testing - * @param {BN} n Number to test - * @param {BN} e Optional RSA exponent to check against the prime - * @param {Integer} k Optional number of iterations of Miller-Rabin test + * @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 */ async function isProbablePrime(n, e, k) { - if (e && !n.subn(1).gcd(e).eqn(1)) { + if (e && !n.dec().gcd(e).isOne()) { return false; } - if (!divisionTest(n)) { + if (!await divisionTest(n)) { return false; } - if (!fermat(n)) { + if (!await fermat(n)) { return false; } if (!await millerRabin(n, k)) { @@ -91,24 +92,26 @@ 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 {BN} n Number to test - * @param {Integer} b Optional Fermat test base + * Fails if b^(n-1) mod n != 1. + * @param {BigInteger} n Number to test + * @param {BigInteger} b Optional Fermat test base * @returns {boolean} */ -function fermat(n, b) { - b = b || new BN(2); - return b.toRed(BN.mont(n)).redPow(n.subn(1)).fromRed().cmpn(1) === 0; +async function fermat(n, b) { + const BigInteger = await util.getBigInteger(); + b = b || new BigInteger(2); + return b.modExp(n.dec(), n).isOne(); } -function divisionTest(n) { - return small_primes.every(m => { - return n.modn(m) !== 0; +async function divisionTest(n) { + const BigInteger = await util.getBigInteger(); + return smallPrimes.every(m => { + return n.mod(new BigInteger(m)) !== 0; }); } // https://github.com/gpg/libgcrypt/blob/master/cipher/primegen.c -const small_primes = [ +const smallPrimes = [ 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, @@ -223,45 +226,43 @@ const small_primes = [ /** * Tests whether n is probably prime or not using the Miller-Rabin test. * See HAC Remark 4.28. - * @param {BN} 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 {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 * @returns {boolean} * @async */ async function millerRabin(n, k, rand) { + const BigInteger = await util.getBigInteger(); const len = n.bitLength(); - const red = BN.mont(n); - const rone = new BN(1).toRed(red); if (!k) { k = Math.max(1, (len / 48) | 0); } - const n1 = n.subn(1); - const rn1 = n1.toRed(red); + const n1 = n.dec(); // n - 1 // Find d and s, (n - 1) = (2 ^ s) * d; let s = 0; - while (!n1.testn(s)) { s++; } - const d = n.shrn(s); + while (!n1.getBit(s)) { s++; } + const d = n.rightShift(new BigInteger(s)); for (; k > 0; k--) { - const a = rand ? rand() : await random.getRandomBN(new BN(2), n1); + const a = rand ? rand() : await random.getRandomBigInteger(new BigInteger(2), n1); - let x = a.toRed(red).redPow(d); - if (x.eq(rone) || x.eq(rn1)) { + let x = a.modExp(d, n); + if (x.isOne() || x.equal(n1)) { continue; } let i; for (i = 1; i < s; i++) { - x = x.redSqr(); + x = x.mul(x).mod(n); - if (x.eq(rone)) { + if (x.isOne()) { return false; } - if (x.eq(rn1)) { + if (x.equal(n1)) { break; } } diff --git a/src/crypto/public_key/rsa.js b/src/crypto/public_key/rsa.js index 26e49d70..b3b10790 100644 --- a/src/crypto/public_key/rsa.js +++ b/src/crypto/public_key/rsa.js @@ -17,7 +17,6 @@ /** * @fileoverview RSA implementation - * @requires bn.js * @requires crypto/public_key/prime * @requires crypto/random * @requires config @@ -25,14 +24,12 @@ * @module crypto/public_key/rsa */ -import BN from 'bn.js'; import prime from './prime'; import random from '../random'; import config from '../../config'; import util from '../../util'; import pkcs1 from '../pkcs1'; import enums from '../../enums'; -import type_mpi from '../../type/mpi'; const webCrypto = util.getWebCrypto(); const nodeCrypto = util.getNodeCrypto(); @@ -171,16 +168,18 @@ export default { * When possible, webCrypto or nodeCrypto is used. Otherwise, primes are generated using * 40 rounds of the Miller-Rabin probabilistic random prime generation algorithm. * @see module:crypto/public_key/prime - * @param {Integer} B RSA bit length - * @param {String} E RSA public exponent in hex string - * @returns {{n: BN, e: BN, d: BN, - * p: BN, q: BN, u: BN}} RSA public modulus, RSA public exponent, RSA private exponent, - * RSA private prime p, RSA private prime q, u = q ** -1 mod p + * @param {Integer} bits RSA bit length + * @param {Integer} e RSA public exponent + * @returns {{n, e, d, + * p, q ,u: BigInteger}} RSA public modulus, RSA public exponent, RSA private exponent, + * RSA private prime p, RSA private prime q, u = q ** 1 mod p * @async */ - generate: async function(B, E) { + generate: async function(bits, e) { + const BigInteger = await util.getBigInteger(); + let key; - E = new BN(E, 16); + e = new BigInteger(e); // Native RSA keygen using Web Crypto if (util.getWebCrypto()) { @@ -190,8 +189,8 @@ export default { // current standard spec keyGenOpt = { name: 'RSASSA-PKCS1-v1_5', - modulusLength: B, // the specified keysize in bits - publicExponent: E.toArrayLike(Uint8Array), // take three bytes (max 65537) for exponent + modulusLength: bits, // the specified keysize in bits + publicExponent: e.toUint8Array(), // take three bytes (max 65537) for exponent hash: { name: 'SHA-1' // not required for actual RSA keys, but for crypto api 'sign' and 'verify' } @@ -202,8 +201,8 @@ export default { // outdated spec implemented by old Webkit keyGenOpt = { name: 'RSA-OAEP', - modulusLength: B, // the specified keysize in bits - publicExponent: E.toArrayLike(Uint8Array), // take three bytes (max 65537) for exponent + modulusLength: bits, // the specified keysize in bits + publicExponent: e.toUint8Array(), // take three bytes (max 65537) for exponent hash: { name: 'SHA-1' // not required for actual RSA keys, but for crypto api 'sign' and 'verify' } @@ -224,19 +223,19 @@ export default { } // map JWK parameters to BN key = {}; - key.n = new BN(util.b64ToUint8Array(jwk.n)); - key.e = E; - key.d = new BN(util.b64ToUint8Array(jwk.d)); + key.n = new BigInteger(util.b64ToUint8Array(jwk.n)); + key.e = e; + key.d = new BigInteger(util.b64ToUint8Array(jwk.d)); // switch p and q - key.p = new BN(util.b64ToUint8Array(jwk.q)); - key.q = new BN(util.b64ToUint8Array(jwk.p)); + key.p = new BigInteger(util.b64ToUint8Array(jwk.q)); + key.q = new BigInteger(util.b64ToUint8Array(jwk.p)); // Since p and q are switched in places, we could keep u - key.u = new BN(util.b64ToUint8Array(jwk.qi)); + key.u = new BigInteger(util.b64ToUint8Array(jwk.qi)); return key; } else if (util.getNodeCrypto() && nodeCrypto.generateKeyPair && RSAPrivateKey) { const opts = { - modulusLength: Number(B.toString(10)), - publicExponent: Number(E.toString(10)), + modulusLength: bits, + publicExponent: e.toNumber(), publicKeyEncoding: { type: 'pkcs1', format: 'der' }, privateKeyEncoding: { type: 'pkcs1', format: 'der' } }; @@ -266,23 +265,23 @@ export default { // RSA keygen fallback using 40 iterations of the Miller-Rabin test // See https://stackoverflow.com/a/6330138 for justification // Also see section C.3 here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST - let q = await prime.randomProbablePrime(B - (B >> 1), E, 40); - let p = await prime.randomProbablePrime(B >> 1, E, 40); + let q = await prime.randomProbablePrime(bits - (bits >> 1), e, 40); + let p = await prime.randomProbablePrime(bits >> 1, e, 40); - if (q.cmp(p) < 0) { + if (q.lt(p)) { [p, q] = [q, p]; } - const phi = p.subn(1).mul(q.subn(1)); + const phi = p.dec().imul(q.dec()); return { n: p.mul(q), - e: E, - d: E.invm(phi), + e, + d: e.modInv(phi), p: p, q: q, // dp: d.mod(p.subn(1)), // dq: d.mod(q.subn(1)), - u: p.invm(q) + u: p.modInv(q) }; }, @@ -298,25 +297,25 @@ export default { * @async */ validateParams: async function (n, e, d, p, q, u) { - n = new BN(n); - p = new BN(p); - q = new BN(q); + const BigInteger = await util.getBigInteger(); + n = new BigInteger(n); + p = new BigInteger(p); + q = new BigInteger(q); // expect pq = n - if (!p.mul(q).eq(n)) { + if (!p.mul(q).equal(n)) { return false; } - const one = new BN(1); - const two = new BN(2); + const two = new BigInteger(2); // expect p*u = 1 mod q - u = new BN(u); - if (!p.mul(u).umod(q).eq(one)) { + u = new BigInteger(u); + if (!p.mul(u).mod(q).isOne()) { return false; } - e = new BN(e); - d = new BN(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)] @@ -324,10 +323,11 @@ export default { * * We blind the multiplication with r, and check that rde = r mod lcm(p-1, q-1) */ - const r = await random.getRandomBN(two, two.shln(n.bitLength() / 3)); // r in [ 2, 2^{|n|/3} ) < p and q + const nSizeOver3 = new BigInteger(Math.floor(n.bitLength() / 3)); + const r = await random.getRandomBigInteger(two, two.leftShift(nSizeOver3)); // r in [ 2, 2^{|n|/3} ) < p and q const rde = r.mul(d).mul(e); - const areInverses = rde.umod(p.sub(one)).eq(r) && rde.umod(q.sub(one)).eq(r); + const areInverses = rde.mod(p.dec()).equal(r) && rde.mod(q.dec()).equal(r); if (!areInverses) { return false; } @@ -336,14 +336,14 @@ export default { }, bnSign: async function (hash_algo, n, d, hashed) { - n = new BN(n); - const m = new BN(await pkcs1.emsa.encode(hash_algo, hashed, n.byteLength()), 16); - d = new BN(d); - if (n.cmp(m) <= 0) { + const BigInteger = await util.getBigInteger(); + n = new BigInteger(n); + const m = new BigInteger(await pkcs1.emsa.encode(hash_algo, hashed, n.byteLength())); + d = new BigInteger(d); + if (m.gte(n)) { throw new Error('Message size cannot exceed modulus size'); } - const nred = new BN.red(n); - return m.toRed(nred).redPow(d).toArrayLike(Uint8Array, 'be', n.byteLength()); + return m.modExp(d, n).toUint8Array('be', n.byteLength()); }, webSign: async function (hash_name, data, n, e, d, p, q, u) { @@ -353,7 +353,7 @@ export default { * OpenPGP.js used to generate RSA keys the wrong way around (p > q), and still * does if the underlying Web Crypto does so (e.g. old MS Edge 50% of the time). */ - const jwk = privateToJwk(n, e, d, p, q, u); + const jwk = await privateToJwk(n, e, d, p, q, u); const algo = { name: "RSASSA-PKCS1-v1_5", hash: { name: hash_name } @@ -364,6 +364,7 @@ export default { }, nodeSign: async function (hash_algo, data, n, e, d, p, q, u) { + const { default: BN } = await import('bn.js'); const pBNum = new BN(p); const qBNum = new BN(q); const dBNum = new BN(d); @@ -396,16 +397,16 @@ export default { }, bnVerify: async function (hash_algo, s, n, e, hashed) { - n = new BN(n); - s = new BN(s); - e = new BN(e); - if (n.cmp(s) <= 0) { + const BigInteger = await util.getBigInteger(); + 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'); } - const nred = new BN.red(n); - const EM1 = s.toRed(nred).redPow(e).toArrayLike(Uint8Array, 'be', n.byteLength()); + const EM1 = s.modExp(e, n).toUint8Array('be', n.byteLength()); const EM2 = await pkcs1.emsa.encode(hash_algo, hashed, n.byteLength()); - return util.uint8ArrayToHex(EM1) === EM2; + return util.equalsUint8Array(EM1, EM2); }, webVerify: async function (hash_name, data, s, n, e) { @@ -419,6 +420,8 @@ export default { }, nodeVerify: async function (hash_algo, data, s, n, e) { + const { default: BN } = await import('bn.js'); + const verify = nodeCrypto.createVerify(enums.read(enums.hash, hash_algo)); verify.write(data); verify.end(); @@ -443,6 +446,8 @@ export default { }, nodeEncrypt: async function (data, n, e) { + const { default: BN } = await import('bn.js'); + const keyObject = { modulus: new BN(n), publicExponent: new BN(e) @@ -461,18 +466,19 @@ export default { }, bnEncrypt: async function (data, n, e) { - n = new BN(n); - data = new type_mpi(await pkcs1.eme.encode(util.uint8ArrayToStr(data), n.byteLength())); - data = data.toBN(); - e = new BN(e); - if (n.cmp(data) <= 0) { + const BigInteger = await util.getBigInteger(); + n = new BigInteger(n); + data = new BigInteger(await pkcs1.eme.encode(data, n.byteLength())); + e = new BigInteger(e); + if (data.gte(n)) { throw new Error('Message size cannot exceed modulus size'); } - const nred = new BN.red(n); - return data.toRed(nred).redPow(e).toArrayLike(Uint8Array, 'be', n.byteLength()); + return data.modExp(e, n).toUint8Array('be', n.byteLength()); }, - nodeDecrypt: function (data, n, e, d, p, q, u) { + nodeDecrypt: async function (data, n, e, d, p, q, u) { + const { default: BN } = await import('bn.js'); + const pBNum = new BN(p); const qBNum = new BN(q); const dBNum = new BN(d); @@ -502,50 +508,46 @@ export default { key = { key: pem, padding: nodeCrypto.constants.RSA_PKCS1_PADDING }; } try { - return util.uint8ArrayToStr(nodeCrypto.privateDecrypt(key, data)); + return new Uint8Array(nodeCrypto.privateDecrypt(key, data)); } catch (err) { throw new Error('Decryption error'); } }, bnDecrypt: async function(data, n, e, d, p, q, u) { - data = new BN(data); - n = new BN(n); - e = new BN(e); - d = new BN(d); - p = new BN(p); - q = new BN(q); - u = new BN(u); - if (n.cmp(data) <= 0) { + const BigInteger = await util.getBigInteger(); + 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.subn(1)); // d mod (q-1) - const dp = d.mod(p.subn(1)); // d mod (p-1) - const pred = new BN.red(p); - const qred = new BN.red(q); - const nred = new BN.red(n); + const dq = d.mod(q.dec()); // d mod (q-1) + const dp = d.mod(p.dec()); // d mod (p-1) let blinder; let unblinder; if (config.rsaBlinding) { - unblinder = (await random.getRandomBN(new BN(2), n)).toRed(nred); - blinder = unblinder.redInvm().redPow(e); - data = data.toRed(nred).redMul(blinder).fromRed(); + unblinder = (await random.getRandomBigInteger(new BigInteger(2), n)).mod(n); + blinder = unblinder.modInv(n).modExp(e, n); + data = data.mul(blinder).mod(n); } - const mp = data.toRed(pred).redPow(dp); - const mq = data.toRed(qred).redPow(dq); - const t = mq.redSub(mp.fromRed().toRed(qred)); - const h = u.toRed(qred).redMul(t).fromRed(); + 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.mul(p).add(mp).toRed(nred); + let result = h.mul(p).add(mp); // result < n due to relations above if (config.rsaBlinding) { - result = result.redMul(unblinder); + result = result.mul(unblinder).mod(n); } - result = new type_mpi(result).toUint8Array('be', n.byteLength()); // preserve leading zeros - return pkcs1.eme.decode(util.Uint8Array_to_str(result)); + return pkcs1.eme.decode(result.toUint8Array('be', n.byteLength())); }, prime: prime @@ -561,15 +563,16 @@ export default { * @param {Uint8Array} q * @param {Uint8Array} u */ -function privateToJwk(n, e, d, p, q, u) { - const pBNum = new BN(p); - const qBNum = new BN(q); - const dBNum = new BN(d); +async function privateToJwk(n, e, d, p, q, u) { + const BigInteger = await util.getBigInteger(); + const pNum = new BigInteger(p); + const qNum = new BigInteger(q); + const dNum = new BigInteger(d); - let dq = dBNum.mod(qBNum.subn(1)); // d mod (q-1) - let dp = dBNum.mod(pBNum.subn(1)); // d mod (p-1) - dp = dp.toArrayLike(Uint8Array); - dq = dq.toArrayLike(Uint8Array); + 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(); return { kty: 'RSA', n: util.uint8ArrayToB64(n, true), diff --git a/src/crypto/random.js b/src/crypto/random.js index 0718f94c..ae34ca96 100644 --- a/src/crypto/random.js +++ b/src/crypto/random.js @@ -19,12 +19,9 @@ /** * @fileoverview Provides tools for retrieving secure randomness from browsers or Node.js - * @requires bn.js * @requires util * @module crypto/random */ - -import BN from 'bn.js'; import util from '../util'; // Do not use util.getNodeCrypto because we need this regardless of useNative setting @@ -122,14 +119,16 @@ export default { }, /** - * Create a secure random MPI that is greater than or equal to min and less than max. - * @param {module:type/mpi} min Lower bound, included - * @param {module:type/mpi} max Upper bound, excluded - * @returns {module:BN} Random MPI + * 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 {module:BigInteger} Random BigInteger * @async */ - getRandomBN: async function(min, max) { - if (max.cmp(min) <= 0) { + getRandomBigInteger: async function(min, max) { + const BigInteger = await util.getBigInteger(); + + if (max.lt(min)) { throw new Error('Illegal parameter value: max <= min'); } @@ -139,7 +138,7 @@ export default { // 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 BN(await this.getRandomBytes(bytes + 8)); + const r = new BigInteger(await this.getRandomBytes(bytes + 8)); return r.mod(modulus).add(min); }, diff --git a/src/crypto/signature.js b/src/crypto/signature.js index edd2271e..eb8f763c 100644 --- a/src/crypto/signature.js +++ b/src/crypto/signature.js @@ -42,12 +42,12 @@ export default { return publicKey.rsa.verify(hash_algo, data, m, n, e, hashed); } case enums.publicKey.dsa: { - const r = msg_MPIs[0].toBN(); - const s = msg_MPIs[1].toBN(); - const p = pub_MPIs[0].toBN(); - const q = pub_MPIs[1].toBN(); - const g = pub_MPIs[2].toBN(); - const y = pub_MPIs[3].toBN(); + const r = await msg_MPIs[0].toBigInteger(); + const s = await msg_MPIs[1].toBigInteger(); + const p = await pub_MPIs[0].toBigInteger(); + const q = await pub_MPIs[1].toBigInteger(); + const g = await pub_MPIs[2].toBigInteger(); + const y = await pub_MPIs[3].toBigInteger(); return publicKey.dsa.verify(hash_algo, r, s, hashed, g, p, q, y); } case enums.publicKey.ecdsa: { @@ -101,10 +101,10 @@ export default { return util.uint8ArrayToMpi(signature); } case enums.publicKey.dsa: { - const p = key_params[0].toBN(); - const q = key_params[1].toBN(); - const g = key_params[2].toBN(); - const x = key_params[4].toBN(); + const p = await key_params[0].toBigInteger(); + const q = await key_params[1].toBigInteger(); + const g = await key_params[2].toBigInteger(); + const x = await key_params[4].toBigInteger(); const signature = await publicKey.dsa.sign(hash_algo, hashed, g, p, q, x); return util.concatUint8Array([ util.uint8ArrayToMpi(signature.r), diff --git a/src/packet/public_key_encrypted_session_key.js b/src/packet/public_key_encrypted_session_key.js index d33ad53b..519816e6 100644 --- a/src/packet/public_key_encrypted_session_key.js +++ b/src/packet/public_key_encrypted_session_key.js @@ -103,10 +103,11 @@ class PublicKeyEncryptedSessionKeyPacket { * @async */ async encrypt(key) { - let data = String.fromCharCode(enums.write(enums.symmetric, this.sessionKeyAlgorithm)); - - data += util.uint8ArrayToStr(this.sessionKey); - data += util.uint8ArrayToStr(util.writeChecksum(this.sessionKey)); + const data = util.concatUint8Array([ + new Uint8Array([enums.write(enums.symmetric, this.sessionKeyAlgorithm)]), + this.sessionKey, + util.writeChecksum(this.sessionKey) + ]); const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm); this.encrypted = await crypto.publicKeyEncrypt( algo, key.params, data, key.getFingerprintBytes()); @@ -130,14 +131,13 @@ class PublicKeyEncryptedSessionKeyPacket { throw new Error('Decryption error'); } const decoded = await crypto.publicKeyDecrypt(algo, key.params, this.encrypted, key.getFingerprintBytes()); - const checksum = util.strToUint8Array(decoded.substr(decoded.length - 2)); - key = util.strToUint8Array(decoded.substring(1, decoded.length - 2)); - - if (!util.equalsUint8Array(checksum, util.writeChecksum(key))) { + const checksum = decoded.subarray(decoded.length - 2); + const sessionKey = decoded.subarray(1, decoded.length - 2); + if (!util.equalsUint8Array(checksum, util.writeChecksum(sessionKey))) { throw new Error('Decryption error'); } else { - this.sessionKey = key; - this.sessionKeyAlgorithm = enums.read(enums.symmetric, decoded.charCodeAt(0)); + this.sessionKey = sessionKey; + this.sessionKeyAlgorithm = enums.read(enums.symmetric, decoded[0]); } return true; } diff --git a/src/type/mpi.js b/src/type/mpi.js index d2590443..6b5df1ef 100644 --- a/src/type/mpi.js +++ b/src/type/mpi.js @@ -28,12 +28,9 @@ * An MPI consists of two pieces: a two-octet scalar that is the length * of the MPI in bits followed by a string of octets that contain the * actual integer. - * @requires bn.js * @requires util * @module type/mpi */ - -import BN from 'bn.js'; import util from '../util'; class MPI { @@ -41,7 +38,9 @@ class MPI { /** An implementation dependent integer */ if (data instanceof MPI) { this.data = data.data; - } else if (BN.isBN(data)) { + } else if (util.isBigInteger(data)) { + this.fromBigInteger(data); + } else if (util.isBN(data)) { this.fromBN(data); } else if (util.isUint8Array(data)) { this.fromUint8Array(data); @@ -121,8 +120,13 @@ class MPI { this.fromUint8Array(util.strToUint8Array(str), endian); } - toBN() { - return new BN(this.toUint8Array()); + async toBigInteger() { + const BigInteger = await util.getBigInteger(); + return new BigInteger(this.toUint8Array()); + } + + fromBigInteger(n) { + this.data = n.toUint8Array(); } fromBN(bn) { diff --git a/src/util.js b/src/util.js index 6516bdff..33c2e229 100644 --- a/src/util.js +++ b/src/util.js @@ -31,6 +31,7 @@ import stream from 'web-stream-tools'; import config from './config'; import util from './util'; // re-import module to access util functions import b64 from './encoding/base64'; +import { getBigInteger } from './biginteger'; export default { isString: function(data) { @@ -41,6 +42,18 @@ export default { return Array.prototype.isPrototypeOf(data); }, + isBigInteger: function(data) { + return data !== null && typeof data === 'object' && data.value && + // eslint-disable-next-line valid-typeof + (typeof data.value === 'bigint' || this.isBN(data.value)); + }, + + isBN: function(data) { + return data !== null && typeof data === 'object' && + (data.constructor.name === 'BN' || + (data.constructor.wordSize === 26 && Array.isArray(data.words))); // taken from BN.isBN() + }, + isUint8Array: stream.isUint8Array, isStream: stream.isStream, @@ -519,6 +532,20 @@ export default { typeof globalThis.process.versions === 'object'; }, + /** + * Detect native BigInt support + */ + detectBigInt: () => typeof BigInt !== 'undefined', + + /** + * Get BigInteger class + * It wraps the native BigInt type if it's available + * Otherwise it relies on bn.js + * @returns {BigInteger} + * @async + */ + getBigInteger, + /** * Get native Node.js crypto api. The default configuration is to use * the api when available. But it can also be deactivated with config.useNative diff --git a/test/crypto/crypto.js b/test/crypto/crypto.js index a823885d..508a86eb 100644 --- a/test/crypto/crypto.js +++ b/test/crypto/crypto.js @@ -354,28 +354,24 @@ module.exports = () => describe('API functional testing', function() { testAESGCM("12345678901234567890123456789012345678901234567890", false); }); - it('Asymmetric using RSA with eme_pkcs1 padding', function () { - const symmKey = util.uint8ArrayToStr(crypto.generateSessionKey('aes256')); - crypto.publicKeyEncrypt(1, RSApubMPIs, symmKey).then(RSAEncryptedData => { + it('Asymmetric using RSA with eme_pkcs1 padding', async function () { + const symmKey = await crypto.generateSessionKey('aes256'); + return crypto.publicKeyEncrypt(1, RSApubMPIs, symmKey).then(RSAEncryptedData => { return crypto.publicKeyDecrypt( 1, RSApubMPIs.concat(RSAsecMPIs), RSAEncryptedData ).then(data => { - data = new openpgp.MPI(data).write(); - data = util.uint8ArrayToStr(data.subarray(2, data.length)); - expect(data).to.equal(symmKey); + expect(data).to.deep.equal(symmKey); }); }); }); - it('Asymmetric using Elgamal with eme_pkcs1 padding', function () { - const symmKey = util.uint8ArrayToStr(crypto.generateSessionKey('aes256')); - crypto.publicKeyEncrypt(16, ElgamalpubMPIs, symmKey).then(ElgamalEncryptedData => { + it('Asymmetric using Elgamal with eme_pkcs1 padding', async function () { + const symmKey = await crypto.generateSessionKey('aes256'); + return crypto.publicKeyEncrypt(16, ElgamalpubMPIs, symmKey).then(ElgamalEncryptedData => { return crypto.publicKeyDecrypt( 16, ElgamalpubMPIs.concat(ElgamalsecMPIs), ElgamalEncryptedData ).then(data => { - data = new openpgp.MPI(data).write(); - data = util.uint8ArrayToStr(data.subarray(2, data.length)); - expect(data).to.equal(symmKey); + expect(data).to.deep.equal(symmKey); }); }); }); diff --git a/test/crypto/pkcs5.js b/test/crypto/pkcs5.js index d7dd5957..3d4c68cb 100644 --- a/test/crypto/pkcs5.js +++ b/test/crypto/pkcs5.js @@ -3,37 +3,13 @@ const openpgp = typeof window !== 'undefined' && window.openpgp ? window.openpgp const expect = require('chai').expect; module.exports = () => describe('PKCS5 padding', function() { - function repeat(pattern, count) { - let result = ''; - for (let k = 0; k < count; ++k) { - result += pattern; - } - return result; - } const pkcs5 = openpgp.crypto.pkcs5; - it('Add padding', function () { - let s = ''; - while (s.length < 16) { - const r = pkcs5.encode(s); - // 0..7 -> 8, 8..15 -> 16 - const l = Math.ceil((s.length + 1) / 8) * 8; - const c = l - s.length; - expect(r.length).to.equal(l); - expect(c).is.at.least(1).is.at.most(8); - expect(r.substr(-1)).to.equal(String.fromCharCode(c)); - s += ' '; - } - }); - it('Remove padding', function () { - for (let k = 1; k <= 8; ++k) { - const s = repeat(' ', 8 - k); - const r = s + repeat(String.fromCharCode(k), k); - const t = pkcs5.decode(r); - expect(t).to.equal(s); - } - }); - it('Invalid padding', function () { - expect(function () { pkcs5.decode(' '); }).to.throw(Error, /Invalid padding/); - expect(function () { pkcs5.decode(''); }).to.throw(Error, /Invalid padding/); + it('Add and remove padding', function () { + const m = new Uint8Array([0,1,2,3,4,5,6,7,8]); + const padded = pkcs5.encode(m); + const unpadded = pkcs5.decode(padded); + expect(padded[padded.length - 1]).to.equal(7); + expect(padded.length % 8).to.equal(0); + expect(unpadded).to.deep.equal(m); }); }); diff --git a/test/crypto/rsa.js b/test/crypto/rsa.js index 3da49239..51962e15 100644 --- a/test/crypto/rsa.js +++ b/test/crypto/rsa.js @@ -11,7 +11,7 @@ const native = openpgp.util.getWebCrypto() || openpgp.util.getNodeCrypto(); module.exports = () => (!native ? describe.skip : describe)('basic RSA cryptography with native crypto', function () { it('generate rsa key', async function() { const bits = openpgp.util.getWebCryptoAll() ? 2048 : 1024; - const keyObject = await openpgp.crypto.publicKey.rsa.generate(bits, "10001"); + const keyObject = await openpgp.crypto.publicKey.rsa.generate(bits, 65537); expect(keyObject.n).to.exist; expect(keyObject.e).to.exist; expect(keyObject.d).to.exist; @@ -47,11 +47,11 @@ module.exports = () => (!native ? describe.skip : describe)('basic RSA cryptogra const p = keyParams[3].toUint8Array(); const q = keyParams[4].toUint8Array(); const u = keyParams[5].toUint8Array(); - const message = openpgp.util.uint8ArrayToStr(await openpgp.crypto.generateSessionKey('aes256')); - const encrypted = await openpgp.crypto.publicKey.rsa.encrypt(openpgp.util.strToUint8Array(message), n, e); + const message = await openpgp.crypto.generateSessionKey('aes256'); + const encrypted = await openpgp.crypto.publicKey.rsa.encrypt(message, n, e); const result = new openpgp.MPI(encrypted); const decrypted = await openpgp.crypto.publicKey.rsa.decrypt(result.toUint8Array(), n, e, d, p, q, u); - expect(decrypted).to.be.equal(message); + expect(decrypted).to.deep.equal(message); }); it('decrypt nodeCrypto by bnCrypto and vice versa', async function() { @@ -66,15 +66,13 @@ module.exports = () => (!native ? describe.skip : describe)('basic RSA cryptogra const p = keyParams[3].toUint8Array(); const q = keyParams[4].toUint8Array(); const u = keyParams[5].toUint8Array(); - const message = openpgp.util.uint8ArrayToStr(await openpgp.crypto.generateSessionKey('aes256')); - const encryptedBn = await openpgp.crypto.publicKey.rsa.bnEncrypt(openpgp.util.strToUint8Array(message), n, e); - const resultBN = new openpgp.MPI(encryptedBn); - const decrypted1 = await openpgp.crypto.publicKey.rsa.nodeDecrypt(resultBN.toUint8Array(), n, e, d, p, q, u); - expect(decrypted1).to.be.equal(message); - const encryptedNode = await openpgp.crypto.publicKey.rsa.nodeEncrypt(openpgp.util.strToUint8Array(message), n, e); - const resultNode = new openpgp.MPI(encryptedNode); - const decrypted2 = await openpgp.crypto.publicKey.rsa.bnDecrypt(resultNode.toUint8Array(), n, e, d, p, q, u); - expect(decrypted2).to.be.equal(message); + const message = await openpgp.crypto.generateSessionKey('aes256'); + const encryptedBn = await openpgp.crypto.publicKey.rsa.bnEncrypt(message, n, e); + const decrypted1 = await openpgp.crypto.publicKey.rsa.nodeDecrypt(encryptedBn, n, e, d, p, q, u); + expect(decrypted1).to.deep.equal(message); + const encryptedNode = await openpgp.crypto.publicKey.rsa.nodeEncrypt(message, n, e); + const decrypted2 = await openpgp.crypto.publicKey.rsa.bnDecrypt(encryptedNode, n, e, d, p, q, u); + expect(decrypted2).to.deep.equal(message); }); it('compare webCrypto and bn math sign', async function() { diff --git a/test/general/biginteger.js b/test/general/biginteger.js new file mode 100644 index 00000000..29171198 --- /dev/null +++ b/test/general/biginteger.js @@ -0,0 +1,168 @@ +const openpgp = typeof window !== 'undefined' && window.openpgp ? window.openpgp : require('../..'); +const BN = require('bn.js'); +const chai = require('chai'); +chai.use(require('chai-as-promised')); + +const expect = chai.expect; +let BigInteger; + +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(await openpgp.crypto.random.getRandomBytes(bytes + 8)); + return r.mod(modulus).add(min); +} + +module.exports = () => describe('BigInteger interface', function() { + before(async () => { + BigInteger = await openpgp.util.getBigInteger(); + }); + + it('constructor throws on undefined input', function() { + expect(() => new BigInteger()).to.throw('Invalid BigInteger input'); + }); + + + it('constructor supports strings', function() { + const input = '417653931840771530406225971293556769925351769207235721650257629558293828796031115397206059067934284452829611906818956352854418342467914729341523414945427019410284762464062112274326172407819051167058569790660930309496043254270888417520676082271432948852231332576271876251597199882908964994070268531832274431027'; + const got = new BigInteger(input); + const expected = new BN(input); + expect(got.toString()).to.equal(expected.toString()); + }); + + it('constructor supports Uint8Arrays', function() { + const expected = new BN('417653931840771530406225971293556769925351769207235721650257629558293828796031115397206059067934284452829611906818956352854418342467914729341523414945427019410284762464062112274326172407819051167058569790660930309496043254270888417520676082271432948852231332576271876251597199882908964994070268531832274431027'); + const input = expected.toArrayLike(Uint8Array); + const got = new BigInteger(input); + expect(got.toString()).to.equal(expected.toString()); + }); + + it('conditional operators are correct', function() { + const a = new BigInteger(12); + const b = new BigInteger(34); + + expect(a.equal(a)).to.be.true; + expect(a.equal(b)).to.be.false; + expect(a.gt(a) === a.lt(a)).to.be.true; + expect(a.gt(b) === a.lt(b)).to.be.false; + expect(a.gte(a) === a.lte(a)).to.be.true; + + const zero = new BigInteger(0); + const one = new BigInteger(1); + expect(zero.isZero()).to.be.true; + expect(one.isZero()).to.be.false; + + expect(one.isOne()).to.be.true; + expect(zero.isOne()).to.be.false; + + expect(zero.isEven()).to.be.true; + expect(one.isEven()).to.be.false; + + expect(zero.isNegative()).to.be.false; + expect(zero.dec().isNegative()).to.be.true; + }); + + it('bitLength is correct', function() { + const n = new BigInteger(127); + let expected = 7; + expect(n.bitLength() === expected).to.be.true; + expect(n.inc().bitLength() === (++expected)).to.be.true; + }); + + it('byteLength is correct', function() { + const n = new BigInteger(65535); + let expected = 2; + expect(n.byteLength() === expected).to.be.true; + expect(n.inc().byteLength() === (++expected)).to.be.true; + }); + + it('toUint8Array is correct', function() { + const nString = '417653931840771530406225971293556769925351769207235721650257629558293828796031115397206059067934284452829611906818956352854418342467914729341523414945427019410284762464062112274326172407819051167058569790660930309496043254270888417520676082271432948852231332576271876251597199882908964994070268531832274431027'; + const n = new BigInteger(nString); + const paddedSize = Number(n.byteLength()) + 1; + // big endian, unpadded + let expected = new BN(nString).toArrayLike(Uint8Array); + expect(n.toUint8Array()).to.deep.equal(expected); + // big endian, padded + expected = new BN(nString).toArrayLike(Uint8Array, 'be', paddedSize); + expect(n.toUint8Array('be', paddedSize)).to.deep.equal(expected); + // little endian, unpadded + expected = new BN(nString).toArrayLike(Uint8Array, 'le'); + expect(n.toUint8Array('le')).to.deep.equal(expected); + //little endian, padded + expected = new BN(nString).toArrayLike(Uint8Array, 'le', paddedSize); + expect(n.toUint8Array('le', paddedSize)).to.deep.equal(expected); + }); + + it('binary operators are consistent', function() { + const a = new BigInteger(12); + const b = new BigInteger(34); + const ops = ['add', 'sub', 'mul', 'mod', 'leftShift', 'rightShift']; + ops.forEach(op => { + const iop = `i${op}`; + expect(a[op](b).equal(a[iop](b))).to.be.true; + }); + }); + + it('unary operators are consistent', function() { + const a = new BigInteger(12); + const one = new BigInteger(1); + expect(a.sub(one).equal(a.dec())).to.be.true; + expect(a.add(one).equal(a.inc())).to.be.true; + }); + + it('modExp is correct (large values)', function() { + const stringX = '417653931840771530406225971293556769925351769207235721650257629558293828796031115397206059067934284452829611906818956352854418342467914729341523414945427019410284762464062112274326172407819051167058569790660930309496043254270888417520676082271432948852231332576271876251597199882908964994070268531832274431027'; + const stringE = '21139356010872569239159922781526379521587348169074209285187910481667533072168468011617194695181255483288792585413365359733692097084373249198758148704369207793873998901870577262254971784191473102265830193058813215898765238784670469696574407580179153118937858890572095234316482449291777882525949871374961971753'; + const stringN = '129189808515414783602892982235788912674846062846614219472827821758734760420002631653235573915244294540972376140705505703576175711417114803419704967903726436285518767606681184247119430411311152556442947708732584954518890222684529678365388350886907287414896703685680210648760841628375425909680236584021041565183'; + const x = new BigInteger(stringX); + const e = new BigInteger(stringE); + const n = new BigInteger(stringN); + + const got = x.modExp(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()).to.be.true; + }); + + 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 = new BigInteger(aBN.toString()); + const b = new BigInteger(bBN.toString()); + const expected = aBN.gcd(bBN); + expect(a.gcd(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 = new BigInteger(baseBN.toString()); + const n = new BigInteger(moduloBN.toString()); + const expected = baseBN.invm(moduloBN); + expect(a.modInv(n).toString()).to.equal(expected.toString()); + }); + + it('getBit is correct', async function() { + const i = 5; + const nBN = await getRandomBN(new BN(2), new BN(200)); + const n = new BigInteger(nBN.toString()); + const expected = nBN.testn(5) ? 1 : 0; + expect(n.getBit(i) === expected).to.be.true; + }); + + describe('MPI and BigInteger conversions', function() { + it('MPI to/from BigInteger is correct', async function() { + const input = '417653931840771530406225971293556769925351769207235721650257629558293828796031115397206059067934284452829611906818956352854418342467914729341523414945427019410284762464062112274326172407819051167058569790660930309496043254270888417520676082271432948852231332576271876251597199882908964994070268531832274431027'; + const n = new BigInteger(input); + const mpi = new openpgp.MPI(n); + + expect((await mpi.toBigInteger()).equal(n)).to.be.true; + }); + }); +}); diff --git a/test/general/index.js b/test/general/index.js index 613ee7b0..692542d1 100644 --- a/test/general/index.js +++ b/test/general/index.js @@ -1,5 +1,6 @@ module.exports = () => describe('General', function () { require('./util.js')(); + require('./biginteger.js')(); require('./armor.js')(); require('./packet.js')(); require('./keyring.js')(); diff --git a/test/general/key.js b/test/general/key.js index e675623e..2ed28d6e 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -2588,9 +2588,9 @@ module.exports = () => describe('Key', function() { let v5KeysVal; let aeadProtectVal; const rsaGenValue = { - 512: openpgp.crypto.publicKey.rsa.generate(512, "10001"), - 1024: openpgp.crypto.publicKey.rsa.generate(1024, "10001"), - 2048: openpgp.crypto.publicKey.rsa.generate(2048, "10001") + 512: openpgp.crypto.publicKey.rsa.generate(512, 65537), + 1024: openpgp.crypto.publicKey.rsa.generate(1024, 65537), + 2048: openpgp.crypto.publicKey.rsa.generate(2048, 65537) }; beforeEach(function() { diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 105ba282..75731fc0 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -551,7 +551,7 @@ function withCompression(tests) { module.exports = () => describe('OpenPGP.js public api tests', function() { let rsaGenStub; - let rsaGenValue = openpgp.crypto.publicKey.rsa.generate(openpgp.util.getWebCryptoAll() ? 2048 : 512, "10001"); + let rsaGenValue = openpgp.crypto.publicKey.rsa.generate(openpgp.util.getWebCryptoAll() ? 2048 : 512, 65537); beforeEach(function() { rsaGenStub = stub(openpgp.crypto.publicKey.rsa, 'generate'); @@ -2345,6 +2345,7 @@ module.exports = () => describe('OpenPGP.js public api tests', function() { }); it('RSA decryption with PKCS1 padding of wrong length should fail', async function() { + const key = await openpgp.key.readArmored(rsaPrivateKeyPKCS1); const key = (await openpgp.key.readArmored(rsaPrivateKeyPKCS1)).keys[0]; // the paddings of these messages are prefixed by 0x02 and 0x000002 instead of 0x0002 // the code should discriminate between these cases by checking the length of the padded plaintext diff --git a/test/general/packet.js b/test/general/packet.js index 647048b9..c120f471 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -318,7 +318,7 @@ module.exports = () => describe("Packet", function() { const rsa = openpgp.crypto.publicKey.rsa; const keySize = openpgp.util.getWebCryptoAll() ? 2048 : 512; // webkit webcrypto accepts minimum 2048 bit keys - return rsa.generate(keySize, "10001").then(function(mpiGen) { + return rsa.generate(keySize, 65537).then(function(mpiGen) { let mpi = [mpiGen.n, mpiGen.e, mpiGen.d, mpiGen.p, mpiGen.q, mpiGen.u]; mpi = mpi.map(function(k) { @@ -848,7 +848,7 @@ V+HOQJQxXJkVRYa3QrFUehiMzTeqqMdgC6ZqJy7+ const rsa = openpgp.crypto.publicKey.rsa; const keySize = openpgp.util.getWebCryptoAll() ? 2048 : 512; // webkit webcrypto accepts minimum 2048 bit keys - return rsa.generate(keySize, "10001").then(async function(mpiGen) { + return rsa.generate(keySize, 65537).then(async function(mpiGen) { let mpi = [mpiGen.n, mpiGen.e, mpiGen.d, mpiGen.p, mpiGen.q, mpiGen.u]; mpi = mpi.map(function(k) { return new openpgp.MPI(k); @@ -880,7 +880,7 @@ V+HOQJQxXJkVRYa3QrFUehiMzTeqqMdgC6ZqJy7+ const keySize = openpgp.util.getWebCryptoAll() ? 2048 : 512; // webkit webcrypto accepts minimum 2048 bit keys try { - const mpiGen = await rsa.generate(keySize, "10001"); + const mpiGen = await rsa.generate(keySize, 65537); let mpi = [mpiGen.n, mpiGen.e, mpiGen.d, mpiGen.p, mpiGen.q, mpiGen.u]; mpi = mpi.map(function(k) { return new openpgp.MPI(k); @@ -909,7 +909,7 @@ V+HOQJQxXJkVRYa3QrFUehiMzTeqqMdgC6ZqJy7+ const rsa = openpgp.crypto.publicKey.rsa; const keySize = openpgp.util.getWebCryptoAll() ? 2048 : 512; // webkit webcrypto accepts minimum 2048 bit keys - return rsa.generate(keySize, "10001").then(function(mpiGen) { + return rsa.generate(keySize, 65537).then(function(mpiGen) { let mpi = [mpiGen.n, mpiGen.e, mpiGen.d, mpiGen.p, mpiGen.q, mpiGen.u]; mpi = mpi.map(function(k) { return new openpgp.MPI(k);