v1.0.0: pack EC and RSA ssh public keys
This commit is contained in:
commit
99b53c3d97
69
README.md
Normal file
69
README.md
Normal file
@ -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)
|
18
bin/ssh-to-jwk.js
Executable file
18
bin/ssh-to-jwk.js
Executable file
@ -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);
|
2
index.js
Normal file
2
index.js
Normal file
@ -0,0 +1,2 @@
|
||||
'use strict';
|
||||
module.exports = require('./lib/ssh-packer.js');
|
15
lib/encoding.js
Normal file
15
lib/encoding.js
Normal file
@ -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');
|
||||
};
|
76
lib/ssh-packer.js
Normal file
76
lib/ssh-packer.js
Normal file
@ -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;
|
||||
};
|
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": "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 <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||
"license": "MPL-2.0"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user