Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto: modernize DH/ECDH/ECDH-ES #31178

Closed
wants to merge 5 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 41 additions & 6 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
@@ -1232,6 +1232,9 @@ passing keys as strings or `Buffer`s due to improved security features.
<!-- YAML
added: v11.6.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/31178
description: Added support for `'dh'`.
- version: v12.0.0
pr-url: https://github.com/nodejs/node/pull/26960
description: Added support for `'rsa-pss'`
@@ -1260,6 +1263,7 @@ types are:
* `'x448'` (OID 1.3.101.111)
* `'ed25519'` (OID 1.3.101.112)
* `'ed448'` (OID 1.3.101.113)
* `'dh'` (OID 1.2.840.113549.1.3.1)

This property is `undefined` for unrecognized `KeyObject` types and symmetric
keys.
@@ -2085,10 +2089,27 @@ 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
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/31178
description: Add support for Diffie-Hellman.
- version: v12.0.0
pr-url: https://github.com/nodejs/node/pull/26774
description: Add ability to generate X25519 and X448 key pairs.
@@ -2102,21 +2123,26 @@ changes:
-->

* `type`: {string} Must be `'rsa'`, `'dsa'`, `'ec'`, `'ed25519'`, `'ed448'`,
`'x25519'`, or `'x448'`.
`'x25519'`, `'x448'`, or `'dh'`.
* `options`: {Object}
* `modulusLength`: {number} Key size in bits (RSA, DSA).
* `publicExponent`: {number} Public exponent (RSA). **Default:** `0x10001`.
* `divisorLength`: {number} Size of `q` in bits (DSA).
* `namedCurve`: {string} Name of the curve to use (EC).
* `prime`: {Buffer} The prime parameter (DH).
* `primeLength`: {number} Prime length in bits (DH).
* `generator`: {number} Custom generator (DH). **Default:** `2`.
* `groupName`: {string} Diffie-Hellman group name (DH). See
[`crypto.getDiffieHellman()`][].
* `publicKeyEncoding`: {Object} See [`keyObject.export()`][].
* `privateKeyEncoding`: {Object} See [`keyObject.export()`][].
* `callback`: {Function}
* `err`: {Error}
* `publicKey`: {string | Buffer | KeyObject}
* `privateKey`: {string | Buffer | KeyObject}

Generates a new asymmetric key pair of the given `type`. RSA, DSA, EC, Ed25519
and Ed448 are currently supported.
Generates a new asymmetric key pair of the given `type`. RSA, DSA, EC, Ed25519,
Ed448, X25519, X448, and DH are currently supported.

If a `publicKeyEncoding` or `privateKeyEncoding` was specified, this function
behaves as if [`keyObject.export()`][] had been called on its result. Otherwise,
@@ -2154,6 +2180,9 @@ a `Promise` for an `Object` with `publicKey` and `privateKey` properties.
<!-- YAML
added: v10.12.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/31178
description: Add support for Diffie-Hellman.
- version: v12.0.0
pr-url: https://github.com/nodejs/node/pull/26554
description: Add ability to generate Ed25519 and Ed448 key pairs.
@@ -2163,20 +2192,26 @@ changes:
produce key objects if no encoding was specified.
-->

* `type`: {string} Must be `'rsa'`, `'dsa'`, `'ec'`, `'ed25519'`, or `'ed448'`.
* `type`: {string} Must be `'rsa'`, `'dsa'`, `'ec'`, `'ed25519'`, `'ed448'`,
`'x25519'`, `'x448'`, or `'dh'`.
* `options`: {Object}
* `modulusLength`: {number} Key size in bits (RSA, DSA).
* `publicExponent`: {number} Public exponent (RSA). **Default:** `0x10001`.
* `divisorLength`: {number} Size of `q` in bits (DSA).
* `namedCurve`: {string} Name of the curve to use (EC).
* `prime`: {Buffer} The prime parameter (DH).
* `primeLength`: {number} Prime length in bits (DH).
* `generator`: {number} Custom generator (DH). **Default:** `2`.
* `groupName`: {string} Diffie-Hellman group name (DH). See
[`crypto.getDiffieHellman()`][].
* `publicKeyEncoding`: {Object} See [`keyObject.export()`][].
* `privateKeyEncoding`: {Object} See [`keyObject.export()`][].
* Returns: {Object}
* `publicKey`: {string | Buffer | KeyObject}
* `privateKey`: {string | Buffer | KeyObject}

Generates a new asymmetric key pair of the given `type`. RSA, DSA, EC, Ed25519
and Ed448 are currently supported.
Generates a new asymmetric key pair of the given `type`. RSA, DSA, EC, Ed25519,
Ed448, X25519, X448, and DH are currently supported.

