diff --git a/bindings/node/README.md b/bindings/node/README.md index de235027d..8bda98d68 100644 --- a/bindings/node/README.md +++ b/bindings/node/README.md @@ -59,6 +59,12 @@ npm test ## 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 +106,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,6 +278,8 @@ 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) @@ -534,10 +549,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 +584,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. + +| 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) @@ -621,6 +648,16 @@ the original ones. ## MongoCryptError An error indicating that something went wrong specifically with MongoDB Client Encryption + + +## BSONValue +any serializable BSON value + + + +## Long +A 64 bit integer, represented by the js-bson Long type. + ## KMSProviders @@ -771,3 +808,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..f2ddafe03 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -1,5 +1,10 @@ -import type { Document, Binary } from 'bson'; -import type { MongoClient, BulkWriteResult, ClientSession, DeleteResult, FindCursor } from 'mongodb'; +import type { Document, Binary, Long } from 'bson'; +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 */ @@ -330,6 +342,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 +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'; + 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 +382,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; } /** @@ -371,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 @@ -474,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. @@ -493,23 +520,35 @@ 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 */ - 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; } diff --git a/bindings/node/lib/clientEncryption.js b/bindings/node/lib/clientEncryption.js index a67d501f1..21839187b 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. + * + * @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,57 @@ 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. + * + * @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; + 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 }); + } + + 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..cea9359d1 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 => process.nextTick(callback, undefined, result), + error => process.nextTick(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/credentialsProvider.js b/bindings/node/lib/credentialsProvider.js index c1484ac0a..be8d505e5 100644 --- a/bindings/node/lib/credentialsProvider.js +++ b/bindings/node/lib/credentialsProvider.js @@ -8,12 +8,14 @@ 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. + * + * @ignore */ async function loadCredentials(kmsProviders) { if (awsCredentialProviders) { 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..f8a7fbb58 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 (strcasecmp(algorithm.c_str(), "rangepreview") == 0) { + 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..188341378 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(); @@ -514,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; } @@ -695,6 +692,142 @@ describe('ClientEncryption', function () { }); }); + describe('encrypt()', function () { + let clientEncryption; + let completeOptions; + let dataKey; + + beforeEach(async function () { + 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 function () { + 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 () { + expect(delete completeOptions.rangeOptions).to.be.true; + const errorOrResult = await clientEncryption + .encryptExpression(expression, completeOptions) + .catch(e => e); + + expect(errorOrResult).to.be.instanceof(TypeError); + }); + + it('throws if algorithm is not provided', async function () { + expect(delete completeOptions.algorithm).to.be.true; + const errorOrResult = await clientEncryption + .encryptExpression(expression, completeOptions) + .catch(e => e); + + expect(errorOrResult).to.be.instanceof(TypeError); + }); + + 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).to.be.instanceof(TypeError); + }); + + 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); + }); + }); +});