From db33797d905e5028daf403a52c06755940cc7e20 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 01/16] 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. --- doc/api/crypto.md | 198 ++- doc/api/errors.md | 5 + lib/crypto.js | 8 + lib/internal/crypto/cipher.js | 43 +- lib/internal/crypto/keygen.js | 135 +- lib/internal/crypto/keys.js | 331 +++++ lib/internal/crypto/sig.js | 29 +- lib/internal/errors.js | 2 + node.gyp | 1 + src/env.h | 1 + src/node_crypto.cc | 1290 ++++++++++++----- src/node_crypto.h | 166 ++- .../test-crypto-cipheriv-decipheriv.js | 4 +- test/parallel/test-crypto-key-objects.js | 79 + 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 + 18 files changed, 1858 insertions(+), 556 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 3e1de25f750feb..f54193f521dd58 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -1101,6 +1101,76 @@ 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.getType() + +* Returns: {string} + +Depending on the type of this `KeyObject`, this function either returns +`'secret'` for symmetric keys, `'public'` for public (asymmetric) keys or +`'private'` for private (asymmetric) keys. + +### keyObject.getSymmetricSize() + +* Returns: {number} + +For symmetric keys, this function returns the size of the key in bytes. This +function is undefined for asymmetric keys. + +### keyObject.getAsymmetricKeyType() + +* Returns: {string} + +For asymmetric keys, this function returns the type of the embedded key (e.g., +`'rsa'` or `'dsa'`). This function 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`. + ## Class: Sign -* `privateKey` {string | Object} - - `key` {string} - - `passphrase` {string} +* `privateKey` {Object | string | Buffer | KeyObject} + - `key` {string | Buffer | KeyObject} A private key. + - `passphrase` {string | Buffer} An optional passphrase for the private key. - `padding` {integer} - `saltLength` {integer} * `outputEncoding` {string} The [encoding][] of the return value. @@ -1299,7 +1372,10 @@ 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} + - `key` {string | Buffer | KeyObject} A public key. + - `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 @@ -1310,7 +1386,7 @@ 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) +* `key`: {string} - The public key (required) * `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} @@ -1525,6 +1604,9 @@ to create the `Decipher` object. +* `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'`. + - `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, it is parsed as a PEM-encoded private key; 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, it is parsed as a PEM-encoded public key; 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 (symmetric) key. + ### crypto.createSign(algorithm[, options]) * `type`: {string} Must be `'rsa'`, `'dsa'` or `'ec'`. * `options`: {Object} @@ -1757,11 +1884,12 @@ 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`. * `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. @@ -1801,6 +1929,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 +1951,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 +2196,14 @@ 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} + - `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`, `crypto.constants.RSA_PKCS1_PADDING`, or @@ -2082,10 +2220,14 @@ treated as the key with no passphrase and will use `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`. @@ -2101,10 +2243,14 @@ treated as the key with no passphrase and will use `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} + - `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` or `crypto.constants.RSA_PKCS1_PADDING`. @@ -2123,10 +2269,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 diff --git a/doc/api/errors.md b/doc/api/errors.md index 6c255d07106546..8d08152c36451b 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 has an invalid type. + ### ERR_CRYPTO_INVALID_STATE diff --git a/lib/crypto.js b/lib/crypto.js index 4707ab2b35ca51..a0062a3d530f62 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 1e5dc91c8d5790..bf81cc3cc66819 100644 --- a/lib/internal/crypto/cipher.js +++ b/lib/internal/crypto/cipher.js @@ -6,12 +6,18 @@ const { } = internalBinding('constants').crypto; const { + ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, ERR_CRYPTO_INVALID_STATE, ERR_INVALID_ARG_TYPE, ERR_INVALID_OPT_VALUE } = require('internal/errors').codes; const { validateString } = require('internal/validators'); +const { + isKeyObject, + preparePrivateKey, + preparePublicOrPrivateKey +} = require('internal/crypto/keys'); const { getDefaultEncoding, kHandle, @@ -37,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); @@ -84,10 +96,10 @@ function createCipherBase(cipher, credential, options, decipher, iv) { LazyTransform.call(this, options); } -function invalidArrayBufferView(name, value) { +function invalidArrayBufferView(name, value, ...other) { return new ERR_INVALID_ARG_TYPE( name, - ['string', 'Buffer', 'TypedArray', 'DataView'], + ['string', 'Buffer', 'TypedArray', 'DataView', ...other], value ); } @@ -104,9 +116,14 @@ 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); + if (typeof key !== 'string' && !isArrayBufferView(key)) { + if (isKeyObject(key)) { + if (key.getType() !== 'secret') + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.getType(), 'secret'); + key = key[kHandle]; + } else { + throw invalidArrayBufferView('key', key, 'KeyObject'); + } } iv = toBuf(iv); diff --git a/lib/internal/crypto/keygen.js b/lib/internal/crypto/keygen.js index 61898989b08abe..3c09459c426857 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 } = 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 { @@ -182,8 +139,8 @@ function check(type, options, callback) { } impl = (wrap) => generateKeyPairRSA(modulusLength, publicExponent, - publicType, publicFormat, - privateType, privateFormat, + publicFormat, publicType, + privateFormat, privateType, cipher, passphrase, wrap); } break; @@ -201,8 +158,8 @@ function check(type, options, callback) { } impl = (wrap) => generateKeyPairDSA(modulusLength, divisorLength, - publicType, publicFormat, - privateType, privateFormat, + publicFormat, publicType, + privateFormat, privateType, cipher, passphrase, wrap); } break; @@ -220,8 +177,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..3cd36411acce4c --- /dev/null +++ b/lib/internal/crypto/keys.js @@ -0,0 +1,331 @@ +'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 + }); + } + + getType() { + return this[kKeyType]; + } + + getSize() { + return this[kHandle].getSize(); + } +} + +class SecretKeyObject extends KeyObject { + constructor(handle) { + super('secret', handle); + } + + getSymmetricKeySize() { + return this[kHandle].getSymmetricKeySize(); + } + + export() { + return this[kHandle].export(); + } +} + +const kAsymmetricKeyType = Symbol('kAsymmetricKeyType'); + +class AsymmetricKeyObject extends KeyObject { + getAsymmetricKeyType() { + 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.getAsymmetricKeyType()); + 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.getAsymmetricKeyType()); + 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.getType() !== expectedType) + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.getType(), 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 createSecretKey(key) { + if (!isArrayBufferView(key)) { + throw new ERR_INVALID_ARG_TYPE('key', + ['Buffer', 'TypedArray', 'DataView'], + key); + } + 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, + SecretKeyObject, + PublicKeyObject, + PrivateKeyObject, + isKeyObject +}; diff --git a/lib/internal/crypto/sig.js b/lib/internal/crypto/sig.js index fa2d4998b6c990..e0336c4e559d1e 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'); function Sign(algorithm, options) { @@ -71,17 +75,14 @@ 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); + let ret = this[kHandle].sign(data, format, type, passphrase, rsaPadding, + pssSaltLength); encoding = encoding || getDefaultEncoding(); if (encoding && encoding !== 'buffer') @@ -108,7 +109,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 @@ -116,12 +122,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 b8f8b4cfa2d725..73d9a108192e1c 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 cf334c4acec13d..c231a45f6c6f37 100644 --- a/node.gyp +++ b/node.gyp @@ -102,6 +102,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 ec0368e040a608..664eb4cbbe07b3 100644 --- a/src/env.h +++ b/src/env.h @@ -321,6 +321,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 16d958ea889599..90c3e0973ab50d 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -73,6 +73,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; @@ -2689,6 +2690,841 @@ static bool IsSupportedAuthenticatedMode(const EVP_CIPHER_CTX* ctx) { return IsSupportedAuthenticatedMode(cipher); } +template +static T* CHECKED_OPENSSL_malloc(size_t count) { + void* mem = OPENSSL_malloc(count); + CHECK_NOT_NULL(mem); + return static_cast(mem); +} + +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 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 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 || !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 == 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 MaybeLocal WritePublicKey(Environment* env, + EVP_PKEY* pkey, + const PublicKeyEncodingConfig& config) { + BIOPointer bio(BIO_new(BIO_s_mem())); + CHECK(bio); + + bool err; + + 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. + err = PEM_write_bio_RSAPublicKey(bio.get(), rsa.get()) != 1; + } else { + // Encode PKCS#1 as DER. + CHECK_EQ(config.format_, kKeyFormatDER); + err = i2d_RSAPublicKey_bio(bio.get(), rsa.get()) != 1; + } + } else { + CHECK_EQ(config.type_.ToChecked(), kKeyEncodingSPKI); + if (config.format_ == kKeyFormatPEM) { + // Encode SPKI as PEM. + err = PEM_write_bio_PUBKEY(bio.get(), pkey) != 1; + } else { + // Encode SPKI as DER. + CHECK_EQ(config.format_, kKeyFormatDER); + err = i2d_PUBKEY_bio(bio.get(), pkey) != 1; + } + } + + if (err) { + 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() : data_(nullptr), allocated_data_(nullptr), size_(0) {} + +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 = CHECKED_OPENSSL_malloc(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 = CHECKED_OPENSSL_malloc(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); + // Note that this is only safe as long as the key object cannot be accessed + // by other threads. + 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(); + } +} + +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_; +}; + +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 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 == 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 = true; + if (data.size() >= 3) { + // An RSAPrivateKey structure always starts with a single-byte + // integer whose value is either 0 or 1, whereas an RSAPublicKey + // starts with the modulus, so we can decide the type of the + // structure based on the first three bytes. + const char* p = data.get(); + if (p[0] == 2 && data.get()[1] == 1 && + (data.get()[2] == 0 || data.get()[2] == 1)) { + is_public = false; + } + } + 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 = CHECKED_OPENSSL_malloc(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); + const char* name; + switch (EVP_PKEY_id(this->asymmetric_key_.get())) { + case EVP_PKEY_RSA: + name = "rsa"; + break; + case EVP_PKEY_DSA: + name = "dsa"; + break; + case EVP_PKEY_EC: + name = "ec"; + break; + default: + printf("%d\n", EVP_PKEY_id(this->asymmetric_key_.get())); + CHECK(false); + } + return OneByteString(env()->isolate(), name); +} + +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); @@ -2866,9 +3702,17 @@ 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])); + + // A key can be passed as a string, buffer or KeyObject with type 'symmetric'. + // 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. + ByteSource key; + if (args[1]->IsString() || Buffer::HasInstance(args[1])) { + key = std::move(ByteSource::FromStringOrBuffer(env, args[1])); + } else { + key = ByteSource::FromSymmetricKeyObject(args[1]); + } + ssize_t iv_len; const unsigned char* iv_buf; if (args[2]->IsNull()) { @@ -2889,7 +3733,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); } @@ -3596,7 +4445,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) { @@ -3658,7 +4507,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]; @@ -3687,9 +4536,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_) @@ -3697,21 +4544,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) { @@ -3747,25 +4579,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); @@ -3781,72 +4609,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); @@ -3890,8 +4652,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, @@ -3900,15 +4661,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; @@ -3936,20 +4693,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); @@ -3960,36 +4717,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; @@ -4016,18 +4749,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; @@ -4035,9 +4767,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, @@ -5042,46 +5772,16 @@ 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::move(private_key_encoding)), pkey_(nullptr) {} inline void DoThreadPoolWork() override { @@ -5110,7 +5810,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; } @@ -5137,197 +5837,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]); + std::move(private_key_encoding.Release()))); + if (args[offset]->IsObject()) + return GenerateKeyPairJob::Run(std::move(job), args[offset]); env->PrintSyncTrace(); job->DoThreadPoolWork(); Local err, pubkey, privkey; @@ -5772,6 +6334,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); @@ -5803,12 +6366,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..00035ff73bfe95 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -341,6 +341,161 @@ class SSLWrap { friend class SecureContext; }; +// A helper class representing a read-only byte array. When deallocated, its +// contents are zeroed. +class ByteSource { + public: + ByteSource(); + 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_; + char* allocated_data_; + size_t size_; + + 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); +}; + +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 +684,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 +703,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 +736,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/test/parallel/test-crypto-cipheriv-decipheriv.js b/test/parallel/test-crypto-cipheriv-decipheriv.js index c0073abcfd4ee9..28e392efdb9864 100644 --- a/test/parallel/test-crypto-cipheriv-decipheriv.js +++ b/test/parallel/test-crypto-cipheriv-decipheriv.js @@ -102,7 +102,7 @@ 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' + 'TypedArray, DataView, or KeyObject. Received type object' }); common.expectsError( @@ -139,7 +139,7 @@ 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' + 'TypedArray, DataView, or KeyObject. Received type object' }); common.expectsError( diff --git a/test/parallel/test-crypto-key-objects.js b/test/parallel/test-crypto-key-objects.js new file mode 100644 index 00000000000000..14100445568158 --- /dev/null +++ b/test/parallel/test-crypto-key-objects.js @@ -0,0 +1,79 @@ +'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.getType(), 'secret'); + assert.strictEqual(key.getSymmetricKeySize(), 32); + + 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.getType(), 'public'); + assert.strictEqual(publicKey.getAsymmetricKeyType(), 'rsa'); + + const privateKey = createPrivateKey(privatePem); + assert.strictEqual(privateKey.getType(), 'private'); + assert.strictEqual(privateKey.getAsymmetricKeyType(), 'rsa'); + + const plaintext = Buffer.from('Hello world', 'utf8'); + const ciphertexts = [ + publicEncrypt(publicKey, plaintext), + publicEncrypt({ key: publicKey }, plaintext) + ]; + + for (const ciphertext of ciphertexts) { + const deciphered = privateDecrypt(privateKey, ciphertext); + assert(plaintext.equals(deciphered)); + } +} diff --git a/test/parallel/test-crypto-keygen.js b/test/parallel/test-crypto-keygen.js index 241e4aa73ac684..f605dd980545e8 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.getType(), 'public'); + assert.strictEqual(publicKey.getAsymmetricKeyType(), '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.getType(), 'private'); + assert.strictEqual(privateKey.getAsymmetricKeyType(), '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', From 3ef114b00e730e2d8a093051fafa6204e9d89eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Fri, 9 Nov 2018 17:35:25 +0100 Subject: [PATCH 02/16] fixup! crypto: add key object API --- lib/internal/crypto/sig.js | 6 ++-- src/node_crypto.cc | 58 ++++++++++++++++++++------------------ 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/lib/internal/crypto/sig.js b/lib/internal/crypto/sig.js index e0336c4e559d1e..32f7c37ec271f9 100644 --- a/lib/internal/crypto/sig.js +++ b/lib/internal/crypto/sig.js @@ -81,12 +81,12 @@ Sign.prototype.sign = function sign(options, encoding) { const rsaPadding = getPadding(options); const pssSaltLength = getSaltLength(options); - let ret = this[kHandle].sign(data, format, type, 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; }; diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 90c3e0973ab50d..bc638bc29c8178 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -2692,12 +2692,12 @@ static bool IsSupportedAuthenticatedMode(const EVP_CIPHER_CTX* ctx) { template static T* CHECKED_OPENSSL_malloc(size_t count) { - void* mem = OPENSSL_malloc(count); + void* mem = OPENSSL_malloc(MultiplyWithOverflowCheck(count, sizeof(T))); CHECK_NOT_NULL(mem); return static_cast(mem); } -enum ParsePublicKeyResult { +enum class ParsePublicKeyResult { kParsePublicOk, kParsePublicNotRecognized, kParsePublicFailed @@ -2717,7 +2717,7 @@ static ParsePublicKeyResult TryParsePublicKey( MarkPopErrorOnReturn mark_pop_error_on_return; if (PEM_bytes_read_bio(&der_data, &der_len, nullptr, name, bp.get(), nullptr, nullptr) != 1) - return kParsePublicNotRecognized; + return ParsePublicKeyResult::kParsePublicNotRecognized; } // OpenSSL might modify the pointer, so we need to make a copy before parsing. @@ -2725,7 +2725,8 @@ static ParsePublicKeyResult TryParsePublicKey( pkey->reset(parse(&p, der_len)); OPENSSL_clear_free(der_data, der_len); - return *pkey ? kParsePublicOk : kParsePublicFailed; + return *pkey ? ParsePublicKeyResult::kParsePublicOk : + ParsePublicKeyResult::kParsePublicFailed; } static ParsePublicKeyResult ParsePublicKeyPEM(EVPKeyPointer* pkey, @@ -2734,7 +2735,7 @@ static ParsePublicKeyResult ParsePublicKeyPEM(EVPKeyPointer* pkey, bool allow_certificate) { BIOPointer bp(BIO_new_mem_buf(const_cast(key_pem), key_pem_len)); if (!bp) - return kParsePublicFailed; + return ParsePublicKeyResult::kParsePublicFailed; ParsePublicKeyResult ret; @@ -2743,7 +2744,7 @@ static ParsePublicKeyResult ParsePublicKeyPEM(EVPKeyPointer* pkey, [](const unsigned char** p, long l) { // NOLINT(runtime/int) return d2i_PUBKEY(nullptr, p, l); }); - if (ret != kParsePublicNotRecognized) + if (ret != ParsePublicKeyResult::kParsePublicNotRecognized) return ret; // Maybe it is PKCS#1. @@ -2752,7 +2753,8 @@ static ParsePublicKeyResult ParsePublicKeyPEM(EVPKeyPointer* pkey, [](const unsigned char** p, long l) { // NOLINT(runtime/int) return d2i_PublicKey(EVP_PKEY_RSA, nullptr, p, l); }); - if (ret != kParsePublicNotRecognized || !allow_certificate) + if (ret != ParsePublicKeyResult::kParsePublicNotRecognized || + !allow_certificate) return ret; // X.509 fallback. @@ -2772,7 +2774,7 @@ static bool ParsePublicKey(EVPKeyPointer* pkey, if (config.format_ == kKeyFormatPEM) { ParsePublicKeyResult r = ParsePublicKeyPEM(pkey, key, key_len, allow_certificate); - return r == kParsePublicOk; + return r == ParsePublicKeyResult::kParsePublicOk; } else { CHECK_EQ(config.format_, kKeyFormatDER); const unsigned char* p = reinterpret_cast(key); @@ -2804,39 +2806,41 @@ static inline Local BIOToStringOrBuffer(Environment* env, } } -static MaybeLocal WritePublicKey(Environment* env, - EVP_PKEY* pkey, - const PublicKeyEncodingConfig& config) { - BIOPointer bio(BIO_new(BIO_s_mem())); - CHECK(bio); - - bool err; - +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. - err = PEM_write_bio_RSAPublicKey(bio.get(), rsa.get()) != 1; + return PEM_write_bio_RSAPublicKey(bio.get(), rsa.get()) == 1; } else { // Encode PKCS#1 as DER. CHECK_EQ(config.format_, kKeyFormatDER); - err = i2d_RSAPublicKey_bio(bio.get(), rsa.get()) != 1; + return i2d_RSAPublicKey_bio(bio.get(), rsa.get()) == 1; } } else { CHECK_EQ(config.type_.ToChecked(), kKeyEncodingSPKI); if (config.format_ == kKeyFormatPEM) { // Encode SPKI as PEM. - err = PEM_write_bio_PUBKEY(bio.get(), pkey) != 1; + return PEM_write_bio_PUBKEY(bio.get(), pkey) == 1; } else { // Encode SPKI as DER. CHECK_EQ(config.format_, kKeyFormatDER); - err = i2d_PUBKEY_bio(bio.get(), pkey) != 1; + return i2d_PUBKEY_bio(bio.get(), pkey) == 1; } } +} - if (err) { +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(); } @@ -3157,7 +3161,7 @@ static ManagedEVPPKey GetPublicOrPrivateKeyFromJS( ParsePublicKeyResult ret = ParsePublicKeyPEM(&pkey, data.get(), data.size(), allow_certificate); - if (ret == kParsePublicNotRecognized) { + if (ret == ParsePublicKeyResult::kParsePublicNotRecognized) { pkey = ParsePrivateKey(config, data.get(), data.size()); } } else { @@ -3706,12 +3710,10 @@ void CipherBase::InitIv(const FunctionCallbackInfo& args) { // A key can be passed as a string, buffer or KeyObject with type 'symmetric'. // 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. - ByteSource key; - if (args[1]->IsString() || Buffer::HasInstance(args[1])) { - key = std::move(ByteSource::FromStringOrBuffer(env, args[1])); - } else { - key = ByteSource::FromSymmetricKeyObject(args[1]); - } + const ByteSource key = + args[1]->IsString() || Buffer::HasInstance(args[1]) ? + std::move(ByteSource::FromStringOrBuffer(env, args[1])) : + ByteSource::FromSymmetricKeyObject(args[1]); ssize_t iv_len; const unsigned char* iv_buf; From b7888585584f92a627e79984c63fe234c02288b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Fri, 9 Nov 2018 18:03:19 +0100 Subject: [PATCH 03/16] fixup! crypto: add key object API --- doc/api/crypto.md | 8 ++++---- src/env.h | 5 ++++- src/node_crypto.cc | 12 ++++-------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index f54193f521dd58..866b02d2a9d1a0 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -1806,8 +1806,8 @@ added: REPLACEME * Returns: {KeyObject} Creates and returns a new key object containing a private key. If `key` is a -string, it is parsed as a PEM-encoded private key; otherwise, `key` must be an -object with the properties described above. +string or `Buffer`, it is parsed as a PEM-encoded private key; otherwise, `key` +must be an object with the properties described above. ### crypto.createPublicKey(key) * `algorithm` {string} -* `key` {string | Buffer | TypedArray | DataView} +* `key` {string | Buffer | TypedArray | DataView | KeyObject} * `options` {Object} [`stream.transform` options][] * Returns: {Hmac} @@ -1831,7 +1835,8 @@ added: REPLACEME * `key` {Buffer} * Returns: {KeyObject} -Creates and returns a new key object containing a secret (symmetric) key. +Creates and returns a new key object containing a secret key for symmetric +encryption or `Hmac`. ### crypto.createSign(algorithm[, options]) @@ -1931,7 +1936,7 @@ a `Promise` for an `Object` with `publicKey` and `privateKey` properties. added: v10.12.0 changes: - version: REPLACEME - pr-url: ??? + pr-url: https://github.com/nodejs/node/pull/24234 description: The `generateKeyPair` and `generateKeyPairSync` functions now produce key objects if no encoding was specified. --> @@ -2198,7 +2203,7 @@ An array of supported digest functions can be retrieved using added: v0.11.14 changes: - version: REPLACEME - pr-url: https://github.com/nodejs/node/pull/??? + pr-url: https://github.com/nodejs/node/pull/24234 description: This function now supports key objects. --> * `privateKey` {Object | string | Buffer | KeyObject} @@ -2222,7 +2227,7 @@ treated as the key with no passphrase and will use `RSA_PKCS1_OAEP_PADDING`. added: v1.1.0 changes: - version: REPLACEME - pr-url: https://github.com/nodejs/node/pull/??? + pr-url: https://github.com/nodejs/node/pull/24234 description: This function now supports key objects. --> * `privateKey` {Object | string | Buffer | KeyObject} @@ -2245,7 +2250,7 @@ treated as the key with no passphrase and will use `RSA_PKCS1_PADDING`. added: v1.1.0 changes: - version: REPLACEME - pr-url: https://github.com/nodejs/node/pull/??? + pr-url: https://github.com/nodejs/node/pull/24234 description: This function now supports key objects. --> * `key` {Object | string | Buffer | KeyObject} @@ -2271,7 +2276,7 @@ be passed instead of a public key. added: v0.11.14 changes: - version: REPLACEME - pr-url: https://github.com/nodejs/node/pull/??? + pr-url: https://github.com/nodejs/node/pull/24234 description: This function now supports key objects. --> * `key` {Object | string | Buffer | KeyObject} diff --git a/lib/internal/crypto/cipher.js b/lib/internal/crypto/cipher.js index bf81cc3cc66819..0e8e5c4cf8baf9 100644 --- a/lib/internal/crypto/cipher.js +++ b/lib/internal/crypto/cipher.js @@ -6,7 +6,6 @@ const { } = internalBinding('constants').crypto; const { - ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, ERR_CRYPTO_INVALID_STATE, ERR_INVALID_ARG_TYPE, ERR_INVALID_OPT_VALUE @@ -14,9 +13,9 @@ const { const { validateString } = require('internal/validators'); const { - isKeyObject, preparePrivateKey, - preparePublicOrPrivateKey + preparePublicOrPrivateKey, + prepareSecretKey } = require('internal/crypto/keys'); const { getDefaultEncoding, @@ -96,10 +95,10 @@ function createCipherBase(cipher, credential, options, decipher, iv) { LazyTransform.call(this, options); } -function invalidArrayBufferView(name, value, ...other) { +function invalidArrayBufferView(name, value) { return new ERR_INVALID_ARG_TYPE( name, - ['string', 'Buffer', 'TypedArray', 'DataView', ...other], + ['string', 'Buffer', 'TypedArray', 'DataView'], value ); } @@ -116,16 +115,7 @@ function createCipher(cipher, password, options, decipher) { function createCipherWithIV(cipher, key, options, decipher, iv) { validateString(cipher, 'cipher'); - if (typeof key !== 'string' && !isArrayBufferView(key)) { - if (isKeyObject(key)) { - if (key.getType() !== 'secret') - throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.getType(), 'secret'); - key = key[kHandle]; - } else { - throw invalidArrayBufferView('key', key, 'KeyObject'); - } - } - + 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 f289d11cf8b9c0..713ded3d1815a0 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/keys.js b/lib/internal/crypto/keys.js index 3cd36411acce4c..ff27966a82719b 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -281,12 +281,25 @@ function preparePublicOrPrivateKey(key, allowKeyObject) { return prepareAsymmetricKey(key, undefined, allowKeyObject); } -function createSecretKey(key) { - if (!isArrayBufferView(key)) { - throw new ERR_INVALID_ARG_TYPE('key', - ['Buffer', 'TypedArray', 'DataView'], - key); +function prepareSecretKey(key, bufferOnly = false) { + if (!isArrayBufferView(key) && (bufferOnly || typeof key !== 'string')) { + if (isKeyObject(key) && !bufferOnly) { + if (key.getType() !== 'secret') + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.getType(), '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); @@ -324,6 +337,7 @@ module.exports = { preparePublicKey, preparePrivateKey, preparePublicOrPrivateKey, + prepareSecretKey, SecretKeyObject, PublicKeyObject, PrivateKeyObject, diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 690e204dfd7631..7c56d67acbb7d1 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -3694,6 +3694,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()); @@ -3702,14 +3711,7 @@ void CipherBase::InitIv(const FunctionCallbackInfo& args) { CHECK_GE(args.Length(), 4); const node::Utf8Value cipher_type(env->isolate(), args[0]); - - // A key can be passed as a string, buffer or KeyObject with type 'symmetric'. - // 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. - const ByteSource key = - args[1]->IsString() || Buffer::HasInstance(args[1]) ? - std::move(ByteSource::FromStringOrBuffer(env, args[1])) : - ByteSource::FromSymmetricKeyObject(args[1]); + const ByteSource key = GetSecretKeyBytes(env, args[1]); ssize_t iv_len; const unsigned char* iv_buf; @@ -4192,9 +4194,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()); } diff --git a/test/parallel/test-crypto-cipheriv-decipheriv.js b/test/parallel/test-crypto-cipheriv-decipheriv.js index 28e392efdb9864..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, DataView, or KeyObject. 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, DataView, or KeyObject. 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 223b5c3c077251..2be1b755762f35 100644 --- a/test/parallel/test-crypto-hmac.js +++ b/test/parallel/test-crypto-hmac.js @@ -26,20 +26,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) @@ -86,24 +103,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 = [ { @@ -332,6 +336,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'), @@ -387,30 +395,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'), From 2338f0c3a9a15c0498d90b5147390eaa45d84afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Sun, 11 Nov 2018 23:31:51 +0100 Subject: [PATCH 05/16] fixup! crypto: add key object API --- src/node_crypto.cc | 25 ------------------------- src/node_crypto.h | 8 ++++---- src/util.h | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 7c56d67acbb7d1..afded53e949967 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -2889,8 +2889,6 @@ static EVPKeyPointer ParsePrivateKey(const PrivateKeyEncodingConfig& config, return pkey; } -ByteSource::ByteSource() : data_(nullptr), allocated_data_(nullptr), size_(0) {} - ByteSource::ByteSource(ByteSource&& other) : data_(other.data_), allocated_data_(other.allocated_data_), @@ -3049,29 +3047,6 @@ static ManagedEVPPKey GetPublicKeyFromJS( } } -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_; -}; - static NonCopyableMaybe GetPrivateKeyEncodingFromJS( const FunctionCallbackInfo& args, unsigned int* offset, diff --git a/src/node_crypto.h b/src/node_crypto.h index 00035ff73bfe95..cf612e05178c72 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -345,7 +345,7 @@ class SSLWrap { // contents are zeroed. class ByteSource { public: - ByteSource(); + ByteSource() = default; ByteSource(ByteSource&& other); ~ByteSource(); @@ -370,9 +370,9 @@ class ByteSource { static ByteSource FromSymmetricKeyObject(v8::Local handle); private: - const char* data_; - char* allocated_data_; - size_t size_; + const char* data_ = nullptr; + char* allocated_data_ = nullptr; + size_t size_ = 0; ByteSource(const char* data, char* allocated_data, size_t size); diff --git a/src/util.h b/src/util.h index 9bf8bb05823c51..070be1c31bec9e 100644 --- a/src/util.h +++ b/src/util.h @@ -450,6 +450,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 { }; From fc76a28dd027ed52e6ab7ac2ddc77b866125ff5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Mon, 12 Nov 2018 18:35:25 +0100 Subject: [PATCH 06/16] fixup! crypto: add key object API --- src/node_crypto.cc | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/node_crypto.cc b/src/node_crypto.cc index afded53e949967..142c5964f3ef54 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -2691,7 +2691,7 @@ static bool IsSupportedAuthenticatedMode(const EVP_CIPHER_CTX* ctx) { } template -static T* CHECKED_OPENSSL_malloc(size_t count) { +static T* MallocOpenSSL(size_t count) { void* mem = OPENSSL_malloc(MultiplyWithOverflowCheck(count, sizeof(T))); CHECK_NOT_NULL(mem); return static_cast(mem); @@ -2930,7 +2930,7 @@ ByteSource ByteSource::FromString(Environment* env, Local str, CHECK(str->IsString()); size_t size = str->Utf8Length(env->isolate()); size_t alloc_size = ntc ? size + 1 : size; - char* data = CHECKED_OPENSSL_malloc(alloc_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); @@ -2940,7 +2940,7 @@ ByteSource ByteSource::FromString(Environment* env, Local str, ByteSource ByteSource::FromBuffer(Local buffer, bool ntc) { size_t size = Buffer::Length(buffer); if (ntc) { - char* data = CHECKED_OPENSSL_malloc(size + 1); + char* data = MallocOpenSSL(size + 1); memcpy(data, Buffer::Data(buffer), size); data[size] = 0; return Allocated(data, size); @@ -3407,7 +3407,7 @@ void KeyObject::Init(const FunctionCallbackInfo& args) { void KeyObject::InitSecret(const char* key, size_t key_len) { CHECK_EQ(this->key_type_, kKeyTypeSecret); - char* mem = CHECKED_OPENSSL_malloc(key_len); + char* mem = MallocOpenSSL(key_len); memcpy(mem, key, key_len); this->symmetric_key_ = std::unique_ptr>(mem, [key_len](char* p) { @@ -3430,7 +3430,6 @@ void KeyObject::InitPrivate(const ManagedEVPPKey& pkey) { Local KeyObject::GetAsymmetricKeyType() const { CHECK_NE(this->key_type_, kKeyTypeSecret); - const char* name; switch (EVP_PKEY_id(this->asymmetric_key_.get())) { case EVP_PKEY_RSA: return env()->crypto_rsa_string(); From 71fb9fdfbe299094ca12a30830eccd26d54a6f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Mon, 12 Nov 2018 19:14:26 +0100 Subject: [PATCH 07/16] fixup! crypto: add key object API --- src/node_crypto.cc | 5 +++-- src/node_crypto.h | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 142c5964f3ef54..4e50d4b321e74a 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -5754,7 +5754,8 @@ class GenerateKeyPairJob : public CryptoJob { : CryptoJob(env), config_(std::move(config)), public_key_encoding_(public_key_encoding), - private_key_encoding_(std::move(private_key_encoding)), + private_key_encoding_(std::forward( + private_key_encoding)), pkey_(nullptr) {} inline void DoThreadPoolWork() override { @@ -5860,7 +5861,7 @@ void GenerateKeyPair(const FunctionCallbackInfo& args, std::unique_ptr job( new GenerateKeyPairJob(env, std::move(config), public_key_encoding, - std::move(private_key_encoding.Release()))); + private_key_encoding.Release())); if (args[offset]->IsObject()) return GenerateKeyPairJob::Run(std::move(job), args[offset]); env->PrintSyncTrace(); diff --git a/src/node_crypto.h b/src/node_crypto.h index cf612e05178c72..ef8f2bce2e99f4 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -378,6 +378,8 @@ class ByteSource { static ByteSource Allocated(char* data, size_t size); static ByteSource Foreign(const char* data, size_t size); + + DISALLOW_COPY_AND_ASSIGN(ByteSource); }; enum PKEncodingType { From d3a83d36c5d4d15a590d515ff0908950a13ef6d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Tue, 13 Nov 2018 18:30:21 +0100 Subject: [PATCH 08/16] fixup! crypto: add key object API --- src/node_crypto.cc | 48 ++++++++++++++++++------ test/parallel/test-crypto-key-objects.js | 31 +++++++++++++-- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 4e50d4b321e74a..695068312e4339 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -3117,6 +3117,40 @@ static ManagedEVPPKey GetPrivateKeyFromJS( } } +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, @@ -3146,18 +3180,8 @@ static ManagedEVPPKey GetPublicOrPrivateKeyFromJS( bool is_public; switch (config.type_.ToChecked()) { case kKeyEncodingPKCS1: - is_public = true; - if (data.size() >= 3) { - // An RSAPrivateKey structure always starts with a single-byte - // integer whose value is either 0 or 1, whereas an RSAPublicKey - // starts with the modulus, so we can decide the type of the - // structure based on the first three bytes. - const char* p = data.get(); - if (p[0] == 2 && data.get()[1] == 1 && - (data.get()[2] == 0 || data.get()[2] == 1)) { - is_public = false; - } - } + is_public = !IsRSAPrivateKey( + reinterpret_cast(data.get()), data.size()); break; case kKeyEncodingSPKI: is_public = true; diff --git a/test/parallel/test-crypto-key-objects.js b/test/parallel/test-crypto-key-objects.js index 14100445568158..61ca9021583f29 100644 --- a/test/parallel/test-crypto-key-objects.js +++ b/test/parallel/test-crypto-key-objects.js @@ -66,14 +66,39 @@ const privatePem = fixtures.readSync('test_rsa_privkey.pem', 'ascii'); assert.strictEqual(privateKey.getType(), 'private'); assert.strictEqual(privateKey.getAsymmetricKeyType(), 'rsa'); + 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) + 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) { - const deciphered = privateDecrypt(privateKey, ciphertext); - assert(plaintext.equals(deciphered)); + for (const key of decryptionKeys) { + const deciphered = privateDecrypt(key, ciphertext); + assert(plaintext.equals(deciphered)); + } } } From fe13dbc3c7889deca6ab1d3e8087d3ac8f598be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Tue, 20 Nov 2018 21:34:02 +0100 Subject: [PATCH 09/16] Switch to getters --- doc/api/crypto.md | 24 ++++++++++++------------ lib/internal/crypto/keys.js | 22 +++++++++------------- test/parallel/test-crypto-key-objects.js | 15 +++++++++------ test/parallel/test-crypto-keygen.js | 8 ++++---- 4 files changed, 34 insertions(+), 35 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index f116212f2f2717..6c29786c8fe6c9 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -1116,33 +1116,33 @@ 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.getType() +### keyObject.type -* Returns: {string} +* {string} -Depending on the type of this `KeyObject`, this function either returns -`'secret'` for symmetric keys, `'public'` for public (asymmetric) keys or -`'private'` for private (asymmetric) keys. +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. -### keyObject.getSymmetricSize() +### keyObject.symmetricSize -* Returns: {number} +* {number} -For symmetric keys, this function returns the size of the key in bytes. This -function is undefined for asymmetric keys. +For secret keys, this property represents the size of the key in bytes. This +property is undefined for asymmetric keys. -### keyObject.getAsymmetricKeyType() +### keyObject.asymmetricKeyType * Returns: {string} -For asymmetric keys, this function returns the type of the embedded key (e.g., -`'rsa'` or `'dsa'`). This function is undefined for symmetric keys. +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]) -* {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. - -### keyObject.symmetricSize - -* {number} - -For secret keys, this property represents the size of the key in bytes. This -property is `undefined` for asymmetric keys. - ### keyObject.asymmetricKeyType -* Returns: {string} +* {string} For asymmetric keys, this property represents the type of the embedded key (`'rsa'`, `'dsa'` or `'ec'`). This property is `undefined` for symmetric keys. @@ -1173,6 +1154,25 @@ For private keys, the following encoding options can be used: * `passphrase`: {string | Buffer} The passphrase to use for encryption, see `cipher`. +### 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 * `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'`. + - `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 +Creates and returns a new key object containing a private key. If `key` is a string or `Buffer`, it is parsed as a PEM-encoded private key; otherwise, `key` must be an object with the properties described above. @@ -1821,7 +1821,7 @@ added: REPLACEME --> * `key` {Object | string | Buffer} - `key`: {string | Buffer} - - `format`: {string} Must be `'pem'` or `'der'`. Default: `'pem'`. + - `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} From cf81edebc6641b94fbef5c794ad6ba6bad7341de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Wed, 12 Dec 2018 20:15:06 +0100 Subject: [PATCH 13/16] Add missing references --- doc/api/crypto.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 97e23bc2960a87..b8389775b58f11 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -1108,10 +1108,10 @@ added: REPLACEME 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. +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. @@ -3085,6 +3085,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 From d9da635249194d7a360ee20af8c0a26d0a6eead3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Wed, 19 Dec 2018 23:36:09 +0100 Subject: [PATCH 14/16] fixup! crypto: add key object API --- doc/api/crypto.md | 90 +++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 46 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index b8389775b58f11..a645026eecde0d 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -1154,6 +1154,9 @@ For private keys, the following encoding options can be used: * `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 * `privateKey` {Object | string | Buffer | KeyObject} - - `key` {string | Buffer | KeyObject} A private key. - - `passphrase` {string | Buffer} An optional passphrase for the private key. - `padding` {integer} - `saltLength` {integer} * `outputEncoding` {string} The [encoding][] of the return value. @@ -1259,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` @@ -1375,7 +1374,6 @@ changes: description: Support for RSASSA-PSS and additional options was added. --> * `object` {Object | string | Buffer | KeyObject} - - `key` {string | Buffer | KeyObject} A public key. - `padding` {integer} - `saltLength` {integer} * `signature` {string | Buffer | TypedArray | DataView} @@ -1384,11 +1382,11 @@ changes: 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 | KeyObject} - The 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` @@ -1555,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 @@ -1647,7 +1646,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 @@ -1777,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 @@ -1812,7 +1813,7 @@ added: REPLACEME * Returns: {KeyObject} Creates and returns a new key object containing a private key. If `key` is a -string or `Buffer`, it is parsed as a PEM-encoded private key; otherwise, `key` +string or `Buffer`, `format` is assumed to be `'pem'`; otherwise, `key` must be an object with the properties described above. ### crypto.createPublicKey(key) @@ -1827,7 +1828,7 @@ added: REPLACEME * Returns: {KeyObject} Creates and returns a new key object containing a public key. If `key` is a -string or `Buffer`, it is parsed as a PEM-encoded public key; otherwise, `key` +string or `Buffer`, `format` is assumed to be `'pem'`; otherwise, `key` must be an object with the properties described above. ### crypto.createSecretKey(key) @@ -1881,18 +1882,8 @@ changes: - `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 | Buffer} 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 | KeyObject} @@ -1901,8 +1892,12 @@ changes: 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'); @@ -1924,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. @@ -2209,8 +2200,6 @@ changes: description: This function now supports key objects. --> * `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`, `crypto.constants.RSA_PKCS1_PADDING`, or @@ -2221,8 +2210,10 @@ changes: 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) * `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` or @@ -2267,8 +2259,10 @@ changes: 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. @@ -2295,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. @@ -3074,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 @@ -3109,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 From 15bb93afe6b11882fe02cf64e4681cc884e05354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Wed, 19 Dec 2018 23:43:24 +0100 Subject: [PATCH 15/16] fixup! crypto: add key object API --- doc/api/errors.md | 2 +- lib/internal/crypto/keys.js | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/doc/api/errors.md b/doc/api/errors.md index 8d08152c36451b..1e42357cc15c0c 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -766,7 +766,7 @@ An invalid [crypto digest algorithm][] was specified. ### ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE -The given crypto key object has an invalid type. +The given crypto key object's type is invalid for the attempted operation. ### ERR_CRYPTO_INVALID_STATE diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index 757b77b1f888f7..ad828350806f3a 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -199,20 +199,16 @@ function parseKeyEncoding(enc, keyType, isPublic, objName) { 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. - */ +// 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. - */ +// 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); } From 3122c386df79a13d79f3d024c3812cf5fad4e8d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Thu, 20 Dec 2018 00:11:57 +0100 Subject: [PATCH 16/16] crypto: always accept certificates as public keys --- doc/api/crypto.md | 2 ++ src/node_crypto.cc | 31 ++++++++++++------------------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index a645026eecde0d..1f28f3a04b571a 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -1831,6 +1831,8 @@ 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. +If the format is `'pem'`, the `'key'` may also be an X.509 certificate. + ### crypto.createSecretKey(key)