466 lines
12 KiB
JavaScript
466 lines
12 KiB
JavaScript
/*!
|
|
* algorithms/ecdh.js - Elliptic Curve Diffie-Hellman algorithms
|
|
*
|
|
* Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file.
|
|
*/
|
|
"use strict";
|
|
|
|
var merge = require("../util/merge"),
|
|
util = require("../util"),
|
|
ecUtil = require("./ec-util.js"),
|
|
hkdf = require("./hkdf.js"),
|
|
concat = require("./concat.js"),
|
|
aesKw = require("./aes-kw.js"),
|
|
helpers = require("./helpers.js"),
|
|
CONSTANTS = require("./constants.js");
|
|
|
|
var clone = require("lodash/clone");
|
|
var omit = require("lodash/omit");
|
|
var pick = require("lodash/pick");
|
|
|
|
function idealHash(curve) {
|
|
switch (curve) {
|
|
case "P-256":
|
|
return "SHA-256";
|
|
case "P-384":
|
|
return "SHA-384";
|
|
case "P-521":
|
|
return "SHA-512";
|
|
default:
|
|
throw new Error("unsupported curve: " + curve);
|
|
}
|
|
}
|
|
|
|
// ### Exported
|
|
var ecdh = module.exports = {};
|
|
|
|
// ### Derivation algorithms
|
|
// ### "raw" ECDH
|
|
function ecdhDeriveFn() {
|
|
var alg = {
|
|
name: "ECDH"
|
|
};
|
|
|
|
var validatePublic = function(pk, form) {
|
|
var pubKey = pk && ecUtil.convertToForge(pk, true);
|
|
if (!pubKey || !pubKey.isValid()) {
|
|
return Promise.reject(new Error("invalid EC public key"));
|
|
}
|
|
|
|
switch (form) {
|
|
case "jwk":
|
|
pubKey = ecUtil.convertToJWK(pk, true);
|
|
break;
|
|
case "buffer":
|
|
pubKey = ecUtil.convertToBuffer(pk, true);
|
|
break;
|
|
}
|
|
return Promise.resolve(pubKey);
|
|
}
|
|
|
|
// ### fallback implementation -- uses ecc + forge
|
|
var fallback = function(key, props) {
|
|
props = props || {};
|
|
var keyLen = props.length || 0;
|
|
// assume {key} is privateKey
|
|
// assume {props.public} is publicKey
|
|
var privKey = ecUtil.convertToForge(key, false);
|
|
|
|
var p = validatePublic(props.public, "forge");
|
|
p = p.then(function(pubKey) {
|
|
// {pubKey} is "forge"
|
|
|
|
var secret = privKey.computeSecret(pubKey);
|
|
if (keyLen) {
|
|
// truncate to requested key length
|
|
if (secret.length < keyLen) {
|
|
return Promise.reject(new Error("key length too large: " + keyLen));
|
|
}
|
|
secret = secret.slice(0, keyLen);
|
|
}
|
|
|
|
return secret;
|
|
});
|
|
return p;
|
|
};
|
|
|
|
// ### WebCryptoAPI implementation
|
|
// TODO: cache CryptoKey sooner
|
|
var webcrypto = function(key, props) {
|
|
key = key || {};
|
|
props = props || {};
|
|
|
|
var keyLen = props.length || 0,
|
|
algParams = merge(clone(alg), {
|
|
namedCurve: key.crv
|
|
});
|
|
|
|
// assume {key} is privateKey
|
|
if (!keyLen) {
|
|
// calculate key length from private key size
|
|
keyLen = key.d.length;
|
|
}
|
|
var privKey = ecUtil.convertToJWK(key, false);
|
|
privKey = helpers.subtleCrypto.importKey("jwk",
|
|
privKey,
|
|
algParams,
|
|
false,
|
|
[ "deriveBits" ]);
|
|
|
|
// assume {props.public} is publicKey
|
|
var pubKey = validatePublic(props.public, "jwk");
|
|
pubKey = pubKey.then(function(pubKey) {
|
|
// {pubKey} is "jwk"
|
|
return helpers.subtleCrypto.importKey("jwk",
|
|
pubKey,
|
|
algParams,
|
|
false,
|
|
[]);
|
|
});
|
|
|
|
var p = Promise.all([privKey, pubKey]);
|
|
p = p.then(function(keypair) {
|
|
var privKey = keypair[0],
|
|
pubKey = keypair[1];
|
|
|
|
var algParams = merge(clone(alg), {
|
|
public: pubKey
|
|
});
|
|
return helpers.subtleCrypto.deriveBits(algParams, privKey, keyLen * 8);
|
|
});
|
|
p = p.then(function(result) {
|
|
result = Buffer.from(result);
|
|
return result;
|
|
});
|
|
return p;
|
|
};
|
|
|
|
var nodejs = function(key, props) {
|
|
if ("function" !== typeof helpers.nodeCrypto.createECDH) {
|
|
throw new Error("unsupported algorithm: ECDH");
|
|
}
|
|
|
|
props = props || {};
|
|
var keyLen = props.length || 0;
|
|
var curve;
|
|
switch (key.crv) {
|
|
case "P-256":
|
|
curve = "prime256v1";
|
|
break;
|
|
case "P-384":
|
|
curve = "secp384r1";
|
|
break;
|
|
case "P-521":
|
|
curve = "secp521r1";
|
|
break;
|
|
default:
|
|
return Promise.reject(new Error("invalid curve: " + curve));
|
|
}
|
|
|
|
// assume {key} is privateKey
|
|
// assume {props.public} is publicKey
|
|
var privKey = ecUtil.convertToBuffer(key, false);
|
|
|
|
var p = validatePublic(props.public, "buffer");
|
|
p = p.then(function(pubKey) {
|
|
// {pubKey} is "buffer"
|
|
var ecdh = helpers.nodeCrypto.createECDH(curve);
|
|
// dummy call so computeSecret doesn't fail
|
|
// ecdh.generateKeys();
|
|
ecdh.setPrivateKey(privKey);
|
|
var secret = ecdh.computeSecret(pubKey);
|
|
if (keyLen) {
|
|
if (secret.length < keyLen) {
|
|
return Promise.reject(new Error("key length too large: " + keyLen));
|
|
}
|
|
secret = secret.slice(0, keyLen);
|
|
}
|
|
return secret;
|
|
});
|
|
return p;
|
|
};
|
|
|
|
return helpers.setupFallback(nodejs, webcrypto, fallback);
|
|
}
|
|
|
|
function ecdhConcatDeriveFn() {
|
|
// NOTE: no nodejs/webcrypto/fallback model, since this algorithm is
|
|
// implemented using other primitives
|
|
|
|
var fn = function(key, props) {
|
|
props = props || {};
|
|
|
|
var hash;
|
|
try {
|
|
hash = props.hash || idealHash(key.crv);
|
|
if (!hash) {
|
|
throw new Error("invalid hash: " + hash);
|
|
}
|
|
hash.toUpperCase();
|
|
} catch (ex) {
|
|
return Promise.reject(ex);
|
|
}
|
|
|
|
var params = ["public"];
|
|
// derive shared secret
|
|
// NOTE: whitelist items from {props} for ECDH
|
|
var promise = ecdh.ECDH.derive(key, pick(props, params));
|
|
// expand
|
|
promise = promise.then(function(shared) {
|
|
// NOTE: blacklist items from {props} for ECDH
|
|
return concat["CONCAT-" + hash].derive(shared, omit(props, params));
|
|
});
|
|
return promise;
|
|
};
|
|
|
|
return fn;
|
|
}
|
|
|
|
function ecdhHkdfDeriveFn() {
|
|
// NOTE: no nodejs/webcrypto/fallback model, since this algorithm is
|
|
// implemented using other primitives
|
|
|
|
var fn = function(key, props) {
|
|
props = props || {};
|
|
|
|
var hash;
|
|
try {
|
|
hash = props.hash || idealHash(key.crv);
|
|
if (!hash) {
|
|
throw new Error("invalid hash: " + hash);
|
|
}
|
|
hash.toUpperCase();
|
|
} catch (ex) {
|
|
return Promise.reject(ex);
|
|
}
|
|
|
|
var params = ["public"];
|
|
// derive shared secret
|
|
// NOTE: whitelist items from {props} for ECDH
|
|
var promise = ecdh.ECDH.derive(key, pick(props, params));
|
|
// extract-and-expand
|
|
promise = promise.then(function(shared) {
|
|
// NOTE: blacklist items from {props} for ECDH
|
|
return hkdf["HKDF-" + hash].derive(shared, omit(props, params));
|
|
});
|
|
return promise;
|
|
};
|
|
|
|
return fn;
|
|
}
|
|
|
|
// ### Wrap/Unwrap algorithms
|
|
function doEcdhesCommonDerive(privKey, pubKey, props) {
|
|
function prependLen(input) {
|
|
return Buffer.concat([
|
|
helpers.int32ToBuffer(input.length),
|
|
input
|
|
]);
|
|
}
|
|
|
|
var algId = props.algorithm || "",
|
|
keyLen = CONSTANTS.KEYLENGTH[algId],
|
|
apu = util.asBuffer(props.apu || "", "base64url"),
|
|
apv = util.asBuffer(props.apv || "", "base64url");
|
|
var otherInfo = Buffer.concat([
|
|
prependLen(Buffer.from(algId, "utf8")),
|
|
prependLen(apu),
|
|
prependLen(apv),
|
|
helpers.int32ToBuffer(keyLen)
|
|
]);
|
|
|
|
var params = {
|
|
public: pubKey,
|
|
length: keyLen / 8,
|
|
hash: "SHA-256",
|
|
otherInfo: otherInfo
|
|
};
|
|
return ecdh["ECDH-CONCAT"].derive(privKey, params);
|
|
}
|
|
|
|
function ecdhesDirEncryptFn() {
|
|
// NOTE: no nodejs/webcrypto/fallback model, since this algorithm is
|
|
// implemented using other primitives
|
|
var fn = function(key, pdata, props) {
|
|
props = props || {};
|
|
|
|
// {props.epk} is private
|
|
if (!props.epk || !props.epk.d) {
|
|
return Promise.reject(new Error("missing ephemeral private key"));
|
|
}
|
|
var epk = ecUtil.convertToObj(props.epk, false);
|
|
|
|
// {key} is public
|
|
if (!key || !key.x || !key.y) {
|
|
return Promise.reject(new Error("missing static public key"));
|
|
}
|
|
var spk = ecUtil.convertToObj(key, true);
|
|
|
|
// derive ECDH shared
|
|
var promise = doEcdhesCommonDerive(epk, spk, {
|
|
algorithm: props.enc,
|
|
apu: props.apu,
|
|
apv: props.apv
|
|
});
|
|
promise = promise.then(function(shared) {
|
|
return {
|
|
data: shared,
|
|
once: true,
|
|
direct: true
|
|
};
|
|
});
|
|
return promise;
|
|
};
|
|
|
|
return fn;
|
|
}
|
|
function ecdhesDirDecryptFn() {
|
|
// NOTE: no nodejs/webcrypto/fallback model, since this algorithm is
|
|
// implemented using other primitives
|
|
var fn = function(key, cdata, props) {
|
|
props = props || {};
|
|
|
|
// {props.epk} is public
|
|
if (!props.epk || !props.epk.x || !props.epk.y) {
|
|
return Promise.reject(new Error("missing ephemeral public key"));
|
|
}
|
|
var epk = ecUtil.convertToObj(props.epk, true);
|
|
|
|
// {key} is private
|
|
if (!key || !key.d) {
|
|
return Promise.reject(new Error("missing static private key"));
|
|
}
|
|
var spk = ecUtil.convertToObj(key, false);
|
|
|
|
// derive ECDH shared
|
|
var promise = doEcdhesCommonDerive(spk, epk, {
|
|
algorithm: props.enc,
|
|
apu: props.apu,
|
|
apv: props.apv
|
|
});
|
|
promise = promise.then(function(shared) {
|
|
return shared;
|
|
});
|
|
return promise;
|
|
};
|
|
|
|
return fn;
|
|
}
|
|
|
|
function ecdhesKwEncryptFn(wrap) {
|
|
// NOTE: no nodejs/webcrypto/fallback model, since this algorithm is
|
|
// implemented using other primitives
|
|
var fn = function(key, pdata, props) {
|
|
props = props || {};
|
|
|
|
// {props.epk} is private
|
|
if (!props.epk || !props.epk.d) {
|
|
return Promise.reject(new Error("missing ephemeral private key"));
|
|
}
|
|
var epk = ecUtil.convertToObj(props.epk, false);
|
|
|
|
// {key} is public
|
|
if (!key || !key.x || !key.y) {
|
|
return Promise.reject(new Error("missing static public key"));
|
|
}
|
|
var spk = ecUtil.convertToObj(key, true);
|
|
|
|
// derive ECDH shared
|
|
var promise = doEcdhesCommonDerive(epk, spk, {
|
|
algorithm: props.alg,
|
|
apu: props.apu,
|
|
apv: props.apv
|
|
});
|
|
promise = promise.then(function(shared) {
|
|
// wrap provided key with ECDH shared
|
|
return wrap(shared, pdata);
|
|
});
|
|
return promise;
|
|
};
|
|
|
|
return fn;
|
|
}
|
|
|
|
function ecdhesKwDecryptFn(unwrap) {
|
|
// NOTE: no nodejs/webcrypto/fallback model, since this algorithm is
|
|
// implemented using other primitives
|
|
var fn = function(key, cdata, props) {
|
|
props = props || {};
|
|
|
|
// {props.epk} is public
|
|
if (!props.epk || !props.epk.x || !props.epk.y) {
|
|
return Promise.reject(new Error("missing ephemeral public key"));
|
|
}
|
|
var epk = ecUtil.convertToObj(props.epk, true);
|
|
|
|
// {key} is private
|
|
if (!key || !key.d) {
|
|
return Promise.reject(new Error("missing static private key"));
|
|
}
|
|
var spk = ecUtil.convertToObj(key, false);
|
|
|
|
// derive ECDH shared
|
|
var promise = doEcdhesCommonDerive(spk, epk, {
|
|
algorithm: props.alg,
|
|
apu: props.apu,
|
|
apv: props.apv
|
|
});
|
|
promise = promise.then(function(shared) {
|
|
// unwrap provided key with ECDH shared
|
|
return unwrap(shared, cdata);
|
|
});
|
|
return promise;
|
|
};
|
|
|
|
return fn;
|
|
}
|
|
|
|
// ### Public API
|
|
// * [name].derive
|
|
[
|
|
"ECDH",
|
|
"ECDH-HKDF",
|
|
"ECDH-CONCAT"
|
|
].forEach(function(name) {
|
|
var kdf = /^ECDH(?:-(\w+))?$/g.exec(name || "")[1];
|
|
var op = ecdh[name] = ecdh[name] || {};
|
|
switch (kdf || "") {
|
|
case "CONCAT":
|
|
op.derive = ecdhConcatDeriveFn();
|
|
break;
|
|
case "HKDF":
|
|
op.derive = ecdhHkdfDeriveFn();
|
|
break;
|
|
case "":
|
|
op.derive = ecdhDeriveFn();
|
|
break;
|
|
default:
|
|
op.derive = null;
|
|
}
|
|
});
|
|
|
|
// * [name].encrypt
|
|
// * [name].decrypt
|
|
[
|
|
"ECDH-ES",
|
|
"ECDH-ES+A128KW",
|
|
"ECDH-ES+A192KW",
|
|
"ECDH-ES+A256KW"
|
|
].forEach(function(name) {
|
|
var kw = /^ECDH-ES(?:\+(.+))?/g.exec(name || "")[1];
|
|
var op = ecdh[name] = ecdh[name] || {};
|
|
if (!kw) {
|
|
op.encrypt = ecdhesDirEncryptFn();
|
|
op.decrypt = ecdhesDirDecryptFn();
|
|
} else {
|
|
kw = aesKw[kw];
|
|
if (kw) {
|
|
op.encrypt = ecdhesKwEncryptFn(kw.encrypt);
|
|
op.decrypt = ecdhesKwDecryptFn(kw.decrypt);
|
|
} else {
|
|
op.ecrypt = op.decrypt = null;
|
|
}
|
|
}
|
|
});
|
|
//*/
|