Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
crypto: add crypto.diffieHellman
Currently, Node.js has separate (stateful) APIs for DH/ECDH, and no
support for ECDH-ES. This commit adds a single stateless function to
compute the DH/ECDH/ECDH-ES secret based on two KeyObjects.

PR-URL: #31178
Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
  • Loading branch information
tniessen authored and targos committed Apr 28, 2020
1 parent 1977136 commit 40253cc
Show file tree
Hide file tree
Showing 7 changed files with 345 additions and 14 deletions.
14 changes: 14 additions & 0 deletions doc/api/crypto.md
Expand Up @@ -2089,6 +2089,20 @@ the corresponding digest algorithm. This does not work for all signature
algorithms, such as `'ecdsa-with-SHA256'`, so it is best to always use digest
algorithm names.

### `crypto.diffieHellman(options)`
<!-- YAML
added: REPLACEME
-->

* `options`: {Object}
* `privateKey`: {KeyObject}
* `publicKey`: {KeyObject}
* Returns: {Buffer}

Computes the Diffie-Hellman secret based on a `privateKey` and a `publicKey`.
Both keys must have the same `asymmetricKeyType`, which must be one of `'dh'`
(for Diffie-Hellman), `'ec'` (for ECDH), `'x448'`, or `'x25519'` (for ECDH-ES).

### `crypto.generateKeyPair(type, options, callback)`
<!-- YAML
added: v10.12.0
Expand Down
5 changes: 5 additions & 0 deletions doc/api/errors.md
Expand Up @@ -774,6 +774,11 @@ be called no more than one time per instance of a `Hash` object.

[`hash.update()`][] failed for any reason. This should rarely, if ever, happen.

<a id="ERR_CRYPTO_INCOMPATIBLE_KEY"></a>
### `ERR_CRYPTO_INCOMPATIBLE_KEY`

The given crypto keys are incompatible with the attempted operation.

<a id="ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS"></a>
### `ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS`

