Skip to content

Commit 40253cc

Browse files
tniessentargos
authored andcommitted
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>
1 parent 1977136 commit 40253cc

File tree

7 files changed

+345
-14
lines changed

7 files changed

+345
-14
lines changed

doc/api/crypto.md

+14
Original file line numberDiff line numberDiff line change
@@ -2089,6 +2089,20 @@ the corresponding digest algorithm. This does not work for all signature
20892089
algorithms, such as `'ecdsa-with-SHA256'`, so it is best to always use digest
20902090
algorithm names.
20912091

2092+
### `crypto.diffieHellman(options)`
2093+
<!-- YAML
2094+
added: REPLACEME
2095+
-->
2096+
2097+
* `options`: {Object}
2098+
* `privateKey`: {KeyObject}
2099+
* `publicKey`: {KeyObject}
2100+
* Returns: {Buffer}
2101+
2102+
Computes the Diffie-Hellman secret based on a `privateKey` and a `publicKey`.
2103+
Both keys must have the same `asymmetricKeyType`, which must be one of `'dh'`
2104+
(for Diffie-Hellman), `'ec'` (for ECDH), `'x448'`, or `'x25519'` (for ECDH-ES).
2105+
20922106
### `crypto.generateKeyPair(type, options, callback)`
20932107
<!-- YAML
20942108
added: v10.12.0

doc/api/errors.md

+5
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,11 @@ be called no more than one time per instance of a `Hash` object.
774774

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

777+
<a id="ERR_CRYPTO_INCOMPATIBLE_KEY"></a>
778+
### `ERR_CRYPTO_INCOMPATIBLE_KEY`
779+
780+
The given crypto keys are incompatible with the attempted operation.
781+
777782
<a id="ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS"></a>
778783
### `ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS`
779784

