UNPKG

ec-key-patch

Version:

Wrapper around an Elliptic Curve private or public keys

589 lines (492 loc) 18 kB
'use strict'; const crypto = require('crypto'); const asn = require('asn1.js'); const util = require('util'); /* ========================================================================== * * From RFC-4492 (Appendix A) Equivalent Curves (Informative) * * ========================================================================== * * * * +------------------------------------------------------------------------+ * * | Curve names chosen by | * * | different standards organizations | * * +-----------+------------+------------+------------+---------------------+ * * | SECG | ANSI X9.62 | NIST | OpenSSL | ASN.1 OID | * * +-----------+------------+------------+------------+---------------------+ * * | secp256r1 | prime256v1 | NIST P-256 | prime256v1 | 1.2.840.10045.3.1.7 | * * | secp256k1 | | | secp256k1 | 1.3.132.0.10 | * * | secp384r1 | | NIST P-384 | secp384r1 | 1.3.132.0.34 | * * | secp521r1 | | NIST P-521 | secp521r1 | 1.3.132.0.35 | * * +-----------+------------+------------+------------+---------------------+ * * ========================================================================== */ /* Byte lengths for validation */ const lengths = { prime256v1 : Math.ceil(256 / 8), secp256k1 : Math.ceil(256 / 8), secp384r1 : Math.ceil(384 / 8), secp521r1 : Math.ceil(521 / 8) }; /* JWK curve names */ const jwkCurves = { prime256v1 : 'P-256', secp256k1 : 'P-256K', /* NOT A STANDARD NAME: See the README.md file */ secp384r1 : 'P-384', secp521r1 : 'P-521' }; /* OpenSSL curve names */ const curves = { 'P-256' : 'prime256v1', 'P-256K' : 'secp256k1', /* NOT A STANDARD NAME: See the README.md file */ 'P-384' : 'secp384r1', 'P-521' : 'secp521r1' }; /* ========================================================================== * * ASN.1 * * ========================================================================== */ const ASN1ECRfc5915Key = asn.define('Rfc5915Key', function() { this.seq().obj( this.key('version').int(), this.key('privateKey').octstr(), this.key('parameters').optional().explicit(0).objid({ '1 2 840 10045 3 1 7' : 'prime256v1', '1 3 132 0 10' : 'secp256k1', '1 3 132 0 34' : 'secp384r1', '1 3 132 0 35' : 'secp521r1' }), this.key('publicKey').optional().explicit(1).bitstr() ); }); /* ========================================================================== */ const ASN1ECPkcs8Key = asn.define('Pkcs8Key', function() { this.seq().obj( this.key('version').int(), this.key('algorithmIdentifier').seq().obj( this.key('privateKeyType').objid({ '1 2 840 10045 2 1': 'EC' }), this.key('parameters').objid({ '1 2 840 10045 3 1 7' : 'prime256v1', '1 3 132 0 10' : 'secp256k1', '1 3 132 0 34' : 'secp384r1', '1 3 132 0 35' : 'secp521r1' }) ), this.key('privateKey').octstr() ); }); /* ========================================================================== */ const ASN1ECSpkiKey = asn.define('SpkiKey', function() { this.seq().obj( this.key('algorithmIdentifier').seq().obj( this.key('publicKeyType').objid({ '1 2 840 10045 2 1': 'EC' }), this.key('parameters').objid({ '1 2 840 10045 3 1 7' : 'prime256v1', '1 3 132 0 10' : 'secp256k1', '1 3 132 0 34' : 'secp384r1', '1 3 132 0 35' : 'secp521r1' }) ), this.key('publicKey').bitstr() ); }); /* ========================================================================== * * ASN.1 PARSING * * ========================================================================== */ /* Parse a public key buffer, split X and Y */ function parsePublicKeyBuffer(curve, buffer) { var bytes = lengths[curve]; if (buffer[0] == 4) { if (buffer.length != ((bytes * 2) + 1)) throw new TypeError('Invalid uncompressed key size'); return { c: curve, x: buffer.slice(1, bytes + 1), y: buffer.slice(bytes + 1), } } else { throw new TypeError("Compressed key unsupported"); } } /* Parse PKCS8 from RFC 5208 */ function parsePkcs8(buffer) { var key = ASN1ECPkcs8Key.decode(buffer, 'der'); var privateKeyWrapper = ASN1ECRfc5915Key.decode(key.privateKey, 'der'); var curve = key.algorithmIdentifier.parameters; var bytes = lengths[curve]; var privateKey = privateKeyWrapper.privateKey; if (privateKey.length > bytes) { throw new TypeError('Invalid private key size: expected ' + bytes + ' gotten ' + privateKey.length); } var components = parsePublicKeyBuffer(curve, privateKeyWrapper.publicKey.data); components.d = privateKey; return components; } /* Parse EC from RFC 5915 */ function parseRfc5915(buffer) { var key = ASN1ECRfc5915Key.decode(buffer, 'der'); var bytes = lengths[key.parameters]; var privateKey = key.privateKey; if (privateKey.length > bytes) { throw new TypeError('Invalid private key size: expected ' + bytes + ' gotten ' + privateKey.length); } var components = parsePublicKeyBuffer(key.parameters, key.publicKey.data); components.d = privateKey; return components; } /* Parse SPKI from RFC 5280 */ function parseSpki(buffer) { var key = ASN1ECSpkiKey.decode(buffer, 'der'); return parsePublicKeyBuffer(key.algorithmIdentifier.parameters, key.publicKey.data); } /* ========================================================================== * * PEM PARSING * * ========================================================================== */ const pemRfc5915RE = /-+BEGIN EC PRIVATE KEY-+([\s\S]+)-+END EC PRIVATE KEY-+/m; const pemPkcs8RE = /-+BEGIN PRIVATE KEY-+([\s\S]*)-+END PRIVATE KEY-+/m; const pemSpkiRE = /-+BEGIN PUBLIC KEY-+([\s\S]*)-+END PUBLIC KEY-+/m; function parsePem(pem) { if (! util.isString(pem)) throw new TypeError("PEM must be a string"); var match = null; if (match = pem.match(pemRfc5915RE)) { var buffer = new Buffer(match[1].replace(/[\s-]/mg, ''), 'base64'); return parseRfc5915(buffer); } else if (match = pem.match(pemPkcs8RE)) { var buffer = new Buffer(match[1].replace(/[\s-]/mg, ''), 'base64'); return parsePkcs8(buffer); } else if (match = pem.match(pemSpkiRE)) { var buffer = new Buffer(match[1].replace(/[\s-]/mg, ''), 'base64'); return parseSpki(buffer); } else { console.log(pem); throw new TypeError('Unrecognized PEM key structure'); } } /* ========================================================================== * * CLASS DEFINITION * * ========================================================================== */ function ECKey(key, format) { if (!(this instanceof ECKey)) return new ECKey(key, format); var curve, d, x, y; if (! format) format = 'pem'; // BUFFER KEYS: either in "pkcs8" or "spki" format (base64) or "pem" (ascii) if (util.isBuffer(key)) { if (format == 'pem') { var k = parsePem(key.toString('ascii')); curve = k.c; x = k.x; y = k.y; d = k.d; } else if ((format == "pkcs8") || (format == "rfc5208")) { var k = parsePkcs8(key); curve = k.c; x = k.x; y = k.y; d = k.d; } else if ((format == "spki") || (format == "rfc5280")) { var k = parseSpki(key); curve = k.c; x = k.x; y = k.y; d = k.d; } else { throw new TypeError("Unknown format for EC Key \"" + format + "\""); } } // STRING KEYS: base64 all the time, but also allowed in PEM else if (util.isString(key)) { if (format == 'pem') { var k = parsePem(key); curve = k.c; x = k.x; y = k.y; d = k.d; } else if ((format == "pkcs8") || (format == "rfc5208")) { var k = parsePkcs8(new Buffer(key, 'base64')); curve = k.c; x = k.x; y = k.y; d = k.d; } else if ((format == "spki") || (format == "rfc5280")) { var k = parseSpki(new Buffer(key, 'base64')); curve = k.c; x = k.x; y = k.y; d = k.d; } else { throw new TypeError("Unknown format for EC Key \"" + format + "\""); } } // OBJECT KEY: needs to contain "(curve|crv)", "(d|privateKey)" if it's a // private key and "(publicKey|x,y)" always required (both for priv and pub) else if (util.isObject(key)) { // Curves if (util.isString(key.curve)) { curve = key.curve; } else if (util.isString(key.crv)) { curve = curves[key.crv] || key.crv; } // Private key or "d" if (util.isBuffer(key.privateKey)) { d = key.privateKey; } else if (util.isString(key.privateKey)) { d = new Buffer(key.privateKey, 'base64'); } else if (util.isBuffer(key.d)) { d = key.d; } else if (util.isString(key.d)) { d = new Buffer(key.d, 'base64'); } // Public key, or x and y if (util.isBuffer(key.publicKey)) { var k = parsePublicKeyBuffer(curve, key.publicKey); x = k.x; y = k.y; } else if (util.isString(key.publicKey)) { var k = parsePublicKeyBuffer(curve, new Buffer(key.publicKey, 'base64')); x = k.x; y = k.y; } else { // Need to get x and y if (util.isBuffer(key.x)) { x = key.x; } else if (util.isString(key.x)) { x = new Buffer(key.x, 'base64'); } if (util.isBuffer(key.y)) { y = key.y; } else if (util.isString(key.y)) { y = new Buffer(key.y, 'base64'); } } } else { throw new TypeError('Unrecognized format for EC key'); } // Validate curve, d, x and y if (! curve) throw new TypeError("EC Key curve not specified"); if ((! x) || (! y)) throw new TypeError("Public EC Key point unavailable"); var length = lengths[curve]; if (! length) throw new TypeError("EC Key curve \"" + curve + "\" invalid"); if (x.length != length) throw new TypeError("Public EC Key point X of wrong length"); if (y.length != length) throw new TypeError("Public EC Key point Y of wrong length"); if (d && (y.length != length)) throw new TypeError("Private EC Key of wrong length"); // Define our properties Object.defineProperties(this, { 'curve': { enumerable: true, configurable: false, value: curve }, 'isPrivateECKey': { enumerable: true, configurable: false, value: (d != null) }, 'x': { enumerable: true, configurable: false, get: function() { return new Buffer(x) } }, 'y': { enumerable: true, configurable: false, get: function() { return new Buffer(y) } }, // "non-enumerable" 'jsonCurve': { enumerable: false, configurable: false, value: jwkCurves[curve] }, 'publicCodePoint': { enumerable: false, configurable: false, get: function() { return Buffer.concat([new Buffer([0x04]), x, y]); } } }); // The "d" (private key) is optional if (d) Object.defineProperty(this, 'd', { enumerable: true, configurable: false, get: function() { return new Buffer(d) } }); } ECKey.createECKey = function(curve) { if (!curve) curve = 'prime256v1'; if (curves[curve]) curve = curves[curve]; if (!jwkCurves[curve]) throw new TypeError("Invalid/unknown curve \"" + curve + "\""); var ecdh = crypto.createECDH(curve); ecdh.generateKeys(); return new ECKey({ privateKey: ecdh.getPrivateKey(), publicKey: ecdh.getPublicKey(), curve: curve }); } /* ========================================================================== * * ECDH/SIGNING/VALIDATION * * ========================================================================== */ ECKey.prototype.computeSecret = function computeSecret(key) { return this.createECDH().computeSecret(key); } ECKey.prototype.createECDH = function createECDH() { if (this.isPrivateECKey) { var ecdh = crypto.createECDH(this.curve); ecdh.keys = ecdh.curve.keyFromPublic(this.publicCodePoint) ecdh.setPrivateKey(this.d); var ecdhComputeSecret = ecdh.computeSecret; ecdh.computeSecret = function(key) { if (key instanceof ECKey) { return ecdhComputeSecret.call(ecdh, key.publicCodePoint); } else { return ecdhComputeSecret.apply(ecdh, arguments); } } return ecdh; } else { throw new Error('Can only create ECDH from private keys'); } } ECKey.prototype.createSign = function createSign(hash) { if (! this.isPrivateECKey) throw new Error("EC Private Key needed to sign"); var sign = crypto.createSign('RSA-' + hash); // RSA works with EC keys, too var signFunction = sign.sign; var self = this; sign.sign = function(format) { return signFunction.call(sign, self.toString("pem"), format); } return sign; } ECKey.prototype.createVerify = function createVerify(hash) { var verify = crypto.createVerify('RSA-' + hash); // RSA works with EC keys, too var verifyFunction = verify.verify; var key = this.isPrivateECKey ? this.asPublicECKey() : this; verify.verify = function(signature, format) { return verifyFunction.call(verify, key.toString("pem"), signature, format); } return verify; } ECKey.prototype.asPublicECKey = function asPublicECKey() { if (! this.isPrivateECKey) return this; return new ECKey({ curve: this.curve, x: this.x, y: this.y }); } /* ========================================================================== * * CONVERSION * * ========================================================================== */ ECKey.prototype.toBuffer = function toBuffer(format) { if (! format) format = 'pem'; // Simple PEM conversion, wrapping the string in the buffer if (format == 'pem') return new Buffer(this.toString('pem'), 'ascii'); if (this.isPrivateECKey) { // Strip leading zeroes from private key var d = this.d; while (d[0] == 0) d = d.slice(1); // Known formats: "pkcs8" (default), "pem", "openssl" if ((format == "pkcs8") || (format == "rfc5208")) { // Encode in PKCS8 return ASN1ECPkcs8Key.encode({ version: 0, algorithmIdentifier: { privateKeyType: 'EC', parameters: this.curve, }, // Private key is RFC5915 minus curve privateKey: ASN1ECRfc5915Key.encode({ version: 1, privateKey: d, publicKey: { data: this.publicCodePoint } }, 'der') }, 'der'); } else if (format == "rfc5915") { // Simply encode in ASN.1 return ASN1ECRfc5915Key.encode({ version: 1, privateKey: d, parameters: this.curve, publicKey: { data: this.publicCodePoint } }, 'der'); } else { throw new TypeError("Unknown format for private key \"" + format + "\""); } } else { if ((format == "spki") || (format == "rfc5280")) { return ASN1ECSpkiKey.encode({ algorithmIdentifier: { publicKeyType: 'EC', parameters: this.curve }, publicKey: { data: this.publicCodePoint } }, 'der'); } else { throw new TypeError("Unknown format for public key \"" + format + "\""); } } } ECKey.prototype.toString = function toString(format) { if (! format) format = "pem"; if (this.isPrivateECKey) { if (format == "pem") { // pkcs8, wrapped return '-----BEGIN PRIVATE KEY-----\n' + this.toBuffer('pkcs8').toString('base64').match(/.{1,64}/g).join('\n') + '\n-----END PRIVATE KEY-----\n'; } else if (format == "rfc5915") { // rfc5915, wrapped return '-----BEGIN EC PRIVATE KEY-----\n' + this.toBuffer('rfc5915').toString('base64').match(/.{1,64}/g).join('\n') + '\n-----END EC PRIVATE KEY-----\n'; } else if ((format == "pkcs8") || (format == "rfc5208")) { return this.toBuffer('pkcs8').toString('base64'); } else if ((format == "spki") || (format == "rfc5280")) { return this.toBuffer('spki').toString('base64'); } else { throw new TypeError("Unknown format for private key \"" + format + "\""); } } else { if (format == "pem") { return '-----BEGIN PUBLIC KEY-----\n' + this.toBuffer('spki').toString('base64').match(/.{1,64}/g).join('\n') + '\n-----END PUBLIC KEY-----\n'; } else if ((format == "spki") || (format == "rfc5280")) { return this.toBuffer('spki').toString('base64'); } else { throw new TypeError("Unknown format for public key \"" + format + "\""); } } } ECKey.prototype.toJSON = function toJSON() { function urlsafe(buffer) { return buffer.toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } var jwk = { kty: "EC", crv: jwkCurves[this.curve], x: urlsafe(this.x), y: urlsafe(this.y), }; var d = this.d; if (d) { var bytes = lengths[this.curve]; if (d.length < bytes) { var remaining = bytes - d.length; d = Buffer.concat([new Buffer(remaining).fill(0), d]); } jwk.d = urlsafe(d); } return jwk; } /* ========================================================================== * * EXPORTS * * ========================================================================== */ exports = module.exports = ECKey;