Skip to content
This repository has been archived by the owner on Jul 21, 2023. It is now read-only.

Commit

Permalink
perf: remove asn1.js and use node-forge (#166)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Alan Shaw authored Feb 26, 2020
1 parent 0f4c533 commit 00477e3
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 131 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
23 changes: 10 additions & 13 deletions src/keys/ecdh-browser.js
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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)
}

Expand All @@ -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)
})
11 changes: 1 addition & 10 deletions src/keys/jwk2pem.js
Original file line number Diff line number Diff line change
@@ -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]))
Expand Down
122 changes: 38 additions & 84 deletions src/keys/rsa-utils.js
Original file line number Diff line number Diff line change
@@ -1,114 +1,68 @@
'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'
}
}

// 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'
}
}

// 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')
}
57 changes: 46 additions & 11 deletions src/util.js
Original file line number Diff line number Diff line change
@@ -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
}
31 changes: 19 additions & 12 deletions test/util.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})

0 comments on commit 00477e3

Please sign in to comment.