diff --git a/.eslintignore b/.eslintignore index fdc4ad443c6..39a231fa1fc 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,6 +2,3 @@ lib test/disabled !etc/docs - -src/client-side-encryption -test/unit/client-side-encryption diff --git a/.eslintrc.json b/.eslintrc.json index ba4f33a112e..5c38c6c90a9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -9,6 +9,7 @@ "import", "@typescript-eslint", "prettier", + "unused-imports", "tsdoc" ], "extends": [ @@ -275,6 +276,16 @@ "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/no-empty-function": "off" } + }, + { + // Settings for generated definition files + "files": [ + "mongodb.d.ts" + ], + "parser": "@typescript-eslint/parser", + "rules": { + "unused-imports/no-unused-imports": "error" + } } ] } diff --git a/package-lock.json b/package-lock.json index c7db725d21d..d9a5f0d05a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,12 +41,13 @@ "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-tsdoc": "^0.2.17", + "eslint-plugin-unused-imports": "^2.0.0", "express": "^4.18.2", "gcp-metadata": "^5.2.0", "js-yaml": "^4.1.0", "mocha": "^10.2.0", "mocha-sinon": "^2.1.2", - "mongodb-client-encryption": "^6.0.0-alpha.0", + "mongodb-client-encryption": "^6.0.0-alpha.1", "mongodb-legacy": "^5.0.0", "nyc": "^15.1.0", "prettier": "^2.8.8", @@ -74,7 +75,7 @@ "@mongodb-js/zstd": "^1.1.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0-alpha.0 <7", + "mongodb-client-encryption": ">=6.0.0-alpha.1 <7", "snappy": "^7.2.2", "socks": "^2.7.1" }, @@ -4511,6 +4512,36 @@ "@microsoft/tsdoc-config": "0.16.2" } }, + "node_modules/eslint-plugin-unused-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz", + "integrity": "sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==", + "dev": true, + "dependencies": { + "eslint-rule-composer": "^0.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0", + "eslint": "^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/eslint-rule-docs": { "version": "1.1.235", "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.235.tgz", @@ -6598,9 +6629,9 @@ } }, "node_modules/mongodb-client-encryption": { - "version": "6.0.0-alpha.0", - "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-6.0.0-alpha.0.tgz", - "integrity": "sha512-lwkwJcjgXnxtd3A5otzTchxtqS+aVmsGpVaYnpnrL2m2s59uWXJpVStPQBt54SYDPt0Eu7pcT8nrWcVvZGZFfg==", + "version": "6.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-6.0.0-alpha.1.tgz", + "integrity": "sha512-SaYli844l5TN8oog4nJW8KKWpSPwSx2auojv30JtDQv8hgWV979Bnc4bwF2pf+R9fquqgoLr27CWxganGY0Zfg==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -6609,7 +6640,7 @@ "prebuild-install": "^7.1.1" }, "engines": { - "node": ">=12.9.0" + "node": ">=16.20.1" } }, "node_modules/mongodb-connection-string-url": { diff --git a/package.json b/package.json index ac7ffaa6d93..815a924d6f6 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@mongodb-js/zstd": "^1.1.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0-alpha.0 <7", + "mongodb-client-encryption": ">=6.0.0-alpha.1 <7", "snappy": "^7.2.2", "socks": "^2.7.1" }, @@ -92,12 +92,13 @@ "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-tsdoc": "^0.2.17", + "eslint-plugin-unused-imports": "^2.0.0", "express": "^4.18.2", "gcp-metadata": "^5.2.0", "js-yaml": "^4.1.0", "mocha": "^10.2.0", "mocha-sinon": "^2.1.2", - "mongodb-client-encryption": "^6.0.0-alpha.0", + "mongodb-client-encryption": "^6.0.0-alpha.1", "mongodb-legacy": "^5.0.0", "nyc": "^15.1.0", "prettier": "^2.8.8", @@ -125,7 +126,7 @@ "scripts": { "build:evergreen": "node .evergreen/generate_evergreen_tasks.js", "build:ts": "node ./node_modules/typescript/bin/tsc", - "build:dts": "npm run build:ts && api-extractor run && node etc/clean_definition_files.cjs", + "build:dts": "npm run build:ts && api-extractor run && node etc/clean_definition_files.cjs && eslint mongodb.d.ts --fix", "build:docs": "./etc/docs/build.ts", "build:typedoc": "typedoc", "build:nightly": "node ./.github/scripts/nightly.mjs", diff --git a/src/client-side-encryption/autoEncrypter.js b/src/client-side-encryption/autoEncrypter.js deleted file mode 100644 index d6fafe0e5d3..00000000000 --- a/src/client-side-encryption/autoEncrypter.js +++ /dev/null @@ -1,428 +0,0 @@ -import { databaseNamespace } from './common'; -import { StateMachine } from './stateMachine'; -import { MongocryptdManager } from './mongocryptdManager'; -import {MongoClient} from '../mongo_client'; -import {MongoError} from '../error'; -import { loadCredentials } from './providers'; -import * as cryptoCallbacks from './cryptoCallbacks'; -import { serialize, deserialize } from '../bson'; -import { getMongoDBClientEncryption } from '../deps'; - -/** - * Configuration options for a automatic client encryption. - * - * @typedef {Object} AutoEncrypter~AutoEncryptionOptions - * @property {MongoClient} [keyVaultClient] A `MongoClient` used to fetch keys from a key vault - * @property {string} [keyVaultNamespace] The namespace where keys are stored in the key vault - * @property {KMSProviders} [kmsProviders] Configuration options that are used by specific KMS providers during key generation, encryption, and decryption. - * @property {object} [schemaMap] A map of namespaces to a local JSON schema for encryption - * @property {boolean} [bypassAutoEncryption] Allows the user to bypass auto encryption, maintaining implicit decryption - * @property {AutoEncrypter~logger} [options.logger] An optional hook to catch logging messages from the underlying encryption engine - * @property {AutoEncrypter~AutoEncryptionExtraOptions} [extraOptions] Extra options related to the mongocryptd process - */ - -/** - * Extra options related to the mongocryptd process - * \* _Available in MongoDB 6.0 or higher._ - * @typedef {object} AutoEncrypter~AutoEncryptionExtraOptions - * @property {string} [mongocryptdURI] A local process the driver communicates with to determine how to encrypt values in a command. Defaults to "mongodb://%2Fvar%2Fmongocryptd.sock" if domain sockets are available or "mongodb://localhost:27020" otherwise - * @property {boolean} [mongocryptdBypassSpawn=false] If true, autoEncryption will not attempt to spawn a mongocryptd before connecting - * @property {string} [mongocryptdSpawnPath] The path to the mongocryptd executable on the system - * @property {string[]} [mongocryptdSpawnArgs] Command line arguments to use when auto-spawning a mongocryptd - * @property {string} [cryptSharedLibPath] Full path to a MongoDB Crypt shared library on the system. If specified, autoEncryption will not attempt to spawn a mongocryptd, but makes use of the shared library file specified. Note that the path must point to the shared libary file itself, not the folder which contains it \* - * @property {boolean} [cryptSharedLibRequired] If true, never use mongocryptd and fail when the MongoDB Crypt shared libary cannot be loaded. Defaults to true if [cryptSharedLibPath] is specified and false otherwise \* - */ - -/** - * @callback AutoEncrypter~logger - * @description A callback that is invoked with logging information from - * the underlying C++ Bindings. - * @param {AutoEncrypter~logLevel} level The level of logging. - * @param {string} message The message to log - */ - -/** - * @name AutoEncrypter~logLevel - * @enum {number} - * @description - * The level of severity of the log message - * - * | Value | Level | - * |-------|-------| - * | 0 | Fatal Error | - * | 1 | Error | - * | 2 | Warning | - * | 3 | Info | - * | 4 | Trace | - */ - -/** - * @internal An internal class to be used by the driver for auto encryption - * **NOTE**: Not meant to be instantiated directly, this is for internal use only. - */ -export class AutoEncrypter { - /** - * Create an AutoEncrypter - * - * **Note**: Do not instantiate this class directly. Rather, supply the relevant options to a MongoClient - * - * **Note**: Supplying `options.schemaMap` provides more security than relying on JSON Schemas obtained from the server. - * It protects against a malicious server advertising a false JSON Schema, which could trick the client into sending unencrypted data that should be encrypted. - * Schemas supplied in the schemaMap only apply to configuring automatic encryption for Client-Side Field Level Encryption. - * Other validation rules in the JSON schema will not be enforced by the driver and will result in an error. - * @param {MongoClient} client The client autoEncryption is enabled on - * @param {AutoEncrypter~AutoEncryptionOptions} [options] Optional settings - * - * @example Create an AutoEncrypter that makes use of mongocryptd - * // Enabling autoEncryption via a MongoClient using mongocryptd - * const { MongoClient } = require('mongodb'); - * const client = new MongoClient(URL, { - * autoEncryption: { - * kmsProviders: { - * aws: { - * accessKeyId: AWS_ACCESS_KEY, - * secretAccessKey: AWS_SECRET_KEY - * } - * } - * } - * }); - * - * await client.connect(); - * // From here on, the client will be encrypting / decrypting automatically - * @example Create an AutoEncrypter that makes use of libmongocrypt's CSFLE shared library - * // Enabling autoEncryption via a MongoClient using CSFLE shared library - * const { MongoClient } = require('mongodb'); - * const client = new MongoClient(URL, { - * autoEncryption: { - * kmsProviders: { - * aws: {} - * }, - * extraOptions: { - * cryptSharedLibPath: '/path/to/local/crypt/shared/lib', - * cryptSharedLibRequired: true - * } - * } - * }); - * - * await client.connect(); - * // From here on, the client will be encrypting / decrypting automatically - */ - constructor(client, options) { - this._client = client; - this._bypassEncryption = options.bypassAutoEncryption === true; - - this._keyVaultNamespace = options.keyVaultNamespace || 'admin.datakeys'; - this._keyVaultClient = options.keyVaultClient || client; - this._metaDataClient = options.metadataClient || client; - this._proxyOptions = options.proxyOptions || {}; - this._tlsOptions = options.tlsOptions || {}; - this._onKmsProviderRefresh = options.onKmsProviderRefresh; - this._kmsProviders = options.kmsProviders || {}; - - const mongoCryptOptions = {}; - if (options.schemaMap) { - mongoCryptOptions.schemaMap = Buffer.isBuffer(options.schemaMap) - ? options.schemaMap - : serialize(options.schemaMap); - } - - if (options.encryptedFieldsMap) { - mongoCryptOptions.encryptedFieldsMap = Buffer.isBuffer(options.encryptedFieldsMap) - ? options.encryptedFieldsMap - : serialize(options.encryptedFieldsMap); - } - - mongoCryptOptions.kmsProviders = !Buffer.isBuffer(this._kmsProviders) - ? serialize(this._kmsProviders) - : this._kmsProviders; - - if (options.logger) { - mongoCryptOptions.logger = options.logger; - } - - if (options.extraOptions && options.extraOptions.cryptSharedLibPath) { - mongoCryptOptions.cryptSharedLibPath = options.extraOptions.cryptSharedLibPath; - } - - if (options.bypassQueryAnalysis) { - mongoCryptOptions.bypassQueryAnalysis = options.bypassQueryAnalysis; - } - - this._bypassMongocryptdAndCryptShared = this._bypassEncryption || options.bypassQueryAnalysis; - - if (options.extraOptions && options.extraOptions.cryptSharedLibSearchPaths) { - // Only for driver testing - mongoCryptOptions.cryptSharedLibSearchPaths = - options.extraOptions.cryptSharedLibSearchPaths; - } else if (!this._bypassMongocryptdAndCryptShared) { - mongoCryptOptions.cryptSharedLibSearchPaths = ['$SYSTEM']; - } - - Object.assign(mongoCryptOptions, { cryptoCallbacks }); - const { MongoCrypt } = getMongoDBClientEncryption(); - this._mongocrypt = new MongoCrypt(mongoCryptOptions); - this._contextCounter = 0; - - if ( - options.extraOptions && - options.extraOptions.cryptSharedLibRequired && - !this.cryptSharedLibVersionInfo - ) { - throw new MongoError('`cryptSharedLibRequired` set but no crypt_shared library loaded'); - } - - // Only instantiate mongocryptd manager/client once we know for sure - // that we are not using the CSFLE shared library. - if (!this._bypassMongocryptdAndCryptShared && !this.cryptSharedLibVersionInfo) { - this._mongocryptdManager = new MongocryptdManager(options.extraOptions); - const clientOptions = { - serverSelectionTimeoutMS: 10000 - }; - - if ( - options.extraOptions == null || - typeof options.extraOptions.mongocryptdURI !== 'string' - ) { - clientOptions.family = 4; - } - - this._mongocryptdClient = new MongoClient(this._mongocryptdManager.uri, clientOptions); - } - } - - /** - * @ignore - * @param {Function} callback Invoked when the mongocryptd client either successfully connects or errors - */ - init(callback) { - if (this._bypassMongocryptdAndCryptShared || this.cryptSharedLibVersionInfo) { - return callback(); - } - const _callback = (err, res) => { - if ( - err && - err.message && - (err.message.match(/timed out after/) || err.message.match(/ENOTFOUND/)) - ) { - callback( - new MongoError( - 'Unable to connect to `mongocryptd`, please make sure it is running or in your PATH for auto-spawn' - ) - ); - return; - } - - callback(err, res); - }; - - if (this._mongocryptdManager.bypassSpawn) { - return this._mongocryptdClient.connect().then( - result => { - return _callback(null, result); - }, - error => { - _callback(error, null); - } - ); - } - - this._mongocryptdManager.spawn(() => { - this._mongocryptdClient.connect().then( - result => { - return _callback(null, result); - }, - error => { - _callback(error, null); - } - ); - }); - } - - /** - * @ignore - * @param {Function} callback Invoked when the mongocryptd client either successfully disconnects or errors - */ - teardown(force, callback) { - if (this._mongocryptdClient) { - this._mongocryptdClient.close(force).then( - result => { - return callback(null, result); - }, - error => { - callback(error); - } - ); - } else { - callback(); - } - } - - /** - * @ignore - * Encrypt a command for a given namespace. - * - * @param {string} ns The namespace for this encryption context - * @param {object} cmd The command to encrypt - * @param {Function} callback - */ - encrypt(ns, cmd, options, callback) { - if (typeof ns !== 'string') { - throw new TypeError('Parameter `ns` must be a string'); - } - - if (typeof cmd !== 'object') { - throw new TypeError('Parameter `cmd` must be an object'); - } - - if (typeof options === 'function' && callback == null) { - callback = options; - options = {}; - } - - // If `bypassAutoEncryption` has been specified, don't encrypt - if (this._bypassEncryption) { - callback(undefined, cmd); - return; - } - - const commandBuffer = Buffer.isBuffer(cmd) ? cmd : serialize(cmd, options); - - let context; - try { - context = this._mongocrypt.makeEncryptionContext(databaseNamespace(ns), commandBuffer); - } catch (err) { - callback(err, null); - return; - } - - // TODO: should these be accessors from the addon? - context.id = this._contextCounter++; - context.ns = ns; - context.document = cmd; - - const stateMachine = new StateMachine({ - ...options, - promoteValues: false, - promoteLongs: false, - proxyOptions: this._proxyOptions, - tlsOptions: this._tlsOptions - }); - stateMachine.execute(this, context, callback); - } - - /** - * @ignore - * Decrypt a command response - * - * @param {Buffer} buffer - * @param {Function} callback - */ - decrypt(response, options, callback) { - if (typeof options === 'function' && callback == null) { - callback = options; - options = {}; - } - - const buffer = Buffer.isBuffer(response) ? response : serialize(response, options); - - let context; - try { - context = this._mongocrypt.makeDecryptionContext(buffer); - } catch (err) { - callback(err, null); - return; - } - - // TODO: should this be an accessor from the addon? - context.id = this._contextCounter++; - - const stateMachine = new StateMachine({ - ...options, - proxyOptions: this._proxyOptions, - tlsOptions: this._tlsOptions - }); - - const decorateResult = this[Symbol.for('@@mdb.decorateDecryptionResult')]; - stateMachine.execute(this, context, function (err, result) { - // Only for testing/internal usage - if (!err && result && decorateResult) { - err = decorateDecryptionResult(result, response); - if (err) return callback(err); - } - callback(err, result); - }); - } - - /** - * 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. - */ - async askForKMSCredentials() { - return this._onKmsProviderRefresh - ? this._onKmsProviderRefresh() - : loadCredentials(this._kmsProviders); - } - - /** - * Return the current libmongocrypt's CSFLE shared library version - * as `{ version: bigint, versionStr: string }`, or `null` if no CSFLE - * shared library was loaded. - */ - get cryptSharedLibVersionInfo() { - return this._mongocrypt.cryptSharedLibVersionInfo; - } - - static get libmongocryptVersion() { - const { MongoCrypt } = getMongoDBClientEncryption(); - return MongoCrypt.libmongocryptVersion; - } -} - -/** - * Recurse through the (identically-shaped) `decrypted` and `original` - * objects and attach a `decryptedKeys` property on each sub-object that - * contained encrypted fields. Because we only call this on BSON responses, - * we do not need to worry about circular references. - * - * @internal - * @ignore - */ -function decorateDecryptionResult(decrypted, original, isTopLevelDecorateCall = true) { - const decryptedKeys = Symbol.for('@@mdb.decryptedKeys'); - if (isTopLevelDecorateCall) { - // The original value could have been either a JS object or a BSON buffer - if (Buffer.isBuffer(original)) { - original = deserialize(original); - } - if (Buffer.isBuffer(decrypted)) { - return new Error('Expected result of decryption to be deserialized BSON object'); - } - } - - if (!decrypted || typeof decrypted !== 'object') return; - for (const k of Object.keys(decrypted)) { - const originalValue = original[k]; - - // An object was decrypted by libmongocrypt if and only if it was - // a BSON Binary object with subtype 6. - if (originalValue && originalValue._bsontype === 'Binary' && originalValue.sub_type === 6) { - if (!decrypted[decryptedKeys]) { - Object.defineProperty(decrypted, decryptedKeys, { - value: [], - configurable: true, - enumerable: false, - writable: false - }); - } - decrypted[decryptedKeys].push(k); - // Do not recurse into this decrypted value. It could be a subdocument/array, - // in which case there is no original value associated with its subfields. - continue; - } - - decorateDecryptionResult(decrypted[k], originalValue, false); - } -} diff --git a/src/client-side-encryption/auto_encrypter.ts b/src/client-side-encryption/auto_encrypter.ts new file mode 100644 index 00000000000..5bcbd1c683c --- /dev/null +++ b/src/client-side-encryption/auto_encrypter.ts @@ -0,0 +1,663 @@ +import { + type MongoCrypt, + type MongoCryptConstructor, + type MongoCryptOptions +} from 'mongodb-client-encryption'; + +import { deserialize, type Document, serialize } from '../bson'; +import { type CommandOptions, type ProxyOptions } from '../cmap/connection'; +import { getMongoDBClientEncryption } from '../deps'; +import { type AnyError, MongoRuntimeError } from '../error'; +import { MongoClient, type MongoClientOptions } from '../mongo_client'; +import { type Callback, MongoDBCollectionNamespace } from '../utils'; +import * as cryptoCallbacks from './crypto_callbacks'; +import { MongoCryptInvalidArgumentError } from './errors'; +import { MongocryptdManager } from './mongocryptd_manager'; +import { type KMSProviders, refreshKMSCredentials } from './providers'; +import { + type CSFLEKMSTlsOptions, + StateMachine, + type StateMachineExecutable +} from './state_machine'; + +/** @public */ +export interface AutoEncryptionOptions { + /** @internal client for metadata lookups */ + metadataClient?: MongoClient; + /** A `MongoClient` used to fetch keys from a key vault */ + keyVaultClient?: MongoClient; + /** The namespace where keys are stored in the key vault */ + keyVaultNamespace?: string; + /** Configuration options that are used by specific KMS providers during key generation, encryption, and decryption. */ + kmsProviders?: { + /** Configuration options for using 'aws' as your KMS provider */ + aws?: + | { + /** The access key used for the AWS KMS provider */ + accessKeyId: string; + /** The secret access key used for the AWS KMS provider */ + secretAccessKey: string; + /** + * An optional AWS session token that will be used as the + * X-Amz-Security-Token header for AWS requests. + */ + sessionToken?: string; + } + | Record; + /** Configuration options for using 'local' as your KMS provider */ + local?: { + /** + * The master key used to encrypt/decrypt data keys. + * A 96-byte long Buffer or base64 encoded string. + */ + key: Buffer | string; + }; + /** Configuration options for using 'azure' as your KMS provider */ + azure?: + | { + /** The tenant ID identifies the organization for the account */ + tenantId: string; + /** The client ID to authenticate a registered application */ + clientId: string; + /** The client secret to authenticate a registered application */ + clientSecret: string; + /** + * If present, a host with optional port. E.g. "example.com" or "example.com:443". + * This is optional, and only needed if customer is using a non-commercial Azure instance + * (e.g. a government or China account, which use different URLs). + * Defaults to "login.microsoftonline.com" + */ + identityPlatformEndpoint?: string | undefined; + } + | { + /** + * If present, an access token to authenticate with Azure. + */ + accessToken: string; + } + | Record; + /** Configuration options for using 'gcp' as your KMS provider */ + gcp?: + | { + /** The service account email to authenticate */ + email: string; + /** A PKCS#8 encrypted key. This can either be a base64 string or a binary representation */ + privateKey: string | Buffer; + /** + * If present, a host with optional port. E.g. "example.com" or "example.com:443". + * Defaults to "oauth2.googleapis.com" + */ + endpoint?: string | undefined; + } + | { + /** + * If present, an access token to authenticate with GCP. + */ + accessToken: string; + } + | Record; + /** + * Configuration options for using 'kmip' as your KMS provider + */ + kmip?: { + /** + * The output endpoint string. + * The endpoint consists of a hostname and port separated by a colon. + * E.g. "example.com:123". A port is always present. + */ + endpoint?: string; + }; + }; + /** + * A map of namespaces to a local JSON schema for encryption + * + * **NOTE**: Supplying options.schemaMap provides more security than relying on JSON Schemas obtained from the server. + * It protects against a malicious server advertising a false JSON Schema, which could trick the client into sending decrypted data that should be encrypted. + * Schemas supplied in the schemaMap only apply to configuring automatic encryption for Client-Side Field Level Encryption. + * Other validation rules in the JSON schema will not be enforced by the driver and will result in an error. + */ + schemaMap?: Document; + /** Supply a schema for the encrypted fields in the document */ + encryptedFieldsMap?: Document; + /** Allows the user to bypass auto encryption, maintaining implicit decryption */ + bypassAutoEncryption?: boolean; + /** Allows users to bypass query analysis */ + bypassQueryAnalysis?: boolean; + options?: { + /** An optional hook to catch logging messages from the underlying encryption engine */ + logger?: (level: AutoEncryptionLoggerLevel, message: string) => void; + }; + extraOptions?: { + /** + * A local process the driver communicates with to determine how to encrypt values in a command. + * Defaults to "mongodb://%2Fvar%2Fmongocryptd.sock" if domain sockets are available or "mongodb://localhost:27020" otherwise + */ + mongocryptdURI?: string; + /** If true, autoEncryption will not attempt to spawn a mongocryptd before connecting */ + mongocryptdBypassSpawn?: boolean; + /** The path to the mongocryptd executable on the system */ + mongocryptdSpawnPath?: string; + /** Command line arguments to use when auto-spawning a mongocryptd */ + mongocryptdSpawnArgs?: string[]; + /** + * Full path to a MongoDB Crypt shared library to be used (instead of mongocryptd). + * + * This needs to be the path to the file itself, not a directory. + * It can be an absolute or relative path. If the path is relative and + * its first component is `$ORIGIN`, it will be replaced by the directory + * containing the mongodb-client-encryption native addon file. Otherwise, + * the path will be interpreted relative to the current working directory. + * + * Currently, loading different MongoDB Crypt shared library files from different + * MongoClients in the same process is not supported. + * + * If this option is provided and no MongoDB Crypt shared library could be loaded + * from the specified location, creating the MongoClient will fail. + * + * If this option is not provided and `cryptSharedLibRequired` is not specified, + * the AutoEncrypter will attempt to spawn and/or use mongocryptd according + * to the mongocryptd-specific `extraOptions` options. + * + * Specifying a path prevents mongocryptd from being used as a fallback. + * + * Requires the MongoDB Crypt shared library, available in MongoDB 6.0 or higher. + */ + cryptSharedLibPath?: string; + /** + * If specified, never use mongocryptd and instead fail when the MongoDB Crypt + * shared library could not be loaded. + * + * This is always true when `cryptSharedLibPath` is specified. + * + * Requires the MongoDB Crypt shared library, available in MongoDB 6.0 or higher. + */ + cryptSharedLibRequired?: boolean; + /** + * Search paths for a MongoDB Crypt shared library to be used (instead of mongocryptd) + * Only for driver testing! + * @internal + */ + cryptSharedLibSearchPaths?: string[]; + }; + proxyOptions?: ProxyOptions; + /** The TLS options to use connecting to the KMS provider */ + tlsOptions?: CSFLEKMSTlsOptions; +} + +/** + * @public + * + * Extra options related to the mongocryptd process + * \* _Available in MongoDB 6.0 or higher._ + */ +export type AutoEncryptionExtraOptions = NonNullable; + +/** @public */ +export const AutoEncryptionLoggerLevel = Object.freeze({ + FatalError: 0, + Error: 1, + Warning: 2, + Info: 3, + Trace: 4 +} as const); + +/** + * @public + * The level of severity of the log message + * + * | Value | Level | + * |-------|-------| + * | 0 | Fatal Error | + * | 1 | Error | + * | 2 | Warning | + * | 3 | Info | + * | 4 | Trace | + */ +export type AutoEncryptionLoggerLevel = + (typeof AutoEncryptionLoggerLevel)[keyof typeof AutoEncryptionLoggerLevel]; + +// Typescript errors if we index objects with `Symbol.for(...)`, so +// to avoid TS errors we pull them out into variables. Then we can type +// the objects (and class) that we expect to see them on and prevent TS +// errors. +/** @internal */ +const kDecorateResult = Symbol.for('@@mdb.decorateDecryptionResult'); +/** @internal */ +const kDecoratedKeys = Symbol.for('@@mdb.decryptedKeys'); + +/** + * @internal An internal class to be used by the driver for auto encryption + * **NOTE**: Not meant to be instantiated directly, this is for internal use only. + */ +export class AutoEncrypter implements StateMachineExecutable { + _client: MongoClient; + _bypassEncryption: boolean; + _keyVaultNamespace: string; + _keyVaultClient: MongoClient; + _metaDataClient: MongoClient; + _proxyOptions: ProxyOptions; + _tlsOptions: CSFLEKMSTlsOptions; + _kmsProviders: KMSProviders; + _bypassMongocryptdAndCryptShared: boolean; + _contextCounter: number; + + _mongocryptdManager?: MongocryptdManager; + _mongocryptdClient?: MongoClient; + + /** @internal */ + _mongocrypt: MongoCrypt; + + /** + * Used by devtools to enable decorating decryption results. + * + * When set and enabled, `decrypt` will automatically recursively + * traverse a decrypted document and if a field has been decrypted, + * it will mark it as decrypted. Compass uses this to determine which + * fields were decrypted. + */ + [kDecorateResult] = false; + + /** @internal */ + static getMongoCrypt(): MongoCryptConstructor { + const encryption = getMongoDBClientEncryption(); + if ('kModuleError' in encryption) { + throw encryption.kModuleError; + } + return encryption.MongoCrypt; + } + + /** + * Create an AutoEncrypter + * + * **Note**: Do not instantiate this class directly. Rather, supply the relevant options to a MongoClient + * + * **Note**: Supplying `options.schemaMap` provides more security than relying on JSON Schemas obtained from the server. + * It protects against a malicious server advertising a false JSON Schema, which could trick the client into sending unencrypted data that should be encrypted. + * Schemas supplied in the schemaMap only apply to configuring automatic encryption for Client-Side Field Level Encryption. + * Other validation rules in the JSON schema will not be enforced by the driver and will result in an error. + * + * @example Create an AutoEncrypter that makes use of mongocryptd + * ```ts + * // Enabling autoEncryption via a MongoClient using mongocryptd + * const { MongoClient } = require('mongodb'); + * const client = new MongoClient(URL, { + * autoEncryption: { + * kmsProviders: { + * aws: { + * accessKeyId: AWS_ACCESS_KEY, + * secretAccessKey: AWS_SECRET_KEY + * } + * } + * } + * }); + * ``` + * + * await client.connect(); + * // From here on, the client will be encrypting / decrypting automatically + * @example Create an AutoEncrypter that makes use of libmongocrypt's CSFLE shared library + * ```ts + * // Enabling autoEncryption via a MongoClient using CSFLE shared library + * const { MongoClient } = require('mongodb'); + * const client = new MongoClient(URL, { + * autoEncryption: { + * kmsProviders: { + * aws: {} + * }, + * extraOptions: { + * cryptSharedLibPath: '/path/to/local/crypt/shared/lib', + * cryptSharedLibRequired: true + * } + * } + * }); + * ``` + * + * await client.connect(); + * // From here on, the client will be encrypting / decrypting automatically + */ + constructor(client: MongoClient, options: AutoEncryptionOptions) { + this._client = client; + this._bypassEncryption = options.bypassAutoEncryption === true; + + this._keyVaultNamespace = options.keyVaultNamespace || 'admin.datakeys'; + this._keyVaultClient = options.keyVaultClient || client; + this._metaDataClient = options.metadataClient || client; + this._proxyOptions = options.proxyOptions || {}; + this._tlsOptions = options.tlsOptions || {}; + this._kmsProviders = options.kmsProviders || {}; + + const mongoCryptOptions: MongoCryptOptions = { + cryptoCallbacks + }; + if (options.schemaMap) { + mongoCryptOptions.schemaMap = Buffer.isBuffer(options.schemaMap) + ? options.schemaMap + : (serialize(options.schemaMap) as Buffer); + } + + if (options.encryptedFieldsMap) { + mongoCryptOptions.encryptedFieldsMap = Buffer.isBuffer(options.encryptedFieldsMap) + ? options.encryptedFieldsMap + : (serialize(options.encryptedFieldsMap) as Buffer); + } + + mongoCryptOptions.kmsProviders = !Buffer.isBuffer(this._kmsProviders) + ? (serialize(this._kmsProviders) as Buffer) + : this._kmsProviders; + + if (options.options?.logger) { + mongoCryptOptions.logger = options.options.logger; + } + + if (options.extraOptions && options.extraOptions.cryptSharedLibPath) { + mongoCryptOptions.cryptSharedLibPath = options.extraOptions.cryptSharedLibPath; + } + + if (options.bypassQueryAnalysis) { + mongoCryptOptions.bypassQueryAnalysis = options.bypassQueryAnalysis; + } + + this._bypassMongocryptdAndCryptShared = this._bypassEncryption || !!options.bypassQueryAnalysis; + + if (options.extraOptions && options.extraOptions.cryptSharedLibSearchPaths) { + // Only for driver testing + mongoCryptOptions.cryptSharedLibSearchPaths = options.extraOptions.cryptSharedLibSearchPaths; + } else if (!this._bypassMongocryptdAndCryptShared) { + mongoCryptOptions.cryptSharedLibSearchPaths = ['$SYSTEM']; + } + + const MongoCrypt = AutoEncrypter.getMongoCrypt(); + this._mongocrypt = new MongoCrypt(mongoCryptOptions); + this._contextCounter = 0; + + if ( + options.extraOptions && + options.extraOptions.cryptSharedLibRequired && + !this.cryptSharedLibVersionInfo + ) { + throw new MongoCryptInvalidArgumentError( + '`cryptSharedLibRequired` set but no crypt_shared library loaded' + ); + } + + // Only instantiate mongocryptd manager/client once we know for sure + // that we are not using the CSFLE shared library. + if (!this._bypassMongocryptdAndCryptShared && !this.cryptSharedLibVersionInfo) { + this._mongocryptdManager = new MongocryptdManager(options.extraOptions); + const clientOptions: MongoClientOptions = { + serverSelectionTimeoutMS: 10000 + }; + + if (options.extraOptions == null || typeof options.extraOptions.mongocryptdURI !== 'string') { + clientOptions.family = 4; + } + + this._mongocryptdClient = new MongoClient(this._mongocryptdManager.uri, clientOptions); + } + } + + /** + * Initializes the auto encrypter by spawning a mongocryptd and connecting to it. + * + * This function is a no-op when bypassSpawn is set or the crypt shared library is used. + */ + init(callback: Callback) { + if (this._bypassMongocryptdAndCryptShared || this.cryptSharedLibVersionInfo) { + return callback(); + } + if (!this._mongocryptdManager) { + return callback( + new MongoRuntimeError( + 'Reached impossible state: mongocryptdManager is undefined when neither bypassSpawn nor the shared lib are specified.' + ) + ); + } + if (!this._mongocryptdClient) { + return callback( + new MongoRuntimeError( + 'Reached impossible state: mongocryptdClient is undefined when neither bypassSpawn nor the shared lib are specified.' + ) + ); + } + const _callback = (err?: AnyError, res?: MongoClient) => { + if ( + err && + err.message && + (err.message.match(/timed out after/) || err.message.match(/ENOTFOUND/)) + ) { + callback( + new MongoRuntimeError( + 'Unable to connect to `mongocryptd`, please make sure it is running or in your PATH for auto-spawn' + ) + ); + return; + } + + callback(err, res); + }; + + if (this._mongocryptdManager.bypassSpawn) { + this._mongocryptdClient.connect().then( + result => { + return _callback(undefined, result); + }, + error => { + _callback(error, undefined); + } + ); + return; + } + + this._mongocryptdManager.spawn(() => { + if (!this._mongocryptdClient) { + return callback( + new MongoRuntimeError( + 'Reached impossible state: mongocryptdClient is undefined after spawning libmongocrypt.' + ) + ); + } + this._mongocryptdClient.connect().then( + result => { + return _callback(undefined, result); + }, + error => { + _callback(error, undefined); + } + ); + }); + } + + /** + * Cleans up the `_mongocryptdClient`, if present. + */ + teardown(force: boolean, callback: Callback) { + if (this._mongocryptdClient) { + this._mongocryptdClient.close(force).then( + result => { + return callback(undefined, result); + }, + error => { + callback(error); + } + ); + } else { + callback(); + } + } + + encrypt(ns: string, cmd: Document, callback: Callback): void; + encrypt( + ns: string, + cmd: Document, + options: CommandOptions, + callback: Callback + ): void; + /** + * Encrypt a command for a given namespace. + */ + encrypt( + ns: string, + cmd: Document, + options?: CommandOptions | Callback, + callback?: Callback + ) { + callback = typeof options === 'function' ? options : callback; + + if (callback == null) { + throw new MongoCryptInvalidArgumentError('Callback must be provided'); + } + + options = typeof options === 'function' ? {} : options; + + // If `bypassAutoEncryption` has been specified, don't encrypt + if (this._bypassEncryption) { + callback(undefined, cmd); + return; + } + + const commandBuffer = Buffer.isBuffer(cmd) ? cmd : serialize(cmd, options); + + let context; + try { + context = this._mongocrypt.makeEncryptionContext( + MongoDBCollectionNamespace.fromString(ns).db, + commandBuffer + ); + } catch (err) { + callback(err, undefined); + return; + } + + context.id = this._contextCounter++; + context.ns = ns; + context.document = cmd; + + const stateMachine = new StateMachine({ + promoteValues: false, + promoteLongs: false, + proxyOptions: this._proxyOptions, + tlsOptions: this._tlsOptions + }); + stateMachine.execute(this, context, callback); + } + + /** + * Decrypt a command response + */ + decrypt( + response: Uint8Array, + options: CommandOptions | Callback, + callback?: Callback + ) { + callback = typeof options === 'function' ? options : callback; + + if (callback == null) { + throw new MongoCryptInvalidArgumentError('Callback must be provided'); + } + + options = typeof options === 'function' ? {} : options; + + const buffer = Buffer.isBuffer(response) ? response : serialize(response, options); + + let context; + try { + context = this._mongocrypt.makeDecryptionContext(buffer); + } catch (err) { + callback(err, undefined); + return; + } + + context.id = this._contextCounter++; + + const stateMachine = new StateMachine({ + ...options, + proxyOptions: this._proxyOptions, + tlsOptions: this._tlsOptions + }); + + const decorateResult = this[kDecorateResult]; + stateMachine.execute(this, context, function (error?: Error, result?: Document) { + // Only for testing/internal usage + if (!error && result && decorateResult) { + const error = decorateDecryptionResult(result, response); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (error) return callback!(error); + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + callback!(error, result); + }); + } + + /** + * 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. + */ + async askForKMSCredentials(): Promise { + return refreshKMSCredentials(this._kmsProviders); + } + + /** + * Return the current libmongocrypt's CSFLE shared library version + * as `{ version: bigint, versionStr: string }`, or `null` if no CSFLE + * shared library was loaded. + */ + get cryptSharedLibVersionInfo(): { version: bigint; versionStr: string } | null { + return this._mongocrypt.cryptSharedLibVersionInfo; + } + + static get libmongocryptVersion(): string { + return AutoEncrypter.getMongoCrypt().libmongocryptVersion; + } +} + +/** + * Recurse through the (identically-shaped) `decrypted` and `original` + * objects and attach a `decryptedKeys` property on each sub-object that + * contained encrypted fields. Because we only call this on BSON responses, + * we do not need to worry about circular references. + * + * @internal + */ +function decorateDecryptionResult( + decrypted: Document & { [kDecoratedKeys]?: Array }, + original: Document, + isTopLevelDecorateCall = true +): Error | void { + if (isTopLevelDecorateCall) { + // The original value could have been either a JS object or a BSON buffer + if (Buffer.isBuffer(original)) { + original = deserialize(original); + } + if (Buffer.isBuffer(decrypted)) { + return new MongoRuntimeError('Expected result of decryption to be deserialized BSON object'); + } + } + + if (!decrypted || typeof decrypted !== 'object') return; + for (const k of Object.keys(decrypted)) { + const originalValue = original[k]; + + // An object was decrypted by libmongocrypt if and only if it was + // a BSON Binary object with subtype 6. + if (originalValue && originalValue._bsontype === 'Binary' && originalValue.sub_type === 6) { + if (!decrypted[kDecoratedKeys]) { + Object.defineProperty(decrypted, kDecoratedKeys, { + value: [], + configurable: true, + enumerable: false, + writable: false + }); + } + // this is defined in the preceeding if-statement + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + decrypted[kDecoratedKeys]!.push(k); + // Do not recurse into this decrypted value. It could be a subdocument/array, + // in which case there is no original value associated with its subfields. + continue; + } + + decorateDecryptionResult(decrypted[k], originalValue, false); + } +} diff --git a/src/client-side-encryption/buffer_pool.js b/src/client-side-encryption/buffer_pool.js deleted file mode 100644 index a1164ab1a31..00000000000 --- a/src/client-side-encryption/buffer_pool.js +++ /dev/null @@ -1,119 +0,0 @@ -/** - * @internal - * @ignore - * */ -const kBuffers = Symbol('buffers'); -/** - * @internal - * @ignore - * - * */ -const kLength = Symbol('length'); - -/** - * A pool of Buffers which allow you to read them as if they were one - * @internal - * @ignore - */ -export class BufferPool { - // [kBuffers]: Buffer[]; - // [kLength]: number; - - constructor() { - this[kBuffers] = []; - this[kLength] = 0; - } - - get length() { - return this[kLength]; - } - - /** - * Adds a buffer to the internal buffer pool list - * @param {Buffer} buffer - buffer to append to the pool - * @returns {void} - */ - append(buffer) { - this[kBuffers].push(buffer); - this[kLength] += buffer.length; - } - - /** - * Returns the requested number of bytes without consuming them - * @param {number} size - the number of bytes to return from the head of the pool - * @returns {Buffer} - */ - peek(size) { - return this.read(size, false); - } - - /** - * Reads the requested number of bytes, optionally consuming them - * @param {number} size - the number of bytes to return from the head of the pool - * @param {boolean} [consume] - whether the bytes returned should be removed, defaults to true - * @returns {Buffer} - */ - read(size, consume = true) { - if (typeof size !== 'number' || size < 0) { - throw new Error('Argument "size" must be a non-negative number'); - } - - if (size > this[kLength]) { - return Buffer.alloc(0); - } - - let result; - - // read the whole buffer - if (size === this.length) { - result = Buffer.concat(this[kBuffers]); - - if (consume) { - this[kBuffers] = []; - this[kLength] = 0; - } - } - - // size is within first buffer, no need to concat - else if (size <= this[kBuffers][0].length) { - result = this[kBuffers][0].slice(0, size); - if (consume) { - this[kBuffers][0] = this[kBuffers][0].slice(size); - this[kLength] -= size; - } - } - - // size is beyond first buffer, need to track and copy - else { - result = Buffer.allocUnsafe(size); - - let idx; - let offset = 0; - let bytesToCopy = size; - for (idx = 0; idx < this[kBuffers].length; ++idx) { - let bytesCopied; - if (bytesToCopy > this[kBuffers][idx].length) { - bytesCopied = this[kBuffers][idx].copy(result, offset, 0); - offset += bytesCopied; - } else { - bytesCopied = this[kBuffers][idx].copy(result, offset, 0, bytesToCopy); - if (consume) { - this[kBuffers][idx] = this[kBuffers][idx].slice(bytesCopied); - } - offset += bytesCopied; - break; - } - - bytesToCopy -= bytesCopied; - } - - // compact the internal buffer array - if (consume) { - this[kBuffers] = this[kBuffers].slice(idx); - this[kLength] -= size; - } - } - - return result; - } -} diff --git a/src/client-side-encryption/clientEncryption.js b/src/client-side-encryption/clientEncryption.js deleted file mode 100644 index 5c6e734a09c..00000000000 --- a/src/client-side-encryption/clientEncryption.js +++ /dev/null @@ -1,803 +0,0 @@ - -import { databaseNamespace, collectionNamespace, promiseOrCallback, maybeCallback } from './common'; -import { StateMachine } from './stateMachine'; -import { - MongoCryptCreateEncryptedCollectionError, - MongoCryptCreateDataKeyError -} from './errors'; -import { loadCredentials } from './providers/index'; -import * as cryptoCallbacks from './cryptoCallbacks'; -import { promisify } from 'util'; -import { serialize, deserialize } from '../bson'; -import { getMongoDBClientEncryption } from '../deps'; - -/** @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 - * @property {string} [aws.accessKeyId] The access key used for the AWS KMS provider - * @property {string} [aws.secretAccessKey] The secret access key used for the AWS KMS provider - * @property {object} [local] Configuration options for using 'local' as your KMS provider - * @property {Buffer} [local.key] The master key used to encrypt/decrypt data keys. A 96-byte long Buffer. - * @property {object} [azure] Configuration options for using 'azure' as your KMS provider - * @property {string} [azure.tenantId] The tenant ID identifies the organization for the account - * @property {string} [azure.clientId] The client ID to authenticate a registered application - * @property {string} [azure.clientSecret] The client secret to authenticate a registered application - * @property {string} [azure.identityPlatformEndpoint] If present, a host with optional port. E.g. "example.com" or "example.com:443". This is optional, and only needed if customer is using a non-commercial Azure instance (e.g. a government or China account, which use different URLs). Defaults to "login.microsoftonline.com" - * @property {object} [gcp] Configuration options for using 'gcp' as your KMS provider - * @property {string} [gcp.email] The service account email to authenticate - * @property {string|Binary} [gcp.privateKey] A PKCS#8 encrypted key. This can either be a base64 string or a binary representation - * @property {string} [gcp.endpoint] If present, a host with optional port. E.g. "example.com" or "example.com:443". Defaults to "oauth2.googleapis.com" - */ - -/** - * @typedef {object} DataKey A data key as stored in the database. - * @property {UUID} _id A unique identifier for the key. - * @property {number} version A numeric identifier for the schema version of this document. Implicitly 0 if unset. - * @property {string[]} [keyAltNames] Alternate names to search for keys by. Used for a per-document key scenario in support of GDPR scenarios. - * @property {Binary} keyMaterial Encrypted data key material, BinData type General. - * @property {Date} creationDate The datetime the wrapped data key material was imported into the Key Database. - * @property {Date} updateDate The datetime the wrapped data key material was last modified. On initial import, this value will be set to creationDate. - * @property {number} status 0 = enabled, 1 = disabled - * @property {object} masterKey the encrypted master key - */ - -/** - * @typedef {string} KmsProvider A string containing the name of a kms provider. Valid options are 'aws', 'azure', 'gcp', 'kmip', or 'local' - */ - -/** - * @typedef {object} ClientSession The ClientSession class from the MongoDB Node driver (see https://mongodb.github.io/node-mongodb-native/4.8/classes/ClientSession.html) - */ - -/** - * @typedef {object} DeleteResult The result of a delete operation from the MongoDB Node driver (see https://mongodb.github.io/node-mongodb-native/4.8/interfaces/DeleteResult.html) - * @property {boolean} acknowledged Indicates whether this write result was acknowledged. If not, then all other members of this result will be undefined. - * @property {number} deletedCount The number of documents that were deleted - */ - -/** - * @typedef {object} BulkWriteResult The BulkWriteResult class from the MongoDB Node driver (https://mongodb.github.io/node-mongodb-native/4.8/classes/BulkWriteResult.html) - */ - -/** - * @typedef {object} FindCursor The FindCursor class from the MongoDB Node driver (see https://mongodb.github.io/node-mongodb-native/4.8/classes/FindCursor.html) - */ - -/** - * The public interface for explicit in-use encryption - */ -export class ClientEncryption { - /** - * Create a new encryption instance - * - * @param {MongoClient} client The client used for encryption - * @param {object} options Additional settings - * @param {string} options.keyVaultNamespace The namespace of the key vault, used to store encryption keys - * @param {object} options.tlsOptions An object that maps KMS provider names to TLS options. - * @param {MongoClient} [options.keyVaultClient] A `MongoClient` used to fetch keys from a key vault. Defaults to `client` - * @param {KMSProviders} [options.kmsProviders] options for specific KMS providers to use - * - * @example - * new ClientEncryption(mongoClient, { - * keyVaultNamespace: 'client.encryption', - * kmsProviders: { - * local: { - * key: masterKey // The master key used for encryption/decryption. A 96-byte long Buffer - * } - * } - * }); - * - * @example - * new ClientEncryption(mongoClient, { - * keyVaultNamespace: 'client.encryption', - * kmsProviders: { - * aws: { - * accessKeyId: AWS_ACCESS_KEY, - * secretAccessKey: AWS_SECRET_KEY - * } - * } - * }); - */ - constructor(client, options) { - this._client = client; - this._proxyOptions = options.proxyOptions; - this._tlsOptions = options.tlsOptions; - this._kmsProviders = options.kmsProviders || {}; - - if (options.keyVaultNamespace == null) { - throw new TypeError('Missing required option `keyVaultNamespace`'); - } - - const mongoCryptOptions = { ...options, cryptoCallbacks }; - - mongoCryptOptions.kmsProviders = !Buffer.isBuffer(this._kmsProviders) - ? serialize(this._kmsProviders) - : this._kmsProviders; - - this._onKmsProviderRefresh = options.onKmsProviderRefresh; - this._keyVaultNamespace = options.keyVaultNamespace; - this._keyVaultClient = options.keyVaultClient || client; - const { MongoCrypt } = getMongoDBClientEncryption(); - this._mongoCrypt = new MongoCrypt(mongoCryptOptions); - } - - /** - * @typedef {Binary} ClientEncryptionDataKeyId - * The id of an existing dataKey. Is a bson Binary value. - * Can be used for {@link ClientEncryption.encrypt}, and can be used to directly - * query for the data key itself against the key vault namespace. - */ - - /** - * @callback ClientEncryptionCreateDataKeyCallback - * @param {Error} [error] If present, indicates an error that occurred in the creation of the data key - * @param {ClientEncryption~dataKeyId} [dataKeyId] If present, returns the id of the created data key - */ - - /** - * @typedef {object} AWSEncryptionKeyOptions Configuration options for making an AWS encryption key - * @property {string} region The AWS region of the KMS - * @property {string} key The Amazon Resource Name (ARN) to the AWS customer master key (CMK) - * @property {string} [endpoint] An alternate host to send KMS requests to. May include port number - */ - - /** - * @typedef {object} GCPEncryptionKeyOptions Configuration options for making a GCP encryption key - * @property {string} projectId GCP project id - * @property {string} location Location name (e.g. "global") - * @property {string} keyRing Key ring name - * @property {string} keyName Key name - * @property {string} [keyVersion] Key version - * @property {string} [endpoint] KMS URL, defaults to `https://www.googleapis.com/auth/cloudkms` - */ - - /** - * @typedef {object} AzureEncryptionKeyOptions Configuration options for making an Azure encryption key - * @property {string} keyName Key name - * @property {string} keyVaultEndpoint Key vault URL, typically `.vault.azure.net` - * @property {string} [keyVersion] Key version - */ - - /** - * Creates a data key used for explicit encryption and inserts it into the key vault namespace - * - * @param {string} provider The KMS provider used for this data key. Must be `'aws'`, `'azure'`, `'gcp'`, or `'local'` - * @param {object} [options] Options for creating the data key - * @param {AWSEncryptionKeyOptions|AzureEncryptionKeyOptions|GCPEncryptionKeyOptions} [options.masterKey] Idenfities a new KMS-specific key used to encrypt the new data key - * @param {string[]} [options.keyAltNames] An optional list of string alternate names used to reference a key. If a key is created with alternate names, then encryption may refer to the key by the unique alternate name instead of by _id. - * @param {ClientEncryptionCreateDataKeyCallback} [callback] Optional callback to invoke when key is created - * @returns {Promise|void} If no callback is provided, returns a Promise that either resolves with {@link ClientEncryption~dataKeyId the id of the created data key}, or rejects with an error. If a callback is provided, returns nothing. - * @example - * // Using callbacks to create a local key - * clientEncryption.createDataKey('local', (err, dataKey) => { - * if (err) { - * // This means creating the key failed. - * } else { - * // key creation succeeded - * } - * }); - * - * @example - * // Using async/await to create a local key - * const dataKeyId = await clientEncryption.createDataKey('local'); - * - * @example - * // Using async/await to create an aws key - * const dataKeyId = await clientEncryption.createDataKey('aws', { - * masterKey: { - * region: 'us-east-1', - * key: 'xxxxxxxxxxxxxx' // CMK ARN here - * } - * }); - * - * @example - * // Using async/await to create an aws key with a keyAltName - * const dataKeyId = await clientEncryption.createDataKey('aws', { - * masterKey: { - * region: 'us-east-1', - * key: 'xxxxxxxxxxxxxx' // CMK ARN here - * }, - * keyAltNames: [ 'mySpecialKey' ] - * }); - */ - createDataKey(provider, options, callback) { - if (typeof options === 'function') { - callback = options; - options = {}; - } - if (options == null) { - options = {}; - } - - const dataKey = Object.assign({ provider }, options.masterKey); - - if (options.keyAltNames && !Array.isArray(options.keyAltNames)) { - throw new TypeError( - `Option "keyAltNames" must be an array of strings, but was of type ${typeof options.keyAltNames}.` - ); - } - - let keyAltNames = undefined; - if (options.keyAltNames && options.keyAltNames.length > 0) { - keyAltNames = options.keyAltNames.map((keyAltName, i) => { - if (typeof keyAltName !== 'string') { - throw new TypeError( - `Option "keyAltNames" must be an array of strings, but item at index ${i} was of type ${typeof keyAltName}` - ); - } - - return serialize({ keyAltName }); - }); - } - - let keyMaterial = undefined; - if (options.keyMaterial) { - keyMaterial = serialize({ keyMaterial: options.keyMaterial }); - } - - const dataKeyBson = serialize(dataKey); - const context = this._mongoCrypt.makeDataKeyContext(dataKeyBson, { - keyAltNames, - keyMaterial - }); - const stateMachine = new StateMachine({ - proxyOptions: this._proxyOptions, - tlsOptions: this._tlsOptions - }); - - return promiseOrCallback(callback, cb => { - stateMachine.execute(this, context, (err, dataKey) => { - if (err) { - cb(err, null); - return; - } - - const dbName = databaseNamespace(this._keyVaultNamespace); - const collectionName = collectionNamespace(this._keyVaultNamespace); - - this._keyVaultClient - .db(dbName) - .collection(collectionName) - .insertOne(dataKey, { writeConcern: { w: 'majority' } }) - .then( - result => { - return cb(null, result.insertedId); - }, - err => { - cb(err, null); - } - ); - }); - }); - } - - /** - * @typedef {object} RewrapManyDataKeyResult - * @property {BulkWriteResult} [bulkWriteResult] An optional BulkWriteResult, if any keys were matched and attempted to be re-wrapped. - */ - - /** - * Searches the keyvault for any data keys matching the provided filter. If there are matches, rewrapManyDataKey then attempts to re-wrap the data keys using the provided options. - * - * If no matches are found, then no bulk write is performed. - * - * @param {object} filter A valid MongoDB filter. Any documents matching this filter will be re-wrapped. - * @param {object} [options] - * @param {KmsProvider} options.provider The KMS provider to use when re-wrapping the data keys. - * @param {AWSEncryptionKeyOptions | AzureEncryptionKeyOptions | GCPEncryptionKeyOptions} [options.masterKey] - * @returns {Promise} - * - * @example - * // rewrapping all data data keys (using a filter that matches all documents) - * const filter = {}; - * - * const result = await clientEncryption.rewrapManyDataKey(filter); - * if (result.bulkWriteResult != null) { - * // keys were re-wrapped, results will be available in the bulkWrite object. - * } - * - * @example - * // attempting to rewrap all data keys with no matches - * const filter = { _id: new Binary() } // assume _id matches no documents in the database - * const result = await clientEncryption.rewrapManyDataKey(filter); - * - * if (result.bulkWriteResult == null) { - * // no keys matched, `bulkWriteResult` does not exist on the result object - * } - */ - async rewrapManyDataKey(filter, options) { - let keyEncryptionKeyBson = undefined; - if (options) { - const keyEncryptionKey = Object.assign({ provider: options.provider }, options.masterKey); - keyEncryptionKeyBson = serialize(keyEncryptionKey); - } else { - // Always make sure `options` is an object below. - options = {}; - } - const filterBson = serialize(filter); - const context = this._mongoCrypt.makeRewrapManyDataKeyContext( - filterBson, - keyEncryptionKeyBson - ); - const stateMachine = new StateMachine({ - proxyOptions: this._proxyOptions, - tlsOptions: this._tlsOptions - }); - - const execute = promisify(stateMachine.execute.bind(stateMachine)); - - const dataKey = await execute(this, context); - if (!dataKey || dataKey.v.length === 0) { - return {}; - } - - const dbName = databaseNamespace(this._keyVaultNamespace); - const collectionName = collectionNamespace(this._keyVaultNamespace); - const replacements = dataKey.v.map(key => ({ - updateOne: { - filter: { _id: key._id }, - update: { - $set: { - masterKey: key.masterKey, - keyMaterial: key.keyMaterial - }, - $currentDate: { - updateDate: true - } - } - } - })); - - const result = await this._keyVaultClient - .db(dbName) - .collection(collectionName) - .bulkWrite(replacements, { - writeConcern: { w: 'majority' } - }); - - return { bulkWriteResult: result }; - } - - /** - * Deletes the key with the provided id from the keyvault, if it exists. - * - * @param {ClientEncryptionDataKeyId} _id - the id of the document to delete. - * @returns {Promise} Returns a promise that either resolves to a {@link DeleteResult} or rejects with an error. - * - * @example - * // delete a key by _id - * const id = new Binary(); // id is a bson binary subtype 4 object - * const { deletedCount } = await clientEncryption.deleteKey(id); - * - * if (deletedCount != null && deletedCount > 0) { - * // successful deletion - * } - * - */ - async deleteKey(_id) { - const dbName = databaseNamespace(this._keyVaultNamespace); - const collectionName = collectionNamespace(this._keyVaultNamespace); - return await this._keyVaultClient - .db(dbName) - .collection(collectionName) - .deleteOne({ _id }, { writeConcern: { w: 'majority' } }); - } - - /** - * Finds all the keys currently stored in the keyvault. - * - * This method will not throw. - * - * @returns {FindCursor} a FindCursor over all keys in the keyvault. - * @example - * // fetching all keys - * const keys = await clientEncryption.getKeys().toArray(); - */ - getKeys() { - const dbName = databaseNamespace(this._keyVaultNamespace); - const collectionName = collectionNamespace(this._keyVaultNamespace); - return this._keyVaultClient - .db(dbName) - .collection(collectionName) - .find({}, { readConcern: { level: 'majority' } }); - } - - /** - * Finds a key in the keyvault with the specified _id. - * - * @param {ClientEncryptionDataKeyId} _id - the id of the document to delete. - * @returns {Promise} Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents - * match the id. The promise rejects with an error if an error is thrown. - * @example - * // getting a key by id - * const id = new Binary(); // id is a bson binary subtype 4 object - * const key = await clientEncryption.getKey(id); - * if (!key) { - * // key is null if there was no matching key - * } - */ - async getKey(_id) { - const dbName = databaseNamespace(this._keyVaultNamespace); - const collectionName = collectionNamespace(this._keyVaultNamespace); - return await this._keyVaultClient - .db(dbName) - .collection(collectionName) - .findOne({ _id }, { readConcern: { level: 'majority' } }); - } - - /** - * Finds a key in the keyvault which has the specified keyAltName. - * - * @param {string} keyAltName - a keyAltName to search for a key - * @returns {Promise} Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents - * match the keyAltName. The promise rejects with an error if an error is thrown. - * @example - * // get a key by alt name - * const keyAltName = 'keyAltName'; - * const key = await clientEncryption.getKeyByAltName(keyAltName); - * if (!key) { - * // key is null if there is no matching key - * } - */ - async getKeyByAltName(keyAltName) { - const dbName = databaseNamespace(this._keyVaultNamespace); - const collectionName = collectionNamespace(this._keyVaultNamespace); - return await this._keyVaultClient - .db(dbName) - .collection(collectionName) - .findOne({ keyAltNames: keyAltName }, { readConcern: { level: 'majority' } }); - } - - /** - * Adds a keyAltName to a key identified by the provided _id. - * - * This method resolves to/returns the *old* key value (prior to adding the new altKeyName). - * - * @param {ClientEncryptionDataKeyId} _id The id of the document to update. - * @param {string} keyAltName - a keyAltName to search for a key - * @returns {Promise} Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents - * match the id. The promise rejects with an error if an error is thrown. - * @example - * // adding an keyAltName to a data key - * const id = new Binary(); // id is a bson binary subtype 4 object - * const keyAltName = 'keyAltName'; - * const oldKey = await clientEncryption.addKeyAltName(id, keyAltName); - * if (!oldKey) { - * // null is returned if there is no matching document with an id matching the supplied id - * } - */ - async addKeyAltName(_id, keyAltName) { - const dbName = databaseNamespace(this._keyVaultNamespace); - const collectionName = collectionNamespace(this._keyVaultNamespace); - const { value } = await this._keyVaultClient - .db(dbName) - .collection(collectionName) - .findOneAndUpdate( - { _id }, - { $addToSet: { keyAltNames: keyAltName } }, - { writeConcern: { w: 'majority' }, returnDocument: 'before' } - ); - - return value; - } - - /** - * Adds a keyAltName to a key identified by the provided _id. - * - * This method resolves to/returns the *old* key value (prior to removing the new altKeyName). - * - * If the removed keyAltName is the last keyAltName for that key, the `altKeyNames` property is unset from the document. - * - * @param {ClientEncryptionDataKeyId} _id The id of the document to update. - * @param {string} keyAltName - a keyAltName to search for a key - * @returns {Promise} Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents - * match the id. The promise rejects with an error if an error is thrown. - * @example - * // removing a key alt name from a data key - * const id = new Binary(); // id is a bson binary subtype 4 object - * const keyAltName = 'keyAltName'; - * const oldKey = await clientEncryption.removeKeyAltName(id, keyAltName); - * - * if (!oldKey) { - * // null is returned if there is no matching document with an id matching the supplied id - * } - */ - async removeKeyAltName(_id, keyAltName) { - const dbName = databaseNamespace(this._keyVaultNamespace); - const collectionName = collectionNamespace(this._keyVaultNamespace); - const pipeline = [ - { - $set: { - keyAltNames: { - $cond: [ - { - $eq: ['$keyAltNames', [keyAltName]] - }, - '$$REMOVE', - { - $filter: { - input: '$keyAltNames', - cond: { - $ne: ['$$this', keyAltName] - } - } - } - ] - } - } - } - ]; - const { value } = await this._keyVaultClient - .db(dbName) - .collection(collectionName) - .findOneAndUpdate({ _id }, pipeline, { - writeConcern: { w: 'majority' }, - returnDocument: 'before' - }); - - return value; - } - - /** - * A convenience method for creating an encrypted collection. - * This method will create data keys for any encryptedFields that do not have a `keyId` defined - * and then create a new collection with the full set of encryptedFields. - * - * @template {TSchema} - Schema for the collection being created - * @param {Db} db - A Node.js driver Db object with which to create the collection - * @param {string} name - The name of the collection to be created - * @param {object} options - Options for createDataKey and for createCollection - * @param {string} options.provider - KMS provider name - * @param {AWSEncryptionKeyOptions | AzureEncryptionKeyOptions | GCPEncryptionKeyOptions} [options.masterKey] - masterKey to pass to createDataKey - * @param {CreateCollectionOptions} options.createCollectionOptions - options to pass to createCollection, must include `encryptedFields` - * @returns {Promise<{ collection: Collection, encryptedFields: Document }>} - created collection and generated encryptedFields - * @throws {MongoCryptCreateDataKeyError} - If part way through the process a createDataKey invocation fails, an error will be rejected that has the partial `encryptedFields` that were created. - * @throws {MongoCryptCreateEncryptedCollectionError} - If creating the collection fails, an error will be rejected that has the entire `encryptedFields` that were created. - */ - async createEncryptedCollection(db, name, options) { - const { - provider, - masterKey, - createCollectionOptions: { - encryptedFields: { ...encryptedFields }, - ...createCollectionOptions - } - } = options; - - if (Array.isArray(encryptedFields.fields)) { - const createDataKeyPromises = encryptedFields.fields.map(async field => - field == null || typeof field !== 'object' || field.keyId != null - ? field - : { - ...field, - keyId: await this.createDataKey(provider, { masterKey }) - } - ); - - const createDataKeyResolutions = await Promise.allSettled(createDataKeyPromises); - - encryptedFields.fields = createDataKeyResolutions.map((resolution, index) => - resolution.status === 'fulfilled' ? resolution.value : encryptedFields.fields[index] - ); - - const rejection = createDataKeyResolutions.find(({ status }) => status === 'rejected'); - if (rejection != null) { - throw new MongoCryptCreateDataKeyError({ encryptedFields, cause: rejection.reason }); - } - } - - try { - const collection = await db.createCollection(name, { - ...createCollectionOptions, - encryptedFields - }); - return { collection, encryptedFields }; - } catch (cause) { - throw new MongoCryptCreateEncryptedCollectionError({ encryptedFields, cause }); - } - } - - /** - * @callback ClientEncryptionEncryptCallback - * @param {Error} [err] If present, indicates an error that occurred in the process of encryption - * @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] - the contention factor. - * @property {'equality' | 'rangePreview'} queryType - the query type supported. only the query type `equality` is stable at this time. queryType `rangePreview` is experimental. - * @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 {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. - * - * @example - * // Encryption with callback API - * function encryptMyData(value, callback) { - * clientEncryption.createDataKey('local', (err, keyId) => { - * if (err) { - * return callback(err); - * } - * clientEncryption.encrypt(value, { keyId, algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' }, callback); - * }); - * } - * - * @example - * // Encryption with async/await api - * async function encryptMyData(value) { - * const keyId = await clientEncryption.createDataKey('local'); - * return clientEncryption.encrypt(value, { keyId, algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' }); - * } - * - * @example - * // Encryption using a keyAltName - * async function encryptMyData(value) { - * await clientEncryption.createDataKey('local', { keyAltNames: 'mySpecialKey' }); - * return clientEncryption.encrypt(value, { keyAltName: 'mySpecialKey', algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' }); - * } - */ - encrypt(value, options, callback) { - return maybeCallback(() => this._encrypt(value, false, options), callback); - } - - /** - * 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 production 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); - } - - /** - * @callback ClientEncryption~decryptCallback - * @param {Error} [err] If present, indicates an error that occurred in the process of decryption - * @param {object} [result] If present, is the decrypted result - */ - - /** - * Explicitly decrypt a provided encrypted value - * - * @param {Buffer | Binary} value An encrypted value - * @param {ClientEncryption~decryptCallback} callback Optional callback to invoke when value is decrypted - * @returns {Promise|void} If no callback is provided, returns a Promise that either resolves with the decrypted value, or rejects with an error. If a callback is provided, returns nothing. - * - * @example - * // Decrypting value with callback API - * function decryptMyValue(value, callback) { - * clientEncryption.decrypt(value, callback); - * } - * - * @example - * // Decrypting value with async/await API - * async function decryptMyValue(value) { - * return clientEncryption.decrypt(value); - * } - */ - decrypt(value, callback) { - const valueBuffer = serialize({ v: value }); - const context = this._mongoCrypt.makeExplicitDecryptionContext(valueBuffer); - - const stateMachine = new StateMachine({ - proxyOptions: this._proxyOptions, - tlsOptions: this._tlsOptions - }); - - return promiseOrCallback(callback, cb => { - stateMachine.execute(this, context, (err, result) => { - if (err) { - cb(err, null); - return; - } - - cb(null, result.v); - }); - }); - } - - /** - * 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. - */ - async askForKMSCredentials() { - return this._onKmsProviderRefresh - ? this._onKmsProviderRefresh() - : loadCredentials(this._kmsProviders); - } - - static get libmongocryptVersion() { - const { MongoCrypt } = getMongoDBClientEncryption(); - return 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 valueBuffer = 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 = serialize({ keyAltName }); - } - - if ('rangeOptions' in options) { - contextOptions.rangeOptions = serialize(options.rangeOptions); - } - - const stateMachine = new StateMachine({ - proxyOptions: this._proxyOptions, - tlsOptions: this._tlsOptions - }); - const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions); - - const result = await stateMachine.executeAsync(this, context); - return result.v; - } - - -} diff --git a/src/client-side-encryption/client_encryption.ts b/src/client-side-encryption/client_encryption.ts new file mode 100644 index 00000000000..5d06134c226 --- /dev/null +++ b/src/client-side-encryption/client_encryption.ts @@ -0,0 +1,1022 @@ +import type { + ExplicitEncryptionContextOptions, + MongoCrypt, + MongoCryptConstructor, + MongoCryptOptions +} from 'mongodb-client-encryption'; + +import { type Binary, type Document, type Long, serialize } from '../bson'; +import { type AnyBulkWriteOperation, type BulkWriteResult } from '../bulk/common'; +import { type ProxyOptions } from '../cmap/connection'; +import { type Collection } from '../collection'; +import { type FindCursor } from '../cursor/find_cursor'; +import { type Db } from '../db'; +import { getMongoDBClientEncryption } from '../deps'; +import { type MongoClient } from '../mongo_client'; +import { type Filter } from '../mongo_types'; +import { type CreateCollectionOptions } from '../operations/create_collection'; +import { type DeleteResult } from '../operations/delete'; +import { type Callback, MongoDBCollectionNamespace } from '../utils'; +import { maybeCallback, promiseOrCallback } from './common'; +import * as cryptoCallbacks from './crypto_callbacks'; +import { + MongoCryptCreateDataKeyError, + MongoCryptCreateEncryptedCollectionError, + MongoCryptInvalidArgumentError +} from './errors'; +import { type KMSProvider, type KMSProviders, refreshKMSCredentials } from './providers/index'; +import { + type CSFLEKMSTlsOptions, + StateMachine, + type StateMachineExecutable +} from './state_machine'; + +/** + * The schema for a DataKey in the key vault collection. + */ +export interface DataKey { + _id: Binary; + version?: number; + keyAltNames?: string[]; + keyMaterial: Binary; + creationDate: Date; + updateDate: Date; + status: number; + masterKey: Document; +} + +/** + * The public interface for explicit in-use encryption + */ +export class ClientEncryption implements StateMachineExecutable { + _client: MongoClient; + _keyVaultNamespace: string; + _keyVaultClient: MongoClient; + _proxyOptions: ProxyOptions; + _tlsOptions: CSFLEKMSTlsOptions; + _kmsProviders: KMSProviders; + + /** @internal */ + _mongoCrypt: MongoCrypt; + + /** @internal */ + static getMongoCrypt(): MongoCryptConstructor { + const encryption = getMongoDBClientEncryption(); + if ('kModuleError' in encryption) { + throw encryption.kModuleError; + } + return encryption.MongoCrypt; + } + + /** + * Create a new encryption instance + * + * @example + * ```ts + * new ClientEncryption(mongoClient, { + * keyVaultNamespace: 'client.encryption', + * kmsProviders: { + * local: { + * key: masterKey // The master key used for encryption/decryption. A 96-byte long Buffer + * } + * } + * }); + * ``` + * + * @example + * ```ts + * new ClientEncryption(mongoClient, { + * keyVaultNamespace: 'client.encryption', + * kmsProviders: { + * aws: { + * accessKeyId: AWS_ACCESS_KEY, + * secretAccessKey: AWS_SECRET_KEY + * } + * } + * }); + * ``` + */ + constructor(client: MongoClient, options: ClientEncryptionOptions) { + this._client = client; + this._proxyOptions = options.proxyOptions ?? {}; + this._tlsOptions = options.tlsOptions ?? {}; + this._kmsProviders = options.kmsProviders || {}; + + if (options.keyVaultNamespace == null) { + throw new MongoCryptInvalidArgumentError('Missing required option `keyVaultNamespace`'); + } + + const mongoCryptOptions: MongoCryptOptions = { + ...options, + cryptoCallbacks, + kmsProviders: !Buffer.isBuffer(this._kmsProviders) + ? (serialize(this._kmsProviders) as Buffer) + : this._kmsProviders + }; + + this._keyVaultNamespace = options.keyVaultNamespace; + this._keyVaultClient = options.keyVaultClient || client; + const MongoCrypt = ClientEncryption.getMongoCrypt(); + this._mongoCrypt = new MongoCrypt(mongoCryptOptions); + } + + /** + * Creates a data key used for explicit encryption and inserts it into the key vault namespace + * + * @example + * ```ts + * // Using callbacks to create a local key + * clientEncryption.createDataKey('local', (err, dataKey) => { + * if (err) { + * // This means creating the key failed. + * } else { + * // key creation succeeded + * } + * }); + * ``` + * + * @example + * ```ts + * // Using async/await to create a local key + * const dataKeyId = await clientEncryption.createDataKey('local'); + * ``` + * + * @example + * ```ts + * // Using async/await to create an aws key + * const dataKeyId = await clientEncryption.createDataKey('aws', { + * masterKey: { + * region: 'us-east-1', + * key: 'xxxxxxxxxxxxxx' // CMK ARN here + * } + * }); + * ``` + * + * @example + * ```ts + * // Using async/await to create an aws key with a keyAltName + * const dataKeyId = await clientEncryption.createDataKey('aws', { + * masterKey: { + * region: 'us-east-1', + * key: 'xxxxxxxxxxxxxx' // CMK ARN here + * }, + * keyAltNames: [ 'mySpecialKey' ] + * }); + * ``` + */ + createDataKey( + provider: KMSProvider, + options?: ClientEncryptionCreateDataKeyProviderOptions, + callback?: Callback + ) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + if (options == null) { + options = {}; + } + + const dataKey = Object.assign({ provider }, options.masterKey); + + if (options.keyAltNames && !Array.isArray(options.keyAltNames)) { + throw new MongoCryptInvalidArgumentError( + `Option "keyAltNames" must be an array of strings, but was of type ${typeof options.keyAltNames}.` + ); + } + + let keyAltNames = undefined; + if (options.keyAltNames && options.keyAltNames.length > 0) { + keyAltNames = options.keyAltNames.map((keyAltName, i) => { + if (typeof keyAltName !== 'string') { + throw new MongoCryptInvalidArgumentError( + `Option "keyAltNames" must be an array of strings, but item at index ${i} was of type ${typeof keyAltName}` + ); + } + + return serialize({ keyAltName }); + }); + } + + let keyMaterial = undefined; + if (options.keyMaterial) { + keyMaterial = serialize({ keyMaterial: options.keyMaterial }); + } + + const dataKeyBson = serialize(dataKey); + const context = this._mongoCrypt.makeDataKeyContext(dataKeyBson, { + keyAltNames, + keyMaterial + }); + const stateMachine = new StateMachine({ + proxyOptions: this._proxyOptions, + tlsOptions: this._tlsOptions + }); + + // @ts-expect-error We did not convert promiseOrCallback to TS + return promiseOrCallback(callback, cb => { + stateMachine.execute(this, context, (err, dataKey) => { + if (err || !dataKey) { + cb(err, null); + return; + } + + const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString( + this._keyVaultNamespace + ); + + this._keyVaultClient + .db(dbName) + .collection(collectionName) + .insertOne(dataKey, { writeConcern: { w: 'majority' } }) + .then( + result => { + return cb(null, result.insertedId); + }, + err => { + cb(err, null); + } + ); + }); + }); + } + + /** + * Searches the keyvault for any data keys matching the provided filter. If there are matches, rewrapManyDataKey then attempts to re-wrap the data keys using the provided options. + * + * If no matches are found, then no bulk write is performed. + * + * @example + * ```ts + * // rewrapping all data data keys (using a filter that matches all documents) + * const filter = {}; + * + * const result = await clientEncryption.rewrapManyDataKey(filter); + * if (result.bulkWriteResult != null) { + * // keys were re-wrapped, results will be available in the bulkWrite object. + * } + * ``` + * + * @example + * ```ts + * // attempting to rewrap all data keys with no matches + * const filter = { _id: new Binary() } // assume _id matches no documents in the database + * const result = await clientEncryption.rewrapManyDataKey(filter); + * + * if (result.bulkWriteResult == null) { + * // no keys matched, `bulkWriteResult` does not exist on the result object + * } + * ``` + */ + async rewrapManyDataKey(filter: Filter, options: RewrapManyDataKeyOptions) { + let keyEncryptionKeyBson = undefined; + if (options) { + const keyEncryptionKey = Object.assign({ provider: options.provider }, options.masterKey); + keyEncryptionKeyBson = serialize(keyEncryptionKey); + } + const filterBson = serialize(filter); + const context = this._mongoCrypt.makeRewrapManyDataKeyContext(filterBson, keyEncryptionKeyBson); + const stateMachine = new StateMachine({ + proxyOptions: this._proxyOptions, + tlsOptions: this._tlsOptions + }); + + const dataKey = await stateMachine.executeAsync<{ v: DataKey[] }>(this, context); + if (!dataKey || dataKey.v.length === 0) { + return {}; + } + + const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString( + this._keyVaultNamespace + ); + + const replacements = dataKey.v.map( + (key: DataKey): AnyBulkWriteOperation => ({ + updateOne: { + filter: { _id: key._id }, + update: { + $set: { + masterKey: key.masterKey, + keyMaterial: key.keyMaterial + }, + $currentDate: { + updateDate: true + } + } + } + }) + ); + + const result = await this._keyVaultClient + .db(dbName) + .collection(collectionName) + .bulkWrite(replacements, { + writeConcern: { w: 'majority' } + }); + + return { bulkWriteResult: result }; + } + + /** + * Deletes the key with the provided id from the keyvault, if it exists. + * + * @example + * ```ts + * // delete a key by _id + * const id = new Binary(); // id is a bson binary subtype 4 object + * const { deletedCount } = await clientEncryption.deleteKey(id); + * + * if (deletedCount != null && deletedCount > 0) { + * // successful deletion + * } + * ``` + * + */ + async deleteKey(_id: Binary): Promise { + const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString( + this._keyVaultNamespace + ); + + return this._keyVaultClient + .db(dbName) + .collection(collectionName) + .deleteOne({ _id }, { writeConcern: { w: 'majority' } }); + } + + /** + * Finds all the keys currently stored in the keyvault. + * + * This method will not throw. + * + * @returns a FindCursor over all keys in the keyvault. + * @example + * ```ts + * // fetching all keys + * const keys = await clientEncryption.getKeys().toArray(); + * ``` + */ + getKeys(): FindCursor { + const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString( + this._keyVaultNamespace + ); + + return this._keyVaultClient + .db(dbName) + .collection(collectionName) + .find({}, { readConcern: { level: 'majority' } }); + } + + /** + * Finds a key in the keyvault with the specified _id. + * + * Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents + * match the id. The promise rejects with an error if an error is thrown. + * @example + * ```ts + * // getting a key by id + * const id = new Binary(); // id is a bson binary subtype 4 object + * const key = await clientEncryption.getKey(id); + * if (!key) { + * // key is null if there was no matching key + * } + * ``` + */ + async getKey(_id: Binary): Promise { + const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString( + this._keyVaultNamespace + ); + + return this._keyVaultClient + .db(dbName) + .collection(collectionName) + .findOne({ _id }, { readConcern: { level: 'majority' } }); + } + + /** + * Finds a key in the keyvault which has the specified keyAltName. + * + * @param keyAltName - a keyAltName to search for a key + * @returns Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents + * match the keyAltName. The promise rejects with an error if an error is thrown. + * @example + * ```ts + * // get a key by alt name + * const keyAltName = 'keyAltName'; + * const key = await clientEncryption.getKeyByAltName(keyAltName); + * if (!key) { + * // key is null if there is no matching key + * } + * ``` + */ + async getKeyByAltName(keyAltName: string) { + const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString( + this._keyVaultNamespace + ); + + return this._keyVaultClient + .db(dbName) + .collection(collectionName) + .findOne({ keyAltNames: keyAltName }, { readConcern: { level: 'majority' } }); + } + + /** + * Adds a keyAltName to a key identified by the provided _id. + * + * This method resolves to/returns the *old* key value (prior to adding the new altKeyName). + * + * @param _id - The id of the document to update. + * @param keyAltName - a keyAltName to search for a key + * @returns Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents + * match the id. The promise rejects with an error if an error is thrown. + * @example + * ```ts + * // adding an keyAltName to a data key + * const id = new Binary(); // id is a bson binary subtype 4 object + * const keyAltName = 'keyAltName'; + * const oldKey = await clientEncryption.addKeyAltName(id, keyAltName); + * if (!oldKey) { + * // null is returned if there is no matching document with an id matching the supplied id + * } + * ``` + */ + async addKeyAltName(_id: Binary, keyAltName: string) { + const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString( + this._keyVaultNamespace + ); + + const { value } = await this._keyVaultClient + .db(dbName) + .collection(collectionName) + .findOneAndUpdate( + { _id }, + { $addToSet: { keyAltNames: keyAltName } }, + { writeConcern: { w: 'majority' }, returnDocument: 'before' } + ); + + return value; + } + + /** + * Adds a keyAltName to a key identified by the provided _id. + * + * This method resolves to/returns the *old* key value (prior to removing the new altKeyName). + * + * If the removed keyAltName is the last keyAltName for that key, the `altKeyNames` property is unset from the document. + * + * @param _id - The id of the document to update. + * @param keyAltName - a keyAltName to search for a key + * @returns Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents + * match the id. The promise rejects with an error if an error is thrown. + * @example + * ```ts + * // removing a key alt name from a data key + * const id = new Binary(); // id is a bson binary subtype 4 object + * const keyAltName = 'keyAltName'; + * const oldKey = await clientEncryption.removeKeyAltName(id, keyAltName); + * + * if (!oldKey) { + * // null is returned if there is no matching document with an id matching the supplied id + * } + * ``` + */ + async removeKeyAltName(_id: Binary, keyAltName: string) { + const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString( + this._keyVaultNamespace + ); + + const pipeline = [ + { + $set: { + keyAltNames: { + $cond: [ + { + $eq: ['$keyAltNames', [keyAltName]] + }, + '$$REMOVE', + { + $filter: { + input: '$keyAltNames', + cond: { + $ne: ['$$this', keyAltName] + } + } + } + ] + } + } + } + ]; + const { value } = await this._keyVaultClient + .db(dbName) + .collection(collectionName) + .findOneAndUpdate({ _id }, pipeline, { + writeConcern: { w: 'majority' }, + returnDocument: 'before' + }); + + return value; + } + + /** + * A convenience method for creating an encrypted collection. + * This method will create data keys for any encryptedFields that do not have a `keyId` defined + * and then create a new collection with the full set of encryptedFields. + * + * @param db - A Node.js driver Db object with which to create the collection + * @param name - The name of the collection to be created + * @param options - Options for createDataKey and for createCollection + * @returns created collection and generated encryptedFields + * @throws MongoCryptCreateDataKeyError - If part way through the process a createDataKey invocation fails, an error will be rejected that has the partial `encryptedFields` that were created. + * @throws MongoCryptCreateEncryptedCollectionError - If creating the collection fails, an error will be rejected that has the entire `encryptedFields` that were created. + */ + async createEncryptedCollection( + db: Db, + name: string, + options: { + provider: KMSProvider; + createCollectionOptions: Omit & { + encryptedFields: Document; + }; + masterKey?: AWSEncryptionKeyOptions | AzureEncryptionKeyOptions | GCPEncryptionKeyOptions; + } + ): Promise<{ collection: Collection; encryptedFields: Document }> { + const { + provider, + masterKey, + createCollectionOptions: { + encryptedFields: { ...encryptedFields }, + ...createCollectionOptions + } + } = options; + + if (Array.isArray(encryptedFields.fields)) { + const createDataKeyPromises = encryptedFields.fields.map(async field => + field == null || typeof field !== 'object' || field.keyId != null + ? field + : { + ...field, + keyId: await this.createDataKey(provider, { masterKey }) + } + ); + + const createDataKeyResolutions = await Promise.allSettled(createDataKeyPromises); + + encryptedFields.fields = createDataKeyResolutions.map((resolution, index) => + resolution.status === 'fulfilled' ? resolution.value : encryptedFields.fields[index] + ); + + const rejection = createDataKeyResolutions.find( + (result): result is PromiseRejectedResult => result.status === 'rejected' + ); + if (rejection != null) { + throw new MongoCryptCreateDataKeyError({ encryptedFields, cause: rejection.reason }); + } + } + + try { + const collection = await db.createCollection(name, { + ...createCollectionOptions, + encryptedFields + }); + return { collection, encryptedFields }; + } catch (cause) { + throw new MongoCryptCreateEncryptedCollectionError({ encryptedFields, cause }); + } + } + + /** + * 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 options - + * @param callback - Optional callback to invoke when value is encrypted + * @returns 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. + * + * @example + * ```ts + * // Encryption with callback API + * function encryptMyData(value, callback) { + * clientEncryption.createDataKey('local', (err, keyId) => { + * if (err) { + * return callback(err); + * } + * clientEncryption.encrypt(value, { keyId, algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' }, callback); + * }); + * } + * ``` + * + * @example + * ```ts + * // Encryption with async/await api + * async function encryptMyData(value) { + * const keyId = await clientEncryption.createDataKey('local'); + * return clientEncryption.encrypt(value, { keyId, algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' }); + * } + * ``` + * + * @example + * ```ts + * // Encryption using a keyAltName + * async function encryptMyData(value) { + * await clientEncryption.createDataKey('local', { keyAltNames: 'mySpecialKey' }); + * return clientEncryption.encrypt(value, { keyAltName: 'mySpecialKey', algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' }); + * } + * ``` + */ + encrypt( + value: unknown, + options: ClientEncryptionEncryptOptions, + callback: Callback + ): Promise | void { + return maybeCallback(() => this._encrypt(value, false, options), callback); + } + + /** + * 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 production use. It is subject to breaking changes. + * + * @param 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 options - + * @returns Returns a Promise that either resolves with the encrypted value or rejects with an error. + */ + async encryptExpression( + expression: Document, + options: ClientEncryptionEncryptOptions + ): Promise { + return this._encrypt(expression, true, options); + } + + /** + * Explicitly decrypt a provided encrypted value + * + * @param value - An encrypted value + * @param callback - Optional callback to invoke when value is decrypted + * @returns If no callback is provided, returns a Promise that either resolves with the decrypted value, or rejects with an error. If a callback is provided, returns nothing. + * + * ```ts + * @example + * // Decrypting value with callback API + * function decryptMyValue(value, callback) { + * clientEncryption.decrypt(value, callback); + * } + * ``` + * + * @example + * ```ts + * // Decrypting value with async/await API + * async function decryptMyValue(value) { + * return clientEncryption.decrypt(value); + * } + * ``` + */ + decrypt(value: Binary, callback?: Callback): Promise | void { + const valueBuffer = serialize({ v: value }); + const context = this._mongoCrypt.makeExplicitDecryptionContext(valueBuffer); + + const stateMachine = new StateMachine({ + proxyOptions: this._proxyOptions, + tlsOptions: this._tlsOptions + }); + + // @ts-expect-error We did not convert promiseOrCallback to TS + return promiseOrCallback(callback, cb => { + stateMachine.execute<{ v: T }>(this, context, (err, result) => { + if (err || !result) { + cb(err, null); + return; + } + + cb(null, result.v); + }); + }); + } + + /** + * 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. + */ + async askForKMSCredentials(): Promise { + return refreshKMSCredentials(this._kmsProviders); + } + + static get libmongocryptVersion() { + return ClientEncryption.getMongoCrypt().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 expressionMode - a boolean that indicates whether or not to encrypt the value as an expression + * @param options - options to pass to encrypt + * @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. + * + */ + private async _encrypt( + value: unknown, + expressionMode: boolean, + options: ClientEncryptionEncryptOptions + ): Promise { + const { algorithm, keyId, keyAltName, contentionFactor, queryType, rangeOptions } = options; + const contextOptions: ExplicitEncryptionContextOptions = { + expressionMode, + algorithm + }; + if (keyId) { + contextOptions.keyId = keyId.buffer; + } + if (keyAltName) { + if (keyId) { + throw new MongoCryptInvalidArgumentError( + `"options" cannot contain both "keyId" and "keyAltName"` + ); + } + if (typeof keyAltName !== 'string') { + throw new MongoCryptInvalidArgumentError( + `"options.keyAltName" must be of type string, but was of type ${typeof keyAltName}` + ); + } + + contextOptions.keyAltName = serialize({ keyAltName }); + } + if (typeof contentionFactor === 'number' || typeof contentionFactor === 'bigint') { + contextOptions.contentionFactor = contentionFactor; + } + if (typeof queryType === 'string') { + contextOptions.queryType = queryType; + } + + if (typeof rangeOptions === 'object') { + contextOptions.rangeOptions = serialize(rangeOptions); + } + + const valueBuffer = serialize({ v: value }); + const stateMachine = new StateMachine({ + proxyOptions: this._proxyOptions, + tlsOptions: this._tlsOptions + }); + const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions); + + const result = await stateMachine.executeAsync<{ v: Binary }>(this, context); + return result.v; + } +} + +/** + * Options to provide when encrypting data. + */ +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'; + + /** + * The id of the Binary dataKey to use for encryption + */ + keyId?: Binary; + + /** + * A unique string name corresponding to an already existing dataKey. + */ + keyAltName?: string; + + /** The contention factor. */ + contentionFactor?: bigint | number; + + /** + * The query type supported. Only the queryType `equality` is stable. + * + * @experimental Public Technical Preview: The queryType `rangePreview` is experimental. + */ + queryType?: 'equality' | 'rangePreview'; + + /** @experimental Public Technical Preview: The index options for a Queryable Encryption field supporting "rangePreview" queries.*/ + rangeOptions?: RangeOptions; +} + +/** @experimental */ +export interface RewrapManyDataKeyOptions { + provider: KMSProvider; + masterKey?: + | AWSEncryptionKeyOptions + | AzureEncryptionKeyOptions + | GCPEncryptionKeyOptions + | undefined; +} + +/** + * Additional settings to provide when creating a new `ClientEncryption` instance. + */ +export interface ClientEncryptionOptions { + /** + * The namespace of the key vault, used to store encryption keys + */ + keyVaultNamespace: string; + + /** + * A MongoClient used to fetch keys from a key vault. Defaults to client. + */ + keyVaultClient?: MongoClient | undefined; + + /** + * Options for specific KMS providers to use + */ + kmsProviders?: KMSProviders; + + /** + * Options for specifying a Socks5 proxy to use for connecting to the KMS. + */ + proxyOptions?: ProxyOptions; + + /** + * TLS options for kms providers to use. + */ + tlsOptions?: CSFLEKMSTlsOptions; +} + +/** + * Configuration options for making an AWS encryption key + */ +export interface AWSEncryptionKeyOptions { + /** + * The AWS region of the KMS + */ + region: string; + + /** + * The Amazon Resource Name (ARN) to the AWS customer master key (CMK) + */ + key: string; + + /** + * An alternate host to send KMS requests to. May include port number. + */ + endpoint?: string | undefined; +} + +/** + * Configuration options for making an AWS encryption key + */ +export interface GCPEncryptionKeyOptions { + /** + * GCP project ID + */ + projectId: string; + + /** + * Location name (e.g. "global") + */ + location: string; + + /** + * Key ring name + */ + keyRing: string; + + /** + * Key name + */ + keyName: string; + + /** + * Key version + */ + keyVersion?: string | undefined; + + /** + * KMS URL, defaults to `https://www.googleapis.com/auth/cloudkms` + */ + endpoint?: string | undefined; +} + +/** + * Configuration options for making an Azure encryption key + */ +export interface AzureEncryptionKeyOptions { + /** + * Key name + */ + keyName: string; + + /** + * Key vault URL, typically `.vault.azure.net` + */ + keyVaultEndpoint: string; + + /** + * Key version + */ + keyVersion?: string | undefined; +} + +/** + * Options to provide when creating a new data key. + */ +export interface ClientEncryptionCreateDataKeyProviderOptions { + /** + * Identifies a new KMS-specific key used to encrypt the new data key + */ + masterKey?: + | AWSEncryptionKeyOptions + | AzureEncryptionKeyOptions + | GCPEncryptionKeyOptions + | undefined; + + /** + * An optional list of string alternate names used to reference a key. + * If a key is created with alternate names, then encryption may refer to the key by the unique alternate name instead of by _id. + */ + keyAltNames?: string[] | undefined; + + /** @experimental */ + keyMaterial?: Buffer | Binary; +} + +/** @experimental */ +export interface RewrapManyDataKeyOptions { + provider: KMSProvider; + masterKey?: + | AWSEncryptionKeyOptions + | AzureEncryptionKeyOptions + | GCPEncryptionKeyOptions + | undefined; +} + +/** @experimental */ +export interface ClientEncryptionRewrapManyDataKeyResult { + /** The result of rewrapping data keys. If unset, no keys matched the filter. */ + bulkWriteResult?: BulkWriteResult; +} + +/** + * RangeOptions 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. + */ +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'; + + /** + * The id of the Binary dataKey to use for encryption + */ + keyId?: Binary; + + /** + * A unique string name corresponding to an already existing dataKey. + */ + keyAltName?: string; + + /** The contention factor. */ + contentionFactor?: bigint | number; + + /** + * The query type supported. Only the queryType `equality` is stable. + * + * @experimental Public Technical Preview: The queryType `rangePreview` is experimental. + */ + queryType?: 'equality' | 'rangePreview'; + + /** @experimental Public Technical Preview: The index options for a Queryable Encryption field supporting "rangePreview" queries.*/ + rangeOptions?: RangeOptions; +} diff --git a/src/client-side-encryption/common.js b/src/client-side-encryption/common.js index 0685530727f..1d7dd2fe328 100644 --- a/src/client-side-encryption/common.js +++ b/src/client-side-encryption/common.js @@ -1,33 +1,4 @@ -/** - * @ignore - * Helper function for logging. Enabled by setting the environment flag MONGODB_CRYPT_DEBUG. - * @param {*} msg Anything you want to be logged. - */ -export function debug(msg) { - if (process.env.MONGODB_CRYPT_DEBUG) { - // eslint-disable-next-line no-console - console.error(msg); - } -} - -/** - * @ignore - * Gets the database portion of a namespace string - * @param {string} ns A string in the format of a namespace (database.collection) - * @returns {string} The database portion of the namespace - */ -export function databaseNamespace(ns) { - return ns.split('.')[0]; -} -/** - * @ignore - * Gets the collection portion of a namespace string - * @param {string} ns A string in the format of a namespace (database.collection) - * @returns {string} The collection portion of the namespace - */ -export function collectionNamespace(ns) { - return ns.split('.').slice(1).join('.'); -} +/* eslint-disable */ export function maybeCallback(promiseFn, callback) { const promise = promiseFn(); diff --git a/src/client-side-encryption/cryptoCallbacks.js b/src/client-side-encryption/crypto_callbacks.ts similarity index 65% rename from src/client-side-encryption/cryptoCallbacks.js rename to src/client-side-encryption/crypto_callbacks.ts index 30ac77fe336..1e2f1f7f070 100644 --- a/src/client-side-encryption/cryptoCallbacks.js +++ b/src/client-side-encryption/crypto_callbacks.ts @@ -1,11 +1,16 @@ import * as crypto from 'crypto'; -export function makeAES256Hook(method, mode) { - return function (key, iv, input, output) { +type AES256Callback = (key: Buffer, iv: Buffer, input: Buffer, output: Buffer) => number | Error; + +export function makeAES256Hook( + method: 'createCipheriv' | 'createDecipheriv', + mode: 'aes-256-cbc' | 'aes-256-ctr' +): AES256Callback { + return function (key: Buffer, iv: Buffer, input: Buffer, output: Buffer): number | Error { let result; try { - let cipher = crypto[method](mode, key, iv); + const cipher = crypto[method](mode, key, iv); cipher.setAutoPadding(false); result = cipher.update(input); const final = cipher.final(); @@ -21,7 +26,7 @@ export function makeAES256Hook(method, mode) { }; } -export function randomHook(buffer, count) { +export function randomHook(buffer: Buffer, count: number): number | Error { try { crypto.randomFillSync(buffer, 0, count); } catch (e) { @@ -30,7 +35,7 @@ export function randomHook(buffer, count) { return count; } -export function sha256Hook(input, output) { +export function sha256Hook(input: Buffer, output: Buffer): number | Error { let result; try { result = crypto.createHash('sha256').update(input).digest(); @@ -42,8 +47,9 @@ export function sha256Hook(input, output) { return result.length; } -export function makeHmacHook(algorithm) { - return (key, input, output) => { +type HMACHook = (key: Buffer, input: Buffer, output: Buffer) => number | Error; +export function makeHmacHook(algorithm: 'sha512' | 'sha256'): HMACHook { + return (key: Buffer, input: Buffer, output: Buffer): number | Error => { let result; try { result = crypto.createHmac(algorithm, key).update(input).digest(); @@ -56,7 +62,7 @@ export function makeHmacHook(algorithm) { }; } -export function signRsaSha256Hook(key, input, output) { +export function signRsaSha256Hook(key: Buffer, input: Buffer, output: Buffer): number | Error { let result; try { const signer = crypto.createSign('sha256WithRSAEncryption'); @@ -73,7 +79,6 @@ export function signRsaSha256Hook(key, input, output) { return result.length; } - export const aes256CbcEncryptHook = makeAES256Hook('createCipheriv', 'aes-256-cbc'); export const aes256CbcDecryptHook = makeAES256Hook('createDecipheriv', 'aes-256-cbc'); export const aes256CtrEncryptHook = makeAES256Hook('createCipheriv', 'aes-256-ctr'); diff --git a/src/client-side-encryption/errors.ts b/src/client-side-encryption/errors.ts index 615446e4c70..d187bac0d97 100644 --- a/src/client-side-encryption/errors.ts +++ b/src/client-side-encryption/errors.ts @@ -15,6 +15,21 @@ export class MongoCryptError extends Error { } } +/** + * @public + * + * An error indicating an invalid argument was provided to an encryption API. + */ +export class MongoCryptInvalidArgumentError extends MongoCryptError { + /** @internal */ + constructor(message: string) { + super(message); + } + + override get name() { + return 'MongoCryptInvalidArgumentError'; + } +} /** * @public * An error indicating that `ClientEncryption.createEncryptedCollection()` failed to create data keys diff --git a/src/client-side-encryption/mongocryptdManager.js b/src/client-side-encryption/mongocryptdManager.js deleted file mode 100644 index 3bde6df2f15..00000000000 --- a/src/client-side-encryption/mongocryptdManager.js +++ /dev/null @@ -1,62 +0,0 @@ -import { spawn } from 'child_process' - -/** - * @internal - * An internal class that handles spawning a mongocryptd. - */ -export class MongocryptdManager { - static DEFAULT_MONGOCRYPTD_URI = 'mongodb://localhost:27020'; - - /** - * @ignore - * Creates a new Mongocryptd Manager - * @param {AutoEncrypter~AutoEncryptionExtraOptions} [extraOptions] extra options that determine how/when to spawn a mongocryptd - */ - constructor(extraOptions) { - extraOptions = extraOptions || {}; - - this.uri = - typeof extraOptions.mongocryptdURI === 'string' && extraOptions.mongocryptdURI.length > 0 - ? extraOptions.mongocryptdURI - : MongocryptdManager.DEFAULT_MONGOCRYPTD_URI; - - this.bypassSpawn = !!extraOptions.mongocryptdBypassSpawn; - - this.spawnPath = extraOptions.mongocryptdSpawnPath || ''; - this.spawnArgs = []; - if (Array.isArray(extraOptions.mongocryptdSpawnArgs)) { - this.spawnArgs = this.spawnArgs.concat(extraOptions.mongocryptdSpawnArgs); - } - if ( - this.spawnArgs - .filter(arg => typeof arg === 'string') - .every(arg => arg.indexOf('--idleShutdownTimeoutSecs') < 0) - ) { - this.spawnArgs.push('--idleShutdownTimeoutSecs', 60); - } - } - - /** - * @ignore - * Will check to see if a mongocryptd is up. If it is not up, it will attempt - * to spawn a mongocryptd in a detached process, and then wait for it to be up. - * @param {Function} callback Invoked when we think a mongocryptd is up - */ - spawn(callback) { - const cmdName = this.spawnPath || 'mongocryptd'; - - // Spawned with stdio: ignore and detatched:true - // to ensure child can outlive parent. - this._child = spawn(cmdName, this.spawnArgs, { - stdio: 'ignore', - detached: true - }); - - this._child.on('error', () => {}); - - // unref child to remove handle from event loop - this._child.unref(); - - process.nextTick(callback); - } -} diff --git a/src/client-side-encryption/mongocryptd_manager.ts b/src/client-side-encryption/mongocryptd_manager.ts new file mode 100644 index 00000000000..cf69f0fd7b5 --- /dev/null +++ b/src/client-side-encryption/mongocryptd_manager.ts @@ -0,0 +1,79 @@ +import type { ChildProcess } from 'child_process'; + +import { type Callback } from '../utils'; +import { type AutoEncryptionExtraOptions } from './auto_encrypter'; + +/** + * @internal + * An internal class that handles spawning a mongocryptd. + */ +export class MongocryptdManager { + static DEFAULT_MONGOCRYPTD_URI = 'mongodb://localhost:27020'; + + uri: string; + bypassSpawn: boolean; + spawnPath: string; + spawnArgs: Array; + _child?: ChildProcess; + + constructor(extraOptions: AutoEncryptionExtraOptions = {}) { + this.uri = + typeof extraOptions.mongocryptdURI === 'string' && extraOptions.mongocryptdURI.length > 0 + ? extraOptions.mongocryptdURI + : MongocryptdManager.DEFAULT_MONGOCRYPTD_URI; + + this.bypassSpawn = !!extraOptions.mongocryptdBypassSpawn; + + this.spawnPath = extraOptions.mongocryptdSpawnPath || ''; + this.spawnArgs = []; + if (Array.isArray(extraOptions.mongocryptdSpawnArgs)) { + this.spawnArgs = this.spawnArgs.concat(extraOptions.mongocryptdSpawnArgs); + } + if ( + this.spawnArgs + .filter(arg => typeof arg === 'string') + .every(arg => arg.indexOf('--idleShutdownTimeoutSecs') < 0) + ) { + this.spawnArgs.push('--idleShutdownTimeoutSecs', '60'); + } + } + + /** + * Will check to see if a mongocryptd is up. If it is not up, it will attempt + * to spawn a mongocryptd in a detached process, and then wait for it to be up. + */ + spawn(callback: Callback) { + const cmdName = this.spawnPath || 'mongocryptd'; + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { spawn } = require('child_process') as typeof import('child_process'); + + // Spawned with stdio: ignore and detached: true + // to ensure child can outlive parent. + this._child = spawn(cmdName, this.spawnArgs, { + stdio: 'ignore', + detached: true + }); + + this._child.on('error', () => { + // From the FLE spec: + // "The stdout and stderr of the spawned process MUST not be exposed in the driver + // (e.g. redirect to /dev/null). Users can pass the argument --logpath to + // extraOptions.mongocryptdSpawnArgs if they need to inspect mongocryptd logs. + // If spawning is necessary, the driver MUST spawn mongocryptd whenever server + // selection on the MongoClient to mongocryptd fails. If the MongoClient fails to + // connect after spawning, the server selection error is propagated to the user." + // The AutoEncrypter and MongoCryptdManager should work together to spawn + // mongocryptd whenever necessary. Additionally, the `mongocryptd` intentionally + // shuts down after 60s and gets respawned when necessary. We rely on server + // selection timeouts when connecting to the `mongocryptd` to inform users that something + // has been configured incorrectly. For those reasons, we suppress stderr from + // the `mongocryptd` process and immediately unref the process. + }); + + // unref child to remove handle from event loop + this._child.unref(); + + process.nextTick(callback); + } +} diff --git a/src/client-side-encryption/providers/azure.ts b/src/client-side-encryption/providers/azure.ts index eaac6422a77..53536902bc3 100644 --- a/src/client-side-encryption/providers/azure.ts +++ b/src/client-side-encryption/providers/azure.ts @@ -1,5 +1,4 @@ import { type Document } from '../../bson'; - import { MongoCryptAzureKMSRequestError, MongoCryptKMSRequestNetworkTimeoutError } from '../errors'; import { type KMSProviders } from './index'; import { get } from './utils'; @@ -60,7 +59,10 @@ export class AzureCredentialCache { export const tokenCache = new AzureCredentialCache(); /** @internal */ -async function parseResponse(response: { body: string; status?: number }): Promise { +async function parseResponse(response: { + body: string; + status?: number; +}): Promise { const { status, body: rawBody } = response; const body: { expires_in?: number; access_token?: string } = (() => { @@ -121,7 +123,9 @@ export function prepareRequest(options: AzureKMSRequestOptions): { headers: Document; url: URL; } { - const url = new URL(options.url?.toString() ?? 'http://169.254.169.254/metadata/identity/oauth2/token'); + const url = new URL( + options.url?.toString() ?? 'http://169.254.169.254/metadata/identity/oauth2/token' + ); url.searchParams.append('api-version', '2018-02-01'); url.searchParams.append('resource', 'https://vault.azure.net'); diff --git a/src/client-side-encryption/providers/index.ts b/src/client-side-encryption/providers/index.ts index 5e4024d51e8..d44f9d41279 100644 --- a/src/client-side-encryption/providers/index.ts +++ b/src/client-side-encryption/providers/index.ts @@ -147,7 +147,7 @@ export function isEmptyCredentials(providerName: KMSProvider, kmsProviders: KMSP * * @internal */ -export async function loadCredentials(kmsProviders: KMSProviders): Promise { +export async function refreshKMSCredentials(kmsProviders: KMSProviders): Promise { let finalKMSProviders = kmsProviders; if (isEmptyCredentials('aws', kmsProviders)) { diff --git a/src/client-side-encryption/stateMachine.js b/src/client-side-encryption/state_machine.ts similarity index 53% rename from src/client-side-encryption/stateMachine.js rename to src/client-side-encryption/state_machine.ts index 931437ee472..121ee3fe8f0 100644 --- a/src/client-side-encryption/stateMachine.js +++ b/src/client-side-encryption/state_machine.ts @@ -1,19 +1,28 @@ +import * as fs from 'fs'; +import { type MongoCryptContext, type MongoCryptKMSRequest } from 'mongodb-client-encryption'; +import * as net from 'net'; +import * as tls from 'tls'; import { promisify } from 'util'; -import * as tls from 'tls'; -import * as net from 'net'; -import * as fs from 'fs'; -import { once } from 'events'; +import { + type BSONSerializeOptions, + deserialize, + type Document, + pluckBSONSerializeOptions, + serialize +} from '../bson'; +import { type ProxyOptions } from '../cmap/connection'; +import { getSocks, type SocksLib } from '../deps'; import { MongoNetworkTimeoutError } from '../error'; -import { debug, databaseNamespace, collectionNamespace } from './common'; +import { type MongoClient, type MongoClientOptions } from '../mongo_client'; +import { BufferPool, type Callback, MongoDBCollectionNamespace } from '../utils'; +import { type DataKey } from './client_encryption'; import { MongoCryptError } from './errors'; -import { BufferPool } from './buffer_pool'; -import { serialize, deserialize } from '../bson'; -import { getSocks } from '../deps'; +import { type MongocryptdManager } from './mongocryptd_manager'; +import { type KMSProvider, type KMSProviders } from './providers'; -/** @type {import('../deps').SocksLib | null} */ -let socks = null; -function loadSocks() { +let socks: SocksLib | null = null; +function loadSocks(): SocksLib { if (socks == null) { const socksImport = getSocks(); if ('kModuleError' in socksImport) { @@ -51,78 +60,130 @@ const INSECURE_TLS_OPTIONS = [ 'tlsInsecure', 'tlsAllowInvalidCertificates', 'tlsAllowInvalidHostnames', + + // These options are disallowed by the spec, so we explicitly filter them out if provided, even + // though the StateMachine does not declare support for these options. 'tlsDisableOCSPEndpointCheck', 'tlsDisableCertificateRevocationCheck' ]; /** - * @ignore - * @callback StateMachine~executeCallback - * @param {Error} [err] If present, indicates that the execute call failed with the given error - * @param {object} [result] If present, is the result of executing the state machine. - * @returns {void} + * Helper function for logging. Enabled by setting the environment flag MONGODB_CRYPT_DEBUG. + * @param msg - Anything you want to be logged. */ +function debug(msg: unknown) { + if (process.env.MONGODB_CRYPT_DEBUG) { + // eslint-disable-next-line no-console + console.error(msg); + } +} -/** - * @ignore - * @callback StateMachine~fetchCollectionInfoCallback - * @param {Error} [err] If present, indicates that fetching the collection info failed with the given error - * @param {object} [result] If present, is the fetched collection info for the first collection to match the given filter - * @returns {void} - */ +declare module 'mongodb-client-encryption' { + // the properties added to `MongoCryptContext` here are only used for the `StateMachine`'s + // execute method and are not part of the C++ bindings. + interface MongoCryptContext { + id: number; + document: Document; + ns: string; + } +} /** - * @ignore - * @callback StateMachine~markCommandCallback - * @param {Error} [err] If present, indicates that marking the command failed with the given error - * @param {Buffer} [result] If present, is the marked command serialized into bson - * @returns {void} + * @public + * + * TLS options to use when connecting. The spec specifically calls out which insecure + * tls options are not allowed: + * + * - tlsAllowInvalidCertificates + * - tlsAllowInvalidHostnames + * - tlsInsecure */ +export type CSFLETlsOptions = Pick< + MongoClientOptions, + 'tlsCAFile' | 'tlsCertificateKeyFile' | 'tlsCertificateKeyFilePassword' +>; + +/** @public */ +export type CSFLEKMSTlsOptions = { + aws?: CSFLETlsOptions; + gcp?: CSFLETlsOptions; + kmip?: CSFLETlsOptions; + local?: CSFLETlsOptions; + azure?: CSFLETlsOptions; +}; /** - * @ignore - * @callback StateMachine~fetchKeysCallback - * @param {Error} [err] If present, indicates that fetching the keys failed with the given error - * @param {object[]} [result] If present, is all the keys from the keyVault collection that matched the given filter + * @internal + * + * An interface representing an object that can be passed to the `StateMachine.execute` method. + * + * Not all properties are required for all operations. */ +export interface StateMachineExecutable { + _keyVaultNamespace: string; + _keyVaultClient: MongoClient; + + /** only used for auto encryption */ + _metaDataClient?: MongoClient; + /** only used for auto encryption */ + _mongocryptdClient?: MongoClient; + /** only used for auto encryption */ + _mongocryptdManager?: MongocryptdManager; + askForKMSCredentials: () => Promise; +} + +export type StateMachineOptions = { + /** socks5 proxy options, if set. */ + proxyOptions: ProxyOptions; + + /** TLS options for KMS requests, if set. */ + tlsOptions: CSFLEKMSTlsOptions; +} & Pick; /** - * @ignore + * @internal * An internal class that executes across a MongoCryptContext until either * a finishing state or an error is reached. Do not instantiate directly. - * @class StateMachine */ -class StateMachine { - constructor(options) { - this.options = options || {}; - - this.executeAsync = promisify((autoEncrypter, context, callback) => - this.execute(autoEncrypter, context, callback) - ); +export class StateMachine { + constructor( + private options: StateMachineOptions, + private bsonOptions = pluckBSONSerializeOptions(options) + ) {} + + executeAsync(executor: StateMachineExecutable, context: MongoCryptContext): Promise { + // @ts-expect-error The callback version allows undefined for the result, but we'll never actually have an undefined result without an error. + return promisify(this.execute.bind(this))(executor, context); } /** - * @ignore * Executes the state machine according to the specification - * @param {AutoEncrypter|ClientEncryption} autoEncrypter The JS encryption object - * @param {object} context The C++ context object returned from the bindings - * @param {StateMachine~executeCallback} callback Invoked with the result/error of executing the state machine - * @returns {void} */ - execute(autoEncrypter, context, callback) { - const keyVaultNamespace = autoEncrypter._keyVaultNamespace; - const keyVaultClient = autoEncrypter._keyVaultClient; - const metaDataClient = autoEncrypter._metaDataClient; - const mongocryptdClient = autoEncrypter._mongocryptdClient; - const mongocryptdManager = autoEncrypter._mongocryptdManager; + execute( + executor: StateMachineExecutable, + context: MongoCryptContext, + callback: Callback + ) { + const keyVaultNamespace = executor._keyVaultNamespace; + const keyVaultClient = executor._keyVaultClient; + const metaDataClient = executor._metaDataClient; + const mongocryptdClient = executor._mongocryptdClient; + const mongocryptdManager = executor._mongocryptdManager; debug(`[context#${context.id}] ${stateToString.get(context.state) || context.state}`); switch (context.state) { case MONGOCRYPT_CTX_NEED_MONGO_COLLINFO: { const filter = deserialize(context.nextMongoOperation()); + if (!metaDataClient) { + return callback( + new MongoCryptError( + 'unreachable state machine state: entered MONGOCRYPT_CTX_NEED_MONGO_COLLINFO but metadata client is undefined' + ) + ); + } this.fetchCollectionInfo(metaDataClient, context.ns, filter, (err, collInfo) => { if (err) { - return callback(err, null); + return callback(err); } if (collInfo) { @@ -130,7 +191,7 @@ class StateMachine { } context.finishMongoOperation(); - this.execute(autoEncrypter, context, callback); + this.execute(executor, context, callback); }); return; @@ -138,8 +199,15 @@ class StateMachine { case MONGOCRYPT_CTX_NEED_MONGO_MARKINGS: { const command = context.nextMongoOperation(); + if (!mongocryptdClient) { + return callback( + new MongoCryptError( + 'unreachable state machine state: entered MONGOCRYPT_CTX_NEED_MONGO_MARKINGS but mongocryptdClient is undefined' + ) + ); + } this.markCommand(mongocryptdClient, context.ns, command, (err, markedCommand) => { - if (err) { + if (err || !markedCommand) { // If we are not bypassing spawning, then we should retry once on a MongoTimeoutError (server selection error) if ( err instanceof MongoNetworkTimeoutError && @@ -149,22 +217,22 @@ class StateMachine { mongocryptdManager.spawn(() => { // TODO: should we be shadowing the variables here? this.markCommand(mongocryptdClient, context.ns, command, (err, markedCommand) => { - if (err) return callback(err, null); + if (err || !markedCommand) return callback(err); context.addMongoOperationResponse(markedCommand); context.finishMongoOperation(); - this.execute(autoEncrypter, context, callback); + this.execute(executor, context, callback); }); }); return; } - return callback(err, null); + return callback(err); } context.addMongoOperationResponse(markedCommand); context.finishMongoOperation(); - this.execute(autoEncrypter, context, callback); + this.execute(executor, context, callback); }); return; @@ -173,29 +241,27 @@ class StateMachine { case MONGOCRYPT_CTX_NEED_MONGO_KEYS: { const filter = context.nextMongoOperation(); this.fetchKeys(keyVaultClient, keyVaultNamespace, filter, (err, keys) => { - if (err) return callback(err, null); + if (err || !keys) return callback(err); keys.forEach(key => { context.addMongoOperationResponse(serialize(key)); }); context.finishMongoOperation(); - this.execute(autoEncrypter, context, callback); + this.execute(executor, context, callback); }); return; } case MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS: { - autoEncrypter + executor .askForKMSCredentials() .then(kmsProviders => { - context.provideKMSProviders( - !Buffer.isBuffer(kmsProviders) ? serialize(kmsProviders) : kmsProviders - ); - this.execute(autoEncrypter, context, callback); + context.provideKMSProviders(serialize(kmsProviders)); + this.execute(executor, context, callback); }) .catch(err => { - callback(err, null); + callback(err); }); return; @@ -212,10 +278,10 @@ class StateMachine { Promise.all(promises) .then(() => { context.finishKMSRequests(); - this.execute(autoEncrypter, context, callback); + this.execute(executor, context, callback); }) .catch(err => { - callback(err, null); + callback(err); }); return; @@ -227,17 +293,23 @@ class StateMachine { // TODO: Maybe rework the logic here so that instead of doing // the callback here, finalize stores the result, and then // we wait to MONGOCRYPT_CTX_DONE to do the callback + // @ts-expect-error finalize can change the state, check for error if (context.state === MONGOCRYPT_CTX_ERROR) { const message = context.status.message || 'Finalization error'; callback(new MongoCryptError(message)); return; } - callback(null, deserialize(finalizedContext, this.options)); + callback(undefined, deserialize(finalizedContext, this.options) as T); return; } case MONGOCRYPT_CTX_ERROR: { const message = context.status.message; - callback(new MongoCryptError(message)); + callback( + new MongoCryptError( + message ?? + 'unidentifiable error in MongoCrypt - received an error status from `libmongocrypt` but received no error message.' + ) + ); return; } @@ -252,24 +324,28 @@ class StateMachine { } /** - * @ignore * Handles the request to the KMS service. Exposed for testing purposes. Do not directly invoke. - * @param {*} kmsContext A C++ KMS context returned from the bindings - * @returns {Promise} A promise that resolves when the KMS reply has be fully parsed + * @param kmsContext - A C++ KMS context returned from the bindings + * @returns A promise that resolves when the KMS reply has be fully parsed */ - kmsRequest(request) { + kmsRequest(request: MongoCryptKMSRequest): Promise { const parsedUrl = request.endpoint.split(':'); const port = parsedUrl[1] != null ? Number.parseInt(parsedUrl[1], 10) : HTTPS_PORT; - const options = { host: parsedUrl[0], servername: parsedUrl[0], port }; + const options: tls.ConnectionOptions & { host: string; port: number } = { + host: parsedUrl[0], + servername: parsedUrl[0], + port + }; const message = request.message; // TODO(NODE-3959): We can adopt `for-await on(socket, 'data')` with logic to control abort - // eslint-disable-next-line no-async-promise-executor + // eslint-disable-next-line no-async-promise-executor, @typescript-eslint/no-misused-promises return new Promise(async (resolve, reject) => { const buffer = new BufferPool(); - let socket; - let rawSocket; + /* eslint-disable prefer-const */ + let socket: net.Socket; + let rawSocket: net.Socket; function destroySockets() { for (const sock of [socket, rawSocket]) { @@ -285,10 +361,9 @@ class StateMachine { reject(new MongoCryptError('KMS request timed out')); } - function onerror(err) { + function onerror(err: Error) { destroySockets(); - const mcError = new MongoCryptError('KMS request failed'); - mcError.originalError = err; + const mcError = new MongoCryptError('KMS request failed', { cause: err }); reject(mcError); } @@ -301,7 +376,9 @@ class StateMachine { rawSocket.on('timeout', ontimeout); rawSocket.on('error', onerror); try { - await once(rawSocket, 'connect'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const events = require('events') as typeof import('events'); + await events.once(rawSocket, 'connect'); socks ??= loadSocks(); options.socket = ( await socks.SocksClient.createConnection({ @@ -325,7 +402,7 @@ class StateMachine { const tlsOptions = this.options.tlsOptions; if (tlsOptions) { - const kmsProvider = request.kmsProvider; + const kmsProvider = request.kmsProvider as KMSProvider; const providerTlsOptions = tlsOptions[kmsProvider]; if (providerTlsOptions) { const error = this.validateTlsOptions(kmsProvider, providerTlsOptions); @@ -357,33 +434,29 @@ class StateMachine { } /** - * @ignore * Validates the provided TLS options are secure. * - * @param {string} kmsProvider The KMS provider name. - * @param {ClientEncryptionTLSOptions} tlsOptions The client TLS options for the provider. + * @param kmsProvider - The KMS provider name. + * @param tlsOptions - The client TLS options for the provider. * - * @returns {Error} If any option is invalid. + * @returns An error if any option is invalid. */ - validateTlsOptions(kmsProvider, tlsOptions) { + validateTlsOptions(kmsProvider: string, tlsOptions: CSFLETlsOptions): MongoCryptError | void { const tlsOptionNames = Object.keys(tlsOptions); for (const option of INSECURE_TLS_OPTIONS) { if (tlsOptionNames.includes(option)) { - return new MongoCryptError( - `Insecure TLS options prohibited for ${kmsProvider}: ${option}` - ); + return new MongoCryptError(`Insecure TLS options prohibited for ${kmsProvider}: ${option}`); } } } /** - * @ignore * Sets only the valid secure TLS options. * - * @param {ClientEncryptionTLSOptions} tlsOptions The client TLS options for the provider. - * @param {Object} options The existing connection options. + * @param tlsOptions - The client TLS options for the provider. + * @param options - The existing connection options. */ - setTlsOptions(tlsOptions, options) { + setTlsOptions(tlsOptions: CSFLETlsOptions, options: tls.ConnectionOptions) { if (tlsOptions.tlsCertificateKeyFile) { const cert = fs.readFileSync(tlsOptions.tlsCertificateKeyFile); options.cert = options.key = cert; @@ -397,22 +470,26 @@ class StateMachine { } /** - * @ignore * Fetches collection info for a provided namespace, when libmongocrypt * enters the `MONGOCRYPT_CTX_NEED_MONGO_COLLINFO` state. The result is * used to inform libmongocrypt of the schema associated with this * namespace. Exposed for testing purposes. Do not directly invoke. * - * @param {MongoClient} client A MongoClient connected to the topology - * @param {string} ns The namespace to list collections from - * @param {object} filter A filter for the listCollections command - * @param {StateMachine~fetchCollectionInfoCallback} callback Invoked with the info of the requested collection, or with an error + * @param client - A MongoClient connected to the topology + * @param ns - The namespace to list collections from + * @param filter - A filter for the listCollections command + * @param callback - Invoked with the info of the requested collection, or with an error */ - fetchCollectionInfo(client, ns, filter, callback) { - const dbName = databaseNamespace(ns); + fetchCollectionInfo( + client: MongoClient, + ns: string, + filter: Document, + callback: Callback + ) { + const { db } = MongoDBCollectionNamespace.fromString(ns); client - .db(dbName) + .db(db) .listCollections(filter, { promoteLongs: false, promoteValues: false @@ -421,71 +498,74 @@ class StateMachine { .then( collections => { const info = collections.length > 0 ? serialize(collections[0]) : null; - return callback(null, info); + return callback(undefined, info); }, err => { - callback(err, null); + callback(err); } ); } /** - * @ignore * Calls to the mongocryptd to provide markings for a command. * Exposed for testing purposes. Do not directly invoke. - * @param {MongoClient} client A MongoClient connected to a mongocryptd - * @param {string} ns The namespace (database.collection) the command is being executed on - * @param {object} command The command to execute. - * @param {StateMachine~markCommandCallback} callback Invoked with the serialized and marked bson command, or with an error - * @returns {void} + * @param client - A MongoClient connected to a mongocryptd + * @param ns - The namespace (database.collection) the command is being executed on + * @param command - The command to execute. + * @param callback - Invoked with the serialized and marked bson command, or with an error */ - markCommand(client, ns, command, callback) { + markCommand( + client: MongoClient, + ns: string, + command: Uint8Array, + callback: Callback + ) { const options = { promoteLongs: false, promoteValues: false }; - const dbName = databaseNamespace(ns); + const { db } = MongoDBCollectionNamespace.fromString(ns); const rawCommand = deserialize(command, options); client - .db(dbName) + .db(db) .command(rawCommand, options) .then( response => { - return callback(null, serialize(response, this.options)); + return callback(undefined, serialize(response, this.bsonOptions)); }, err => { - callback(err, null); + callback(err); } ); } /** - * @ignore * Requests keys from the keyVault collection on the topology. * Exposed for testing purposes. Do not directly invoke. - * @param {MongoClient} client A MongoClient connected to the topology - * @param {string} keyVaultNamespace The namespace (database.collection) of the keyVault Collection - * @param {object} filter The filter for the find query against the keyVault Collection - * @param {StateMachine~fetchKeysCallback} callback Invoked with the found keys, or with an error - * @returns {void} + * @param client - A MongoClient connected to the topology + * @param keyVaultNamespace - The namespace (database.collection) of the keyVault Collection + * @param filter - The filter for the find query against the keyVault Collection + * @param callback - Invoked with the found keys, or with an error */ - fetchKeys(client, keyVaultNamespace, filter, callback) { - const dbName = databaseNamespace(keyVaultNamespace); - const collectionName = collectionNamespace(keyVaultNamespace); - filter = deserialize(filter); + fetchKeys( + client: MongoClient, + keyVaultNamespace: string, + filter: Uint8Array, + callback: Callback> + ) { + const { db: dbName, collection: collectionName } = + MongoDBCollectionNamespace.fromString(keyVaultNamespace); client .db(dbName) - .collection(collectionName, { readConcern: { level: 'majority' } }) - .find(filter) + .collection(collectionName, { readConcern: { level: 'majority' } }) + .find(deserialize(filter)) .toArray() .then( keys => { - return callback(null, keys); + return callback(undefined, keys); }, err => { - callback(err, null); + callback(err); } ); } } - -module.exports = { StateMachine }; diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index e87006f075a..5afb8a522be 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -2,7 +2,7 @@ import { clearTimeout, setTimeout } from 'timers'; import { promisify } from 'util'; import type { BSONSerializeOptions, Document, ObjectId } from '../bson'; -import type { AutoEncrypter } from '../client-side-encryption/autoEncrypter'; +import type { AutoEncrypter } from '../client-side-encryption/auto_encrypter'; import { CLOSE, CLUSTER_TIME_RECEIVED, @@ -598,8 +598,6 @@ export class CryptoConnection extends Connection { ? cmd.indexes.map((index: { key: Map }) => index.key) : null; - // TODO(NODE-5422): add typescript support - // @ts-expect-error no typescript support yet autoEncrypter.encrypt(ns.toString(), cmd, options, (err, encrypted) => { if (err || encrypted == null) { callback(err, null); @@ -612,6 +610,7 @@ export class CryptoConnection extends Connection { } if (indexKeys != null && cmd.createIndexes) { for (const [offset, index] of indexKeys.entries()) { + // @ts-expect-error `encrypted` is a generic "command", but we've narrowed for only `createIndexes` commands here encrypted.indexes[offset].key = index; } } diff --git a/src/deps.ts b/src/deps.ts index 319dbe1fc0c..aa5e9c3e4d9 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -1,9 +1,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -import type { Document } from './bson'; import { type Stream } from './cmap/connect'; -import type { ProxyOptions } from './cmap/connection'; import { MongoMissingDependencyError } from './error'; -import type { MongoClient } from './mongo_client'; import type { Callback } from './utils'; function makeErrorModule(error: any) { @@ -257,212 +254,10 @@ try { aws4 = require('aws4'); } catch {} // eslint-disable-line -/** @public */ -export const AutoEncryptionLoggerLevel = Object.freeze({ - FatalError: 0, - Error: 1, - Warning: 2, - Info: 3, - Trace: 4 -} as const); - -/** @public */ -export type AutoEncryptionLoggerLevel = - (typeof AutoEncryptionLoggerLevel)[keyof typeof AutoEncryptionLoggerLevel]; - -/** @public */ -export interface AutoEncryptionTlsOptions { - /** - * Specifies the location of a local .pem file that contains - * either the client's TLS/SSL certificate and key. - */ - tlsCertificateKeyFile?: string; - /** - * Specifies the password to de-crypt the tlsCertificateKeyFile. - */ - tlsCertificateKeyFilePassword?: string; - /** - * Specifies the location of a local .pem file that contains the - * root certificate chain from the Certificate Authority. - * This file is used to validate the certificate presented by the - * KMS provider. - */ - tlsCAFile?: string; -} - -/** @public */ -export interface AutoEncryptionOptions { - /** @internal client for metadata lookups */ - metadataClient?: MongoClient; - /** A `MongoClient` used to fetch keys from a key vault */ - keyVaultClient?: MongoClient; - /** The namespace where keys are stored in the key vault */ - keyVaultNamespace?: string; - /** Configuration options that are used by specific KMS providers during key generation, encryption, and decryption. */ - kmsProviders?: { - /** Configuration options for using 'aws' as your KMS provider */ - aws?: - | { - /** The access key used for the AWS KMS provider */ - accessKeyId: string; - /** The secret access key used for the AWS KMS provider */ - secretAccessKey: string; - /** - * An optional AWS session token that will be used as the - * X-Amz-Security-Token header for AWS requests. - */ - sessionToken?: string; - } - | Record; - /** Configuration options for using 'local' as your KMS provider */ - local?: { - /** - * The master key used to encrypt/decrypt data keys. - * A 96-byte long Buffer or base64 encoded string. - */ - key: Buffer | string; - }; - /** Configuration options for using 'azure' as your KMS provider */ - azure?: - | { - /** The tenant ID identifies the organization for the account */ - tenantId: string; - /** The client ID to authenticate a registered application */ - clientId: string; - /** The client secret to authenticate a registered application */ - clientSecret: string; - /** - * If present, a host with optional port. E.g. "example.com" or "example.com:443". - * This is optional, and only needed if customer is using a non-commercial Azure instance - * (e.g. a government or China account, which use different URLs). - * Defaults to "login.microsoftonline.com" - */ - identityPlatformEndpoint?: string | undefined; - } - | { - /** - * If present, an access token to authenticate with Azure. - */ - accessToken: string; - } - | Record; - /** Configuration options for using 'gcp' as your KMS provider */ - gcp?: - | { - /** The service account email to authenticate */ - email: string; - /** A PKCS#8 encrypted key. This can either be a base64 string or a binary representation */ - privateKey: string | Buffer; - /** - * If present, a host with optional port. E.g. "example.com" or "example.com:443". - * Defaults to "oauth2.googleapis.com" - */ - endpoint?: string | undefined; - } - | { - /** - * If present, an access token to authenticate with GCP. - */ - accessToken: string; - } - | Record; - /** - * Configuration options for using 'kmip' as your KMS provider - */ - kmip?: { - /** - * The output endpoint string. - * The endpoint consists of a hostname and port separated by a colon. - * E.g. "example.com:123". A port is always present. - */ - endpoint?: string; - }; - }; - /** - * A map of namespaces to a local JSON schema for encryption - * - * **NOTE**: Supplying options.schemaMap provides more security than relying on JSON Schemas obtained from the server. - * It protects against a malicious server advertising a false JSON Schema, which could trick the client into sending decrypted data that should be encrypted. - * Schemas supplied in the schemaMap only apply to configuring automatic encryption for Client-Side Field Level Encryption. - * Other validation rules in the JSON schema will not be enforced by the driver and will result in an error. - */ - schemaMap?: Document; - /** Supply a schema for the encrypted fields in the document */ - encryptedFieldsMap?: Document; - /** Allows the user to bypass auto encryption, maintaining implicit decryption */ - bypassAutoEncryption?: boolean; - /** Allows users to bypass query analysis */ - bypassQueryAnalysis?: boolean; - options?: { - /** An optional hook to catch logging messages from the underlying encryption engine */ - logger?: (level: AutoEncryptionLoggerLevel, message: string) => void; - }; - extraOptions?: { - /** - * A local process the driver communicates with to determine how to encrypt values in a command. - * Defaults to "mongodb://%2Fvar%2Fmongocryptd.sock" if domain sockets are available or "mongodb://localhost:27020" otherwise - */ - mongocryptdURI?: string; - /** If true, autoEncryption will not attempt to spawn a mongocryptd before connecting */ - mongocryptdBypassSpawn?: boolean; - /** The path to the mongocryptd executable on the system */ - mongocryptdSpawnPath?: string; - /** Command line arguments to use when auto-spawning a mongocryptd */ - mongocryptdSpawnArgs?: string[]; - /** - * Full path to a MongoDB Crypt shared library to be used (instead of mongocryptd). - * - * This needs to be the path to the file itself, not a directory. - * It can be an absolute or relative path. If the path is relative and - * its first component is `$ORIGIN`, it will be replaced by the directory - * containing the mongodb-client-encryption native addon file. Otherwise, - * the path will be interpreted relative to the current working directory. - * - * Currently, loading different MongoDB Crypt shared library files from different - * MongoClients in the same process is not supported. - * - * If this option is provided and no MongoDB Crypt shared library could be loaded - * from the specified location, creating the MongoClient will fail. - * - * If this option is not provided and `cryptSharedLibRequired` is not specified, - * the AutoEncrypter will attempt to spawn and/or use mongocryptd according - * to the mongocryptd-specific `extraOptions` options. - * - * Specifying a path prevents mongocryptd from being used as a fallback. - * - * Requires the MongoDB Crypt shared library, available in MongoDB 6.0 or higher. - */ - cryptSharedLibPath?: string; - /** - * If specified, never use mongocryptd and instead fail when the MongoDB Crypt - * shared library could not be loaded. - * - * This is always true when `cryptSharedLibPath` is specified. - * - * Requires the MongoDB Crypt shared library, available in MongoDB 6.0 or higher. - */ - cryptSharedLibRequired?: boolean; - /** - * Search paths for a MongoDB Crypt shared library to be used (instead of mongocryptd) - * Only for driver testing! - * @internal - */ - cryptSharedLibSearchPaths?: string[]; - }; - proxyOptions?: ProxyOptions; - /** The TLS options to use connecting to the KMS provider */ - tlsOptions?: { - aws?: AutoEncryptionTlsOptions; - local?: AutoEncryptionTlsOptions; - azure?: AutoEncryptionTlsOptions; - gcp?: AutoEncryptionTlsOptions; - kmip?: AutoEncryptionTlsOptions; - }; -} - -type MongoCrypt = { MongoCrypt: any }; /** A utility function to get the instance of mongodb-client-encryption, if it exists. */ -export function getMongoDBClientEncryption(): MongoCrypt | null { +export function getMongoDBClientEncryption(): + | typeof import('mongodb-client-encryption') + | { kModuleError: MongoMissingDependencyError } { let mongodbClientEncryption = null; try { @@ -470,11 +265,13 @@ export function getMongoDBClientEncryption(): MongoCrypt | null { // Cannot be moved to helper utility function, bundlers search and replace the actual require call // in a way that makes this line throw at bundle time, not runtime, catching here will make bundling succeed mongodbClientEncryption = require('mongodb-client-encryption'); - } catch { - // ignore + } catch (cause) { + const kModuleError = new MongoMissingDependencyError( + 'Optional module `mongodb-client-encryption` not found. Please install it to use auto encryption or ClientEncryption.', + { cause } + ); + return { kModuleError }; } return mongodbClientEncryption; } - -export type MongodbClientEncryption = ReturnType; diff --git a/src/encrypter.ts b/src/encrypter.ts index 53cfebacda4..f03e8d6c1a9 100644 --- a/src/encrypter.ts +++ b/src/encrypter.ts @@ -1,6 +1,6 @@ -import { AutoEncrypter } from './client-side-encryption/autoEncrypter'; +import { AutoEncrypter, type AutoEncryptionOptions } from './client-side-encryption/auto_encrypter'; import { MONGO_CLIENT_EVENTS } from './constants'; -import { type AutoEncryptionOptions, getMongoDBClientEncryption } from './deps'; +import { getMongoDBClientEncryption } from './deps'; import { MongoInvalidArgumentError, MongoMissingDependencyError } from './error'; import { MongoClient, type MongoClientOptions } from './mongo_client'; import { type Callback } from './utils'; @@ -118,7 +118,7 @@ export class Encrypter { static checkForMongoCrypt(): void { const mongodbClientEncryption = getMongoDBClientEncryption(); - if (mongodbClientEncryption == null) { + if ('kModuleError' in mongodbClientEncryption) { throw new MongoMissingDependencyError( 'Auto-encryption requested, but the module is not installed. ' + 'Please add `mongodb-client-encryption` as a dependency of your project' diff --git a/src/index.ts b/src/index.ts index 9e94d18dca7..38e5fdf9644 100644 --- a/src/index.ts +++ b/src/index.ts @@ -95,11 +95,11 @@ export { // enums export { BatchType } from './bulk/common'; +export { AutoEncryptionLoggerLevel } from './client-side-encryption/auto_encrypter'; export { GSSAPICanonicalizationValue } from './cmap/auth/gssapi'; export { AuthMechanism } from './cmap/auth/providers'; export { Compressor } from './cmap/wire_protocol/compression'; export { CURSOR_FLAGS } from './cursor/abstract_cursor'; -export { AutoEncryptionLoggerLevel } from './deps'; export { MongoErrorLabel } from './error'; export { ExplainVerbosity } from './explain'; export { ServerApiVersion } from './mongo_client'; @@ -201,8 +201,16 @@ export type { ResumeToken, UpdateDescription } from './change_stream'; -export type { AutoEncrypter } from './client-side-encryption/autoEncrypter'; -export type { MongocryptdManager } from './client-side-encryption/mongocryptdManager'; +export type { AutoEncrypter } from './client-side-encryption/auto_encrypter'; +export type { AutoEncryptionOptions } from './client-side-encryption/auto_encrypter'; +export type { AutoEncryptionExtraOptions } from './client-side-encryption/auto_encrypter'; +export type { MongocryptdManager } from './client-side-encryption/mongocryptd_manager'; +export type { KMSProviders } from './client-side-encryption/providers/index'; +export type { + CSFLEKMSTlsOptions, + CSFLETlsOptions, + StateMachineExecutable +} from './client-side-encryption/state_machine'; export type { AuthContext } from './cmap/auth/auth_provider'; export type { AuthMechanismProperties, @@ -287,7 +295,6 @@ export type { } from './cursor/list_search_indexes_cursor'; export type { RunCursorCommandOptions } from './cursor/run_command_cursor'; export type { DbOptions, DbPrivate } from './db'; -export type { AutoEncryptionOptions, AutoEncryptionTlsOptions } from './deps'; export type { Encrypter, EncrypterOptions } from './encrypter'; export type { AnyError, ErrorDescription, MongoNetworkErrorOptions } from './error'; export type { Explain, ExplainOptions, ExplainVerbosityLike } from './explain'; diff --git a/src/mongo_client.ts b/src/mongo_client.ts index eb5b5ac06ea..8d5fafb0882 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -4,7 +4,7 @@ import { promisify } from 'util'; import { type BSONSerializeOptions, type Document, resolveBSONOptions } from './bson'; import { ChangeStream, type ChangeStreamDocument, type ChangeStreamOptions } from './change_stream'; -import { type AutoEncrypter } from './client-side-encryption/autoEncrypter'; +import type { AutoEncrypter, AutoEncryptionOptions } from './client-side-encryption/auto_encrypter'; import { type AuthMechanismProperties, DEFAULT_ALLOWED_HOSTS, @@ -18,7 +18,6 @@ import type { CompressorName } from './cmap/wire_protocol/compression'; import { parseOptions, resolveSRVRecord } from './connection_string'; import { MONGO_CLIENT_EVENTS } from './constants'; import { Db, type DbOptions } from './db'; -import type { AutoEncryptionOptions } from './deps'; import type { Encrypter } from './encrypter'; import { MongoInvalidArgumentError } from './error'; import { MongoLogger, type MongoLoggerOptions } from './mongo_logger'; diff --git a/src/sdam/server.ts b/src/sdam/server.ts index c67ec609274..5ef614db077 100644 --- a/src/sdam/server.ts +++ b/src/sdam/server.ts @@ -1,7 +1,7 @@ import { promisify } from 'util'; import type { Document } from '../bson'; -import { type AutoEncrypter } from '../client-side-encryption/autoEncrypter'; +import { type AutoEncrypter } from '../client-side-encryption/auto_encrypter'; import { type CommandOptions, Connection, type DestroyOptions } from '../cmap/connection'; import { ConnectionPool, diff --git a/src/utils.ts b/src/utils.ts index aa9afc51a22..436c2049dac 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -346,6 +346,10 @@ export class MongoDBCollectionNamespace extends MongoDBNamespace { constructor(db: string, override collection: string) { super(db, collection); } + + static override fromString(namespace?: string): MongoDBCollectionNamespace { + return super.fromString(namespace) as MongoDBCollectionNamespace; + } } /** @internal */ diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.06.corpus.test.js b/test/integration/client-side-encryption/client_side_encryption.prose.06.corpus.test.js index f837cdd9f8e..ec5b7b35796 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.06.corpus.test.js +++ b/test/integration/client-side-encryption/client_side_encryption.prose.06.corpus.test.js @@ -10,7 +10,7 @@ const { expect } = require('chai'); const { getEncryptExtraOptions } = require('../../tools/utils'); const { installNodeDNSWorkaroundHooks } = require('../../tools/runner/hooks/configuration'); // eslint-disable-next-line no-restricted-modules -const { ClientEncryption } = require('../../../src/client-side-encryption/clientEncryption'); +const { ClientEncryption } = require('../../../src/client-side-encryption/client_encryption'); describe('Client Side Encryption Prose Corpus Test', function () { const metadata = { diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.12.deadlock.test.ts b/test/integration/client-side-encryption/client_side_encryption.prose.12.deadlock.test.ts index 77a7dafa3c0..edde58f2fbb 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.12.deadlock.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.prose.12.deadlock.test.ts @@ -5,7 +5,7 @@ import * as path from 'path'; import * as util from 'util'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports -import { ClientEncryption } from '../../../src/client-side-encryption/clientEncryption'; +import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption'; import { type CommandStartedEvent, MongoClient, type MongoClientOptions } from '../../mongodb'; import { installNodeDNSWorkaroundHooks } from '../../tools/runner/hooks/configuration'; import { getEncryptExtraOptions } from '../../tools/utils'; diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.14.decryption_events.test.ts b/test/integration/client-side-encryption/client_side_encryption.prose.14.decryption_events.test.ts index 46a01c2a6bd..424b01a1ace 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.14.decryption_events.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.prose.14.decryption_events.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports -import { ClientEncryption } from '../../../src/client-side-encryption/clientEncryption'; +import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption'; import { Binary, BSON, diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.17.on_demand_gcp.test.ts b/test/integration/client-side-encryption/client_side_encryption.prose.17.on_demand_gcp.test.ts index 5d2df93a11f..f069eb15a6f 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.17.on_demand_gcp.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.prose.17.on_demand_gcp.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { env } from 'process'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports -import { ClientEncryption } from '../../../src/client-side-encryption/clientEncryption'; +import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption'; import { Binary } from '../../mongodb'; const metadata: MongoDBMetadataUI = { diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.19.on_demand_azure.test.ts b/test/integration/client-side-encryption/client_side_encryption.prose.19.on_demand_azure.test.ts index 2ba17f7f6cd..e75c09e4fd3 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.19.on_demand_azure.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.prose.19.on_demand_azure.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { env } from 'process'; /* eslint-disable @typescript-eslint/no-restricted-imports */ -import { ClientEncryption } from '../../../src/client-side-encryption/clientEncryption'; +import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption'; /* eslint-disable @typescript-eslint/no-restricted-imports */ import { MongoCryptAzureKMSRequestError } from '../../../src/client-side-encryption/errors'; import { Binary } from '../../mongodb'; diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.21.automatic_data_encryption_keys.test.ts b/test/integration/client-side-encryption/client_side_encryption.prose.21.automatic_data_encryption_keys.test.ts index 493406e15ed..9990d773767 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.21.automatic_data_encryption_keys.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.prose.21.automatic_data_encryption_keys.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; /* eslint-disable @typescript-eslint/no-restricted-imports */ -import { ClientEncryption } from '../../../src/client-side-encryption/clientEncryption'; +import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption'; /* eslint-disable @typescript-eslint/no-restricted-imports */ import { MongoCryptCreateEncryptedCollectionError } from '../../../src/client-side-encryption/errors'; import { BSON, Collection, type Db, MongoServerError } from '../../mongodb'; diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.22.range_explicit_encryption.test.ts b/test/integration/client-side-encryption/client_side_encryption.prose.22.range_explicit_encryption.test.ts index b111becaf86..af5fbf9fe1a 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.22.range_explicit_encryption.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.prose.22.range_explicit_encryption.test.ts @@ -5,7 +5,7 @@ import { join } from 'path'; import { Decimal128, type Document, Double, Long, type MongoClient } from '../../../src'; /* eslint-disable @typescript-eslint/no-restricted-imports */ -import { ClientEncryption } from '../../../src/client-side-encryption/clientEncryption'; +import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption'; /* eslint-disable @typescript-eslint/no-restricted-imports */ import { MongoCryptError } from '../../../src/client-side-encryption/errors'; import { installNodeDNSWorkaroundHooks } from '../../tools/runner/hooks/configuration'; diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.test.js b/test/integration/client-side-encryption/client_side_encryption.prose.test.js index 667e1dabd50..2b1bf98a193 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.test.js +++ b/test/integration/client-side-encryption/client_side_encryption.prose.test.js @@ -17,7 +17,7 @@ const { externalSchema } = require('../../spec/client-side-encryption/external/external-schema.json'); /* eslint-disable no-restricted-modules */ -const { ClientEncryption } = require('../../../src/client-side-encryption/clientEncryption'); +const { ClientEncryption } = require('../../../src/client-side-encryption/client_encryption'); const getKmsProviders = (localKey, kmipEndpoint, azureEndpoint, gcpEndpoint) => { const result = BSON.EJSON.parse(process.env.CSFLE_KMS_PROVIDERS || '{}'); @@ -1507,7 +1507,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { expect.fail('it must fail with no tls'); } catch (e) { // Expect an error indicating TLS handshake failed. - expect(e.originalError.message).to.include('certificate required'); + expect(e.cause.message).to.include('certificate required'); } }); @@ -1530,7 +1530,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { expect.fail('it must fail with invalid certificate'); } catch (e) { // Expect an error indicating TLS handshake failed due to an expired certificate. - expect(e.originalError.message).to.include('certificate has expired'); + expect(e.cause.message).to.include('certificate has expired'); } }); @@ -1542,7 +1542,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { expect.fail('it must fail with invalid hostnames'); } catch (e) { // Expect an error indicating TLS handshake failed due to an invalid hostname. - expect(e.originalError.message).to.include('does not match certificate'); + expect(e.cause.message).to.include('does not match certificate'); } }); }); @@ -1560,7 +1560,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { expect.fail('it must fail with no tls'); } catch (e) { //Expect an error indicating TLS handshake failed. - expect(e.originalError.message).to.include('certificate required'); + expect(e.cause.message).to.include('certificate required'); } }); @@ -1581,7 +1581,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { expect.fail('it must fail with expired certificates'); } catch (e) { // Expect an error indicating TLS handshake failed due to an expired certificate. - expect(e.originalError.message).to.include('certificate has expired'); + expect(e.cause.message).to.include('certificate has expired'); } }); @@ -1591,7 +1591,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { expect.fail('it must fail with invalid hostnames'); } catch (e) { // Expect an error indicating TLS handshake failed due to an invalid hostname. - expect(e.originalError.message).to.include('does not match certificate'); + expect(e.cause.message).to.include('does not match certificate'); } }); }); @@ -1611,7 +1611,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { expect.fail('it must fail with no tls'); } catch (e) { //Expect an error indicating TLS handshake failed. - expect(e.originalError.message).to.include('certificate required'); + expect(e.cause.message).to.include('certificate required'); } }); @@ -1632,7 +1632,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { expect.fail('it must fail with expired certificates'); } catch (e) { // Expect an error indicating TLS handshake failed due to an expired certificate. - expect(e.originalError.message).to.include('certificate has expired'); + expect(e.cause.message).to.include('certificate has expired'); } }); @@ -1642,7 +1642,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { expect.fail('it must fail with invalid hostnames'); } catch (e) { // Expect an error indicating TLS handshake failed due to an invalid hostname. - expect(e.originalError.message).to.include('does not match certificate'); + expect(e.cause.message).to.include('does not match certificate'); } }); }); @@ -1661,7 +1661,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { expect.fail('it must fail with no tls'); } catch (e) { //Expect an error indicating TLS handshake failed. - expect(e.originalError.message).to.match(/before secure TLS connection|handshake/); + expect(e.cause.message).to.match(/before secure TLS connection|handshake/); } }); @@ -1677,7 +1677,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { expect.fail('it must fail with expired certificates'); } catch (e) { // Expect an error indicating TLS handshake failed due to an expired certificate. - expect(e.originalError.message).to.include('certificate has expired'); + expect(e.cause.message).to.include('certificate has expired'); } }); @@ -1687,7 +1687,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { expect.fail('it must fail with invalid hostnames'); } catch (e) { // Expect an error indicating TLS handshake failed due to an invalid hostname. - expect(e.originalError.message).to.include('does not match certificate'); + expect(e.cause.message).to.include('does not match certificate'); } }); }); diff --git a/test/integration/client-side-encryption/driver.test.ts b/test/integration/client-side-encryption/driver.test.ts index c01ae18a97f..d22033c890a 100644 --- a/test/integration/client-side-encryption/driver.test.ts +++ b/test/integration/client-side-encryption/driver.test.ts @@ -3,7 +3,7 @@ import { expect } from 'chai'; import * as crypto from 'crypto'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports -import { ClientEncryption } from '../../../src/client-side-encryption/clientEncryption'; +import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption'; import { type Collection, type CommandStartedEvent, type MongoClient } from '../../mongodb'; import * as BSON from '../../mongodb'; import { installNodeDNSWorkaroundHooks } from '../../tools/runner/hooks/configuration'; diff --git a/test/integration/node-specific/auto_encrypter.test.ts b/test/integration/node-specific/auto_encrypter.test.ts index 0ba023ed34a..e8da0fc8d90 100644 --- a/test/integration/node-specific/auto_encrypter.test.ts +++ b/test/integration/node-specific/auto_encrypter.test.ts @@ -6,11 +6,11 @@ import * as sinon from 'sinon'; import { promisify } from 'util'; /* eslint-disable @typescript-eslint/no-restricted-imports */ -import { AutoEncrypter } from '../../../src/client-side-encryption/autoEncrypter'; +import { AutoEncrypter } from '../../../src/client-side-encryption/auto_encrypter'; /* eslint-disable @typescript-eslint/no-restricted-imports */ -import { MongocryptdManager } from '../../../src/client-side-encryption/mongocryptdManager'; +import { MongocryptdManager } from '../../../src/client-side-encryption/mongocryptd_manager'; /* eslint-disable @typescript-eslint/no-restricted-imports */ -import { StateMachine } from '../../../src/client-side-encryption/stateMachine'; +import { StateMachine } from '../../../src/client-side-encryption/state_machine'; import { BSON, deserialize, serialize } from '../../mongodb'; import { type MongoClient, MongoError, MongoNetworkTimeoutError } from '../../mongodb'; import { getEncryptExtraOptions } from '../../tools/utils'; diff --git a/test/integration/node-specific/client_encryption.test.ts b/test/integration/node-specific/client_encryption.test.ts index 0b9a27f25a1..561fdde95d5 100644 --- a/test/integration/node-specific/client_encryption.test.ts +++ b/test/integration/node-specific/client_encryption.test.ts @@ -6,9 +6,11 @@ import * as sinon from 'sinon'; import { ClientEncryption, type DataKey -} from '../../../src/client-side-encryption/clientEncryption'; +} from '../../../src/client-side-encryption/client_encryption'; /* eslint-disable @typescript-eslint/no-restricted-imports */ -import { StateMachine } from '../../../src/client-side-encryption/stateMachine'; +import { MongoCryptInvalidArgumentError } from '../../../src/client-side-encryption/errors'; +/* eslint-disable @typescript-eslint/no-restricted-imports */ +import { StateMachine } from '../../../src/client-side-encryption/state_machine'; import { Binary, type Collection, Int32, Long, type MongoClient } from '../../mongodb'; function readHttpResponse(path) { @@ -538,7 +540,7 @@ describe('ClientEncryption integration tests', function () { it(`should fail if typeof keyAltNames = ${typeof val}`, metadata, function () { const options = makeOptions(val); expect(() => clientEncryption.createDataKey('aws', options, () => undefined)).to.throw( - TypeError + MongoCryptInvalidArgumentError ); }); }); @@ -547,7 +549,7 @@ describe('ClientEncryption integration tests', function () { it(`should fail if typeof keyAltNames[x] = ${typeof val}`, metadata, function () { const options = makeOptions([val]); expect(() => clientEncryption.createDataKey('aws', options, () => undefined)).to.throw( - TypeError + MongoCryptInvalidArgumentError ); }); }); diff --git a/test/integration/node-specific/crypto_callbacks.test.ts b/test/integration/node-specific/crypto_callbacks.test.ts index 3390ae01f5a..e3564164eb7 100644 --- a/test/integration/node-specific/crypto_callbacks.test.ts +++ b/test/integration/node-specific/crypto_callbacks.test.ts @@ -2,9 +2,9 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; /* eslint-disable @typescript-eslint/no-restricted-imports */ -import { ClientEncryption } from '../../../src/client-side-encryption/clientEncryption'; +import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption'; /* eslint-disable @typescript-eslint/no-restricted-imports */ -import * as cryptoCallbacks from '../../../src/client-side-encryption/cryptoCallbacks'; +import * as cryptoCallbacks from '../../../src/client-side-encryption/crypto_callbacks'; import { type MongoClient } from '../../mongodb'; // Data Key Stuff diff --git a/test/tools/fixtures/shared_library_test.js b/test/tools/fixtures/shared_library_test.js index 0b1eab9d743..e87140a4834 100644 --- a/test/tools/fixtures/shared_library_test.js +++ b/test/tools/fixtures/shared_library_test.js @@ -1,6 +1,6 @@ /* eslint-disable no-restricted-modules */ const { EJSON } = require('bson'); -const { AutoEncrypter } = require('../../../lib/client-side-encryption/autoEncrypter'); +const { AutoEncrypter } = require('../../../lib/client-side-encryption/auto_encrypter'); const { MongoClient } = require('../../../lib/mongo_client'); try { diff --git a/test/tools/unified-spec-runner/unified-utils.ts b/test/tools/unified-spec-runner/unified-utils.ts index 106d284890e..d15a1b30638 100644 --- a/test/tools/unified-spec-runner/unified-utils.ts +++ b/test/tools/unified-spec-runner/unified-utils.ts @@ -5,7 +5,7 @@ import { gte as semverGte, lte as semverLte } from 'semver'; import { isDeepStrictEqual } from 'util'; /* eslint-disable @typescript-eslint/no-restricted-imports */ -import { ClientEncryption } from '../../../src/client-side-encryption/clientEncryption'; +import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption'; import { type AutoEncryptionOptions, type CollectionOptions, diff --git a/test/unit/client-side-encryption/autoEncrypter.test.js b/test/unit/client-side-encryption/auto_encrypter.test.ts similarity index 78% rename from test/unit/client-side-encryption/autoEncrypter.test.js rename to test/unit/client-side-encryption/auto_encrypter.test.ts index 96c76f9b74a..965bce6e3d5 100644 --- a/test/unit/client-side-encryption/autoEncrypter.test.js +++ b/test/unit/client-side-encryption/auto_encrypter.test.ts @@ -1,20 +1,20 @@ -'use strict'; +import { expect } from 'chai'; +import * as fs from 'fs'; +import * as sinon from 'sinon'; + +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { AutoEncrypter } from '../../../src/client-side-encryption/auto_encrypter'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { MongocryptdManager } from '../../../src/client-side-encryption/mongocryptd_manager'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { StateMachine } from '../../../src/client-side-encryption/state_machine'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { MongoClient } from '../../../src/mongo_client'; +import { BSON } from '../../mongodb'; +import * as requirements from './requirements.helper'; -const fs = require('fs'); -const path = require('path'); -const sinon = require('sinon'); -const { MongoClient } = require('../../../src/mongo_client'); -const { BSON } = require('../../mongodb'); const bson = BSON; const { EJSON } = BSON; -const requirements = require('./requirements.helper'); -const { MongoError, MongoNetworkTimeoutError } = require('../../../src/error'); -const { StateMachine } = require('../../../src/client-side-encryption/stateMachine'); - -const { AutoEncrypter } = require('../../../src/client-side-encryption/autoEncrypter'); -const { MongocryptdManager } = require('../../../src/client-side-encryption/mongocryptdManager'); - -const { expect } = require('chai'); function readExtendedJsonToBuffer(path) { const ejson = EJSON.parse(fs.readFileSync(path, 'utf8')); @@ -27,7 +27,9 @@ function readHttpResponse(path) { return Buffer.from(data, 'utf8'); } -const TEST_COMMAND = JSON.parse(fs.readFileSync(`${__dirname}/data/cmd.json`)); +const TEST_COMMAND = JSON.parse( + fs.readFileSync(`${__dirname}/data/cmd.json`, { encoding: 'utf-8' }) +); const MOCK_COLLINFO_RESPONSE = readExtendedJsonToBuffer(`${__dirname}/data/collection-info.json`); const MOCK_MONGOCRYPTD_RESPONSE = readExtendedJsonToBuffer( `${__dirname}/data/mongocryptd-reply.json` @@ -35,13 +37,7 @@ const MOCK_MONGOCRYPTD_RESPONSE = readExtendedJsonToBuffer( const MOCK_KEYDOCUMENT_RESPONSE = readExtendedJsonToBuffer(`${__dirname}/data/key-document.json`); const MOCK_KMS_DECRYPT_REPLY = readHttpResponse(`${__dirname}/data/kms-decrypt-reply.txt`); -class MockClient { - constructor() { - this.topology = { - bson - }; - } -} +class MockClient {} const originalAccessKeyId = process.env.AWS_ACCESS_KEY_ID; const originalSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; @@ -49,7 +45,7 @@ const originalSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; describe('AutoEncrypter', function () { this.timeout(12000); let ENABLE_LOG_TEST = false; - let sandbox = sinon.createSandbox(); + const sandbox = sinon.createSandbox(); beforeEach(() => { sandbox.restore(); sandbox.stub(StateMachine.prototype, 'kmsRequest').callsFake(request => { @@ -91,11 +87,14 @@ describe('AutoEncrypter', function () { describe('#constructor', function () { context('when using mongocryptd', function () { - const client = new MockClient(); + const client = new MockClient() as MongoClient; const autoEncrypterOptions = { mongocryptdBypassSpawn: true, keyVaultNamespace: 'admin.datakeys', - logger: () => { }, + options: { + // eslint-disable-next-line @typescript-eslint/no-empty-function + logger: () => {} + }, kmsProviders: { aws: { accessKeyId: 'example', secretAccessKey: 'example' }, local: { key: Buffer.alloc(96) } @@ -104,21 +103,19 @@ describe('AutoEncrypter', function () { const autoEncrypter = new AutoEncrypter(client, autoEncrypterOptions); it('instantiates a mongo client on the auto encrypter', function () { - expect(autoEncrypter) - .to.have.property('_mongocryptdClient') - .to.be.instanceOf(MongoClient); + expect(autoEncrypter).to.have.property('_mongocryptdClient').to.be.instanceOf(MongoClient); }); it('sets serverSelectionTimeoutMS to 10000ms', function () { expect(autoEncrypter).to.have.nested.property('_mongocryptdClient.s.options'); - const options = autoEncrypter._mongocryptdClient.s.options; + const options = autoEncrypter._mongocryptdClient?.s.options; expect(options).to.have.property('serverSelectionTimeoutMS', 10000); }); context('when mongocryptdURI is not specified', () => { it('sets the ip address family to ipv4', function () { expect(autoEncrypter).to.have.nested.property('_mongocryptdClient.s.options'); - const options = autoEncrypter._mongocryptdClient.s.options; + const options = autoEncrypter._mongocryptdClient?.s.options; expect(options).to.have.property('family', 4); }); }); @@ -131,7 +128,7 @@ describe('AutoEncrypter', function () { }); expect(autoEncrypter).to.have.nested.property('_mongocryptdClient.s.options'); - const options = autoEncrypter._mongocryptdClient.s.options; + const options = autoEncrypter._mongocryptdClient?.s.options; expect(options).not.to.have.property('family', 4); }); }); @@ -144,7 +141,10 @@ describe('AutoEncrypter', function () { bypassAutoEncryption: true, mongocryptdBypassSpawn: true, keyVaultNamespace: 'admin.datakeys', - logger: () => { }, + options: { + // eslint-disable-next-line @typescript-eslint/no-empty-function + logger: () => {} + }, kmsProviders: { aws: { accessKeyId: 'example', secretAccessKey: 'example' }, local: { key: Buffer.alloc(96) } @@ -161,10 +161,13 @@ describe('AutoEncrypter', function () { describe('state machine', function () { it('should decrypt mock data', function (done) { const input = readExtendedJsonToBuffer(`${__dirname}/data/encrypted-document.json`); - const client = new MockClient(); + const client = new MockClient() as MongoClient; const mc = new AutoEncrypter(client, { keyVaultNamespace: 'admin.datakeys', - logger: () => { }, + options: { + // eslint-disable-next-line @typescript-eslint/no-empty-function + logger: () => {} + }, kmsProviders: { aws: { accessKeyId: 'example', secretAccessKey: 'example' }, local: { key: Buffer.alloc(96) } @@ -187,7 +190,10 @@ describe('AutoEncrypter', function () { const client = new MockClient(); const mc = new AutoEncrypter(client, { keyVaultNamespace: 'admin.datakeys', - logger: () => { }, + options: { + // eslint-disable-next-line @typescript-eslint/no-empty-function + logger: () => {} + }, kmsProviders: { aws: { accessKeyId: 'example', secretAccessKey: 'example' }, local: { key: Buffer.alloc(96) } @@ -220,27 +226,7 @@ describe('AutoEncrypter', function () { }); }); - it('should decrypt mock data with per-context KMS credentials', function (done) { - const input = readExtendedJsonToBuffer(`${__dirname}/data/encrypted-document.json`); - const client = new MockClient(); - const mc = new AutoEncrypter(client, { - keyVaultNamespace: 'admin.datakeys', - logger: () => { }, - kmsProviders: { - aws: {} - }, - async onKmsProviderRefresh() { - return { aws: { accessKeyId: 'example', secretAccessKey: 'example' } }; - } - }); - mc.decrypt(input, (err, decrypted) => { - if (err) return done(err); - expect(decrypted).to.eql({ filter: { find: 'test', ssn: '457-55-5462' } }); - done(); - }); - }); - - context('when no refresh function is provided', function () { + context('when the aws sdk is installed', function () { const accessKey = 'example'; const secretKey = 'example'; @@ -266,7 +252,10 @@ describe('AutoEncrypter', function () { const client = new MockClient(); const mc = new AutoEncrypter(client, { keyVaultNamespace: 'admin.datakeys', - logger: () => { }, + options: { + // eslint-disable-next-line @typescript-eslint/no-empty-function + logger: () => {} + }, kmsProviders: { aws: {} } @@ -305,7 +294,10 @@ describe('AutoEncrypter', function () { const client = new MockClient(); const mc = new AutoEncrypter(client, { keyVaultNamespace: 'admin.datakeys', - logger: () => { }, + options: { + // eslint-disable-next-line @typescript-eslint/no-empty-function + logger: () => {} + }, kmsProviders: { aws: {} } @@ -323,7 +315,10 @@ describe('AutoEncrypter', function () { const client = new MockClient(); const mc = new AutoEncrypter(client, { keyVaultNamespace: 'admin.datakeys', - logger: () => { }, + options: { + // eslint-disable-next-line @typescript-eslint/no-empty-function + logger: () => {} + }, kmsProviders: { aws: { accessKeyId: 'example', secretAccessKey: 'example' }, local: { key: Buffer.alloc(96) } @@ -351,41 +346,6 @@ describe('AutoEncrypter', function () { done(); }); }); - - it('should encrypt mock data with per-context KMS credentials', function (done) { - const client = new MockClient(); - const mc = new AutoEncrypter(client, { - keyVaultNamespace: 'admin.datakeys', - logger: () => { }, - kmsProviders: { - aws: {} - }, - async onKmsProviderRefresh() { - return { aws: { accessKeyId: 'example', secretAccessKey: 'example' } }; - } - }); - - mc.encrypt('test.test', TEST_COMMAND, (err, encrypted) => { - if (err) return done(err); - const expected = EJSON.parse( - JSON.stringify({ - find: 'test', - filter: { - ssn: { - $binary: { - base64: - 'AWFhYWFhYWFhYWFhYWFhYWECRTOW9yZzNDn5dGwuqsrJQNLtgMEKaujhs9aRWRp+7Yo3JK8N8jC8P0Xjll6C1CwLsE/iP5wjOMhVv1KMMyOCSCrHorXRsb2IKPtzl2lKTqQ=', - subType: '6' - } - } - } - }) - ); - - expect(encrypted).to.containSubset(expected); - done(); - }); - }); }); describe('logging', function () { diff --git a/test/unit/client-side-encryption/clientEncryption.test.js b/test/unit/client-side-encryption/client_encryption.test.ts similarity index 89% rename from test/unit/client-side-encryption/clientEncryption.test.js rename to test/unit/client-side-encryption/client_encryption.test.ts index 5927b6e69d7..5e2586990ac 100644 --- a/test/unit/client-side-encryption/clientEncryption.test.js +++ b/test/unit/client-side-encryption/client_encryption.test.ts @@ -1,19 +1,22 @@ -'use strict'; -const fs = require('fs'); -const { expect } = require('chai'); -const sinon = require('sinon'); -const { MongoClient } = require('../../../lib/mongo_client'); -const cryptoCallbacks = require('../../../src/client-side-encryption/cryptoCallbacks'); -const { StateMachine } = require('../../../src/client-side-encryption/stateMachine'); -const { ClientEncryption } = require('../../../src/client-side-encryption/clientEncryption') -const { Binary, BSON, deserialize, Long, Int32 } = require('../../mongodb'); -const { EJSON } = BSON; +import { expect } from 'chai'; +import * as fs from 'fs'; +import { resolve } from 'path'; +import * as sinon from 'sinon'; + +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import * as cryptoCallbacks from '../../../src/client-side-encryption/crypto_callbacks'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { + MongoCryptCreateDataKeyError, + MongoCryptCreateEncryptedCollectionError +} from '../../../src/client-side-encryption/errors'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { StateMachine } from '../../../src/client-side-encryption/state_machine'; +import { Binary, BSON, deserialize } from '../../mongodb'; -const { - MongoCryptCreateEncryptedCollectionError, - MongoCryptCreateDataKeyError -} = require('../../../src/client-side-encryption/errors'); -const { resolve } = require('path'); +const { EJSON } = BSON; class MockClient { db(dbName) { @@ -25,14 +28,11 @@ class MockClient { } } - describe('ClientEncryption', function () { this.timeout(12000); - - context('with stubbed key material and fixed random source', function () { - let sandbox = sinon.createSandbox(); + const sandbox = sinon.createSandbox(); afterEach(() => { sandbox.restore(); @@ -44,7 +44,7 @@ describe('ClientEncryption', function () { ); let rndPos = 0; sandbox.stub(cryptoCallbacks, 'randomHook').callsFake((buffer, count) => { - if (rndPos + count > rndData) { + if (rndPos + count > rndData.length) { return new Error('Out of fake random data'); } buffer.set(rndData.subarray(rndPos, rndPos + count)); @@ -56,10 +56,12 @@ describe('ClientEncryption', function () { sandbox.stub(StateMachine.prototype, 'fetchKeys').callsFake((client, ns, filter, cb) => { filter = deserialize(filter); const keyIds = filter.$or[0]._id.$in.map(key => key.toString('hex')); - const fileNames = keyIds.map( - keyId => resolve(`${__dirname}/data/keys/${keyId.toUpperCase()}-local-document.json`) + const fileNames = keyIds.map(keyId => + resolve(`${__dirname}/data/keys/${keyId.toUpperCase()}-local-document.json`) + ); + const contents = fileNames.map(filename => + EJSON.parse(fs.readFileSync(filename, { encoding: 'utf-8' })) ); - const contents = fileNames.map(filename => EJSON.parse(fs.readFileSync(filename))); cb(null, contents); }); }); @@ -94,7 +96,6 @@ describe('ClientEncryption', function () { }); describe('createEncryptedCollection()', () => { - /** @type {InstanceType} */ let clientEncryption; const client = new MockClient(); let db; @@ -118,21 +119,27 @@ describe('ClientEncryption', function () { const error = await clientEncryption .createEncryptedCollection(db, collectionName) .catch(error => error); - expect(error).to.be.instanceOf(TypeError, /provider/); + expect(error) + .to.be.instanceOf(TypeError) + .to.match(/provider/); }); it('throws TypeError if options.createCollectionOptions are omitted', async () => { const error = await clientEncryption .createEncryptedCollection(db, collectionName, {}) .catch(error => error); - expect(error).to.be.instanceOf(TypeError, /encryptedFields/); + expect(error) + .to.be.instanceOf(TypeError) + .to.match(/encryptedFields/); }); it('throws TypeError if options.createCollectionOptions.encryptedFields are omitted', async () => { const error = await clientEncryption .createEncryptedCollection(db, collectionName, { createCollectionOptions: {} }) .catch(error => error); - expect(error).to.be.instanceOf(TypeError, /Cannot read properties/); + expect(error) + .to.be.instanceOf(TypeError) + .to.match(/Cannot read properties/); }); }); diff --git a/test/unit/client-side-encryption/common.test.js b/test/unit/client-side-encryption/common.test.js index 247c593a703..93232d9779a 100644 --- a/test/unit/client-side-encryption/common.test.js +++ b/test/unit/client-side-encryption/common.test.js @@ -1,7 +1,8 @@ 'use strict'; const { expect } = require('chai'); -const maybeCallback = require('../../../src/client-side-encryption/common').maybeCallback; +// eslint-disable-next-line no-restricted-modules +const { maybeCallback } = require('../../../src/client-side-encryption/common'); describe('maybeCallback()', () => { it('should accept two arguments', () => { diff --git a/test/unit/client-side-encryption/mongocryptdManager.test.js b/test/unit/client-side-encryption/mongocryptd_manager.test.ts similarity index 78% rename from test/unit/client-side-encryption/mongocryptdManager.test.js rename to test/unit/client-side-encryption/mongocryptd_manager.test.ts index 19529a19d39..8122841e3b5 100644 --- a/test/unit/client-side-encryption/mongocryptdManager.test.js +++ b/test/unit/client-side-encryption/mongocryptd_manager.test.ts @@ -1,25 +1,25 @@ -'use strict'; +import { expect } from 'chai'; -const MongocryptdManager = require('../../../src/client-side-encryption/mongocryptdManager').MongocryptdManager; -const { expect } = require('chai'); +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { MongocryptdManager } from '../../../src/client-side-encryption/mongocryptd_manager'; describe('MongocryptdManager', function () { it('should default to having spawnArgs of --idleShutdownTimeoutSecs=60', function () { const mcdm = new MongocryptdManager(); - expect(mcdm.spawnArgs).to.deep.equal(['--idleShutdownTimeoutSecs', 60]); + expect(mcdm.spawnArgs).to.deep.equal(['--idleShutdownTimeoutSecs', '60']); }); it('should concat --idleShutdownTimeoutSecs=60 to provided args', function () { - const mcdm = new MongocryptdManager({ mongocryptdSpawnArgs: ['foo', 12] }); - expect(mcdm.spawnArgs).to.deep.equal(['foo', 12, '--idleShutdownTimeoutSecs', 60]); + const mcdm = new MongocryptdManager({ mongocryptdSpawnArgs: ['foo', '12'] }); + expect(mcdm.spawnArgs).to.deep.equal(['foo', '12', '--idleShutdownTimeoutSecs', '60']); }); it('should not override `idleShutdownTimeoutSecs` if the user sets it using `key value` form', function () { const mcdm = new MongocryptdManager({ - mongocryptdSpawnArgs: ['--idleShutdownTimeoutSecs', 12] + mongocryptdSpawnArgs: ['--idleShutdownTimeoutSecs', '12'] }); - expect(mcdm.spawnArgs).to.deep.equal(['--idleShutdownTimeoutSecs', 12]); + expect(mcdm.spawnArgs).to.deep.equal(['--idleShutdownTimeoutSecs', '12']); }); it('should not override `idleShutdownTimeoutSecs` if the user sets it using `key=value` form', function () { diff --git a/test/unit/client-side-encryption/providers/credentialsProvider.test.ts b/test/unit/client-side-encryption/providers/credentialsProvider.test.ts index f8f735033bb..ddd9c87b453 100644 --- a/test/unit/client-side-encryption/providers/credentialsProvider.test.ts +++ b/test/unit/client-side-encryption/providers/credentialsProvider.test.ts @@ -11,7 +11,7 @@ import { import { isEmptyCredentials, type KMSProviders, - loadCredentials + refreshKMSCredentials } from '../../../../src/client-side-encryption/providers'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { @@ -26,7 +26,7 @@ const originalAccessKeyId = process.env.AWS_ACCESS_KEY_ID; const originalSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; const originalSessionToken = process.env.AWS_SESSION_TOKEN; -describe('#loadCredentials', function () { +describe('#refreshKMSCredentials', function () { context('isEmptyCredentials()', () => { it('returns true for an empty object', () => { expect(isEmptyCredentials('aws', { aws: {} })).to.be.true; @@ -77,15 +77,15 @@ describe('#loadCredentials', function () { const kmsProviders = { aws: {} }; before(function () { - if (!requirements.credentialProvidersInstalled.aws) { - this.currentTest!.skipReason = 'Cannot refresh credentials without sdk provider'; - this.currentTest!.skip(); + if (!requirements.credentialProvidersInstalled.aws && this.currentTest) { + this.currentTest.skipReason = 'Cannot refresh credentials without sdk provider'; + this.currentTest.skip(); return; } }); it('refreshes the aws credentials', async function () { - const providers = await loadCredentials(kmsProviders); + const providers = await refreshKMSCredentials(kmsProviders); expect(providers).to.deep.equal({ aws: { accessKeyId: accessKey, @@ -106,15 +106,15 @@ describe('#loadCredentials', function () { }; before(function () { - if (!requirements.credentialProvidersInstalled.aws) { - this.currentTest!.skipReason = 'Cannot refresh credentials without sdk provider'; - this.currentTest!.skip(); + if (!requirements.credentialProvidersInstalled.aws && this.currentTest) { + this.currentTest.skipReason = 'Cannot refresh credentials without sdk provider'; + this.currentTest.skip(); return; } }); it('refreshes only the aws credentials', async function () { - const providers = await loadCredentials(kmsProviders); + const providers = await refreshKMSCredentials(kmsProviders); expect(providers).to.deep.equal({ local: { key: Buffer.alloc(96) @@ -139,15 +139,15 @@ describe('#loadCredentials', function () { }; before(function () { - if (!requirements.credentialProvidersInstalled.aws) { - this.currentTest!.skipReason = 'Cannot refresh credentials without sdk provider'; - this.currentTest!.skip(); + if (!requirements.credentialProvidersInstalled.aws && this.currentTest) { + this.currentTest.skipReason = 'Cannot refresh credentials without sdk provider'; + this.currentTest.skip(); return; } }); it('does not refresh credentials', async function () { - const providers = await loadCredentials(kmsProviders); + const providers = await refreshKMSCredentials(kmsProviders); expect(providers).to.deep.equal(kmsProviders); }); }); @@ -163,15 +163,15 @@ describe('#loadCredentials', function () { }; before(function () { - if (requirements.credentialProvidersInstalled.aws) { - this.currentTest!.skipReason = 'Credentials will be loaded when sdk present'; - this.currentTest!.skip(); + if (requirements.credentialProvidersInstalled.aws && this.currentTest) { + this.currentTest.skipReason = 'Credentials will be loaded when sdk present'; + this.currentTest.skip(); return; } }); it('does not refresh credentials', async function () { - const providers = await loadCredentials(kmsProviders); + const providers = await refreshKMSCredentials(kmsProviders); expect(providers).to.deep.equal(kmsProviders); }); }); @@ -209,9 +209,9 @@ describe('#loadCredentials', function () { context('and gcp-metadata is installed', () => { beforeEach(function () { - if (!requirements.credentialProvidersInstalled.gcp) { - this.currentTest!.skipReason = 'Tests require gcp-metadata to be installed'; - this.currentTest!.skip(); + if (!requirements.credentialProvidersInstalled.gcp && this.currentTest) { + this.currentTest.skipReason = 'Tests require gcp-metadata to be installed'; + this.currentTest.skip(); return; } }); @@ -222,7 +222,7 @@ describe('#loadCredentials', function () { const kmsProviders = { gcp: {} }; it('refreshes the gcp credentials', async function () { - const providers = await loadCredentials(kmsProviders); + const providers = await refreshKMSCredentials(kmsProviders); expect(providers).to.deep.equal({ gcp: { accessToken: 'abc' @@ -238,7 +238,7 @@ describe('#loadCredentials', function () { const kmsProviders = { gcp: {} }; it('surfaces error from server', async function () { - const error = await loadCredentials(kmsProviders).catch(error => error); + const error = await refreshKMSCredentials(kmsProviders).catch(error => error); expect(error).to.be.instanceOf(Error); }); }); @@ -247,9 +247,9 @@ describe('#loadCredentials', function () { context('and gcp-metadata is not installed', () => { beforeEach(function () { - if (requirements.credentialProvidersInstalled.gcp) { - this.currentTest!.skipReason = 'Tests require gcp-metadata to be installed'; - this.currentTest!.skip(); + if (requirements.credentialProvidersInstalled.gcp && this.currentTest) { + this.currentTest.skipReason = 'Tests require gcp-metadata to be installed'; + this.currentTest.skip(); return; } }); @@ -258,7 +258,7 @@ describe('#loadCredentials', function () { const kmsProviders = { gcp: {} }; it('does not modify the gcp credentials', async function () { - const providers = await loadCredentials(kmsProviders); + const providers = await refreshKMSCredentials(kmsProviders); expect(providers).to.deep.equal({ gcp: {} }); }); }); @@ -358,7 +358,7 @@ describe('#loadCredentials', function () { httpSpy = sinon.stub(utils, 'get'); httpSpy.resolves(mockResponse); - await loadCredentials({ azure: {} }); + await refreshKMSCredentials({ azure: {} }); }); it('sets the `api-version` param to 2012-02-01', () => { @@ -441,7 +441,7 @@ describe('#loadCredentials', function () { }); it('throws a MongoCryptKMSRequestError', async () => { - const error = await loadCredentials({ azure: {} }).catch(e => e); + const error = await refreshKMSCredentials({ azure: {} }).catch(e => e); expect(error).to.be.instanceOf(MongoCryptAzureKMSRequestError); }); }); @@ -453,7 +453,7 @@ describe('#loadCredentials', function () { }); it('throws a MongoCryptKMSRequestError', async () => { - const error = await loadCredentials({ azure: {} }).catch(e => e); + const error = await refreshKMSCredentials({ azure: {} }).catch(e => e); expect(error).to.be.instanceOf(MongoCryptAzureKMSRequestError); expect(error).to.match(/Malformed JSON body in GET request/); }); @@ -465,7 +465,7 @@ describe('#loadCredentials', function () { }); it('throws a MongoCryptKMSRequestError', async () => { - const error = await loadCredentials({ azure: {} }).catch(e => e); + const error = await refreshKMSCredentials({ azure: {} }).catch(e => e); expect(error).to.be.instanceOf(MongoCryptAzureKMSRequestError); expect(error).to.match(/Malformed JSON body in GET request/); }); @@ -479,12 +479,12 @@ describe('#loadCredentials', function () { }); it('throws a MongoCryptKMSRequestError', async () => { - const error = await loadCredentials({ azure: {} }).catch(e => e); + const error = await refreshKMSCredentials({ azure: {} }).catch(e => e); expect(error).to.be.instanceOf(MongoCryptAzureKMSRequestError); }); it('attaches the body to the error', async () => { - const error = await loadCredentials({ azure: {} }).catch(e => e); + const error = await refreshKMSCredentials({ azure: {} }).catch(e => e); expect(error).to.have.property('body').to.deep.equal({ error: 'something went wrong' }); }); }); @@ -497,7 +497,7 @@ describe('#loadCredentials', function () { }); it('throws a MongoCryptKMSRequestError', async () => { - const error = await loadCredentials({ azure: {} }).catch(e => e); + const error = await refreshKMSCredentials({ azure: {} }).catch(e => e); expect(error).to.be.instanceOf(MongoCryptAzureKMSRequestError); expect(error).to.match(/Malformed JSON body in GET request/); }); @@ -509,7 +509,7 @@ describe('#loadCredentials', function () { }); it('throws a MongoCryptKMSRequestError', async () => { - const error = await loadCredentials({ azure: {} }).catch(e => e); + const error = await refreshKMSCredentials({ azure: {} }).catch(e => e); expect(error).to.be.instanceOf(MongoCryptAzureKMSRequestError); expect(error).to.match(/Malformed JSON body in GET request/); }); @@ -521,7 +521,7 @@ describe('#loadCredentials', function () { }); it('throws a MongoCryptKMSRequestError', async () => { - const error = await loadCredentials({ azure: {} }).catch(e => e); + const error = await refreshKMSCredentials({ azure: {} }).catch(e => e); expect(error).to.be.instanceOf(MongoCryptAzureKMSRequestError); expect(error).to.match(/missing field `access_token/); }); @@ -533,7 +533,7 @@ describe('#loadCredentials', function () { }); it('throws a MongoCryptKMSRequestError', async () => { - const error = await loadCredentials({ azure: {} }).catch(e => e); + const error = await refreshKMSCredentials({ azure: {} }).catch(e => e); expect(error).to.be.instanceOf(MongoCryptAzureKMSRequestError); expect(error).to.match(/missing field `expires_in/); }); @@ -548,7 +548,7 @@ describe('#loadCredentials', function () { }); it('throws a MongoCryptKMSRequestError', async () => { - const error = await loadCredentials({ azure: {} }).catch(e => e); + const error = await refreshKMSCredentials({ azure: {} }).catch(e => e); expect(error).to.be.instanceOf(MongoCryptAzureKMSRequestError); expect(error).to.match(/unable to parse int from `expires_in` field/); }); @@ -563,7 +563,7 @@ describe('#loadCredentials', function () { }); it('returns the token in the `azure` field of the kms providers', async () => { - const kmsProviders = await loadCredentials({ azure: {} }); + const kmsProviders = await refreshKMSCredentials({ azure: {} }); const azure = kmsProviders.azure; expect(azure).to.have.property('accessToken', 'token'); }); diff --git a/test/unit/client-side-encryption/stateMachine.test.js b/test/unit/client-side-encryption/state_machine.test.ts similarity index 85% rename from test/unit/client-side-encryption/stateMachine.test.js rename to test/unit/client-side-encryption/state_machine.test.ts index 63ab990861c..f1a7042d46e 100644 --- a/test/unit/client-side-encryption/stateMachine.test.js +++ b/test/unit/client-side-encryption/state_machine.test.ts @@ -1,23 +1,28 @@ -'use strict'; - -const { EventEmitter, once } = require('events'); -const net = require('net'); -const tls = require('tls'); -const fs = require('fs'); -const { expect } = require('chai'); -const sinon = require('sinon'); -const { serialize, Long, Int32 } = require('../../mongodb'); -const { StateMachine } = require('../../../src/client-side-encryption/stateMachine'); -const { Db } = require('../../../src/db'); -const { MongoClient } = require('../../../src/mongo_client'); +import { expect } from 'chai'; +import { EventEmitter, once } from 'events'; +import * as fs from 'fs'; +import { type MongoCryptKMSRequest } from 'mongodb-client-encryption'; +import * as net from 'net'; +import * as sinon from 'sinon'; +import { setTimeout } from 'timers'; +import * as tls from 'tls'; + +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { StateMachine } from '../../../src/client-side-encryption/state_machine'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { Db } from '../../../src/db'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { MongoClient } from '../../../src/mongo_client'; +import { Int32, Long, serialize } from '../../mongodb'; describe('StateMachine', function () { - class MockRequest { - constructor(message, bytesNeeded) { + class MockRequest implements MongoCryptKMSRequest { + _bytesNeeded: number; + endpoint = 'some.fake.host.com'; + _kmsProvider = 'aws'; + + constructor(public _message: Buffer, bytesNeeded) { this._bytesNeeded = typeof bytesNeeded === 'number' ? bytesNeeded : 1024; - this._message = message; - this.endpoint = 'some.fake.host.com'; - this._kmsProvider = 'aws'; } get message() { @@ -32,6 +37,10 @@ describe('StateMachine', function () { return this._kmsProvider; } + get status() { + return { type: 1, code: 2, message: 'something went wrong' }; + } + addResponse(buffer) { this._bytesNeeded -= buffer.length; } @@ -60,7 +69,8 @@ describe('StateMachine', function () { }; const options = { promoteLongs: false, promoteValues: false }; const serializedCommand = serialize(command); - const stateMachine = new StateMachine(); + const stateMachine = new StateMachine({} as any); + // eslint-disable-next-line @typescript-eslint/no-empty-function const callback = () => {}; context('when executing the command', function () { @@ -77,7 +87,9 @@ describe('StateMachine', function () { super(); this.on('connect', callback); } + // eslint-disable-next-line @typescript-eslint/no-empty-function write() {} + // eslint-disable-next-line @typescript-eslint/no-empty-function destroy() {} end(callback) { Promise.resolve().then(callback); @@ -98,7 +110,7 @@ describe('StateMachine', function () { }); it('should only resolve once bytesNeeded drops to zero', function (done) { - const stateMachine = new StateMachine(); + const stateMachine = new StateMachine({} as any); const request = new MockRequest(Buffer.from('foobar'), 500); let status = 'pending'; stateMachine @@ -107,6 +119,7 @@ describe('StateMachine', function () { () => (status = 'resolved'), () => (status = 'rejected') ) + // eslint-disable-next-line @typescript-eslint/no-empty-function .catch(() => {}); this.fakeSocket.emit('connect'); @@ -141,7 +154,7 @@ describe('StateMachine', function () { context(`when the option is ${option}`, function () { const stateMachine = new StateMachine({ tlsOptions: { aws: { [option]: true } } - }); + } as any); const request = new MockRequest(Buffer.from('foobar'), 500); it('rejects with the validation error', function (done) { @@ -158,7 +171,7 @@ describe('StateMachine', function () { context('when providing tlsCertificateKeyFile', function () { const stateMachine = new StateMachine({ tlsOptions: { aws: { tlsCertificateKeyFile: 'test.pem' } } - }); + } as any); const request = new MockRequest(Buffer.from('foobar'), -1); const buffer = Buffer.from('foobar'); let connectOptions; @@ -185,7 +198,7 @@ describe('StateMachine', function () { context('when providing tlsCAFile', function () { const stateMachine = new StateMachine({ tlsOptions: { aws: { tlsCAFile: 'test.pem' } } - }); + } as any); const request = new MockRequest(Buffer.from('foobar'), -1); const buffer = Buffer.from('foobar'); let connectOptions; @@ -211,7 +224,7 @@ describe('StateMachine', function () { context('when providing tlsCertificateKeyFilePassword', function () { const stateMachine = new StateMachine({ tlsOptions: { aws: { tlsCertificateKeyFilePassword: 'test' } } - }); + } as any); const request = new MockRequest(Buffer.from('foobar'), -1); let connectOptions; @@ -286,14 +299,14 @@ describe('StateMachine', function () { proxyHost: 'localhost', proxyPort: socks5srv.address().port } - }); + } as any); const request = new MockRequest(Buffer.from('foobar'), 500); try { await stateMachine.kmsRequest(request); } catch (err) { expect(err.name).to.equal('MongoCryptError'); - expect(err.originalError.code).to.equal('ECONNRESET'); + expect(err.cause.code).to.equal('ECONNRESET'); expect(hasTlsConnection).to.equal(true); return; } @@ -309,14 +322,14 @@ describe('StateMachine', function () { proxyUsername: 'foo', proxyPassword: 'bar' } - }); + } as any); const request = new MockRequest(Buffer.from('foobar'), 500); try { await stateMachine.kmsRequest(request); } catch (err) { expect(err.name).to.equal('MongoCryptError'); - expect(err.originalError.code).to.equal('ECONNRESET'); + expect(err.cause.code).to.equal('ECONNRESET'); expect(hasTlsConnection).to.equal(true); return; }