From 3b53df07486b0d5881c84d768108d45303cc49a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Thu, 20 Sep 2018 19:53:44 +0200 Subject: [PATCH] crypto: add key object API This commit makes multiple important changes: 1. A new key object API is introduced. The KeyObject class itself is not exposed to users, instead, several new APIs can be used to construct key objects: createSecretKey, createPrivateKey and createPublicKey. The new API also allows to convert between different key formats, and even though the API itself is not compatible to the WebCrypto standard in any way, it makes interoperability much simpler. 2. Key objects can be used instead of the raw key material in all relevant crypto APIs. 3. The handling of asymmetric keys has been unified and greatly improved. Node.js now fully supports both PEM-encoded and DER-encoded public and private keys. 4. Conversions between buffers and strings have been moved to native code for sensitive data such as symmetric keys due to security considerations such as zeroing temporary buffers. 5. For compatibility with older versions of the crypto API, this change allows to specify Buffers and strings as the "passphrase" option when reading or writing an encoded key. Note that this can result in unexpected behavior if the password contains a null byte. PR-URL: https://github.com/nodejs/node/pull/24234 Reviewed-By: Refael Ackermann Reviewed-By: James M Snell --- doc/api/crypto.md | 278 +++- doc/api/errors.md | 5 + lib/crypto.js | 8 + lib/internal/crypto/cipher.js | 33 +- lib/internal/crypto/hash.js | 9 +- lib/internal/crypto/keygen.js | 135 +- lib/internal/crypto/keys.js | 337 +++++ lib/internal/crypto/sig.js | 31 +- lib/internal/errors.js | 2 + node.gyp | 1 + src/env.h | 4 + src/node_crypto.cc | 1292 ++++++++++++----- src/node_crypto.h | 168 ++- src/util.h | 23 + .../test-crypto-cipheriv-decipheriv.js | 8 +- test/parallel/test-crypto-hmac.js | 88 +- test/parallel/test-crypto-key-objects.js | 107 ++ test/parallel/test-crypto-keygen.js | 117 +- test/parallel/test-crypto-rsa-dsa.js | 2 +- test/parallel/test-crypto-sign-verify.js | 2 +- tools/doc/type-parser.js | 1 + 21 files changed, 1998 insertions(+), 653 deletions(-) create mode 100644 lib/internal/crypto/keys.js create mode 100644 test/parallel/test-crypto-key-objects.js diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 736ac360c903d7..017eb91e67882f 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -1101,6 +1101,81 @@ encoding of `'utf8'` is enforced. If `data` is a [`Buffer`][], `TypedArray`, or This can be called many times with new data as it is streamed. +## Class: KeyObject + + +Node.js uses an internal `KeyObject` class which should not be accessed +directly. Instead, factory functions exist to create instances of this class +in a secure manner, see [`crypto.createSecretKey()`][], +[`crypto.createPublicKey()`][] and [`crypto.createPrivateKey()`][]. A +`KeyObject` can represent a symmetric or asymmetric key, and each kind of key +exposes different functions. + +Most applications should consider using the new `KeyObject` API instead of +passing keys as strings or `Buffer`s due to improved security features. + +### keyObject.asymmetricKeyType + +* {string} + +For asymmetric keys, this property represents the type of the embedded key +(`'rsa'`, `'dsa'` or `'ec'`). This property is `undefined` for symmetric keys. + +### keyObject.export([options]) + +* `options`: {Object} +* Returns: {string | Buffer} + +For symmetric keys, this function allocates a `Buffer` containing the key +material and ignores any options. + +For asymmetric keys, the `options` parameter is used to determine the export +format. + +For public keys, the following encoding options can be used: + +* `type`: {string} Must be one of `'pkcs1'` (RSA only) or `'spki'`. +* `format`: {string} Must be `'pem'` or `'der'`. + +For private keys, the following encoding options can be used: + +* `type`: {string} Must be one of `'pkcs1'` (RSA only), `'pkcs8'` or + `'sec1'` (EC only). +* `format`: {string} Must be `'pem'` or `'der'`. +* `cipher`: {string} If specified, the private key will be encrypted with + the given `cipher` and `passphrase` using PKCS#5 v2.0 password based + encryption. +* `passphrase`: {string | Buffer} The passphrase to use for encryption, see + `cipher`. + +When PEM encoding was selected, the result will be a string, otherwise it will +be a buffer containing the data encoded as DER. + +### keyObject.symmetricSize + +* {number} + +For secret keys, this property represents the size of the key in bytes. This +property is `undefined` for asymmetric keys. + +### keyObject.type + +* {string} + +Depending on the type of this `KeyObject`, this property is either +`'secret'` for secret (symmetric) keys, `'public'` for public (asymmetric) keys +or `'private'` for private (asymmetric) keys. + ## Class: Sign -* `privateKey` {string | Object} - - `key` {string} - - `passphrase` {string} +* `privateKey` {Object | string | Buffer | KeyObject} - `padding` {integer} - `saltLength` {integer} * `outputEncoding` {string} The [encoding][] of the return value. @@ -1184,12 +1260,10 @@ changes: Calculates the signature on all the data passed through using either [`sign.update()`][] or [`sign.write()`][stream-writable-write]. -The `privateKey` argument can be an object or a string. If `privateKey` is a -string, it is treated as a raw key with no passphrase. If `privateKey` is an -object, it must contain one or more of the following properties: +If `privateKey` is not a [`KeyObject`][], this function behaves as if +`privateKey` had been passed to [`crypto.createPrivateKey()`][]. If it is an +object, the following additional properties can be passed: -* `key`: {string} - PEM encoded private key (required) -* `passphrase`: {string} - passphrase for the private key * `padding`: {integer} - Optional padding value for RSA, one of the following: * `crypto.constants.RSA_PKCS1_PADDING` (default) * `crypto.constants.RSA_PKCS1_PSS_PADDING` @@ -1299,18 +1373,20 @@ changes: pr-url: https://github.com/nodejs/node/pull/11705 description: Support for RSASSA-PSS and additional options was added. --> -* `object` {string | Object} +* `object` {Object | string | Buffer | KeyObject} + - `padding` {integer} + - `saltLength` {integer} * `signature` {string | Buffer | TypedArray | DataView} * `signatureEncoding` {string} The [encoding][] of the `signature` string. * Returns: {boolean} `true` or `false` depending on the validity of the signature for the data and public key. Verifies the provided data using the given `object` and `signature`. -The `object` argument can be either a string containing a PEM encoded object, -which can be an RSA public key, a DSA public key, or an X.509 certificate, -or an object with one or more of the following properties: -* `key`: {string} - PEM encoded public key (required) +If `object` is not a [`KeyObject`][], this function behaves as if +`object` had been passed to [`crypto.createPublicKey()`][]. If it is an +object, the following additional properties can be passed: + * `padding`: {integer} - Optional padding value for RSA, one of the following: * `crypto.constants.RSA_PKCS1_PADDING` (default) * `crypto.constants.RSA_PKCS1_PSS_PADDING` @@ -1436,6 +1512,9 @@ Adversaries][] for details. * `algorithm` {string} -* `key` {string | Buffer | TypedArray | DataView} +* `key` {string | Buffer | TypedArray | DataView | KeyObject} * `iv` {string | Buffer | TypedArray | DataView} * `options` {Object} [`stream.transform` options][] * Returns: {Cipher} @@ -1474,7 +1553,8 @@ display the available cipher algorithms. The `key` is the raw key used by the `algorithm` and `iv` is an [initialization vector][]. Both arguments must be `'utf8'` encoded strings, -[Buffers][`Buffer`], `TypedArray`, or `DataView`s. If the cipher does not need +[Buffers][`Buffer`], `TypedArray`, or `DataView`s. The `key` may optionally be +a [`KeyObject`][] of type `secret`. If the cipher does not need an initialization vector, `iv` may be `null`. Initialization vectors should be unpredictable and unique; ideally, they will be @@ -1525,6 +1605,9 @@ to create the `Decipher` object. * `algorithm` {string} -* `key` {string | Buffer | TypedArray | DataView} +* `key` {string | Buffer | TypedArray | DataView | KeyObject} * `options` {Object} [`stream.transform` options][] * Returns: {Hmac} @@ -1689,7 +1777,8 @@ 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. -The `key` is the HMAC key used to generate the cryptographic HMAC hash. +The `key` is the HMAC key used to generate the cryptographic HMAC hash. If it is +a [`KeyObject`][], its type must be `secret`. Example: generating the sha256 HMAC of a file @@ -1711,6 +1800,47 @@ input.on('readable', () => { }); ``` +### crypto.createPrivateKey(key) + +* `key` {Object | string | Buffer} + - `key`: {string | Buffer} The key material, either in PEM or DER format. + - `format`: {string} Must be `'pem'` or `'der'`. **Default:** `'pem'`. + - `type`: {string} Must be `'pkcs1'`, `'pkcs8'` or `'sec1'`. This option is + required only if the `format` is `'der'` and ignored if it is `'pem'`. + - `passphrase`: {string | Buffer} The passphrase to use for decryption. +* Returns: {KeyObject} + +Creates and returns a new key object containing a private key. If `key` is a +string or `Buffer`, `format` is assumed to be `'pem'`; otherwise, `key` +must be an object with the properties described above. + +### crypto.createPublicKey(key) + +* `key` {Object | string | Buffer} + - `key`: {string | Buffer} + - `format`: {string} Must be `'pem'` or `'der'`. **Default:** `'pem'`. + - `type`: {string} Must be `'pkcs1'` or `'spki'`. This option is required + only if the `format` is `'der'`. +* Returns: {KeyObject} + +Creates and returns a new key object containing a public key. If `key` is a +string or `Buffer`, `format` is assumed to be `'pem'`; otherwise, `key` +must be an object with the properties described above. + +### crypto.createSecretKey(key) + +* `key` {Buffer} +* Returns: {KeyObject} + +Creates and returns a new key object containing a secret key for symmetric +encryption or `Hmac`. + ### crypto.createSign(algorithm[, options]) * `type`: {string} Must be `'rsa'`, `'dsa'` or `'ec'`. * `options`: {Object} @@ -1747,27 +1882,22 @@ added: v10.12.0 - `publicExponent`: {number} Public exponent (RSA). **Default:** `0x10001`. - `divisorLength`: {number} Size of `q` in bits (DSA). - `namedCurve`: {string} Name of the curve to use (EC). - - `publicKeyEncoding`: {Object} - - `type`: {string} Must be one of `'pkcs1'` (RSA only) or `'spki'`. - - `format`: {string} Must be `'pem'` or `'der'`. - - `privateKeyEncoding`: {Object} - - `type`: {string} Must be one of `'pkcs1'` (RSA only), `'pkcs8'` or - `'sec1'` (EC only). - - `format`: {string} Must be `'pem'` or `'der'`. - - `cipher`: {string} If specified, the private key will be encrypted with - the given `cipher` and `passphrase` using PKCS#5 v2.0 password based - encryption. - - `passphrase`: {string} The passphrase to use for encryption, see `cipher`. + - `publicKeyEncoding`: {Object} See [`keyObject.export()`][]. + - `privateKeyEncoding`: {Object} See [`keyObject.export()`][]. * `callback`: {Function} - `err`: {Error} - - `publicKey`: {string|Buffer} - - `privateKey`: {string|Buffer} + - `publicKey`: {string | Buffer | KeyObject} + - `privateKey`: {string | Buffer | KeyObject} Generates a new asymmetric key pair of the given `type`. Only RSA, DSA and EC are currently supported. +If a `publicKeyEncoding` or `privateKeyEncoding` was specified, this function +behaves as if [`keyObject.export()`][] had been called on its result. Otherwise, +the respective part of the key is returned as a [`KeyObject`]. + It is recommended to encode public keys as `'spki'` and private keys as -`'pkcs8'` with encryption: +`'pkcs8'` with encryption for long-term storage: ```js const { generateKeyPair } = require('crypto'); @@ -1789,11 +1919,7 @@ generateKeyPair('rsa', { ``` On completion, `callback` will be called with `err` set to `undefined` and -`publicKey` / `privateKey` representing the generated key pair. When PEM -encoding was selected, the result will be a string, otherwise it will be a -buffer containing the data encoded as DER. Note that Node.js itself does not -accept DER, it is supported for interoperability with other libraries such as -WebCrypto only. +`publicKey` / `privateKey` representing the generated key pair. If this method is invoked as its [`util.promisify()`][]ed version, it returns a `Promise` for an `Object` with `publicKey` and `privateKey` properties. @@ -1801,6 +1927,11 @@ a `Promise` for an `Object` with `publicKey` and `privateKey` properties. ### crypto.generateKeyPairSync(type, options) * `type`: {string} Must be `'rsa'`, `'dsa'` or `'ec'`. * `options`: {Object} @@ -1818,10 +1949,11 @@ added: v10.12.0 - `cipher`: {string} If specified, the private key will be encrypted with the given `cipher` and `passphrase` using PKCS#5 v2.0 password based encryption. - - `passphrase`: {string} The passphrase to use for encryption, see `cipher`. + - `passphrase`: {string | Buffer} The passphrase to use for encryption, see + `cipher`. * Returns: {Object} - - `publicKey`: {string|Buffer} - - `privateKey`: {string|Buffer} + - `publicKey`: {string | Buffer | KeyObject} + - `privateKey`: {string | Buffer | KeyObject} Generates a new asymmetric key pair of the given `type`. Only RSA, DSA and EC are currently supported. @@ -2062,10 +2194,12 @@ An array of supported digest functions can be retrieved using ### crypto.privateDecrypt(privateKey, buffer) -* `privateKey` {Object | string} - - `key` {string} A PEM encoded private key. - - `passphrase` {string} An optional passphrase for the private key. +* `privateKey` {Object | string | Buffer | KeyObject} - `padding` {crypto.constants} An optional padding value defined in `crypto.constants`, which may be: `crypto.constants.RSA_NO_PADDING`, `crypto.constants.RSA_PKCS1_PADDING`, or @@ -2076,16 +2210,22 @@ added: v0.11.14 Decrypts `buffer` with `privateKey`. `buffer` was previously encrypted using the corresponding public key, for example using [`crypto.publicEncrypt()`][]. -`privateKey` can be an object or a string. If `privateKey` is a string, it is -treated as the key with no passphrase and will use `RSA_PKCS1_OAEP_PADDING`. +If `privateKey` is not a [`KeyObject`][], this function behaves as if +`privateKey` had been passed to [`crypto.createPrivateKey()`][]. If it is an +object, the `padding` property can be passed. Otherwise, this function uses +`RSA_PKCS1_OAEP_PADDING`. ### crypto.privateEncrypt(privateKey, buffer) -* `privateKey` {Object | string} - - `key` {string} A PEM encoded private key. - - `passphrase` {string} An optional passphrase for the private key. +* `privateKey` {Object | string | Buffer | KeyObject} + - `key` {string | Buffer | KeyObject} A PEM encoded private key. + - `passphrase` {string | Buffer} An optional passphrase for the private key. - `padding` {crypto.constants} An optional padding value defined in `crypto.constants`, which may be: `crypto.constants.RSA_NO_PADDING` or `crypto.constants.RSA_PKCS1_PADDING`. @@ -2095,16 +2235,21 @@ added: v1.1.0 Encrypts `buffer` with `privateKey`. The returned data can be decrypted using the corresponding public key, for example using [`crypto.publicDecrypt()`][]. -`privateKey` can be an object or a string. If `privateKey` is a string, it is -treated as the key with no passphrase and will use `RSA_PKCS1_PADDING`. +If `privateKey` is not a [`KeyObject`][], this function behaves as if +`privateKey` had been passed to [`crypto.createPrivateKey()`][]. If it is an +object, the `padding` property can be passed. Otherwise, this function uses +`RSA_PKCS1_PADDING`. ### crypto.publicDecrypt(key, buffer) -* `key` {Object | string} - - `key` {string} A PEM encoded public or private key. - - `passphrase` {string} An optional passphrase for the private key. +* `key` {Object | string | Buffer | KeyObject} + - `passphrase` {string | Buffer} An optional passphrase for the private key. - `padding` {crypto.constants} An optional padding value defined in `crypto.constants`, which may be: `crypto.constants.RSA_NO_PADDING` or `crypto.constants.RSA_PKCS1_PADDING`. @@ -2114,8 +2259,10 @@ added: v1.1.0 Decrypts `buffer` with `key`.`buffer` was previously encrypted using the corresponding private key, for example using [`crypto.privateEncrypt()`][]. -`key` can be an object or a string. If `key` is a string, it is treated as -the key with no passphrase and will use `RSA_PKCS1_PADDING`. +If `key` is not a [`KeyObject`][], this function behaves as if +`key` had been passed to [`crypto.createPublicKey()`][]. If it is an +object, the `padding` property can be passed. Otherwise, this function uses +`RSA_PKCS1_PADDING`. Because RSA public keys can be derived from private keys, a private key may be passed instead of a public key. @@ -2123,10 +2270,14 @@ be passed instead of a public key. ### crypto.publicEncrypt(key, buffer) -* `key` {Object | string} - - `key` {string} A PEM encoded public or private key. - - `passphrase` {string} An optional passphrase for the private key. +* `key` {Object | string | Buffer | KeyObject} + - `key` {string | Buffer | KeyObject} A PEM encoded public or private key. + - `passphrase` {string | Buffer} An optional passphrase for the private key. - `padding` {crypto.constants} An optional padding value defined in `crypto.constants`, which may be: `crypto.constants.RSA_NO_PADDING`, `crypto.constants.RSA_PKCS1_PADDING`, or @@ -2138,8 +2289,10 @@ Encrypts the content of `buffer` with `key` and returns a new [`Buffer`][] with encrypted content. The returned data can be decrypted using the corresponding private key, for example using [`crypto.privateDecrypt()`][]. -`key` can be an object or a string. If `key` is a string, it is treated as -the key with no passphrase and will use `RSA_PKCS1_OAEP_PADDING`. +If `key` is not a [`KeyObject`][], this function behaves as if +`key` had been passed to [`crypto.createPublicKey()`][]. If it is an +object, the `padding` property can be passed. Otherwise, this function uses +`RSA_PKCS1_OAEP_PADDING`. Because RSA public keys can be derived from private keys, a private key may be passed instead of a public key. @@ -2917,6 +3070,7 @@ the `crypto`, `tls`, and `https` modules and are generally specific to OpenSSL. [`Buffer`]: buffer.html [`EVP_BytesToKey`]: https://www.openssl.org/docs/man1.1.0/crypto/EVP_BytesToKey.html +[`KeyObject`]: #crypto_class_keyobject [`UV_THREADPOOL_SIZE`]: cli.html#cli_uv_threadpool_size_size [`cipher.final()`]: #crypto_cipher_final_outputencoding [`cipher.update()`]: #crypto_cipher_update_data_inputencoding_outputencoding @@ -2928,6 +3082,9 @@ the `crypto`, `tls`, and `https` modules and are generally specific to OpenSSL. [`crypto.createECDH()`]: #crypto_crypto_createecdh_curvename [`crypto.createHash()`]: #crypto_crypto_createhash_algorithm_options [`crypto.createHmac()`]: #crypto_crypto_createhmac_algorithm_key_options +[`crypto.createPrivateKey()`]: #crypto_crypto_createprivatekey_key +[`crypto.createPublicKey()`]: #crypto_crypto_createpublickey_key +[`crypto.createSecretKey()`]: #crypto_crypto_createsecretkey_key [`crypto.createSign()`]: #crypto_crypto_createsign_algorithm_options [`crypto.createVerify()`]: #crypto_crypto_createverify_algorithm_options [`crypto.getCurves()`]: #crypto_crypto_getcurves @@ -2949,6 +3106,7 @@ the `crypto`, `tls`, and `https` modules and are generally specific to OpenSSL. [`hash.update()`]: #crypto_hash_update_data_inputencoding [`hmac.digest()`]: #crypto_hmac_digest_encoding [`hmac.update()`]: #crypto_hmac_update_data_inputencoding +[`keyObject.export()`]: #crypto_keyobject_export_options [`sign.sign()`]: #crypto_sign_sign_privatekey_outputencoding [`sign.update()`]: #crypto_sign_update_data_inputencoding [`stream.Writable` options]: stream.html#stream_constructor_new_stream_writable_options diff --git a/doc/api/errors.md b/doc/api/errors.md index 05ecad70214fb4..d7e0023e3e7830 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -763,6 +763,11 @@ The selected public or private key encoding is incompatible with other options. An invalid [crypto digest algorithm][] was specified. + +### ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE + +The given crypto key object's type is invalid for the attempted operation. + ### ERR_CRYPTO_INVALID_STATE diff --git a/lib/crypto.js b/lib/crypto.js index c1526e20fef8d0..5b83a669c643d7 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -59,6 +59,11 @@ const { generateKeyPair, generateKeyPairSync } = require('internal/crypto/keygen'); +const { + createSecretKey, + createPublicKey, + createPrivateKey +} = require('internal/crypto/keys'); const { DiffieHellman, DiffieHellmanGroup, @@ -149,6 +154,9 @@ module.exports = exports = { createECDH, createHash, createHmac, + createPrivateKey, + createPublicKey, + createSecretKey, createSign, createVerify, getCiphers, diff --git a/lib/internal/crypto/cipher.js b/lib/internal/crypto/cipher.js index cdb92465ece578..0a0514103399a0 100644 --- a/lib/internal/crypto/cipher.js +++ b/lib/internal/crypto/cipher.js @@ -12,6 +12,11 @@ const { } = require('internal/errors').codes; const { validateString } = require('internal/validators'); +const { + preparePrivateKey, + preparePublicOrPrivateKey, + prepareSecretKey +} = require('internal/crypto/keys'); const { getDefaultEncoding, kHandle, @@ -38,19 +43,25 @@ const { deprecate, normalizeEncoding } = require('internal/util'); // Lazy loaded for startup performance. let StringDecoder; -function rsaFunctionFor(method, defaultPadding) { +function rsaFunctionFor(method, defaultPadding, keyType) { return (options, buffer) => { - const key = options.key || options; + const { format, type, data, passphrase } = + keyType === 'private' ? + preparePrivateKey(options) : + preparePublicOrPrivateKey(options); const padding = options.padding || defaultPadding; - const passphrase = options.passphrase || null; - return method(toBuf(key), buffer, padding, passphrase); + return method(data, format, type, passphrase, buffer, padding); }; } -const publicEncrypt = rsaFunctionFor(_publicEncrypt, RSA_PKCS1_OAEP_PADDING); -const publicDecrypt = rsaFunctionFor(_publicDecrypt, RSA_PKCS1_PADDING); -const privateEncrypt = rsaFunctionFor(_privateEncrypt, RSA_PKCS1_PADDING); -const privateDecrypt = rsaFunctionFor(_privateDecrypt, RSA_PKCS1_OAEP_PADDING); +const publicEncrypt = rsaFunctionFor(_publicEncrypt, RSA_PKCS1_OAEP_PADDING, + 'public'); +const publicDecrypt = rsaFunctionFor(_publicDecrypt, RSA_PKCS1_PADDING, + 'private'); +const privateEncrypt = rsaFunctionFor(_privateEncrypt, RSA_PKCS1_PADDING, + 'private'); +const privateDecrypt = rsaFunctionFor(_privateDecrypt, RSA_PKCS1_OAEP_PADDING, + 'public'); function getDecoder(decoder, encoding) { encoding = normalizeEncoding(encoding); @@ -105,11 +116,7 @@ function createCipher(cipher, password, options, decipher) { function createCipherWithIV(cipher, key, options, decipher, iv) { validateString(cipher, 'cipher'); - key = toBuf(key); - if (!isArrayBufferView(key)) { - throw invalidArrayBufferView('key', key); - } - + key = prepareSecretKey(key); iv = toBuf(iv); if (iv !== null && !isArrayBufferView(iv)) { throw invalidArrayBufferView('iv', iv); diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 6803d8fa954e73..7c697eb4772a19 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -12,6 +12,10 @@ const { toBuf } = require('internal/crypto/util'); +const { + prepareSecretKey +} = require('internal/crypto/keys'); + const { Buffer } = require('buffer'); const { @@ -88,10 +92,7 @@ function Hmac(hmac, key, options) { if (!(this instanceof Hmac)) return new Hmac(hmac, key, options); validateString(hmac, 'hmac'); - if (typeof key !== 'string' && !isArrayBufferView(key)) { - throw new ERR_INVALID_ARG_TYPE('key', - ['string', 'TypedArray', 'DataView'], key); - } + key = prepareSecretKey(key); this[kHandle] = new _Hmac(); this[kHandle].init(hmac, toBuf(key)); this[kState] = { diff --git a/lib/internal/crypto/keygen.js b/lib/internal/crypto/keygen.js index 7222d301f08692..7c0c4110439860 100644 --- a/lib/internal/crypto/keygen.js +++ b/lib/internal/crypto/keygen.js @@ -6,24 +6,32 @@ const { generateKeyPairDSA, generateKeyPairEC, OPENSSL_EC_NAMED_CURVE, - OPENSSL_EC_EXPLICIT_CURVE, - PK_ENCODING_PKCS1, - PK_ENCODING_PKCS8, - PK_ENCODING_SPKI, - PK_ENCODING_SEC1, - PK_FORMAT_DER, - PK_FORMAT_PEM + OPENSSL_EC_EXPLICIT_CURVE } = internalBinding('crypto'); +const { + parsePublicKeyEncoding, + parsePrivateKeyEncoding, + + PublicKeyObject, + PrivateKeyObject +} = require('internal/crypto/keys'); const { customPromisifyArgs } = require('internal/util'); const { isUint32, validateString } = require('internal/validators'); const { - ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_INVALID_CALLBACK, ERR_INVALID_OPT_VALUE } = require('internal/errors').codes; +const { isArrayBufferView } = require('internal/util/types'); + +function wrapKey(key, ctor) { + if (typeof key === 'string' || isArrayBufferView(key)) + return key; + return new ctor(key); +} + function generateKeyPair(type, options, callback) { if (typeof options === 'function') { callback = options; @@ -38,6 +46,9 @@ function generateKeyPair(type, options, callback) { const wrap = new AsyncWrap(Providers.KEYPAIRGENREQUEST); wrap.ondone = (ex, pubkey, privkey) => { if (ex) return callback.call(wrap, ex); + // If no encoding was chosen, return key objects instead. + pubkey = wrapKey(pubkey, PublicKeyObject); + privkey = wrapKey(privkey, PrivateKeyObject); callback.call(wrap, null, pubkey, privkey); }; @@ -69,86 +80,32 @@ function handleError(impl, wrap) { function parseKeyEncoding(keyType, options) { const { publicKeyEncoding, privateKeyEncoding } = options; - if (publicKeyEncoding == null || typeof publicKeyEncoding !== 'object') - throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding', publicKeyEncoding); - - const { format: strPublicFormat, type: strPublicType } = publicKeyEncoding; - - let publicType; - if (strPublicType === 'pkcs1') { - if (keyType !== 'rsa') { - throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( - strPublicType, 'can only be used for RSA keys'); - } - publicType = PK_ENCODING_PKCS1; - } else if (strPublicType === 'spki') { - publicType = PK_ENCODING_SPKI; + let publicFormat, publicType; + if (publicKeyEncoding == null) { + publicFormat = publicType = undefined; + } else if (typeof publicKeyEncoding === 'object') { + ({ + format: publicFormat, + type: publicType + } = parsePublicKeyEncoding(publicKeyEncoding, keyType, + 'publicKeyEncoding')); } else { - throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding.type', strPublicType); + throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding', publicKeyEncoding); } - let publicFormat; - if (strPublicFormat === 'der') { - publicFormat = PK_FORMAT_DER; - } else if (strPublicFormat === 'pem') { - publicFormat = PK_FORMAT_PEM; + let privateFormat, privateType, cipher, passphrase; + if (privateKeyEncoding == null) { + privateFormat = privateType = undefined; + } else if (typeof privateKeyEncoding === 'object') { + ({ + format: privateFormat, + type: privateType, + cipher, + passphrase + } = parsePrivateKeyEncoding(privateKeyEncoding, keyType, + 'privateKeyEncoding')); } else { - throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding.format', - strPublicFormat); - } - - if (privateKeyEncoding == null || typeof privateKeyEncoding !== 'object') throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding', privateKeyEncoding); - - const { - cipher, - passphrase, - format: strPrivateFormat, - type: strPrivateType - } = privateKeyEncoding; - - let privateType; - if (strPrivateType === 'pkcs1') { - if (keyType !== 'rsa') { - throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( - strPrivateType, 'can only be used for RSA keys'); - } - privateType = PK_ENCODING_PKCS1; - } else if (strPrivateType === 'pkcs8') { - privateType = PK_ENCODING_PKCS8; - } else if (strPrivateType === 'sec1') { - if (keyType !== 'ec') { - throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( - strPrivateType, 'can only be used for EC keys'); - } - privateType = PK_ENCODING_SEC1; - } else { - throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.type', strPrivateType); - } - - let privateFormat; - if (strPrivateFormat === 'der') { - privateFormat = PK_FORMAT_DER; - } else if (strPrivateFormat === 'pem') { - privateFormat = PK_FORMAT_PEM; - } else { - throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.format', - strPrivateFormat); - } - - if (cipher != null) { - if (typeof cipher !== 'string') - throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.cipher', cipher); - if (privateFormat === PK_FORMAT_DER && - (privateType === PK_ENCODING_PKCS1 || - privateType === PK_ENCODING_SEC1)) { - throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( - strPrivateType, 'does not support encryption'); - } - if (typeof passphrase !== 'string') { - throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.passphrase', - passphrase); - } } return { @@ -181,8 +138,8 @@ function check(type, options, callback) { } impl = (wrap) => generateKeyPairRSA(modulusLength, publicExponent, - publicType, publicFormat, - privateType, privateFormat, + publicFormat, publicType, + privateFormat, privateType, cipher, passphrase, wrap); } break; @@ -200,8 +157,8 @@ function check(type, options, callback) { } impl = (wrap) => generateKeyPairDSA(modulusLength, divisorLength, - publicType, publicFormat, - privateType, privateFormat, + publicFormat, publicType, + privateFormat, privateType, cipher, passphrase, wrap); } break; @@ -219,8 +176,8 @@ function check(type, options, callback) { throw new ERR_INVALID_OPT_VALUE('paramEncoding', paramEncoding); impl = (wrap) => generateKeyPairEC(namedCurve, paramEncoding, - publicType, publicFormat, - privateType, privateFormat, + publicFormat, publicType, + privateFormat, privateType, cipher, passphrase, wrap); } break; diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js new file mode 100644 index 00000000000000..ad828350806f3a --- /dev/null +++ b/lib/internal/crypto/keys.js @@ -0,0 +1,337 @@ +'use strict'; + +const { + KeyObject: KeyObjectHandle, + kKeyTypeSecret, + kKeyTypePublic, + kKeyTypePrivate, + kKeyFormatPEM, + kKeyFormatDER, + kKeyEncodingPKCS1, + kKeyEncodingPKCS8, + kKeyEncodingSPKI, + kKeyEncodingSEC1 +} = internalBinding('crypto'); +const { + ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS, + ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_INVALID_OPT_VALUE, + ERR_OUT_OF_RANGE +} = require('internal/errors').codes; +const { kHandle } = require('internal/crypto/util'); + +const { isArrayBufferView } = require('internal/util/types'); + +const kKeyType = Symbol('kKeyType'); + +const encodingNames = []; +for (const m of [[kKeyEncodingPKCS1, 'pkcs1'], [kKeyEncodingPKCS8, 'pkcs8'], + [kKeyEncodingSPKI, 'spki'], [kKeyEncodingSEC1, 'sec1']]) + encodingNames[m[0]] = m[1]; + +class KeyObject { + constructor(type, handle) { + if (type !== 'secret' && type !== 'public' && type !== 'private') + throw new ERR_INVALID_ARG_VALUE('type', type); + if (typeof handle !== 'object') + throw new ERR_INVALID_ARG_TYPE('handle', 'string', handle); + + this[kKeyType] = type; + + Object.defineProperty(this, kHandle, { + value: handle, + enumerable: false, + configurable: false, + writable: false + }); + } + + get type() { + return this[kKeyType]; + } +} + +class SecretKeyObject extends KeyObject { + constructor(handle) { + super('secret', handle); + } + + get symmetricKeySize() { + return this[kHandle].getSymmetricKeySize(); + } + + export() { + return this[kHandle].export(); + } +} + +const kAsymmetricKeyType = Symbol('kAsymmetricKeyType'); + +class AsymmetricKeyObject extends KeyObject { + get asymmetricKeyType() { + return this[kAsymmetricKeyType] || + (this[kAsymmetricKeyType] = this[kHandle].getAsymmetricKeyType()); + } +} + +class PublicKeyObject extends AsymmetricKeyObject { + constructor(handle) { + super('public', handle); + } + + export(encoding) { + const { + format, + type + } = parsePublicKeyEncoding(encoding, this.asymmetricKeyType); + return this[kHandle].export(format, type); + } +} + +class PrivateKeyObject extends AsymmetricKeyObject { + constructor(handle) { + super('private', handle); + } + + export(encoding) { + const { + format, + type, + cipher, + passphrase + } = parsePrivateKeyEncoding(encoding, this.asymmetricKeyType); + return this[kHandle].export(format, type, cipher, passphrase); + } +} + +function parseKeyFormat(formatStr, defaultFormat, optionName) { + if (formatStr === undefined && defaultFormat !== undefined) + return defaultFormat; + else if (formatStr === 'pem') + return kKeyFormatPEM; + else if (formatStr === 'der') + return kKeyFormatDER; + throw new ERR_INVALID_OPT_VALUE(optionName, formatStr); +} + +function parseKeyType(typeStr, required, keyType, isPublic, optionName) { + if (typeStr === undefined && !required) { + return undefined; + } else if (typeStr === 'pkcs1') { + if (keyType !== undefined && keyType !== 'rsa') { + throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( + typeStr, 'can only be used for RSA keys'); + } + return kKeyEncodingPKCS1; + } else if (typeStr === 'spki' && isPublic !== false) { + return kKeyEncodingSPKI; + } else if (typeStr === 'pkcs8' && isPublic !== true) { + return kKeyEncodingPKCS8; + } else if (typeStr === 'sec1' && isPublic !== true) { + if (keyType !== undefined && keyType !== 'ec') { + throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( + typeStr, 'can only be used for EC keys'); + } + return kKeyEncodingSEC1; + } + + throw new ERR_INVALID_OPT_VALUE(optionName, typeStr); +} + +function option(name, objName) { + return objName === undefined ? name : `${objName}.${name}`; +} + +function parseKeyFormatAndType(enc, keyType, isPublic, objName) { + const { format: formatStr, type: typeStr } = enc; + + const isInput = keyType === undefined; + const format = parseKeyFormat(formatStr, + isInput ? kKeyFormatPEM : undefined, + option('format', objName)); + + const type = parseKeyType(typeStr, + !isInput || format === kKeyFormatDER, + keyType, + isPublic, + option('type', objName)); + + return { format, type }; +} + +function isStringOrBuffer(val) { + return typeof val === 'string' || isArrayBufferView(val); +} + +function parseKeyEncoding(enc, keyType, isPublic, objName) { + const isInput = keyType === undefined; + + const { + format, + type + } = parseKeyFormatAndType(enc, keyType, isPublic, objName); + + let cipher, passphrase; + if (isPublic !== true) { + ({ cipher, passphrase } = enc); + + if (!isInput && cipher != null) { + if (typeof cipher !== 'string') + throw new ERR_INVALID_OPT_VALUE(option('cipher', objName), cipher); + if (format === kKeyFormatDER && + (type === kKeyEncodingPKCS1 || + type === kKeyEncodingSEC1)) { + throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( + encodingNames[type], 'does not support encryption'); + } + } + + if ((isInput && passphrase !== undefined && + !isStringOrBuffer(passphrase)) || + (!isInput && cipher != null && !isStringOrBuffer(passphrase))) { + throw new ERR_INVALID_OPT_VALUE(option('passphrase', objName), + passphrase); + } + } + + return { format, type, cipher, passphrase }; +} + +// Parses the public key encoding based on an object. keyType must be undefined +// when this is used to parse an input encoding and must be a valid key type if +// used to parse an output encoding. +function parsePublicKeyEncoding(enc, keyType, objName) { + return parseKeyFormatAndType(enc, keyType, true, objName); +} + +// Parses the private key encoding based on an object. keyType must be undefined +// when this is used to parse an input encoding and must be a valid key type if +// used to parse an output encoding. +function parsePrivateKeyEncoding(enc, keyType, objName) { + return parseKeyEncoding(enc, keyType, false, objName); +} + +function getKeyObjectHandle(key, isPublic, allowKeyObject) { + if (!allowKeyObject) { + return new ERR_INVALID_ARG_TYPE( + 'key', + ['string', 'Buffer', 'TypedArray', 'DataView'], + key + ); + } + if (isPublic != null) { + const expectedType = isPublic ? 'public' : 'private'; + if (key.type !== expectedType) + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, expectedType); + } + return key[kHandle]; +} + +function prepareAsymmetricKey(key, isPublic, allowKeyObject = true) { + if (isKeyObject(key)) { + // Best case: A key object, as simple as that. + return { data: getKeyObjectHandle(key, isPublic, allowKeyObject) }; + } else if (typeof key === 'string' || isArrayBufferView(key)) { + // Expect PEM by default, mostly for backward compatibility. + return { format: kKeyFormatPEM, data: key }; + } else if (typeof key === 'object') { + const data = key.key; + // The 'key' property can be a KeyObject as well to allow specifying + // additional options such as padding along with the key. + if (isKeyObject(data)) + return { data: getKeyObjectHandle(data, isPublic, allowKeyObject) }; + // Either PEM or DER using PKCS#1 or SPKI. + if (!isStringOrBuffer(data)) { + throw new ERR_INVALID_ARG_TYPE( + 'key', + ['string', 'Buffer', 'TypedArray', 'DataView', + ...(allowKeyObject ? ['KeyObject'] : [])], + key); + } + return { data, ...parseKeyEncoding(key, undefined, isPublic) }; + } else { + throw new ERR_INVALID_ARG_TYPE( + 'key', + ['string', 'Buffer', 'TypedArray', 'DataView', + ...(allowKeyObject ? ['KeyObject'] : [])], + key + ); + } +} + +function preparePublicKey(key, allowKeyObject) { + return prepareAsymmetricKey(key, true, allowKeyObject); +} + +function preparePrivateKey(key, allowKeyObject) { + return prepareAsymmetricKey(key, false, allowKeyObject); +} + +function preparePublicOrPrivateKey(key, allowKeyObject) { + return prepareAsymmetricKey(key, undefined, allowKeyObject); +} + +function prepareSecretKey(key, bufferOnly = false) { + if (!isArrayBufferView(key) && (bufferOnly || typeof key !== 'string')) { + if (isKeyObject(key) && !bufferOnly) { + if (key.type !== 'secret') + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'secret'); + return key[kHandle]; + } else { + throw new ERR_INVALID_ARG_TYPE( + 'key', + ['Buffer', 'TypedArray', 'DataView', + ...(bufferOnly ? [] : ['string', 'KeyObject'])], + key); + } + } + return key; +} + +function createSecretKey(key) { + key = prepareSecretKey(key, true); + if (key.byteLength === 0) + throw new ERR_OUT_OF_RANGE('key.byteLength', '> 0', key.byteLength); + const handle = new KeyObjectHandle(kKeyTypeSecret); + handle.init(key); + return new SecretKeyObject(handle); +} + +function createPublicKey(key) { + const { format, type, data } = preparePublicKey(key, false); + const handle = new KeyObjectHandle(kKeyTypePublic); + handle.init(data, format, type); + return new PublicKeyObject(handle); +} + +function createPrivateKey(key) { + const { format, type, data, passphrase } = preparePrivateKey(key, false); + const handle = new KeyObjectHandle(kKeyTypePrivate); + handle.init(data, format, type, passphrase); + return new PrivateKeyObject(handle); +} + +function isKeyObject(key) { + return key instanceof KeyObject; +} + +module.exports = { + // Public API. + createSecretKey, + createPublicKey, + createPrivateKey, + + // These are designed for internal use only and should not be exposed. + parsePublicKeyEncoding, + parsePrivateKeyEncoding, + preparePublicKey, + preparePrivateKey, + preparePublicOrPrivateKey, + prepareSecretKey, + SecretKeyObject, + PublicKeyObject, + PrivateKeyObject, + isKeyObject +}; diff --git a/lib/internal/crypto/sig.js b/lib/internal/crypto/sig.js index 9f02c866739f24..a694ba856777c1 100644 --- a/lib/internal/crypto/sig.js +++ b/lib/internal/crypto/sig.js @@ -17,6 +17,10 @@ const { toBuf, validateArrayBufferView, } = require('internal/crypto/util'); +const { + preparePrivateKey, + preparePublicKey +} = require('internal/crypto/keys'); const { Writable } = require('stream'); const { inherits } = require('util'); @@ -71,21 +75,18 @@ Sign.prototype.sign = function sign(options, encoding) { if (!options) throw new ERR_CRYPTO_SIGN_KEY_REQUIRED(); - var key = options.key || options; - var passphrase = options.passphrase || null; + const { data, format, type, passphrase } = preparePrivateKey(options, true); // Options specific to RSA - var rsaPadding = getPadding(options); - - var pssSaltLength = getSaltLength(options); + const rsaPadding = getPadding(options); + const pssSaltLength = getSaltLength(options); - key = validateArrayBufferView(key, 'key'); - - var ret = this[kHandle].sign(key, passphrase, rsaPadding, pssSaltLength); + const ret = this[kHandle].sign(data, format, type, passphrase, rsaPadding, + pssSaltLength); encoding = encoding || getDefaultEncoding(); if (encoding && encoding !== 'buffer') - ret = ret.toString(encoding); + return ret.toString(encoding); return ret; }; @@ -107,7 +108,12 @@ Verify.prototype._write = Sign.prototype._write; Verify.prototype.update = Sign.prototype.update; Verify.prototype.verify = function verify(options, signature, sigEncoding) { - var key = options.key || options; + const { + data, + format, + type + } = preparePublicKey(options, true); + sigEncoding = sigEncoding || getDefaultEncoding(); // Options specific to RSA @@ -115,12 +121,11 @@ Verify.prototype.verify = function verify(options, signature, sigEncoding) { var pssSaltLength = getSaltLength(options); - key = validateArrayBufferView(key, 'key'); - signature = validateArrayBufferView(toBuf(signature, sigEncoding), 'signature'); - return this[kHandle].verify(key, signature, rsaPadding, pssSaltLength); + return this[kHandle].verify(data, format, type, signature, + rsaPadding, pssSaltLength); }; legacyNativeHandle(Verify); diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 04c68c2af21874..f1965e825ec3d2 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -573,6 +573,8 @@ E('ERR_CRYPTO_HASH_UPDATE_FAILED', 'Hash update failed', Error); E('ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', 'The selected key encoding %s %s.', Error); E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s', TypeError); +E('ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', + 'Invalid key object type %s, expected %s.', TypeError); E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error); E('ERR_CRYPTO_PBKDF2_ERROR', 'PBKDF2 error', Error); E('ERR_CRYPTO_SCRYPT_INVALID_PARAMETER', 'Invalid scrypt parameter', Error); diff --git a/node.gyp b/node.gyp index f2d5fe8db83d03..9dc31b98ca5af7 100644 --- a/node.gyp +++ b/node.gyp @@ -100,6 +100,7 @@ 'lib/internal/crypto/diffiehellman.js', 'lib/internal/crypto/hash.js', 'lib/internal/crypto/keygen.js', + 'lib/internal/crypto/keys.js', 'lib/internal/crypto/pbkdf2.js', 'lib/internal/crypto/random.js', 'lib/internal/crypto/scrypt.js', diff --git a/src/env.h b/src/env.h index 8363a7cee30004..a1ace30c59bc94 100644 --- a/src/env.h +++ b/src/env.h @@ -139,6 +139,9 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2; V(chunks_sent_since_last_write_string, "chunksSentSinceLastWrite") \ V(code_string, "code") \ V(constants_string, "constants") \ + V(crypto_dsa_string, "dsa") \ + V(crypto_ec_string, "ec") \ + V(crypto_rsa_string, "rsa") \ V(cwd_string, "cwd") \ V(dest_string, "dest") \ V(destroyed_string, "destroyed") \ @@ -321,6 +324,7 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2; V(async_wrap_object_ctor_template, v8::FunctionTemplate) \ V(buffer_prototype_object, v8::Object) \ V(context, v8::Context) \ + V(crypto_key_object_constructor, v8::Function) \ V(domain_callback, v8::Function) \ V(domexception_function, v8::Function) \ V(fd_constructor_template, v8::ObjectTemplate) \ diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 42fe00b1d0458b..655929343df904 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -71,6 +71,7 @@ using v8::DontDelete; using v8::EscapableHandleScope; using v8::Exception; using v8::External; +using v8::Function; using v8::FunctionCallback; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; @@ -2668,6 +2669,837 @@ static bool IsSupportedAuthenticatedMode(const EVP_CIPHER_CTX* ctx) { return IsSupportedAuthenticatedMode(cipher); } +template +static T* MallocOpenSSL(size_t count) { + void* mem = OPENSSL_malloc(MultiplyWithOverflowCheck(count, sizeof(T))); + CHECK_NOT_NULL(mem); + return static_cast(mem); +} + +enum class ParsePublicKeyResult { + kParsePublicOk, + kParsePublicNotRecognized, + kParsePublicFailed +}; + +static ParsePublicKeyResult TryParsePublicKey( + EVPKeyPointer* pkey, + const BIOPointer& bp, + const char* name, + // NOLINTNEXTLINE(runtime/int) + std::function parse) { + unsigned char* der_data; + long der_len; // NOLINT(runtime/int) + + // This skips surrounding data and decodes PEM to DER. + { + MarkPopErrorOnReturn mark_pop_error_on_return; + if (PEM_bytes_read_bio(&der_data, &der_len, nullptr, name, + bp.get(), nullptr, nullptr) != 1) + return ParsePublicKeyResult::kParsePublicNotRecognized; + } + + // OpenSSL might modify the pointer, so we need to make a copy before parsing. + const unsigned char* p = der_data; + pkey->reset(parse(&p, der_len)); + OPENSSL_clear_free(der_data, der_len); + + return *pkey ? ParsePublicKeyResult::kParsePublicOk : + ParsePublicKeyResult::kParsePublicFailed; +} + +static ParsePublicKeyResult ParsePublicKeyPEM(EVPKeyPointer* pkey, + const char* key_pem, + int key_pem_len, + bool allow_certificate) { + BIOPointer bp(BIO_new_mem_buf(const_cast(key_pem), key_pem_len)); + if (!bp) + return ParsePublicKeyResult::kParsePublicFailed; + + ParsePublicKeyResult ret; + + // Try PKCS#8 first. + ret = TryParsePublicKey(pkey, bp, "PUBLIC KEY", + [](const unsigned char** p, long l) { // NOLINT(runtime/int) + return d2i_PUBKEY(nullptr, p, l); + }); + if (ret != ParsePublicKeyResult::kParsePublicNotRecognized) + return ret; + + // Maybe it is PKCS#1. + CHECK(BIO_reset(bp.get())); + ret = TryParsePublicKey(pkey, bp, "RSA PUBLIC KEY", + [](const unsigned char** p, long l) { // NOLINT(runtime/int) + return d2i_PublicKey(EVP_PKEY_RSA, nullptr, p, l); + }); + if (ret != ParsePublicKeyResult::kParsePublicNotRecognized || + !allow_certificate) + return ret; + + // X.509 fallback. + CHECK(BIO_reset(bp.get())); + return TryParsePublicKey(pkey, bp, "CERTIFICATE", + [](const unsigned char** p, long l) { // NOLINT(runtime/int) + X509Pointer x509(d2i_X509(nullptr, p, l)); + return x509 ? X509_get_pubkey(x509.get()) : nullptr; + }); +} + +static bool ParsePublicKey(EVPKeyPointer* pkey, + const PublicKeyEncodingConfig& config, + const char* key, + size_t key_len, + bool allow_certificate) { + if (config.format_ == kKeyFormatPEM) { + ParsePublicKeyResult r = + ParsePublicKeyPEM(pkey, key, key_len, allow_certificate); + return r == ParsePublicKeyResult::kParsePublicOk; + } else { + CHECK_EQ(config.format_, kKeyFormatDER); + const unsigned char* p = reinterpret_cast(key); + if (config.type_.ToChecked() == kKeyEncodingPKCS1) { + pkey->reset(d2i_PublicKey(EVP_PKEY_RSA, nullptr, &p, key_len)); + return pkey; + } else { + CHECK_EQ(config.type_.ToChecked(), kKeyEncodingSPKI); + pkey->reset(d2i_PUBKEY(nullptr, &p, key_len)); + return pkey; + } + } +} + +static inline Local BIOToStringOrBuffer(Environment* env, + BIO* bio, + PKFormatType format) { + BUF_MEM* bptr; + BIO_get_mem_ptr(bio, &bptr); + if (format == kKeyFormatPEM) { + // PEM is an ASCII format, so we will return it as a string. + return String::NewFromUtf8(env->isolate(), bptr->data, + NewStringType::kNormal, + bptr->length).ToLocalChecked(); + } else { + CHECK_EQ(format, kKeyFormatDER); + // DER is binary, return it as a buffer. + return Buffer::Copy(env, bptr->data, bptr->length).ToLocalChecked(); + } +} + +static bool WritePublicKeyInner(EVP_PKEY* pkey, + const BIOPointer& bio, + const PublicKeyEncodingConfig& config) { + if (config.type_.ToChecked() == kKeyEncodingPKCS1) { + // PKCS#1 is only valid for RSA keys. + CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_RSA); + RSAPointer rsa(EVP_PKEY_get1_RSA(pkey)); + if (config.format_ == kKeyFormatPEM) { + // Encode PKCS#1 as PEM. + return PEM_write_bio_RSAPublicKey(bio.get(), rsa.get()) == 1; + } else { + // Encode PKCS#1 as DER. + CHECK_EQ(config.format_, kKeyFormatDER); + return i2d_RSAPublicKey_bio(bio.get(), rsa.get()) == 1; + } + } else { + CHECK_EQ(config.type_.ToChecked(), kKeyEncodingSPKI); + if (config.format_ == kKeyFormatPEM) { + // Encode SPKI as PEM. + return PEM_write_bio_PUBKEY(bio.get(), pkey) == 1; + } else { + // Encode SPKI as DER. + CHECK_EQ(config.format_, kKeyFormatDER); + return i2d_PUBKEY_bio(bio.get(), pkey) == 1; + } + } +} + +static MaybeLocal WritePublicKey(Environment* env, + EVP_PKEY* pkey, + const PublicKeyEncodingConfig& config) { + BIOPointer bio(BIO_new(BIO_s_mem())); + CHECK(bio); + + if (!WritePublicKeyInner(pkey, bio, config)) { + ThrowCryptoError(env, ERR_get_error(), "Failed to encode public key"); + return MaybeLocal(); + } + return BIOToStringOrBuffer(env, bio.get(), config.format_); +} + +static EVPKeyPointer ParsePrivateKey(const PrivateKeyEncodingConfig& config, + const char* key, + size_t key_len) { + EVPKeyPointer pkey; + + if (config.format_ == kKeyFormatPEM) { + BIOPointer bio(BIO_new_mem_buf(key, key_len)); + CHECK(bio); + + char* pass = const_cast(config.passphrase_.get()); + pkey.reset(PEM_read_bio_PrivateKey(bio.get(), + nullptr, + PasswordCallback, + pass)); + } else { + CHECK_EQ(config.format_, kKeyFormatDER); + + if (config.type_.ToChecked() == kKeyEncodingPKCS1) { + const unsigned char* p = reinterpret_cast(key); + pkey.reset(d2i_PrivateKey(EVP_PKEY_RSA, nullptr, &p, key_len)); + } else if (config.type_.ToChecked() == kKeyEncodingPKCS8) { + BIOPointer bio(BIO_new_mem_buf(key, key_len)); + CHECK(bio); + char* pass = const_cast(config.passphrase_.get()); + pkey.reset(d2i_PKCS8PrivateKey_bio(bio.get(), + nullptr, + PasswordCallback, + pass)); + } else { + CHECK_EQ(config.type_.ToChecked(), kKeyEncodingSEC1); + const unsigned char* p = reinterpret_cast(key); + pkey.reset(d2i_PrivateKey(EVP_PKEY_EC, nullptr, &p, key_len)); + } + } + + // OpenSSL can fail to parse the key but still return a non-null pointer. + if (ERR_peek_error() != 0) + pkey.reset(); + + return pkey; +} + +ByteSource::ByteSource(ByteSource&& other) + : data_(other.data_), + allocated_data_(other.allocated_data_), + size_(other.size_) { + other.allocated_data_ = nullptr; +} + +ByteSource::~ByteSource() { + OPENSSL_clear_free(allocated_data_, size_); +} + +ByteSource& ByteSource::operator=(ByteSource&& other) { + if (&other != this) { + OPENSSL_clear_free(allocated_data_, size_); + data_ = other.data_; + allocated_data_ = other.allocated_data_; + other.allocated_data_ = nullptr; + size_ = other.size_; + } + return *this; +} + +const char* ByteSource::get() const { + return data_; +} + +size_t ByteSource::size() const { + return size_; +} + +ByteSource ByteSource::FromStringOrBuffer(Environment* env, + Local value) { + return Buffer::HasInstance(value) ? FromBuffer(value) + : FromString(env, value.As()); +} + +ByteSource ByteSource::FromString(Environment* env, Local str, + bool ntc) { + CHECK(str->IsString()); + size_t size = str->Utf8Length(env->isolate()); + size_t alloc_size = ntc ? size + 1 : size; + char* data = MallocOpenSSL(alloc_size); + int opts = String::NO_OPTIONS; + if (!ntc) opts |= String::NO_NULL_TERMINATION; + str->WriteUtf8(env->isolate(), data, alloc_size, nullptr, opts); + return Allocated(data, size); +} + +ByteSource ByteSource::FromBuffer(Local buffer, bool ntc) { + size_t size = Buffer::Length(buffer); + if (ntc) { + char* data = MallocOpenSSL(size + 1); + memcpy(data, Buffer::Data(buffer), size); + data[size] = 0; + return Allocated(data, size); + } + return Foreign(Buffer::Data(buffer), size); +} + +ByteSource ByteSource::NullTerminatedCopy(Environment* env, + Local value) { + return Buffer::HasInstance(value) ? FromBuffer(value, true) + : FromString(env, value.As(), true); +} + +ByteSource ByteSource::FromSymmetricKeyObject(Local handle) { + CHECK(handle->IsObject()); + KeyObject* key = Unwrap(handle.As()); + CHECK(key); + return Foreign(key->GetSymmetricKey(), key->GetSymmetricKeySize()); +} + +ByteSource::ByteSource(const char* data, char* allocated_data, size_t size) + : data_(data), + allocated_data_(allocated_data), + size_(size) {} + +ByteSource ByteSource::Allocated(char* data, size_t size) { + return ByteSource(data, data, size); +} + +ByteSource ByteSource::Foreign(const char* data, size_t size) { + return ByteSource(data, nullptr, size); +} + +enum KeyEncodingContext { + kKeyContextInput, + kKeyContextExport, + kKeyContextGenerate +}; + +static void GetKeyFormatAndTypeFromJs( + AsymmetricKeyEncodingConfig* config, + const FunctionCallbackInfo& args, + unsigned int* offset, + KeyEncodingContext context) { + // During key pair generation, it is possible not to specify a key encoding, + // which will lead to a key object being returned. + if (args[*offset]->IsUndefined()) { + CHECK_EQ(context, kKeyContextGenerate); + CHECK(args[*offset + 1]->IsUndefined()); + config->output_key_object_ = true; + } else { + config->output_key_object_ = false; + + CHECK(args[*offset]->IsInt32()); + config->format_ = static_cast( + args[*offset].As()->Value()); + + if (args[*offset + 1]->IsInt32()) { + config->type_ = Just(static_cast( + args[*offset + 1].As()->Value())); + } else { + CHECK(context == kKeyContextInput && config->format_ == kKeyFormatPEM); + CHECK(args[*offset + 1]->IsNullOrUndefined()); + config->type_ = Nothing(); + } + } + + *offset += 2; +} + +static PublicKeyEncodingConfig GetPublicKeyEncodingFromJs( + const FunctionCallbackInfo& args, + unsigned int* offset, + KeyEncodingContext context) { + PublicKeyEncodingConfig result; + GetKeyFormatAndTypeFromJs(&result, args, offset, context); + return result; +} + +static ManagedEVPPKey GetPublicKeyFromJs( + const FunctionCallbackInfo& args, + unsigned int* offset, + bool allow_key_object, + bool allow_certificate) { + if (args[*offset]->IsString() || Buffer::HasInstance(args[*offset])) { + Environment* env = Environment::GetCurrent(args); + ByteSource key = ByteSource::FromStringOrBuffer(env, args[(*offset)++]); + PublicKeyEncodingConfig config = + GetPublicKeyEncodingFromJs(args, offset, kKeyContextInput); + EVPKeyPointer pkey; + ParsePublicKey(&pkey, config, key.get(), key.size(), allow_certificate); + if (!pkey) + ThrowCryptoError(env, ERR_get_error(), "Failed to read public key"); + return ManagedEVPPKey(pkey.release()); + } else { + CHECK(args[*offset]->IsObject() && allow_key_object); + KeyObject* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args[*offset].As(), ManagedEVPPKey()); + CHECK_EQ(key->GetKeyType(), kKeyTypePublic); + (*offset) += 3; + return key->GetAsymmetricKey(); + } +} + +static NonCopyableMaybe GetPrivateKeyEncodingFromJs( + const FunctionCallbackInfo& args, + unsigned int* offset, + KeyEncodingContext context) { + Environment* env = Environment::GetCurrent(args); + + PrivateKeyEncodingConfig result; + GetKeyFormatAndTypeFromJs(&result, args, offset, context); + + if (result.output_key_object_) { + if (context != kKeyContextInput) + (*offset)++; + } else { + bool needs_passphrase = false; + if (context != kKeyContextInput) { + if (args[*offset]->IsString()) { + String::Utf8Value cipher_name(env->isolate(), + args[*offset].As()); + result.cipher_ = EVP_get_cipherbyname(*cipher_name); + if (result.cipher_ == nullptr) { + env->ThrowError("Unknown cipher"); + return NonCopyableMaybe(); + } + needs_passphrase = true; + } else { + CHECK(args[*offset]->IsNullOrUndefined()); + result.cipher_ = nullptr; + } + (*offset)++; + } + + if (args[*offset]->IsString() || Buffer::HasInstance(args[*offset])) { + CHECK_IMPLIES(context != kKeyContextInput, result.cipher_ != nullptr); + + result.passphrase_ = ByteSource::NullTerminatedCopy(env, args[*offset]); + } else { + CHECK(args[*offset]->IsNullOrUndefined() && !needs_passphrase); + } + } + + (*offset)++; + return NonCopyableMaybe(std::move(result)); +} + +static ManagedEVPPKey GetPrivateKeyFromJs( + const FunctionCallbackInfo& args, + unsigned int* offset, + bool allow_key_object) { + if (args[*offset]->IsString() || Buffer::HasInstance(args[*offset])) { + Environment* env = Environment::GetCurrent(args); + ByteSource key = ByteSource::FromStringOrBuffer(env, args[(*offset)++]); + NonCopyableMaybe config = + GetPrivateKeyEncodingFromJs(args, offset, kKeyContextInput); + if (config.IsEmpty()) + return ManagedEVPPKey(); + EVPKeyPointer pkey = + ParsePrivateKey(config.Release(), key.get(), key.size()); + if (!pkey) + ThrowCryptoError(env, ERR_get_error(), "Failed to read private key"); + return ManagedEVPPKey(pkey.release()); + } else { + CHECK(args[*offset]->IsObject() && allow_key_object); + KeyObject* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args[*offset].As(), ManagedEVPPKey()); + CHECK_EQ(key->GetKeyType(), kKeyTypePrivate); + (*offset) += 4; + return key->GetAsymmetricKey(); + } +} + +static bool IsRSAPrivateKey(const unsigned char* data, size_t size) { + // Both RSAPrivateKey and RSAPublicKey structures start with a SEQUENCE. + if (size >= 2 && data[0] == 0x30) { + size_t offset; + if (data[1] & 0x80) { + // Long form. + size_t n_bytes = data[1] & ~0x80; + if (n_bytes + 2 > size || n_bytes > sizeof(size_t)) + return false; + size_t i, length = 0; + for (i = 0; i < n_bytes; i++) + length = (length << 8) | data[i + 2]; + offset = 2 + n_bytes; + size = std::min(size, length + 2); + } else { + // Short form. + offset = 2; + size = std::min(size, data[1] + 2); + } + + // An RSAPrivateKey sequence always starts with a single-byte integer whose + // value is either 0 or 1, whereas an RSAPublicKey starts with the modulus + // (which is the product of two primes and therefore at least 4), so we can + // decide the type of the structure based on the first three bytes of the + // sequence. + return size - offset >= 3 && + data[offset] == 2 && + data[offset + 1] == 1 && + !(data[offset + 2] & 0xfe); + } + + return false; +} + +static ManagedEVPPKey GetPublicOrPrivateKeyFromJs( + const FunctionCallbackInfo& args, + unsigned int* offset, + bool allow_key_object, + bool allow_certificate) { + if (args[*offset]->IsString() || Buffer::HasInstance(args[*offset])) { + Environment* env = Environment::GetCurrent(args); + ByteSource data = ByteSource::FromStringOrBuffer(env, args[(*offset)++]); + NonCopyableMaybe config_ = + GetPrivateKeyEncodingFromJs(args, offset, kKeyContextInput); + if (config_.IsEmpty()) + return ManagedEVPPKey(); + PrivateKeyEncodingConfig config = config_.Release(); + EVPKeyPointer pkey; + if (config.format_ == kKeyFormatPEM) { + // For PEM, we can easily determine whether it is a public or private key + // by looking for the respective PEM tags. + ParsePublicKeyResult ret = ParsePublicKeyPEM(&pkey, data.get(), + data.size(), + allow_certificate); + if (ret == ParsePublicKeyResult::kParsePublicNotRecognized) { + pkey = ParsePrivateKey(config, data.get(), data.size()); + } + } else { + // For DER, the type determines how to parse it. SPKI, PKCS#8 and SEC1 are + // easy, but PKCS#1 can be a public key or a private key. + bool is_public; + switch (config.type_.ToChecked()) { + case kKeyEncodingPKCS1: + is_public = !IsRSAPrivateKey( + reinterpret_cast(data.get()), data.size()); + break; + case kKeyEncodingSPKI: + is_public = true; + break; + case kKeyEncodingPKCS8: + case kKeyEncodingSEC1: + is_public = false; + break; + default: + CHECK(!"Invalid key encoding type"); + } + + if (is_public) { + ParsePublicKey(&pkey, config, data.get(), data.size(), + allow_certificate); + } else { + pkey = ParsePrivateKey(config, data.get(), data.size()); + } + } + if (!pkey) + ThrowCryptoError(env, ERR_get_error(), "Failed to read asymmetric key"); + return ManagedEVPPKey(pkey.release()); + } else { + CHECK(args[*offset]->IsObject() && allow_key_object); + KeyObject* key = Unwrap(args[*offset].As()); + CHECK(key); + CHECK_NE(key->GetKeyType(), kKeyTypeSecret); + (*offset) += 4; + return key->GetAsymmetricKey(); + } +} + +static MaybeLocal WritePrivateKey( + Environment* env, + EVP_PKEY* pkey, + const PrivateKeyEncodingConfig& config) { + BIOPointer bio(BIO_new(BIO_s_mem())); + CHECK(bio); + + bool err; + + if (config.type_.ToChecked() == kKeyEncodingPKCS1) { + // PKCS#1 is only permitted for RSA keys. + CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_RSA); + + RSAPointer rsa(EVP_PKEY_get1_RSA(pkey)); + if (config.format_ == kKeyFormatPEM) { + // Encode PKCS#1 as PEM. + const char* pass = config.passphrase_.get(); + err = PEM_write_bio_RSAPrivateKey( + bio.get(), rsa.get(), + config.cipher_, + reinterpret_cast(const_cast(pass)), + config.passphrase_.size(), + nullptr, nullptr) != 1; + } else { + // Encode PKCS#1 as DER. This does not permit encryption. + CHECK_EQ(config.format_, kKeyFormatDER); + CHECK_NULL(config.cipher_); + err = i2d_RSAPrivateKey_bio(bio.get(), rsa.get()) != 1; + } + } else if (config.type_.ToChecked() == kKeyEncodingPKCS8) { + if (config.format_ == kKeyFormatPEM) { + // Encode PKCS#8 as PEM. + err = PEM_write_bio_PKCS8PrivateKey( + bio.get(), pkey, + config.cipher_, + const_cast(config.passphrase_.get()), + config.passphrase_.size(), + nullptr, nullptr) != 1; + } else { + // Encode PKCS#8 as DER. + CHECK_EQ(config.format_, kKeyFormatDER); + err = i2d_PKCS8PrivateKey_bio( + bio.get(), pkey, + config.cipher_, + const_cast(config.passphrase_.get()), + config.passphrase_.size(), + nullptr, nullptr) != 1; + } + } else { + CHECK_EQ(config.type_.ToChecked(), kKeyEncodingSEC1); + + // SEC1 is only permitted for EC keys. + CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_EC); + + ECKeyPointer ec_key(EVP_PKEY_get1_EC_KEY(pkey)); + if (config.format_ == kKeyFormatPEM) { + // Encode SEC1 as PEM. + const char* pass = config.passphrase_.get(); + err = PEM_write_bio_ECPrivateKey( + bio.get(), ec_key.get(), + config.cipher_, + reinterpret_cast(const_cast(pass)), + config.passphrase_.size(), + nullptr, nullptr) != 1; + } else { + // Encode SEC1 as DER. This does not permit encryption. + CHECK_EQ(config.format_, kKeyFormatDER); + CHECK_NULL(config.cipher_); + err = i2d_ECPrivateKey_bio(bio.get(), ec_key.get()) != 1; + } + } + + if (err) { + ThrowCryptoError(env, ERR_get_error(), "Failed to encode private key"); + return MaybeLocal(); + } + return BIOToStringOrBuffer(env, bio.get(), config.format_); +} + +ManagedEVPPKey::ManagedEVPPKey() : pkey_(nullptr) {} + +ManagedEVPPKey::ManagedEVPPKey(EVP_PKEY* pkey) : pkey_(pkey) {} + +ManagedEVPPKey::ManagedEVPPKey(const ManagedEVPPKey& key) : pkey_(nullptr) { + *this = key; +} + +ManagedEVPPKey::ManagedEVPPKey(ManagedEVPPKey&& key) { + *this = key; +} + +ManagedEVPPKey::~ManagedEVPPKey() { + EVP_PKEY_free(pkey_); +} + +ManagedEVPPKey& ManagedEVPPKey::operator=(const ManagedEVPPKey& key) { + EVP_PKEY_free(pkey_); + pkey_ = key.pkey_; + EVP_PKEY_up_ref(pkey_); + return *this; +} + +ManagedEVPPKey& ManagedEVPPKey::operator=(ManagedEVPPKey&& key) { + EVP_PKEY_free(pkey_); + pkey_ = key.pkey_; + key.pkey_ = nullptr; + return *this; +} + +ManagedEVPPKey::operator bool() const { + return pkey_ != nullptr; +} + +EVP_PKEY* ManagedEVPPKey::get() const { + return pkey_; +} + +Local KeyObject::Initialize(Environment* env, Local target) { + Local t = env->NewFunctionTemplate(New); + t->InstanceTemplate()->SetInternalFieldCount(1); + + env->SetProtoMethod(t, "init", Init); + env->SetProtoMethodNoSideEffect(t, "getSymmetricKeySize", + GetSymmetricKeySize); + env->SetProtoMethodNoSideEffect(t, "getAsymmetricKeyType", + GetAsymmetricKeyType); + env->SetProtoMethod(t, "export", Export); + + target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "KeyObject"), + t->GetFunction(env->context()).ToLocalChecked()); + + return t->GetFunction(); +} + +Local KeyObject::Create(Environment* env, + KeyType key_type, + const ManagedEVPPKey& pkey) { + CHECK_NE(key_type, kKeyTypeSecret); + Local type = Integer::New(env->isolate(), key_type); + Local obj = + env->crypto_key_object_constructor()->NewInstance(env->context(), + 1, &type) + .ToLocalChecked(); + KeyObject* key = Unwrap(obj); + CHECK(key); + if (key_type == kKeyTypePublic) + key->InitPublic(pkey); + else + key->InitPrivate(pkey); + return obj; +} + +ManagedEVPPKey KeyObject::GetAsymmetricKey() const { + CHECK_NE(key_type_, kKeyTypeSecret); + return this->asymmetric_key_; +} + +const char* KeyObject::GetSymmetricKey() const { + CHECK_EQ(key_type_, kKeyTypeSecret); + return this->symmetric_key_.get(); +} + +size_t KeyObject::GetSymmetricKeySize() const { + CHECK_EQ(key_type_, kKeyTypeSecret); + return this->symmetric_key_len_; +} + +void KeyObject::New(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + CHECK(args[0]->IsInt32()); + KeyType key_type = static_cast(args[0].As()->Value()); + Environment* env = Environment::GetCurrent(args); + new KeyObject(env, args.This(), key_type); +} + +KeyType KeyObject::GetKeyType() const { + return this->key_type_; +} + +void KeyObject::Init(const FunctionCallbackInfo& args) { + KeyObject* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args.Holder()); + + unsigned int offset; + ManagedEVPPKey pkey; + + switch (key->key_type_) { + case kKeyTypeSecret: + CHECK_EQ(args.Length(), 1); + key->InitSecret(Buffer::Data(args[0]), Buffer::Length(args[0])); + break; + case kKeyTypePublic: + CHECK_EQ(args.Length(), 3); + + offset = 0; + pkey = GetPublicKeyFromJs(args, &offset, false, false); + if (!pkey) + return; + key->InitPublic(pkey); + break; + case kKeyTypePrivate: + CHECK_EQ(args.Length(), 4); + + offset = 0; + pkey = GetPrivateKeyFromJs(args, &offset, false); + if (!pkey) + return; + key->InitPrivate(pkey); + break; + default: + CHECK(false); + } +} + +void KeyObject::InitSecret(const char* key, size_t key_len) { + CHECK_EQ(this->key_type_, kKeyTypeSecret); + + char* mem = MallocOpenSSL(key_len); + memcpy(mem, key, key_len); + this->symmetric_key_ = std::unique_ptr>(mem, + [key_len](char* p) { + OPENSSL_clear_free(p, key_len); + }); + this->symmetric_key_len_ = key_len; +} + +void KeyObject::InitPublic(const ManagedEVPPKey& pkey) { + CHECK_EQ(this->key_type_, kKeyTypePublic); + CHECK(pkey); + this->asymmetric_key_ = pkey; +} + +void KeyObject::InitPrivate(const ManagedEVPPKey& pkey) { + CHECK_EQ(this->key_type_, kKeyTypePrivate); + CHECK(pkey); + this->asymmetric_key_ = pkey; +} + +Local KeyObject::GetAsymmetricKeyType() const { + CHECK_NE(this->key_type_, kKeyTypeSecret); + switch (EVP_PKEY_id(this->asymmetric_key_.get())) { + case EVP_PKEY_RSA: + return env()->crypto_rsa_string(); + case EVP_PKEY_DSA: + return env()->crypto_dsa_string(); + case EVP_PKEY_EC: + return env()->crypto_ec_string(); + default: + CHECK(false); + } +} + +void KeyObject::GetAsymmetricKeyType(const FunctionCallbackInfo& args) { + KeyObject* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args.Holder()); + + args.GetReturnValue().Set(key->GetAsymmetricKeyType()); +} + +void KeyObject::GetSymmetricKeySize(const FunctionCallbackInfo& args) { + KeyObject* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args.Holder()); + args.GetReturnValue().Set(static_cast(key->GetSymmetricKeySize())); +} + +void KeyObject::Export(const v8::FunctionCallbackInfo& args) { + KeyObject* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args.Holder()); + + MaybeLocal result; + if (key->key_type_ == kKeyTypeSecret) { + result = key->ExportSecretKey(); + } else if (key->key_type_ == kKeyTypePublic) { + unsigned int offset = 0; + PublicKeyEncodingConfig config = + GetPublicKeyEncodingFromJs(args, &offset, kKeyContextExport); + CHECK_EQ(offset, static_cast(args.Length())); + result = key->ExportPublicKey(config); + } else { + CHECK_EQ(key->key_type_, kKeyTypePrivate); + unsigned int offset = 0; + NonCopyableMaybe config = + GetPrivateKeyEncodingFromJs(args, &offset, kKeyContextExport); + if (config.IsEmpty()) + return; + CHECK_EQ(offset, static_cast(args.Length())); + result = key->ExportPrivateKey(config.Release()); + } + + if (!result.IsEmpty()) + args.GetReturnValue().Set(result.ToLocalChecked()); +} + +Local KeyObject::ExportSecretKey() const { + return Buffer::Copy(env(), symmetric_key_.get(), symmetric_key_len_) + .ToLocalChecked(); +} + +MaybeLocal KeyObject::ExportPublicKey( + const PublicKeyEncodingConfig& config) const { + return WritePublicKey(env(), asymmetric_key_.get(), config); +} + +MaybeLocal KeyObject::ExportPrivateKey( + const PrivateKeyEncodingConfig& config) const { + return WritePrivateKey(env(), asymmetric_key_.get(), config); +} + + void CipherBase::Initialize(Environment* env, Local target) { Local t = env->NewFunctionTemplate(New); @@ -2837,6 +3669,15 @@ void CipherBase::InitIv(const char* cipher_type, } +static ByteSource GetSecretKeyBytes(Environment* env, Local value) { + // A key can be passed as a string, buffer or KeyObject with type 'secret'. + // If it is a string, we need to convert it to a buffer. We are not doing that + // in JS to avoid creating an unprotected copy on the heap. + return value->IsString() || Buffer::HasInstance(value) ? + ByteSource::FromStringOrBuffer(env, value) : + ByteSource::FromSymmetricKeyObject(value); +} + void CipherBase::InitIv(const FunctionCallbackInfo& args) { CipherBase* cipher; ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); @@ -2845,9 +3686,8 @@ void CipherBase::InitIv(const FunctionCallbackInfo& args) { CHECK_GE(args.Length(), 4); const node::Utf8Value cipher_type(env->isolate(), args[0]); - ssize_t key_len = Buffer::Length(args[1]); - const unsigned char* key_buf = reinterpret_cast( - Buffer::Data(args[1])); + const ByteSource key = GetSecretKeyBytes(env, args[1]); + ssize_t iv_len; const unsigned char* iv_buf; if (args[2]->IsNull()) { @@ -2868,7 +3708,12 @@ void CipherBase::InitIv(const FunctionCallbackInfo& args) { auth_tag_len = kNoAuthTagLength; } - cipher->InitIv(*cipher_type, key_buf, key_len, iv_buf, iv_len, auth_tag_len); + cipher->InitIv(*cipher_type, + reinterpret_cast(key.get()), + key.size(), + iv_buf, + iv_len, + auth_tag_len); } @@ -3324,9 +4169,8 @@ void Hmac::HmacInit(const FunctionCallbackInfo& args) { Environment* env = hmac->env(); const node::Utf8Value hash_type(env->isolate(), args[0]); - const char* buffer_data = Buffer::Data(args[1]); - size_t buffer_length = Buffer::Length(args[1]); - hmac->HmacInit(*hash_type, buffer_data, buffer_length); + ByteSource key = GetSecretKeyBytes(env, args[1]); + hmac->HmacInit(*hash_type, key.get(), key.size()); } @@ -3575,7 +4419,7 @@ void SignBase::CheckThrow(SignBase::Error error) { } } -static bool ApplyRSAOptions(const EVPKeyPointer& pkey, +static bool ApplyRSAOptions(const ManagedEVPPKey& pkey, EVP_PKEY_CTX* pkctx, int padding, int salt_len) { @@ -3637,7 +4481,7 @@ void Sign::SignUpdate(const FunctionCallbackInfo& args) { } static MallocedBuffer Node_SignFinal(EVPMDPointer&& mdctx, - const EVPKeyPointer& pkey, + const ManagedEVPPKey& pkey, int padding, int pss_salt_len) { unsigned char m[EVP_MAX_MD_SIZE]; @@ -3666,9 +4510,7 @@ static MallocedBuffer Node_SignFinal(EVPMDPointer&& mdctx, } Sign::SignResult Sign::SignFinal( - const char* key_pem, - int key_pem_len, - const char* passphrase, + const ManagedEVPPKey& pkey, int padding, int salt_len) { if (!mdctx_) @@ -3676,21 +4518,6 @@ Sign::SignResult Sign::SignFinal( EVPMDPointer mdctx = std::move(mdctx_); - BIOPointer bp(BIO_new_mem_buf(const_cast(key_pem), key_pem_len)); - if (!bp) - return SignResult(kSignPrivateKey); - - EVPKeyPointer pkey(PEM_read_bio_PrivateKey(bp.get(), - nullptr, - PasswordCallback, - const_cast(passphrase))); - - // Errors might be injected into OpenSSL's error stack - // without `pkey` being set to nullptr; - // cf. the test of `test_bad_rsa_privkey.pem` for an example. - if (!pkey || 0 != ERR_peek_error()) - return SignResult(kSignPrivateKey); - #ifdef NODE_FIPS_MODE /* Validate DSA2 parameters from FIPS 186-4 */ if (FIPS_mode() && EVP_PKEY_DSA == pkey->type) { @@ -3726,25 +4553,21 @@ void Sign::SignFinal(const FunctionCallbackInfo& args) { Sign* sign; ASSIGN_OR_RETURN_UNWRAP(&sign, args.Holder()); - unsigned int len = args.Length(); - - node::Utf8Value passphrase(env->isolate(), args[1]); - - size_t buf_len = Buffer::Length(args[0]); - char* buf = Buffer::Data(args[0]); + unsigned int offset = 0; + ManagedEVPPKey key = GetPrivateKeyFromJs(args, &offset, true); + if (!key) + return; - CHECK(args[2]->IsInt32()); - int padding = args[2].As()->Value(); + CHECK(args[offset]->IsInt32()); + int padding = args[offset].As()->Value(); - CHECK(args[3]->IsInt32()); - int salt_len = args[3].As()->Value(); + CHECK(args[offset + 1]->IsInt32()); + int salt_len = args[offset + 1].As()->Value(); ClearErrorOnReturn clear_error_on_return; SignResult ret = sign->SignFinal( - buf, - buf_len, - len >= 2 && !args[1]->IsNull() ? *passphrase : nullptr, + key, padding, salt_len); @@ -3760,72 +4583,6 @@ void Sign::SignFinal(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(rc); } -enum ParsePublicKeyResult { - kParsePublicOk, - kParsePublicNotRecognized, - kParsePublicFailed -}; - -static ParsePublicKeyResult TryParsePublicKey( - EVPKeyPointer* pkey, - const BIOPointer& bp, - const char* name, - // NOLINTNEXTLINE(runtime/int) - std::function parse) { - unsigned char* der_data; - long der_len; // NOLINT(runtime/int) - - // This skips surrounding data and decodes PEM to DER. - { - MarkPopErrorOnReturn mark_pop_error_on_return; - if (PEM_bytes_read_bio(&der_data, &der_len, nullptr, name, - bp.get(), nullptr, nullptr) != 1) - return kParsePublicNotRecognized; - } - - // OpenSSL might modify the pointer, so we need to make a copy before parsing. - const unsigned char* p = der_data; - pkey->reset(parse(&p, der_len)); - OPENSSL_clear_free(der_data, der_len); - - return *pkey ? kParsePublicOk : kParsePublicFailed; -} - -static ParsePublicKeyResult ParsePublicKey(EVPKeyPointer* pkey, - const char* key_pem, - int key_pem_len) { - BIOPointer bp(BIO_new_mem_buf(const_cast(key_pem), key_pem_len)); - if (!bp) - return kParsePublicFailed; - - ParsePublicKeyResult ret; - - // Try PKCS#8 first. - ret = TryParsePublicKey(pkey, bp, "PUBLIC KEY", - [](const unsigned char** p, long l) { // NOLINT(runtime/int) - return d2i_PUBKEY(nullptr, p, l); - }); - if (ret != kParsePublicNotRecognized) - return ret; - - // Maybe it is PKCS#1. - CHECK(BIO_reset(bp.get())); - ret = TryParsePublicKey(pkey, bp, "RSA PUBLIC KEY", - [](const unsigned char** p, long l) { // NOLINT(runtime/int) - return d2i_PublicKey(EVP_PKEY_RSA, nullptr, p, l); - }); - if (ret != kParsePublicNotRecognized) - return ret; - - // X.509 fallback. - CHECK(BIO_reset(bp.get())); - return TryParsePublicKey(pkey, bp, "CERTIFICATE", - [](const unsigned char** p, long l) { // NOLINT(runtime/int) - X509Pointer x509(d2i_X509(nullptr, p, l)); - return x509 ? X509_get_pubkey(x509.get()) : nullptr; - }); -} - void Verify::Initialize(Environment* env, Local target) { Local t = env->NewFunctionTemplate(New); @@ -3869,8 +4626,7 @@ void Verify::VerifyUpdate(const FunctionCallbackInfo& args) { } -SignBase::Error Verify::VerifyFinal(const char* key_pem, - int key_pem_len, +SignBase::Error Verify::VerifyFinal(const ManagedEVPPKey& pkey, const char* sig, int siglen, int padding, @@ -3879,15 +4635,11 @@ SignBase::Error Verify::VerifyFinal(const char* key_pem, if (!mdctx_) return kSignNotInitialised; - EVPKeyPointer pkey; unsigned char m[EVP_MAX_MD_SIZE]; unsigned int m_len; *verify_result = false; EVPMDPointer mdctx = std::move(mdctx_); - if (ParsePublicKey(&pkey, key_pem, key_pem_len) != kParsePublicOk) - return kSignPublicKey; - if (!EVP_DigestFinal_ex(mdctx.get(), m, &m_len)) return kSignPublicKey; @@ -3915,20 +4667,20 @@ void Verify::VerifyFinal(const FunctionCallbackInfo& args) { Verify* verify; ASSIGN_OR_RETURN_UNWRAP(&verify, args.Holder()); - char* kbuf = Buffer::Data(args[0]); - ssize_t klen = Buffer::Length(args[0]); + unsigned int offset = 0; + ManagedEVPPKey pkey = GetPublicKeyFromJs(args, &offset, true, true); - char* hbuf = Buffer::Data(args[1]); - ssize_t hlen = Buffer::Length(args[1]); + char* hbuf = Buffer::Data(args[offset]); + ssize_t hlen = Buffer::Length(args[offset]); - CHECK(args[2]->IsInt32()); - int padding = args[2].As()->Value(); + CHECK(args[offset + 1]->IsInt32()); + int padding = args[offset + 1].As()->Value(); - CHECK(args[3]->IsInt32()); - int salt_len = args[3].As()->Value(); + CHECK(args[offset + 2]->IsInt32()); + int salt_len = args[offset + 2].As()->Value(); bool verify_result; - Error err = verify->VerifyFinal(kbuf, klen, hbuf, hlen, padding, salt_len, + Error err = verify->VerifyFinal(pkey, hbuf, hlen, padding, salt_len, &verify_result); if (err != kSignOk) return verify->CheckThrow(err); @@ -3939,36 +4691,12 @@ void Verify::VerifyFinal(const FunctionCallbackInfo& args) { template -bool PublicKeyCipher::Cipher(const char* key_pem, - int key_pem_len, - const char* passphrase, +bool PublicKeyCipher::Cipher(const ManagedEVPPKey& pkey, int padding, const unsigned char* data, int len, unsigned char** out, size_t* out_len) { - EVPKeyPointer pkey; - - // Check if this is a PKCS#8 or RSA public key before trying as X.509 and - // private key. - if (operation == kPublic) { - ParsePublicKeyResult pkeyres = ParsePublicKey(&pkey, key_pem, key_pem_len); - if (pkeyres == kParsePublicFailed) - return false; - } - if (!pkey) { - // Private key fallback. - BIOPointer bp(BIO_new_mem_buf(const_cast(key_pem), key_pem_len)); - if (!bp) - return false; - pkey.reset(PEM_read_bio_PrivateKey(bp.get(), - nullptr, - PasswordCallback, - const_cast(passphrase))); - if (!pkey) - return false; - } - EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(pkey.get(), nullptr)); if (!ctx) return false; @@ -3995,18 +4723,17 @@ template & args) { Environment* env = Environment::GetCurrent(args); - THROW_AND_RETURN_IF_NOT_BUFFER(env, args[0], "Key"); - char* kbuf = Buffer::Data(args[0]); - ssize_t klen = Buffer::Length(args[0]); + unsigned int offset = 0; + ManagedEVPPKey pkey = GetPublicOrPrivateKeyFromJs(args, &offset, true, true); + if (!pkey) + return; - THROW_AND_RETURN_IF_NOT_BUFFER(env, args[1], "Data"); - char* buf = Buffer::Data(args[1]); - ssize_t len = Buffer::Length(args[1]); + THROW_AND_RETURN_IF_NOT_BUFFER(env, args[offset], "Data"); + char* buf = Buffer::Data(args[offset]); + ssize_t len = Buffer::Length(args[offset]); uint32_t padding; - if (!args[2]->Uint32Value(env->context()).To(&padding)) return; - - String::Utf8Value passphrase(args.GetIsolate(), args[3]); + if (!args[offset + 1]->Uint32Value(env->context()).To(&padding)) return; unsigned char* out_value = nullptr; size_t out_len = 0; @@ -4014,9 +4741,7 @@ void PublicKeyCipher::Cipher(const FunctionCallbackInfo& args) { ClearErrorOnReturn clear_error_on_return; bool r = Cipher( - kbuf, - klen, - args.Length() >= 4 && !args[3]->IsNull() ? *passphrase : nullptr, + pkey, padding, reinterpret_cast(buf), len, @@ -5021,46 +5746,17 @@ class ECKeyPairGenerationConfig : public KeyPairGenerationConfig { const int param_encoding_; }; -enum PKEncodingType { - // RSAPublicKey / RSAPrivateKey according to PKCS#1. - PK_ENCODING_PKCS1, - // PrivateKeyInfo or EncryptedPrivateKeyInfo according to PKCS#8. - PK_ENCODING_PKCS8, - // SubjectPublicKeyInfo according to X.509. - PK_ENCODING_SPKI, - // ECPrivateKey according to SEC1. - PK_ENCODING_SEC1 -}; - -enum PKFormatType { - PK_FORMAT_DER, - PK_FORMAT_PEM -}; - -struct KeyPairEncodingConfig { - PKEncodingType type_; - PKFormatType format_; -}; - -typedef KeyPairEncodingConfig PublicKeyEncodingConfig; - -struct PrivateKeyEncodingConfig : public KeyPairEncodingConfig { - const EVP_CIPHER* cipher_; - // This char* will be passed to OPENSSL_clear_free. - std::shared_ptr passphrase_; - unsigned int passphrase_length_; -}; - class GenerateKeyPairJob : public CryptoJob { public: GenerateKeyPairJob(Environment* env, std::unique_ptr config, PublicKeyEncodingConfig public_key_encoding, - PrivateKeyEncodingConfig private_key_encoding) + PrivateKeyEncodingConfig&& private_key_encoding) : CryptoJob(env), config_(std::move(config)), public_key_encoding_(public_key_encoding), - private_key_encoding_(private_key_encoding), + private_key_encoding_(std::forward( + private_key_encoding)), pkey_(nullptr) {} inline void DoThreadPoolWork() override { @@ -5089,7 +5785,7 @@ class GenerateKeyPairJob : public CryptoJob { EVP_PKEY* pkey = nullptr; if (EVP_PKEY_keygen(ctx.get(), &pkey) != 1) return false; - pkey_.reset(pkey); + pkey_ = ManagedEVPPKey(pkey); return true; } @@ -5116,197 +5812,59 @@ class GenerateKeyPairJob : public CryptoJob { } inline bool EncodeKeys(Local* pubkey, Local* privkey) { - EVP_PKEY* pkey = pkey_.get(); - BIOPointer bio(BIO_new(BIO_s_mem())); - CHECK(bio); - // Encode the public key. - if (public_key_encoding_.type_ == PK_ENCODING_PKCS1) { - // PKCS#1 is only valid for RSA keys. - CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_RSA); - RSAPointer rsa(EVP_PKEY_get1_RSA(pkey)); - if (public_key_encoding_.format_ == PK_FORMAT_PEM) { - // Encode PKCS#1 as PEM. - if (PEM_write_bio_RSAPublicKey(bio.get(), rsa.get()) != 1) - return false; - } else { - // Encode PKCS#1 as DER. - CHECK_EQ(public_key_encoding_.format_, PK_FORMAT_DER); - if (i2d_RSAPublicKey_bio(bio.get(), rsa.get()) != 1) - return false; - } + if (public_key_encoding_.output_key_object_) { + // Note that this has the downside of containing sensitive data of the + // private key. + *pubkey = KeyObject::Create(env, kKeyTypePublic, pkey_); } else { - CHECK_EQ(public_key_encoding_.type_, PK_ENCODING_SPKI); - if (public_key_encoding_.format_ == PK_FORMAT_PEM) { - // Encode SPKI as PEM. - if (PEM_write_bio_PUBKEY(bio.get(), pkey) != 1) - return false; - } else { - // Encode SPKI as DER. - CHECK_EQ(public_key_encoding_.format_, PK_FORMAT_DER); - if (i2d_PUBKEY_bio(bio.get(), pkey) != 1) - return false; - } + MaybeLocal maybe_pubkey = + WritePublicKey(env, pkey_.get(), public_key_encoding_); + if (maybe_pubkey.IsEmpty()) + return false; + *pubkey = maybe_pubkey.ToLocalChecked(); } - // Convert the contents of the BIO to a JavaScript object. - BIOToStringOrBuffer(bio.get(), public_key_encoding_.format_, pubkey); - USE(BIO_reset(bio.get())); - - // Now do the same for the private key (which is a bit more difficult). - if (private_key_encoding_.type_ == PK_ENCODING_PKCS1) { - // PKCS#1 is only permitted for RSA keys. - CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_RSA); - - RSAPointer rsa(EVP_PKEY_get1_RSA(pkey)); - if (private_key_encoding_.format_ == PK_FORMAT_PEM) { - // Encode PKCS#1 as PEM. - char* pass = private_key_encoding_.passphrase_.get(); - if (PEM_write_bio_RSAPrivateKey( - bio.get(), rsa.get(), - private_key_encoding_.cipher_, - reinterpret_cast(pass), - private_key_encoding_.passphrase_length_, - nullptr, nullptr) != 1) - return false; - } else { - // Encode PKCS#1 as DER. This does not permit encryption. - CHECK_EQ(private_key_encoding_.format_, PK_FORMAT_DER); - CHECK_NULL(private_key_encoding_.cipher_); - if (i2d_RSAPrivateKey_bio(bio.get(), rsa.get()) != 1) - return false; - } - } else if (private_key_encoding_.type_ == PK_ENCODING_PKCS8) { - if (private_key_encoding_.format_ == PK_FORMAT_PEM) { - // Encode PKCS#8 as PEM. - if (PEM_write_bio_PKCS8PrivateKey( - bio.get(), pkey, - private_key_encoding_.cipher_, - private_key_encoding_.passphrase_.get(), - private_key_encoding_.passphrase_length_, - nullptr, nullptr) != 1) - return false; - } else { - // Encode PKCS#8 as DER. - CHECK_EQ(private_key_encoding_.format_, PK_FORMAT_DER); - if (i2d_PKCS8PrivateKey_bio( - bio.get(), pkey, - private_key_encoding_.cipher_, - private_key_encoding_.passphrase_.get(), - private_key_encoding_.passphrase_length_, - nullptr, nullptr) != 1) - return false; - } + // Now do the same for the private key. + if (private_key_encoding_.output_key_object_) { + *privkey = KeyObject::Create(env, kKeyTypePrivate, pkey_); } else { - CHECK_EQ(private_key_encoding_.type_, PK_ENCODING_SEC1); - - // SEC1 is only permitted for EC keys. - CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_EC); - - ECKeyPointer ec_key(EVP_PKEY_get1_EC_KEY(pkey)); - if (private_key_encoding_.format_ == PK_FORMAT_PEM) { - // Encode SEC1 as PEM. - char* pass = private_key_encoding_.passphrase_.get(); - if (PEM_write_bio_ECPrivateKey( - bio.get(), ec_key.get(), - private_key_encoding_.cipher_, - reinterpret_cast(pass), - private_key_encoding_.passphrase_length_, - nullptr, nullptr) != 1) - return false; - } else { - // Encode SEC1 as DER. This does not permit encryption. - CHECK_EQ(private_key_encoding_.format_, PK_FORMAT_DER); - CHECK_NULL(private_key_encoding_.cipher_); - if (i2d_ECPrivateKey_bio(bio.get(), ec_key.get()) != 1) - return false; - } + MaybeLocal maybe_privkey = + WritePrivateKey(env, pkey_.get(), private_key_encoding_); + if (maybe_privkey.IsEmpty()) + return false; + *privkey = maybe_privkey.ToLocalChecked(); } - BIOToStringOrBuffer(bio.get(), private_key_encoding_.format_, privkey); return true; } - inline void BIOToStringOrBuffer(BIO* bio, PKFormatType format, - Local* out) const { - BUF_MEM* bptr; - BIO_get_mem_ptr(bio, &bptr); - if (format == PK_FORMAT_PEM) { - // PEM is an ASCII format, so we will return it as a string. - *out = String::NewFromUtf8(env->isolate(), bptr->data, - NewStringType::kNormal, - bptr->length).ToLocalChecked(); - } else { - CHECK_EQ(format, PK_FORMAT_DER); - // DER is binary, return it as a buffer. - *out = Buffer::Copy(env, bptr->data, bptr->length).ToLocalChecked(); - } - } - private: CryptoErrorVector errors_; std::unique_ptr config_; PublicKeyEncodingConfig public_key_encoding_; PrivateKeyEncodingConfig private_key_encoding_; - EVPKeyPointer pkey_; + ManagedEVPPKey pkey_; }; void GenerateKeyPair(const FunctionCallbackInfo& args, - unsigned int n_opts, + unsigned int offset, std::unique_ptr config) { Environment* env = Environment::GetCurrent(args); - PublicKeyEncodingConfig public_key_encoding; - PrivateKeyEncodingConfig private_key_encoding; - - // Public key encoding: type (int) + pem (bool) - CHECK(args[n_opts]->IsInt32()); - public_key_encoding.type_ = static_cast( - args[n_opts].As()->Value()); - CHECK(args[n_opts + 1]->IsInt32()); - public_key_encoding.format_ = static_cast( - args[n_opts + 1].As()->Value()); - - // Private key encoding: type (int) + pem (bool) + cipher (optional, string) + - // passphrase (optional, string) - CHECK(args[n_opts + 2]->IsInt32()); - private_key_encoding.type_ = static_cast( - args[n_opts + 2].As()->Value()); - CHECK(args[n_opts + 1]->IsInt32()); - private_key_encoding.format_ = static_cast( - args[n_opts + 3].As()->Value()); - if (args[n_opts + 4]->IsString()) { - String::Utf8Value cipher_name(env->isolate(), - args[n_opts + 4].As()); - private_key_encoding.cipher_ = EVP_get_cipherbyname(*cipher_name); - if (private_key_encoding.cipher_ == nullptr) - return env->ThrowError("Unknown cipher"); - - // We need to take ownership of the string and want to avoid creating an - // unnecessary copy in memory, that's why we are not using String::Utf8Value - // here. - CHECK(args[n_opts + 5]->IsString()); - Local passphrase = args[n_opts + 5].As(); - int len = passphrase->Utf8Length(env->isolate()); - private_key_encoding.passphrase_length_ = len; - void* mem = OPENSSL_malloc(private_key_encoding.passphrase_length_ + 1); - CHECK_NOT_NULL(mem); - private_key_encoding.passphrase_.reset(static_cast(mem), - [len](char* p) { - OPENSSL_clear_free(p, len); - }); - passphrase->WriteUtf8(env->isolate(), - private_key_encoding.passphrase_.get()); - } else { - CHECK(args[n_opts + 5]->IsNullOrUndefined()); - private_key_encoding.cipher_ = nullptr; - private_key_encoding.passphrase_length_ = 0; - } + + PublicKeyEncodingConfig public_key_encoding = + GetPublicKeyEncodingFromJs(args, &offset, kKeyContextGenerate); + NonCopyableMaybe private_key_encoding = + GetPrivateKeyEncodingFromJs(args, &offset, kKeyContextGenerate); + + if (private_key_encoding.IsEmpty()) + return; std::unique_ptr job( new GenerateKeyPairJob(env, std::move(config), public_key_encoding, - private_key_encoding)); - if (args[n_opts + 6]->IsObject()) - return GenerateKeyPairJob::Run(std::move(job), args[n_opts + 6]); + private_key_encoding.Release())); + if (args[offset]->IsObject()) + return GenerateKeyPairJob::Run(std::move(job), args[offset]); env->PrintSyncTrace(); job->DoThreadPoolWork(); Local err, pubkey, privkey; @@ -5750,6 +6308,7 @@ void Initialize(Local target, Environment* env = Environment::GetCurrent(context); SecureContext::Initialize(env, target); + env->set_crypto_key_object_constructor(KeyObject::Initialize(env, target)); CipherBase::Initialize(env, target); DiffieHellman::Initialize(env, target); ECDH::Initialize(env, target); @@ -5778,12 +6337,15 @@ void Initialize(Local target, env->SetMethod(target, "generateKeyPairEC", GenerateKeyPairEC); NODE_DEFINE_CONSTANT(target, OPENSSL_EC_NAMED_CURVE); NODE_DEFINE_CONSTANT(target, OPENSSL_EC_EXPLICIT_CURVE); - NODE_DEFINE_CONSTANT(target, PK_ENCODING_PKCS1); - NODE_DEFINE_CONSTANT(target, PK_ENCODING_PKCS8); - NODE_DEFINE_CONSTANT(target, PK_ENCODING_SPKI); - NODE_DEFINE_CONSTANT(target, PK_ENCODING_SEC1); - NODE_DEFINE_CONSTANT(target, PK_FORMAT_DER); - NODE_DEFINE_CONSTANT(target, PK_FORMAT_PEM); + NODE_DEFINE_CONSTANT(target, kKeyEncodingPKCS1); + NODE_DEFINE_CONSTANT(target, kKeyEncodingPKCS8); + NODE_DEFINE_CONSTANT(target, kKeyEncodingSPKI); + NODE_DEFINE_CONSTANT(target, kKeyEncodingSEC1); + NODE_DEFINE_CONSTANT(target, kKeyFormatDER); + NODE_DEFINE_CONSTANT(target, kKeyFormatPEM); + NODE_DEFINE_CONSTANT(target, kKeyTypeSecret); + NODE_DEFINE_CONSTANT(target, kKeyTypePublic); + NODE_DEFINE_CONSTANT(target, kKeyTypePrivate); env->SetMethod(target, "randomBytes", RandomBytes); env->SetMethodNoSideEffect(target, "timingSafeEqual", TimingSafeEqual); env->SetMethodNoSideEffect(target, "getSSLCiphers", GetSSLCiphers); diff --git a/src/node_crypto.h b/src/node_crypto.h index 0ee45cf9ea2c02..ef8f2bce2e99f4 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -341,6 +341,163 @@ class SSLWrap { friend class SecureContext; }; +// A helper class representing a read-only byte array. When deallocated, its +// contents are zeroed. +class ByteSource { + public: + ByteSource() = default; + ByteSource(ByteSource&& other); + ~ByteSource(); + + ByteSource& operator=(ByteSource&& other); + + const char* get() const; + size_t size() const; + + static ByteSource FromStringOrBuffer(Environment* env, + v8::Local value); + + static ByteSource FromString(Environment* env, + v8::Local str, + bool ntc = false); + + static ByteSource FromBuffer(v8::Local buffer, + bool ntc = false); + + static ByteSource NullTerminatedCopy(Environment* env, + v8::Local value); + + static ByteSource FromSymmetricKeyObject(v8::Local handle); + + private: + const char* data_ = nullptr; + char* allocated_data_ = nullptr; + size_t size_ = 0; + + ByteSource(const char* data, char* allocated_data, size_t size); + + static ByteSource Allocated(char* data, size_t size); + static ByteSource Foreign(const char* data, size_t size); + + DISALLOW_COPY_AND_ASSIGN(ByteSource); +}; + +enum PKEncodingType { + // RSAPublicKey / RSAPrivateKey according to PKCS#1. + kKeyEncodingPKCS1, + // PrivateKeyInfo or EncryptedPrivateKeyInfo according to PKCS#8. + kKeyEncodingPKCS8, + // SubjectPublicKeyInfo according to X.509. + kKeyEncodingSPKI, + // ECPrivateKey according to SEC1. + kKeyEncodingSEC1 +}; + +enum PKFormatType { + kKeyFormatDER, + kKeyFormatPEM +}; + +struct AsymmetricKeyEncodingConfig { + bool output_key_object_; + PKFormatType format_; + v8::Maybe type_ = v8::Nothing(); +}; + +typedef AsymmetricKeyEncodingConfig PublicKeyEncodingConfig; + +struct PrivateKeyEncodingConfig : public AsymmetricKeyEncodingConfig { + const EVP_CIPHER* cipher_; + ByteSource passphrase_; +}; + +enum KeyType { + kKeyTypeSecret, + kKeyTypePublic, + kKeyTypePrivate +}; + +// This uses the built-in reference counter of OpenSSL to manage an EVP_PKEY +// which is slightly more efficient than using a shared pointer and easier to +// use. +class ManagedEVPPKey { + public: + ManagedEVPPKey(); + explicit ManagedEVPPKey(EVP_PKEY* pkey); + ManagedEVPPKey(const ManagedEVPPKey& key); + ManagedEVPPKey(ManagedEVPPKey&& key); + ~ManagedEVPPKey(); + + ManagedEVPPKey& operator=(const ManagedEVPPKey& key); + ManagedEVPPKey& operator=(ManagedEVPPKey&& key); + + operator bool() const; + EVP_PKEY* get() const; + + private: + EVP_PKEY* pkey_; +}; + +class KeyObject : public BaseObject { + public: + static v8::Local Initialize(Environment* env, + v8::Local target); + + static v8::Local Create(Environment* env, + KeyType type, + const ManagedEVPPKey& pkey); + + // TODO(tniessen): track the memory used by OpenSSL types + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(KeyObject) + SET_SELF_SIZE(KeyObject) + + KeyType GetKeyType() const; + + // These functions allow unprotected access to the raw key material and should + // only be used to implement cryptograohic operations requiring the key. + ManagedEVPPKey GetAsymmetricKey() const; + const char* GetSymmetricKey() const; + size_t GetSymmetricKeySize() const; + + protected: + static void New(const v8::FunctionCallbackInfo& args); + + static void Init(const v8::FunctionCallbackInfo& args); + void InitSecret(const char* key, size_t key_len); + void InitPublic(const ManagedEVPPKey& pkey); + void InitPrivate(const ManagedEVPPKey& pkey); + + static void GetAsymmetricKeyType( + const v8::FunctionCallbackInfo& args); + v8::Local GetAsymmetricKeyType() const; + + static void GetSymmetricKeySize( + const v8::FunctionCallbackInfo& args); + + static void Export(const v8::FunctionCallbackInfo& args); + v8::Local ExportSecretKey() const; + v8::MaybeLocal ExportPublicKey( + const PublicKeyEncodingConfig& config) const; + v8::MaybeLocal ExportPrivateKey( + const PrivateKeyEncodingConfig& config) const; + + KeyObject(Environment* env, + v8::Local wrap, + KeyType key_type) + : BaseObject(env, wrap), + key_type_(key_type), + symmetric_key_(nullptr, nullptr) { + MakeWeak(); + } + + private: + const KeyType key_type_; + std::unique_ptr> symmetric_key_; + unsigned int symmetric_key_len_; + ManagedEVPPKey asymmetric_key_; +}; + class CipherBase : public BaseObject { public: static void Initialize(Environment* env, v8::Local target); @@ -529,9 +686,7 @@ class Sign : public SignBase { }; SignResult SignFinal( - const char* key_pem, - int key_pem_len, - const char* passphrase, + const ManagedEVPPKey& pkey, int padding, int saltlen); @@ -550,8 +705,7 @@ class Verify : public SignBase { public: static void Initialize(Environment* env, v8::Local target); - Error VerifyFinal(const char* key_pem, - int key_pem_len, + Error VerifyFinal(const ManagedEVPPKey& key, const char* sig, int siglen, int padding, @@ -584,9 +738,7 @@ class PublicKeyCipher { template - static bool Cipher(const char* key_pem, - int key_pem_len, - const char* passphrase, + static bool Cipher(const ManagedEVPPKey& pkey, int padding, const unsigned char* data, int len, diff --git a/src/util.h b/src/util.h index 9a10904394de61..36a2ec9e3a6270 100644 --- a/src/util.h +++ b/src/util.h @@ -473,6 +473,29 @@ struct MallocedBuffer { MallocedBuffer& operator=(const MallocedBuffer&) = delete; }; +template +class NonCopyableMaybe { + public: + NonCopyableMaybe() : empty_(true) {} + explicit NonCopyableMaybe(T&& value) + : empty_(false), + value_(std::move(value)) {} + + bool IsEmpty() const { + return empty_; + } + + T&& Release() { + CHECK_EQ(empty_, false); + empty_ = true; + return std::move(value_); + } + + private: + bool empty_; + T value_; +}; + // Test whether some value can be called with (). template struct is_callable : std::is_function { }; diff --git a/test/parallel/test-crypto-cipheriv-decipheriv.js b/test/parallel/test-crypto-cipheriv-decipheriv.js index c0073abcfd4ee9..e4c7fced584c5f 100644 --- a/test/parallel/test-crypto-cipheriv-decipheriv.js +++ b/test/parallel/test-crypto-cipheriv-decipheriv.js @@ -101,8 +101,8 @@ function testCipher3(key, iv) { { code: 'ERR_INVALID_ARG_TYPE', type: TypeError, - message: 'The "key" argument must be one of type string, Buffer, ' + - 'TypedArray, or DataView. Received type object' + message: 'The "key" argument must be one of type Buffer, TypedArray, ' + + 'DataView, string, or KeyObject. Received type object' }); common.expectsError( @@ -138,8 +138,8 @@ function testCipher3(key, iv) { { code: 'ERR_INVALID_ARG_TYPE', type: TypeError, - message: 'The "key" argument must be one of type string, Buffer, ' + - 'TypedArray, or DataView. Received type object' + message: 'The "key" argument must be one of type Buffer, TypedArray, ' + + 'DataView, string, or KeyObject. Received type object' }); common.expectsError( diff --git a/test/parallel/test-crypto-hmac.js b/test/parallel/test-crypto-hmac.js index f21db29d36107f..9e0e364a4fa395 100644 --- a/test/parallel/test-crypto-hmac.js +++ b/test/parallel/test-crypto-hmac.js @@ -36,20 +36,37 @@ common.expectsError( { code: 'ERR_INVALID_ARG_TYPE', type: TypeError, - message: 'The "key" argument must be one of type string, TypedArray, or ' + - 'DataView. Received type object' + message: 'The "key" argument must be one of type Buffer, TypedArray, ' + + 'DataView, string, or KeyObject. Received type object' }); +function testHmac(algo, key, data, expected) { + // FIPS does not support MD5. + if (common.hasFipsCrypto && algo === 'md5') + return; + + if (!Array.isArray(data)) + data = [data]; + + // If the key is a Buffer, test Hmac with a key object as well. + const keyWrappers = [ + (key) => key, + ...(typeof key === 'string' ? [] : [crypto.createSecretKey]) + ]; + + for (const keyWrapper of keyWrappers) { + const hmac = crypto.createHmac(algo, keyWrapper(key)); + for (const chunk of data) + hmac.update(chunk); + const actual = hmac.digest('hex'); + assert.strictEqual(actual, expected); + } +} + { - // Test HMAC - const actual = crypto.createHmac('sha1', 'Node') - .update('some data') - .update('to hmac') - .digest('hex'); - const expected = '19fd6e1ba73d9ed2224dd5094a71babe85d9a892'; - assert.strictEqual(actual, - expected, - `Test HMAC: ${actual} must be ${expected}`); + // Test HMAC with multiple updates. + testHmac('sha1', 'Node', ['some data', 'to hmac'], + '19fd6e1ba73d9ed2224dd5094a71babe85d9a892'); } // Test HMAC (Wikipedia Test Cases) @@ -96,24 +113,11 @@ const wikipedia = [ }, ]; -for (let i = 0, l = wikipedia.length; i < l; i++) { - for (const hash in wikipedia[i].hmac) { - // FIPS does not support MD5. - if (common.hasFipsCrypto && hash === 'md5') - continue; - const expected = wikipedia[i].hmac[hash]; - const actual = crypto.createHmac(hash, wikipedia[i].key) - .update(wikipedia[i].data) - .digest('hex'); - assert.strictEqual( - actual, - expected, - `Test HMAC-${hash} wikipedia case ${i + 1}: ${actual} must be ${expected}` - ); - } +for (const { key, data, hmac } of wikipedia) { + for (const hash in hmac) + testHmac(hash, key, data, hmac[hash]); } - // Test HMAC-SHA-* (rfc 4231 Test Cases) const rfc4231 = [ { @@ -342,6 +346,10 @@ const rfc2202_md5 = [ hmac: '6f630fad67cda0ee1fb1f562db3aa53e' } ]; + +for (const { key, data, hmac } of rfc2202_md5) + testHmac('md5', key, data, hmac); + const rfc2202_sha1 = [ { key: Buffer.from('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b', 'hex'), @@ -397,30 +405,8 @@ const rfc2202_sha1 = [ } ]; -if (!common.hasFipsCrypto) { - for (let i = 0, l = rfc2202_md5.length; i < l; i++) { - const actual = crypto.createHmac('md5', rfc2202_md5[i].key) - .update(rfc2202_md5[i].data) - .digest('hex'); - const expected = rfc2202_md5[i].hmac; - assert.strictEqual( - actual, - expected, - `Test HMAC-MD5 rfc 2202 case ${i + 1}: ${actual} must be ${expected}` - ); - } -} -for (let i = 0, l = rfc2202_sha1.length; i < l; i++) { - const actual = crypto.createHmac('sha1', rfc2202_sha1[i].key) - .update(rfc2202_sha1[i].data) - .digest('hex'); - const expected = rfc2202_sha1[i].hmac; - assert.strictEqual( - actual, - expected, - `Test HMAC-SHA1 rfc 2202 case ${i + 1}: ${actual} must be ${expected}` - ); -} +for (const { key, data, hmac } of rfc2202_sha1) + testHmac('sha1', key, data, hmac); common.expectsError( () => crypto.createHmac('sha256', 'w00t').digest('ucs2'), diff --git a/test/parallel/test-crypto-key-objects.js b/test/parallel/test-crypto-key-objects.js new file mode 100644 index 00000000000000..dddbd5f2703d93 --- /dev/null +++ b/test/parallel/test-crypto-key-objects.js @@ -0,0 +1,107 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + createCipheriv, + createDecipheriv, + createSecretKey, + createPublicKey, + createPrivateKey, + randomBytes, + publicEncrypt, + privateDecrypt +} = require('crypto'); + +const fixtures = require('../common/fixtures'); + +const publicPem = fixtures.readSync('test_rsa_pubkey.pem', 'ascii'); +const privatePem = fixtures.readSync('test_rsa_privkey.pem', 'ascii'); + +{ + // Attempting to create an empty key should throw. + common.expectsError(() => { + createSecretKey(Buffer.alloc(0)); + }, { + type: RangeError, + code: 'ERR_OUT_OF_RANGE', + message: 'The value of "key.byteLength" is out of range. ' + + 'It must be > 0. Received 0' + }); +} + +{ + const keybuf = randomBytes(32); + const key = createSecretKey(keybuf); + assert.strictEqual(key.type, 'secret'); + assert.strictEqual(key.symmetricKeySize, 32); + assert.strictEqual(key.asymmetricKeyType, undefined); + + const exportedKey = key.export(); + assert(keybuf.equals(exportedKey)); + + const plaintext = Buffer.from('Hello world', 'utf8'); + + const cipher = createCipheriv('aes-256-ecb', key, null); + const ciphertext = Buffer.concat([ + cipher.update(plaintext), cipher.final() + ]); + + const decipher = createDecipheriv('aes-256-ecb', key, null); + const deciphered = Buffer.concat([ + decipher.update(ciphertext), decipher.final() + ]); + + assert(plaintext.equals(deciphered)); +} + +{ + const publicKey = createPublicKey(publicPem); + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa'); + assert.strictEqual(publicKey.symmetricKeySize, undefined); + + const privateKey = createPrivateKey(privatePem); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa'); + assert.strictEqual(privateKey.symmetricKeySize, undefined); + + const publicDER = publicKey.export({ + format: 'der', + type: 'pkcs1' + }); + + const privateDER = privateKey.export({ + format: 'der', + type: 'pkcs1' + }); + + assert(Buffer.isBuffer(publicDER)); + assert(Buffer.isBuffer(privateDER)); + + const plaintext = Buffer.from('Hello world', 'utf8'); + const ciphertexts = [ + publicEncrypt(publicKey, plaintext), + publicEncrypt({ key: publicKey }, plaintext), + // Test distinguishing PKCS#1 public and private keys based on the + // DER-encoded data only. + publicEncrypt({ format: 'der', type: 'pkcs1', key: publicDER }, plaintext), + publicEncrypt({ format: 'der', type: 'pkcs1', key: privateDER }, plaintext) + ]; + + const decryptionKeys = [ + privateKey, + { format: 'pem', key: privatePem }, + { format: 'der', type: 'pkcs1', key: privateDER } + ]; + + for (const ciphertext of ciphertexts) { + for (const key of decryptionKeys) { + const deciphered = privateDecrypt(key, ciphertext); + assert(plaintext.equals(deciphered)); + } + } +} diff --git a/test/parallel/test-crypto-keygen.js b/test/parallel/test-crypto-keygen.js index 241e4aa73ac684..43319c38599ebf 100644 --- a/test/parallel/test-crypto-keygen.js +++ b/test/parallel/test-crypto-keygen.js @@ -65,23 +65,6 @@ const pkcs8EncExp = getRegExpForPEM('ENCRYPTED PRIVATE KEY'); const sec1Exp = getRegExpForPEM('EC PRIVATE KEY'); const sec1EncExp = (cipher) => getRegExpForPEM('EC PRIVATE KEY', cipher); -// Since our own APIs only accept PEM, not DER, we need to convert DER to PEM -// for testing. -function convertDERToPEM(label, der) { - const base64 = der.toString('base64'); - const lines = []; - let i = 0; - while (i < base64.length) { - const n = Math.min(base64.length - i, 64); - lines.push(base64.substr(i, n)); - i += n; - } - const body = lines.join('\n'); - const r = `-----BEGIN ${label}-----\n${body}\n-----END ${label}-----\n`; - assert(getRegExpForPEM(label).test(r)); - return r; -} - { // To make the test faster, we will only test sync key generation once and // with a relatively small key. @@ -113,14 +96,16 @@ function convertDERToPEM(label, der) { } { + const publicKeyEncoding = { + type: 'pkcs1', + format: 'der' + }; + // Test async RSA key generation. generateKeyPair('rsa', { publicExponent: 0x10001, modulusLength: 512, - publicKeyEncoding: { - type: 'pkcs1', - format: 'der' - }, + publicKeyEncoding, privateKeyEncoding: { type: 'pkcs1', format: 'pem' @@ -128,16 +113,14 @@ function convertDERToPEM(label, der) { }, common.mustCall((err, publicKeyDER, privateKey) => { assert.ifError(err); - // The public key is encoded as DER (which is binary) instead of PEM. We - // will still need to convert it to PEM for testing. assert(Buffer.isBuffer(publicKeyDER)); - const publicKey = convertDERToPEM('RSA PUBLIC KEY', publicKeyDER); - assertApproximateSize(publicKey, 180); + assertApproximateSize(publicKeyDER, 74); assert.strictEqual(typeof privateKey, 'string'); assert(pkcs1PrivExp.test(privateKey)); assertApproximateSize(privateKey, 512); + const publicKey = { key: publicKeyDER, ...publicKeyEncoding }; testEncryptDecrypt(publicKey, privateKey); testSignVerify(publicKey, privateKey); })); @@ -146,10 +129,7 @@ function convertDERToPEM(label, der) { generateKeyPair('rsa', { publicExponent: 0x10001, modulusLength: 512, - publicKeyEncoding: { - type: 'pkcs1', - format: 'der' - }, + publicKeyEncoding, privateKeyEncoding: { type: 'pkcs1', format: 'pem', @@ -159,16 +139,14 @@ function convertDERToPEM(label, der) { }, common.mustCall((err, publicKeyDER, privateKey) => { assert.ifError(err); - // The public key is encoded as DER (which is binary) instead of PEM. We - // will still need to convert it to PEM for testing. assert(Buffer.isBuffer(publicKeyDER)); - const publicKey = convertDERToPEM('RSA PUBLIC KEY', publicKeyDER); - assertApproximateSize(publicKey, 180); + assertApproximateSize(publicKeyDER, 74); assert.strictEqual(typeof privateKey, 'string'); assert(pkcs1EncExp('AES-256-CBC').test(privateKey)); // Since the private key is encrypted, signing shouldn't work anymore. + const publicKey = { key: publicKeyDER, ...publicKeyEncoding }; assert.throws(() => { testSignVerify(publicKey, privateKey); }, /bad decrypt|asn1 encoding routines/); @@ -180,6 +158,11 @@ function convertDERToPEM(label, der) { } { + const privateKeyEncoding = { + type: 'pkcs8', + format: 'der' + }; + // Test async DSA key generation. generateKeyPair('dsa', { modulusLength: 512, @@ -189,10 +172,9 @@ function convertDERToPEM(label, der) { format: 'pem' }, privateKeyEncoding: { - type: 'pkcs8', - format: 'der', cipher: 'aes-128-cbc', - passphrase: 'secret' + passphrase: 'secret', + ...privateKeyEncoding } }, common.mustCall((err, publicKey, privateKeyDER) => { assert.ifError(err); @@ -201,19 +183,22 @@ function convertDERToPEM(label, der) { assert(spkiExp.test(publicKey)); // The private key is DER-encoded. assert(Buffer.isBuffer(privateKeyDER)); - const privateKey = convertDERToPEM('ENCRYPTED PRIVATE KEY', privateKeyDER); assertApproximateSize(publicKey, 440); - assertApproximateSize(privateKey, 512); + assertApproximateSize(privateKeyDER, 336); // Since the private key is encrypted, signing shouldn't work anymore. assert.throws(() => { - testSignVerify(publicKey, privateKey); + testSignVerify(publicKey, { + key: privateKeyDER, + ...privateKeyEncoding + }); }, /bad decrypt|asn1 encoding routines/); // Signing should work with the correct password. testSignVerify(publicKey, { - key: privateKey, + key: privateKeyDER, + ...privateKeyEncoding, passphrase: 'secret' }); })); @@ -369,8 +354,52 @@ function convertDERToPEM(label, der) { } { - // Missing / invalid publicKeyEncoding. - for (const enc of [undefined, null, 0, 'a', true]) { + // If no publicKeyEncoding is specified, a key object should be returned. + generateKeyPair('rsa', { + modulusLength: 1024, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem' + } + }, common.mustCall((err, publicKey, privateKey) => { + assert.ifError(err); + + assert.strictEqual(typeof publicKey, 'object'); + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa'); + + // The private key should still be a string. + assert.strictEqual(typeof privateKey, 'string'); + + testEncryptDecrypt(publicKey, privateKey); + testSignVerify(publicKey, privateKey); + })); + + // If no privateKeyEncoding is specified, a key object should be returned. + generateKeyPair('rsa', { + modulusLength: 1024, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + } + }, common.mustCall((err, publicKey, privateKey) => { + assert.ifError(err); + + // The public key should still be a string. + assert.strictEqual(typeof publicKey, 'string'); + + assert.strictEqual(typeof privateKey, 'object'); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa'); + + testEncryptDecrypt(publicKey, privateKey); + testSignVerify(publicKey, privateKey); + })); +} + +{ + // Invalid publicKeyEncoding. + for (const enc of [0, 'a', true]) { common.expectsError(() => generateKeyPairSync('rsa', { modulusLength: 4096, publicKeyEncoding: enc, @@ -425,8 +454,8 @@ function convertDERToPEM(label, der) { }); } - // Missing / invalid privateKeyEncoding. - for (const enc of [undefined, null, 0, 'a', true]) { + // Invalid privateKeyEncoding. + for (const enc of [0, 'a', true]) { common.expectsError(() => generateKeyPairSync('rsa', { modulusLength: 4096, publicKeyEncoding: { diff --git a/test/parallel/test-crypto-rsa-dsa.js b/test/parallel/test-crypto-rsa-dsa.js index 744dc5657b089d..348fd15b74d495 100644 --- a/test/parallel/test-crypto-rsa-dsa.js +++ b/test/parallel/test-crypto-rsa-dsa.js @@ -100,7 +100,7 @@ const decryptError = assert.throws(() => { crypto.publicDecrypt({ key: rsaKeyPemEncrypted, - passphrase: [].concat.apply([], Buffer.from('password')) + passphrase: Buffer.from('wrong') }, encryptedBuffer); }, decryptError); } diff --git a/test/parallel/test-crypto-sign-verify.js b/test/parallel/test-crypto-sign-verify.js index eaf555ff57f9d8..0499b3091ca5a9 100644 --- a/test/parallel/test-crypto-sign-verify.js +++ b/test/parallel/test-crypto-sign-verify.js @@ -352,7 +352,7 @@ common.expectsError( code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError [ERR_INVALID_ARG_TYPE]', message: 'The "key" argument must be one of type string, Buffer, ' + - `TypedArray, or DataView. Received type ${type}` + `TypedArray, DataView, or KeyObject. Received type ${type}` }; assert.throws(() => sign.sign(input), errObj); diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js index 527b44f969a77f..e3588856209e28 100644 --- a/tools/doc/type-parser.js +++ b/tools/doc/type-parser.js @@ -50,6 +50,7 @@ const customTypesMap = { 'ECDH': 'crypto.html#crypto_class_ecdh', 'Hash': 'crypto.html#crypto_class_hash', 'Hmac': 'crypto.html#crypto_class_hmac', + 'KeyObject': 'crypto.html#crypto_class_keyobject', 'Sign': 'crypto.html#crypto_class_sign', 'Verify': 'crypto.html#crypto_class_verify', 'crypto.constants': 'crypto.html#crypto_crypto_constants_1',