If a `publicKeyEncoding` or `privateKeyEncoding` was specified, this function
behaves as if [`keyObject.export()`][] had been called on its result. Otherwise,
18 changes: 18 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
@@ -776,6 +776,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`

@@ -826,6 +831,12 @@ A signing `key` was not provided to the [`sign.sign()`][] method.
[`crypto.timingSafeEqual()`][] was called with `Buffer`, `TypedArray`, or
`DataView` arguments of different lengths.

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

An unknown Diffie-Hellman group name was given. See
[`crypto.getDiffieHellman()`][] for a list of valid group names.

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

@@ -1514,6 +1525,12 @@ strict compliance with the API specification (which in some cases may accept
An [ES Module][] loader hook specified `format: 'dynamic'` but did not provide
a `dynamicInstantiate` hook.

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

For APIs that accept options objects, some options might be mandatory. This code
is thrown if a required option is missing.

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

@@ -2423,6 +2440,7 @@ such as `process.stdout.on('data')`.
[`Writable`]: stream.html#stream_class_stream_writable
[`child_process`]: child_process.html
[`cipher.getAuthTag()`]: crypto.html#crypto_cipher_getauthtag
[`crypto.getDiffieHellman()`]: crypto.html#crypto_crypto_getdiffiehellman_groupname
[`crypto.scrypt()`]: crypto.html#crypto_crypto_scrypt_password_salt_keylen_options_callback
[`crypto.scryptSync()`]: crypto.html#crypto_crypto_scryptsync_password_salt_keylen_options
[`crypto.timingSafeEqual()`]: crypto.html#crypto_crypto_timingsafeequal_a_b
4 changes: 3 additions & 1 deletion lib/crypto.js
Original file line number Diff line number Diff line change
@@ -70,7 +70,8 @@ const {
const {
DiffieHellman,
DiffieHellmanGroup,
ECDH
ECDH,
diffieHellman
} = require('internal/crypto/diffiehellman');
const {
Cipher,
@@ -163,6 +164,7 @@ module.exports = {
createSecretKey,
createSign,
createVerify,
diffieHellman,
getCiphers,
getCurves,
getDiffieHellman: createDiffieHellmanGroup,
44 changes: 41 additions & 3 deletions lib/internal/crypto/diffiehellman.js
Original file line number Diff line number Diff line change
@@ -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,
@@ -21,7 +26,8 @@ const {
DiffieHellman: _DiffieHellman,
DiffieHellmanGroup: _DiffieHellmanGroup,
ECDH: _ECDH,
ECDHConvertKey: _ECDHConvertKey
ECDHConvertKey: _ECDHConvertKey,
statelessDH
} = internalBinding('crypto');
const {
POINT_CONVERSION_COMPRESSED,
@@ -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
};
48 changes: 47 additions & 1 deletion lib/internal/crypto/keygen.js
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ const {
generateKeyPairDSA,
generateKeyPairEC,
generateKeyPairNid,
generateKeyPairDH,
EVP_PKEY_ED25519,
EVP_PKEY_ED448,
EVP_PKEY_X25519,
@@ -28,10 +29,12 @@ const {
const { customPromisifyArgs } = require('internal/util');
const { isUint32, validateString } = require('internal/validators');
const {
ERR_INCOMPATIBLE_OPTION_PAIR,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_CALLBACK,
ERR_INVALID_OPT_VALUE
ERR_INVALID_OPT_VALUE,
ERR_MISSING_OPTION
} = require('internal/errors').codes;

const { isArrayBufferView } = require('internal/util/types');
@@ -245,6 +248,49 @@ function check(type, options, callback) {
cipher, passphrase, wrap);
}
break;
case 'dh':
{
const { group, primeLength, prime, generator } = needOptions();
let args;
if (group != null) {
if (prime != null)
throw new ERR_INCOMPATIBLE_OPTION_PAIR('group', 'prime');
if (primeLength != null)
throw new ERR_INCOMPATIBLE_OPTION_PAIR('group', 'primeLength');
if (generator != null)
throw new ERR_INCOMPATIBLE_OPTION_PAIR('group', 'generator');
if (typeof group !== 'string')
throw new ERR_INVALID_OPT_VALUE('group', group);
args = [group];
} else {
if (prime != null) {
if (primeLength != null)
throw new ERR_INCOMPATIBLE_OPTION_PAIR('prime', 'primeLength');
if (!isArrayBufferView(prime))
throw new ERR_INVALID_OPT_VALUE('prime', prime);
} else if (primeLength != null) {
if (!isUint32(primeLength))
throw new ERR_INVALID_OPT_VALUE('primeLength', primeLength);
} else {
throw new ERR_MISSING_OPTION(
'At least one of the group, prime, or primeLength options');
}

if (generator != null) {
if (!isUint32(generator))
throw new ERR_INVALID_OPT_VALUE('generator', generator);
}

args = [prime != null ? prime : primeLength,
generator == null ? 2 : generator];
}

impl = (wrap) => generateKeyPairDH(...args,
publicFormat, publicType,
privateFormat, privateType,
cipher, passphrase, wrap);
}
break;
default:
throw new ERR_INVALID_ARG_VALUE('type', type,
'must be a supported key type');
2 changes: 2 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
@@ -767,6 +767,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);
@@ -1187,6 +1188,7 @@ E('ERR_MISSING_ARGS',
E('ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK',
'The ES Module loader may not return a format of \'dynamic\' when no ' +
'dynamicInstantiate function was provided', Error);
E('ERR_MISSING_OPTION', '%s is required', TypeError);
E('ERR_MULTIPLE_CALLBACK', 'Callback called multiple times', Error);
E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function', TypeError);
E('ERR_NAPI_INVALID_DATAVIEW_ARGS',
1 change: 1 addition & 0 deletions src/env.h
Original file line number Diff line number Diff line change
@@ -192,6 +192,7 @@ constexpr size_t kFsStatsBufferLength =
V(commonjs_string, "commonjs") \
V(config_string, "config") \
V(constants_string, "constants") \
V(crypto_dh_string, "dh") \
V(crypto_dsa_string, "dsa") \
V(crypto_ec_string, "ec") \
V(crypto_ed25519_string, "ed25519") \
Loading