Expand Down
4 changes: 3 additions & 1 deletion lib/crypto.js
Expand Up @@ -70,7 +70,8 @@ const {
const {
DiffieHellman,
DiffieHellmanGroup,
ECDH
ECDH,
diffieHellman
} = require('internal/crypto/diffiehellman');
const {
Cipher,
Expand Down Expand Up @@ -163,6 +164,7 @@ module.exports = {
createSecretKey,
createSign,
createVerify,
diffieHellman,
getCiphers,
getCurves,
getDiffieHellman: createDiffieHellmanGroup,
Expand Down
44 changes: 41 additions & 3 deletions lib/internal/crypto/diffiehellman.js
Expand Up @@ -2,16 +2,21 @@

const {
ObjectDefineProperty,
Set
} = primordials;

const { Buffer } = require('buffer');
const {
ERR_CRYPTO_ECDH_INVALID_FORMAT,
ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY,
ERR_INVALID_ARG_TYPE
ERR_CRYPTO_INCOMPATIBLE_KEY,
ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_OPT_VALUE
} = require('internal/errors').codes;
const { validateString } = require('internal/validators');
const { isArrayBufferView } = require('internal/util/types');
const { KeyObject } = require('internal/crypto/keys');
const {
getDefaultEncoding,
kHandle,
Expand All @@ -21,7 +26,8 @@ const {
DiffieHellman: _DiffieHellman,
DiffieHellmanGroup: _DiffieHellmanGroup,
ECDH: _ECDH,
ECDHConvertKey: _ECDHConvertKey
ECDHConvertKey: _ECDHConvertKey,
statelessDH
} = internalBinding('crypto');
const {
POINT_CONVERSION_COMPRESSED,
Expand Down Expand Up @@ -232,8 +238,40 @@ function getFormat(format) {
return POINT_CONVERSION_UNCOMPRESSED;
}

const dhEnabledKeyTypes = new Set(['dh', 'ec', 'x448', 'x25519']);

function diffieHellman(options) {
if (typeof options !== 'object')
throw new ERR_INVALID_ARG_TYPE('options', 'object', options);

const { privateKey, publicKey } = options;
if (!(privateKey instanceof KeyObject))
throw new ERR_INVALID_OPT_VALUE('privateKey', privateKey);

if (!(publicKey instanceof KeyObject))
throw new ERR_INVALID_OPT_VALUE('publicKey', publicKey);

if (privateKey.type !== 'private')
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(privateKey.type, 'private');

if (publicKey.type !== 'public' && publicKey.type !== 'private') {
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(publicKey.type,
'private or public');
}

const privateType = privateKey.asymmetricKeyType;
const publicType = publicKey.asymmetricKeyType;
if (privateType !== publicType || !dhEnabledKeyTypes.has(privateType)) {
throw new ERR_CRYPTO_INCOMPATIBLE_KEY('key types for Diffie-Hellman',
`${privateType} and ${publicType}`);
}

return statelessDH(privateKey[kHandle], publicKey[kHandle]);
}

module.exports = {
DiffieHellman,
DiffieHellmanGroup,
ECDH
ECDH,
diffieHellman
};
1 change: 1 addition & 0 deletions lib/internal/errors.js
Expand Up @@ -766,6 +766,7 @@ E('ERR_CRYPTO_FIPS_UNAVAILABLE', 'Cannot set FIPS mode in a non-FIPS build.',
Error);
E('ERR_CRYPTO_HASH_FINALIZED', 'Digest already called', Error);
E('ERR_CRYPTO_HASH_UPDATE_FAILED', 'Hash update failed', Error);
E('ERR_CRYPTO_INCOMPATIBLE_KEY', 'Incompatible %s: %s', Error);
E('ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', 'The selected key encoding %s %s.',
Error);
E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s', TypeError);
Expand Down
69 changes: 59 additions & 10 deletions src/node_crypto.cc
Expand Up @@ -5288,6 +5288,20 @@ void DiffieHellman::GetPrivateKey(const FunctionCallbackInfo<Value>& args) {
}, "No private key - did you forget to generate one?");
}

static void ZeroPadDiffieHellmanSecret(size_t remainder_size,
AllocatedBuffer* ret) {
// DH_size returns number of bytes in a prime number.
// DH_compute_key returns number of bytes in a remainder of exponent, which
// may have less bytes than a prime number. Therefore add 0-padding to the
// allocated buffer.
const size_t prime_size = ret->size();
if (remainder_size != prime_size) {
CHECK_LT(remainder_size, prime_size);
const size_t padding = prime_size - remainder_size;
memmove(ret->data() + padding, ret->data(), remainder_size);
memset(ret->data(), 0, padding);
}
}

void DiffieHellman::ComputeSecret(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Expand Down Expand Up @@ -5334,16 +5348,7 @@ void DiffieHellman::ComputeSecret(const FunctionCallbackInfo<Value>& args) {
}

CHECK_GE(size, 0);

// DH_size returns number of bytes in a prime number
// DH_compute_key returns number of bytes in a remainder of exponent, which
// may have less bytes than a prime number. Therefore add 0-padding to the
// allocated buffer.
if (static_cast<size_t>(size) != ret.size()) {
CHECK_GT(ret.size(), static_cast<size_t>(size));
memmove(ret.data() + ret.size() - size, ret.data(), size);
memset(ret.data(), 0, ret.size() - size);
}
ZeroPadDiffieHellmanSecret(static_cast<size_t>(size), &ret);

args.GetReturnValue().Set(ret.ToBuffer().ToLocalChecked());
}
Expand Down Expand Up @@ -6679,6 +6684,49 @@ void ConvertKey(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(buf);
}

AllocatedBuffer StatelessDiffieHellman(Environment* env, ManagedEVPPKey our_key,
ManagedEVPPKey their_key) {
size_t out_size;

EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(our_key.get(), nullptr));
if (!ctx ||
EVP_PKEY_derive_init(ctx.get()) <= 0 ||
EVP_PKEY_derive_set_peer(ctx.get(), their_key.get()) <= 0 ||
EVP_PKEY_derive(ctx.get(), nullptr, &out_size) <= 0)
return AllocatedBuffer();

AllocatedBuffer result = env->AllocateManaged(out_size);
CHECK_NOT_NULL(result.data());

unsigned char* data = reinterpret_cast<unsigned char*>(result.data());
if (EVP_PKEY_derive(ctx.get(), data, &out_size) <= 0)
return AllocatedBuffer();

ZeroPadDiffieHellmanSecret(out_size, &result);
return result;
}

void StatelessDiffieHellman(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);

CHECK(args[0]->IsObject() && args[1]->IsObject());
KeyObject* our_key_object;
ASSIGN_OR_RETURN_UNWRAP(&our_key_object, args[0].As<Object>());
CHECK_EQ(our_key_object->GetKeyType(), kKeyTypePrivate);
KeyObject* their_key_object;
ASSIGN_OR_RETURN_UNWRAP(&their_key_object, args[1].As<Object>());
CHECK_NE(their_key_object->GetKeyType(), kKeyTypeSecret);

ManagedEVPPKey our_key = our_key_object->GetAsymmetricKey();
ManagedEVPPKey their_key = their_key_object->GetAsymmetricKey();

AllocatedBuffer out = StatelessDiffieHellman(env, our_key, their_key);
if (out.size() == 0)
return ThrowCryptoError(env, ERR_get_error(), "diffieHellman failed");

args.GetReturnValue().Set(out.ToBuffer().ToLocalChecked());
}


void TimingSafeEqual(const FunctionCallbackInfo<Value>& args) {
ArrayBufferViewContents<char> buf1(args[0]);
Expand Down Expand Up @@ -6848,6 +6896,7 @@ void Initialize(Local<Object> target,
NODE_DEFINE_CONSTANT(target, kKeyTypePrivate);
NODE_DEFINE_CONSTANT(target, kSigEncDER);
NODE_DEFINE_CONSTANT(target, kSigEncP1363);
env->SetMethodNoSideEffect(target, "statelessDH", StatelessDiffieHellman);
env->SetMethod(target, "randomBytes", RandomBytes);
env->SetMethod(target, "signOneShot", SignOneShot);
env->SetMethod(target, "verifyOneShot", VerifyOneShot);
Expand Down

0 comments on commit 40253cc

Please sign in to comment.