diff --git a/index.js b/index.js index c538483..a64d0cd 100644 --- a/index.js +++ b/index.js @@ -1,2 +1,2 @@ 'use strict'; -module.exports = require('./lib/rasha.js'); +module.exports = require('./lib/rsa.js'); diff --git a/lib/crypto.js b/lib/crypto.js new file mode 100644 index 0000000..46dab0d --- /dev/null +++ b/lib/crypto.js @@ -0,0 +1,51 @@ +'use strict'; +/*global Promise*/ + +var PEM = require('./pem.js'); +var x509 = require('./x509.js'); +var ASN1 = require('./asn1.js'); + +// Hacky-do, wrappy-do +module.exports.generate = function (opts) { + if (!opts) { opts = {}; } + return new Promise(function (resolve, reject) { + try { + var modlen = opts.modulusLength || 2048; + var exp = opts.publicExponent || 0x10001; + var pair = require('./generate-privkey.js')(modlen,exp); + if (pair.private) { resolve(pair); return; } + pair = toJwks(pair); + resolve({ private: pair.private , public: pair.public }); + } catch(e) { + reject(e); + } + }); +}; + +// PKCS1 to JWK only +function toJwks(oldpair) { + var block = PEM.parseBlock(oldpair.privateKeyPem); + var asn1 = ASN1.parse(block.bytes); + var jwk = { kty: 'RSA', n: null, e: null }; + jwk = x509.parsePkcs1(block.bytes, asn1, jwk); + return { private: jwk, public: neuter(jwk) }; +} + +// Copied from rsa.js to prevent circular dep +var privates = [ 'p', 'q', 'd', 'dp', 'dq', 'qi' ]; +function neuter(priv) { + var pub = {}; + Object.keys(priv).forEach(function (key) { + if (!privates.includes(key)) { + pub[key] = priv[key]; + } + }); + return pub; +} + +if (require.main === module) { + module.exports.generate().then(function (pair) { + console.info(JSON.stringify(pair.private, null, 2)); + console.info(JSON.stringify(pair.public, null, 2)); + }); +} diff --git a/lib/generate-privkey-forge.js b/lib/generate-privkey-forge.js new file mode 100644 index 0000000..55bd2e2 --- /dev/null +++ b/lib/generate-privkey-forge.js @@ -0,0 +1,53 @@ +// Copyright 2016-2018 AJ ONeal. All rights reserved +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +'use strict'; + +module.exports = function (bitlen, exp) { + var k = require('node-forge').pki.rsa + .generateKeyPair({ bits: bitlen || 2048, e: exp || 0x10001 }).privateKey; + var jwk = { + kty: "RSA" + , n: _toUrlBase64(k.n) + , e: _toUrlBase64(k.e) + , d: _toUrlBase64(k.d) + , p: _toUrlBase64(k.p) + , q: _toUrlBase64(k.q) + , dp: _toUrlBase64(k.dP) + , dq: _toUrlBase64(k.dQ) + , qi: _toUrlBase64(k.qInv) + }; + return { + private: jwk + , public: { + kty: jwk.kty + , n: jwk.n + , e: jwk.e + } + }; +}; + +function _toUrlBase64(fbn) { + var hex = fbn.toRadix(16); + if (hex.length % 2) { + // Invalid hex string + hex = '0' + hex; + } + while ('00' === hex.slice(0, 2)) { + hex = hex.slice(2); + } + return Buffer.from(hex, 'hex').toString('base64') + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g,"") + ; +} + +if (require.main === module) { + var keypair = module.exports(2048, 0x10001); + console.info(keypair.private); + console.warn(keypair.public); + //console.info(keypair.privateKeyJwk); + //console.warn(keypair.publicKeyJwk); +} diff --git a/lib/generate-privkey-node.js b/lib/generate-privkey-node.js new file mode 100644 index 0000000..d79f6e1 --- /dev/null +++ b/lib/generate-privkey-node.js @@ -0,0 +1,26 @@ +// Copyright 2016-2018 AJ ONeal. All rights reserved +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +'use strict'; + +module.exports = function (bitlen, exp) { + var keypair = require('crypto').generateKeyPairSync( + 'rsa' + , { modulusLength: bitlen + , publicExponent: exp + , privateKeyEncoding: { type: 'pkcs1', format: 'pem' } + , publicKeyEncoding: { type: 'pkcs1', format: 'pem' } + } + ); + var result = { privateKeyPem: keypair.privateKey.trim() }; + return result; +}; + +if (require.main === module) { + var keypair = module.exports(2048, 0x10001); + console.info(keypair.privateKeyPem); + console.warn(keypair.publicKeyPem); + //console.info(keypair.privateKeyJwk); + //console.warn(keypair.publicKeyJwk); +} diff --git a/lib/generate-privkey-ursa.js b/lib/generate-privkey-ursa.js new file mode 100644 index 0000000..01ef076 --- /dev/null +++ b/lib/generate-privkey-ursa.js @@ -0,0 +1,25 @@ +// Copyright 2016-2018 AJ ONeal. All rights reserved +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +'use strict'; + +module.exports = function (bitlen, exp) { + var ursa; + try { + ursa = require('ursa'); + } catch(e) { + ursa = require('ursa-optional'); + } + var keypair = ursa.generatePrivateKey(bitlen, exp); + var result = { privateKeyPem: keypair.toPrivatePem().toString('ascii').trim() }; + return result; +}; + +if (require.main === module) { + var keypair = module.exports(2048, 0x10001); + console.info(keypair.privateKeyPem); + console.warn(keypair.publicKeyPem); + //console.info(keypair.privateKeyJwk); + //console.warn(keypair.publicKeyJwk); +} diff --git a/lib/generate-privkey.js b/lib/generate-privkey.js new file mode 100644 index 0000000..0304024 --- /dev/null +++ b/lib/generate-privkey.js @@ -0,0 +1,67 @@ +// Copyright 2016-2018 AJ ONeal. All rights reserved +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +'use strict'; + +var oldver = false; + +module.exports = function (bitlen, exp) { + bitlen = parseInt(bitlen, 10) || 2048; + exp = parseInt(exp, 10) || 65537; + + try { + return require('./generate-privkey-node.js')(bitlen, exp); + } catch(e) { + if (!/generateKeyPairSync is not a function/.test(e.message)) { + throw e; + } + try { + return require('./generate-privkey-ursa.js')(bitlen, exp); + } catch(e) { + if (e.code !== 'MODULE_NOT_FOUND') { + console.error("[rsa-compat] Unexpected error when using 'ursa':"); + console.error(e); + } + if (!oldver) { + oldver = true; + console.warn("[WARN] rsa-compat: Your version of node does not have crypto.generateKeyPair()"); + console.warn("[WARN] rsa-compat: Please update to node >= v10.12 or 'npm install --save ursa node-forge'"); + console.warn("[WARN] rsa-compat: Using node-forge as a fallback may be unacceptably slow."); + if (/arm|mips/i.test(require('os').arch)) { + console.warn("================================================================"); + console.warn(" WARNING"); + console.warn("================================================================"); + console.warn(""); + console.warn("WARNING: You are generating an RSA key using pure JavaScript on"); + console.warn(" a VERY SLOW cpu. This could take DOZENS of minutes!"); + console.warn(""); + console.warn(" We recommend installing node >= v10.12, or 'gcc' and 'ursa'"); + console.warn(""); + console.warn("EXAMPLE:"); + console.warn(""); + console.warn(" sudo apt-get install build-essential && npm install ursa"); + console.warn(""); + console.warn("================================================================"); + } + } + try { + return require('./generate-privkey-forge.js')(bitlen, exp); + } catch(e) { + if (e.code !== 'MODULE_NOT_FOUND') { + throw e; + } + console.error("[ERROR] rsa-compat: could not generate a private key."); + console.error("None of crypto.generateKeyPair, ursa, nor node-forge are present"); + } + } + } +}; + +if (require.main === module) { + var keypair = module.exports(2048, 0x10001); + console.info(keypair.privateKeyPem); + console.warn(keypair.publicKeyPem); + //console.info(keypair.privateKeyJwk); + //console.warn(keypair.publicKeyJwk); +} diff --git a/lib/rasha.js b/lib/rsa.js similarity index 79% rename from lib/rasha.js rename to lib/rsa.js index d16e610..315fcb5 100644 --- a/lib/rasha.js +++ b/lib/rsa.js @@ -6,19 +6,30 @@ var PEM = require('./pem.js'); var x509 = require('./x509.js'); var ASN1 = require('./asn1.js'); var Enc = require('./encoding.js'); +var Crypto = require('./crypto.js'); /*global Promise*/ RSA.generate = function (opts) { - return Promise.resolve().then(function () { - var typ = 'rsa'; + opts.kty = "RSA"; + return Crypto.generate(opts).then(function (pair) { var format = opts.format; var encoding = opts.encoding; - var priv; - var pub; - if (!format) { + // The easy way + if ('json' === format && !encoding) { format = 'jwk'; + encoding = 'json'; + } + if (('jwk' === format || !format) && ('json' === encoding || !encoding)) { return pair; } + if ('jwk' === format || 'json' === encoding) { + throw new Error("format '" + format + "' is incompatible with encoding '" + encoding + "'"); } + + // The... less easy way + /* + var priv; + var pub; + if ('spki' === format || 'pkcs8' === format) { format = 'pkcs8'; pub = 'spki'; @@ -32,13 +43,8 @@ RSA.generate = function (opts) { encoding = 'der'; } - if ('jwk' === format || 'json' === format) { - format = 'jwk'; - encoding = 'json'; - } else { - priv = format; - pub = pub || format; - } + priv = format; + pub = pub || format; if (!encoding) { encoding = 'pem'; @@ -52,29 +58,19 @@ RSA.generate = function (opts) { priv = { type: 'pkcs1', format: 'pem' }; pub = { type: 'pkcs1', format: 'pem' }; } + */ + if (('pem' === format || 'der' === format) && !encoding) { + encoding = format; + format = 'pkcs1'; + } - return new Promise(function (resolve, reject) { - return require('crypto').generateKeyPair(typ, { - modulusLength: opts.modulusLength || 2048 - , publicExponent: opts.publicExponent || 0x10001 - , privateKeyEncoding: priv - , publicKeyEncoding: pub - }, function (err, pubkey, privkey) { - if (err) { reject(err); } - resolve({ - private: privkey - , public: pubkey - }); + var exOpts = { jwk: pair.private, format: format, encoding: encoding }; + return RSA.export(exOpts).then(function (priv) { + exOpts.public = true; + if ('pkcs8' === exOpts.format) { exOpts.format = 'spki'; } + return RSA.export(exOpts).then(function (pub) { + return { private: priv, public: pub }; }); - }).then(function (keypair) { - if ('jwk' !== format) { - return keypair; - } - - return { - private: RSA.importSync({ pem: keypair.private, format: priv.type }) - , public: RSA.importSync({ pem: keypair.public, format: pub.type, public: true }) - }; }); }); }; @@ -102,7 +98,7 @@ RSA.importSync = function (opts) { } if (opts.public) { - jwk = RSA.nueter(jwk); + jwk = RSA.neuter(jwk); } return jwk; }; @@ -139,7 +135,7 @@ RSA.exportSync = function (opts) { var format = opts.format; var pub = opts.public; if (pub || -1 !== [ 'spki', 'pkix', 'ssh', 'rfc4716' ].indexOf(format)) { - jwk = RSA.nueter(jwk); + jwk = RSA.neuter(jwk); } if ('RSA' !== jwk.kty) { throw new Error("options.jwk.kty must be 'RSA' for RSA keys"); @@ -193,15 +189,16 @@ RSA.pack = function (opts) { RSA.toPem = RSA.export = RSA.pack; // snip the _private_ parts... hAHAHAHA! -RSA.nueter = function (jwk) { - // (snip rather than new object to keep potential extra data) - // otherwise we could just do this: - // return { kty: jwk.kty, n: jwk.n, e: jwk.e }; - [ 'p', 'q', 'd', 'dp', 'dq', 'qi' ].forEach(function (key) { - if (key in jwk) { jwk[key] = undefined; } - return jwk; +var privates = [ 'p', 'q', 'd', 'dp', 'dq', 'qi' ]; +// fix misspelling without breaking the API +RSA.neuter = RSA.nueter = function (priv) { + var pub = {}; + Object.keys(priv).forEach(function (key) { + if (!privates.includes(key)) { + pub[key] = priv[key]; + } }); - return jwk; + return pub; }; RSA.__thumbprint = function (jwk) { diff --git a/package.json b/package.json index 0eddaf0..b72b473 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rasha", - "version": "1.2.5", + "version": "1.3.0", "description": "💯 PEM-to-JWK and JWK-to-PEM for RSA keys in a lightweight, zero-dependency library focused on perfect universal compatibility.", "homepage": "https://git.coolaj86.com/coolaj86/rasha.js", "main": "index.js", @@ -35,5 +35,8 @@ "PEM-to-SSH" ], "author": "AJ ONeal (https://coolaj86.com/)", - "license": "MPL-2.0" + "license": "MPL-2.0", + "trulyOptionalDependencies": { + "node-forge": "^0.8.2" + } } diff --git a/test.sh b/test.sh index 56ab318..afdec5e 100755 --- a/test.sh +++ b/test.sh @@ -123,13 +123,21 @@ jwktopem "" echo "" echo "testing node key generation" +echo "defaults" node bin/rasha.js > /dev/null +echo "jwk" node bin/rasha.js jwk > /dev/null +echo "json 2048" node bin/rasha.js json 2048 > /dev/null +echo "der" node bin/rasha.js der > /dev/null +echo "pkcs8 der" node bin/rasha.js pkcs8 der > /dev/null +echo "pem" node bin/rasha.js pem > /dev/null +echo "pkcs1" node bin/rasha.js pkcs1 pem > /dev/null +echo "spki" node bin/rasha.js spki > /dev/null echo "PASS"