diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 14c80bf8d39448..1d411f8526365f 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -2093,6 +2093,9 @@ algorithm names. * `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} @@ -2119,8 +2127,8 @@ changes: * `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, @@ -2158,6 +2166,9 @@ a `Promise` for an `Object` with `publicKey` and `privateKey` properties. -* `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, diff --git a/doc/api/errors.md b/doc/api/errors.md index a7d4583ea15ed4..18fe855719dfa6 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -824,6 +824,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. + +### `ERR_CRYPTO_UNKNOWN_DH_GROUP` + +An unknown Diffie-Hellman group name was given. See +[`crypto.getDiffieHellman()`][] for a list of valid group names. + ### `ERR_DIR_CLOSED` @@ -1524,6 +1530,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. + +### `ERR_MISSING_OPTION` + +For APIs that accept options objects, some options might be mandatory. This code +is thrown if a required option is missing. + ### `ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST` @@ -2439,6 +2451,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 diff --git a/lib/internal/crypto/keygen.js b/lib/internal/crypto/keygen.js index 88d2822fa6fad0..ced1a0608fa4aa 100644 --- a/lib/internal/crypto/keygen.js +++ b/lib/internal/crypto/keygen.js @@ -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'); diff --git a/lib/internal/errors.js b/lib/internal/errors.js index b1323018f4f659..591976e1e4c800 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1201,6 +1201,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', diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 2ba95a0541739c..fcbe50b47ad712 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -6086,6 +6086,71 @@ class NidKeyPairGenerationConfig : public KeyPairGenerationConfig { const int id_; }; +// TODO(tniessen): Use std::variant instead. +// Diffie-Hellman can either generate keys using a fixed prime, or by first +// generating a random prime of a given size (in bits). Only one of both options +// may be specified. +struct PrimeInfo { + BignumPointer fixed_value_; + unsigned int prime_size_; +}; + +class DHKeyPairGenerationConfig : public KeyPairGenerationConfig { + public: + explicit DHKeyPairGenerationConfig(PrimeInfo&& prime_info, + unsigned int generator) + : prime_info_(std::move(prime_info)), + generator_(generator) {} + + EVPKeyCtxPointer Setup() override { + EVPKeyPointer params; + if (prime_info_.fixed_value_) { + DHPointer dh(DH_new()); + if (!dh) + return nullptr; + + BIGNUM* prime = prime_info_.fixed_value_.get(); + BignumPointer bn_g(BN_new()); + if (!BN_set_word(bn_g.get(), generator_) || + !DH_set0_pqg(dh.get(), prime, nullptr, bn_g.get())) + return nullptr; + + prime_info_.fixed_value_.release(); + bn_g.release(); + + params = EVPKeyPointer(EVP_PKEY_new()); + CHECK(params); + EVP_PKEY_assign_DH(params.get(), dh.release()); + } else { + EVPKeyCtxPointer param_ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_DH, nullptr)); + if (!param_ctx) + return nullptr; + + if (EVP_PKEY_paramgen_init(param_ctx.get()) <= 0) + return nullptr; + + if (EVP_PKEY_CTX_set_dh_paramgen_prime_len(param_ctx.get(), + prime_info_.prime_size_) <= 0) + return nullptr; + + if (EVP_PKEY_CTX_set_dh_paramgen_generator(param_ctx.get(), + generator_) <= 0) + return nullptr; + + EVP_PKEY* raw_params = nullptr; + if (EVP_PKEY_paramgen(param_ctx.get(), &raw_params) <= 0) + return nullptr; + params = EVPKeyPointer(raw_params); + } + + return EVPKeyCtxPointer(EVP_PKEY_CTX_new(params.get(), nullptr)); + } + + private: + PrimeInfo prime_info_; + unsigned int generator_; +}; + class GenerateKeyPairJob : public CryptoJob { public: GenerateKeyPairJob(Environment* env, @@ -6299,6 +6364,39 @@ void GenerateKeyPairNid(const FunctionCallbackInfo& args) { GenerateKeyPair(args, 1, std::move(config)); } +void GenerateKeyPairDH(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + PrimeInfo prime_info = {}; + unsigned int generator; + if (args[0]->IsString()) { + String::Utf8Value group_name(args.GetIsolate(), args[0].As()); + const modp_group* group = FindDiffieHellmanGroup(*group_name); + if (group == nullptr) + return THROW_ERR_CRYPTO_UNKNOWN_DH_GROUP(env); + + prime_info.fixed_value_ = BignumPointer( + BN_bin2bn(reinterpret_cast(group->prime), + group->prime_size, nullptr)); + generator = group->gen; + } else { + if (args[0]->IsInt32()) { + prime_info.prime_size_ = args[0].As()->Value(); + } else { + ArrayBufferViewContents input(args[0]); + prime_info.fixed_value_ = BignumPointer( + BN_bin2bn(input.data(), input.length(), nullptr)); + } + + CHECK(args[1]->IsInt32()); + generator = args[1].As()->Value(); + } + + std::unique_ptr config( + new DHKeyPairGenerationConfig(std::move(prime_info), generator)); + GenerateKeyPair(args, 2, std::move(config)); +} + void GetSSLCiphers(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -6732,6 +6830,7 @@ void Initialize(Local target, env->SetMethod(target, "generateKeyPairDSA", GenerateKeyPairDSA); env->SetMethod(target, "generateKeyPairEC", GenerateKeyPairEC); env->SetMethod(target, "generateKeyPairNid", GenerateKeyPairNid); + env->SetMethod(target, "generateKeyPairDH", GenerateKeyPairDH); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ED25519); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ED448); NODE_DEFINE_CONSTANT(target, EVP_PKEY_X25519); diff --git a/src/node_errors.h b/src/node_errors.h index f8a130680aadb9..e7f49437699618 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -37,6 +37,7 @@ void OnFatalError(const char* location, const char* message); V(ERR_BUFFER_TOO_LARGE, Error) \ V(ERR_CONSTRUCT_CALL_REQUIRED, TypeError) \ V(ERR_CONSTRUCT_CALL_INVALID, TypeError) \ + V(ERR_CRYPTO_UNKNOWN_DH_GROUP, Error) \ V(ERR_INVALID_ARG_VALUE, TypeError) \ V(ERR_OSSL_EVP_INVALID_DIGEST, Error) \ V(ERR_INVALID_ARG_TYPE, TypeError) \ @@ -89,6 +90,7 @@ void OnFatalError(const char* location, const char* message); "Buffer is not available for the current Context") \ V(ERR_CONSTRUCT_CALL_INVALID, "Constructor cannot be called") \ V(ERR_CONSTRUCT_CALL_REQUIRED, "Cannot call constructor without `new`") \ + V(ERR_CRYPTO_UNKNOWN_DH_GROUP, "Unknown DH group") \ V(ERR_INVALID_TRANSFER_OBJECT, "Found invalid object in transferList") \ V(ERR_MEMORY_ALLOCATION_FAILED, "Failed to allocate memory") \ V(ERR_OSSL_EVP_INVALID_DIGEST, "Invalid digest used") \ diff --git a/test/parallel/test-crypto-keygen.js b/test/parallel/test-crypto-keygen.js index 30b04e34d6ec36..e408d54164e659 100644 --- a/test/parallel/test-crypto-keygen.js +++ b/test/parallel/test-crypto-keygen.js @@ -981,6 +981,75 @@ const sec1EncExp = (cipher) => getRegExpForPEM('EC PRIVATE KEY', cipher); } } +// Test classic Diffie-Hellman key generation. +{ + generateKeyPair('dh', { + primeLength: 1024 + }, common.mustCall((err, publicKey, privateKey) => { + assert.ifError(err); + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'dh'); + + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'dh'); + })); + + assert.throws(() => { + generateKeyPair('dh', common.mustNotCall()); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options" argument must be of type object. Received undefined' + }); + + assert.throws(() => { + generateKeyPair('dh', {}, common.mustNotCall()); + }, { + name: 'TypeError', + code: 'ERR_MISSING_OPTION', + message: 'At least one of the group, prime, or primeLength options is ' + + 'required' + }); + + assert.throws(() => { + generateKeyPair('dh', { + group: 'modp0' + }, common.mustNotCall()); + }, { + name: 'Error', + code: 'ERR_CRYPTO_UNKNOWN_DH_GROUP', + message: 'Unknown DH group' + }); + + // Test incompatible options. + const allOpts = { + group: 'modp5', + prime: Buffer.alloc(0), + primeLength: 1024, + generator: 2 + }; + const incompatible = [ + ['group', 'prime'], + ['group', 'primeLength'], + ['group', 'generator'], + ['prime', 'primeLength'] + ]; + for (const [opt1, opt2] of incompatible) { + assert.throws(() => { + generateKeyPairSync('dh', { + [opt1]: allOpts[opt1], + [opt2]: allOpts[opt2] + }); + }, { + name: 'TypeError', + code: 'ERR_INCOMPATIBLE_OPTION_PAIR', + message: `Option "${opt1}" cannot be used in combination with option ` + + `"${opt2}"` + }); + } +} + // Test invalid key encoding types. { // Invalid public key type.