diff --git a/app.js b/app.js index fcacb77..f2066dd 100644 --- a/app.js +++ b/app.js @@ -1,68 +1,98 @@ (function () { -'use strict'; + 'use strict'; -var Keypairs = window.Keypairs; + var Keypairs = window.Keypairs; -function $(sel) { - return document.querySelector(sel); -} -function $$(sel) { - return Array.prototype.slice.call(document.querySelectorAll(sel)); -} + function $(sel) { + return document.querySelector(sel); + } + function $$(sel) { + return Array.prototype.slice.call(document.querySelectorAll(sel)); + } -function run() { - console.log('hello'); + function run() { + console.log('hello'); - // Show different options for ECDSA vs RSA - $$('input[name="kty"]').forEach(function ($el) { - $el.addEventListener('change', function (ev) { - console.log(this); - console.log(ev); - if ("RSA" === ev.target.value) { - $('.js-rsa-opts').hidden = false; - $('.js-ec-opts').hidden = true; - } else { - $('.js-rsa-opts').hidden = true; - $('.js-ec-opts').hidden = false; - } + // Show different options for ECDSA vs RSA + $$('input[name="kty"]').forEach(function ($el) { + $el.addEventListener('change', function (ev) { + console.log(this); + console.log(ev); + if ("RSA" === ev.target.value) { + $('.js-rsa-opts').hidden = false; + $('.js-ec-opts').hidden = true; + } else { + $('.js-rsa-opts').hidden = true; + $('.js-ec-opts').hidden = false; + } + }); }); - }); - // Generate a key on submit - $('form.js-keygen').addEventListener('submit', function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - $('.js-loading').hidden = false; - $('.js-jwk').hidden = true; - $$('input').map(function ($el) { $el.disabled = true; }); - $$('button').map(function ($el) { $el.disabled = true; }); - var opts = { - kty: $('input[name="kty"]:checked').value - , namedCurve: $('input[name="ec-crv"]:checked').value - , modulusLength: $('input[name="rsa-len"]:checked').value - }; - console.log('opts', opts); - Keypairs.generate(opts).then(function (results) { - $('.js-jwk').innerText = JSON.stringify(results, null, 2); - // - $('.js-loading').hidden = true; - $('.js-jwk').hidden = false; - $$('input').map(function ($el) { $el.disabled = false; }); - $$('button').map(function ($el) { $el.disabled = false; }); - $('.js-toc-jwk').hidden = false; + // Generate a key on submit + $('form.js-keygen').addEventListener('submit', function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + $('.js-loading').hidden = false; + $('.js-jwk').hidden = true; + $('.js-toc-der-public').hidden = true; + $('.js-toc-pem-public').hidden = true; + $('.js-toc-der-private').hidden = true; + $('.js-toc-pem-private').hidden = true; + $$('input').map(function ($el) { $el.disabled = true; }); + $$('button').map(function ($el) { $el.disabled = true; }); + var opts = { + kty: $('input[name="kty"]:checked').value + , namedCurve: $('input[name="ec-crv"]:checked').value + , modulusLength: $('input[name="rsa-len"]:checked').value + }; + console.log('opts', opts); + Keypairs.generate(opts).then(function (results) { + var der_public, der_private; + if (opts.kty == 'EC') { + der_public = x509.packSpki(results.public); + der_private = x509.packPkcs8(results.private); + var pem_private = Eckles.export({ jwk: results.private }) + var pem_public = Eckles.export({ jwk: results.public, public: true }) + $('.js-input-pem-public').innerText = pem_public; + $('.js-toc-pem-public').hidden = false; + $('.js-input-pem-private').innerText = pem_private; + $('.js-toc-pem-private').hidden = false; + } else { + der_private = x509.packPkcs8(results.private); + der_public = x509.packPkcs8(results.public); + Rasha.pack({ jwk: results.private }).then(function (pem) { + $('.js-input-pem-private').innerText = pem; + $('.js-toc-pem-private').hidden = false; + }) + Rasha.pack({ jwk: results.public }).then(function (pem) { + $('.js-input-pem-public').innerText = pem; + $('.js-toc-pem-public').hidden = false; + }) + } + + $('.js-der-public').innerText = der_public; + $('.js-toc-der-public').hidden = false; + $('.js-der-private').innerText = der_private; + $('.js-toc-der-private').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; }); + $$('button').map(function ($el) { $el.disabled = false; }); + $('.js-toc-jwk').hidden = false; + }); }); - }); - $('form.js-acme-account').addEventListener('submit', function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - $('.js-loading').hidden = false; - ACME.accounts.create - }); + $('form.js-acme-account').addEventListener('submit', function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + $('.js-loading').hidden = false; + ACME.accounts.create + }); - $('.js-generate').hidden = false; - $('.js-create-account').hidden = false; -} + $('.js-generate').hidden = false; + $('.js-create-account').hidden = false; + } -window.addEventListener('load', run); + window.addEventListener('load', run); }()); diff --git a/index.html b/index.html index da066a9..047936b 100644 --- a/index.html +++ b/index.html @@ -1,6 +1,20 @@ BlueCrypt +

BlueCrypt for the Browser

@@ -58,6 +72,22 @@ JWK Keypair
 
+ + + + - + + + diff --git a/lib/bluecrypt-encoding.js b/lib/bluecrypt-encoding.js index d3f2292..7dc1073 100644 --- a/lib/bluecrypt-encoding.js +++ b/lib/bluecrypt-encoding.js @@ -1,6 +1,6 @@ (function (exports) { -var Enc = exports.BluecryptEncoding = {}; +var Enc = exports.Enc = {}; Enc.bufToBin = function (buf) { var bin = ''; diff --git a/lib/ecdsa.js b/lib/ecdsa.js index dedc4fb..deb2c86 100644 --- a/lib/ecdsa.js +++ b/lib/ecdsa.js @@ -32,7 +32,7 @@ EC.generate = function (opts) { + " Please choose either 'P-256' or 'P-384'. " + EC._stance)); } - + var extractable = true; return window.crypto.subtle.generateKey( wcOpts @@ -51,6 +51,59 @@ EC.generate = function (opts) { }); }; +EC.export = function (opts) { + if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) { + throw new Error("must pass { jwk: jwk } as a JSON object"); + } + var jwk = JSON.parse(JSON.stringify(opts.jwk)); + var format = opts.format; + if (opts.public || -1 !== [ 'spki', 'pkix', 'ssh', 'rfc4716' ].indexOf(format)) { + jwk.d = null; + } + if ('EC' !== jwk.kty) { + throw new Error("options.jwk.kty must be 'EC' for EC keys"); + } + if (!jwk.d) { + if (!format || -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' or 'ssh' for public EC keys, not (" + + typeof format + ") " + format); + } + } else { + if (!format || 'sec1' === format) { + format = 'sec1'; + } else if ('pkcs8' !== format) { + throw new Error("options.format must be 'sec1' or 'pkcs8' for private EC keys, not '" + format + "'"); + } + } + if (-1 === [ 'P-256', 'P-384' ].indexOf(jwk.crv)) { + throw new Error("options.jwk.crv must be either P-256 or P-384 for EC keys, not '" + jwk.crv + "'"); + } + if (!jwk.y) { + throw new Error("options.jwk.y must be a urlsafe base64-encoded either P-256 or P-384"); + } + + if ('sec1' === format) { + return PEM.packBlock({ type: "EC PRIVATE KEY", bytes: x509.packSec1(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.packSsh(jwk); + } else { + throw new Error("Sanity Error: reached unreachable code block with format: " + format); + } +}; +EC.pack = function (opts) { + return Promise.resolve().then(function () { + return EC.exportSync(opts); + }); +}; + // Chopping off the private parts is now part of the public API. // I thought it sounded a little too crude at first, but it really is the best name in every possible way. EC.neuter = function (opts) { diff --git a/lib/keypairs.js b/lib/keypairs.js index 1492954..79566d9 100644 --- a/lib/keypairs.js +++ b/lib/keypairs.js @@ -3,8 +3,8 @@ 'use strict'; var Keypairs = exports.Keypairs = {}; -var Rasha = exports.Rasha || require('rasha'); -var Eckles = exports.Eckles || require('eckles'); +var Rasha = exports.Rasha; +var Eckles = exports.Eckles; var Enc = exports.Enc || {}; Keypairs._stance = "We take the stance that if you're knowledgeable enough to" @@ -34,10 +34,12 @@ Keypairs.generate = function (opts) { }; -// Chopping off the private parts is now part of the public API. -// I thought it sounded a little too crude at first, but it really is the best name in every possible way. +/** + * Chopping off the private parts is now part of the public API. + * I thought it sounded a little too crude at first, but it really is the best name in every possible way. + */ Keypairs.neuter = Keypairs._neuter = function (opts) { - // trying to find the best balance of an immutable copy with custom attributes + /** trying to find the best balance of an immutable copy with custom attributes */ var jwk = {}; Object.keys(opts.jwk).forEach(function (k) { if ('undefined' === typeof opts.jwk[k]) { return; } @@ -61,7 +63,7 @@ Keypairs.thumbprint = function (opts) { Keypairs.publish = function (opts) { if ('object' !== typeof opts.jwk || !opts.jwk.kty) { throw new Error("invalid jwk: " + JSON.stringify(opts.jwk)); } - // returns a copy + /** returns a copy */ var jwk = Keypairs.neuter(opts); if (jwk.exp) { 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 63d1a7d..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; @@ -128,7 +149,7 @@ var x = Enc.base64ToHex(jwk.x); var y = Enc.base64ToHex(jwk.y); var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384; - return Enc.hexToUint8( + return Enc.hexToBuf( ASN1('30' , ASN1.UInt('01') , ASN1('04', d) @@ -136,12 +157,54 @@ , ASN1('A1', ASN1.BitStr('04' + x + y))) ); }; + /** + * take a private jwk and creates a der from it + * @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); var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384; - return Enc.hexToUint8( + return Enc.hexToBuf( ASN1('30' , ASN1.UInt('00') , ASN1('30' @@ -159,7 +222,7 @@ var x = Enc.base64ToHex(jwk.x); var y = Enc.base64ToHex(jwk.y); var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384; - return Enc.hexToUint8( + return Enc.hexToBuf( ASN1('30' , ASN1('30' , OBJ_ID_EC_PUB