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
+
+ DER Private Binary
+
+
+
+ DER Public Binary
+
+
+
+ PEM Private (base64-encoded DER)
+
+
+
+ PEM Public (base64-encoded DER)
+
+
ACME Account Request
@@ -66,8 +96,10 @@
ACME Account Response
-
+
+
+
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