diff --git a/browser.js b/browser.js new file mode 100644 index 0000000..3b08495 --- /dev/null +++ b/browser.js @@ -0,0 +1,134 @@ +;(function (exports) { +'use strict'; + +var PromiseA; +try { + /*global Promise*/ + PromiseA = Promise; +} catch(e) { + PromiseA = require('bluebird'); +} + + +// https://stackoverflow.com/questions/40314257/export-webcrypto-key-to-pem-format +function derToPem(keydata, pemName, privacy){ + var keydataS = arrayBufferToString(keydata); + var keydataB64 = window.btoa(keydataS); + var keydataB64Pem = formatAsPem(keydataB64, pemName, privacy); + return keydataB64Pem; +} + +function arrayBufferToString( buffer ) { + var binary = []; + var bytes = new Uint8Array( buffer ); + var len = bytes.byteLength; + for (var i = 0; i < len; i++) { + binary.push(String.fromCharCode( bytes[ i ] )); + } + return binary.join(''); +} + + +function formatAsPem(str, pemName, privacy) { + var privstr = (privacy ? privacy + ' ' : ''); + var finalString = '-----BEGIN ' + pemName + ' ' + privstr + 'KEY-----\n'; + + while (str.length > 0) { + finalString += str.substring(0, 64) + '\n'; + str = str.substring(64); + } + + finalString = finalString + '-----END ' + pemName + ' ' + privstr + 'KEY-----'; + + return finalString; +} + +var Keypairs = exports.Keypairs = { + generate: function(opts) { + if (!opts) { opts = {}; } + if (!opts.type) { opts.type = 'EC'; } + + var supported = [ 'EC', 'RSA' ]; + if (-1 === supported.indexOf(opts.type)) { + return PromiseA.reject(new Error("'" + opts.type + "' not implemented. Try one of " + supported.join(', '))); + } + + if ('EC' === opts.type) { + return Keypairs._generateEc(opts); + } + if ('RSA' === opts.type) { + return Keypairs._generateRsa(opts); + } + } +, _generateEc: function (opts) { + if (!opts.namedCurve) { opts.namedCurve = 'P-256'; } + if ('P-256' !== opts.namedCurve) { + console.warn("'" + opts.namedCurve + "' is not supported, but it _might_ happen to work anyway."); + } + + // https://github.com/diafygi/webcrypto-examples#ecdsa---generatekey + var extractable = true; + + return window.crypto.subtle.generateKey( + { name: "ECDSA", namedCurve: opts.namedCurve } + , extractable + , [ 'sign', 'verify' ] + ).then(function (result) { + return window.crypto.subtle.exportKey( + "jwk" + , result.privateKey + ).then(function (jwk) { + return window.crypto.subtle.exportKey( + "pkcs8" + , result.privateKey + ).then(function (keydata) { + return { + type: 'EC' + , privateJwk: jwk + , privatePem: derToPem(keydata, 'EC', 'PRIVATE') + }; + }); + }); + }); + } +, _generateRsa: function (opts) { + if (!opts.bitlength) { opts.bitlength = 2048; } + if (-1 === [ 2048, 4096 ].indexOf(opts.bitlength)) { + return PromiseA.reject("opts.bitlength = (" + typeof opts.bitlength + ") " + opts.bitlength + ": Are you serious?"); + } + + // https://github.com/diafygi/webcrypto-examples#rsa---generatekey + var extractable = true; + + return window.crypto.subtle.generateKey( + { name: "RSASSA-PKCS1-v1_5" + , modulusLength: opts.bitlength + , publicExponent: new Uint8Array([0x01, 0x00, 0x01]) + , hash: { name: "SHA-256" } + } + , extractable + , [ 'sign', 'verify' ] + ).then(function (result) { + return window.crypto.subtle.exportKey( + "jwk" + , result.privateKey + ).then(function (jwk) { + return window.crypto.subtle.exportKey( + "pkcs8" + , result.privateKey + ).then(function (keydata) { + return { + type: 'RSA' + , privateJwk: jwk + , privatePem: derToPem(keydata, 'RSA', 'PRIVATE') + }; + }); + }); + }); + } +}; + +}('undefined' === typeof module ? window : module.exports)); + +// How we might use this +// var Keypairs = require('keypairs').Keypairs diff --git a/index.js b/index.js index 3b08495..320ee40 100644 --- a/index.js +++ b/index.js @@ -1,134 +1,3 @@ -;(function (exports) { 'use strict'; -var PromiseA; -try { - /*global Promise*/ - PromiseA = Promise; -} catch(e) { - PromiseA = require('bluebird'); -} - - -// https://stackoverflow.com/questions/40314257/export-webcrypto-key-to-pem-format -function derToPem(keydata, pemName, privacy){ - var keydataS = arrayBufferToString(keydata); - var keydataB64 = window.btoa(keydataS); - var keydataB64Pem = formatAsPem(keydataB64, pemName, privacy); - return keydataB64Pem; -} - -function arrayBufferToString( buffer ) { - var binary = []; - var bytes = new Uint8Array( buffer ); - var len = bytes.byteLength; - for (var i = 0; i < len; i++) { - binary.push(String.fromCharCode( bytes[ i ] )); - } - return binary.join(''); -} - - -function formatAsPem(str, pemName, privacy) { - var privstr = (privacy ? privacy + ' ' : ''); - var finalString = '-----BEGIN ' + pemName + ' ' + privstr + 'KEY-----\n'; - - while (str.length > 0) { - finalString += str.substring(0, 64) + '\n'; - str = str.substring(64); - } - - finalString = finalString + '-----END ' + pemName + ' ' + privstr + 'KEY-----'; - - return finalString; -} - -var Keypairs = exports.Keypairs = { - generate: function(opts) { - if (!opts) { opts = {}; } - if (!opts.type) { opts.type = 'EC'; } - - var supported = [ 'EC', 'RSA' ]; - if (-1 === supported.indexOf(opts.type)) { - return PromiseA.reject(new Error("'" + opts.type + "' not implemented. Try one of " + supported.join(', '))); - } - - if ('EC' === opts.type) { - return Keypairs._generateEc(opts); - } - if ('RSA' === opts.type) { - return Keypairs._generateRsa(opts); - } - } -, _generateEc: function (opts) { - if (!opts.namedCurve) { opts.namedCurve = 'P-256'; } - if ('P-256' !== opts.namedCurve) { - console.warn("'" + opts.namedCurve + "' is not supported, but it _might_ happen to work anyway."); - } - - // https://github.com/diafygi/webcrypto-examples#ecdsa---generatekey - var extractable = true; - - return window.crypto.subtle.generateKey( - { name: "ECDSA", namedCurve: opts.namedCurve } - , extractable - , [ 'sign', 'verify' ] - ).then(function (result) { - return window.crypto.subtle.exportKey( - "jwk" - , result.privateKey - ).then(function (jwk) { - return window.crypto.subtle.exportKey( - "pkcs8" - , result.privateKey - ).then(function (keydata) { - return { - type: 'EC' - , privateJwk: jwk - , privatePem: derToPem(keydata, 'EC', 'PRIVATE') - }; - }); - }); - }); - } -, _generateRsa: function (opts) { - if (!opts.bitlength) { opts.bitlength = 2048; } - if (-1 === [ 2048, 4096 ].indexOf(opts.bitlength)) { - return PromiseA.reject("opts.bitlength = (" + typeof opts.bitlength + ") " + opts.bitlength + ": Are you serious?"); - } - - // https://github.com/diafygi/webcrypto-examples#rsa---generatekey - var extractable = true; - - return window.crypto.subtle.generateKey( - { name: "RSASSA-PKCS1-v1_5" - , modulusLength: opts.bitlength - , publicExponent: new Uint8Array([0x01, 0x00, 0x01]) - , hash: { name: "SHA-256" } - } - , extractable - , [ 'sign', 'verify' ] - ).then(function (result) { - return window.crypto.subtle.exportKey( - "jwk" - , result.privateKey - ).then(function (jwk) { - return window.crypto.subtle.exportKey( - "pkcs8" - , result.privateKey - ).then(function (keydata) { - return { - type: 'RSA' - , privateJwk: jwk - , privatePem: derToPem(keydata, 'RSA', 'PRIVATE') - }; - }); - }); - }); - } -}; - -}('undefined' === typeof module ? window : module.exports)); - -// How we might use this -// var Keypairs = require('keypairs').Keypairs +module.exports = require('./lib/keypairs.js'); diff --git a/lib/ec.js b/lib/ec.js new file mode 100644 index 0000000..e4434dc --- /dev/null +++ b/lib/ec.js @@ -0,0 +1,62 @@ +'use strict'; + +var ASN1 = require('./asn1-packer.js'); +var Enc = require('./encoding.js'); +var x509 = module.exports; + +// 1.2.840.10045.3.1.7 +// prime256v1 (ANSI X9.62 named elliptic curve) +var OBJ_ID_EC = '06 08 2A8648CE3D030107'.replace(/\s+/g, '').toLowerCase(); +// 1.3.132.0.34 +// secp384r1 (SECG (Certicom) named elliptic curve) +var OBJ_ID_EC_384 = '06 05 2B81040022'.replace(/\s+/g, '').toLowerCase(); +// 1.2.840.10045.2.1 +// ecPublicKey (ANSI X9.62 public key type) +var OBJ_ID_EC_PUB = '06 07 2A8648CE3D0201'.replace(/\s+/g, '').toLowerCase(); + +x509.packSec1 = function (jwk) { + 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( + ASN1('30' + , ASN1.UInt('01') + , ASN1('04', d) + , ASN1('A0', objId) + , ASN1('A1', ASN1.BitStr('04' + x + y))) + ); +}; +x509.packPkcs8 = function (jwk) { + 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( + ASN1('30' + , ASN1.UInt('00') + , ASN1('30' + , OBJ_ID_EC_PUB + , objId + ) + , ASN1('04' + , ASN1('30' + , ASN1.UInt('01') + , ASN1('04', d) + , ASN1('A1', ASN1.BitStr('04' + x + y))))) + ); +}; +x509.packSpki = function (jwk) { + 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( + ASN1('30' + , ASN1('30' + , OBJ_ID_EC_PUB + , objId + ) + , ASN1.BitStr('04' + x + y)) + ); +}; +x509.packPkix = x509.packSpki; diff --git a/lib/keypairs.js b/lib/keypairs.js new file mode 100644 index 0000000..e4fc43b --- /dev/null +++ b/lib/keypairs.js @@ -0,0 +1,104 @@ +'use strict'; + +/*global Promise*/ +var keypairs = module.exports; + +var PEM = require('./pem-parser.js'); +PEM.packBlock = require('./pem-packer.js').packBlock; + +var ASN1 = require('./asn1-parser.js'); +ASN1.pack = require('./asn1-packer.js').pack; + +var x509 = require('./x509-parser.js'); + +var SSH = require('./ssh-parser.js'); +SSH.pack = require('./ssh-packer.js').pack; + +// sign, signJws, signJwt +var JWS = require('./jws.js'); +var JWT = require('./jwt.js'); + +var RSA = require('./rsa.js'); +var EC = require('./ec.js'); + +keypairs.import = function (opts) { + return Promise.resolve().then(function () { + var jwk = opts.jwk; + var pem; + var der; + var typ; + + if (opts.pem) { + pem = PEM.parseBlock(opts.pem); + if (/OPENSSH/.test(pem.type)) { + jwk = SSH.parse(pem); + } else { + der = pem.bytes; + jwk = x509.parse(der); + } + } + if (opts.ssh) { + jwk = SSH.parse(opts.ssh); + } + if (jwk) { + // Both RSA and EC use 'd' as part of the private key + if (jwk.d) { + typ = 'PRIVATE KEY'; + der = x509.pack({ jwk: jwk, format: 'pkcs8', encoding: 'pem' }); + } else { + typ = 'PUBLIC KEY'; + der = x509.pack({ jwk: jwk, format: 'spki', encoding: 'pem' }); + } + pem = PEM.packBlock({ type: typ, bytes: der }); + } + + return { pem: pem, jwk: jwk }; + }); +}; + +keypairs.export = function (opts) { + // { pem, jwk, format, encoding } + var format = opts.format; + var encoding = opts.encoding; + var jwk = opts.jwk; + var pem = opts.pem; + var der = opts.der; + var pub = opts.public; + + if (opts.key) { + if ('string' === typeof opts.key) { + pem = opts.key; + } else if (opts.key.d) { + jwk = opts.key; + } else if (opts.key.length) { + der = opts.der; + } else { + throw new Error("'key' must be of type 'string' (PEM), 'object' (JWK), Buffer, or Array (DER)"); + } + } + if (!format) { format = 'jwk'; } + + if (!jwk) { + jwk = keypairs.import({ pem: pem }).jwk; + } + if (pub) { + if ('RSA' === jwk.kty) { + jwk = { kty: jwk.kty, n: jwk.n, e: jwk.e }; + } else { + jwk = { kty: jwk.kty, x: jwk.x, y: jwk.y }; + } + } + if ('jwk' === format) { + if (encoding && 'json' !== encoding) { + throw new Error("'encoding' must be 'json' for 'jwk'"); + } + return jwk; + } + + if ('openssh' === format || 'ssh' === format) { + // TODO if ('ssh' === format) { format = 'pkcs8'; } + // TODO 'ssh2' public key is a special variant of pkcs8 + return SSH.pack({ jwk: jwk, public: opts.public }); + } + return x509.pack({ jwk: jwk, format: opts.format, encoding: opts.encoding, public: opts.public }); +}; diff --git a/lib/x509-packer.js b/lib/x509-packer.js new file mode 100644 index 0000000..914b3e8 --- /dev/null +++ b/lib/x509-packer.js @@ -0,0 +1,14 @@ +'use strict'; + +var x509 = module.exports; + +var RSA = require('./rsa.js'); +var EC = require('./ec.js'); + +x509.pack = function (opts) { + if ('RSA' === opts.jwk.kty) { + return RSA.pack(opts); + } else { + return EC.pack(opts); + } +}; diff --git a/lib/x509-parser.js b/lib/x509-parser.js new file mode 100644 index 0000000..870d5ba --- /dev/null +++ b/lib/x509-parser.js @@ -0,0 +1,119 @@ +'use strict'; + +var Enc = require('./encoding.js'); +var x509 = module.exports; + +// 1.2.840.10045.3.1.7 +// prime256v1 (ANSI X9.62 named elliptic curve) +var OBJ_ID_EC = '06 08 2A8648CE3D030107'.replace(/\s+/g, '').toLowerCase(); +// 1.3.132.0.34 +// secp384r1 (SECG (Certicom) named elliptic curve) +var OBJ_ID_EC_384 = '06 05 2B81040022'.replace(/\s+/g, '').toLowerCase(); + +x509.parseSec1 = function parseEcOnlyPrivkey(u8, jwk) { + var index = 7; + var len = 32; + var olen = OBJ_ID_EC.length/2; + + if ("P-384" === jwk.crv) { + olen = OBJ_ID_EC_384.length/2; + index = 8; + len = 48; + } + if (len !== u8[index - 1]) { + throw new Error("Unexpected bitlength " + len); + } + + // private part is d + var d = u8.slice(index, index + len); + // compression bit index + var ci = index + len + 2 + olen + 2 + 3; + var c = u8[ci]; + var x, y; + + if (0x04 === c) { + y = u8.slice(ci + 1 + len, ci + 1 + len + len); + } else if (0x02 !== c) { + throw new Error("not a supported EC private key"); + } + x = u8.slice(ci + 1, ci + 1 + len); + + return { + kty: jwk.kty + , crv: jwk.crv + , d: Enc.bufToUrlBase64(d) + //, dh: Enc.bufToHex(d) + , x: Enc.bufToUrlBase64(x) + //, xh: Enc.bufToHex(x) + , y: Enc.bufToUrlBase64(y) + //, yh: Enc.bufToHex(y) + }; +}; + +x509.parsePkcs8 = function parseEcPkcs8(u8, jwk) { + var index = 24 + (OBJ_ID_EC.length/2); + var len = 32; + if ("P-384" === jwk.crv) { + index = 24 + (OBJ_ID_EC_384.length/2) + 2; + len = 48; + } + + //console.log(index, u8.slice(index)); + if (0x04 !== u8[index]) { + //console.log(jwk); + throw new Error("privkey not found"); + } + var d = u8.slice(index+2, index+2+len); + var ci = index+2+len+5; + var xi = ci+1; + var x = u8.slice(xi, xi + len); + var yi = xi+len; + var y; + if (0x04 === u8[ci]) { + y = u8.slice(yi, yi + len); + } else if (0x02 !== u8[ci]) { + throw new Error("invalid compression bit (expected 0x04 or 0x02)"); + } + + return { + kty: jwk.kty + , crv: jwk.crv + , d: Enc.bufToUrlBase64(d) + //, dh: Enc.bufToHex(d) + , x: Enc.bufToUrlBase64(x) + //, xh: Enc.bufToHex(x) + , y: Enc.bufToUrlBase64(y) + //, yh: Enc.bufToHex(y) + }; +}; + +x509.parseSpki = function parsePem(u8, jwk) { + var ci = 16 + OBJ_ID_EC.length/2; + var len = 32; + + if ("P-384" === jwk.crv) { + ci = 16 + OBJ_ID_EC_384.length/2; + len = 48; + } + + var c = u8[ci]; + var xi = ci + 1; + var x = u8.slice(xi, xi + len); + var yi = xi + len; + var y; + if (0x04 === c) { + y = u8.slice(yi, yi + len); + } else if (0x02 !== c) { + throw new Error("not a supported EC private key"); + } + + return { + kty: jwk.kty + , crv: jwk.crv + , x: Enc.bufToUrlBase64(x) + //, xh: Enc.bufToHex(x) + , y: Enc.bufToUrlBase64(y) + //, yh: Enc.bufToHex(y) + }; +}; +x509.parsePkix = x509.parseSpki;