Skip to content

Commit bf46c30

Browse files
tniessencodebytere
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 0d3e095 commit bf46c30

File tree

7 files changed

+345
-14
lines changed

7 files changed

+345
-14
lines changed

doc/api/crypto.md

Lines changed: 14 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,11 @@ be called no more than one time per instance of a `Hash` object.
776776

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

779+
<a id="ERR_CRYPTO_INCOMPATIBLE_KEY"></a>
780+
### `ERR_CRYPTO_INCOMPATIBLE_KEY`
781+
782+
The given crypto keys are incompatible with the attempted operation.
783+
779784
<a id="ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS"></a>
780785
### `ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS`
781786

lib/crypto.js

Lines changed: 3 additions & 1 deletion
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

Lines changed: 41 additions & 3 deletions
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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,7 @@ E('ERR_CRYPTO_FIPS_UNAVAILABLE', 'Cannot set FIPS mode in a non-FIPS build.',
767767
Error);
768768
E('ERR_CRYPTO_HASH_FINALIZED', 'Digest already called', Error);
769769
E('ERR_CRYPTO_HASH_UPDATE_FAILED', 'Hash update failed', Error);
770+
E('ERR_CRYPTO_INCOMPATIBLE_KEY', 'Incompatible %s: %s', Error);
770771
E('ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', 'The selected key encoding %s %s.',
771772
Error);
772773
E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s', TypeError);

src/node_crypto.cc

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5830,6 +5830,20 @@ void DiffieHellman::GetPrivateKey(const FunctionCallbackInfo<Value>& args) {
58305830
}, "No private key - did you forget to generate one?");
58315831
}
58325832

5833+
static void ZeroPadDiffieHellmanSecret(size_t remainder_size,
5834+
AllocatedBuffer* ret) {
5835+
// DH_size returns number of bytes in a prime number.
5836+
// DH_compute_key returns number of bytes in a remainder of exponent, which
5837+
// may have less bytes than a prime number. Therefore add 0-padding to the
5838+
// allocated buffer.
5839+
const size_t prime_size = ret->size();
5840+
if (remainder_size != prime_size) {
5841+
CHECK_LT(remainder_size, prime_size);
5842+
const size_t padding = prime_size - remainder_size;
5843+
memmove(ret->data() + padding, ret->data(), remainder_size);
5844+
memset(ret->data(), 0, padding);
5845+
}
5846+
}
58335847

58345848
void DiffieHellman::ComputeSecret(const FunctionCallbackInfo<Value>& args) {
58355849
Environment* env = Environment::GetCurrent(args);
@@ -5880,16 +5894,7 @@ void DiffieHellman::ComputeSecret(const FunctionCallbackInfo<Value>& args) {
58805894
}
58815895

58825896
CHECK_GE(size, 0);
5883-
5884-
// DH_size returns number of bytes in a prime number
5885-
// DH_compute_key returns number of bytes in a remainder of exponent, which
5886-
// may have less bytes than a prime number. Therefore add 0-padding to the
5887-
// allocated buffer.
5888-
if (static_cast<size_t>(size) != ret.size()) {
5889-
CHECK_GT(ret.size(), static_cast<size_t>(size));
5890-
memmove(ret.data() + ret.size() - size, ret.data(), size);
5891-
memset(ret.data(), 0, ret.size() - size);
5892-
}
5897+
ZeroPadDiffieHellmanSecret(static_cast<size_t>(size), &ret);
58935898

58945899
args.GetReturnValue().Set(ret.ToBuffer().ToLocalChecked());
58955900
}
@@ -7200,6 +7205,49 @@ void ConvertKey(const FunctionCallbackInfo<Value>& args) {
72007205
args.GetReturnValue().Set(buf);
72017206
}
72027207

7208+
AllocatedBuffer StatelessDiffieHellman(Environment* env, ManagedEVPPKey our_key,
7209+
ManagedEVPPKey their_key) {
7210+
size_t out_size;
7211+
7212+
EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(our_key.get(), nullptr));
7213+
if (!ctx ||
7214+
EVP_PKEY_derive_init(ctx.get()) <= 0 ||
7215+
EVP_PKEY_derive_set_peer(ctx.get(), their_key.get()) <= 0 ||
7216+
EVP_PKEY_derive(ctx.get(), nullptr, &out_size) <= 0)
7217+
return AllocatedBuffer();
7218+
7219+
AllocatedBuffer result = env->AllocateManaged(out_size);
7220+
CHECK_NOT_NULL(result.data());
7221+
7222+
unsigned char* data = reinterpret_cast<unsigned char*>(result.data());
7223+
if (EVP_PKEY_derive(ctx.get(), data, &out_size) <= 0)
7224+
return AllocatedBuffer();
7225+
7226+
ZeroPadDiffieHellmanSecret(out_size, &result);
7227+
return result;
7228+
}
7229+
7230+
void StatelessDiffieHellman(const FunctionCallbackInfo<Value>& args) {
7231+
Environment* env = Environment::GetCurrent(args);
7232+
7233+
CHECK(args[0]->IsObject() && args[1]->IsObject());
7234+
KeyObject* our_key_object;
7235+
ASSIGN_OR_RETURN_UNWRAP(&our_key_object, args[0].As<Object>());
7236+
CHECK_EQ(our_key_object->GetKeyType(), kKeyTypePrivate);
7237+
KeyObject* their_key_object;
7238+
ASSIGN_OR_RETURN_UNWRAP(&their_key_object, args[1].As<Object>());
7239+
CHECK_NE(their_key_object->GetKeyType(), kKeyTypeSecret);
7240+
7241+
ManagedEVPPKey our_key = our_key_object->GetAsymmetricKey();
7242+
ManagedEVPPKey their_key = their_key_object->GetAsymmetricKey();
7243+
7244+
AllocatedBuffer out = StatelessDiffieHellman(env, our_key, their_key);
7245+
if (out.size() == 0)
7246+
return ThrowCryptoError(env, ERR_get_error(), "diffieHellman failed");
7247+
7248+
args.GetReturnValue().Set(out.ToBuffer().ToLocalChecked());
7249+
}
7250+
72037251

72047252
void TimingSafeEqual(const FunctionCallbackInfo<Value>& args) {
72057253
ArrayBufferViewContents<char> buf1(args[0]);
@@ -7369,6 +7417,7 @@ void Initialize(Local<Object> target,
73697417
NODE_DEFINE_CONSTANT(target, kKeyTypePrivate);
73707418
NODE_DEFINE_CONSTANT(target, kSigEncDER);
73717419
NODE_DEFINE_CONSTANT(target, kSigEncP1363);
7420+
env->SetMethodNoSideEffect(target, "statelessDH", StatelessDiffieHellman);
73727421
env->SetMethod(target, "randomBytes", RandomBytes);
73737422
env->SetMethod(target, "signOneShot", SignOneShot);
73747423
env->SetMethod(target, "verifyOneShot", VerifyOneShot);

0 commit comments

Comments
 (0)