From 00477e3bcb66e4cb57b1f9261d87d5eea0acf858 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 26 Feb 2020 16:16:32 +0000 Subject: [PATCH] perf: remove asn1.js and use node-forge (#166) * perf: remove asn1.js from rsa * fix: tweaks * fix: it works, but I do not know 100% why * chore: remove asn1.js * fix: ensure jwk params encoded as uint * fix: util tests * fix: zero pad base64urlToBuffer * fix: more zero pad * test: add round trip test * test: base64url to Buffer with padding --- package.json | 1 - src/keys/ecdh-browser.js | 23 ++++---- src/keys/jwk2pem.js | 11 +--- src/keys/rsa-utils.js | 122 ++++++++++++--------------------------- src/util.js | 57 ++++++++++++++---- test/util.spec.js | 31 ++++++---- 6 files changed, 114 insertions(+), 131 deletions(-) diff --git a/package.json b/package.json index c6e749f7..7fc380b6 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "license": "MIT", "dependencies": { "asmcrypto.js": "^2.3.2", - "asn1.js": "^5.2.0", "bn.js": "^5.0.0", "browserify-aes": "^1.2.0", "bs58": "^4.0.1", diff --git a/src/keys/ecdh-browser.js b/src/keys/ecdh-browser.js index 48f08433..8e3e3b1f 100644 --- a/src/keys/ecdh-browser.js +++ b/src/keys/ecdh-browser.js @@ -1,9 +1,9 @@ 'use strict' const errcode = require('err-code') +const { Buffer } = require('buffer') const webcrypto = require('../webcrypto') -const BN = require('asn1.js').bignum -const { toBase64, toBn } = require('../util') +const { bufferToBase64url, base64urlToBuffer } = require('../util') const validateCurveType = require('./validate-curve-type') const bits = { @@ -89,8 +89,8 @@ function marshalPublicKey (jwk) { return Buffer.concat([ Buffer.from([4]), // uncompressed point - toBn(jwk.x).toArrayLike(Buffer, 'be', byteLen), - toBn(jwk.y).toArrayLike(Buffer, 'be', byteLen) + base64urlToBuffer(jwk.x, byteLen), + base64urlToBuffer(jwk.y, byteLen) ], 1 + byteLen * 2) } @@ -101,20 +101,17 @@ function unmarshalPublicKey (curve, key) { if (!key.slice(0, 1).equals(Buffer.from([4]))) { throw errcode(new Error('Cannot unmarshal public key - invalid key format'), 'ERR_INVALID_KEY_FORMAT') } - const x = new BN(key.slice(1, byteLen + 1)) - const y = new BN(key.slice(1 + byteLen)) return { kty: 'EC', crv: curve, - x: toBase64(x, byteLen), - y: toBase64(y, byteLen), + x: bufferToBase64url(key.slice(1, byteLen + 1), byteLen), + y: bufferToBase64url(key.slice(1 + byteLen), byteLen), ext: true } } -function unmarshalPrivateKey (curve, key) { - const result = unmarshalPublicKey(curve, key.public) - result.d = toBase64(new BN(key.private)) - return result -} +const unmarshalPrivateKey = (curve, key) => ({ + ...unmarshalPublicKey(curve, key.public), + d: bufferToBase64url(key.private) +}) diff --git a/src/keys/jwk2pem.js b/src/keys/jwk2pem.js index c5166929..82345351 100644 --- a/src/keys/jwk2pem.js +++ b/src/keys/jwk2pem.js @@ -1,17 +1,8 @@ 'use strict' require('node-forge/lib/rsa') -require('node-forge/lib/util') -require('node-forge/lib/jsbn') const forge = require('node-forge/lib/forge') - -function base64urlToBigInteger (str) { - var bytes = forge.util.decode64( - (str + '==='.slice((str.length + 3) % 4)) - .replace(/-/g, '+') - .replace(/_/g, '/')) - return new forge.jsbn.BigInteger(forge.util.bytesToHex(bytes), 16) -} +const { base64urlToBigInteger } = require('../util') function convert (key, types) { return types.map(t => base64urlToBigInteger(key[t])) diff --git a/src/keys/rsa-utils.js b/src/keys/rsa-utils.js index d10f2a1f..da377447 100644 --- a/src/keys/rsa-utils.js +++ b/src/keys/rsa-utils.js @@ -1,68 +1,27 @@ 'use strict' -const asn1 = require('asn1.js') - -const util = require('./../util') -const toBase64 = util.toBase64 -const toBn = util.toBn - -const RSAPrivateKey = asn1.define('RSAPrivateKey', function () { - this.seq().obj( - this.key('version').int(), - this.key('modulus').int(), - this.key('publicExponent').int(), - this.key('privateExponent').int(), - this.key('prime1').int(), - this.key('prime2').int(), - this.key('exponent1').int(), - this.key('exponent2').int(), - this.key('coefficient').int() - ) -}) - -const AlgorithmIdentifier = asn1.define('AlgorithmIdentifier', function () { - this.seq().obj( - this.key('algorithm').objid({ - '1.2.840.113549.1.1.1': 'rsa' - }), - this.key('none').optional().null_(), - this.key('curve').optional().objid(), - this.key('params').optional().seq().obj( - this.key('p').int(), - this.key('q').int(), - this.key('g').int() - ) - ) -}) - -const PublicKey = asn1.define('RSAPublicKey', function () { - this.seq().obj( - this.key('algorithm').use(AlgorithmIdentifier), - this.key('subjectPublicKey').bitstr() - ) -}) - -const RSAPublicKey = asn1.define('RSAPublicKey', function () { - this.seq().obj( - this.key('modulus').int(), - this.key('publicExponent').int() - ) -}) +const { Buffer } = require('buffer') +require('node-forge/lib/asn1') +require('node-forge/lib/rsa') +const forge = require('node-forge/lib/forge') +const { bigIntegerToUintBase64url, base64urlToBigInteger } = require('./../util') // Convert a PKCS#1 in ASN1 DER format to a JWK key exports.pkcs1ToJwk = function (bytes) { - const asn1 = RSAPrivateKey.decode(bytes, 'der') + const asn1 = forge.asn1.fromDer(bytes.toString('binary')) + const privateKey = forge.pki.privateKeyFromAsn1(asn1) + // https://tools.ietf.org/html/rfc7518#section-6.3.1 return { kty: 'RSA', - n: toBase64(asn1.modulus), - e: toBase64(asn1.publicExponent), - d: toBase64(asn1.privateExponent), - p: toBase64(asn1.prime1), - q: toBase64(asn1.prime2), - dp: toBase64(asn1.exponent1), - dq: toBase64(asn1.exponent2), - qi: toBase64(asn1.coefficient), + n: bigIntegerToUintBase64url(privateKey.n), + e: bigIntegerToUintBase64url(privateKey.e), + d: bigIntegerToUintBase64url(privateKey.d), + p: bigIntegerToUintBase64url(privateKey.p), + q: bigIntegerToUintBase64url(privateKey.q), + dp: bigIntegerToUintBase64url(privateKey.dP), + dq: bigIntegerToUintBase64url(privateKey.dQ), + qi: bigIntegerToUintBase64url(privateKey.qInv), alg: 'RS256', kid: '2011-04-29' } @@ -70,28 +29,29 @@ exports.pkcs1ToJwk = function (bytes) { // Convert a JWK key into PKCS#1 in ASN1 DER format exports.jwkToPkcs1 = function (jwk) { - return RSAPrivateKey.encode({ - version: 0, - modulus: toBn(jwk.n), - publicExponent: toBn(jwk.e), - privateExponent: toBn(jwk.d), - prime1: toBn(jwk.p), - prime2: toBn(jwk.q), - exponent1: toBn(jwk.dp), - exponent2: toBn(jwk.dq), - coefficient: toBn(jwk.qi) - }, 'der') + const asn1 = forge.pki.privateKeyToAsn1({ + n: base64urlToBigInteger(jwk.n), + e: base64urlToBigInteger(jwk.e), + d: base64urlToBigInteger(jwk.d), + p: base64urlToBigInteger(jwk.p), + q: base64urlToBigInteger(jwk.q), + dP: base64urlToBigInteger(jwk.dp), + dQ: base64urlToBigInteger(jwk.dq), + qInv: base64urlToBigInteger(jwk.qi) + }) + + return Buffer.from(forge.asn1.toDer(asn1).getBytes(), 'binary') } // Convert a PKCIX in ASN1 DER format to a JWK key exports.pkixToJwk = function (bytes) { - const ndata = PublicKey.decode(bytes, 'der') - const asn1 = RSAPublicKey.decode(ndata.subjectPublicKey.data, 'der') + const asn1 = forge.asn1.fromDer(bytes.toString('binary')) + const publicKey = forge.pki.publicKeyFromAsn1(asn1) return { kty: 'RSA', - n: toBase64(asn1.modulus), - e: toBase64(asn1.publicExponent), + n: bigIntegerToUintBase64url(publicKey.n), + e: bigIntegerToUintBase64url(publicKey.e), alg: 'RS256', kid: '2011-04-29' } @@ -99,16 +59,10 @@ exports.pkixToJwk = function (bytes) { // Convert a JWK key to PKCIX in ASN1 DER format exports.jwkToPkix = function (jwk) { - return PublicKey.encode({ - algorithm: { - algorithm: 'rsa', - none: null - }, - subjectPublicKey: { - data: RSAPublicKey.encode({ - modulus: toBn(jwk.n), - publicExponent: toBn(jwk.e) - }, 'der') - } - }, 'der') + const asn1 = forge.pki.publicKeyToAsn1({ + n: base64urlToBigInteger(jwk.n), + e: base64urlToBigInteger(jwk.e) + }) + + return Buffer.from(forge.asn1.toDer(asn1).getBytes(), 'binary') } diff --git a/src/util.js b/src/util.js index 3624bfe8..6938d5ca 100644 --- a/src/util.js +++ b/src/util.js @@ -1,20 +1,55 @@ 'use strict' -const BN = require('asn1.js').bignum +const { Buffer } = require('buffer') +require('node-forge/lib/util') +require('node-forge/lib/jsbn') +const forge = require('node-forge/lib/forge') -// Convert a BN.js instance to a base64 encoded string without padding -// Adapted from https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#appendix-C -exports.toBase64 = function toBase64 (bn, len) { - // if len is defined then the bytes are leading-0 padded to the length - const s = bn.toArrayLike(Buffer, 'be', len).toString('base64') +exports.bigIntegerToUintBase64url = (num, len) => { + // Call `.abs()` to convert to unsigned + let buf = Buffer.from(num.abs().toByteArray()) // toByteArray converts to big endian + + // toByteArray() gives us back a signed array, which will include a leading 0 + // byte if the most significant bit of the number is 1: + // https://docs.microsoft.com/en-us/windows/win32/seccertenroll/about-integer + // Our number will always be positive so we should remove the leading padding. + buf = buf[0] === 0 ? buf.slice(1) : buf + + if (len != null) { + if (buf.length > len) throw new Error('byte array longer than desired length') + buf = Buffer.concat([Buffer.alloc(len - buf.length), buf]) + } - return s - .replace(/(=*)$/, '') // Remove any trailing '='s + return exports.bufferToBase64url(buf) +} + +// Convert a Buffer to a base64 encoded string without padding +// Adapted from https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#appendix-C +exports.bufferToBase64url = buf => { + return buf + .toString('base64') + .split('=')[0] // Remove any trailing '='s .replace(/\+/g, '-') // 62nd char of encoding .replace(/\//g, '_') // 63rd char of encoding } -// Convert a base64 encoded string to a BN.js instance -exports.toBn = function toBn (str) { - return new BN(Buffer.from(str, 'base64')) +// Convert a base64url encoded string to a BigInteger +exports.base64urlToBigInteger = str => { + const buf = exports.base64urlToBuffer(str) + return new forge.jsbn.BigInteger(buf.toString('hex'), 16) +} + +exports.base64urlToBuffer = (str, len) => { + str = (str + '==='.slice((str.length + 3) % 4)) + .replace(/-/g, '+') + .replace(/_/g, '/') + + let buf = Buffer.from(str, 'base64') + + if (len != null) { + if (buf.length > len) throw new Error('byte array longer than desired length') + buf = Buffer.concat([Buffer.alloc(len - buf.length), buf]) + } + + return buf } diff --git a/test/util.spec.js b/test/util.spec.js index 3688861b..5c362d13 100644 --- a/test/util.spec.js +++ b/test/util.spec.js @@ -6,26 +6,33 @@ const chai = require('chai') const dirtyChai = require('dirty-chai') const expect = chai.expect chai.use(dirtyChai) - +require('node-forge/lib/jsbn') +const forge = require('node-forge/lib/forge') const util = require('../src/util') -const BN = require('bn.js') describe('Util', () => { let bn - before((done) => { - bn = new BN('dead', 16) - done() + before(() => { + bn = new forge.jsbn.BigInteger('dead', 16) + }) + + it('should convert BigInteger to a uint base64url encoded string', () => { + expect(util.bigIntegerToUintBase64url(bn)).to.eql('3q0') + }) + + it('should convert BigInteger to a uint base64url encoded string with padding', () => { + const bnpad = new forge.jsbn.BigInteger('ff', 16) + expect(util.bigIntegerToUintBase64url(bnpad, 2)).to.eql('AP8') }) - it('toBase64', (done) => { - expect(util.toBase64(bn)).to.eql('3q0') - done() + it('should convert base64url encoded string to BigInteger', () => { + const num = util.base64urlToBigInteger('3q0') + expect(num.equals(bn)).to.be.true() }) - it('toBase64 zero padding', (done) => { - const bnpad = new BN('ff', 16) - expect(util.toBase64(bnpad, 2)).to.eql('AP8') - done() + it('should convert base64url encoded string to Buffer with padding', () => { + const buf = util.base64urlToBuffer('AP8', 2) + expect(Buffer.from([0, 255])).to.eql(buf) }) })