diff --git a/README.md b/README.md index a17b7ce..64c17ad 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,191 @@ -Placeholder +Rasha.js +========= -I've just completed these: +Sponsored by [Root](https://therootcompany.com). +Built for [ACME.js](https://git.coolaj86.com/coolaj86/acme.js) +and [Greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) -* [ECDSA-CSR.js](https://git.coolaj86.com/coolaj86/ecdsa-csr.js) -* [eckles.js](https://git.coolaj86.com/coolaj86/eckles.js) - JWK-to-PEM and PEM-to-JWK for EC / ECDSA P-256 and P-384 +RSA tools. Lightweight. Zero Dependencies. Universal compatibility. -I've got working prototypes for the RSA variants as well and I'm in the middle of cleaning them up to publish. +* [x] PEM-to-JWK +* [ ] JWK-to-PEM (in progress) +* [x] SSH "pub" format + + + +## PEM-to-JWK + +* [x] PKCS#1 (traditional), PKCS#8, SPKI/PKIX +* [x] 2048-bit, 4096-bit (and ostensibily all others) +* [x] SSH (RFC4716), (RFC 4716/SSH2) + +```js +var Rasha = require('rasha'); +var pem = require('fs') + .readFileSync('./node_modles/rasha/fixtures/privkey-rsa-2048.pkcs1.pem', 'ascii'); + +Rasha.import({ pem: pem }).then(function (jwk) { + console.log(jwk); +}); +``` + +```js +{ + "kty": "RSA", + "n": "m2ttVBxPlWw06ZmGBWVDl...QlEz7UNNj9RGps_50-CNw", + "e": "AQAB", + "d": "Cpfo7Mm9Nu8YMC_xrZ54W...Our1IdDzJ_YfHPt9sHMQQ", + "p": "ynG-t9HwKCN3MWRYFdnFz...E9S4DsGcAarIuOT2TsTCE", + "q": "xIkAjgUzB1zaUzJtW2Zgv...38ahSrBFEVnxjpnPh1Q1c", + "dp": "tzDGjECFOU0ehqtuqhcu...dVGAXJoGOdv5VpaZ7B1QE", + "dq": "kh5dyDk7YCz7sUFbpsmu...aX9PKa12HFlny6K1daL48", + "qi": "AlHWbx1gp6Z9pbw_1hlS...lhmIOgRApS0t9VoXtHhFU" +} +``` + + + +### Advanced Options + + + +`public: 'true'`: + +If a private key is used as input, a private key will be output. + +If you'd like to output a public key instead you can pass `public: true`. + + Testing ------- -``` + + +You can compare these keys to the ones that you get from OpenSSL, ssh-keygen, and WebCrypto: + +```bash +# Generate 2048-bit RSA Keypair openssl genrsa -out privkey-rsa-2048.pkcs1.pem 2048 + +# Convert PKCS1 (traditional) RSA Keypair to PKCS8 format openssl rsa -in privkey-rsa-2048.pkcs1.pem -pubout -out pub-rsa-2048.spki.pem + +# Export Public-only RSA Key in PKCS1 (traditional) format openssl pkcs8 -topk8 -nocrypt -in privkey-rsa-2048.pkcs1.pem -out privkey-rsa-2048.pkcs8.pem + +# Convert PKCS1 (traditional) RSA Public Key to SPKI/PKIX format openssl rsa -in pub-rsa-2048.spki.pem -pubin -RSAPublicKey_out -out pub-rsa-2048.pkcs1.pem + +# Convert RSA public key to SSH format ssh-keygen -f ./pub-rsa-2048.spki.pem -i -mPKCS8 > ./pub-rsa-2048.ssh.pub ``` -** unified openssl commands ** +Goals of this project +----- -https://gist.github.com/briansmith/2ee42439923d8e65a266994d0f70180b +* Zero Dependencies +* Focused support for 2048-bit and 4096-bit RSA keypairs (although any size is technically supported) +* Convert both ways +* Browser support as well (TODO) +* OpenSSL, ssh-keygen, and WebCrypto compatibility + +Legal +----- + +Licensed MPL-2.0 + +[Terms of Use](https://therootcompany.com/legal/#terms) | +[Privacy Policy](https://therootcompany.com/legal/#privacy) diff --git a/lib/rasha.js b/lib/rasha.js index 4a9b915..01e0714 100644 --- a/lib/rasha.js +++ b/lib/rasha.js @@ -13,13 +13,14 @@ RSA.parse = function parseRsa(opts) { if (!opts || !opts.pem || 'string' !== typeof opts.pem) { throw new Error("must pass { pem: pem } as a string"); } + + var jwk = { kty: 'RSA', n: null, e: null }; if (0 === opts.pem.indexOf('ssh-rsa ')) { - return SSH.parse(opts.pem); + return SSH.parse(opts.pem, jwk); } var pem = opts.pem; var block = PEM.parseBlock(pem); //var hex = toHex(u8); - var jwk = { kty: 'RSA', n: null, e: null }; var asn1 = ASN1.parse(block.der); var meta = x509.guess(block.der, asn1); @@ -30,6 +31,13 @@ RSA.parse = function parseRsa(opts) { jwk = RSA.parsePkcs8(block.der, asn1, jwk); } + if (opts.public) { + jwk = { + kty: jwk.kty + , n: jwk.n + , e: jwk.e + }; + } return jwk; }); }; diff --git a/lib/ssh.js b/lib/ssh.js index 28fa201..4f04b6a 100644 --- a/lib/ssh.js +++ b/lib/ssh.js @@ -1,10 +1,38 @@ 'use strict'; var SSH = module.exports; +var Enc = require('./encoding.js'); - // 7 s s h - r s a + // 7 s s h - r s a SSH.RSA = '00000007 73 73 68 2d 72 73 61'.replace(/\s+/g, '').toLowerCase(); -SSH.parse = function (pem) { +SSH.parse = function (pem, jwk) { + var parts = pem.split(/\s+/); + var buf = Enc.base64ToBuf(parts[1]); + var els = []; + var index = 0; + var len; + var i = 0; + var offset = (buf.byteOffset || 0); + // using dataview to be browser-compatible (I do want _some_ code reuse) + var dv = new DataView(buf.buffer.slice(offset, offset + buf.byteLength)); + + if (SSH.RSA !== Enc.bufToHex(buf.slice(0, SSH.RSA.length/2))) { + throw new Error("does not lead with ssh header"); + } + + while (index < buf.byteLength) { + i += 1; + if (i > 3) { throw new Error("15+ elements, probably not a public ssh key"); } + len = dv.getUint32(index, false); + index += 4; + els.push(buf.slice(index, index + len)); + index += len; + } + + jwk.n = Enc.bufToUrlBase64(els[2]); + jwk.e = Enc.bufToUrlBase64(els[1]); + + return jwk; }; diff --git a/lib/telemetry.js b/lib/telemetry.js new file mode 100644 index 0000000..c628a2d --- /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 index 75a89ad..98b07c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rasha", - "version": "0.0.2", + "version": "0.7.0", "description": "PEM-to-JWK and JWK-to-PEM for RSA keys in a lightweight, zero-dependency library focused on perfect universal compatibility.", "homepage": "https://git.coolaj86.com/coolaj86/rasha.js", "main": "index.js", @@ -25,7 +25,6 @@ }, "xkeywords": [ "zero-dependency", - "JWK-to-PEM", "PEM-to-JWK", "RSA", "2048", @@ -33,6 +32,9 @@ "asn1", "x509" ], + "xkeywords": [ + "JWK-to-PEM" + ], "author": "AJ ONeal (https://coolaj86.com/)", "license": "MPL-2.0" }