diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 9088b78babdadb..28a766c5ca29ab 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -2551,8 +2551,8 @@ added: * 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). +Both keys must have the same `asymmetricKeyType`, which must be one of `'dh'`, +`'ec'`, `'x448'`, or `'x25519'`. ### `crypto.generateKey(type, options, callback)` + +> Stability: 1 - Experimental + +The `crypto.promises` API provides an alternative set of asynchronous crypto +methods that return `Promise` objects and execute operations in libuv's +threadpool. +The API is accessible via `require('crypto').promises` or `require('crypto/promises')`. + +### `cryptoPromises.diffieHellman(options)` + + +* `options`: {Object} + * `privateKey`: {KeyObject|CryptoKey} + * `publicKey`: {KeyObject|CryptoKey} +* Returns: {Promise} containing {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'`, +`'ec'`, `'x448'`, or `'x25519'`. + +### `cryptoPromises.digest(algorithm, data[, options])` + + +* `algorithm` {string} +* `data` {ArrayBuffer|TypedArray|DataView|Buffer} +* `options` {Object} + * `outputLength` {number} Used to specify the desired output length in bytes + for XOF hash functions such as `'shake256'`. +* Returns: {Promise} containing {Buffer} + +Calculates the digest for the `data` using the given `algorithm`. + +The `algorithm` is dependent on the available algorithms supported by the +version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc. +On recent releases of OpenSSL, `openssl list -digest-algorithms` +(`openssl list-message-digest-algorithms` for older versions of OpenSSL) will +display the available digest algorithms. + +### `cryptoPromises.hmac(algorithm, data, key)` + + +* `algorithm` {string} +* `data` {ArrayBuffer|TypedArray|DataView|Buffer} +* `key` {KeyObject|CryptoKey} +* Returns: {Promise} containing {Buffer} + +Calculates the HMAC digest for the `data` using the given `algorithm`. + +The `algorithm` is dependent on the available algorithms supported by the +version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc. +On recent releases of OpenSSL, `openssl list -digest-algorithms` +(`openssl list-message-digest-algorithms` for older versions of OpenSSL) will +display the available digest algorithms. + +### `cryptoPromises.sign(algorithm, data, key[, options])` + + +* `algorithm` {string|null|undefined} +* `data` {ArrayBuffer|TypedArray|DataView|Buffer} +* `key` {KeyObject|CryptoKey} +* `options` {Object} + * `dsaEncoding` {string} For DSA and ECDSA, this option specifies the + format of the generated signature. It can be one of the following: + * `'der'` (default): DER-encoded ASN.1 signature structure encoding `(r, s)`. + * `'ieee-p1363'`: Signature format `r || s` as proposed in IEEE-P1363. + * `padding` {integer} Optional padding value for RSA, one of the following: + * `crypto.constants.RSA_PKCS1_PADDING` (default) + * `crypto.constants.RSA_PKCS1_PSS_PADDING` + * `saltLength` {integer} Salt length for when padding is + `RSA_PKCS1_PSS_PADDING`. The special value + `crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest + size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the + maximum permissible value. +* Returns: {Promise} containing {Buffer} + +Calculates the signature for `data` using the given private key and +algorithm. If `algorithm` is `null` or `undefined`, then the algorithm is +dependent upon the key type (especially Ed25519 and Ed448). + +### `cryptoPromises.verify(algorithm, data, key, signature[, options])` + + +* `algorithm` {string|null|undefined} +* `data` {ArrayBuffer|TypedArray|DataView|Buffer} +* `key` {KeyObject|CryptoKey} +* `signature` {ArrayBuffer|TypedArray|DataView|Buffer} +* `options` {Object} + * `dsaEncoding` {string} For DSA and ECDSA, this option specifies the + format of the generated signature. It can be one of the following: + * `'der'` (default): DER-encoded ASN.1 signature structure encoding `(r, s)`. + * `'ieee-p1363'`: Signature format `r || s` as proposed in IEEE-P1363. + * `padding` {integer} Optional padding value for RSA, one of the following: + * `crypto.constants.RSA_PKCS1_PADDING` (default) + * `crypto.constants.RSA_PKCS1_PSS_PADDING` + * `saltLength` {integer} Salt length for when padding is + `RSA_PKCS1_PSS_PADDING`. The special value + `crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest + size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the + maximum permissible value. +* Returns: {Promise} containing {boolean} + +Verifies the given signature for `data` using the given key and algorithm. If +`algorithm` is `null` or `undefined`, then the algorithm is dependent upon the +key type (especially Ed25519 and Ed448). + +The `signature` argument is the previously calculated signature for the `data`. + +Because public keys can be derived from private keys, a private key or a public +key may be passed for `key`. + ## Notes ### Legacy streams API (prior to Node.js 0.10) diff --git a/lib/crypto.js b/lib/crypto.js index 3f110bef5844f8..98cd74f428373d 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -172,6 +172,9 @@ function createVerify(algorithm, options) { return new Verify(algorithm, options); } +// Lazy loaded +let promises = null; + module.exports = { // Methods checkPrime, @@ -327,5 +330,14 @@ ObjectDefineProperties(module.exports, { value: pendingDeprecation ? deprecate(randomBytes, 'crypto.rng is deprecated.', 'DEP0115') : randomBytes + }, + promises: { + configurable: true, + enumerable: true, + get() { + if (promises === null) + promises = require('internal/crypto/promises').exports; + return promises; + } } }); diff --git a/lib/crypto/promises.js b/lib/crypto/promises.js new file mode 100644 index 00000000000000..29ed875864efa8 --- /dev/null +++ b/lib/crypto/promises.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('internal/crypto/promises').exports; diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index bcf9b1e5090d1a..519cac4758c1d9 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -346,6 +346,46 @@ function deriveBitsDH(publicKey, privateKey, callback) { job.run(); } +async function asyncDiffieHellman(publicKey, privateKey) { + const { asymmetricKeyType } = privateKey; + + if (asymmetricKeyType === 'dh') { + return new Promise((resolve, reject) => { + deriveBitsDH( + publicKey[kHandle], + privateKey[kHandle], + (err, bits) => { + if (err) reject(err); + else resolve(bits); + }); + }); + } + + if (asymmetricKeyType === 'x25519' || asymmetricKeyType === 'x448') { + return new Promise((resolve, reject) => { + deriveBitsECDH( + `NODE-${asymmetricKeyType.toUpperCase()}`, + publicKey[kHandle], + privateKey[kHandle], + (err, bits) => { + if (err) reject(err); + else resolve(bits); + }); + }); + } + + return new Promise((resolve, reject) => { + deriveBitsECDH( + undefined, + publicKey[kHandle], + privateKey[kHandle], + (err, bits) => { + if (err) reject(err); + else resolve(bits); + }); + }); +} + function verifyAcceptableDhKeyUse(name, type, usages) { let checkSet; switch (type) { @@ -601,9 +641,11 @@ module.exports = { DiffieHellman, DiffieHellmanGroup, ECDH, + asyncDiffieHellman, diffieHellman, deriveBitsECDH, deriveBitsDH, + dhEnabledKeyTypes, dhGenerateKey, asyncDeriveBitsECDH, asyncDeriveBitsDH, diff --git a/lib/internal/crypto/promises.js b/lib/internal/crypto/promises.js new file mode 100644 index 00000000000000..6eef699f2fe734 --- /dev/null +++ b/lib/internal/crypto/promises.js @@ -0,0 +1,266 @@ +'use strict'; + +const { + ArrayPrototypeIncludes, + ArrayPrototypeJoin, +} = primordials; + +const { + HashJob, + HmacJob, + kCryptoJobAsync, + kSigEncDER, + kSigEncP1363, + kSignJobModeSign, + kSignJobModeVerify, + SignJob, +} = internalBinding('crypto'); + +const { + crypto: constants, +} = internalBinding('constants'); + +const { + asyncDiffieHellman, + dhEnabledKeyTypes, +} = require('internal/crypto/diffiehellman'); + +const { + createPublicKey, + isCryptoKey, + isKeyObject, +} = require('internal/crypto/keys'); + +const { + jobPromise, + kHandle, + kKeyObject, +} = require('internal/crypto/util'); + +const { + hideStackFrames, + codes: { + ERR_CRYPTO_INCOMPATIBLE_KEY, + ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + } +} = require('internal/errors'); + +const { + isArrayBufferView, +} = require('internal/util/types'); + +const { + validateObject, + validateString, + validateUint32, +} = require('internal/validators'); + +const { Buffer } = require('buffer'); + +const validateKeyType = hideStackFrames((value, oneOf) => { + if (!ArrayPrototypeIncludes(oneOf, value)) { + const allowed = ArrayPrototypeJoin(oneOf, ' or '); + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(value, allowed); + } +}); + +function getIntOption(name, options) { + if (typeof options === 'object') { + const value = options[name]; + if (value !== undefined) { + if (value === value >> 0) { + return value; + } + throw new ERR_INVALID_ARG_VALUE(`options.${name}`, value); + } + } + return undefined; +} + +function getPadding(options) { + return getIntOption('padding', options); +} + +function getSaltLength(options) { + return getIntOption('saltLength', options); +} + +function getOutputLength(options) { + let outputLength = getIntOption('outputLength', options); + if (outputLength !== undefined) { + validateUint32(outputLength, 'options.outputLength'); + outputLength = outputLength << 3; + } + return outputLength; +} + +function getDSASignatureEncoding(options) { + if (typeof options === 'object') { + const { dsaEncoding = 'der' } = options; + if (dsaEncoding === 'der') + return kSigEncDER; + else if (dsaEncoding === 'ieee-p1363') + return kSigEncP1363; + throw new ERR_INVALID_ARG_VALUE('options.dsaEncoding', dsaEncoding); + } + + return kSigEncDER; +} + +function getKeyObject(key, argument, ...types) { + if (isCryptoKey(key)) + key = key[kKeyObject]; + + if (!isKeyObject(key)) + throw new ERR_INVALID_ARG_TYPE(argument, ['KeyObject', 'CryptoKey'], key); + + validateKeyType(key.type, types); + + return key; +} + +function getArrayBufferView(data, argument) { + if (!isArrayBufferView(data)) { + throw new ERR_INVALID_ARG_TYPE( + argument, + ['Buffer', 'TypedArray', 'DataView'], + data + ); + } + + return data; +} + +function getData(data) { + return getArrayBufferView(data, 'data'); +} + +function getSignature(data) { + return getArrayBufferView(data, 'signature'); +} + +async function digest(algorithm, data, options) { + validateString(algorithm, 'algorithm'); + + data = getData(data); + + const outputLength = getOutputLength(options); + + const digest = await jobPromise(new HashJob( + kCryptoJobAsync, + algorithm, + data, + outputLength)); + + return Buffer.from(digest); +} + +async function hmac(algorithm, data, key) { + validateString(algorithm, 'algorithm'); + + data = getData(data); + + const keyObject = getKeyObject(key, 'key', 'secret'); + + const digest = await jobPromise(new HmacJob( + kCryptoJobAsync, + kSignJobModeSign, + algorithm, + keyObject[kHandle], + data)); + + return Buffer.from(digest); +} + +async function sign(algorithm, data, key, options) { + if (algorithm != null) + validateString(algorithm, 'algorithm'); + + data = getData(data); + + const keyObject = getKeyObject(key, 'key', 'private'); + + // Options specific to RSA + const rsaPadding = getPadding(options); + const pssSaltLength = getSaltLength(options); + + // Options specific to (EC)DSA + // TODO(@jasnell): add dsaSigEnc to SignJob + const dsaSigEnc = getDSASignatureEncoding(options); + + const signature = await jobPromise(new SignJob( + kCryptoJobAsync, + kSignJobModeSign, + keyObject[kHandle], + data, + algorithm, + pssSaltLength, + rsaPadding)); + + return Buffer.from(signature); +} + +async function verify(algorithm, data, key, signature, options) { + if (algorithm != null) + validateString(algorithm, 'algorithm'); + + data = getData(data); + + let keyObject = getKeyObject(key, 'key', 'public', 'private'); + + if (keyObject.type === 'private') + keyObject = createPublicKey(keyObject); + + signature = getSignature(signature); + + // Options specific to RSA + const rsaPadding = getPadding(options); + const pssSaltLength = getSaltLength(options); + + // Options specific to (EC)DSA + // TODO(@jasnell): add dsaSigEnc to SignJob + const dsaSigEnc = getDSASignatureEncoding(options); + + return jobPromise(new SignJob( + kCryptoJobAsync, + kSignJobModeVerify, + keyObject[kHandle], + data, + algorithm, + pssSaltLength, + rsaPadding, + signature)); +} + +async function diffieHellman(options) { + validateObject(options, 'options'); + + const privateKey = getKeyObject(options.privateKey, + 'options.privateKey', 'private'); + const publicKey = getKeyObject(options.publicKey, + 'options.publicKey', '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}`); + } + + const bits = await asyncDiffieHellman(publicKey, privateKey); + + return Buffer.from(bits); +} + +module.exports = { + exports: { + diffieHellman, + digest, + hmac, + sign, + verify, + constants, + } +}; diff --git a/node.gyp b/node.gyp index 01ef5e3b6a2f27..8157083c79907e 100644 --- a/node.gyp +++ b/node.gyp @@ -47,6 +47,7 @@ 'lib/console.js', 'lib/constants.js', 'lib/crypto.js', + 'lib/crypto/promises.js', 'lib/cluster.js', 'lib/diagnostics_channel.js', 'lib/dgram.js', @@ -135,6 +136,7 @@ 'lib/internal/crypto/keys.js', 'lib/internal/crypto/mac.js', 'lib/internal/crypto/pbkdf2.js', + 'lib/internal/crypto/promises.js', 'lib/internal/crypto/random.js', 'lib/internal/crypto/rsa.js', 'lib/internal/crypto/scrypt.js', diff --git a/test/parallel/test-crypto-promises.js b/test/parallel/test-crypto-promises.js new file mode 100644 index 00000000000000..fae6642409dea6 --- /dev/null +++ b/test/parallel/test-crypto-promises.js @@ -0,0 +1,123 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + createSign, + createVerify, + createSecretKey, + createPublicKey, + createPrivateKey, +} = require('crypto'); + +const { + diffieHellman, + digest, + hmac, + sign, + verify, + constants, +} = require('crypto/promises'); + +const fixtures = require('../common/fixtures'); + +async function doTest() { + // Test RSA-PSS. + { + // This key pair does not restrict the message digest algorithm or salt + // length. + const publicPem = fixtures.readKey('rsa_pss_public_2048.pem'); + const privatePem = fixtures.readKey('rsa_pss_private_2048.pem'); + + const publicKey = createPublicKey(publicPem); + const privateKey = createPrivateKey(privatePem); + + // Any algorithm should work. + for (const algo of ['sha1', 'sha256']) { + // Any salt length should work. + for (const saltLength of [undefined, 8, 10, 12, 16, 18, 20]) { + const signature = await sign(algo, Buffer.from('foo'), privateKey, { saltLength }) + + for (const key of [publicKey, privateKey]) { + const okay = await verify(algo, Buffer.from('foo'), key, signature, { saltLength }) + + assert.ok(okay); + } + } + } + } + + { + // This key pair enforces sha256 as the message digest and the MGF1 + // message digest and a salt length of at least 16 bytes. + const publicPem = + fixtures.readKey('rsa_pss_public_2048_sha256_sha256_16.pem'); + const privatePem = + fixtures.readKey('rsa_pss_private_2048_sha256_sha256_16.pem'); + + const publicKey = createPublicKey(publicPem); + const privateKey = createPrivateKey(privatePem); + + // Signing with anything other than sha256 should fail. + await assert.rejects(async () => { + await sign('sha1', Buffer.from('foo'), privateKey); + }, /digest not allowed/); + + // Signing with salt lengths less than 16 bytes should fail. + for (const saltLength of [8, 10, 12]) { + await assert.rejects(async () => { + await sign('sha256', Buffer.from('foo'), privateKey, { saltLength }); + }, /pss saltlen too small/); + } + + // Signing with sha256 and appropriate salt lengths should work. + for (const saltLength of [undefined, 16, 18, 20]) { + const signature = await sign('sha256', Buffer.from('foo'), privateKey, { saltLength }) + + for (const key of [publicKey, privateKey]) { + const okay = await verify('sha256', Buffer.from('foo'), key, signature, { saltLength }) + + assert.ok(okay); + } + } + } + + { + // This key enforces sha512 as the message digest and sha256 as the MGF1 + // message digest. + const publicPem = + fixtures.readKey('rsa_pss_public_2048_sha512_sha256_20.pem'); + const privatePem = + fixtures.readKey('rsa_pss_private_2048_sha512_sha256_20.pem'); + + const publicKey = createPublicKey(publicPem); + const privateKey = createPrivateKey(privatePem); + + // Node.js usually uses the same hash function for the message and for MGF1. + // However, when a different MGF1 message digest algorithm has been + // specified as part of the key, it should automatically switch to that. + // This behavior is required by sections 3.1 and 3.3 of RFC4055. + + // sha256 matches the MGF1 hash function and should be used internally, + // but it should not be permitted as the main message digest algorithm. + for (const algo of ['sha1', 'sha256']) { + await assert.rejects(async () => { + await sign(algo, Buffer.from('foo'), privateKey); + }, /digest not allowed/); + } + + // sha512 should produce a valid signature. + const signature = await sign('sha512', Buffer.from('foo'), privateKey); + + for (const pkey of [publicKey, privateKey]) { + const okay = await verify('sha512', Buffer.from('foo'), pkey, signature); + + assert.ok(okay); + } + } +} + +doTest().then(common.mustCall());