lib/crypto.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ const {
7070
const {
7171
DiffieHellman,
7272
DiffieHellmanGroup,
73-
ECDH
73+
ECDH,
74+
diffieHellman
7475
} = require('internal/crypto/diffiehellman');
7576
const {
7677
Cipher,
@@ -163,6 +164,7 @@ module.exports = {
163164
createSecretKey,
164165
createSign,
165166
createVerify,
167+
diffieHellman,
166168
getCiphers,
167169
getCurves,
168170
getDiffieHellman: createDiffieHellmanGroup,

lib/internal/crypto/diffiehellman.js

+41-3
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@
22

33
const {
44
ObjectDefineProperty,
5+
Set
56
} = primordials;
67

78
const { Buffer } = require('buffer');
89
const {
910
ERR_CRYPTO_ECDH_INVALID_FORMAT,
1011
ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY,
11-
ERR_INVALID_ARG_TYPE
12+
ERR_CRYPTO_INCOMPATIBLE_KEY,
13+
ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE,
14+
ERR_INVALID_ARG_TYPE,
15+
ERR_INVALID_OPT_VALUE
1216
} = require('internal/errors').codes;
1317
const { validateString } = require('internal/validators');
1418
const { isArrayBufferView } = require('internal/util/types');
19+
const { KeyObject } = require('internal/crypto/keys');
1520
const {
1621
getDefaultEncoding,
1722
kHandle,
@@ -21,7 +26,8 @@ const {
2126
DiffieHellman: _DiffieHellman,
2227
DiffieHellmanGroup: _DiffieHellmanGroup,
2328
ECDH: _ECDH,
24-
ECDHConvertKey: _ECDHConvertKey
29+
ECDHConvertKey: _ECDHConvertKey,
30+
statelessDH
2531
} = internalBinding('crypto');
2632
const {
2733
POINT_CONVERSION_COMPRESSED,
@@ -232,8 +238,40 @@ function getFormat(format) {
232238
return POINT_CONVERSION_UNCOMPRESSED;
233239
}
234240

241+
const dhEnabledKeyTypes = new Set(['dh', 'ec', 'x448', 'x25519']);
242+
243+
function diffieHellman(options) {
244+
if (typeof options !== 'object')
245+
throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
246+
247+
const { privateKey, publicKey } = options;
248+
if (!(privateKey instanceof KeyObject))
249+
throw new ERR_INVALID_OPT_VALUE('privateKey', privateKey);
250+
251+
if (!(publicKey instanceof KeyObject))
252+
throw new ERR_INVALID_OPT_VALUE('publicKey', publicKey);
253+
254+
if (privateKey.type !== 'private')
255+
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(privateKey.type, 'private');
256+
257+
if (publicKey.type !== 'public' && publicKey.type !== 'private') {
258+
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(publicKey.type,
259+
'private or public');
260+
}
261+
262+
const privateType = privateKey.asymmetricKeyType;
263+
const publicType = publicKey.asymmetricKeyType;
264+
if (privateType !== publicType || !dhEnabledKeyTypes.has(privateType)) {
265+
throw new ERR_CRYPTO_INCOMPATIBLE_KEY('key types for Diffie-Hellman',
266+
`${privateType} and ${publicType}`);
267+
}
268+
269+
return statelessDH(privateKey[kHandle], publicKey[kHandle]);
270+
}
271+
235272
module.exports = {
236273
DiffieHellman,
237274
DiffieHellmanGroup,
238-
ECDH
275+
ECDH,
276+
diffieHellman
239277
};

lib/internal/errors.js

+1
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,7 @@ E('ERR_CRYPTO_FIPS_UNAVAILABLE', 'Cannot set FIPS mode in a non-FIPS build.',
766766
Error);
767767
E('ERR_CRYPTO_HASH_FINALIZED', 'Digest already called', Error);
768768
E('ERR_CRYPTO_HASH_UPDATE_FAILED', 'Hash update failed', Error);
769+
E('ERR_CRYPTO_INCOMPATIBLE_KEY', 'Incompatible %s: %s', Error);
769770
E('ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', 'The selected key encoding %s %s.',
770771
Error);
771772
E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s', TypeError);

src/node_crypto.cc

+59-10
Original file line numberDiff line numberDiff line change
@@ -5288,6 +5288,20 @@ void DiffieHellman::GetPrivateKey(const FunctionCallbackInfo<Value>& args) {
52885288
}, "No private key - did you forget to generate one?");
52895289
}
52905290

5291+
static void ZeroPadDiffieHellmanSecret(size_t remainder_size,
5292+
AllocatedBuffer* ret) {
5293+
// DH_size returns number of bytes in a prime number.
5294+
// DH_compute_key returns number of bytes in a remainder of exponent, which
5295+
// may have less bytes than a prime number. Therefore add 0-padding to the
5296+
// allocated buffer.
5297+
const size_t prime_size = ret->size();
5298+
if (remainder_size != prime_size) {
5299+
CHECK_LT(remainder_size, prime_size);
5300+
const size_t padding = prime_size - remainder_size;
5301+
memmove(ret->data() + padding, ret->data(), remainder_size);
5302+
memset(ret->data(), 0, padding);
5303+
}
5304+
}
52915305

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

53365350
CHECK_GE(size, 0);
5337-
5338-
// DH_size returns number of bytes in a prime number
5339-
// DH_compute_key returns number of bytes in a remainder of exponent, which
5340-
// may have less bytes than a prime number. Therefore add 0-padding to the
5341-
// allocated buffer.
5342-
if (static_cast<size_t>(size) != ret.size()) {
5343-
CHECK_GT(ret.size(), static_cast<size_t>(size));
5344-
memmove(ret.data() + ret.size() - size, ret.data(), size);
5345-
memset(ret.data(), 0, ret.size() - size);
5346-
}
5351+
ZeroPadDiffieHellmanSecret(static_cast<size_t>(size), &ret);
53475352

53485353
args.GetReturnValue().Set(ret.ToBuffer().ToLocalChecked());
53495354
}
@@ -6679,6 +6684,49 @@ void ConvertKey(const FunctionCallbackInfo<Value>& args) {
66796684
args.GetReturnValue().Set(buf);
66806685
}
66816686

6687+
AllocatedBuffer StatelessDiffieHellman(Environment* env, ManagedEVPPKey our_key,
6688+
ManagedEVPPKey their_key) {
6689+
size_t out_size;
6690+
6691+
EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(our_key.get(), nullptr));
6692+
if (!ctx ||
6693+
EVP_PKEY_derive_init(ctx.get()) <= 0 ||
6694+
EVP_PKEY_derive_set_peer(ctx.get(), their_key.get()) <= 0 ||
6695+
EVP_PKEY_derive(ctx.get(), nullptr, &out_size) <= 0)
6696+
return AllocatedBuffer();
6697+
6698+
AllocatedBuffer result = env->AllocateManaged(out_size);
6699+
CHECK_NOT_NULL(result.data());
6700+
6701+
unsigned char* data = reinterpret_cast<unsigned char*>(result.data());
6702+
if (EVP_PKEY_derive(ctx.get(), data, &out_size) <= 0)
6703+
return AllocatedBuffer();
6704+
6705+
ZeroPadDiffieHellmanSecret(out_size, &result);
6706+
return result;
6707+
}
6708+
6709+
void StatelessDiffieHellman(const FunctionCallbackInfo<Value>& args) {
6710+
Environment* env = Environment::GetCurrent(args);
6711+
6712+
CHECK(args[0]->IsObject() && args[1]->IsObject());
6713+
KeyObject* our_key_object;
6714+
ASSIGN_OR_RETURN_UNWRAP(&our_key_object, args[0].As<Object>());
6715+
CHECK_EQ(our_key_object->GetKeyType(), kKeyTypePrivate);
6716+
KeyObject* their_key_object;
6717+
ASSIGN_OR_RETURN_UNWRAP(&their_key_object, args[1].As<Object>());
6718+
CHECK_NE(their_key_object->GetKeyType(), kKeyTypeSecret);
6719+
6720+
ManagedEVPPKey our_key = our_key_object->GetAsymmetricKey();
6721+
ManagedEVPPKey their_key = their_key_object->GetAsymmetricKey();
6722+
6723+
AllocatedBuffer out = StatelessDiffieHellman(env, our_key, their_key);
6724+
if (out.size() == 0)
6725+
return ThrowCryptoError(env, ERR_get_error(), "diffieHellman failed");
6726+
6727+
args.GetReturnValue().Set(out.ToBuffer().ToLocalChecked());
6728+
}
6729+
66826730

66836731
void TimingSafeEqual(const FunctionCallbackInfo<Value>& args) {
66846732
ArrayBufferViewContents<char> buf1(args[0]);
@@ -6848,6 +6896,7 @@ void Initialize(Local<Object> target,
68486896
NODE_DEFINE_CONSTANT(target, kKeyTypePrivate);
68496897
NODE_DEFINE_CONSTANT(target, kSigEncDER);
68506898
NODE_DEFINE_CONSTANT(target, kSigEncP1363);
6899+
env->SetMethodNoSideEffect(target, "statelessDH", StatelessDiffieHellman);
68516900
env->SetMethod(target, "randomBytes", RandomBytes);
68526901
env->SetMethod(target, "signOneShot", SignOneShot);
68536902
env->SetMethod(target, "verifyOneShot", VerifyOneShot);

0 commit comments

Comments
 (0)