v1.0.0: parse EC and RSA ssh public keys
This commit is contained in:
		
						commit
						96d47924bf
					
				
							
								
								
									
										65
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,65 @@
 | 
			
		||||
# SSH to JWK (for node.js)
 | 
			
		||||
 | 
			
		||||
A minimal library to parse an SSH public key (`id_rsa.pub`)
 | 
			
		||||
and convert it into a public JWK.
 | 
			
		||||
 | 
			
		||||
Works for RSA and ECDSA public keys.
 | 
			
		||||
 | 
			
		||||
Features
 | 
			
		||||
========
 | 
			
		||||
 | 
			
		||||
< 100 lines of code | <1kb gzipped | 1.8kb minified | 3.1kb with comments
 | 
			
		||||
 | 
			
		||||
* [x] SSH Public Keys
 | 
			
		||||
* [x] RSA Public Keys
 | 
			
		||||
* [x] EC Public Keys
 | 
			
		||||
  * P-256 (prime256v1, secp256r1)
 | 
			
		||||
  * P-384 (secp384r1)
 | 
			
		||||
* [ ] Browser Version (coming soon)
 | 
			
		||||
 | 
			
		||||
### Need JWK to SSH? SSH to PEM?
 | 
			
		||||
 | 
			
		||||
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 `ssh-to-jwk` and use it from command line:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
npm install -g ssh-to-jwk
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
ssh-to-jwk ~/.ssh/id_rsa.pub
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
# Usage
 | 
			
		||||
 | 
			
		||||
You can also use it from JavaScript:
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
var fs = require('fs');
 | 
			
		||||
var sshtojwk = require('sshtojwk');
 | 
			
		||||
 | 
			
		||||
var pub = fs.readFileSync("./id_rsa.pub");
 | 
			
		||||
var jwk = sshtojwk(pub);
 | 
			
		||||
 | 
			
		||||
console.info(jwk);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Legal
 | 
			
		||||
-----
 | 
			
		||||
 | 
			
		||||
