diff --git a/app.js b/app.js index c56ea50..06c0022 100644 --- a/app.js +++ b/app.js @@ -45,17 +45,23 @@ }; console.log('opts', opts); Keypairs.generate(opts).then(function (results) { + var der; if (opts.kty == 'EC') { - var der = x509.packPkcs8(results.private); + der = x509.packPkcs8(results.private); var pem = Eckles.export({ jwk: results.private }) - $('.js-der').innerText = JSON.stringify(der, null, 2); $('.js-input-pem').innerText = pem; - $('.js-toc-der').hidden = false; $('.js-toc-pem').hidden = false; + } else { + der = x509.packPkcs8(results.private); + var pem = Rasha.pack({ jwk: results.private }).then(function (pem) { + $('.js-input-pem').innerText = pem; + $('.js-toc-pem').hidden = false; + }) } + $('.js-der').innerText = JSON.stringify(der, null, 2); + $('.js-toc-der').hidden = false; $('.js-jwk').innerText = JSON.stringify(results, null, 2); - // $('.js-loading').hidden = true; $('.js-jwk').hidden = false; $$('input').map(function ($el) { $el.disabled = false; }); diff --git a/lib/rsa.js b/lib/rsa.js index 4ec7e07..17ceccb 100644 --- a/lib/rsa.js +++ b/lib/rsa.js @@ -3,6 +3,7 @@ 'use strict'; var RSA = exports.Rasha = {}; +var x509 = exports.x509; if ('undefined' !== typeof module) { module.exports = RSA; } var Enc = {}; var textEncoder = new TextEncoder(); @@ -106,6 +107,66 @@ RSA.thumbprint = function (opts) { }); }; +RSA.export = function (opts) { + if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) { + throw new Error("must pass { jwk: jwk }"); + } + var jwk = JSON.parse(JSON.stringify(opts.jwk)); + var format = opts.format; + var pub = opts.public; + if (pub || -1 !== [ 'spki', 'pkix', 'ssh', 'rfc4716' ].indexOf(format)) { + jwk = RSA.nueter(jwk); + } + if ('RSA' !== jwk.kty) { + throw new Error("options.jwk.kty must be 'RSA' for RSA keys"); + } + if (!jwk.p) { + // TODO test for n and e + pub = true; + if (!format || 'pkcs1' === format) { + format = 'pkcs1'; + } else if (-1 !== [ 'spki', 'pkix' ].indexOf(format)) { + format = 'spki'; + } else if (-1 !== [ 'ssh', 'rfc4716' ].indexOf(format)) { + format = 'ssh'; + } else { + throw new Error("options.format must be 'spki', 'pkcs1', or 'ssh' for public RSA keys, not (" + + typeof format + ") " + format); + } + } else { + // TODO test for all necessary keys (d, p, q ...) + if (!format || 'pkcs1' === format) { + format = 'pkcs1'; + } else if ('pkcs8' !== format) { + throw new Error("options.format must be 'pkcs1' or 'pkcs8' for private RSA keys"); + } + } + + if ('pkcs1' === format) { + if (jwk.d) { + return PEM.packBlock({ type: "RSA PRIVATE KEY", bytes: x509.packPkcs1(jwk) }); + } else { + return PEM.packBlock({ type: "RSA PUBLIC KEY", bytes: x509.packPkcs1(jwk) }); + } + } else if ('pkcs8' === format) { + return PEM.packBlock({ type: "PRIVATE KEY", bytes: x509.packPkcs8(jwk) }); + } else if (-1 !== [ 'spki', 'pkix' ].indexOf(format)) { + return PEM.packBlock({ type: "PUBLIC KEY", bytes: x509.packSpki(jwk) }); + } else if (-1 !== [ 'ssh', 'rfc4716' ].indexOf(format)) { + return SSH.pack({ jwk: jwk, comment: opts.comment }); + } else { + throw new Error("Sanity Error: reached unreachable code block with format: " + format); + } +}; +RSA.pack = function (opts) { + // wrapped in a promise for API compatibility + // with the forthcoming browser version + // (and potential future native node capability) + return Promise.resolve().then(function () { + return RSA.export(opts); + }); +}; + Enc.bufToUrlBase64 = function (u8) { return Enc.bufToBase64(u8) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); diff --git a/lib/x509.js b/lib/x509.js index 3bf60fd..f1ac559 100644 --- a/lib/x509.js +++ b/lib/x509.js @@ -1,7 +1,7 @@ 'use strict'; (function (exports) { 'use strict'; - var x509 = exports.x509 = {}; + var x509 = exports.x509 = {}; var ASN1 = exports.ASN1; var Enc = exports.Enc; @@ -55,6 +55,27 @@ }; }; + x509.packPkcs1 = function (jwk) { + var n = ASN1.UInt(Enc.base64ToHex(jwk.n)); + var e = ASN1.UInt(Enc.base64ToHex(jwk.e)); + + if (!jwk.d) { + return Enc.hexToBuf(ASN1('30', n, e)); + } + + return Enc.hexToBuf(ASN1('30' + , ASN1.UInt('00') + , n + , e + , ASN1.UInt(Enc.base64ToHex(jwk.d)) + , ASN1.UInt(Enc.base64ToHex(jwk.p)) + , ASN1.UInt(Enc.base64ToHex(jwk.q)) + , ASN1.UInt(Enc.base64ToHex(jwk.dp)) + , ASN1.UInt(Enc.base64ToHex(jwk.dq)) + , ASN1.UInt(Enc.base64ToHex(jwk.qi)) + )); + }; + x509.parsePkcs8 = function parseEcPkcs8(u8, jwk) { var index = 24 + (OBJ_ID_EC.length / 2); var len = 32; @@ -141,6 +162,44 @@ * @param {*} jwk */ x509.packPkcs8 = function (jwk) { + if (jwk.kty == 'RSA') { + if (!jwk.d) { + // Public RSA + return Enc.hexToBuf(ASN1('30' + , ASN1('30' + , ASN1('06', '2a864886f70d010101') + , ASN1('05') + ) + , ASN1.BitStr(ASN1('30' + , ASN1.UInt(Enc.base64ToHex(jwk.n)) + , ASN1.UInt(Enc.base64ToHex(jwk.e)) + )) + )); + } + + // Private RSA + return Enc.hexToBuf(ASN1('30' + , ASN1.UInt('00') + , ASN1('30' + , ASN1('06', '2a864886f70d010101') + , ASN1('05') + ) + , ASN1('04' + , ASN1('30' + , ASN1.UInt('00') + , ASN1.UInt(Enc.base64ToHex(jwk.n)) + , ASN1.UInt(Enc.base64ToHex(jwk.e)) + , ASN1.UInt(Enc.base64ToHex(jwk.d)) + , ASN1.UInt(Enc.base64ToHex(jwk.p)) + , ASN1.UInt(Enc.base64ToHex(jwk.q)) + , ASN1.UInt(Enc.base64ToHex(jwk.dp)) + , ASN1.UInt(Enc.base64ToHex(jwk.dq)) + , ASN1.UInt(Enc.base64ToHex(jwk.qi)) + ) + ) + )); + } + var d = Enc.base64ToHex(jwk.d); var x = Enc.base64ToHex(jwk.x); var y = Enc.base64ToHex(jwk.y);