AJ ONeal
5 years ago
commit
96d47924bf
7 changed files with 360 additions and 0 deletions
@ -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) |
@ -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)); |
@ -0,0 +1 @@ |
|||
module.exports = require('./lib/ssh-parser.js'); |
@ -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, ''); |
|||
}; |
@ -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' |
|||
}; |
@ -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' });
|
|||
} |
@ -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…
Reference in new issue