forked from coolaj86/eckles.js
251 lines
7.9 KiB
JavaScript
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;
|