From 5f6dd928783e505b201314f92521470bf6b74ba5 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 18 Jan 2023 15:47:36 -0500 Subject: [PATCH 1/7] feat: add encrypt expression helper --- bindings/node/README.md | 118 +++++++++++++++- bindings/node/index.d.ts | 41 +++++- bindings/node/lib/clientEncryption.js | 143 ++++++++++++++------ bindings/node/lib/common.js | 16 ++- bindings/node/lib/stateMachine.js | 6 + bindings/node/src/mongocrypt.cc | 98 ++++++++------ bindings/node/test/clientEncryption.test.js | 141 ++++++++++++++++++- bindings/node/test/common.test.js | 94 +++++++++++++ 8 files changed, 566 insertions(+), 91 deletions(-) create mode 100644 bindings/node/test/common.test.js diff --git a/bindings/node/README.md b/bindings/node/README.md index de235027d..053804057 100644 --- a/bindings/node/README.md +++ b/bindings/node/README.md @@ -56,9 +56,25 @@ npm test +## Functions + +
+
loadCredentials(kmsProviders)Promise
+

Load cloud provider credentials for the user provided kms providers. +Credentials will only attempt to get loaded if they do not exist +and no existing credentials will get overwritten.

+
+
+ ## Typedefs
+
BSONValue : *
+

any serializable BSON value

+
+
Long : BSON.Long
+

A 64 bit integer, represented by the js-bson Long type.

+
KMSProviders : object

Configuration options that are used by specific KMS providers during key generation, encryption, and decryption.

@@ -100,6 +116,13 @@ query for the data key itself against the key vault namespace.

ClientEncryptionEncryptCallback : function
+
RangeOptions : object
+

min, max, sparsity, and range must match the values set in the encryptedFields of the destination collection. +For double and decimal128, min/max/precision must all be set, or all be unset.

+
+
EncryptOptions : object
+

Options to provide when encrypting data.

