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