[ssh-to-jwk.js](https://git.coolaj86.com/coolaj86/ssh-to-jwk.js) |
 | 
			
		||||
MPL-2.0 |
 | 
			
		||||
[Terms of Use](https://therootcompany.com/legal/#terms) |
 | 
			
		||||
[Privacy Policy](https://therootcompany.com/legal/#privacy)
 | 
			
		||||
							
								
								
									
										17
									
								
								bin/ssh-to-jwk.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										17
									
								
								bin/ssh-to-jwk.js
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
#!/usr/bin/env node
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var fs = require('fs');
 | 
			
		||||
var path = require('path');
 | 
			
		||||
var sshtojwk = require('../index.js');
 | 
			
		||||
 | 
			
		||||
var pubfile = process.argv[2];
 | 
			
		||||
 | 
			
		||||
if (!pubfile) {
 | 
			
		||||
  pubfile = path.join(require('os').homedir(), '.ssh/id_rsa.pub');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var buf = fs.readFileSync(pubfile);
 | 
			
		||||
var ssh = sshtojwk.parse(buf.toString('ascii'));
 | 
			
		||||
 | 
			
		||||
console.info(JSON.stringify(ssh.jwk, null, 2));
 | 
			
		||||
							
								
								
									
										28
									
								
								lib/encoding.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								lib/encoding.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var Enc = module.exports;
 | 
			
		||||
 | 
			
		||||
Enc.base64ToBuf = function (str) {
 | 
			
		||||
  // node handles both base64 and urlBase64 equally
 | 
			
		||||
  return Buffer.from(str, 'base64');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Enc.bufToBase64 = function (u8) {
 | 
			
		||||
  // Ensure a node buffer, even if TypedArray
 | 
			
		||||
  return Buffer.from(u8).toString('base64');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Enc.bufToBin = function (u8) {
 | 
			
		||||
  // Ensure a node buffer, even if TypedArray
 | 
			
		||||
  return Buffer.from(u8).toString('binary');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Enc.bufToHex = function (u8) {
 | 
			
		||||
  // Ensure a node buffer, even if TypedArray
 | 
			
		||||
  return Buffer.from(u8).toString('hex');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Enc.bufToUrlBase64 = function (u8) {
 | 
			
		||||
  return Enc.bufToBase64(u8)
 | 
			
		||||
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										101
									
								
								lib/ssh-parser.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								lib/ssh-parser.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,101 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var SSH = module.exports;
 | 
			
		||||
var Enc = require('./encoding.js');
 | 
			
		||||
 | 
			
		||||
SSH.parse = function (ssh) {
 | 
			
		||||
  ssh = ssh.split(/\s+/g);
 | 
			
		||||
 | 
			
		||||
  var result = { type: ssh[0], jwk: null, comment: ssh[2] || '' };
 | 
			
		||||
  var buf = Enc.base64ToBuf(ssh[1]);
 | 
			
		||||
  var els = SSH.parseElements(buf);
 | 
			
		||||
  var typ = Enc.bufToBin(els[0]);
 | 
			
		||||
  var len;
 | 
			
		||||
 | 
			
		||||
  // RSA keys are all the same
 | 
			
		||||
  if (SSH.types.rsa === typ) {
 | 
			
		||||
    result.jwk = {
 | 
			
		||||
      kty: 'RSA'
 | 
			
		||||
    , n: Enc.bufToUrlBase64(els[2])
 | 
			
		||||
    , e: Enc.bufToUrlBase64(els[1])
 | 
			
		||||
    };
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // EC keys are each different
 | 
			
		||||
  if (SSH.types.p256 === typ) {
 | 
			
		||||
    len = 32;
 | 
			
		||||
    result.jwk = { kty: 'EC', crv: 'P-256' };
 | 
			
		||||
  } else if (SSH.types.p384 === typ) {
 | 
			
		||||
    len = 48;
 | 
			
		||||
    result.jwk = { kty: 'EC', crv: 'P-384' };
 | 
			
		||||
  } else {
 | 
			
		||||
    throw new Error("Unsupported ssh public key type: "
 | 
			
		||||
      + Enc.bufToBin(els[0]));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // els[1] is just a repeat of a subset of els[0]
 | 
			
		||||
  var x = els[2].slice(1, 1 + len);
 | 
			
		||||
  var y = els[2].slice(1 + len, 1 + len + len);
 | 
			
		||||
 | 
			
		||||
  // I don't think EC keys use 0x00 padding, but just in case
 | 
			
		||||
  if (0x00 === x[0]) { x = x.slice(1); }
 | 
			
		||||
  if (0x00 === y[0]) { y = y.slice(1); }
 | 
			
		||||
 | 
			
		||||
  result.jwk.x = Enc.bufToUrlBase64(x);
 | 
			
		||||
  result.jwk.y = Enc.bufToUrlBase64(y);
 | 
			
		||||
 | 
			
		||||
  return result;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
SSH.parseElements = function (buf) {
 | 
			
		||||
  var fulllen = buf.byteLength || buf.length;
 | 
			
		||||
  var offset = (buf.byteOffset || 0);
 | 
			
		||||
  var i = 0;
 | 
			
		||||
  var index = 0;
 | 
			
		||||
  // using dataview to be browser-compatible (I do want _some_ code reuse)
 | 
			
		||||
  var dv = new DataView(buf.buffer.slice(offset, offset + fulllen));
 | 
			
		||||
  var els = [];
 | 
			
		||||
  var el;
 | 
			
		||||
  var len;
 | 
			
		||||
 | 
			
		||||
  while (index < fulllen) {
 | 
			
		||||
    i += 1;
 | 
			
		||||
    if (i > 15) { throw new Error("15+ elements, probably not a public ssh key"); }
 | 
			
		||||
    len = dv.getUint32(index, false);
 | 
			
		||||
    index += 4;
 | 
			
		||||
    el = buf.slice(index, index + len);
 | 
			
		||||
    // remove BigUInt '00' prefix
 | 
			
		||||
    if (0x00 === el[0]) {
 | 
			
		||||
      el = el.slice(1);
 | 
			
		||||
    }
 | 
			
		||||
    els.push(el);
 | 
			
		||||
    index += len;
 | 
			
		||||
  }
 | 
			
		||||
  if (fulllen !== index) {
 | 
			
		||||
    throw new Error(els.map(function (b) {
 | 
			
		||||
      return Enc.bufToHex(b);
 | 
			
		||||
    }).join('\n') + "invalid ssh public key length");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return els;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
SSH.types = {
 | 
			
		||||
  // 19 '00000013'
 | 
			
		||||
  // e c d s a - s h a 2 - n i s t p 2 5 6
 | 
			
		||||
  // 65636473612d736861322d6e69737470323536
 | 
			
		||||
  // 6e69737470323536
 | 
			
		||||
  p256: 'ecdsa-sha2-nistp256'
 | 
			
		||||
 | 
			
		||||
  // 19 '00000013'
 | 
			
		||||
  // e c d s a - s h a 2 - n i s t p 3 8 4
 | 
			
		||||
  // 65636473612d736861322d6e69737470333834
 | 
			
		||||
  // 6e69737470323536
 | 
			
		||||
, p384: 'ecdsa-sha2-nistp384'
 | 
			
		||||
 | 
			
		||||
  // 7 '00000007'
 | 
			
		||||
  // s s h - r s a
 | 
			
		||||
  // 7373682d727361
 | 
			
		||||
, rsa: 'ssh-rsa'
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										111
									
								
								lib/telemetry.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								lib/telemetry.js
									
									
									
									
									
										Normal file
									
								
							@ -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' });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "ssh-to-jwk",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "description": "💯 SSH to JWK in a lightweight, zero-dependency library.",
 | 
			
		||||
  "homepage": "https://git.coolaj86.com/coolaj86/ssh-to-jwk.js",
 | 
			
		||||
  "main": "index.js",
 | 
			
		||||
  "bin": {
 | 
			
		||||
    "ssh-to-jwk": "bin/ssh-to-jwk.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/ssh-to-jwk.js"
 | 
			
		||||
  },
 | 
			
		||||
  "keywords": [
 | 
			
		||||
    "zero-dependency",
 | 
			
		||||
    "SSH-to-JWK",
 | 
			
		||||
    "RSA",
 | 
			
		||||
    "EC",
 | 
			
		||||
    "SSH",
 | 
			
		||||
    "JWK",
 | 
			
		||||
    "ECDSA"
 | 
			
		||||
  ],
 | 
			
		||||
  "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
 | 
			
		||||
  "license": "MPL-2.0"
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user