diff --git a/package.json b/package.json index c203f7f..30d6abf 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,15 @@ "./src/crypto/webcrypto.js": "./src/crypto/webcrypto-browser.js", "./src/crypto/hmac.js": "./src/crypto/hmac-browser.js", "./src/crypto/ecdh.js": "./src/crypto/ecdh-browser.js", - "./src/crypto/ciphers.js": "./src/crypto/ciphers-browser.js" + "./src/crypto/ciphers.js": "./src/crypto/ciphers-browser.js", + "./src/crypto/rsa.js": "./src/crypto/rsa-browser.js" }, "scripts": { "lint": "aegir-lint", "build": "aegir-build", - "test": "aegir-test", + "test": "npm run test:node && npm run test:no-webcrypto && npm run test:browser", "test:node": "aegir-test --env node", + "test:no-webcrypto": "NO_WEBCRYPTO=true aegir-test --env node", "test:browser": "aegir-test --env browser", "release": "aegir-release", "release-minor": "aegir-release --type minor", @@ -34,10 +36,12 @@ "asn1.js": "^4.8.1", "async": "^2.1.2", "browserify-aes": "^1.0.6", + "keypair": "^1.0.0", "multihashing-async": "^0.2.0", - "node-webcrypto-ossl": "^1.0.13", "nodeify": "^1.0.0", + "pem-jwk": "^1.5.1", "protocol-buffers": "^3.2.1", + "rsa-pem-to-jwk": "^1.1.3", "webcrypto-shim": "github:dignifiedquire/webcrypto-shim#master" }, "devDependencies": { @@ -46,6 +50,9 @@ "chai": "^3.5.0", "pre-commit": "^1.1.3" }, + "optionalDependencies": { + "node-webcrypto-ossl": "^1.0.13" + }, "pre-commit": [ "lint", "test" diff --git a/src/crypto/ecdh-browser.js b/src/crypto/ecdh-browser.js index 1970e1b..12a1d6d 100644 --- a/src/crypto/ecdh-browser.js +++ b/src/crypto/ecdh-browser.js @@ -97,7 +97,7 @@ function marshalPublicKey (jwk) { const byteLen = curveLengths[jwk.crv] return Buffer.concat([ - Buffer([4]), // uncompressed point + new Buffer([4]), // uncompressed point toBn(jwk.x).toBuffer('be', byteLen), toBn(jwk.y).toBuffer('be', byteLen) ], 1 + byteLen * 2) @@ -107,7 +107,7 @@ function marshalPublicKey (jwk) { function unmarshalPublicKey (curve, key) { const byteLen = curveLengths[curve] - if (!key.slice(0, 1).equals(Buffer([4]))) { + if (!key.slice(0, 1).equals(new Buffer([4]))) { throw new Error('Invalid key format') } const x = new BN(key.slice(1, byteLen + 1)) diff --git a/src/crypto/rsa-browser.js b/src/crypto/rsa-browser.js new file mode 100644 index 0000000..76eb35f --- /dev/null +++ b/src/crypto/rsa-browser.js @@ -0,0 +1,119 @@ +'use strict' + +const nodeify = require('nodeify') + +const crypto = require('./webcrypto')() + +exports.utils = require('./rsa-utils') + +exports.generateKey = function (bits, callback) { + nodeify(crypto.subtle.generateKey( + { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: bits, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: {name: 'SHA-256'} + }, + true, + ['sign', 'verify'] + ) + .then(exportKey) + .then((keys) => ({ + privateKey: keys[0], + publicKey: keys[1] + })), callback) +} + +// Takes a jwk key +exports.unmarshalPrivateKey = function (key, callback) { + const privateKey = crypto.subtle.importKey( + 'jwk', + key, + { + name: 'RSASSA-PKCS1-v1_5', + hash: {name: 'SHA-256'} + }, + true, + ['sign'] + ) + + nodeify(Promise.all([ + privateKey, + derivePublicFromPrivate(key) + ]).then((keys) => exportKey({ + privateKey: keys[0], + publicKey: keys[1] + })).then((keys) => ({ + privateKey: keys[0], + publicKey: keys[1] + })), callback) +} + +exports.getRandomValues = function (arr) { + return Buffer.from(crypto.getRandomValues(arr)) +} + +exports.hashAndSign = function (key, msg, callback) { + nodeify(crypto.subtle.importKey( + 'jwk', + key, + { + name: 'RSASSA-PKCS1-v1_5', + hash: {name: 'SHA-256'} + }, + false, + ['sign'] + ).then((privateKey) => { + return crypto.subtle.sign( + {name: 'RSASSA-PKCS1-v1_5'}, + privateKey, + Uint8Array.from(msg) + ) + }).then((sig) => Buffer.from(sig)), callback) +} + +exports.hashAndVerify = function (key, sig, msg, callback) { + nodeify(crypto.subtle.importKey( + 'jwk', + key, + { + name: 'RSASSA-PKCS1-v1_5', + hash: {name: 'SHA-256'} + }, + false, + ['verify'] + ).then((publicKey) => { + return crypto.subtle.verify( + {name: 'RSASSA-PKCS1-v1_5'}, + publicKey, + sig, + msg + ) + }), callback) +} + +function exportKey (pair) { + return Promise.all([ + crypto.subtle.exportKey('jwk', pair.privateKey), + crypto.subtle.exportKey('jwk', pair.publicKey) + ]) +} + +function derivePublicFromPrivate (jwKey) { + return crypto.subtle.importKey( + 'jwk', + { + kty: jwKey.kty, + n: jwKey.n, + e: jwKey.e, + alg: jwKey.alg, + kid: jwKey.kid + }, + { + name: 'RSASSA-PKCS1-v1_5', + hash: {name: 'SHA-256'} + }, + true, + ['verify'] + ) +} diff --git a/src/crypto/rsa-utils.js b/src/crypto/rsa-utils.js new file mode 100644 index 0000000..65c2c1e --- /dev/null +++ b/src/crypto/rsa-utils.js @@ -0,0 +1,114 @@ +'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() + ) +}) + +// Convert a PKCS#1 in ASN1 DER format to a JWK key +exports.pkcs1ToJwk = function (bytes) { + const asn1 = RSAPrivateKey.decode(bytes, 'der') + + 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), + 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') +} + +// 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') + + return { + kty: 'RSA', + n: toBase64(asn1.modulus), + e: toBase64(asn1.publicExponent), + 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') +} diff --git a/src/crypto/rsa.js b/src/crypto/rsa.js index 85704be..835b4b9 100644 --- a/src/crypto/rsa.js +++ b/src/crypto/rsa.js @@ -1,228 +1,80 @@ 'use strict' -const nodeify = require('nodeify') -const asn1 = require('asn1.js') +// Node.js land +// First we look if node-webrypto-ossl is available +// otherwise we fall back to using keypair + node core -const util = require('./util') -const toBase64 = util.toBase64 -const toBn = util.toBn -const crypto = require('./webcrypto')() - -exports.generateKey = function (bits, callback) { - nodeify(crypto.subtle.generateKey( - { - name: 'RSASSA-PKCS1-v1_5', - modulusLength: bits, - publicExponent: new Uint8Array([0x01, 0x00, 0x01]), - hash: {name: 'SHA-256'} - }, - true, - ['sign', 'verify'] - ) - .then(exportKey) - .then((keys) => ({ - privateKey: keys[0], - publicKey: keys[1] - })), callback) +let webcrypto +try { + webcrypto = require('node-webcrypto-ossl') +} catch (err) { + // not available, use the code below } -// Takes a jwk key -exports.unmarshalPrivateKey = function (key, callback) { - const privateKey = crypto.subtle.importKey( - 'jwk', - key, - { - name: 'RSASSA-PKCS1-v1_5', - hash: {name: 'SHA-256'} - }, - true, - ['sign'] - ) +if (webcrypto && !process.env.NO_WEBCRYPTO) { + module.exports = require('./rsa-browser') +} else { + const crypto = require('crypto') + const keypair = require('keypair') + const setImmediate = require('async/setImmediate') + const pemToJwk = require('pem-jwk').pem2jwk + const jwkToPem = require('pem-jwk').jwk2pem - nodeify(Promise.all([ - privateKey, - derivePublicFromPrivate(key) - ]).then((keys) => exportKey({ - privateKey: keys[0], - publicKey: keys[1] - })).then((keys) => ({ - privateKey: keys[0], - publicKey: keys[1] - })), callback) -} + exports.utils = require('./rsa-utils') -exports.getRandomValues = function (arr) { - return Buffer.from(crypto.getRandomValues(arr)) -} + exports.generateKey = function (bits, callback) { + const done = (err, res) => setImmediate(() => { + callback(err, res) + }) -exports.hashAndSign = function (key, msg, callback) { - nodeify(crypto.subtle.importKey( - 'jwk', - key, - { - name: 'RSASSA-PKCS1-v1_5', - hash: {name: 'SHA-256'} - }, - false, - ['sign'] - ).then((privateKey) => { - return crypto.subtle.sign( - {name: 'RSASSA-PKCS1-v1_5'}, - privateKey, - Uint8Array.from(msg) - ) - }).then((sig) => Buffer.from(sig)), callback) -} - -exports.hashAndVerify = function (key, sig, msg, callback) { - nodeify(crypto.subtle.importKey( - 'jwk', - key, - { - name: 'RSASSA-PKCS1-v1_5', - hash: {name: 'SHA-256'} - }, - false, - ['verify'] - ).then((publicKey) => { - return crypto.subtle.verify( - {name: 'RSASSA-PKCS1-v1_5'}, - publicKey, - sig, - msg - ) - }), callback) -} - -function exportKey (pair) { - return Promise.all([ - crypto.subtle.exportKey('jwk', pair.privateKey), - crypto.subtle.exportKey('jwk', pair.publicKey) - ]) -} - -function derivePublicFromPrivate (jwKey) { - return crypto.subtle.importKey( - 'jwk', - { - kty: jwKey.kty, - n: jwKey.n, - e: jwKey.e, - alg: jwKey.alg, - kid: jwKey.kid - }, - { - name: 'RSASSA-PKCS1-v1_5', - hash: {name: 'SHA-256'} - }, - true, - ['verify'] - ) -} - -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() - ) -}) - -// Convert a PKCS#1 in ASN1 DER format to a JWK key -exports.pkcs1ToJwk = function (bytes) { - const asn1 = RSAPrivateKey.decode(bytes, 'der') - - 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), - 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') -} - -// 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') - - return { - kty: 'RSA', - n: toBase64(asn1.modulus), - e: toBase64(asn1.publicExponent), - 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') + let key + try { + key = keypair({ + bits: bits + }) + } catch (err) { + done(err) + return } - }, 'der') + + done(null, { + privateKey: pemToJwk(key.private), + publicKey: pemToJwk(key.public) + }) + } + + // Takes a jwk key + exports.unmarshalPrivateKey = function (key, callback) { + callback(null, { + privateKey: key, + publicKey: { + kty: key.kty, + n: key.n, + e: key.e + } + }) + } + + exports.getRandomValues = function (arr) { + return crypto.randomBytes(arr.length) + } + + exports.hashAndSign = function (key, msg, callback) { + const sign = crypto.createSign('RSA-SHA256') + + sign.update(msg) + setImmediate(() => { + callback(null, sign.sign(jwkToPem(key))) + }) + } + + exports.hashAndVerify = function (key, sig, msg, callback) { + const verify = crypto.createVerify('RSA-SHA256') + + verify.update(msg) + + setImmediate(() => { + callback(null, verify.verify(jwkToPem(key), sig)) + }) + } } diff --git a/src/crypto/webcrypto.js b/src/crypto/webcrypto.js index 3dae226..f664ad8 100644 --- a/src/crypto/webcrypto.js +++ b/src/crypto/webcrypto.js @@ -1,7 +1,11 @@ 'use strict' module.exports = function getWebCrypto () { - const WebCrypto = require('node-webcrypto-ossl') - const webCrypto = new WebCrypto() - return webCrypto + try { + const WebCrypto = require('node-webcrypto-ossl') + const webCrypto = new WebCrypto() + return webCrypto + } catch (err) { + // fallback to other things + } } diff --git a/src/keys/rsa.js b/src/keys/rsa.js index f35a931..cd8aabe 100644 --- a/src/keys/rsa.js +++ b/src/keys/rsa.js @@ -17,7 +17,7 @@ class RsaPublicKey { } marshal () { - return crypto.jwkToPkix(this._key) + return crypto.utils.jwkToPkix(this._key) } get bytes () { @@ -71,7 +71,7 @@ class RsaPrivateKey { } marshal () { - return crypto.jwkToPkcs1(this._key) + return crypto.utils.jwkToPkcs1(this._key) } get bytes () { @@ -92,7 +92,7 @@ class RsaPrivateKey { } function unmarshalRsaPrivateKey (bytes, callback) { - const jwk = crypto.pkcs1ToJwk(bytes) + const jwk = crypto.utils.pkcs1ToJwk(bytes) crypto.unmarshalPrivateKey(jwk, (err, keys) => { if (err) { return callback(err) @@ -103,7 +103,7 @@ function unmarshalRsaPrivateKey (bytes, callback) { } function unmarshalRsaPublicKey (bytes) { - const jwk = crypto.pkixToJwk(bytes) + const jwk = crypto.utils.pkixToJwk(bytes) return new RsaPublicKey(jwk) }