commit 99b53c3d97bc1114af1f9cb771eb342ecd86d303 Author: AJ ONeal Date: Sun Dec 2 00:50:49 2018 -0700 v1.0.0: pack EC and RSA ssh public keys diff --git a/README.md b/README.md new file mode 100644 index 0000000..2840fb7 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# JWK to SSH (for node.js) + +A minimal library to encode a JWK +as an SSH public key (`id_rsa.pub`). + +Works for RSA and ECDSA public keys. + +Features +======== + +< 75 lines of code | < 0.7kb gzipped | 1.5kb minified | 2.1kb with comments + +* [x] SSH Public Keys +* [x] RSA Public Keys +* [x] EC Public Keys + * P-256 (prime256v1, secp256r1) + * P-384 (secp384r1) +* [x] Browser Version + * [Bluecrypt JWK to SSH](https://git.coolaj86.com/coolaj86/bluecrypt-jwk-to-ssh.js) + +### Need JWK to SSH? PEM to SSH? + +Try one of these: + +* [jwk-to-ssh.js](https://git.coolaj86.com/coolaj86/jwk-to-ssh.js) (RSA + EC) +* [Eckles.js](https://git.coolaj86.com/coolaj86/eckles.js) (more EC utils) +* [Rasha.js](https://git.coolaj86.com/coolaj86/eckles.js) (more RSA utils) + +### Need SSH Private Keys? + +SSH private keys are just normal PEM files, +so you can use Eckles or Rasha, as mentioned above. + +# CLI + +You can install `jwk-to-ssh` and use it from command line: + +```bash +npm install -g jwk-to-ssh +``` + +```bash +jwk-to-ssh pubkey.jwk.json +``` + +# Usage + +You can also use it from JavaScript: + +```js +var fs = require('fs'); +var jwktossh = require('jwk-to-ssh'); + +var jwk = JSON.parse(fs.readFileSync("./pubkey.jwk.json")); +var pub = jwktossh.pack({ + jwk: jwk +, comment: 'root@localhost' +}); + +console.info(pub); +``` + +Legal +----- + +[jwk-to-ssh.js](https://git.coolaj86.com/coolaj86/jwk-to-ssh.js) | +MPL-2.0 | +[Terms of Use](https://therootcompany.com/legal/#terms) | +[Privacy Policy](https://therootcompany.com/legal/#privacy) diff --git a/bin/ssh-to-jwk.js b/bin/ssh-to-jwk.js new file mode 100755 index 0000000..9493044 --- /dev/null +++ b/bin/ssh-to-jwk.js @@ -0,0 +1,18 @@ +#!/usr/bin/env node +'use strict'; + +var path = require('path'); +var jwktossh = require('../index.js'); + +var pubfile = process.argv[2]; + +if (!pubfile) { + console.error("specify a path to JWK"); + process.exit(1); +} + +var jwk = require(path.join(process.cwd(), pubfile)); +var comment = process.argv[3] || 'root@localhost'; +var pub = jwktossh.pack({ jwk: jwk, comment: comment }); + +console.info(pub); diff --git a/index.js b/index.js new file mode 100644 index 0000000..f698889 --- /dev/null +++ b/index.js @@ -0,0 +1,2 @@ +'use strict'; +module.exports = require('./lib/ssh-packer.js'); diff --git a/lib/encoding.js b/lib/encoding.js new file mode 100644 index 0000000..a369e6c --- /dev/null +++ b/lib/encoding.js @@ -0,0 +1,15 @@ +'use strict'; + +var Enc = module.exports; + +Enc.base64ToHex = function (b64) { + return Buffer.from(b64, 'base64').toString('hex'); +}; + +Enc.binToHex = function (bin) { + return Buffer.from(bin, 'binary').toString('hex'); +}; + +Enc.hexToBase64 = function (hex) { + return Buffer.from(hex, 'hex').toString('base64'); +}; diff --git a/lib/ssh-packer.js b/lib/ssh-packer.js new file mode 100644 index 0000000..d6470b0 --- /dev/null +++ b/lib/ssh-packer.js @@ -0,0 +1,76 @@ +'use strict'; + +var Enc = require('./encoding.js'); +var SSH = module.exports; + +SSH.pack = function (opts) { + var jwk = opts.jwk; + var els = []; + var ssh = { + type: '' + , _elements: els + , comment: opts.comment || '' + }; + var len; + + if ("RSA" === jwk.kty) { + ssh.type = 'ssh-rsa'; + els.push(Enc.binToHex(ssh.type)); + els.push(SSH._padRsa(Enc.base64ToHex(jwk.e))); + els.push(SSH._padRsa(Enc.base64ToHex(jwk.n))); + return SSH._packElements(ssh); + } + + if ("P-256" === jwk.crv) { + ssh.type = 'ecdsa-sha2-nistp256'; + els.push(Enc.binToHex(ssh.type)); + els.push(Enc.binToHex('nistp256')); + len = 32; + } else if ("P-384" === jwk.crv) { + ssh.type = 'ecdsa-sha2-nistp384'; + els.push(Enc.binToHex(ssh.type)); + els.push(Enc.binToHex('nistp384')); + len = 48; + } else { + throw new Error("unknown key type " + (jwk.crv || jwk.kty)); + } + + els.push('04' + + SSH._padEc(Enc.base64ToHex(jwk.x), len) + + SSH._padEc(Enc.base64ToHex(jwk.y), len) + ); + return SSH._packElements(ssh); +}; + +SSH._packElements = function (ssh) { + var hex = ssh._elements.map(function (hex) { + return SSH._numToUint32Hex(hex.length/2) + hex; + }).join(''); + return [ ssh.type, Enc.hexToBase64(hex), ssh.comment ].join(' '); +}; + +SSH._numToUint32Hex = function (num) { + var hex = num.toString(16); + while (hex.length < 8) { + hex = '0' + hex; + } + return hex; +}; + +SSH._padRsa = function (hex) { + // BigInt is negative if the high order bit 0x80 is set, + // so ASN1, SSH, and many other formats pad with '0x00' + // to signifiy a positive number. + var i = parseInt(hex.slice(0, 2), 16); + if (0x80 & i) { + return '00' + hex; + } + return hex; +}; + +SSH._padEc = function (hex, len) { + while (hex.length < len * 2) { + hex = '00' + hex; + } + return hex; +}; diff --git a/lib/telemetry.js b/lib/telemetry.js new file mode 100644 index 0000000..9623b77 --- /dev/null +++ b/lib/telemetry.js @@ -0,0 +1,111 @@ +'use strict'; + +// We believe in a proactive approach to sustainable open source. +// As part of that we make it easy for you to opt-in to following our progress +// and we also stay up-to-date on telemetry such as operating system and node +// version so that we can focus our efforts where they'll have the greatest impact. +// +// Want to learn more about our Terms, Privacy Policy, and Mission? +// Check out https://therootcompany.com/legal/ + +var os = require('os'); +var crypto = require('crypto'); +var https = require('https'); +var pkg = require('../package.json'); + +// to help focus our efforts in the right places +var data = { + package: pkg.name +, version: pkg.version +, node: process.version +, arch: process.arch || os.arch() +, platform: process.platform || os.platform() +, release: os.release() +}; + +function addCommunityMember(opts) { + setTimeout(function () { + var req = https.request({ + hostname: 'api.therootcompany.com' + , port: 443 + , path: '/api/therootcompany.com/public/community' + , method: 'POST' + , headers: { 'Content-Type': 'application/json' } + }, function (resp) { + // let the data flow, so we can ignore it + resp.on('data', function () {}); + //resp.on('data', function (chunk) { console.log(chunk.toString()); }); + resp.on('error', function () { /*ignore*/ }); + //resp.on('error', function (err) { console.error(err); }); + }); + var obj = JSON.parse(JSON.stringify(data)); + obj.action = 'updates'; + try { + obj.ppid = ppid(obj.action); + } catch(e) { + // ignore + //console.error(e); + } + obj.name = opts.name || undefined; + obj.address = opts.email; + obj.community = 'node.js@therootcompany.com'; + + req.write(JSON.stringify(obj, 2, null)); + req.end(); + req.on('error', function () { /*ignore*/ }); + //req.on('error', function (err) { console.error(err); }); + }, 50); +} + +function ping(action) { + setTimeout(function () { + var req = https.request({ + hostname: 'api.therootcompany.com' + , port: 443 + , path: '/api/therootcompany.com/public/ping' + , method: 'POST' + , headers: { 'Content-Type': 'application/json' } + }, function (resp) { + // let the data flow, so we can ignore it + resp.on('data', function () { }); + //resp.on('data', function (chunk) { console.log(chunk.toString()); }); + resp.on('error', function () { /*ignore*/ }); + //resp.on('error', function (err) { console.error(err); }); + }); + var obj = JSON.parse(JSON.stringify(data)); + obj.action = action; + try { + obj.ppid = ppid(obj.action); + } catch(e) { + // ignore + //console.error(e); + } + + req.write(JSON.stringify(obj, 2, null)); + req.end(); + req.on('error', function (/*e*/) { /*console.error('req.error', e);*/ }); + }, 50); +} + +// to help identify unique installs without getting +// the personally identifiable info that we don't want +function ppid(action) { + var parts = [ action, data.package, data.version, data.node, data.arch, data.platform, data.release ]; + var ifaces = os.networkInterfaces(); + Object.keys(ifaces).forEach(function (ifname) { + if (/^en/.test(ifname) || /^eth/.test(ifname) || /^wl/.test(ifname)) { + if (ifaces[ifname] && ifaces[ifname].length) { + parts.push(ifaces[ifname][0].mac); + } + } + }); + return crypto.createHash('sha1').update(parts.join(',')).digest('base64'); +} + +module.exports.ping = ping; +module.exports.joinCommunity = addCommunityMember; + +if (require.main === module) { + ping('install'); + //addCommunityMember({ name: "AJ ONeal", email: 'coolaj86@gmail.com' }); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..534aa9c --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "jwk-to-ssh", + "version": "1.0.0", + "description": "💯 JWK to SSH in a lightweight, zero-dependency library.", + "homepage": "https://git.coolaj86.com/coolaj86/jwk-to-ssh.js", + "main": "index.js", + "bin": { + "jwk-to-ssh": "bin/jwk-to-ssh.js" + }, + "files": [ + "bin", + "fixtures", + "lib" + ], + "directories": { + "lib": "lib" + }, + "scripts": { + "postinstall": "node lib/telemetry.js event:install", + "test": "bash test.sh" + }, + "repository": { + "type": "git", + "url": "https://git.coolaj86.com/coolaj86/jwk-to-ssh.js" + }, + "keywords": [ + "zero-dependency", + "JWK-to-SSH", + "RSA", + "EC", + "SSH", + "JWK", + "ECDSA" + ], + "author": "AJ ONeal (https://coolaj86.com/)", + "license": "MPL-2.0" +}