+
@@ -265,10 +288,14 @@ The public interface for explicit in-use encryption * [.encrypt(value, options, [callback])](#ClientEncryption+encrypt) + * [.encryptExpression(expression, options)](#ClientEncryption+encryptExpression) + * [.decrypt(value, callback)](#ClientEncryption+decrypt) * [.askForKMSCredentials()](#ClientEncryption+askForKMSCredentials) + * [._encrypt(value, expressionMode, options)](#ClientEncryption+_encrypt) + * _inner_ * [~decryptCallback](#ClientEncryption..decryptCallback) @@ -534,10 +561,7 @@ if (!oldKey) { | Param | Type | Description | | --- | --- | --- | | value | \* | The value that you wish to serialize. Must be of a type that can be serialized into BSON | -| options | object | | -| [options.keyId] | [ClientEncryptionDataKeyId](#ClientEncryptionDataKeyId) | The id of the Binary dataKey to use for encryption | -| [options.keyAltName] | string | A unique string name corresponding to an already existing dataKey. | -| [options.algorithm] | | The algorithm to use for encryption. Must be either `'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'`, `'AEAD_AES_256_CBC_HMAC_SHA_512-Random'`, `'Indexed'` or `'Unindexed'` | +| options | [EncryptOptions](#EncryptOptions) | | | [callback] | [ClientEncryptionEncryptCallback](#ClientEncryptionEncryptCallback) | Optional callback to invoke when value is encrypted | Explicitly encrypt a provided value. Note that either `options.keyId` or `options.keyAltName` must @@ -572,6 +596,21 @@ async function encryptMyData(value) { return clientEncryption.encrypt(value, { keyAltName: 'mySpecialKey', algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' }); } ``` + + +### *clientEncryption*.encryptExpression(expression, options) +**Experimental**: The Range algorithm is experimental only. It is not intended for public use. It is subject to breaking changes.The aggregation or match expression you wish to encrypt. The value must be in the form + +| Param | Type | Description | +| --- | --- | --- | +| expression | object | a BSON document of one of the following forms: 1. A Match Expression of this form: `{$and: [{: {$gt: }}, {: {$lt: }}]}` 2. An Aggregate Expression of this form: `{$and: [{$gt: [, ]}, {$lt: [, ]}]}` `$gt` may also be `$gte`. `$lt` may also be `$lte`. | +| options | [EncryptOptions](#EncryptOptions) | | + +Encrypts a Match Expression or Aggregate Expression to query a range index. + +Only supported when queryType is "rangePreview" and algorithm is "RangePreview". + +**Returns**: Promise.<object> - Returns a Promise that either resolves with the encrypted value or rejects with an error. ### *clientEncryption*.decrypt(value, callback) @@ -607,6 +646,23 @@ This returns anything that looks like the kmsProviders original input option. It can be empty, and any provider specified here will override the original ones. + + +### *clientEncryption*._encrypt(value, expressionMode, options) +**Internal**: + +| Param | Type | Description | +| --- | --- | --- | +| value | \* | The value that you wish to encrypt. Must be of a type that can be serialized into BSON | +| expressionMode | boolean | a boolean that indicates whether or not to encrypt the value as an expression | +| options | [EncryptOptions](#EncryptOptions) | | + +A helper that perform explicit encryption of values and expressions. +Explicitly encrypt a provided value. Note that either `options.keyId` or `options.keyAltName` must +be specified. Specifying both `options.keyId` and `options.keyAltName` is considered an error. + +**Returns**: the raw result of the call to stateMachine.execute(). When expressionMode is set to true, the return + value will be a bson document. When false, the value will be a BSON Binary. ### *ClientEncryption*~decryptCallback @@ -621,6 +677,29 @@ the original ones. ## MongoCryptError An error indicating that something went wrong specifically with MongoDB Client Encryption + + +## loadCredentials(kmsProviders) + +| Param | Type | Description | +| --- | --- | --- | +| kmsProviders | Object | The user provided kms providers. | + +Load cloud provider credentials for the user provided kms providers. +Credentials will only attempt to get loaded if they do not exist +and no existing credentials will get overwritten. + +**Returns**: Promise - The new kms providers. + + +## BSONValue +any serializable BSON value + + + +## Long +A 64 bit integer, represented by the js-bson Long type. + ## KMSProviders @@ -771,3 +850,34 @@ Configuration options for making an Azure encryption key | [err] | Error | If present, indicates an error that occurred in the process of encryption | | [result] | Buffer | If present, is the encrypted result | + + +## RangeOptions +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| min | [BSONValue](#BSONValue) | is required if precision is set. | +| max | [BSONValue](#BSONValue) | is required if precision is set. | +| sparsity | BSON.Long | | +| precision | number \| undefined | (may only be set for double or decimal128). | + +min, max, sparsity, and range must match the values set in the encryptedFields of the destination collection. +For double and decimal128, min/max/precision must all be set, or all be unset. + + + +## EncryptOptions +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| [keyId] | [ClientEncryptionDataKeyId](#ClientEncryptionDataKeyId) | The id of the Binary dataKey to use for encryption. | +| [keyAltName] | string | A unique string name corresponding to an already existing dataKey. | +| [algorithm] | string | The algorithm to use for encryption. Must be either `'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'`, `'AEAD_AES_256_CBC_HMAC_SHA_512-Random'`, `'Indexed'` or `'Unindexed'` | +| [contentionFactor] | bigint \| number | (experimental) - the contention factor. | +| queryType | 'equality' \| 'rangePreview' | (experimental) - the query type supported. | +| [rangeOptions] | [RangeOptions](#RangeOptions) | (experimental) The index options for a Queryable Encryption field supporting "rangePreview" queries. | + +Options to provide when encrypting data. + diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index 1e2e00675..1d4ac1f5c 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -1,4 +1,4 @@ -import type { Document, Binary } from 'bson'; +import type { Document, Binary, Long } from 'bson'; import type { MongoClient, BulkWriteResult, ClientSession, DeleteResult, FindCursor } from 'mongodb'; export type ClientEncryptionDataKeyProvider = 'aws' | 'azure' | 'gcp' | 'local' | 'kmip'; @@ -330,6 +330,18 @@ export interface ClientEncryptionRewrapManyDataKeyResult { bulkWriteResult?: BulkWriteResult; } +/** + * RangeOpts specifies index options for a Queryable Encryption field supporting "rangePreview" queries. + * min, max, sparsity, and range must match the values set in the encryptedFields of the destination collection. + * For double and decimal128, min/max/precision must all be set, or all be unset. +*/ +interface RangeOptions { + min?: any; + max?: any; + sparsity: Long; + precision?: number; +} + /** * Options to provide when encrypting data. */ @@ -337,7 +349,7 @@ export interface ClientEncryptionEncryptOptions { /** * The algorithm to use for encryption. */ - algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' | 'AEAD_AES_256_CBC_HMAC_SHA_512-Random' | 'Indexed' | 'Unindexed'; + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' | 'AEAD_AES_256_CBC_HMAC_SHA_512-Random' | 'Indexed' | 'Unindexed' | 'RangePreview'; /** * The id of the Binary dataKey to use for encryption @@ -353,7 +365,10 @@ export interface ClientEncryptionEncryptOptions { contentionFactor?: bigint | number; /** @experimental Public Technical Preview: The query type supported */ - queryType?: 'equality'; + queryType?: 'equality' | 'rangePreview'; + + /** @experimental Public Technical Preview: The index options for a Queryable Encryption field supporting "rangePreview" queries.*/ + rangeOpts?: RangeOptions; } /** @@ -493,6 +508,26 @@ export class ClientEncryption { callback: ClientEncryptionEncryptCallback ): void; + /** + * Encrypts a Match Expression or Aggregate Expression to query a range index. + * + * Only supported when queryType is "rangePreview" and algorithm is "RangePreview". + * + * @experimental The Range algorithm is experimental only. It is not intended for public use. It is subject to breaking changes.The aggregation or match expression you wish to encrypt. The value must be in the form + * + * The expression to encrypt must be one of the following: + * 1. A Match Expression of this form: + * `{$and: [{: {$gt: }}, {: {$lt: }}]}` + * 2. An Aggregate Expression of this form: + * `{$and: [{$gt: [, ]}, {$lt: [, ]}]}` + * + * `$gt` may also be `$gte`. `$lt` may also be `$lte`. + */ + encryptExpression( + value: Document, + options: ClientEncryptionOptions + ): Promise + /** * Explicitly decrypt a provided encrypted value * @param value An encrypted value diff --git a/bindings/node/lib/clientEncryption.js b/bindings/node/lib/clientEncryption.js index a67d501f1..5b36760e3 100644 --- a/bindings/node/lib/clientEncryption.js +++ b/bindings/node/lib/clientEncryption.js @@ -6,12 +6,16 @@ module.exports = function (modules) { const databaseNamespace = common.databaseNamespace; const collectionNamespace = common.collectionNamespace; const promiseOrCallback = common.promiseOrCallback; + const maybeCallback = common.maybeCallback; const StateMachine = modules.stateMachine.StateMachine; const BSON = modules.mongodb.BSON; const { loadCredentials } = require('./credentialsProvider'); const cryptoCallbacks = require('./cryptoCallbacks'); const { promisify } = require('util'); + /** @typedef {*} BSONValue - any serializable BSON value */ + /** @typedef {BSON.Long} Long A 64 bit integer, represented by the js-bson Long type.*/ + /** * @typedef {object} KMSProviders Configuration options that are used by specific KMS providers during key generation, encryption, and decryption. * @property {object} [aws] Configuration options for using 'aws' as your KMS provider @@ -551,15 +555,32 @@ module.exports = function (modules) { * @param {Buffer} [result] If present, is the encrypted result */ + /** + * @typedef {object} RangeOptions + * min, max, sparsity, and range must match the values set in the encryptedFields of the destination collection. + * For double and decimal128, min/max/precision must all be set, or all be unset. + * @property {BSONValue} min is required if precision is set. + * @property {BSONValue} max is required if precision is set. + * @property {BSON.Long} sparsity + * @property {number | undefined} precision (may only be set for double or decimal128). + */ + + /** + * @typedef {object} EncryptOptions Options to provide when encrypting data. + * @property {ClientEncryptionDataKeyId} [keyId] The id of the Binary dataKey to use for encryption. + * @property {string} [keyAltName] A unique string name corresponding to an already existing dataKey. + * @property {string} [algorithm] The algorithm to use for encryption. Must be either `'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'`, `'AEAD_AES_256_CBC_HMAC_SHA_512-Random'`, `'Indexed'` or `'Unindexed'` + * @property {bigint | number} [contentionFactor] (experimental) - the contention factor. + * @property {'equality' | 'rangePreview'} queryType (experimental) - the query type supported. + * @property {RangeOptions} [rangeOptions] (experimental) The index options for a Queryable Encryption field supporting "rangePreview" queries. + */ + /** * Explicitly encrypt a provided value. Note that either `options.keyId` or `options.keyAltName` must * be specified. Specifying both `options.keyId` and `options.keyAltName` is considered an error. * * @param {*} value The value that you wish to serialize. Must be of a type that can be serialized into BSON - * @param {object} options - * @param {ClientEncryptionDataKeyId} [options.keyId] The id of the Binary dataKey to use for encryption - * @param {string} [options.keyAltName] A unique string name corresponding to an already existing dataKey. - * @param {} [options.algorithm] The algorithm to use for encryption. Must be either `'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'`, `'AEAD_AES_256_CBC_HMAC_SHA_512-Random'`, `'Indexed'` or `'Unindexed'` + * @param {EncryptOptions} options * @param {ClientEncryptionEncryptCallback} [callback] Optional callback to invoke when value is encrypted * @returns {Promise|void} If no callback is provided, returns a Promise that either resolves with the encrypted value, or rejects with an error. If a callback is provided, returns nothing. * @@ -589,44 +610,29 @@ module.exports = function (modules) { * } */ encrypt(value, options, callback) { - const bson = this._bson; - const valueBuffer = bson.serialize({ v: value }); - const contextOptions = Object.assign({}, options); - if (options.keyId) { - contextOptions.keyId = options.keyId.buffer; - } - if (options.keyAltName) { - const keyAltName = options.keyAltName; - if (options.keyId) { - throw new TypeError(`"options" cannot contain both "keyId" and "keyAltName"`); - } - const keyAltNameType = typeof keyAltName; - if (keyAltNameType !== 'string') { - throw new TypeError( - `"options.keyAltName" must be of type string, but was of type ${keyAltNameType}` - ); - } - - contextOptions.keyAltName = bson.serialize({ keyAltName }); - } - - const stateMachine = new StateMachine({ - bson, - proxyOptions: this._proxyOptions, - tlsOptions: this._tlsOptions - }); - const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions); - - return promiseOrCallback(callback, cb => { - stateMachine.execute(this, context, (err, result) => { - if (err) { - cb(err, null); - return; - } + return maybeCallback(() => this._encrypt(value, false, options), callback); + } - cb(null, result.v); - }); - }); + /** + * Encrypts a Match Expression or Aggregate Expression to query a range index. + * + * Only supported when queryType is "rangePreview" and algorithm is "RangePreview". + * + * @experimental The Range algorithm is experimental only. It is not intended for public use. It is subject to breaking changes.The aggregation or match expression you wish to encrypt. The value must be in the form + * + * @param {object} expression a BSON document of one of the following forms: + * 1. A Match Expression of this form: + * `{$and: [{: {$gt: }}, {: {$lt: }}]}` + * 2. An Aggregate Expression of this form: + * `{$and: [{$gt: [, ]}, {$lt: [, ]}]}` + * + * `$gt` may also be `$gte`. `$lt` may also be `$lte`. + * + * @param {EncryptOptions} options + * @returns {Promise} Returns a Promise that either resolves with the encrypted value or rejects with an error. + */ + async encryptExpression(expression, options) { + return this._encrypt(expression, true, options); } /** @@ -693,6 +699,61 @@ module.exports = function (modules) { static get libmongocryptVersion() { return mc.MongoCrypt.libmongocryptVersion; } + + /** + * A helper that perform explicit encryption of values and expressions. + * Explicitly encrypt a provided value. Note that either `options.keyId` or `options.keyAltName` must + * be specified. Specifying both `options.keyId` and `options.keyAltName` is considered an error. + * + * @internal + * + * @param {*} value The value that you wish to encrypt. Must be of a type that can be serialized into BSON + * @param {boolean} expressionMode - a boolean that indicates whether or not to encrypt the value as an expression + * @param {EncryptOptions} options + * @returns the raw result of the call to stateMachine.execute(). When expressionMode is set to true, the return + * value will be a bson document. When false, the value will be a BSON Binary. + */ + async _encrypt(value, expressionMode, options) { + const bson = this._bson; + const valueBuffer = bson.serialize({ v: value }); + const contextOptions = Object.assign({}, options, { expressionMode }); + if (options.keyId) { + contextOptions.keyId = options.keyId.buffer; + } + if (options.keyAltName) { + const keyAltName = options.keyAltName; + if (options.keyId) { + throw new TypeError(`"options" cannot contain both "keyId" and "keyAltName"`); + } + const keyAltNameType = typeof keyAltName; + if (keyAltNameType !== 'string') { + throw new TypeError( + `"options.keyAltName" must be of type string, but was of type ${keyAltNameType}` + ); + } + + contextOptions.keyAltName = bson.serialize({ keyAltName }); + } + + const rangePreviewRegex = /^rangepreview$/i; + if (typeof options.algorithm === 'string' && rangePreviewRegex.test(options.algorithm)) { + contextOptions.algorithm = 'rangepreview'; + } + + if ('rangeOptions' in options) { + contextOptions.rangeOptions = bson.serialize(options.rangeOptions); + } + + const stateMachine = new StateMachine({ + bson, + proxyOptions: this._proxyOptions, + tlsOptions: this._tlsOptions + }); + const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions); + + const result = await stateMachine.executeAsync(this, context); + return result.v; + } } return { ClientEncryption }; diff --git a/bindings/node/lib/common.js b/bindings/node/lib/common.js index a9f13b27c..a7f5b0375 100644 --- a/bindings/node/lib/common.js +++ b/bindings/node/lib/common.js @@ -46,6 +46,19 @@ class MongoCryptError extends Error { } } +function maybeCallback(promiseFn, callback) { + const promise = promiseFn(); + if (callback == null) { + return promise; + } + + promise.then( + result => callback(undefined, result), + error => callback(error) + ); + return; +} + /** * @ignore * A helper function. Invokes a function that takes a callback as the final @@ -96,5 +109,6 @@ module.exports = { databaseNamespace, collectionNamespace, MongoCryptError, - promiseOrCallback + promiseOrCallback, + maybeCallback }; diff --git a/bindings/node/lib/stateMachine.js b/bindings/node/lib/stateMachine.js index 05e2c392a..a94dc8831 100644 --- a/bindings/node/lib/stateMachine.js +++ b/bindings/node/lib/stateMachine.js @@ -1,5 +1,7 @@ 'use strict'; +const { promisify } = require('util'); + module.exports = function (modules) { const tls = require('tls'); const net = require('net'); @@ -90,6 +92,10 @@ module.exports = function (modules) { constructor(options) { this.options = options || {}; this.bson = options.bson; + + this.executeAsync = promisify((autoEncrypter, context, callback) => + this.execute(autoEncrypter, context, callback) + ); } /** diff --git a/bindings/node/src/mongocrypt.cc b/bindings/node/src/mongocrypt.cc index 541b5f297..e03154064 100644 --- a/bindings/node/src/mongocrypt.cc +++ b/bindings/node/src/mongocrypt.cc @@ -528,64 +528,86 @@ Value MongoCrypt::MakeExplicitEncryptionContext(const CallbackInfo& info) { throw TypeError::New(Env(), "Parameter `value` must be a Buffer"); } - if (info.Length() > 1) { - Object options = info[1].ToObject(); + Object options = info.Length() > 1 ? info[1].ToObject() : Object::New(info.Env()); - if (options.Has("keyId")) { - Napi::Value keyId = options["keyId"]; + if (options.Has("keyId")) { + Napi::Value keyId = options["keyId"]; - if (!keyId.IsBuffer()) { - throw TypeError::New(Env(), "`keyId` must be a Buffer"); - } + if (!keyId.IsBuffer()) { + throw TypeError::New(Env(), "`keyId` must be a Buffer"); + } - std::unique_ptr binary(BufferToBinary(keyId.As())); - if (!mongocrypt_ctx_setopt_key_id(context.get(), binary.get())) { - throw TypeError::New(Env(), errorStringFromStatus(context.get())); - } + std::unique_ptr binary(BufferToBinary(keyId.As())); + if (!mongocrypt_ctx_setopt_key_id(context.get(), binary.get())) { + throw TypeError::New(Env(), errorStringFromStatus(context.get())); } + } - if (options.Has("keyAltName")) { - Napi::Value keyAltName = options["keyAltName"]; + if (options.Has("keyAltName")) { + Napi::Value keyAltName = options["keyAltName"]; - if (!keyAltName.IsBuffer()) { - throw TypeError::New(Env(), "`keyAltName` must be a Buffer"); - } + if (!keyAltName.IsBuffer()) { + throw TypeError::New(Env(), "`keyAltName` must be a Buffer"); + } - std::unique_ptr binary( - BufferToBinary(keyAltName.As())); - if (!mongocrypt_ctx_setopt_key_alt_name(context.get(), binary.get())) { - throw TypeError::New(Env(), errorStringFromStatus(context.get())); - } + std::unique_ptr binary( + BufferToBinary(keyAltName.As())); + if (!mongocrypt_ctx_setopt_key_alt_name(context.get(), binary.get())) { + throw TypeError::New(Env(), errorStringFromStatus(context.get())); } + } - if (options.Has("algorithm")) { - std::string algorithm = options.Get("algorithm").ToString(); - if (!mongocrypt_ctx_setopt_algorithm( + if (options.Has("algorithm")) { + std::string algorithm = options.Get("algorithm").ToString(); + if (!mongocrypt_ctx_setopt_algorithm( context.get(), algorithm.c_str(), algorithm.size())) { - throw TypeError::New(Env(), errorStringFromStatus(context.get())); - } + throw TypeError::New(Env(), errorStringFromStatus(context.get())); } - if (options.Has("contentionFactor")) { - Napi::Value contention_factor_value = options["contentionFactor"]; - int64_t contention_factor = contention_factor_value.IsBigInt() ? - contention_factor_value.As().Int64Value(nullptr) : - contention_factor_value.ToNumber().Int64Value(); - if (!mongocrypt_ctx_setopt_contention_factor(context.get(), contention_factor)) { - throw TypeError::New(Env(), errorStringFromStatus(context.get())); + if (algorithm == "rangepreview") { + if (!options.Has("rangeOptions")) { + throw TypeError::New(Env(), "`rangeOptions` must be provided if `algorithm` is set to RangePreview"); + } + + Napi::Value rangeOptions = options["rangeOptions"]; + + if (!rangeOptions.IsBuffer()) { + throw TypeError::New(Env(), "`rangeOptions` must be a Buffer"); } - } - if (options.Has("queryType")) { - std::string query_type_str = options.Get("queryType").ToString(); - if (!mongocrypt_ctx_setopt_query_type(context.get(), query_type_str.data(), -1)) { + std::unique_ptr binary(BufferToBinary(rangeOptions.As())); + if (!mongocrypt_ctx_setopt_algorithm_range(context.get(), binary.get())) { throw TypeError::New(Env(), errorStringFromStatus(context.get())); } } } + if (options.Has("contentionFactor")) { + Napi::Value contention_factor_value = options["contentionFactor"]; + int64_t contention_factor = contention_factor_value.IsBigInt() ? + contention_factor_value.As().Int64Value(nullptr) : + contention_factor_value.ToNumber().Int64Value(); + if (!mongocrypt_ctx_setopt_contention_factor(context.get(), contention_factor)) { + throw TypeError::New(Env(), errorStringFromStatus(context.get())); + } + } + + if (options.Has("queryType")) { + std::string query_type_str = options.Get("queryType").ToString(); + if (!mongocrypt_ctx_setopt_query_type(context.get(), query_type_str.data(), -1)) { + throw TypeError::New(Env(), errorStringFromStatus(context.get())); + } + } + std::unique_ptr binaryValue(BufferToBinary(valueBuffer.As())); - if (!mongocrypt_ctx_explicit_encrypt_init(context.get(), binaryValue.get())) { + + const bool isExpressionMode = options.Get("expressionMode").ToBoolean(); + + const bool status = isExpressionMode + ? mongocrypt_ctx_explicit_encrypt_expression_init(context.get(), binaryValue.get()) + : mongocrypt_ctx_explicit_encrypt_init(context.get(), binaryValue.get()); + + if (!status) { throw TypeError::New(Env(), errorStringFromStatus(context.get())); } diff --git a/bindings/node/test/clientEncryption.test.js b/bindings/node/test/clientEncryption.test.js index 600a8496a..1e5a62848 100644 --- a/bindings/node/test/clientEncryption.test.js +++ b/bindings/node/test/clientEncryption.test.js @@ -42,10 +42,7 @@ describe('ClientEncryption', function () { } async function setup() { - client = new MongoClient('mongodb://localhost:27017/test', { - useNewUrlParser: true, - useUnifiedTopology: true - }); + client = new MongoClient(process.env.MONGODB_URI || 'mongodb://localhost:27017/test'); await client.connect(); try { await client.db('client').collection('encryption').drop(); @@ -695,6 +692,142 @@ describe('ClientEncryption', function () { }); }); + describe('encrypt()', function () { + let clientEncryption; + let completeOptions; + let dataKey; + + beforeEach(async () => { + if (requirements.SKIP_LIVE_TESTS) { + this.currentTest.skipReason = `requirements.SKIP_LIVE_TESTS=${requirements.SKIP_LIVE_TESTS}`; + this.test.skip(); + return; + } + + await setup(); + clientEncryption = new ClientEncryption(client, { + keyVaultNamespace: 'client.encryption', + kmsProviders: { local: { key: Buffer.alloc(96) } } + }); + + dataKey = await clientEncryption.createDataKey('local', { + name: 'local', + kmsProviders: { local: { key: Buffer.alloc(96) } } + }); + + completeOptions = { + algorithm: 'RangePreview', + contentionFactor: 0, + rangeOptions: { + sparsity: new BSON.Long(1) + }, + keyId: dataKey + }; + }); + + afterEach(() => teardown()); + + context('when expressionMode is incorrectly provided as an argument', function () { + it('overrides the provided option with the correct value for expression mode', async function () { + const optionsWithExpressionMode = { ...completeOptions, expressionMode: true }; + const result = await clientEncryption.encrypt( + new mongodb.Long(0), + optionsWithExpressionMode + ); + + expect(result).to.be.instanceof(Binary); + }); + }); + }); + + describe('encryptExpression()', function () { + let clientEncryption; + let completeOptions; + let dataKey; + const expression = { + $and: [{ someField: { $gt: 1 } }] + }; + + beforeEach(async () => { + if (requirements.SKIP_LIVE_TESTS) { + this.currentTest.skipReason = `requirements.SKIP_LIVE_TESTS=${requirements.SKIP_LIVE_TESTS}`; + this.test.skip(); + return; + } + + await setup(); + clientEncryption = new ClientEncryption(client, { + keyVaultNamespace: 'client.encryption', + kmsProviders: { local: { key: Buffer.alloc(96) } } + }); + + dataKey = await clientEncryption.createDataKey('local', { + name: 'local', + kmsProviders: { local: { key: Buffer.alloc(96) } } + }); + + completeOptions = { + algorithm: 'RangePreview', + queryType: 'rangePreview', + contentionFactor: 0, + rangeOptions: { + sparsity: new BSON.Long(1) + }, + keyId: dataKey + }; + }); + + afterEach(() => teardown()); + + it('throws if rangeOptions is not provided', async function () { + delete completeOptions['rangeOptions']; + const errorOrResult = await clientEncryption + .encryptExpression(expression, completeOptions) + .catch(e => e); + + expect(errorOrResult).to.be.instanceof(Error); + }); + + it('throws if algorithm is not provided', async function () { + delete completeOptions['algorithm']; + const errorOrResult = await clientEncryption + .encryptExpression(expression, completeOptions) + .catch(e => e); + + expect(errorOrResult).to.be.instanceof(Error); + }); + + it(`throws if algorithm does not equal 'rangePreview'`, async function () { + completeOptions['algorithm'] = 'equality'; + const errorOrResult = await clientEncryption + .encryptExpression(expression, completeOptions) + .catch(e => e); + + expect(errorOrResult, errorOrResult.message).to.be.instanceof(Error); + }); + + it(`does not throw if algorithm has different casing than 'rangePreview'`, async function () { + completeOptions['algorithm'] = 'rAnGePrEvIeW'; + const errorOrResult = await clientEncryption + .encryptExpression(expression, completeOptions) + .catch(e => e); + + expect(errorOrResult).not.to.be.instanceof(Error); + }); + + context('when expressionMode is incorrectly provided as an argument', function () { + it('overrides the provided option with the correct value for expression mode', async function () { + const optionsWithExpressionMode = { ...completeOptions, expressionMode: false }; + const result = await clientEncryption.encryptExpression( + expression, + optionsWithExpressionMode + ); + + expect(result).not.to.be.instanceof(Binary); + }); + }); + }); + it('should provide the libmongocrypt version', function () { expect(ClientEncryption.libmongocryptVersion).to.be.a('string'); }); diff --git a/bindings/node/test/common.test.js b/bindings/node/test/common.test.js new file mode 100644 index 000000000..53e1233f8 --- /dev/null +++ b/bindings/node/test/common.test.js @@ -0,0 +1,94 @@ +'use strict'; + +const expect = require('chai').expect; +const maybeCallback = require('../lib/common').maybeCallback; + +describe('maybeCallback()', () => { + it('should accept two arguments', () => { + expect(maybeCallback).to.have.lengthOf(2); + }); + + describe('when handling an error case', () => { + it('should pass the error to the callback provided', done => { + const superPromiseRejection = Promise.reject(new Error('fail')); + const result = maybeCallback( + () => superPromiseRejection, + (error, result) => { + try { + expect(result).to.not.exist; + expect(error).to.be.instanceOf(Error); + return done(); + } catch (assertionError) { + return done(assertionError); + } + } + ); + expect(result).to.be.undefined; + }); + + it('should return the rejected promise to the caller when no callback is provided', async () => { + const superPromiseRejection = Promise.reject(new Error('fail')); + const returnedPromise = maybeCallback(() => superPromiseRejection, undefined); + expect(returnedPromise).to.equal(superPromiseRejection); + // @ts-expect-error: There is no overload to change the return type not be nullish, + // and we do not want to add one in fear of making it too easy to neglect adding the callback argument + const thrownError = await returnedPromise.catch(error => error); + expect(thrownError).to.be.instanceOf(Error); + }); + + it('should not modify a rejection error promise', async () => { + class MyError extends Error {} + const driverError = Object.freeze(new MyError()); + const rejection = Promise.reject(driverError); + // @ts-expect-error: There is no overload to change the return type not be nullish, + // and we do not want to add one in fear of making it too easy to neglect adding the callback argument + const thrownError = await maybeCallback(() => rejection, undefined).catch(error => error); + expect(thrownError).to.be.equal(driverError); + }); + + it('should not modify a rejection error when passed to callback', done => { + class MyError extends Error {} + const driverError = Object.freeze(new MyError()); + const rejection = Promise.reject(driverError); + maybeCallback( + () => rejection, + error => { + try { + expect(error).to.exist; + expect(error).to.equal(driverError); + done(); + } catch (assertionError) { + done(assertionError); + } + } + ); + }); + }); + + describe('when handling a success case', () => { + it('should pass the result and undefined error to the callback provided', done => { + const superPromiseSuccess = Promise.resolve(2); + + const result = maybeCallback( + () => superPromiseSuccess, + (error, result) => { + try { + expect(error).to.be.undefined; + expect(result).to.equal(2); + done(); + } catch (assertionError) { + done(assertionError); + } + } + ); + expect(result).to.be.undefined; + }); + + it('should return the resolved promise to the caller when no callback is provided', async () => { + const superPromiseSuccess = Promise.resolve(2); + const result = maybeCallback(() => superPromiseSuccess); + expect(result).to.equal(superPromiseSuccess); + expect(await result).to.equal(2); + }); + }); +}); From b3eca7c8fbc9bb639aa0dab6cf7fc7b7afc8b0e0 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Sun, 22 Jan 2023 08:18:48 -0500 Subject: [PATCH 2/7] fix tests --- bindings/node/test/clientEncryption.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bindings/node/test/clientEncryption.test.js b/bindings/node/test/clientEncryption.test.js index 1e5a62848..fd50ecffa 100644 --- a/bindings/node/test/clientEncryption.test.js +++ b/bindings/node/test/clientEncryption.test.js @@ -511,7 +511,7 @@ describe('ClientEncryption', function () { beforeEach(function () { if (requirements.SKIP_AWS_TESTS) { this.currentTest.skipReason = `requirements.SKIP_AWS_TESTS=${requirements.SKIP_AWS_TESTS}`; - this.skip(); + this.currentTest.skip(); return; } @@ -697,7 +697,7 @@ describe('ClientEncryption', function () { let completeOptions; let dataKey; - beforeEach(async () => { + beforeEach(async function () { if (requirements.SKIP_LIVE_TESTS) { this.currentTest.skipReason = `requirements.SKIP_LIVE_TESTS=${requirements.SKIP_LIVE_TESTS}`; this.test.skip(); @@ -748,7 +748,7 @@ describe('ClientEncryption', function () { $and: [{ someField: { $gt: 1 } }] }; - beforeEach(async () => { + beforeEach(async function () { if (requirements.SKIP_LIVE_TESTS) { this.currentTest.skipReason = `requirements.SKIP_LIVE_TESTS=${requirements.SKIP_LIVE_TESTS}`; this.test.skip(); From 4a2aa290489e8bf07e9d6c19aba682eb8df36597 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 23 Jan 2023 11:58:19 -0500 Subject: [PATCH 3/7] address comments - move string comparison for `rangePreview` into C++ - clean up documentation for KMS providers - remove _encrypt() and askForKMSProviders() from external docs --- bindings/node/README.md | 38 +++--------------------- bindings/node/lib/clientEncryption.js | 14 ++++----- bindings/node/lib/credentialsProvider.js | 4 +-- bindings/node/src/mongocrypt.cc | 2 +- 4 files changed, 13 insertions(+), 45 deletions(-) diff --git a/bindings/node/README.md b/bindings/node/README.md index 053804057..bb8888d4e 100644 --- a/bindings/node/README.md +++ b/bindings/node/README.md @@ -60,7 +60,7 @@ npm test
loadCredentials(kmsProviders)Promise
-

Load cloud provider credentials for the user provided kms providers. +

Load cloud provider credentials for the user provided KMS providers. Credentials will only attempt to get loaded if they do not exist and no existing credentials will get overwritten.

@@ -292,10 +292,6 @@ The public interface for explicit in-use encryption * [.decrypt(value, callback)](#ClientEncryption+decrypt) - * [.askForKMSCredentials()](#ClientEncryption+askForKMSCredentials) - - * [._encrypt(value, expressionMode, options)](#ClientEncryption+_encrypt) - * _inner_ * [~decryptCallback](#ClientEncryption..decryptCallback) @@ -599,7 +595,7 @@ async function encryptMyData(value) { ### *clientEncryption*.encryptExpression(expression, options) -**Experimental**: The Range algorithm is experimental only. It is not intended for public use. It is subject to breaking changes.The aggregation or match expression you wish to encrypt. The value must be in the form +**Experimental**: The Range algorithm is experimental only. It is not intended for public use. It is subject to breaking changes. | Param | Type | Description | | --- | --- | --- | @@ -637,32 +633,6 @@ async function decryptMyValue(value) { return clientEncryption.decrypt(value); } ``` - - -### *clientEncryption*.askForKMSCredentials() -Ask the user for KMS credentials. - -This returns anything that looks like the kmsProviders original input -option. It can be empty, and any provider specified here will override -the original ones. - - - -### *clientEncryption*._encrypt(value, expressionMode, options) -**Internal**: - -| Param | Type | Description | -| --- | --- | --- | -| value | \* | The value that you wish to encrypt. Must be of a type that can be serialized into BSON | -| expressionMode | boolean | a boolean that indicates whether or not to encrypt the value as an expression | -| options | [EncryptOptions](#EncryptOptions) | | - -A helper that perform explicit encryption of values and expressions. -Explicitly encrypt a provided value. Note that either `options.keyId` or `options.keyAltName` must -be specified. Specifying both `options.keyId` and `options.keyAltName` is considered an error. - -**Returns**: the raw result of the call to stateMachine.execute(). When expressionMode is set to true, the return - value will be a bson document. When false, the value will be a BSON Binary. ### *ClientEncryption*~decryptCallback @@ -683,9 +653,9 @@ An error indicating that something went wrong specifically with MongoDB Client E | Param | Type | Description | | --- | --- | --- | -| kmsProviders | Object | The user provided kms providers. | +| kmsProviders | Object | The user provided KMS providers. | -Load cloud provider credentials for the user provided kms providers. +Load cloud provider credentials for the user provided KMS providers. Credentials will only attempt to get loaded if they do not exist and no existing credentials will get overwritten. diff --git a/bindings/node/lib/clientEncryption.js b/bindings/node/lib/clientEncryption.js index 5b36760e3..8a2a09a46 100644 --- a/bindings/node/lib/clientEncryption.js +++ b/bindings/node/lib/clientEncryption.js @@ -618,7 +618,7 @@ module.exports = function (modules) { * * Only supported when queryType is "rangePreview" and algorithm is "RangePreview". * - * @experimental The Range algorithm is experimental only. It is not intended for public use. It is subject to breaking changes.The aggregation or match expression you wish to encrypt. The value must be in the form + * @experimental The Range algorithm is experimental only. It is not intended for public use. It is subject to breaking changes. * * @param {object} expression a BSON document of one of the following forms: * 1. A Match Expression of this form: @@ -689,6 +689,8 @@ module.exports = function (modules) { * This returns anything that looks like the kmsProviders original input * option. It can be empty, and any provider specified here will override * the original ones. + * + * @ignore */ async askForKMSCredentials() { return this._onKmsProviderRefresh @@ -705,13 +707,14 @@ module.exports = function (modules) { * Explicitly encrypt a provided value. Note that either `options.keyId` or `options.keyAltName` must * be specified. Specifying both `options.keyId` and `options.keyAltName` is considered an error. * - * @internal - * * @param {*} value The value that you wish to encrypt. Must be of a type that can be serialized into BSON * @param {boolean} expressionMode - a boolean that indicates whether or not to encrypt the value as an expression * @param {EncryptOptions} options * @returns the raw result of the call to stateMachine.execute(). When expressionMode is set to true, the return * value will be a bson document. When false, the value will be a BSON Binary. + * + * @ignore + * */ async _encrypt(value, expressionMode, options) { const bson = this._bson; @@ -735,11 +738,6 @@ module.exports = function (modules) { contextOptions.keyAltName = bson.serialize({ keyAltName }); } - const rangePreviewRegex = /^rangepreview$/i; - if (typeof options.algorithm === 'string' && rangePreviewRegex.test(options.algorithm)) { - contextOptions.algorithm = 'rangepreview'; - } - if ('rangeOptions' in options) { contextOptions.rangeOptions = bson.serialize(options.rangeOptions); } diff --git a/bindings/node/lib/credentialsProvider.js b/bindings/node/lib/credentialsProvider.js index c1484ac0a..388cc6c9c 100644 --- a/bindings/node/lib/credentialsProvider.js +++ b/bindings/node/lib/credentialsProvider.js @@ -8,11 +8,11 @@ try { } catch {} // eslint-disable-line /** - * Load cloud provider credentials for the user provided kms providers. + * Load cloud provider credentials for the user provided KMS providers. * Credentials will only attempt to get loaded if they do not exist * and no existing credentials will get overwritten. * - * @param {Object} kmsProviders - The user provided kms providers. + * @param {Object} kmsProviders - The user provided KMS providers. * @returns {Promise} The new kms providers. */ async function loadCredentials(kmsProviders) { diff --git a/bindings/node/src/mongocrypt.cc b/bindings/node/src/mongocrypt.cc index e03154064..f8a7fbb58 100644 --- a/bindings/node/src/mongocrypt.cc +++ b/bindings/node/src/mongocrypt.cc @@ -564,7 +564,7 @@ Value MongoCrypt::MakeExplicitEncryptionContext(const CallbackInfo& info) { throw TypeError::New(Env(), errorStringFromStatus(context.get())); } - if (algorithm == "rangepreview") { + if (strcasecmp(algorithm.c_str(), "rangepreview") == 0) { if (!options.Has("rangeOptions")) { throw TypeError::New(Env(), "`rangeOptions` must be provided if `algorithm` is set to RangePreview"); } From 5e3824cfdeb1a916e97b64de6541b8b214e484d7 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 23 Jan 2023 12:01:11 -0500 Subject: [PATCH 4/7] chore: ignore correct function --- bindings/node/README.md | 34 ++++++++---------------- bindings/node/lib/clientEncryption.js | 2 -- bindings/node/lib/credentialsProvider.js | 2 ++ 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/bindings/node/README.md b/bindings/node/README.md index bb8888d4e..8bda98d68 100644 --- a/bindings/node/README.md +++ b/bindings/node/README.md @@ -56,16 +56,6 @@ npm test
-## Functions - -
-
loadCredentials(kmsProviders)Promise
-

Load cloud provider credentials for the user provided KMS providers. -Credentials will only attempt to get loaded if they do not exist -and no existing credentials will get overwritten.

-
-
- ## Typedefs
@@ -292,6 +282,8 @@ The public interface for explicit in-use encryption * [.decrypt(value, callback)](#ClientEncryption+decrypt) + * [.askForKMSCredentials()](#ClientEncryption+askForKMSCredentials) + * _inner_ * [~decryptCallback](#ClientEncryption..decryptCallback) @@ -633,6 +625,15 @@ async function decryptMyValue(value) { return clientEncryption.decrypt(value); } ``` + + +### *clientEncryption*.askForKMSCredentials() +Ask the user for KMS credentials. + +This returns anything that looks like the kmsProviders original input +option. It can be empty, and any provider specified here will override +the original ones. + ### *ClientEncryption*~decryptCallback @@ -647,19 +648,6 @@ async function decryptMyValue(value) { ## MongoCryptError An error indicating that something went wrong specifically with MongoDB Client Encryption - - -## loadCredentials(kmsProviders) - -| Param | Type | Description | -| --- | --- | --- | -| kmsProviders | Object | The user provided KMS providers. | - -Load cloud provider credentials for the user provided KMS providers. -Credentials will only attempt to get loaded if they do not exist -and no existing credentials will get overwritten. - -**Returns**: Promise - The new kms providers. ## BSONValue diff --git a/bindings/node/lib/clientEncryption.js b/bindings/node/lib/clientEncryption.js index 8a2a09a46..21839187b 100644 --- a/bindings/node/lib/clientEncryption.js +++ b/bindings/node/lib/clientEncryption.js @@ -689,8 +689,6 @@ module.exports = function (modules) { * This returns anything that looks like the kmsProviders original input * option. It can be empty, and any provider specified here will override * the original ones. - * - * @ignore */ async askForKMSCredentials() { return this._onKmsProviderRefresh diff --git a/bindings/node/lib/credentialsProvider.js b/bindings/node/lib/credentialsProvider.js index 388cc6c9c..be8d505e5 100644 --- a/bindings/node/lib/credentialsProvider.js +++ b/bindings/node/lib/credentialsProvider.js @@ -14,6 +14,8 @@ try { * * @param {Object} kmsProviders - The user provided KMS providers. * @returns {Promise} The new kms providers. + * + * @ignore */ async function loadCredentials(kmsProviders) { if (awsCredentialProviders) { From 4afc453dd164fa2d6fd85dba84b25219fcace65b Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 23 Jan 2023 14:33:01 -0500 Subject: [PATCH 5/7] chore: run prettier on index.d.ts --- bindings/node/index.d.ts | 56 +++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index 1d4ac1f5c..f2ddafe03 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -1,5 +1,10 @@ import type { Document, Binary, Long } from 'bson'; -import type { MongoClient, BulkWriteResult, ClientSession, DeleteResult, FindCursor } from 'mongodb'; +import type { + MongoClient, + BulkWriteResult, + DeleteResult, + FindCursor +} from 'mongodb'; export type ClientEncryptionDataKeyProvider = 'aws' | 'azure' | 'gcp' | 'local' | 'kmip'; @@ -20,8 +25,7 @@ export interface DataKey { /** * An error indicating that something went wrong specifically with MongoDB Client Encryption */ -export class MongoCryptError extends Error { -} +export class MongoCryptError extends Error {} /** * A set of options for specifying a Socks5 proxy. @@ -152,7 +156,7 @@ export interface KMSProviders { * Defaults to "oauth2.googleapis.com" */ endpoint?: string | undefined; - } + }; } /** @@ -306,7 +310,11 @@ export interface ClientEncryptionCreateDataKeyProviderOptions { /** * Idenfities a new KMS-specific key used to encrypt the new data key */ - masterKey?: AWSEncryptionKeyOptions | AzureEncryptionKeyOptions | GCPEncryptionKeyOptions | undefined; + masterKey?: + | AWSEncryptionKeyOptions + | AzureEncryptionKeyOptions + | GCPEncryptionKeyOptions + | undefined; /** * An optional list of string alternate names used to reference a key. @@ -321,7 +329,11 @@ export interface ClientEncryptionCreateDataKeyProviderOptions { /** @experimental */ export interface ClientEncryptionRewrapManyDataKeyProviderOptions { provider: ClientEncryptionDataKeyProvider; - masterKey?: AWSEncryptionKeyOptions | AzureEncryptionKeyOptions | GCPEncryptionKeyOptions | undefined; + masterKey?: + | AWSEncryptionKeyOptions + | AzureEncryptionKeyOptions + | GCPEncryptionKeyOptions + | undefined; } /** @experimental */ @@ -334,7 +346,7 @@ export interface ClientEncryptionRewrapManyDataKeyResult { * RangeOpts specifies index options for a Queryable Encryption field supporting "rangePreview" queries. * min, max, sparsity, and range must match the values set in the encryptedFields of the destination collection. * For double and decimal128, min/max/precision must all be set, or all be unset. -*/ + */ interface RangeOptions { min?: any; max?: any; @@ -349,7 +361,12 @@ export interface ClientEncryptionEncryptOptions { /** * The algorithm to use for encryption. */ - algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' | 'AEAD_AES_256_CBC_HMAC_SHA_512-Random' | 'Indexed' | 'Unindexed' | 'RangePreview'; + algorithm: + | 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' + | 'AEAD_AES_256_CBC_HMAC_SHA_512-Random' + | 'Indexed' + | 'Unindexed' + | 'RangePreview'; /** * The id of the Binary dataKey to use for encryption @@ -386,9 +403,7 @@ export class ClientEncryption { * Creates a data key used for explicit encryption and inserts it into the key vault namespace * @param provider The KMS provider used for this data key. Must be `'aws'`, `'azure'`, `'gcp'`, or `'local'` */ - createDataKey( - provider: ClientEncryptionDataKeyProvider - ): Promise; + createDataKey(provider: ClientEncryptionDataKeyProvider): Promise; /** * Creates a data key used for explicit encryption and inserts it into the key vault namespace @@ -489,10 +504,7 @@ export class ClientEncryption { * @param value The value that you wish to serialize. Must be of a type that can be serialized into BSON * @param options */ - encrypt( - value: any, - options: ClientEncryptionEncryptOptions - ): Promise; + encrypt(value: any, options: ClientEncryptionEncryptOptions): Promise; /** * Explicitly encrypt a provided value. @@ -523,28 +535,20 @@ export class ClientEncryption { * * `$gt` may also be `$gte`. `$lt` may also be `$lte`. */ - encryptExpression( - value: Document, - options: ClientEncryptionOptions - ): Promise + encryptExpression(value: Document, options: ClientEncryptionOptions): Promise; /** * Explicitly decrypt a provided encrypted value * @param value An encrypted value */ - decrypt( - value: Buffer | Binary - ): Promise; + decrypt(value: Buffer | Binary): Promise; /** * Explicitly decrypt a provided encrypted value * @param value An encrypted value * @param callback Callback to invoke when value is decrypted */ - decrypt( - value: Buffer | Binary, - callback: ClientEncryptionDecryptCallback - ): void; + decrypt(value: Buffer | Binary, callback: ClientEncryptionDecryptCallback): void; static readonly libmongocryptVersion: string; } From 17e9778595db4263b5d793443bc1b76fa4feec1a Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 23 Jan 2023 14:37:42 -0500 Subject: [PATCH 6/7] chore: address comments --- bindings/node/test/clientEncryption.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bindings/node/test/clientEncryption.test.js b/bindings/node/test/clientEncryption.test.js index fd50ecffa..188341378 100644 --- a/bindings/node/test/clientEncryption.test.js +++ b/bindings/node/test/clientEncryption.test.js @@ -780,21 +780,21 @@ describe('ClientEncryption', function () { afterEach(() => teardown()); it('throws if rangeOptions is not provided', async function () { - delete completeOptions['rangeOptions']; + expect(delete completeOptions.rangeOptions).to.be.true; const errorOrResult = await clientEncryption .encryptExpression(expression, completeOptions) .catch(e => e); - expect(errorOrResult).to.be.instanceof(Error); + expect(errorOrResult).to.be.instanceof(TypeError); }); it('throws if algorithm is not provided', async function () { - delete completeOptions['algorithm']; + expect(delete completeOptions.algorithm).to.be.true; const errorOrResult = await clientEncryption .encryptExpression(expression, completeOptions) .catch(e => e); - expect(errorOrResult).to.be.instanceof(Error); + expect(errorOrResult).to.be.instanceof(TypeError); }); it(`throws if algorithm does not equal 'rangePreview'`, async function () { @@ -803,7 +803,7 @@ describe('ClientEncryption', function () { .encryptExpression(expression, completeOptions) .catch(e => e); - expect(errorOrResult, errorOrResult.message).to.be.instanceof(Error); + expect(errorOrResult).to.be.instanceof(TypeError); }); it(`does not throw if algorithm has different casing than 'rangePreview'`, async function () { From 08b75f61f85eee0d254ba15c2ed0be6e851ddf77 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 23 Jan 2023 16:05:52 -0500 Subject: [PATCH 7/7] chore: fix maybeCallback --- bindings/node/lib/common.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bindings/node/lib/common.js b/bindings/node/lib/common.js index a7f5b0375..cea9359d1 100644 --- a/bindings/node/lib/common.js +++ b/bindings/node/lib/common.js @@ -53,8 +53,8 @@ function maybeCallback(promiseFn, callback) { } promise.then( - result => callback(undefined, result), - error => callback(error) + result => process.nextTick(callback, undefined, result), + error => process.nextTick(callback, error) ); return; }