From 34d0ca9f133a90660184b7ec823bfad21fdecb79 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 7 Mar 2019 00:04:55 -0700 Subject: [PATCH] v1.2.6: Make neuter public, replace bin with installer --- bin/keypairs.js | 615 +----------------------------------------------- keypairs.js | 7 +- package.json | 4 +- 3 files changed, 15 insertions(+), 611 deletions(-) diff --git a/bin/keypairs.js b/bin/keypairs.js index 8116a3e..1acc2f4 100755 --- a/bin/keypairs.js +++ b/bin/keypairs.js @@ -1,611 +1,12 @@ #!/usr/bin/env node 'use strict'; -// I'm not proud of the way this code is written - it snowballed from a thought -// experiment into a full-fledged CLI, literally overnight (as it it's 4:30am -// right now), but I love what it accomplishes! - -/*global Promise*/ -var fs = require('fs'); -var Rasha = require('rasha'); -var Eckles = require('eckles'); -var Keypairs = require('../'); -var pkg = require('../package.json'); - -var args = process.argv.slice(2); -var opts = { keys: [], jwts: [], jwss: [], payloads: [], names: [], filenames: [], files: [] }; -var conflicts = { - 'namedCurve': 'modulusLength' -, 'public': 'private' -}; -Object.keys(conflicts).forEach(function (k) { - conflicts[conflicts[k]] = k; +var cmd = "npm install --global keypairs-cli"; +console.error(cmd); +require('child_process').exec(cmd, function (err) { + if (err) { + console.error(err); + return; + } + console.info("Run 'keypairs help' to see what you can do!"); }); -function set(key, val) { - if (opts[conflicts[key]]) { - console.error("cannot set '" + key + "' to '" + val + "': '" + conflicts[key] + "' already set as '" + opts[conflicts[key]] + "'"); - process.exit(1); - } - if (opts[key]) { - console.error("cannot set '" + key + "' to '" + val + "': already set as '" + opts[key] + "'"); - process.exit(1); - } - opts[key] = val; -} - -// duck type all the things -// TODO segment off by actions (gen, sign, verify) and allow parse/convert or gen before sign -args.forEach(function (arg) { - var larg = arg.toLowerCase().replace(/[^\w]/g, ''); - var narg = parseInt(arg, 10) || 0; - if (narg.toString() !== arg) { - // i.e. 2048.pem is a valid file name - narg = false; - } - - if ('version' === arg) { - console.info(pkg.name, 'v' + pkg.version); - process.exit(0); - } - - if (setTimes(arg)) { - return; - } - if (setIssuer(arg)) { - return; - } - if (setSubject(arg)) { - return; - } - - if ('ecdsa' === larg || 'ec' === larg) { - set('kty', "EC"); - if (opts.modulusLength) { - console.error("EC keys do not have bit lengths such as '" + opts.modulusLength + "'. Choose either the P-256 or P-384 'curve' instead."); - process.exit(1); - } - } - if ('rsa' === larg) { - set('kty', "RSA"); - if (opts.namedCurve) { - console.error("RSA keys do not have curves such as '" + opts.namedCurve + "'. Choose a modulus bit length, such as 2048 instead."); - process.exit(1); - } - return; - } - - // P-384 - if (-1 !== ['256', 'p256', 'prime256v1', 'secp256r1'].indexOf(larg)) { - set('namedCurve', "P-256"); - return; - } - - // P-384 - if (-1 !== ['384', 'p384', 'secp384r1'].indexOf(larg)) { - set('namedCurve', "P-384"); - return; - } - - // RSA Modulus Length - if (narg) { - if (narg < 2048 || narg % 8 || narg > 8192) { - console.error("RSA modulusLength must be >=2048, <=8192 and divisible by 8"); - process.exit(1); - } - set('modulusLength', narg); - return; - } - - // Booleans - if (-1 !== [ 'private', 'public', 'nocompact', 'nofetch', 'debug', 'overwrite' ].indexOf(arg)) { - set(arg, true); - return; - } - if ('uncompressed' === arg) { - set('uncompressed', true); - return; - } - if (-1 !== [ 'gen', 'sign', 'verify', 'decode' ].indexOf(arg)) { - set('action', arg); - return; - } - - // Key format and encoding - if (-1 !== [ 'spki', 'pkix' ].indexOf(larg)) { - set('pubFormat', 'spki'); - return; - } - // TODO add ssh private key support (it's already built in jwk-to-ssh) - if ('ssh' === larg) { - set('pubFormat', 'ssh'); - return; - } - if (-1 !== [ 'openssh', 'sec1', 'pkcs1', 'pkcs8' ].indexOf(larg)) { - // pkcs1 can be public or private, it's ambiguous - if (!opts.privFormat) { - set('privFormat', larg); - return; - } - - if ('pkcs1' === larg || 'ssh' === larg) { - set('pubFormat', larg); - return; - } - if ('openssh' === larg) { - console.warn("specifying 'openssh' twice? ...assuming that you meant 'ssh'"); - set('pubFormat', 'ssh'); - return; - } - if ('pkcs8' === larg) { - console.warn("specifying 'pkcs8' twice? ...assuming that you meant 'spki' (pkix)"); - set('pubFormat', 'spki'); - return; - } - if ('sec1' === larg) { - console.warn("specifying 'sec1' twice? ...assuming that you meant 'spki' (pkix)"); - set('pubFormat', 'spki'); - return; - } - return; - } - if ('jwk' === larg) { - if (!opts.privFormat) { - set('privFormat', larg); - } else { - set('pubFormat', larg); - } - return; - } - if ('pem' === larg || 'der' === larg || 'json' === larg) { - if (!opts.privEncoding) { - set('privEncoding', larg); - } else { - set('pubEncoding', larg); - } - return; - } - - // Filename - try { - fs.accessSync(arg); - opts.filenames.push(arg); - opts.names.push({ taken: true, name: arg }); - if (!guessFile(arg)) { - opts.files.push(arg); - } - return; - } catch(e) { /* not keypath */ } - - // Test for JWK-ness / payload-ness - if (guess(arg)) { - return; - } - - // Test for JWT-ness - if (setJwt(arg)) { - return; - } - - // Possibly the output file - if (opts.names.length < 3) { - opts.names.push({ taken: false, name: arg }); - return; - } - // check if it's a valid output key - - console.error("too many arguments or didn't understand argument '" + arg + "'"); - if (opts.debug) { - console.warn(opts); - } - process.exit(1); -}); - -function guessFile(filename) { - try { - // TODO der support - var txt = fs.readFileSync(filename).toString('utf8'); - return guess(txt, filename); - } catch(e) { - return false; - } -} - -function guess(txt, filename) { - try { - var json = JSON.parse(txt); - if (-1 !== [ 'RSA', 'EC' ].indexOf(json.kty)) { - opts.keys.push({ raw: txt, jwk: json, filename: filename }); - return true; - } else if (json.signature && json.payload && (json.header || json.protected)) { - opts.jwss.push(json); - return true; - } else { - opts.payloads.push(txt); - return true; - } - } catch(e) { - try { - var jwk = Eckles.importSync({ pem: txt }); - // pem._string = txt; - opts.keys.push({ jwk: jwk, pem: true, raw: txt }); - return true; - } catch(e) { - try { - var jwk = Rasha.importSync({ pem: txt }); - // pem._string = txt; - opts.keys.push({ jwk: jwk, pem: true, raw: txt }); - return true; - } catch(e) { - // ignore - } - } - } - return false; -} - -// node bin/keypairs.js debug spki pem json pkcs1 ~/.ssh/id_rsa.pub foo.pem bar.pem 'abc.abc.abc' '{"kty":"EC"}' '{}' '{"signature":"x", "payload":"x", "header":"x"}' '{"signature":"x", "payload":"x", "protected":"x"}' verify -if (opts.debug) { - console.warn(opts); -} - -var kp; - -if ('gen' === opts.action || (!opts.action && !opts.names.length)) { - if (opts.names.length > 2) { - console.error("there should only be two output files at most when generating keypairs"); - console.error(opts.names.map(function (t) { return t.name; })); - process.exit(1); - return; - } - - kp = genKeypair(); -} else if ('decode' === opts.action) { - if (!opts.jwts.length) { - console.error("no JWTs specified to decode"); - process.exit(1); - return; - } - - return Promise.all(opts.jwts.map(function (jwt, i) { - try { - var decoded = decodeJwt(jwt); - console.info("Decoded #" + (i + 1) + ":"); - console.info(JSON.stringify(decoded, null, 2)); - } catch(e) { - console.error("Failed to decode #" + (i + 1) + ":"); - console.error(e); - } - })); -} else if ('verify' === opts.action || (!opts.action && opts.jwts.length)) { - if (!opts.jwts.length) { - console.error("no JWTs specified to verify"); - process.exit(1); - return; - } - - return Promise.all(opts.jwts.map(function (jwt, i) { - return require('keyfetch').verify({ jwt: jwt }).then(function (decoded) { - console.info("Verified #" + (i + 1) + ":"); - console.info(JSON.stringify(decoded, null, 2)); - }).catch(function (err) { - console.error("Failed to verify #" + (i + 1) + ":"); - console.error(err); - }); - })); -} else { - if (opts.names.length > 3) { - console.error("there should only be one input file and up to two output files when converting keypairs"); - console.error(opts.names.map(function (t) { return t.name; })); - process.exit(1); - return; - } - var pair = readKeypair(); - pair._convert = true; - kp = Promise.resolve(pair); -} - -if ('sign' === opts.action) { - return kp.then(function (pair) { - var jwk = pair.private; - if (!jwk || !jwk.d) { - console.error("the first key was not a private key"); - console.error(opts.names.map(function (t) { return t.name; })); - process.exit(1); - return; - } - if (!opts.payloads.length) { - opts.payloads.push('{}'); - } - return Promise.all(opts.payloads.map(function (payload) { - var claims = JSON.parse(payload); - if (!claims.iss) { claims.iss = opts.issuer; } - if (!claims.iss) { console.warn("No issuer given, token will not be verifiable"); } - if (!claims.sub) { claims.sub = opts.sub; } - if (!claims.exp) { - if (!opts.expiresAt) { setTimes('15m'); } - claims.exp = opts.expiresAt; - } - if (!claims.iat) { claims.iat = opts.issuedAt; } - if (!claims.nbf) { claims.nbf = opts.nbf; } - return Keypairs.signJwt({ jwk: pair.private, claims: claims }).then(function (jwt) { - console.info(jwt); - }); - })); - }); -} else { - return kp.then(function (pair) { - if (pair._convert) { - return convertKeypair(pair); - } - }); -} - -function readKeypair() { - // note that the jwk may be a string - var keyopts = opts.keys.shift(); - var jwk = keyopts && keyopts.jwk; - if (!jwk) { - console.error("no keys could be parsed from the given arguments"); - console.error(opts.names.map(function (t) { return t.name; })); - process.exit(1); - return; - } - - // omit the primary private key from the list of actual (or soon-to-be) files - if (keyopts.filename) { - opts.names = opts.names.filter(function (name) { - return name.name !== keyopts.filename; - }); - } - - var pair = { private: null, public: null, pem: keyopts.pem, raw: keyopts.raw }; - if (jwk.d) { - pair.private = jwk; - } - pair.public = Keypairs._neuter({ jwk: jwk }); - return pair; -} - -// Note: some of the conditions can be factored out -// this was all built in high-speed iterative during the 3ams+ -function convertKeypair(pair) { - //var pair = readKeypair(); - - var ps = []; - // if it's private only, or if it's not public-only, produce the private key - if (pair.private || !opts.public) { - // if it came from pem (or is explicitly json), it should go to jwk - // otherwise, if it came from jwk, it should go to pem - if (((!opts.privEncoding && pair.pem) || 'json' === opts.privEncoding) - && ((!opts.privFormat && pair.pem) || 'jwk' === opts.privFormat)) { - ps.push(Promise.resolve(pair.private)); - } else { - ps.push(Keypairs.export({ jwk: pair.private, format: opts.privFormat, encoding: opts.privEncoding })); - } - } - - // if it's not private key only, we want to produce the public key - if (!opts.private) { - if (opts.public) { - // if it's public-only the ambigious options will fall to the private key - // so we need to fix that - if (!opts.pubFormat) { opts.pubFormat = opts.privFormat; } - if (!opts.pubEncoding) { opts.pubEncoding = opts.privEncoding; } - } - - // same as above - swap formats by default - if (((!opts.pubEncoding && pair.pem) || 'json' === opts.pubEncoding) - && ((!opts.pubFormat && pair.pem) || 'jwk' === opts.pubFormat)) { - ps.push(Promise.resolve(pair.public)); - } else { - ps.push(Keypairs.export({ jwk: pair.public, format: opts.pubFormat, encoding: opts.pubEncoding, public: true })); - } - } - - return Promise.all(ps).then(function (exported) { - // start with the first key, annotating if it should be public - var index = 0; - var key = stringifyIfJson(index, opts.public); - - // re: opts.names - // if we're only doing the public key we can end early - // (if the source key was from a file and was in opts.names, - // we're safe here because we already removed it earlier) - - if (opts.public) { - if (opts.names.length) { - writeFile(opts.names[index].name, key, !opts.public); - } else { - // output public keys to stderr - printPublic(key); - } - // end <-- we're not outputting other keys - return; - } - - // private key stuff - if (opts.names.length >= 1) { - writeFile(opts.names[index].name, key, true); - } else { - printPrivate(key); - } - - // pub key stuff - // we have to output the private key, - // but the public key can be derived at any time - // so we don't need to put the same noise to the screen - if (!opts.private && opts.names.length >= 2) { - index = 1; - key = stringifyIfJson(index, false); - writeFile(opts.names[index].name, key, false); - } - - return pair; - - function stringifyIfJson(i, pub) { - if (exported[i].kty) { - if (pub) { - if (opts.expiresAt) { exported[i].exp = opts.expiresAt; } - exported[i].use = "sig"; - } - exported[i] = JSON.stringify(exported[i]); - } - return exported[i]; - } - }); -} - -function genKeypair() { - return Keypairs.generate({ - kty: opts.kty - , modulusLength: opts.modulusLength - , namedCurve: opts.namedCurve - }).then(function (pair) { - // always generate as jwk by default - var ps = []; - if ((!opts.privEncoding || 'json' === opts.privEncoding) && (!opts.privFormat || 'jwk' === opts.privFormat)) { - ps.push(Promise.resolve(pair.private)); - } else { - ps.push(Keypairs.export({ jwk: pair.private, format: opts.privFormat, encoding: opts.privEncoding })); - } - if ((!opts.pubEncoding || 'json' === opts.pubEncoding) && (!opts.pubFormat || 'jwk' === opts.pubFormat)) { - ps.push(Promise.resolve(pair.public)); - } else { - ps.push(Keypairs.export({ jwk: pair.public, format: opts.pubFormat, encoding: opts.pubEncoding, public: true })); - } - return Promise.all(ps).then(function (arr) { - if (arr[0].kty) { - arr[0] = JSON.stringify(arr[0]); - } - if (arr[1].kty) { - if (opts.expiresAt) { arr[1].exp = opts.expiresAt; } - arr[1].use = "sig"; - arr[1] = JSON.stringify(arr[1]); - } - if (!opts.names.length) { - console.info(arr[0] + "\n"); - console.warn(arr[1] + "\n"); - } - if (opts.names.length >= 1) { - writeFile(opts.names[0].name, arr[0], true); - if (!opts.private && opts.names.length >= 2) { - writeFile(opts.names[1].name, arr[1]); - } - } - - return pair; - }); - }); -} - -function writeFile(name, key, priv) { - var overwrite; - try { - fs.accessSync(name); - overwrite = opts.overwrite; - if (!opts.overwrite) { - if (priv) { - // output private keys to stdout - console.info(key + "\n"); - } else { - // output public keys to stderr - console.warn(key + "\n"); - } - console.error("'" + name + "' exists! force overwrite with 'overwrite'"); - process.exit(1); - return; - } - } catch(e) { - // the file does not exist (or cannot be accessed) - } - fs.writeFileSync(name, key); - if (overwrite) { - console.info("Overwrote " + (priv ? "private" : "public") + " key at '" + name + "'"); - } else { - console.info("Wrote " + (priv ? "private" : "public") + " key to '" + name + "'"); - } -} - -function setJwt(arg) { - try { - var jwt = arg.match(/^([\w-]+)\.([\w-]+)\.([\w-]+)$/); - // make sure header is a JWT header - JSON.parse(Buffer.from(jwt[1], 'base64')); - opts.jwts.push(arg); - return true; - } catch(e) { - // ignore - } -} - -function setSubject(arg) { - if (!/.+@[a-z0-9_-]+\.[a-z0-9_-]+/i.test(arg)) { - return false; - } - - opts.subject = arg; - return false; -} - -function setIssuer(arg) { - if (!/^https?:\/\/[a-z0-9_-]+\.[a-z0-9_-]+/i.test(arg)) { - return false; - } - - try { - new URL(arg); - opts.issuer = arg.replace(/\/$/, ''); - return true; - } catch(e) { - } - return false; -} - -function setTimes(arg) { - var t = arg.match(/^(\-?\d+)([dhms])$/i); - if (!t || !t[0]) { - return false; - } - - var num = parseInt(t[1], 10); - var unit = t[2]; - var mult = 1; - opts.issuedAt = Math.round(Date.now()/1000); - switch(unit) { - // fancy fallthrough, what fun! - case 'd': - mult *= 24; - /*falls through*/ - case 'h': - mult *= 60; - /*falls through*/ - case 'm': - mult *= 60; - /*falls through*/ - case 's': - mult *= 1; - } - if (!opts.expiresIn) { - opts.expiresIn = mult * num; - opts.expiresAt = opts.issuedAt + opts.expiresIn; - } else { - opts.nbf = opts.issuedAt + (mult * num); - } - return true; -} - -function decodeJwt(jwt) { - var parts = jwt.split('.'); - return { - header: JSON.parse(Buffer.from(parts[0], 'base64')) - , payload: JSON.parse(Buffer.from(parts[1], 'base64')) - , signature: parts[2] //Buffer.from(parts[2], 'base64') - }; -} - -function printPrivate(key) { - console.info(key + "\n"); -} -function printPublic(key) { - console.warn(key + "\n"); -} diff --git a/keypairs.js b/keypairs.js index 2b8d85e..c38ed10 100644 --- a/keypairs.js +++ b/keypairs.js @@ -87,7 +87,9 @@ Keypairs.export = function (opts) { }); }; -Keypairs._neuter = function (opts) { +// Chopping off the private parts is now part of the public API. +// I thought it sounded a little too crude at first, but it really is the best name in every possible way. +Keypairs.neuter = Keypairs._neuter = function (opts) { // trying to find the best balance of an immutable copy with custom attributes var jwk = {}; Object.keys(opts.jwk).forEach(function (k) { @@ -97,10 +99,11 @@ Keypairs._neuter = function (opts) { }); return jwk; }; + Keypairs.publish = function (opts) { if ('object' !== typeof opts.jwk || !opts.jwk.kty) { throw new Error("invalid jwk: " + JSON.stringify(opts.jwk)); } - var jwk = Keypairs._neuter(opts); + var jwk = Keypairs.neuter(opts); if (!jwk.exp) { if (opts.expiresIn) { jwk.exp = Math.round(Date.now()/1000) + opts.expiresIn; } diff --git a/package.json b/package.json index 0f82710..6b309f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "keypairs", - "version": "1.2.5", + "version": "1.2.6", "description": "Lightweight RSA/ECDSA keypair generation and JWK <-> PEM", "main": "keypairs.js", "files": [ @@ -11,7 +11,7 @@ "test": "node test.js" }, "bin": { - "keypairs": "bin/keypairs.js" + "keypairs-install": "bin/keypairs.js" }, "repository": { "type": "git",