eckles.js/lib/eckles.js

251 lines
7.9 KiB
JavaScript

'use strict';
var EC = module.exports;
var Enc = require('./encoding.js');
var PEM = require('./pem.js');
var SSH = require('./ssh.js');
var x509 = require('./x509.js');
// 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();
// The one good thing that came from the b***kchain hysteria: good EC documentation
// https://davidederosa.com/basic-blockchain-programming/elliptic-curve-keys/
/*global Promise*/
EC.generate = function (opts) {
return Promise.resolve().then(function () {
var typ = 'ec';
var format = opts.format;
var encoding = opts.encoding;
var priv;
var pub = 'spki';
if (!format) {
format = 'jwk';
}
if (-1 !== [ 'spki', 'pkcs8', 'ssh' ].indexOf(format)) {
format = 'pkcs8';
}
if ('pem' === format) {
format = 'sec1';
encoding = 'pem';
} else if ('der' === format) {
format = 'sec1';
encoding = 'der';
}
if ('jwk' === format || 'json' === format) {
format = 'jwk';
encoding = 'json';
} else {
priv = format;
}
if (!encoding) {
encoding = 'pem';
}
if (priv) {
priv = { type: priv, format: encoding };
pub = { type: pub, format: encoding };
} else {
// jwk
priv = { type: 'sec1', format: 'pem' };
pub = { type: 'spki', format: 'pem' };
}
return new Promise(function (resolve, reject) {
return require('crypto').generateKeyPair(typ, {
namedCurve: opts.crv || opts.namedCurve || 'P-256'
, privateKeyEncoding: priv
, publicKeyEncoding: pub
}, function (err, pubkey, privkey) {
if (err) { reject(err); }
resolve({
private: privkey
, public: pubkey
});
});
}).then(function (keypair) {
if ('jwk' === format) {
return {
private: EC.importSync({ pem: keypair.private, format: priv.type })
, public: EC.importSync({ pem: keypair.public, format: pub.type, public: true })
};
}
if ('ssh' !== opts.format) {
return keypair;
}
return {
private: keypair.private
, public: EC.exportSync({ jwk: EC.importSync({
pem: keypair.public, format: format, public: true
}), format: opts.format, public: true })
};
});
});
};
EC.importSync = function importEcSync(opts) {
if (!opts || !opts.pem || 'string' !== typeof opts.pem) {
throw new Error("must pass { pem: pem } as a string");
}
if (0 === opts.pem.indexOf('ecdsa-sha2-')) {
return SSH.parseSsh(opts.pem);
}
var pem = opts.pem;
var u8 = PEM.parseBlock(pem).bytes;
var hex = Enc.bufToHex(u8);
var jwk = { kty: 'EC', crv: null, x: null, y: null };
//console.log();
if (-1 !== hex.indexOf(OBJ_ID_EC)) {
jwk.crv = "P-256";
// PKCS8
if (0x02 === u8[3] && 0x30 === u8[6] && 0x06 === u8[8]) {
//console.log("PKCS8", u8[3].toString(16), u8[6].toString(16), u8[8].toString(16));
jwk = x509.parsePkcs8(u8, jwk);
// EC-only
} else if (0x02 === u8[2] && 0x04 === u8[5] && 0xA0 === u8[39]) {
//console.log("EC---", u8[2].toString(16), u8[5].toString(16), u8[39].toString(16));
jwk = x509.parseSec1(u8, jwk);
// SPKI/PKIK (Public)
} else if (0x30 === u8[2] && 0x06 === u8[4] && 0x06 === u8[13]) {
//console.log("SPKI-", u8[2].toString(16), u8[4].toString(16), u8[13].toString(16));
jwk = x509.parseSpki(u8, jwk);
// Error
} else {
//console.log("PKCS8", u8[3].toString(16), u8[6].toString(16), u8[8].toString(16));
//console.log("EC---", u8[2].toString(16), u8[5].toString(16), u8[39].toString(16));
//console.log("SPKI-", u8[2].toString(16), u8[4].toString(16), u8[13].toString(16));
throw new Error("unrecognized key format");
}
} else if (-1 !== hex.indexOf(OBJ_ID_EC_384)) {
jwk.crv = "P-384";
// PKCS8
if (0x02 === u8[3] && 0x30 === u8[6] && 0x06 === u8[8]) {
//console.log("PKCS8", u8[3].toString(16), u8[6].toString(16), u8[8].toString(16));
jwk = x509.parsePkcs8(u8, jwk);
// EC-only
} else if (0x02 === u8[3] && 0x04 === u8[6] && 0xA0 === u8[56]) {
//console.log("EC---", u8[3].toString(16), u8[6].toString(16), u8[56].toString(16));
jwk = x509.parseSec1(u8, jwk);
// SPKI/PKIK (Public)
} else if (0x30 === u8[2] && 0x06 === u8[4] && 0x06 === u8[13]) {
//console.log("SPKI-", u8[2].toString(16), u8[4].toString(16), u8[13].toString(16));
jwk = x509.parseSpki(u8, jwk);
// Error
} else {
//console.log("PKCS8", u8[3].toString(16), u8[6].toString(16), u8[8].toString(16));
//console.log("EC---", u8[3].toString(16), u8[6].toString(16), u8[56].toString(16));
//console.log("SPKI-", u8[2].toString(16), u8[4].toString(16), u8[13].toString(16));
throw new Error("unrecognized key format");
}
} else {
throw new Error("Supported key types are P-256 and P-384");
}
if (opts.public) {
if (true !== opts.public) {
throw new Error("options.public must be either `true` or `false` not ("
+ typeof opts.public + ") '" + opts.public + "'");
}
delete jwk.d;
}
return jwk;
};
EC.parse = function parseEc(opts) {
return Promise.resolve().then(function () {
return EC.importSync(opts);
});
};
EC.toJwk = EC.import = EC.parse;
EC.exportSync = 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);
});
};
EC.__thumbprint = function (jwk) {
var buf = require('crypto').createHash('sha256')
// alphabetically sorted keys [ 'crv', 'kty', 'x', 'y' ]
.update('{"crv":"' + jwk.crv + '","kty":"EC","x":"' + jwk.x + '","y":"' + jwk.y + '"}')
.digest()
;
return Enc.bufToUrlBase64(buf);
};
EC.thumbprint = function (opts) {
return Promise.resolve().then(function () {
var jwk;
if ('EC' === opts.kty) {
jwk = opts;
} else if (opts.jwk) {
jwk = opts.jwk;
} else {
jwk = EC.importSync(opts);
}
return EC.__thumbprint(jwk);
});
};
EC.toPem = EC.export = EC.pack;