From 7800fbfefa3a6db66b023cadb68f764443fe0001 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Sat, 8 Jul 2023 09:22:38 -0600 Subject: [PATCH 01/45] move csfle src into driver and tests passing From bbd8855c7b82198f93919f325782782bb798ee59 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 19 Jul 2023 09:44:07 -0600 Subject: [PATCH 02/45] only convert relevant files --- src/client-side-encryption/{errors.js => errors.ts} | 0 src/client-side-encryption/providers/{aws.js => aws.ts} | 0 src/client-side-encryption/providers/{azure.js => azure.ts} | 0 src/client-side-encryption/providers/{gcp.js => gcp.ts} | 0 src/client-side-encryption/providers/{index.js => index.ts} | 0 src/client-side-encryption/providers/{utils.js => utils.ts} | 0 .../{credentialsProvider.test.js => credentialsProvider.test.ts} | 0 .../{requirements.helper.js => requirements.helper.ts} | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename src/client-side-encryption/{errors.js => errors.ts} (100%) rename src/client-side-encryption/providers/{aws.js => aws.ts} (100%) rename src/client-side-encryption/providers/{azure.js => azure.ts} (100%) rename src/client-side-encryption/providers/{gcp.js => gcp.ts} (100%) rename src/client-side-encryption/providers/{index.js => index.ts} (100%) rename src/client-side-encryption/providers/{utils.js => utils.ts} (100%) rename test/unit/client-side-encryption/providers/{credentialsProvider.test.js => credentialsProvider.test.ts} (100%) rename test/unit/client-side-encryption/{requirements.helper.js => requirements.helper.ts} (100%) diff --git a/src/client-side-encryption/errors.js b/src/client-side-encryption/errors.ts similarity index 100% rename from src/client-side-encryption/errors.js rename to src/client-side-encryption/errors.ts diff --git a/src/client-side-encryption/providers/aws.js b/src/client-side-encryption/providers/aws.ts similarity index 100% rename from src/client-side-encryption/providers/aws.js rename to src/client-side-encryption/providers/aws.ts diff --git a/src/client-side-encryption/providers/azure.js b/src/client-side-encryption/providers/azure.ts similarity index 100% rename from src/client-side-encryption/providers/azure.js rename to src/client-side-encryption/providers/azure.ts diff --git a/src/client-side-encryption/providers/gcp.js b/src/client-side-encryption/providers/gcp.ts similarity index 100% rename from src/client-side-encryption/providers/gcp.js rename to src/client-side-encryption/providers/gcp.ts diff --git a/src/client-side-encryption/providers/index.js b/src/client-side-encryption/providers/index.ts similarity index 100% rename from src/client-side-encryption/providers/index.js rename to src/client-side-encryption/providers/index.ts diff --git a/src/client-side-encryption/providers/utils.js b/src/client-side-encryption/providers/utils.ts similarity index 100% rename from src/client-side-encryption/providers/utils.js rename to src/client-side-encryption/providers/utils.ts diff --git a/test/unit/client-side-encryption/providers/credentialsProvider.test.js b/test/unit/client-side-encryption/providers/credentialsProvider.test.ts similarity index 100% rename from test/unit/client-side-encryption/providers/credentialsProvider.test.js rename to test/unit/client-side-encryption/providers/credentialsProvider.test.ts diff --git a/test/unit/client-side-encryption/requirements.helper.js b/test/unit/client-side-encryption/requirements.helper.ts similarity index 100% rename from test/unit/client-side-encryption/requirements.helper.js rename to test/unit/client-side-encryption/requirements.helper.ts From 5d6d76707c6b920c79a80514058c2d77894e18dd Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 19 Jul 2023 09:17:06 -0600 Subject: [PATCH 03/45] convert KMS providers to TS --- package-lock.json | 100 +++++++++++- package.json | 1 + src/client-side-encryption/errors.ts | 42 +++-- src/client-side-encryption/providers/aws.ts | 35 ++--- src/client-side-encryption/providers/azure.ts | 120 +++++++------- src/client-side-encryption/providers/gcp.ts | 28 ++-- src/client-side-encryption/providers/index.ts | 147 ++++++++++++++++-- src/client-side-encryption/providers/utils.ts | 20 +-- src/deps.ts | 20 +++ ...ion.prose.18.azure_kms_mock_server.test.ts | 7 +- .../providers/credentialsProvider.test.ts | 112 +++++++------ .../requirements.helper.ts | 42 ++--- 12 files changed, 453 insertions(+), 221 deletions(-) diff --git a/package-lock.json b/package-lock.json index fd9f2123793..6c1883d2d1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-tsdoc": "^0.2.17", "express": "^4.18.2", + "gcp-metadata": "^5.2.0", "js-yaml": "^4.1.0", "mocha": "^10.2.0", "mocha-sinon": "^2.1.2", @@ -3111,6 +3112,18 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -3391,6 +3404,15 @@ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", "dev": true }, + "node_modules/bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -4706,6 +4728,12 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5041,6 +5069,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "dev": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "dev": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -5401,6 +5457,19 @@ "integrity": "sha512-o0PWwVCSp3O0wS6FvNr6xfBCHgt0m1tvPLFOCc2iFDKTRAXhB7m8klDf7ErowFH8POa6dVdGatKU5I1YYwzUyg==", "dev": true }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -6049,6 +6118,15 @@ "node": ">=4" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dev": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -6515,12 +6593,12 @@ } }, "node_modules/mongodb": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.4.0.tgz", - "integrity": "sha512-6GDKgO7WiYUw+ILap143VXfAou06hjxDGgYUZWGnI4hgoZfP3el0G3l69JqJF8SEQbZmC+SN/2a0aWI/aWJoxA==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.7.0.tgz", + "integrity": "sha512-zm82Bq33QbqtxDf58fLWBwTjARK3NSvKYjyz997KSy6hpat0prjeX/kxjbPVyZY60XYPDNETaHkHJI2UCzSLuw==", "dev": true, "dependencies": { - "bson": "^5.2.0", + "bson": "^5.4.0", "mongodb-connection-string-url": "^2.6.0", "socks": "^2.7.1" }, @@ -6532,6 +6610,8 @@ }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.201.0", + "@mongodb-js/zstd": "^1.1.0", + "kerberos": "^2.0.1", "mongodb-client-encryption": ">=2.3.0 <3", "snappy": "^7.2.2" }, @@ -6539,6 +6619,12 @@ "@aws-sdk/credential-providers": { "optional": true }, + "@mongodb-js/zstd": { + "optional": true + }, + "kerberos": { + "optional": true + }, "mongodb-client-encryption": { "optional": true }, @@ -6548,9 +6634,9 @@ } }, "node_modules/mongodb-client-encryption": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-2.8.0.tgz", - "integrity": "sha512-wIcaETX0Acis9hJkUf2SvtPMq/F1G2gxZXgp8QAe2yJzL+cIUpii8Yv4i3LIeZVwYuYSue8F6/e4pHaE21On7A==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-2.9.0.tgz", + "integrity": "sha512-OGMfTnS+JJ49ksWdExQ5048ynaQJLhPjbOi3i44PbU2sdufKH0Z4YZqn1pvd/eQ4WgLfbmSws3u9kAiFNFxpOg==", "dev": true, "hasInstallScript": true, "dependencies": { diff --git a/package.json b/package.json index 1328c34f913..299e47a61c8 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-tsdoc": "^0.2.17", "express": "^4.18.2", + "gcp-metadata": "^5.2.0", "js-yaml": "^4.1.0", "mocha": "^10.2.0", "mocha-sinon": "^2.1.2", diff --git a/src/client-side-encryption/errors.ts b/src/client-side-encryption/errors.ts index bb0a72ff625..bb4a8badcba 100644 --- a/src/client-side-encryption/errors.ts +++ b/src/client-side-encryption/errors.ts @@ -1,63 +1,77 @@ +import { type Document } from '../bson'; + /** - * @class + * @public * An error indicating that something went wrong specifically with MongoDB Client Encryption */ export class MongoCryptError extends Error { - constructor(message, options = {}) { + cause?: Error | string; + constructor(message: string, options: { cause?: Error | string } = {}) { super(message); if (options.cause != null) { this.cause = options.cause; } } - get name() { + override get name() { return 'MongoCryptError'; } } /** - * @class + * @public * An error indicating that `ClientEncryption.createEncryptedCollection()` failed to create data keys */ export class MongoCryptCreateDataKeyError extends MongoCryptError { + // TODO: type encrypted fields + encryptedFields: Document; constructor({ encryptedFields, cause }) { super(`Unable to complete creating data keys: ${cause.message}`, { cause }); this.encryptedFields = encryptedFields; } - get name() { + override get name() { return 'MongoCryptCreateDataKeyError'; } } /** - * @class + * @public * An error indicating that `ClientEncryption.createEncryptedCollection()` failed to create a collection */ export class MongoCryptCreateEncryptedCollectionError extends MongoCryptError { + // TODO: type encrypted fields + encryptedFields: Document; constructor({ encryptedFields, cause }) { super(`Unable to create collection: ${cause.message}`, { cause }); this.encryptedFields = encryptedFields; } - get name() { + override get name() { return 'MongoCryptCreateEncryptedCollectionError'; } } /** - * @class + * @public * An error indicating that mongodb-client-encryption failed to auto-refresh Azure KMS credentials. */ export class MongoCryptAzureKMSRequestError extends MongoCryptError { - /** - * @param {string} message - * @param {object | undefined} body - */ - constructor(message, body) { + /** The body of the http response that failed, if present. */ + body?: Document; + constructor(message: string, body?: Document) { super(message); this.body = body; } + + override get name(): string { + return 'MongoCryptAzureKMSRequestError'; + } } -export class MongoCryptKMSRequestNetworkTimeoutError extends MongoCryptError {} +/** @public */ +export class MongoCryptKMSRequestNetworkTimeoutError extends MongoCryptError { + override get name(): string { + return 'MongoCryptKMSRequestNetworkTimeoutError'; + } +} diff --git a/src/client-side-encryption/providers/aws.ts b/src/client-side-encryption/providers/aws.ts index 05b0fddb373..30bd201fd74 100644 --- a/src/client-side-encryption/providers/aws.ts +++ b/src/client-side-encryption/providers/aws.ts @@ -1,22 +1,21 @@ -let awsCredentialProviders = null; -/** @ignore */ -export async function loadAWSCredentials(kmsProviders) { - if (awsCredentialProviders == null) { - try { - // Ensure you always wrap an optional require in the try block NODE-3199 - awsCredentialProviders = require('@aws-sdk/credential-providers'); - // eslint-disable-next-line no-empty - } catch {} - } +import { getAwsCredentialProvider } from '../../deps'; +import { type KMSProviders } from '.'; + +/** + * @internal + */ +export async function loadAWSCredentials(kmsProviders: KMSProviders): Promise { + const credentialProvider = getAwsCredentialProvider(); - if (awsCredentialProviders != null) { - const { fromNodeProviderChain } = awsCredentialProviders; - const provider = fromNodeProviderChain(); - // The state machine is the only place calling this so it will - // catch if there is a rejection here. - const aws = await provider(); - return { ...kmsProviders, aws }; + if ('kModuleError' in credentialProvider) { + return kmsProviders; } - return kmsProviders; + const { fromNodeProviderChain } = credentialProvider; + const provider = fromNodeProviderChain(); + // The state machine is the only place calling this so it will + // catch if there is a rejection here. + const aws = await provider(); + // TODO - figure out the type mismatch here + return { ...kmsProviders, aws }; } diff --git a/src/client-side-encryption/providers/azure.ts b/src/client-side-encryption/providers/azure.ts index d6525607dd9..5d13d91d50b 100644 --- a/src/client-side-encryption/providers/azure.ts +++ b/src/client-side-encryption/providers/azure.ts @@ -1,32 +1,31 @@ -import { - MongoCryptAzureKMSRequestError, - MongoCryptKMSRequestNetworkTimeoutError -} from '../errors'; +import { type Document } from 'bson'; + +import { MongoCryptAzureKMSRequestError, MongoCryptKMSRequestNetworkTimeoutError } from '../errors'; +import { type KMSProviders } from '.'; import * as utils from './utils'; const MINIMUM_TOKEN_REFRESH_IN_MILLISECONDS = 6000; +interface AccessToken { + accessToken: string; + expiresOnTimestamp: number; +} /** - * @class - * @ignore + * @internal */ export class AzureCredentialCache { - constructor() { - /** - * @type { { accessToken: string, expiresOnTimestamp: number } | null} - */ - this.cachedToken = null; - } + /** @internal */ + cachedToken: AccessToken | null = null; - async getToken() { + async getToken(): Promise { if (this.needsRefresh(this.cachedToken)) { this.cachedToken = await this._getToken(); } - return { accessToken: this.cachedToken.accessToken }; + return { ...this.cachedToken! }; } - needsRefresh(token) { + needsRefresh(token: typeof this.cachedToken): boolean { if (token == null) { return true; } @@ -36,7 +35,7 @@ export class AzureCredentialCache { /** * exposed for testing - * @ignore + * @internal */ resetCache() { this.cachedToken = null; @@ -44,38 +43,20 @@ export class AzureCredentialCache { /** * exposed for testing - * @ignore + * @internal */ - _getToken() { + _getToken(): Promise { return fetchAzureKMSToken(); } } -/** - * @type{ AzureCredentialCache } - * @ignore - */ -export let tokenCache = new AzureCredentialCache(); -/** - * @typedef {object} KmsRequestResponsePayload - * @property {string | undefined} access_token - * @property {string | undefined} expires_in - * - * @ignore - */ +/** @internal */ +export const tokenCache = new AzureCredentialCache(); -/** - * @param { {body: string, status: number }} response - * @returns { Promise<{ accessToken: string, expiresOnTimestamp: number } >} - * @ignore - */ -export async function parseResponse(response) { +async function parseResponse(response: { body: string; status?: number }): Promise { const { status, body: rawBody } = response; - /** - * @type { KmsRequestResponsePayload } - */ - const body = (() => { + const body: { expires_in?: number; access_token?: string } = (() => { try { return JSON.parse(rawBody); } catch { @@ -113,17 +94,32 @@ export async function parseResponse(response) { } /** - * @param {object} options - * @param {object | undefined} [options.headers] - * @param {URL | undefined} [options.url] + * @internal * - * @ignore + * exposed for CSFLE + * [prose test 18](https://github.com/mongodb/specifications/tree/master/source/client-side-encryption/tests#azure-imds-credentials) */ -export function prepareRequest(options) { +export interface AzureKMSRequestOptions { + headers?: Document; + url?: URL | string; +} + +/** + * @internal + * + * parses any options provided by prose tests to `fetchAzureKMSToken` and merges them with + * the default values for headers and the request url. + */ +export function prepareRequest(options: AzureKMSRequestOptions): { + headers: Document; + url: URL; +} { const url = options.url == null ? new URL('http://169.254.169.254/metadata/identity/oauth2/token') - : new URL(options.url); + : // TODO - figure out why typings are messed up. + // @ts-expect-error the URL constructor supports a url parameter, but the types say it doesn't + new URL(options.url); url.searchParams.append('api-version', '2018-02-01'); url.searchParams.append('resource', 'https://vault.azure.net'); @@ -133,28 +129,16 @@ export function prepareRequest(options) { } /** - * @typedef {object} AzureKMSRequestOptions - * @property {object | undefined} headers - * @property {URL | undefined} url - * @ignore - */ - -/** - * @typedef {object} AzureKMSRequestResponse - * @property {string} accessToken - * @property {number} expiresOnTimestamp - * @ignore - */ - -/** - * exported only for testing purposes in the driver + * @internal * - * @param {AzureKMSRequestOptions} options - * @returns {Promise} + * `AzureKMSRequestOptions` allows prose tests to modify the http request sent to the idms + * servers. This is required to simulate different server conditions. No options are expected to + * be set outside of tests. * - * @ignore + * exposed for CSFLE + * [prose test 18](https://github.com/mongodb/specifications/tree/master/source/client-side-encryption/tests#azure-imds-credentials) */ -export async function fetchAzureKMSToken(options = {}) { +export async function fetchAzureKMSToken(options: AzureKMSRequestOptions = {}) { const { headers, url } = prepareRequest(options); const response = await utils.get(url, { headers }).catch(error => { if (error instanceof MongoCryptKMSRequestNetworkTimeoutError) { @@ -166,9 +150,11 @@ export async function fetchAzureKMSToken(options = {}) { } /** - * @ignore + * @internal + * + * @throws Will reject with a `MongoCryptError` if the http request fails or the http response is malformed. */ -export async function loadAzureCredentials(kmsProviders) { +export async function loadAzureCredentials(kmsProviders: KMSProviders): Promise { const azure = await tokenCache.getToken(); return { ...kmsProviders, azure }; } diff --git a/src/client-side-encryption/providers/gcp.ts b/src/client-side-encryption/providers/gcp.ts index d529e62a33c..0e518914f18 100644 --- a/src/client-side-encryption/providers/gcp.ts +++ b/src/client-side-encryption/providers/gcp.ts @@ -1,20 +1,16 @@ -let gcpMetadata = null; -/** @ignore */ -export async function loadGCPCredentials(kmsProviders) { - if (gcpMetadata == null) { - try { - // Ensure you always wrap an optional require in the try block NODE-3199 - gcpMetadata = require('gcp-metadata'); - // eslint-disable-next-line no-empty - } catch {} - } +import { getGcpMetadata } from '../../deps'; +import { type KMSProviders } from '.'; + +/** @internal */ +export async function loadGCPCredentials(kmsProviders: KMSProviders): Promise { + const gcpMetadata = getGcpMetadata(); - if (gcpMetadata != null) { - const { access_token: accessToken } = await gcpMetadata.instance({ - property: 'service-accounts/default/token' - }); - return { ...kmsProviders, gcp: { accessToken } }; + if ('kModuleError' in gcpMetadata) { + return kmsProviders; } - return kmsProviders; + const { access_token: accessToken } = await gcpMetadata.instance({ + property: 'service-accounts/default/token' + }); + return { ...kmsProviders, gcp: { accessToken } }; } diff --git a/src/client-side-encryption/providers/index.ts b/src/client-side-encryption/providers/index.ts index 506f676ac6a..0d56e0c3ca9 100644 --- a/src/client-side-encryption/providers/index.ts +++ b/src/client-side-encryption/providers/index.ts @@ -1,24 +1,146 @@ -import { loadAWSCredentials } from './aws' -import { loadAzureCredentials, fetchAzureKMSToken }from './azure'; +import { loadAWSCredentials } from './aws'; +import { loadAzureCredentials } from './azure'; import { loadGCPCredentials } from './gcp'; +/** + * @public + */ +export type KMSProvider = 'aws' | 'azure' | 'gcp' | 'local'; + +/** + * @public + * Configuration options that are used by specific KMS providers during key generation, encryption, and decryption. + */ +export interface 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 '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; + }; + + /** + * 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; +} + /** * Auto credential fetching should only occur when the provider is defined on the kmsProviders map * and the settings are an empty object. * * This is distinct from a nullish provider key. * - * @param {'aws' | 'gcp' | 'azure'} provider - * @param {object} kmsProviders - * - * @ignore + * @internal - exposed for testing purposes only */ -function isEmptyCredentials(provider, kmsProviders) { +export function isEmptyCredentials(provider: KMSProvider, kmsProviders: KMSProviders) { return ( provider in kmsProviders && kmsProviders[provider] != null && typeof kmsProviders[provider] === 'object' && - Object.keys(kmsProviders[provider]).length === 0 + // Typescript does not infer that kmsProviders[provider] is non-null, + // even though we check it two lines up. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + Object.keys(kmsProviders[provider]!).length === 0 ); } @@ -27,12 +149,9 @@ function isEmptyCredentials(provider, kmsProviders) { * Credentials will only attempt to get loaded if they do not exist * and no existing credentials will get overwritten. * - * @param {object} kmsProviders - The user provided KMS providers. - * @returns {object} The new kms providers. - * - * @ignore + * @internal */ -async function loadCredentials(kmsProviders) { +export async function loadCredentials(kmsProviders: KMSProviders): Promise { let finalKMSProviders = kmsProviders; if (isEmptyCredentials('aws', kmsProviders)) { @@ -48,5 +167,3 @@ async function loadCredentials(kmsProviders) { } return finalKMSProviders; } - -module.exports = { loadCredentials, isEmptyCredentials, fetchAzureKMSToken }; diff --git a/src/client-side-encryption/providers/utils.ts b/src/client-side-encryption/providers/utils.ts index 942851bf541..8d5362c6993 100644 --- a/src/client-side-encryption/providers/utils.ts +++ b/src/client-side-encryption/providers/utils.ts @@ -1,16 +1,18 @@ -import { MongoCryptKMSRequestNetworkTimeoutError } from '../errors'; import * as http from 'http'; +import { clearTimeout, setTimeout } from 'timers'; + +import { MongoCryptKMSRequestNetworkTimeoutError } from '../errors'; /** - * @param {URL | string} url - * @param {http.RequestOptions} options - * - * @returns { Promise<{ body: string, status: number }> } - * @ignore + * @internal */ -function get(url, options = {}) { +export function get( + url: URL | string, + options: http.RequestOptions = {} +): Promise<{ body: string; status: number | undefined }> { return new Promise((resolve, reject) => { - let timeoutId; + /* eslint-disable prefer-const */ + let timeoutId: NodeJS.Timeout; const request = http .get(url, options, response => { response.setEncoding('utf8'); @@ -33,5 +35,3 @@ function get(url, options = {}) { }, 10000); }); } - -module.exports = { get }; diff --git a/src/deps.ts b/src/deps.ts index e79e3f7fb6e..6f07d70caeb 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -97,6 +97,26 @@ export function getAwsCredentialProvider(): } } +/** @internal */ +export type GcpMetadata = + | typeof import('gcp-metadata') + | { kModuleError: MongoMissingDependencyError }; + +export function getGcpMetadata(): GcpMetadata { + try { + // Ensure you always wrap an optional require in the try block NODE-3199 + const credentialProvider = require('gcp-metadata'); + return credentialProvider; + } catch { + return makeErrorModule( + new MongoMissingDependencyError( + 'Optional module `gcp-metadata` not found.' + + ' Please install it to enable getting gcp credentials via the official sdk.' + ) + ); + } +} + /** @internal */ export type SnappyLib = { /** diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.18.azure_kms_mock_server.test.ts b/test/integration/client-side-encryption/client_side_encryption.prose.18.azure_kms_mock_server.test.ts index 86f2ac216a3..c99820b6f83 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.18.azure_kms_mock_server.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.prose.18.azure_kms_mock_server.test.ts @@ -3,11 +3,14 @@ import { expect } from 'chai'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { MongoCryptAzureKMSRequestError } from '../../../src/client-side-encryption/errors'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports -import { fetchAzureKMSToken } from '../../../src/client-side-encryption/providers/azure'; +import { + type AzureKMSRequestOptions, + fetchAzureKMSToken +} from '../../../src/client-side-encryption/providers/azure'; import { type Document } from '../../mongodb'; const BASE_URL = new URL(`http://127.0.0.1:8080/metadata/identity/oauth2/token`); -class KMSRequestOptions { +class KMSRequestOptions implements AzureKMSRequestOptions { url: URL = BASE_URL; headers: Document; constructor(testCase?: 'empty-json' | 'bad-json' | '404' | '500' | 'slow') { diff --git a/test/unit/client-side-encryption/providers/credentialsProvider.test.ts b/test/unit/client-side-encryption/providers/credentialsProvider.test.ts index e23a7b06ca8..f8f735033bb 100644 --- a/test/unit/client-side-encryption/providers/credentialsProvider.test.ts +++ b/test/unit/client-side-encryption/providers/credentialsProvider.test.ts @@ -1,16 +1,26 @@ -'use strict'; - -const { expect } = require('chai'); -const http = require('http'); -const requirements = require('../requirements.helper'); -const { loadCredentials, isEmptyCredentials } = require('../../../../src/client-side-encryption/providers'); -const { tokenCache, fetchAzureKMSToken } = require('../../../../src/client-side-encryption/providers/azure'); -const sinon = require('sinon'); -const utils = require('../../../../src/client-side-encryption/providers/utils'); -const { - MongoCryptKMSRequestNetworkTimeoutError, - MongoCryptAzureKMSRequestError -} = require('../../../../src/client-side-encryption/errors'); +import { expect } from 'chai'; +import * as http from 'http'; +import * as sinon from 'sinon'; + +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { + MongoCryptAzureKMSRequestError, + MongoCryptKMSRequestNetworkTimeoutError +} from '../../../../src/client-side-encryption/errors'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { + isEmptyCredentials, + type KMSProviders, + loadCredentials +} from '../../../../src/client-side-encryption/providers'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { + fetchAzureKMSToken, + tokenCache +} from '../../../../src/client-side-encryption/providers/azure'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import * as utils from '../../../../src/client-side-encryption/providers/utils'; +import * as requirements from '../requirements.helper'; const originalAccessKeyId = process.env.AWS_ACCESS_KEY_ID; const originalSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; @@ -19,23 +29,28 @@ const originalSessionToken = process.env.AWS_SESSION_TOKEN; describe('#loadCredentials', function () { context('isEmptyCredentials()', () => { it('returns true for an empty object', () => { - expect(isEmptyCredentials('rainyCloud', { rainyCloud: {} })).to.be.true; + expect(isEmptyCredentials('aws', { aws: {} })).to.be.true; }); it('returns false for an object with keys', () => { - expect(isEmptyCredentials('rainyCloud', { rainyCloud: { password: 'secret' } })).to.be.false; + // @ts-expect-error Testing error conditions here + expect(isEmptyCredentials('aws', { aws: { password: 'secret' } })).to.be.false; }); it('returns false for an nullish credentials', () => { - expect(isEmptyCredentials('rainyCloud', { rainyCloud: null })).to.be.false; - expect(isEmptyCredentials('rainyCloud', { rainyCloud: undefined })).to.be.false; - expect(isEmptyCredentials('rainyCloud', {})).to.be.false; + // @ts-expect-error Testing error conditions here + expect(isEmptyCredentials('aws', { aws: null })).to.be.false; + expect(isEmptyCredentials('aws', { aws: undefined })).to.be.false; + expect(isEmptyCredentials('aws', {})).to.be.false; }); it('returns false for non object credentials', () => { - expect(isEmptyCredentials('rainyCloud', { rainyCloud: 0 })).to.be.false; - expect(isEmptyCredentials('rainyCloud', { rainyCloud: false })).to.be.false; - expect(isEmptyCredentials('rainyCloud', { rainyCloud: Symbol('secret') })).to.be.false; + // @ts-expect-error Testing error conditions here + expect(isEmptyCredentials('aws', { aws: 0 })).to.be.false; + // @ts-expect-error Testing error conditions here + expect(isEmptyCredentials('aws', { aws: false })).to.be.false; + // @ts-expect-error Testing error conditions here + expect(isEmptyCredentials('aws', { aws: Symbol('secret') })).to.be.false; }); }); @@ -63,8 +78,8 @@ describe('#loadCredentials', function () { before(function () { if (!requirements.credentialProvidersInstalled.aws) { - this.currentTest.skipReason = 'Cannot refresh credentials without sdk provider'; - this.currentTest.skip(); + this.currentTest!.skipReason = 'Cannot refresh credentials without sdk provider'; + this.currentTest!.skip(); return; } }); @@ -92,8 +107,8 @@ describe('#loadCredentials', function () { before(function () { if (!requirements.credentialProvidersInstalled.aws) { - this.currentTest.skipReason = 'Cannot refresh credentials without sdk provider'; - this.currentTest.skip(); + this.currentTest!.skipReason = 'Cannot refresh credentials without sdk provider'; + this.currentTest!.skip(); return; } }); @@ -114,19 +129,19 @@ describe('#loadCredentials', function () { }); context('when aws is not empty', function () { - const kmsProviders = { + const kmsProviders: KMSProviders = { local: { key: Buffer.alloc(96) }, aws: { accessKeyId: 'example' - } + } as any }; before(function () { if (!requirements.credentialProvidersInstalled.aws) { - this.currentTest.skipReason = 'Cannot refresh credentials without sdk provider'; - this.currentTest.skip(); + this.currentTest!.skipReason = 'Cannot refresh credentials without sdk provider'; + this.currentTest!.skip(); return; } }); @@ -149,8 +164,8 @@ describe('#loadCredentials', function () { before(function () { if (requirements.credentialProvidersInstalled.aws) { - this.currentTest.skipReason = 'Credentials will be loaded when sdk present'; - this.currentTest.skip(); + this.currentTest!.skipReason = 'Credentials will be loaded when sdk present'; + this.currentTest!.skip(); return; } }); @@ -195,8 +210,8 @@ 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(); + this.currentTest!.skipReason = 'Tests require gcp-metadata to be installed'; + this.currentTest!.skip(); return; } }); @@ -233,8 +248,8 @@ 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(); + this.currentTest!.skipReason = 'Tests require gcp-metadata to be installed'; + this.currentTest!.skip(); return; } }); @@ -261,7 +276,7 @@ describe('#loadCredentials', function () { }); context('when there is no cached token', () => { - let mockToken = { + const mockToken = { accessToken: 'mock token', expiresOnTimestamp: Date.now() }; @@ -269,7 +284,7 @@ describe('#loadCredentials', function () { let token; beforeEach(async () => { - sinon.stub(cache, '_getToken').returns(mockToken); + sinon.stub(cache, '_getToken').resolves(mockToken); token = await cache.getToken(); }); it('fetches a token', async () => { @@ -282,7 +297,7 @@ describe('#loadCredentials', function () { context('when there is a cached token', () => { context('when the cached token expires <= 1 minute from the current time', () => { - let mockToken = { + const mockToken = { accessToken: 'mock token', expiresOnTimestamp: Date.now() }; @@ -294,7 +309,7 @@ describe('#loadCredentials', function () { accessToken: 'a new key', expiresOnTimestamp: Date.now() + 3000 }; - sinon.stub(cache, '_getToken').returns(mockToken); + sinon.stub(cache, '_getToken').resolves(mockToken); token = await cache.getToken(); }); @@ -307,12 +322,12 @@ describe('#loadCredentials', function () { }); context('when the cached token expires > 1 minute from the current time', () => { - let expiredToken = { + const expiredToken = { token: 'mock token', expiresOnTimestamp: Date.now() }; - let expectedMockToken = { + const expectedMockToken = { accessToken: 'a new key', expiresOnTimestamp: Date.now() + 10000 }; @@ -320,8 +335,8 @@ describe('#loadCredentials', function () { let token; beforeEach(async () => { - cache.cachedToken = expiredToken; - sinon.stub(cache, '_getToken').returns(expectedMockToken); + cache.cachedToken = expiredToken as any; + sinon.stub(cache, '_getToken').resolves(expectedMockToken); token = await cache.getToken(); }); it('returns the cached token', () => { @@ -420,7 +435,9 @@ describe('#loadCredentials', function () { afterEach(() => sinon.restore()); context('when the request times out', () => { before(() => { - sinon.stub(utils, 'get').rejects(new MongoCryptKMSRequestNetworkTimeoutError()); + sinon + .stub(utils, 'get') + .rejects(new MongoCryptKMSRequestNetworkTimeoutError('request timed out')); }); it('throws a MongoCryptKMSRequestError', async () => { @@ -432,7 +449,7 @@ describe('#loadCredentials', function () { context('when the request returns a non-200 error', () => { context('when the request has no body', () => { before(() => { - sinon.stub(utils, 'get').resolves({ status: 400 }); + sinon.stub(utils, 'get').resolves({ status: 400 } as any); }); it('throws a MongoCryptKMSRequestError', async () => { @@ -476,7 +493,7 @@ describe('#loadCredentials', function () { context('when the request returns a 200 response', () => { context('when the request has no body', () => { before(() => { - sinon.stub(utils, 'get').resolves({ status: 200 }); + sinon.stub(utils, 'get').resolves({ status: 200 } as any); }); it('throws a MongoCryptKMSRequestError', async () => { @@ -547,7 +564,8 @@ describe('#loadCredentials', function () { it('returns the token in the `azure` field of the kms providers', async () => { const kmsProviders = await loadCredentials({ azure: {} }); - expect(kmsProviders).to.have.property('azure').to.deep.equal({ accessToken: 'token' }); + const azure = kmsProviders.azure; + expect(azure).to.have.property('accessToken', 'token'); }); }); }); diff --git a/test/unit/client-side-encryption/requirements.helper.ts b/test/unit/client-side-encryption/requirements.helper.ts index a456c49197c..574278bccf5 100644 --- a/test/unit/client-side-encryption/requirements.helper.ts +++ b/test/unit/client-side-encryption/requirements.helper.ts @@ -1,19 +1,22 @@ -'use strict'; - // Data Key Stuff -const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; -const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY; -const AWS_REGION = process.env.AWS_REGION; -const AWS_CMK_ID = process.env.AWS_CMK_ID; +export const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; +export const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY; +export const AWS_REGION = process.env.AWS_REGION; +export const AWS_CMK_ID = process.env.AWS_CMK_ID; -const awsKmsProviders = { +export const awsKmsProviders = { aws: { accessKeyId: AWS_ACCESS_KEY_ID, secretAccessKey: AWS_SECRET_ACCESS_KEY } }; -const awsDataKeyOptions = { masterKey: { key: AWS_CMK_ID, region: AWS_REGION } }; +export const awsDataKeyOptions = { masterKey: { key: AWS_CMK_ID, region: AWS_REGION } }; -const SKIP_AWS_TESTS = [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_CMK_ID].some(secret => !secret); +export const SKIP_AWS_TESTS = [ + AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY, + AWS_REGION, + AWS_CMK_ID +].some(secret => !secret); -function isAWSCredentialProviderInstalled() { +export function isAWSCredentialProviderInstalled() { try { require.resolve('@aws-sdk/credential-providers'); return true; @@ -22,7 +25,7 @@ function isAWSCredentialProviderInstalled() { } } -function isGCPCredentialProviderInstalled() { +export function isGCPCredentialProviderInstalled() { try { require.resolve('gcp-metadata'); return true; @@ -31,18 +34,7 @@ function isGCPCredentialProviderInstalled() { } } -module.exports = { - SKIP_AWS_TESTS, - KEYS: { - AWS_ACCESS_KEY_ID, - AWS_SECRET_ACCESS_KEY, - AWS_REGION, - AWS_CMK_ID - }, - awsKmsProviders, - awsDataKeyOptions, - credentialProvidersInstalled: { - aws: isAWSCredentialProviderInstalled(), - gcp: isGCPCredentialProviderInstalled() - } +export const credentialProvidersInstalled = { + aws: isAWSCredentialProviderInstalled(), + gcp: isGCPCredentialProviderInstalled() }; From 3b8bc363f93a3b858b265757885ba9fec3bd3bce Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 21 Jul 2023 08:59:40 -0600 Subject: [PATCH 04/45] cleanup requirements --- .../requirements.helper.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/test/unit/client-side-encryption/requirements.helper.ts b/test/unit/client-side-encryption/requirements.helper.ts index 574278bccf5..9737337b55e 100644 --- a/test/unit/client-side-encryption/requirements.helper.ts +++ b/test/unit/client-side-encryption/requirements.helper.ts @@ -1,3 +1,5 @@ +import {getGcpMetadata , getAwsCredentialProvider} from '../../../src/deps'; + // Data Key Stuff export const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; export const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY; @@ -17,21 +19,11 @@ export const SKIP_AWS_TESTS = [ ].some(secret => !secret); export function isAWSCredentialProviderInstalled() { - try { - require.resolve('@aws-sdk/credential-providers'); - return true; - } catch { - return false; - } + return !('kModuleError' in getAwsCredentialProvider()); } export function isGCPCredentialProviderInstalled() { - try { - require.resolve('gcp-metadata'); - return true; - } catch { - return false; - } + return !('kModuleError' in getGcpMetadata()); } export const credentialProvidersInstalled = { From 202fda1861cb51a22592bd8528ac713a95548517 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 21 Jul 2023 09:00:56 -0600 Subject: [PATCH 05/45] cleanup src --- src/client-side-encryption/errors.ts | 6 ++---- src/client-side-encryption/providers/aws.ts | 1 - src/client-side-encryption/providers/azure.ts | 6 ++++-- src/client-side-encryption/providers/gcp.ts | 2 +- src/cmap/auth/mongodb_aws.ts | 8 -------- src/deps.ts | 13 ++++++++++++- 6 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/client-side-encryption/errors.ts b/src/client-side-encryption/errors.ts index bb4a8badcba..e64c3321df0 100644 --- a/src/client-side-encryption/errors.ts +++ b/src/client-side-encryption/errors.ts @@ -23,9 +23,8 @@ export class MongoCryptError extends Error { * An error indicating that `ClientEncryption.createEncryptedCollection()` failed to create data keys */ export class MongoCryptCreateDataKeyError extends MongoCryptError { - // TODO: type encrypted fields encryptedFields: Document; - constructor({ encryptedFields, cause }) { + constructor({encryptedFields, cause}: { encryptedFields: Document, cause: Error }) { super(`Unable to complete creating data keys: ${cause.message}`, { cause }); this.encryptedFields = encryptedFields; } @@ -40,9 +39,8 @@ export class MongoCryptCreateDataKeyError extends MongoCryptError { * An error indicating that `ClientEncryption.createEncryptedCollection()` failed to create a collection */ export class MongoCryptCreateEncryptedCollectionError extends MongoCryptError { - // TODO: type encrypted fields encryptedFields: Document; - constructor({ encryptedFields, cause }) { + constructor({encryptedFields, cause}: { encryptedFields: Document, cause: Error }) { super(`Unable to create collection: ${cause.message}`, { cause }); this.encryptedFields = encryptedFields; } diff --git a/src/client-side-encryption/providers/aws.ts b/src/client-side-encryption/providers/aws.ts index 30bd201fd74..64aa9f0adca 100644 --- a/src/client-side-encryption/providers/aws.ts +++ b/src/client-side-encryption/providers/aws.ts @@ -16,6 +16,5 @@ export async function loadAWSCredentials(kmsProviders: KMSProviders): Promise({ property: 'service-accounts/default/token' }); return { ...kmsProviders, gcp: { accessToken } }; diff --git a/src/cmap/auth/mongodb_aws.ts b/src/cmap/auth/mongodb_aws.ts index 57e3a028ff8..0001c080475 100644 --- a/src/cmap/auth/mongodb_aws.ts +++ b/src/cmap/auth/mongodb_aws.ts @@ -157,14 +157,6 @@ interface AWSTempCredentials { Expiration?: Date; } -/* @internal */ -export interface AWSCredentials { - accessKeyId?: string; - secretAccessKey?: string; - sessionToken?: string; - expiration?: Date; -} - async function makeTempCredentials(credentials: MongoCredentials): Promise { function makeMongoCredentialsFromAWSTemp(creds: AWSTempCredentials) { if (!creds.AccessKeyId || !creds.SecretAccessKey || !creds.Token) { diff --git a/src/deps.ts b/src/deps.ts index 6f07d70caeb..214e60b4e4f 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-var-requires */ import type { Document } from './bson'; -import type { AWSCredentials } from './cmap/auth/mongodb_aws'; import type { ProxyOptions } from './cmap/connection'; import { MongoMissingDependencyError } from './error'; import type { MongoClient } from './mongo_client'; @@ -76,6 +75,18 @@ export function getZstdLibrary(): typeof ZStandard | { kModuleError: MongoMissin } } +/** + * @internal + * Copy of the AwsCredentialIdentityProvider interface from [`@smithy/types`](https://socket.dev/npm/package/@smithy/types/files/1.1.1/dist-types/identity/awsCredentialIdentity.d.ts), + * the return type of the aws-sdk's `fromNodeProviderChain().provider()`. + */ +export interface AWSCredentials { + accessKeyId: string; + secretAccessKey: string; + sessionToken: string; + expiration?: Date; +} + type CredentialProvider = { fromNodeProviderChain(this: void): () => Promise; }; From 1a0af799bc5c221726da0b26245ef6e43c343c7c Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 21 Jul 2023 09:08:42 -0600 Subject: [PATCH 06/45] fix lint --- .evergreen/run-kerberos-tests.sh | 5 --- src/client-side-encryption/errors.ts | 12 ++---- src/client-side-encryption/providers/azure.ts | 37 ++++++++++++------- src/deps.ts | 2 +- .../requirements.helper.ts | 3 +- tsconfig.json | 3 +- 6 files changed, 32 insertions(+), 30 deletions(-) diff --git a/.evergreen/run-kerberos-tests.sh b/.evergreen/run-kerberos-tests.sh index eb6b4b44228..bc3a8d37527 100644 --- a/.evergreen/run-kerberos-tests.sh +++ b/.evergreen/run-kerberos-tests.sh @@ -24,11 +24,6 @@ set -o xtrace npm install kerberos@">=2.0.0-beta.0" npm run check:kerberos -if [ "$NODE_LTS_VERSION" != "latest" ] && [ $NODE_LTS_VERSION -lt 20 ]; then - npm install kerberos@"^1.1.7" - npm run check:kerberos -fi - set +o xtrace # destroy ticket diff --git a/src/client-side-encryption/errors.ts b/src/client-side-encryption/errors.ts index e64c3321df0..561260bc1a9 100644 --- a/src/client-side-encryption/errors.ts +++ b/src/client-side-encryption/errors.ts @@ -5,12 +5,8 @@ import { type Document } from '../bson'; * An error indicating that something went wrong specifically with MongoDB Client Encryption */ export class MongoCryptError extends Error { - cause?: Error | string; - constructor(message: string, options: { cause?: Error | string } = {}) { - super(message); - if (options.cause != null) { - this.cause = options.cause; - } + constructor(message: string, options: { cause?: Error } = {}) { + super(message, options); } override get name() { @@ -24,7 +20,7 @@ export class MongoCryptError extends Error { */ export class MongoCryptCreateDataKeyError extends MongoCryptError { encryptedFields: Document; - constructor({encryptedFields, cause}: { encryptedFields: Document, cause: Error }) { + constructor({ encryptedFields, cause }: { encryptedFields: Document; cause: Error }) { super(`Unable to complete creating data keys: ${cause.message}`, { cause }); this.encryptedFields = encryptedFields; } @@ -40,7 +36,7 @@ export class MongoCryptCreateDataKeyError extends MongoCryptError { */ export class MongoCryptCreateEncryptedCollectionError extends MongoCryptError { encryptedFields: Document; - constructor({encryptedFields, cause}: { encryptedFields: Document, cause: Error }) { + constructor({ encryptedFields, cause }: { encryptedFields: Document; cause: Error }) { super(`Unable to create collection: ${cause.message}`, { cause }); this.encryptedFields = encryptedFields; } diff --git a/src/client-side-encryption/providers/azure.ts b/src/client-side-encryption/providers/azure.ts index df3948ad077..0dd61b72a92 100644 --- a/src/client-side-encryption/providers/azure.ts +++ b/src/client-side-encryption/providers/azure.ts @@ -6,36 +6,43 @@ import * as utils from './utils'; const MINIMUM_TOKEN_REFRESH_IN_MILLISECONDS = 6000; +/** + * The access token that libmongocrypt expects for Azure kms. + */ interface AccessToken { accessToken: string; +} + +/** + * The response from the azure idms endpoint, including the `expiresOnTimestamp`. + * `expiresOnTimestamp` is needed for caching. + */ +interface AzureTokenCacheEntry extends AccessToken { + accessToken: string; expiresOnTimestamp: number; } + /** * @internal */ export class AzureCredentialCache { - /** @internal */ - cachedToken: AccessToken | null = null; + cachedToken: AzureTokenCacheEntry | null = null; async getToken(): Promise { - if (this.needsRefresh(this.cachedToken)) { + if (this.cachedToken == null || this.needsRefresh(this.cachedToken)) { this.cachedToken = await this._getToken(); } - return { ...this.cachedToken! }; + return { accessToken: this.cachedToken.accessToken }; } - needsRefresh(token: typeof this.cachedToken): boolean { - if (token == null) { - return true; - } + needsRefresh(token: AzureTokenCacheEntry): boolean { const timeUntilExpirationMS = token.expiresOnTimestamp - Date.now(); return timeUntilExpirationMS <= MINIMUM_TOKEN_REFRESH_IN_MILLISECONDS; } /** * exposed for testing - * @internal */ resetCache() { this.cachedToken = null; @@ -43,9 +50,8 @@ export class AzureCredentialCache { /** * exposed for testing - * @internal */ - _getToken(): Promise { + _getToken(): Promise { return fetchAzureKMSToken(); } } @@ -53,7 +59,8 @@ export class AzureCredentialCache { /** @internal */ export const tokenCache = new AzureCredentialCache(); -async function parseResponse(response: { body: string; status?: number }): Promise { +/** @internal */ +async function parseResponse(response: { body: string; status?: number }): Promise { const { status, body: rawBody } = response; const body: { expires_in?: number; access_token?: string } = (() => { @@ -120,7 +127,7 @@ export function prepareRequest(options: AzureKMSRequestOptions): { : // The Node URL constructor technically supports "any object that converts to a valid URL string", // but Node types doesn't support this. See the Node docs for new URL(). // https://nodejs.org/api/url.html#new-urlinput-base - // @ts-ignore + // @ts-expect-error URL constructor typings incorrect new URL(options.url); url.searchParams.append('api-version', '2018-02-01'); @@ -140,7 +147,9 @@ export function prepareRequest(options: AzureKMSRequestOptions): { * exposed for CSFLE * [prose test 18](https://github.com/mongodb/specifications/tree/master/source/client-side-encryption/tests#azure-imds-credentials) */ -export async function fetchAzureKMSToken(options: AzureKMSRequestOptions = {}) { +export async function fetchAzureKMSToken( + options: AzureKMSRequestOptions = {} +): Promise { const { headers, url } = prepareRequest(options); const response = await utils.get(url, { headers }).catch(error => { if (error instanceof MongoCryptKMSRequestNetworkTimeoutError) { diff --git a/src/deps.ts b/src/deps.ts index 214e60b4e4f..f3c8965685b 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -77,7 +77,7 @@ export function getZstdLibrary(): typeof ZStandard | { kModuleError: MongoMissin /** * @internal - * Copy of the AwsCredentialIdentityProvider interface from [`@smithy/types`](https://socket.dev/npm/package/@smithy/types/files/1.1.1/dist-types/identity/awsCredentialIdentity.d.ts), + * Copy of the AwsCredentialIdentityProvider interface from [`smithy/types`](https://socket.dev/npm/package/\@smithy/types/files/1.1.1/dist-types/identity/awsCredentialIdentity.d.ts), * the return type of the aws-sdk's `fromNodeProviderChain().provider()`. */ export interface AWSCredentials { diff --git a/test/unit/client-side-encryption/requirements.helper.ts b/test/unit/client-side-encryption/requirements.helper.ts index 9737337b55e..68e6ecfce76 100644 --- a/test/unit/client-side-encryption/requirements.helper.ts +++ b/test/unit/client-side-encryption/requirements.helper.ts @@ -1,4 +1,5 @@ -import {getGcpMetadata , getAwsCredentialProvider} from '../../../src/deps'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { getAwsCredentialProvider, getGcpMetadata } from '../../../src/deps'; // Data Key Stuff export const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; diff --git a/tsconfig.json b/tsconfig.json index c8b6b54e07a..0d08d129980 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,8 @@ "moduleResolution": "node", "skipLibCheck": true, "lib": [ - "es2021" + "es2021", + "ES2022.Error" ], // We don't make use of tslib helpers, all syntax used is supported by target engine "importHelpers": false, From d62209a4edbebeed493cddfff8bf3832cea66a1c Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 24 Jul 2023 13:03:46 -0600 Subject: [PATCH 07/45] fix package.json --- package-lock.json | 85 ++++++++++++++++++++++++++++------------------- package.json | 4 +-- 2 files changed, 53 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6c1883d2d1a..e601bf5af99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "js-yaml": "^4.1.0", "mocha": "^10.2.0", "mocha-sinon": "^2.1.2", - "mongodb-client-encryption": "^2.8.0", + "mongodb-client-encryption": "^6.0.0-alpha.0", "mongodb-legacy": "^5.0.0", "nyc": "^15.1.0", "prettier": "^2.8.8", @@ -74,7 +74,7 @@ "@mongodb-js/zstd": "^1.1.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=2.3.0 <3", + "mongodb-client-encryption": ">=6.0.0-alpha.0 <7", "snappy": "^7.2.2" }, "peerDependenciesMeta": { @@ -6592,7 +6592,52 @@ "node": ">=10" } }, - "node_modules/mongodb": { + "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==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": ">=12.9.0" + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "dependencies": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "dependencies": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, + "node_modules/mongodb-legacy": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mongodb-legacy/-/mongodb-legacy-5.0.0.tgz", + "integrity": "sha512-q2G+MRwde6114bCAF/EZLmMXSsebIKMHmzsfOJq6M/Tj4gr3wLT50+rJsJNkiR0e0kjFx3dllWjqwRR1n11Zsw==", + "dev": true, + "dependencies": { + "mongodb": "^5.0.0" + }, + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongodb-legacy/node_modules/mongodb": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.7.0.tgz", "integrity": "sha512-zm82Bq33QbqtxDf58fLWBwTjARK3NSvKYjyz997KSy6hpat0prjeX/kxjbPVyZY60XYPDNETaHkHJI2UCzSLuw==", @@ -6633,12 +6678,14 @@ } } }, - "node_modules/mongodb-client-encryption": { + "node_modules/mongodb-legacy/node_modules/mongodb-client-encryption": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-2.9.0.tgz", "integrity": "sha512-OGMfTnS+JJ49ksWdExQ5048ynaQJLhPjbOi3i44PbU2sdufKH0Z4YZqn1pvd/eQ4WgLfbmSws3u9kAiFNFxpOg==", "dev": true, "hasInstallScript": true, + "optional": true, + "peer": true, "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^4.3.0", @@ -6662,36 +6709,6 @@ } } }, - "node_modules/mongodb-connection-string-url": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", - "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", - "dependencies": { - "@types/whatwg-url": "^8.2.1", - "whatwg-url": "^11.0.0" - } - }, - "node_modules/mongodb-connection-string-url/node_modules/@types/whatwg-url": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", - "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", - "dependencies": { - "@types/node": "*", - "@types/webidl-conversions": "*" - } - }, - "node_modules/mongodb-legacy": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mongodb-legacy/-/mongodb-legacy-5.0.0.tgz", - "integrity": "sha512-q2G+MRwde6114bCAF/EZLmMXSsebIKMHmzsfOJq6M/Tj4gr3wLT50+rJsJNkiR0e0kjFx3dllWjqwRR1n11Zsw==", - "dev": true, - "dependencies": { - "mongodb": "^5.0.0" - }, - "engines": { - "node": ">=14.20.1" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index 299e47a61c8..12003ba28a2 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@mongodb-js/zstd": "^1.1.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=2.3.0 <3", + "mongodb-client-encryption": ">=6.0.0-alpha.0 <7", "snappy": "^7.2.2" }, "peerDependenciesMeta": { @@ -94,7 +94,7 @@ "js-yaml": "^4.1.0", "mocha": "^10.2.0", "mocha-sinon": "^2.1.2", - "mongodb-client-encryption": "^2.8.0", + "mongodb-client-encryption": "^6.0.0-alpha.0", "mongodb-legacy": "^5.0.0", "nyc": "^15.1.0", "prettier": "^2.8.8", From b5ac95d1ceaf5716598fd699848cb90eb13be59c Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 24 Jul 2023 13:04:41 -0600 Subject: [PATCH 08/45] remove force install --- .evergreen/run-azure-kms-tests.sh | 2 +- .evergreen/run-gcp-kms-tests.sh | 2 +- .evergreen/run-serverless-tests.sh | 2 +- .evergreen/run-socks5-tests.sh | 2 +- .evergreen/run-tests.sh | 2 +- .evergreen/run-unit-tests.sh | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.evergreen/run-azure-kms-tests.sh b/.evergreen/run-azure-kms-tests.sh index 5f1d53de3c8..741b7135b8f 100644 --- a/.evergreen/run-azure-kms-tests.sh +++ b/.evergreen/run-azure-kms-tests.sh @@ -9,7 +9,7 @@ source ".evergreen/init-node-and-npm-env.sh" set -o xtrace -npm install mongodb-client-encryption@alpha --force +npm install mongodb-client-encryption@alpha export MONGODB_URI="mongodb://localhost:27017" diff --git a/.evergreen/run-gcp-kms-tests.sh b/.evergreen/run-gcp-kms-tests.sh index b3c4aeaa2b4..35cb203b073 100644 --- a/.evergreen/run-gcp-kms-tests.sh +++ b/.evergreen/run-gcp-kms-tests.sh @@ -9,7 +9,7 @@ source ".evergreen/init-node-and-npm-env.sh" set -o xtrace -npm install mongodb-client-encryption@alpha --force +npm install mongodb-client-encryption@alpha npm install gcp-metadata export MONGODB_URI="mongodb://localhost:27017" diff --git a/.evergreen/run-serverless-tests.sh b/.evergreen/run-serverless-tests.sh index 7ffae747521..df95d818448 100755 --- a/.evergreen/run-serverless-tests.sh +++ b/.evergreen/run-serverless-tests.sh @@ -10,7 +10,7 @@ if [ -z ${MONGODB_URI+omitted} ]; then echo "MONGODB_URI is unset" && exit 1; fi if [ -z ${SERVERLESS_ATLAS_USER+omitted} ]; then echo "SERVERLESS_ATLAS_USER is unset" && exit 1; fi if [ -z ${SERVERLESS_ATLAS_PASSWORD+omitted} ]; then echo "SERVERLESS_ATLAS_PASSWORD is unset" && exit 1; fi -npm install mongodb-client-encryption@alpha --force +npm install mongodb-client-encryption@alpha npx mocha \ --config test/mocha_mongodb.json \ diff --git a/.evergreen/run-socks5-tests.sh b/.evergreen/run-socks5-tests.sh index de77ac15ecb..9093fe5d0ab 100644 --- a/.evergreen/run-socks5-tests.sh +++ b/.evergreen/run-socks5-tests.sh @@ -20,7 +20,7 @@ function setup_fle() { # CSFLE_AWS_TEMP_ACCESS_KEY_ID, CSFLE_AWS_TEMP_SECRET_ACCESS_KEY, CSFLE_AWS_TEMP_SESSION_TOKEN . "$DRIVERS_TOOLS"/.evergreen/csfle/set-temp-creds.sh - npm i --force mongodb-client-encryption@alpha + npm i mongodb-client-encryption@alpha export KMIP_TLS_CA_FILE="${DRIVERS_TOOLS}/.evergreen/x509gen/ca.pem" export KMIP_TLS_CERT_FILE="${DRIVERS_TOOLS}/.evergreen/x509gen/client.pem" export TEST_CSFLE=true diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index c2988ae6edf..5daf11d3569 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -52,7 +52,7 @@ else source "$DRIVERS_TOOLS"/.evergreen/csfle/set-temp-creds.sh fi -npm install mongodb-client-encryption@alpha --force +npm install mongodb-client-encryption@alpha npm install @mongodb-js/zstd npm install snappy diff --git a/.evergreen/run-unit-tests.sh b/.evergreen/run-unit-tests.sh index 3f95c724d43..79a447a401c 100644 --- a/.evergreen/run-unit-tests.sh +++ b/.evergreen/run-unit-tests.sh @@ -4,6 +4,6 @@ set -o errexit # Exit the script with error if any of the commands fail source "${PROJECT_DIRECTORY}/.evergreen/init-node-and-npm-env.sh" set -o xtrace -npm i --force mongodb-client-encryption@alpha +npm i mongodb-client-encryption@alpha npx nyc npm run check:unit From d2fbad1fc118e39c91c04ef100eae2a57cb1a6b8 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 24 Jul 2023 17:54:31 -0600 Subject: [PATCH 09/45] address comments --- src/client-side-encryption/errors.ts | 4 ++++ src/client-side-encryption/providers/azure.ts | 14 +++++--------- src/client-side-encryption/providers/index.ts | 14 ++++++-------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/client-side-encryption/errors.ts b/src/client-side-encryption/errors.ts index 561260bc1a9..615446e4c70 100644 --- a/src/client-side-encryption/errors.ts +++ b/src/client-side-encryption/errors.ts @@ -5,6 +5,7 @@ import { type Document } from '../bson'; * An error indicating that something went wrong specifically with MongoDB Client Encryption */ export class MongoCryptError extends Error { + /** @internal */ constructor(message: string, options: { cause?: Error } = {}) { super(message, options); } @@ -20,6 +21,7 @@ export class MongoCryptError extends Error { */ export class MongoCryptCreateDataKeyError extends MongoCryptError { encryptedFields: Document; + /** @internal */ constructor({ encryptedFields, cause }: { encryptedFields: Document; cause: Error }) { super(`Unable to complete creating data keys: ${cause.message}`, { cause }); this.encryptedFields = encryptedFields; @@ -36,6 +38,7 @@ export class MongoCryptCreateDataKeyError extends MongoCryptError { */ export class MongoCryptCreateEncryptedCollectionError extends MongoCryptError { encryptedFields: Document; + /** @internal */ constructor({ encryptedFields, cause }: { encryptedFields: Document; cause: Error }) { super(`Unable to create collection: ${cause.message}`, { cause }); this.encryptedFields = encryptedFields; @@ -53,6 +56,7 @@ export class MongoCryptCreateEncryptedCollectionError extends MongoCryptError { export class MongoCryptAzureKMSRequestError extends MongoCryptError { /** The body of the http response that failed, if present. */ body?: Document; + /** @internal */ constructor(message: string, body?: Document) { super(message); this.body = body; diff --git a/src/client-side-encryption/providers/azure.ts b/src/client-side-encryption/providers/azure.ts index 0dd61b72a92..d3620e52008 100644 --- a/src/client-side-encryption/providers/azure.ts +++ b/src/client-side-encryption/providers/azure.ts @@ -1,8 +1,8 @@ -import { type Document } from 'bson'; +import { type Document } from '../../bson'; import { MongoCryptAzureKMSRequestError, MongoCryptKMSRequestNetworkTimeoutError } from '../errors'; -import { type KMSProviders } from '.'; -import * as utils from './utils'; +import { type KMSProviders } from './index'; +import { get } from './utils'; const MINIMUM_TOKEN_REFRESH_IN_MILLISECONDS = 6000; @@ -124,11 +124,7 @@ export function prepareRequest(options: AzureKMSRequestOptions): { const url = options.url == null ? new URL('http://169.254.169.254/metadata/identity/oauth2/token') - : // The Node URL constructor technically supports "any object that converts to a valid URL string", - // but Node types doesn't support this. See the Node docs for new URL(). - // https://nodejs.org/api/url.html#new-urlinput-base - // @ts-expect-error URL constructor typings incorrect - new URL(options.url); + : new URL(options.url.toString()); url.searchParams.append('api-version', '2018-02-01'); url.searchParams.append('resource', 'https://vault.azure.net'); @@ -151,7 +147,7 @@ export async function fetchAzureKMSToken( options: AzureKMSRequestOptions = {} ): Promise { const { headers, url } = prepareRequest(options); - const response = await utils.get(url, { headers }).catch(error => { + const response = await get(url, { headers }).catch(error => { if (error instanceof MongoCryptKMSRequestNetworkTimeoutError) { throw new MongoCryptAzureKMSRequestError(`[Azure KMS] ${error.message}`); } diff --git a/src/client-side-encryption/providers/index.ts b/src/client-side-encryption/providers/index.ts index 0d56e0c3ca9..7125f1c988c 100644 --- a/src/client-side-encryption/providers/index.ts +++ b/src/client-side-encryption/providers/index.ts @@ -132,15 +132,13 @@ export interface KMSProviders { * * @internal - exposed for testing purposes only */ -export function isEmptyCredentials(provider: KMSProvider, kmsProviders: KMSProviders) { +export function isEmptyCredentials(providerName: KMSProvider, kmsProviders: KMSProviders) { + const provider = kmsProviders[providerName]; return ( - provider in kmsProviders && - kmsProviders[provider] != null && - typeof kmsProviders[provider] === 'object' && - // Typescript does not infer that kmsProviders[provider] is non-null, - // even though we check it two lines up. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Object.keys(kmsProviders[provider]!).length === 0 + provider && + provider != null && + typeof provider === 'object' && + Object.keys(provider).length === 0 ); } From 20a4aceb3f1fa967b06178afe6dbc2ff25f3dadb Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 25 Jul 2023 08:34:06 -0600 Subject: [PATCH 10/45] cleanup --- src/client-side-encryption/providers/azure.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/client-side-encryption/providers/azure.ts b/src/client-side-encryption/providers/azure.ts index d3620e52008..eaac6422a77 100644 --- a/src/client-side-encryption/providers/azure.ts +++ b/src/client-side-encryption/providers/azure.ts @@ -121,10 +121,7 @@ export function prepareRequest(options: AzureKMSRequestOptions): { headers: Document; url: URL; } { - const url = - options.url == null - ? new URL('http://169.254.169.254/metadata/identity/oauth2/token') - : new URL(options.url.toString()); + 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'); From b48942f28915ab36bb4be6ef4758417641d34488 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 25 Jul 2023 08:59:06 -0600 Subject: [PATCH 11/45] fix unit tests --- src/client-side-encryption/providers/index.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/client-side-encryption/providers/index.ts b/src/client-side-encryption/providers/index.ts index 7125f1c988c..5e4024d51e8 100644 --- a/src/client-side-encryption/providers/index.ts +++ b/src/client-side-encryption/providers/index.ts @@ -132,14 +132,12 @@ export interface KMSProviders { * * @internal - exposed for testing purposes only */ -export function isEmptyCredentials(providerName: KMSProvider, kmsProviders: KMSProviders) { +export function isEmptyCredentials(providerName: KMSProvider, kmsProviders: KMSProviders): boolean { const provider = kmsProviders[providerName]; - return ( - provider && - provider != null && - typeof provider === 'object' && - Object.keys(provider).length === 0 - ); + if (provider == null) { + return false; + } + return typeof provider === 'object' && Object.keys(provider).length === 0; } /** From f7e0270bc883b6406d983e84f532bcff5b2f11ca Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 25 Jul 2023 12:46:11 -0600 Subject: [PATCH 12/45] convert to TS --- src/client-side-encryption/{autoEncrypter.js => autoEncrypter.ts} | 0 src/client-side-encryption/{buffer_pool.js => buffer_pool.ts} | 0 .../{cryptoCallbacks.js => cryptoCallbacks.ts} | 0 .../{mongocryptdManager.js => mongocryptdManager.ts} | 0 .../{autoEncrypter.test.js => autoEncrypter.test.ts} | 0 .../{mongocryptdManager.test.js => mongocryptdManager.test.ts} | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename src/client-side-encryption/{autoEncrypter.js => autoEncrypter.ts} (100%) rename src/client-side-encryption/{buffer_pool.js => buffer_pool.ts} (100%) rename src/client-side-encryption/{cryptoCallbacks.js => cryptoCallbacks.ts} (100%) rename src/client-side-encryption/{mongocryptdManager.js => mongocryptdManager.ts} (100%) rename test/unit/client-side-encryption/{autoEncrypter.test.js => autoEncrypter.test.ts} (100%) rename test/unit/client-side-encryption/{mongocryptdManager.test.js => mongocryptdManager.test.ts} (100%) diff --git a/src/client-side-encryption/autoEncrypter.js b/src/client-side-encryption/autoEncrypter.ts similarity index 100% rename from src/client-side-encryption/autoEncrypter.js rename to src/client-side-encryption/autoEncrypter.ts diff --git a/src/client-side-encryption/buffer_pool.js b/src/client-side-encryption/buffer_pool.ts similarity index 100% rename from src/client-side-encryption/buffer_pool.js rename to src/client-side-encryption/buffer_pool.ts diff --git a/src/client-side-encryption/cryptoCallbacks.js b/src/client-side-encryption/cryptoCallbacks.ts similarity index 100% rename from src/client-side-encryption/cryptoCallbacks.js rename to src/client-side-encryption/cryptoCallbacks.ts diff --git a/src/client-side-encryption/mongocryptdManager.js b/src/client-side-encryption/mongocryptdManager.ts similarity index 100% rename from src/client-side-encryption/mongocryptdManager.js rename to src/client-side-encryption/mongocryptdManager.ts diff --git a/test/unit/client-side-encryption/autoEncrypter.test.js b/test/unit/client-side-encryption/autoEncrypter.test.ts similarity index 100% rename from test/unit/client-side-encryption/autoEncrypter.test.js rename to test/unit/client-side-encryption/autoEncrypter.test.ts diff --git a/test/unit/client-side-encryption/mongocryptdManager.test.js b/test/unit/client-side-encryption/mongocryptdManager.test.ts similarity index 100% rename from test/unit/client-side-encryption/mongocryptdManager.test.js rename to test/unit/client-side-encryption/mongocryptdManager.test.ts From 433b186465ec59d17f7bbcf727ab76baee315799 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 25 Jul 2023 10:22:21 -0600 Subject: [PATCH 13/45] remove bufferpool duplicate implementation --- src/client-side-encryption/buffer_pool.ts | 119 --------------------- src/client-side-encryption/stateMachine.js | 2 +- 2 files changed, 1 insertion(+), 120 deletions(-) delete mode 100644 src/client-side-encryption/buffer_pool.ts diff --git a/src/client-side-encryption/buffer_pool.ts b/src/client-side-encryption/buffer_pool.ts deleted file mode 100644 index a1164ab1a31..00000000000 --- a/src/client-side-encryption/buffer_pool.ts +++ /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/stateMachine.js b/src/client-side-encryption/stateMachine.js index c08fb259b2d..a057ff991a8 100644 --- a/src/client-side-encryption/stateMachine.js +++ b/src/client-side-encryption/stateMachine.js @@ -8,7 +8,7 @@ import { SocksClient } from 'socks'; import { MongoNetworkTimeoutError } from '../error'; import { debug, databaseNamespace, collectionNamespace } from './common'; import { MongoCryptError } from './errors'; -import { BufferPool } from './buffer_pool'; +import { BufferPool } from '../utils'; import { serialize, deserialize } from '../bson'; // libmongocrypt states From 24c208198aba2d5b35b8f2b3ed96fe51c4dbdc99 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 25 Jul 2023 10:24:49 -0600 Subject: [PATCH 14/45] lazy import once and spawn --- src/client-side-encryption/mongocryptdManager.ts | 4 ++-- src/client-side-encryption/stateMachine.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client-side-encryption/mongocryptdManager.ts b/src/client-side-encryption/mongocryptdManager.ts index 3bde6df2f15..d27aad884de 100644 --- a/src/client-side-encryption/mongocryptdManager.ts +++ b/src/client-side-encryption/mongocryptdManager.ts @@ -1,5 +1,3 @@ -import { spawn } from 'child_process' - /** * @internal * An internal class that handles spawning a mongocryptd. @@ -45,6 +43,8 @@ export class MongocryptdManager { spawn(callback) { const cmdName = this.spawnPath || 'mongocryptd'; + const { spawn } = require('child_process'); + // Spawned with stdio: ignore and detatched:true // to ensure child can outlive parent. this._child = spawn(cmdName, this.spawnArgs, { diff --git a/src/client-side-encryption/stateMachine.js b/src/client-side-encryption/stateMachine.js index a057ff991a8..d713fcd996d 100644 --- a/src/client-side-encryption/stateMachine.js +++ b/src/client-side-encryption/stateMachine.js @@ -3,7 +3,6 @@ import { promisify } from 'util'; import * as tls from 'tls'; import * as net from 'net'; import * as fs from 'fs'; -import { once } from 'events'; import { SocksClient } from 'socks'; import { MongoNetworkTimeoutError } from '../error'; import { debug, databaseNamespace, collectionNamespace } from './common'; @@ -288,6 +287,7 @@ class StateMachine { rawSocket.on('timeout', ontimeout); rawSocket.on('error', onerror); try { + const { once } = require('events'); await once(rawSocket, 'connect'); options.socket = ( await SocksClient.createConnection({ From 65dfc9449f7a68d95c1d50d54c3e2c5f88655eac Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 25 Jul 2023 10:35:09 -0600 Subject: [PATCH 15/45] convert mongocryptdmangager to TS --- src/client-side-encryption/autoEncrypter.ts | 4 ++- .../mongocryptdManager.ts | 26 +++++++++++-------- .../mongocryptdManager.test.ts | 16 ++++++------ 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/client-side-encryption/autoEncrypter.ts b/src/client-side-encryption/autoEncrypter.ts index d6fafe0e5d3..cd0f5bdb4a1 100644 --- a/src/client-side-encryption/autoEncrypter.ts +++ b/src/client-side-encryption/autoEncrypter.ts @@ -6,7 +6,7 @@ import {MongoError} from '../error'; import { loadCredentials } from './providers'; import * as cryptoCallbacks from './cryptoCallbacks'; import { serialize, deserialize } from '../bson'; -import { getMongoDBClientEncryption } from '../deps'; +import { AutoEncryptionOptions, getMongoDBClientEncryption } from '../deps'; /** * Configuration options for a automatic client encryption. @@ -33,6 +33,8 @@ import { getMongoDBClientEncryption } from '../deps'; * @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 \* */ +export type AutoEncryptionExtraOptions = NonNullable; + /** * @callback AutoEncrypter~logger * @description A callback that is invoked with logging information from diff --git a/src/client-side-encryption/mongocryptdManager.ts b/src/client-side-encryption/mongocryptdManager.ts index d27aad884de..541f1f7df0a 100644 --- a/src/client-side-encryption/mongocryptdManager.ts +++ b/src/client-side-encryption/mongocryptdManager.ts @@ -1,3 +1,8 @@ +import type { ChildProcess } from 'child_process'; + +import { type Callback } from '../utils'; +import { type AutoEncryptionExtraOptions } from './autoEncrypter'; + /** * @internal * An internal class that handles spawning a mongocryptd. @@ -5,14 +10,13 @@ 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 || {}; + 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 @@ -30,19 +34,19 @@ export class MongocryptdManager { .filter(arg => typeof arg === 'string') .every(arg => arg.indexOf('--idleShutdownTimeoutSecs') < 0) ) { - this.spawnArgs.push('--idleShutdownTimeoutSecs', 60); + 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) { + spawn(callback: Callback) { const cmdName = this.spawnPath || 'mongocryptd'; + // TODO - figure out why we can't type this. + // eslint-disable-next-line @typescript-eslint/no-var-requires const { spawn } = require('child_process'); // Spawned with stdio: ignore and detatched:true diff --git a/test/unit/client-side-encryption/mongocryptdManager.test.ts b/test/unit/client-side-encryption/mongocryptdManager.test.ts index 19529a19d39..a4e2ec7a14e 100644 --- a/test/unit/client-side-encryption/mongocryptdManager.test.ts +++ b/test/unit/client-side-encryption/mongocryptdManager.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/mongocryptdManager'; 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 () { From dc33b986959caae57e90feba5fc8e5cd626068ed Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 25 Jul 2023 10:51:30 -0600 Subject: [PATCH 16/45] convert cryptocallbacks --- src/client-side-encryption/cryptoCallbacks.ts | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/client-side-encryption/cryptoCallbacks.ts b/src/client-side-encryption/cryptoCallbacks.ts index 30ac77fe336..1d799cd2d97 100644 --- a/src/client-side-encryption/cryptoCallbacks.ts +++ b/src/client-side-encryption/cryptoCallbacks.ts @@ -1,11 +1,26 @@ import * as crypto from 'crypto'; -export function makeAES256Hook(method, mode) { - return function (key, iv, input, output) { +type AES256Callback = ( + key: Uint8Array, + iv: Uint8Array, + input: Uint8Array, + output: Uint8Array +) => number | Error; + +export function makeAES256Hook( + method: 'createCipheriv' | 'createDecipheriv', + mode: 'aes-256-cbc' | 'aes-256-ctr' +): AES256Callback { + return function ( + key: Uint8Array, + iv: Uint8Array, + input: Uint8Array, + output: Uint8Array + ): 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 +36,7 @@ export function makeAES256Hook(method, mode) { }; } -export function randomHook(buffer, count) { +export function randomHook(buffer: Uint8Array, count: number): number | Error { try { crypto.randomFillSync(buffer, 0, count); } catch (e) { @@ -30,7 +45,7 @@ export function randomHook(buffer, count) { return count; } -export function sha256Hook(input, output) { +export function sha256Hook(input: Uint8Array, output: Uint8Array): number | Error { let result; try { result = crypto.createHash('sha256').update(input).digest(); @@ -42,8 +57,9 @@ export function sha256Hook(input, output) { return result.length; } -export function makeHmacHook(algorithm) { - return (key, input, output) => { +type HMACHook = (key: Uint8Array, input: Uint8Array, output: Uint8Array) => number | Error; +export function makeHmacHook(algorithm: 'sha512' | 'sha256'): HMACHook { + return (key: Uint8Array, input: Uint8Array, output: Uint8Array): number | Error => { let result; try { result = crypto.createHmac(algorithm, key).update(input).digest(); @@ -56,11 +72,16 @@ export function makeHmacHook(algorithm) { }; } -export function signRsaSha256Hook(key, input, output) { +export function signRsaSha256Hook( + key: Uint8Array, + input: Uint8Array, + output: Uint8Array +): number | Error { let result; try { const signer = crypto.createSign('sha256WithRSAEncryption'); const privateKey = Buffer.from( + // TODO: check this `-----BEGIN PRIVATE KEY-----\n${key.toString('base64')}\n-----END PRIVATE KEY-----\n` ); @@ -73,7 +94,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'); From ec4c6de5fe78b3f3360e8e2fcd365d2243013a5c Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 25 Jul 2023 11:43:46 -0600 Subject: [PATCH 17/45] convert auto encrypter to TS --- src/client-side-encryption/autoEncrypter.ts | 409 ++++++++++++++---- src/deps.ts | 206 --------- .../autoEncrypter.test.ts | 101 +++-- 3 files changed, 375 insertions(+), 341 deletions(-) diff --git a/src/client-side-encryption/autoEncrypter.ts b/src/client-side-encryption/autoEncrypter.ts index cd0f5bdb4a1..e9992c2dcde 100644 --- a/src/client-side-encryption/autoEncrypter.ts +++ b/src/client-side-encryption/autoEncrypter.ts @@ -1,52 +1,224 @@ +import type { MongoCrypt } from 'mongodb-client-encryption'; + +import { deserialize, type Document, serialize } from '../bson'; +import { type ProxyOptions } from '../cmap/connection'; +import { getMongoDBClientEncryption } from '../deps'; +import { type AnyError, MongoError, MongoRuntimeError } from '../error'; +import { MongoClient, type MongoClientOptions } from '../mongo_client'; +import { type Callback } from '../utils'; import { databaseNamespace } from './common'; -import { StateMachine } from './stateMachine'; +import * as cryptoCallbacks from './cryptoCallbacks'; 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 { AutoEncryptionOptions, getMongoDBClientEncryption } from '../deps'; +import { type KMSProviders, loadCredentials } from './providers'; +import { StateMachine } from './stateMachine'; -/** - * 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 - */ +/** @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; + }; +} /** * 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 \* */ - export type AutoEncryptionExtraOptions = NonNullable; -/** - * @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 - */ +/** @public */ +export const AutoEncryptionLoggerLevel = Object.freeze({ + FatalError: 0, + Error: 1, + Warning: 2, + Info: 3, + Trace: 4 +} as const); /** - * @name AutoEncrypter~logLevel - * @enum {number} - * @description + * @public * The level of severity of the log message * * | Value | Level | @@ -57,12 +229,29 @@ export type AutoEncryptionExtraOptions = NonNullable; + _kmsProviders: KMSProviders; + _bypassMongocryptdAndCryptShared: boolean; + _contextCounter: number; + + _mongocryptdManager?: MongocryptdManager; + _mongocryptdClient?: MongoClient; + + _mongocrypt: MongoCrypt; /** * Create an AutoEncrypter * @@ -72,8 +261,6 @@ export class AutoEncrypter { * 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 @@ -109,7 +296,7 @@ export class AutoEncrypter { * await client.connect(); * // From here on, the client will be encrypting / decrypting automatically */ - constructor(client, options) { + constructor(client: MongoClient, options: AutoEncryptionOptions) { this._client = client; this._bypassEncryption = options.bypassAutoEncryption === true; @@ -118,10 +305,10 @@ export class AutoEncrypter { this._metaDataClient = options.metadataClient || client; this._proxyOptions = options.proxyOptions || {}; this._tlsOptions = options.tlsOptions || {}; - this._onKmsProviderRefresh = options.onKmsProviderRefresh; this._kmsProviders = options.kmsProviders || {}; - const mongoCryptOptions = {}; + // TODO: Add proper type support here, once the bindings are finished. + const mongoCryptOptions: Document = {}; if (options.schemaMap) { mongoCryptOptions.schemaMap = Buffer.isBuffer(options.schemaMap) ? options.schemaMap @@ -138,8 +325,8 @@ export class AutoEncrypter { ? serialize(this._kmsProviders) : this._kmsProviders; - if (options.logger) { - mongoCryptOptions.logger = options.logger; + if (options.options?.logger) { + mongoCryptOptions.logger = options.options.logger; } if (options.extraOptions && options.extraOptions.cryptSharedLibPath) { @@ -150,18 +337,18 @@ export class AutoEncrypter { mongoCryptOptions.bypassQueryAnalysis = options.bypassQueryAnalysis; } - this._bypassMongocryptdAndCryptShared = this._bypassEncryption || options.bypassQueryAnalysis; + this._bypassMongocryptdAndCryptShared = this._bypassEncryption || !!options.bypassQueryAnalysis; if (options.extraOptions && options.extraOptions.cryptSharedLibSearchPaths) { // Only for driver testing - mongoCryptOptions.cryptSharedLibSearchPaths = - options.extraOptions.cryptSharedLibSearchPaths; + mongoCryptOptions.cryptSharedLibSearchPaths = options.extraOptions.cryptSharedLibSearchPaths; } else if (!this._bypassMongocryptdAndCryptShared) { mongoCryptOptions.cryptSharedLibSearchPaths = ['$SYSTEM']; } Object.assign(mongoCryptOptions, { cryptoCallbacks }); - const { MongoCrypt } = getMongoDBClientEncryption(); + // TODO - fix typing issue here. + const { MongoCrypt } = getMongoDBClientEncryption()!; this._mongocrypt = new MongoCrypt(mongoCryptOptions); this._contextCounter = 0; @@ -177,14 +364,11 @@ export class AutoEncrypter { // that we are not using the CSFLE shared library. if (!this._bypassMongocryptdAndCryptShared && !this.cryptSharedLibVersionInfo) { this._mongocryptdManager = new MongocryptdManager(options.extraOptions); - const clientOptions = { + const clientOptions: MongoClientOptions = { serverSelectionTimeoutMS: 10000 }; - if ( - options.extraOptions == null || - typeof options.extraOptions.mongocryptdURI !== 'string' - ) { + if (options.extraOptions == null || typeof options.extraOptions.mongocryptdURI !== 'string') { clientOptions.family = 4; } @@ -193,14 +377,29 @@ export class AutoEncrypter { } /** - * @ignore - * @param {Function} callback Invoked when the mongocryptd client either successfully connects or errors + * 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) { + init(callback: Callback) { if (this._bypassMongocryptdAndCryptShared || this.cryptSharedLibVersionInfo) { return callback(); } - const _callback = (err, res) => { + 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 && @@ -220,35 +419,41 @@ export class AutoEncrypter { if (this._mongocryptdManager.bypassSpawn) { return this._mongocryptdClient.connect().then( result => { - return _callback(null, result); + return _callback(undefined, result); }, error => { - _callback(error, null); + _callback(error, undefined); } ); } 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(null, result); + return _callback(undefined, result); }, error => { - _callback(error, null); + _callback(error, undefined); } ); }); } /** - * @ignore - * @param {Function} callback Invoked when the mongocryptd client either successfully disconnects or errors + * Cleans up the `_mongocryptdClient`, if present. */ - teardown(force, callback) { + teardown(force: boolean, callback: Callback) { if (this._mongocryptdClient) { this._mongocryptdClient.close(force).then( result => { - return callback(null, result); + return callback(undefined, result); }, error => { callback(error); @@ -259,15 +464,16 @@ export class AutoEncrypter { } } + encrypt(ns: string, cmd: Document, callback: Callback): void; /** - * @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) { + encrypt( + ns: string, + cmd: Document, + options?: Document | Callback, + callback?: Callback + ) { if (typeof ns !== 'string') { throw new TypeError('Parameter `ns` must be a string'); } @@ -276,11 +482,15 @@ export class AutoEncrypter { throw new TypeError('Parameter `cmd` must be an object'); } - if (typeof options === 'function' && callback == null) { - callback = options; - options = {}; + callback = + typeof options === 'function' ? (options as Callback) : callback; + + if (callback == null) { + throw new TypeError('Callback must be provided'); } + options = typeof options === 'function' ? {} : options; + // If `bypassAutoEncryption` has been specified, don't encrypt if (this._bypassEncryption) { callback(undefined, cmd); @@ -293,7 +503,7 @@ export class AutoEncrypter { try { context = this._mongocrypt.makeEncryptionContext(databaseNamespace(ns), commandBuffer); } catch (err) { - callback(err, null); + callback(err, undefined); return; } @@ -313,25 +523,30 @@ export class AutoEncrypter { } /** - * @ignore * Decrypt a command response * - * @param {Buffer} buffer - * @param {Function} callback + * TODO: type options */ - decrypt(response, options, callback) { - if (typeof options === 'function' && callback == null) { - callback = options; - options = {}; + decrypt( + response: Uint8Array, + options: Document | Callback, + callback?: Callback + ) { + callback = typeof options === 'function' ? (options as Callback) : callback; + + if (callback == null) { + throw new TypeError('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, null); + callback(err, undefined); return; } @@ -344,14 +559,15 @@ export class AutoEncrypter { tlsOptions: this._tlsOptions }); + // @ts-expect-error unique symbol cannot be used as an index type const decorateResult = this[Symbol.for('@@mdb.decorateDecryptionResult')]; - stateMachine.execute(this, context, function (err, result) { + stateMachine.execute(this, context, function (error?: Error, result?: Document) { // Only for testing/internal usage - if (!err && result && decorateResult) { - err = decorateDecryptionResult(result, response); - if (err) return callback(err); + if (!error && result && decorateResult) { + const error = decorateDecryptionResult(result, response); + if (error) return callback!(error); } - callback(err, result); + callback!(error, result); }); } @@ -362,10 +578,8 @@ export class AutoEncrypter { * 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); + async askForKMSCredentials(): Promise { + return loadCredentials(this._kmsProviders); } /** @@ -378,7 +592,7 @@ export class AutoEncrypter { } static get libmongocryptVersion() { - const { MongoCrypt } = getMongoDBClientEncryption(); + const { MongoCrypt } = getMongoDBClientEncryption()!; return MongoCrypt.libmongocryptVersion; } } @@ -390,9 +604,12 @@ export class AutoEncrypter { * we do not need to worry about circular references. * * @internal - * @ignore */ -function decorateDecryptionResult(decrypted, original, isTopLevelDecorateCall = true) { +function decorateDecryptionResult( + decrypted: Document, + original: Document, + isTopLevelDecorateCall = true +): Error | void { const decryptedKeys = Symbol.for('@@mdb.decryptedKeys'); if (isTopLevelDecorateCall) { // The original value could have been either a JS object or a BSON buffer @@ -411,6 +628,7 @@ function decorateDecryptionResult(decrypted, original, isTopLevelDecorateCall = // 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) { + // @ts-expect-error unique symbols cannot be used as index type if (!decrypted[decryptedKeys]) { Object.defineProperty(decrypted, decryptedKeys, { value: [], @@ -419,6 +637,7 @@ function decorateDecryptionResult(decrypted, original, isTopLevelDecorateCall = writable: false }); } + // @ts-expect-error unique symbols cannot be used as index type 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. diff --git a/src/deps.ts b/src/deps.ts index f3c8965685b..7fee0e8880b 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -1,8 +1,5 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -import type { Document } from './bson'; -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) { @@ -222,209 +219,6 @@ 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 { diff --git a/test/unit/client-side-encryption/autoEncrypter.test.ts b/test/unit/client-side-encryption/autoEncrypter.test.ts index 96c76f9b74a..7efce0a8940 100644 --- a/test/unit/client-side-encryption/autoEncrypter.test.ts +++ b/test/unit/client-side-encryption/autoEncrypter.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/autoEncrypter'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { MongocryptdManager } from '../../../src/client-side-encryption/mongocryptdManager'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { StateMachine } from '../../../src/client-side-encryption/stateMachine'; +// 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) } @@ -225,7 +231,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: {} }, @@ -266,7 +275,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 +317,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 +338,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) } @@ -356,7 +374,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: {} }, From fcdf41b6020ab43789035bf39f59f1f75956ced2 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 25 Jul 2023 13:08:15 -0600 Subject: [PATCH 18/45] cleanups after linking with latest bindings --- src/client-side-encryption/autoEncrypter.ts | 8 ++++++-- src/client-side-encryption/cryptoCallbacks.ts | 2 +- src/client-side-encryption/mongocryptdManager.ts | 4 +--- src/client-side-encryption/stateMachine.js | 4 +--- src/deps.ts | 5 +---- src/encrypter.ts | 4 ++-- src/index.ts | 7 +++++-- src/mongo_client.ts | 3 +-- 8 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/client-side-encryption/autoEncrypter.ts b/src/client-side-encryption/autoEncrypter.ts index e9992c2dcde..b84a36fdd8f 100644 --- a/src/client-side-encryption/autoEncrypter.ts +++ b/src/client-side-encryption/autoEncrypter.ts @@ -1,4 +1,3 @@ -import type { MongoCrypt } from 'mongodb-client-encryption'; import { deserialize, type Document, serialize } from '../bson'; import { type ProxyOptions } from '../cmap/connection'; @@ -251,7 +250,8 @@ export class AutoEncrypter { _mongocryptdManager?: MongocryptdManager; _mongocryptdClient?: MongoClient; - _mongocrypt: MongoCrypt; + _mongocrypt: import('mongodb-client-encryption').MongoCrypt; + /** * Create an AutoEncrypter * @@ -508,8 +508,11 @@ export class AutoEncrypter { } // TODO: should these be accessors from the addon? + // @ts-expect-error context.id = this._contextCounter++; + // @ts-expect-error context.ns = ns; + // @ts-expect-error context.document = cmd; const stateMachine = new StateMachine({ @@ -551,6 +554,7 @@ export class AutoEncrypter { } // TODO: should this be an accessor from the addon? + // @ts-expect-error context.id = this._contextCounter++; const stateMachine = new StateMachine({ diff --git a/src/client-side-encryption/cryptoCallbacks.ts b/src/client-side-encryption/cryptoCallbacks.ts index 1d799cd2d97..d3c04f28c2a 100644 --- a/src/client-side-encryption/cryptoCallbacks.ts +++ b/src/client-side-encryption/cryptoCallbacks.ts @@ -82,7 +82,7 @@ export function signRsaSha256Hook( const signer = crypto.createSign('sha256WithRSAEncryption'); const privateKey = Buffer.from( // TODO: check this - `-----BEGIN PRIVATE KEY-----\n${key.toString('base64')}\n-----END PRIVATE KEY-----\n` + `-----BEGIN PRIVATE KEY-----\n${key.toString()}\n-----END PRIVATE KEY-----\n` ); result = signer.update(input).end().sign(privateKey); diff --git a/src/client-side-encryption/mongocryptdManager.ts b/src/client-side-encryption/mongocryptdManager.ts index 541f1f7df0a..0694b87112a 100644 --- a/src/client-side-encryption/mongocryptdManager.ts +++ b/src/client-side-encryption/mongocryptdManager.ts @@ -45,9 +45,7 @@ export class MongocryptdManager { spawn(callback: Callback) { const cmdName = this.spawnPath || 'mongocryptd'; - // TODO - figure out why we can't type this. - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { spawn } = require('child_process'); + const { spawn } = require('child_process') as typeof import('child_process'); // Spawned with stdio: ignore and detatched:true // to ensure child can outlive parent. diff --git a/src/client-side-encryption/stateMachine.js b/src/client-side-encryption/stateMachine.js index d713fcd996d..8d440948563 100644 --- a/src/client-side-encryption/stateMachine.js +++ b/src/client-side-encryption/stateMachine.js @@ -78,7 +78,7 @@ const INSECURE_TLS_OPTIONS = [ * a finishing state or an error is reached. Do not instantiate directly. * @class StateMachine */ -class StateMachine { +export class StateMachine { constructor(options) { this.options = options || {}; @@ -473,5 +473,3 @@ class StateMachine { ); } } - -module.exports = { StateMachine }; diff --git a/src/deps.ts b/src/deps.ts index 7fee0e8880b..701e5e18e26 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -219,9 +219,8 @@ try { aws4 = require('aws4'); } catch {} // eslint-disable-line -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') | null { let mongodbClientEncryption = null; try { @@ -235,5 +234,3 @@ export function getMongoDBClientEncryption(): MongoCrypt | null { return mongodbClientEncryption; } - -export type MongodbClientEncryption = ReturnType; diff --git a/src/encrypter.ts b/src/encrypter.ts index 53cfebacda4..6cbc0b345ca 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/autoEncrypter'; 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'; diff --git a/src/index.ts b/src/index.ts index 26282013f3f..d2bac9a41e2 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/autoEncrypter'; 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'; @@ -202,6 +202,10 @@ export type { UpdateDescription } from './change_stream'; export type { AutoEncrypter } from './client-side-encryption/autoEncrypter'; +export type { + AutoEncryptionOptions, + AutoEncryptionTlsOptions +} from './client-side-encryption/autoEncrypter'; export type { MongocryptdManager } from './client-side-encryption/mongocryptdManager'; export type { AuthContext } from './cmap/auth/auth_provider'; export type { @@ -287,7 +291,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 0c071086c15..7b682454c1e 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/autoEncrypter'; 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'; From 98faa9ade8a3360b6ee17d712affff40c110a2ec Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 26 Jul 2023 08:57:20 -0600 Subject: [PATCH 19/45] convert CE to TS --- src/client-side-encryption/autoEncrypter.ts | 27 +- ...lientEncryption.js => clientEncryption.ts} | 701 ++++++++++++------ src/client-side-encryption/stateMachine.js | 18 +- src/deps.ts | 12 +- src/encrypter.ts | 2 +- ...ption.test.js => clientEncryption.test.ts} | 60 +- 6 files changed, 529 insertions(+), 291 deletions(-) rename src/client-side-encryption/{clientEncryption.js => clientEncryption.ts} (54%) rename test/unit/client-side-encryption/{clientEncryption.test.js => clientEncryption.test.ts} (89%) diff --git a/src/client-side-encryption/autoEncrypter.ts b/src/client-side-encryption/autoEncrypter.ts index b84a36fdd8f..4c871217353 100644 --- a/src/client-side-encryption/autoEncrypter.ts +++ b/src/client-side-encryption/autoEncrypter.ts @@ -1,4 +1,3 @@ - import { deserialize, type Document, serialize } from '../bson'; import { type ProxyOptions } from '../cmap/connection'; import { getMongoDBClientEncryption } from '../deps'; @@ -252,6 +251,14 @@ export class AutoEncrypter { _mongocrypt: import('mongodb-client-encryption').MongoCrypt; + static getMongoCrypt(): import('mongodb-client-encryption').MongoCryptConstructor { + const encryption = getMongoDBClientEncryption(); + if ('kModuleError' in encryption) { + throw encryption.kModuleError; + } + return encryption.MongoCrypt; + } + /** * Create an AutoEncrypter * @@ -347,8 +354,7 @@ export class AutoEncrypter { } Object.assign(mongoCryptOptions, { cryptoCallbacks }); - // TODO - fix typing issue here. - const { MongoCrypt } = getMongoDBClientEncryption()!; + const MongoCrypt = AutoEncrypter.getMongoCrypt(); this._mongocrypt = new MongoCrypt(mongoCryptOptions); this._contextCounter = 0; @@ -508,11 +514,11 @@ export class AutoEncrypter { } // TODO: should these be accessors from the addon? - // @ts-expect-error + // @ts-expect-error - this is not defined in the bindings context.id = this._contextCounter++; - // @ts-expect-error + // @ts-expect-error - this is not defined in the bindings context.ns = ns; - // @ts-expect-error + // @ts-expect-error - this is not defined in the bindings context.document = cmd; const stateMachine = new StateMachine({ @@ -554,7 +560,7 @@ export class AutoEncrypter { } // TODO: should this be an accessor from the addon? - // @ts-expect-error + // @ts-expect-error - this is not defined in the bindings context.id = this._contextCounter++; const stateMachine = new StateMachine({ @@ -591,13 +597,12 @@ export class AutoEncrypter { * as `{ version: bigint, versionStr: string }`, or `null` if no CSFLE * shared library was loaded. */ - get cryptSharedLibVersionInfo() { + get cryptSharedLibVersionInfo(): { version: bigint; versionStr: string } | null { return this._mongocrypt.cryptSharedLibVersionInfo; } - static get libmongocryptVersion() { - const { MongoCrypt } = getMongoDBClientEncryption()!; - return MongoCrypt.libmongocryptVersion; + static get libmongocryptVersion(): string { + return AutoEncrypter.getMongoCrypt().libmongocryptVersion; } } diff --git a/src/client-side-encryption/clientEncryption.js b/src/client-side-encryption/clientEncryption.ts similarity index 54% rename from src/client-side-encryption/clientEncryption.js rename to src/client-side-encryption/clientEncryption.ts index 5c6e734a09c..163c094f3c5 100644 --- a/src/client-side-encryption/clientEncryption.js +++ b/src/client-side-encryption/clientEncryption.ts @@ -1,86 +1,64 @@ - -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 type { ExplicitEncryptionContextOptions, MongoCrypt } from 'mongodb-client-encryption'; 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) - */ +import { type Binary, type Document, type Long, serialize } from '../bson'; +import { 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 } from '../utils'; +import { collectionNamespace, databaseNamespace, maybeCallback, promiseOrCallback } from './common'; +import * as cryptoCallbacks from './cryptoCallbacks'; +import { MongoCryptCreateDataKeyError, MongoCryptCreateEncryptedCollectionError } from './errors'; +import { type KMSProvider, type KMSProviders, loadCredentials } from './providers/index'; +import { StateMachine } from './stateMachine'; /** - * @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 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 { + _client: MongoClient; + _keyVaultNamespace: string; + _keyVaultClient: MongoClient; + _proxyOptions: ProxyOptions; + _tlsOptions: Record; + _kmsProviders: KMSProviders; + + _mongoCrypt: MongoCrypt; + + static getMongoCrypt(): import('mongodb-client-encryption').MongoCryptConstructor { + const encryption = getMongoDBClientEncryption(); + if ('kModuleError' in encryption) { + throw encryption.kModuleError; + } + return encryption.MongoCrypt; + } + /** * 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 + * ```ts * new ClientEncryption(mongoClient, { * keyVaultNamespace: 'client.encryption', * kmsProviders: { @@ -89,8 +67,10 @@ export class ClientEncryption { * } * } * }); + * ``` * * @example + * ```ts * new ClientEncryption(mongoClient, { * keyVaultNamespace: 'client.encryption', * kmsProviders: { @@ -100,77 +80,36 @@ export class ClientEncryption { * } * } * }); + * ``` */ - constructor(client, options) { + constructor(client: MongoClient, options: ClientEncryptionOptions) { this._client = client; - this._proxyOptions = options.proxyOptions; - this._tlsOptions = options.tlsOptions; + 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 }; + // TODO: type this + const mongoCryptOptions: Document = { ...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(); + const MongoCrypt = ClientEncryption.getMongoCrypt(); 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 + * ```ts * // Using callbacks to create a local key * clientEncryption.createDataKey('local', (err, dataKey) => { * if (err) { @@ -179,12 +118,16 @@ export class ClientEncryption { * // 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: { @@ -192,8 +135,10 @@ export class ClientEncryption { * 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: { @@ -202,8 +147,13 @@ export class ClientEncryption { * }, * keyAltNames: [ 'mySpecialKey' ] * }); + * ``` */ - createDataKey(provider, options, callback) { + createDataKey( + provider: KMSProvider, + options?: ClientEncryptionCreateDataKeyProviderOptions, + callback?: Callback + ) { if (typeof options === 'function') { callback = options; options = {}; @@ -274,23 +224,13 @@ export class ClientEncryption { }); } - /** - * @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 + * ```ts * // rewrapping all data data keys (using a filter that matches all documents) * const filter = {}; * @@ -298,8 +238,10 @@ export class ClientEncryption { * 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); @@ -307,8 +249,9 @@ export class ClientEncryption { * if (result.bulkWriteResult == null) { * // no keys matched, `bulkWriteResult` does not exist on the result object * } + * ``` */ - async rewrapManyDataKey(filter, options) { + async rewrapManyDataKey(filter: Filter, options: RewrapManyDataKeyOptions) { let keyEncryptionKeyBson = undefined; if (options) { const keyEncryptionKey = Object.assign({ provider: options.provider }, options.masterKey); @@ -318,10 +261,7 @@ export class ClientEncryption { options = {}; } const filterBson = serialize(filter); - const context = this._mongoCrypt.makeRewrapManyDataKeyContext( - filterBson, - keyEncryptionKeyBson - ); + const context = this._mongoCrypt.makeRewrapManyDataKeyContext(filterBson, keyEncryptionKeyBson); const stateMachine = new StateMachine({ proxyOptions: this._proxyOptions, tlsOptions: this._tlsOptions @@ -364,10 +304,8 @@ export class ClientEncryption { /** * 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 + * ```ts * // delete a key by _id * const id = new Binary(); // id is a bson binary subtype 4 object * const { deletedCount } = await clientEncryption.deleteKey(id); @@ -375,14 +313,15 @@ export class ClientEncryption { * if (deletedCount != null && deletedCount > 0) { * // successful deletion * } + * ``` * */ - async deleteKey(_id) { + async deleteKey(_id: Binary): Promise { const dbName = databaseNamespace(this._keyVaultNamespace); const collectionName = collectionNamespace(this._keyVaultNamespace); - return await this._keyVaultClient + return this._keyVaultClient .db(dbName) - .collection(collectionName) + .collection(collectionName) .deleteOne({ _id }, { writeConcern: { w: 'majority' } }); } @@ -391,63 +330,68 @@ export class ClientEncryption { * * This method will not throw. * - * @returns {FindCursor} a FindCursor over all keys in the keyvault. + * @returns a FindCursor over all keys in the keyvault. * @example + * ```ts * // fetching all keys * const keys = await clientEncryption.getKeys().toArray(); + * ``` */ - getKeys() { + getKeys(): FindCursor { const dbName = databaseNamespace(this._keyVaultNamespace); const collectionName = collectionNamespace(this._keyVaultNamespace); return this._keyVaultClient .db(dbName) - .collection(collectionName) + .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 + * 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) { + async getKey(_id: Binary): Promise { const dbName = databaseNamespace(this._keyVaultNamespace); const collectionName = collectionNamespace(this._keyVaultNamespace); - return await this._keyVaultClient + return this._keyVaultClient .db(dbName) - .collection(collectionName) + .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 + * @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) { + async getKeyByAltName(keyAltName: string) { const dbName = databaseNamespace(this._keyVaultNamespace); const collectionName = collectionNamespace(this._keyVaultNamespace); - return await this._keyVaultClient + return this._keyVaultClient .db(dbName) - .collection(collectionName) + .collection(collectionName) .findOne({ keyAltNames: keyAltName }, { readConcern: { level: 'majority' } }); } @@ -456,11 +400,12 @@ export class ClientEncryption { * * 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 + * @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'; @@ -468,13 +413,14 @@ export class ClientEncryption { * if (!oldKey) { * // null is returned if there is no matching document with an id matching the supplied id * } + * ``` */ - async addKeyAltName(_id, keyAltName) { + async addKeyAltName(_id: Binary, keyAltName: string) { const dbName = databaseNamespace(this._keyVaultNamespace); const collectionName = collectionNamespace(this._keyVaultNamespace); const { value } = await this._keyVaultClient .db(dbName) - .collection(collectionName) + .collection(collectionName) .findOneAndUpdate( { _id }, { $addToSet: { keyAltNames: keyAltName } }, @@ -491,11 +437,12 @@ export class ClientEncryption { * * 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 + * @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'; @@ -504,8 +451,9 @@ export class ClientEncryption { * if (!oldKey) { * // null is returned if there is no matching document with an id matching the supplied id * } + * ``` */ - async removeKeyAltName(_id, keyAltName) { + async removeKeyAltName(_id: Binary, keyAltName: string) { const dbName = databaseNamespace(this._keyVaultNamespace); const collectionName = collectionNamespace(this._keyVaultNamespace); const pipeline = [ @@ -532,7 +480,7 @@ export class ClientEncryption { ]; const { value } = await this._keyVaultClient .db(dbName) - .collection(collectionName) + .collection(collectionName) .findOneAndUpdate({ _id }, pipeline, { writeConcern: { w: 'majority' }, returnDocument: 'before' @@ -546,18 +494,24 @@ export class ClientEncryption { * 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) { + * @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, @@ -572,9 +526,9 @@ export class ClientEncryption { field == null || typeof field !== 'object' || field.keyId != null ? field : { - ...field, - keyId: await this.createDataKey(provider, { masterKey }) - } + ...field, + keyId: await this.createDataKey(provider, { masterKey }) + } ); const createDataKeyResolutions = await Promise.allSettled(createDataKeyPromises); @@ -583,14 +537,16 @@ export class ClientEncryption { resolution.status === 'fulfilled' ? resolution.value : encryptedFields.fields[index] ); - const rejection = createDataKeyResolutions.find(({ status }) => status === 'rejected'); + 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, { + const collection = await db.createCollection(name, { ...createCollectionOptions, encryptedFields }); @@ -600,42 +556,17 @@ export class ClientEncryption { } } - /** - * @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. + * @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) => { @@ -645,22 +576,31 @@ export class ClientEncryption { * 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, options, callback) { + encrypt( + value: unknown, + options: ClientEncryptionEncryptOptions, + callback: Callback + ): Promise | void { return maybeCallback(() => this._encrypt(value, false, options), callback); } @@ -671,7 +611,7 @@ export class ClientEncryption { * * @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: + * @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: @@ -679,39 +619,40 @@ export class ClientEncryption { * * `$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. + * @param options - + * @returns Returns a Promise that either resolves with the encrypted value or rejects with an error. */ - async encryptExpression(expression, options) { + async encryptExpression( + expression: Document, + options: ClientEncryptionEncryptOptions + ): Promise { 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. + * @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, callback) { + decrypt(value: Binary, callback?: Callback): Promise | void { const valueBuffer = serialize({ v: value }); const context = this._mongoCrypt.makeExplicitDecryptionContext(valueBuffer); @@ -739,15 +680,12 @@ export class ClientEncryption { * 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); + async askForKMSCredentials(): Promise { + return loadCredentials(this._kmsProviders); } static get libmongocryptVersion() { - const { MongoCrypt } = getMongoDBClientEncryption(); - return MongoCrypt.libmongocryptVersion; + return ClientEncryption.getMongoCrypt().libmongocryptVersion; } /** @@ -755,18 +693,22 @@ export class ClientEncryption { * 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 + * @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. * - * @ignore - * */ - async _encrypt(value, expressionMode, options) { + private async _encrypt( + value: unknown, + expressionMode: boolean, + options: ClientEncryptionEncryptOptions + ): Promise { const valueBuffer = serialize({ v: value }); - const contextOptions = Object.assign({}, options, { expressionMode }); + const contextOptions: ExplicitEncryptionContextOptions = Object.assign({}, options, { + expressionMode + }); if (options.keyId) { contextOptions.keyId = options.keyId.buffer; } @@ -785,7 +727,7 @@ export class ClientEncryption { contextOptions.keyAltName = serialize({ keyAltName }); } - if ('rangeOptions' in options) { + if (typeof options.rangeOptions === 'object') { contextOptions.rangeOptions = serialize(options.rangeOptions); } @@ -798,6 +740,285 @@ export class ClientEncryption { const result = await stateMachine.executeAsync(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; +} + +/** + * TLS options to use when connecting. The spec specifically calls out which insecure + * tls options are not allowed: + * + * - tlsAllowInvalidCertificates + * - tlsAllowInvalidHostnames + * - tlsInsecure + * - tlsDisableOCSPEndpointCheck + * - tlsDisableCertificateRevocationCheck + */ +export interface ClientEncryptionTlsOptions { + /** + * Specifies the location of a local .pem file that contains + * either the client's TLS/SSL certificate and key or only the + * client's TLS/SSL key when tlsCertificateFile is used to + * provide the certificate. + */ + 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; +} + +/** + * 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; + + /** + * Optional callback to override KMS providers per-context. + * + * @deprecated Installing optional dependencies will automatically refresh kms + * provider credentials. + */ + onKmsProviderRefresh?: () => Promise; + + /** + * Options for specifying a Socks5 proxy to use for connecting to the KMS. + */ + proxyOptions?: ProxyOptions; + + /** + * TLS options for kms providers to use. + */ + tlsOptions?: { [kms in keyof KMSProviders]?: ClientEncryptionTlsOptions }; +} + +/** + * 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/stateMachine.js b/src/client-side-encryption/stateMachine.js index 8d440948563..451b1a27ddb 100644 --- a/src/client-side-encryption/stateMachine.js +++ b/src/client-side-encryption/stateMachine.js @@ -1,14 +1,14 @@ -import { promisify } from 'util'; - -import * as tls from 'tls'; -import * as net from 'net'; import * as fs from 'fs'; +import * as net from 'net'; import { SocksClient } from 'socks'; +import * as tls from 'tls'; +import { promisify } from 'util'; + +import { deserialize, serialize } from '../bson'; import { MongoNetworkTimeoutError } from '../error'; -import { debug, databaseNamespace, collectionNamespace } from './common'; -import { MongoCryptError } from './errors'; import { BufferPool } from '../utils'; -import { serialize, deserialize } from '../bson'; +import { collectionNamespace, databaseNamespace, debug } from './common'; +import { MongoCryptError } from './errors'; // libmongocrypt states const MONGOCRYPT_CTX_ERROR = 0; @@ -355,9 +355,7 @@ export class StateMachine { 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}`); } } } diff --git a/src/deps.ts b/src/deps.ts index 701e5e18e26..3ee06d8ec9e 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -220,7 +220,9 @@ try { } catch {} // eslint-disable-line /** A utility function to get the instance of mongodb-client-encryption, if it exists. */ -export function getMongoDBClientEncryption(): typeof import('mongodb-client-encryption') | null { +export function getMongoDBClientEncryption(): + | typeof import('mongodb-client-encryption') + | { kModuleError: MongoMissingDependencyError } { let mongodbClientEncryption = null; try { @@ -228,8 +230,12 @@ export function getMongoDBClientEncryption(): typeof import('mongodb-client-encr // 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; diff --git a/src/encrypter.ts b/src/encrypter.ts index 6cbc0b345ca..5c9d0a64166 100644 --- a/src/encrypter.ts +++ b/src/encrypter.ts @@ -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/test/unit/client-side-encryption/clientEncryption.test.js b/test/unit/client-side-encryption/clientEncryption.test.ts similarity index 89% rename from test/unit/client-side-encryption/clientEncryption.test.js rename to test/unit/client-side-encryption/clientEncryption.test.ts index 5927b6e69d7..b5c22dc789e 100644 --- a/test/unit/client-side-encryption/clientEncryption.test.js +++ b/test/unit/client-side-encryption/clientEncryption.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/clientEncryption'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import * as cryptoCallbacks from '../../../src/client-side-encryption/cryptoCallbacks'; +// 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/stateMachine'; +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); }); }); @@ -118,21 +120,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/); }); }); From 2c042338471bb20da348c4ca926b2d2a7dc23bc8 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 26 Jul 2023 12:21:00 -0600 Subject: [PATCH 20/45] convert SM to TS --- src/client-side-encryption/autoEncrypter.ts | 8 +- .../clientEncryption.ts | 4 +- .../{stateMachine.js => stateMachine.ts} | 256 ++++++++++-------- ...teMachine.test.js => stateMachine.test.ts} | 51 ++-- 4 files changed, 175 insertions(+), 144 deletions(-) rename src/client-side-encryption/{stateMachine.js => stateMachine.ts} (62%) rename test/unit/client-side-encryption/{stateMachine.test.js => stateMachine.test.ts} (87%) diff --git a/src/client-side-encryption/autoEncrypter.ts b/src/client-side-encryption/autoEncrypter.ts index 4c871217353..9cae6b40df0 100644 --- a/src/client-side-encryption/autoEncrypter.ts +++ b/src/client-side-encryption/autoEncrypter.ts @@ -8,7 +8,7 @@ import { databaseNamespace } from './common'; import * as cryptoCallbacks from './cryptoCallbacks'; import { MongocryptdManager } from './mongocryptdManager'; import { type KMSProviders, loadCredentials } from './providers'; -import { StateMachine } from './stateMachine'; +import { StateMachine, type StateMachineExecutable } from './stateMachine'; /** @public */ export interface AutoEncryptionTlsOptions { @@ -234,7 +234,7 @@ export type AutoEncryptionLoggerLevel = * @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 { +export class AutoEncrypter implements StateMachineExecutable { _client: MongoClient; _bypassEncryption: boolean; _keyVaultNamespace: string; @@ -514,11 +514,8 @@ export class AutoEncrypter { } // TODO: should these be accessors from the addon? - // @ts-expect-error - this is not defined in the bindings context.id = this._contextCounter++; - // @ts-expect-error - this is not defined in the bindings context.ns = ns; - // @ts-expect-error - this is not defined in the bindings context.document = cmd; const stateMachine = new StateMachine({ @@ -560,7 +557,6 @@ export class AutoEncrypter { } // TODO: should this be an accessor from the addon? - // @ts-expect-error - this is not defined in the bindings context.id = this._contextCounter++; const stateMachine = new StateMachine({ diff --git a/src/client-side-encryption/clientEncryption.ts b/src/client-side-encryption/clientEncryption.ts index 163c094f3c5..8797d74aa49 100644 --- a/src/client-side-encryption/clientEncryption.ts +++ b/src/client-side-encryption/clientEncryption.ts @@ -17,7 +17,7 @@ import { collectionNamespace, databaseNamespace, maybeCallback, promiseOrCallbac import * as cryptoCallbacks from './cryptoCallbacks'; import { MongoCryptCreateDataKeyError, MongoCryptCreateEncryptedCollectionError } from './errors'; import { type KMSProvider, type KMSProviders, loadCredentials } from './providers/index'; -import { StateMachine } from './stateMachine'; +import { StateMachine, type StateMachineExecutable } from './stateMachine'; /** * The schema for a DataKey in the key vault collection. @@ -36,7 +36,7 @@ export interface DataKey { /** * The public interface for explicit in-use encryption */ -export class ClientEncryption { +export class ClientEncryption implements StateMachineExecutable { _client: MongoClient; _keyVaultNamespace: string; _keyVaultClient: MongoClient; diff --git a/src/client-side-encryption/stateMachine.js b/src/client-side-encryption/stateMachine.ts similarity index 62% rename from src/client-side-encryption/stateMachine.js rename to src/client-side-encryption/stateMachine.ts index 451b1a27ddb..3f9a6c74f09 100644 --- a/src/client-side-encryption/stateMachine.js +++ b/src/client-side-encryption/stateMachine.ts @@ -1,14 +1,19 @@ import * as fs from 'fs'; +import { type MongoCryptContext, type MongoCryptKMSRequest } from 'mongodb-client-encryption'; import * as net from 'net'; import { SocksClient } from 'socks'; import * as tls from 'tls'; import { promisify } from 'util'; -import { deserialize, serialize } from '../bson'; +import { deserialize, type Document, serialize } from '../bson'; import { MongoNetworkTimeoutError } from '../error'; -import { BufferPool } from '../utils'; +import { type MongoClient } from '../mongo_client'; +import { BufferPool, type Callback } from '../utils'; +import { type ClientEncryptionTlsOptions, type DataKey } from './clientEncryption'; import { collectionNamespace, databaseNamespace, debug } from './common'; import { MongoCryptError } from './errors'; +import { type MongocryptdManager } from './mongocryptdManager'; +import { type KMSProvider, type KMSProviders } from './providers'; // libmongocrypt states const MONGOCRYPT_CTX_ERROR = 0; @@ -41,74 +46,66 @@ const INSECURE_TLS_OPTIONS = [ '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} - */ - -/** - * @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} - */ - -/** - * @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} - */ +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~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 - */ +export interface StateMachineExecutable { + _keyVaultNamespace: string; + _keyVaultClient: MongoClient; + _metaDataClient?: MongoClient; + _mongocryptdClient?: MongoClient; + _mongocryptdManager?: MongocryptdManager; + askForKMSCredentials: () => Promise; +} /** - * @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 */ export class StateMachine { - constructor(options) { - this.options = options || {}; + // TODO: figure out state machine options type + constructor(private options: Document = {}) {} - this.executeAsync = promisify((autoEncrypter, context, callback) => - this.execute(autoEncrypter, context, callback) - ); + executeAsync(executor: StateMachineExecutable, context: MongoCryptContext): Promise { + 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) { @@ -116,7 +113,7 @@ export class StateMachine { } context.finishMongoOperation(); - this.execute(autoEncrypter, context, callback); + this.execute(executor, context, callback); }); return; @@ -124,8 +121,15 @@ export 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 && @@ -135,22 +139,22 @@ export 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; @@ -159,29 +163,29 @@ export 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); + this.execute(executor, context, callback); }) .catch(err => { - callback(err, null); + callback(err); }); return; @@ -198,10 +202,10 @@ export 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; @@ -213,17 +217,22 @@ export 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 - if (context.state === MONGOCRYPT_CTX_ERROR) { + if ((context.state as 0) === 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 { v: Document }); 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; } @@ -238,24 +247,24 @@ export 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 }; + // TODO: type these options + const options: Document = { 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; + let socket: net.Socket; + let rawSocket: net.Socket; function destroySockets() { for (const sock of [socket, rawSocket]) { @@ -271,10 +280,10 @@ export 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; + // TODO: make note of this + const mcError = new MongoCryptError('KMS request failed', { cause: err }); reject(mcError); } @@ -287,7 +296,8 @@ export class StateMachine { rawSocket.on('timeout', ontimeout); rawSocket.on('error', onerror); try { - const { once } = require('events'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { once } = require('events') as typeof import('events'); await once(rawSocket, 'connect'); options.socket = ( await SocksClient.createConnection({ @@ -311,7 +321,7 @@ export 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); @@ -343,15 +353,18 @@ export 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: KMSProvider, + // TODO: should this be `ClientEncryptionTlsOptions`? + tlsOptions: ClientEncryptionTlsOptions + ): MongoCryptError | void { const tlsOptionNames = Object.keys(tlsOptions); for (const option of INSECURE_TLS_OPTIONS) { if (tlsOptionNames.includes(option)) { @@ -361,13 +374,13 @@ export class StateMachine { } /** - * @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) { + // TODO: should this be `ClientEncryptionTlsOptions`? + setTlsOptions(tlsOptions: ClientEncryptionTlsOptions, options: Document) { if (tlsOptions.tlsCertificateKeyFile) { const cert = fs.readFileSync(tlsOptions.tlsCertificateKeyFile); options.cert = options.key = cert; @@ -381,18 +394,22 @@ export 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) { + fetchCollectionInfo( + client: MongoClient, + ns: string, + filter: Document, + callback: Callback + ) { const dbName = databaseNamespace(ns); client @@ -405,25 +422,28 @@ export 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 rawCommand = deserialize(command, options); @@ -433,40 +453,42 @@ export class StateMachine { .command(rawCommand, options) .then( response => { - return callback(null, serialize(response, this.options)); + return callback(undefined, serialize(response, this.options)); }, 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) { + fetchKeys( + client: MongoClient, + keyVaultNamespace: string, + filter: Uint8Array, + callback: Callback> + ) { const dbName = databaseNamespace(keyVaultNamespace); const collectionName = collectionNamespace(keyVaultNamespace); - filter = deserialize(filter); 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); } ); } diff --git a/test/unit/client-side-encryption/stateMachine.test.js b/test/unit/client-side-encryption/stateMachine.test.ts similarity index 87% rename from test/unit/client-side-encryption/stateMachine.test.js rename to test/unit/client-side-encryption/stateMachine.test.ts index 63ab990861c..2f3fbcf28e5 100644 --- a/test/unit/client-side-encryption/stateMachine.test.js +++ b/test/unit/client-side-encryption/stateMachine.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/stateMachine'; +// 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; } @@ -61,6 +70,7 @@ describe('StateMachine', function () { const options = { promoteLongs: false, promoteValues: false }; const serializedCommand = serialize(command); const stateMachine = new StateMachine(); + // 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); @@ -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'); @@ -293,7 +306,7 @@ describe('StateMachine', function () { 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; } @@ -316,7 +329,7 @@ describe('StateMachine', function () { 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; } From ef3207126ec7723f18958891c5af15b162b5fa4c Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 26 Jul 2023 15:03:16 -0600 Subject: [PATCH 21/45] everything TS! woohoo --- .eslintignore | 3 - src/client-side-encryption/autoEncrypter.ts | 9 ++- .../clientEncryption.ts | 58 ++++++++++--------- src/client-side-encryption/common.js | 12 +--- .../mongocryptdManager.ts | 5 +- src/client-side-encryption/providers/azure.ts | 10 +++- src/client-side-encryption/stateMachine.ts | 25 ++++++-- src/utils.ts | 4 ++ .../clientEncryption.test.ts | 1 - .../client-side-encryption/common.test.js | 3 +- .../providers/credentialsProvider.test.ts | 36 ++++++------ 11 files changed, 93 insertions(+), 73 deletions(-) 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/src/client-side-encryption/autoEncrypter.ts b/src/client-side-encryption/autoEncrypter.ts index 9cae6b40df0..6a52926471a 100644 --- a/src/client-side-encryption/autoEncrypter.ts +++ b/src/client-side-encryption/autoEncrypter.ts @@ -270,6 +270,7 @@ export class AutoEncrypter implements StateMachineExecutable { * 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, { @@ -282,10 +283,12 @@ export class AutoEncrypter implements StateMachineExecutable { * } * } * }); + * ``` * * 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, { @@ -299,6 +302,7 @@ export class AutoEncrypter implements StateMachineExecutable { * } * } * }); + * ``` * * await client.connect(); * // From here on, the client will be encrypting / decrypting automatically @@ -423,7 +427,7 @@ export class AutoEncrypter implements StateMachineExecutable { }; if (this._mongocryptdManager.bypassSpawn) { - return this._mongocryptdClient.connect().then( + this._mongocryptdClient.connect().then( result => { return _callback(undefined, result); }, @@ -431,6 +435,7 @@ export class AutoEncrypter implements StateMachineExecutable { _callback(error, undefined); } ); + return; } this._mongocryptdManager.spawn(() => { @@ -571,8 +576,10 @@ export class AutoEncrypter implements StateMachineExecutable { // 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); }); } diff --git a/src/client-side-encryption/clientEncryption.ts b/src/client-side-encryption/clientEncryption.ts index 8797d74aa49..e04b6e258b1 100644 --- a/src/client-side-encryption/clientEncryption.ts +++ b/src/client-side-encryption/clientEncryption.ts @@ -1,5 +1,4 @@ import type { ExplicitEncryptionContextOptions, MongoCrypt } from 'mongodb-client-encryption'; -import { promisify } from 'util'; import { type Binary, type Document, type Long, serialize } from '../bson'; import { type BulkWriteResult } from '../bulk/common'; @@ -198,9 +197,10 @@ export class ClientEncryption implements StateMachineExecutable { 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) { + if (err || !dataKey) { cb(err, null); return; } @@ -210,8 +210,8 @@ export class ClientEncryption implements StateMachineExecutable { this._keyVaultClient .db(dbName) - .collection(collectionName) - .insertOne(dataKey, { writeConcern: { w: 'majority' } }) + .collection(collectionName) + .insertOne(dataKey as DataKey, { writeConcern: { w: 'majority' } }) .then( result => { return cb(null, result.insertedId); @@ -256,9 +256,6 @@ export class ClientEncryption implements StateMachineExecutable { 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); @@ -267,16 +264,14 @@ export class ClientEncryption implements StateMachineExecutable { tlsOptions: this._tlsOptions }); - const execute = promisify(stateMachine.execute.bind(stateMachine)); - - const dataKey = await execute(this, context); + const dataKey = await stateMachine.executeAsync(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 => ({ + const replacements = dataKey.v.map((key: DataKey) => ({ updateOne: { filter: { _id: key._id }, update: { @@ -293,7 +288,7 @@ export class ClientEncryption implements StateMachineExecutable { const result = await this._keyVaultClient .db(dbName) - .collection(collectionName) + .collection(collectionName) .bulkWrite(replacements, { writeConcern: { w: 'majority' } }); @@ -661,9 +656,10 @@ export class ClientEncryption implements StateMachineExecutable { tlsOptions: this._tlsOptions }); + // @ts-expect-error We did not convert promiseOrCallback to TS return promiseOrCallback(callback, cb => { stateMachine.execute(this, context, (err, result) => { - if (err) { + if (err || !result) { cb(err, null); return; } @@ -705,32 +701,38 @@ export class ClientEncryption implements StateMachineExecutable { expressionMode: boolean, options: ClientEncryptionEncryptOptions ): Promise { - const valueBuffer = serialize({ v: value }); - const contextOptions: ExplicitEncryptionContextOptions = Object.assign({}, options, { - expressionMode - }); - if (options.keyId) { - contextOptions.keyId = options.keyId.buffer; + const { algorithm, keyId, keyAltName, contentionFactor, queryType, rangeOptions } = options; + const contextOptions: ExplicitEncryptionContextOptions = { + expressionMode, + algorithm + }; + if (keyId) { + contextOptions.keyId = keyId.buffer; } - if (options.keyAltName) { - const keyAltName = options.keyAltName; - if (options.keyId) { + if (keyAltName) { + if (keyId) { throw new TypeError(`"options" cannot contain both "keyId" and "keyAltName"`); } - const keyAltNameType = typeof keyAltName; - if (keyAltNameType !== 'string') { + if (typeof keyAltName !== 'string') { throw new TypeError( - `"options.keyAltName" must be of type string, but was of type ${keyAltNameType}` + `"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 options.rangeOptions === 'object') { - contextOptions.rangeOptions = serialize(options.rangeOptions); + if (typeof rangeOptions === 'object') { + contextOptions.rangeOptions = serialize(rangeOptions); } + const valueBuffer = serialize({ v: value }); const stateMachine = new StateMachine({ proxyOptions: this._proxyOptions, tlsOptions: this._tlsOptions @@ -738,7 +740,7 @@ export class ClientEncryption implements StateMachineExecutable { const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions); const result = await stateMachine.executeAsync(this, context); - return result.v; + return result.v as Binary; } } diff --git a/src/client-side-encryption/common.js b/src/client-side-encryption/common.js index 0685530727f..25d703a9d0f 100644 --- a/src/client-side-encryption/common.js +++ b/src/client-side-encryption/common.js @@ -1,14 +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); - } -} +/* eslint-disable */ /** * @ignore diff --git a/src/client-side-encryption/mongocryptdManager.ts b/src/client-side-encryption/mongocryptdManager.ts index 0694b87112a..26f90f6fec9 100644 --- a/src/client-side-encryption/mongocryptdManager.ts +++ b/src/client-side-encryption/mongocryptdManager.ts @@ -45,6 +45,7 @@ export class MongocryptdManager { 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 detatched:true @@ -54,7 +55,9 @@ export class MongocryptdManager { detached: true }); - this._child.on('error', () => {}); + this._child.on('error', () => { + // perhaps questionable, but we swallow mongocryptd spawn errors. + }); // unref child to remove handle from event loop this._child.unref(); 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/stateMachine.ts b/src/client-side-encryption/stateMachine.ts index 3f9a6c74f09..425146ea5ec 100644 --- a/src/client-side-encryption/stateMachine.ts +++ b/src/client-side-encryption/stateMachine.ts @@ -10,7 +10,7 @@ import { MongoNetworkTimeoutError } from '../error'; import { type MongoClient } from '../mongo_client'; import { BufferPool, type Callback } from '../utils'; import { type ClientEncryptionTlsOptions, type DataKey } from './clientEncryption'; -import { collectionNamespace, databaseNamespace, debug } from './common'; +import { collectionNamespace, databaseNamespace } from './common'; import { MongoCryptError } from './errors'; import { type MongocryptdManager } from './mongocryptdManager'; import { type KMSProvider, type KMSProviders } from './providers'; @@ -46,6 +46,17 @@ const INSECURE_TLS_OPTIONS = [ 'tlsDisableCertificateRevocationCheck' ]; +/** + * 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: unknown) { + if (process.env.MONGODB_CRYPT_DEBUG) { + // eslint-disable-next-line no-console + console.error(msg); + } +} + 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. @@ -74,7 +85,8 @@ export class StateMachine { // TODO: figure out state machine options type constructor(private options: Document = {}) {} - executeAsync(executor: StateMachineExecutable, context: MongoCryptContext): Promise { + 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); } @@ -84,7 +96,7 @@ export class StateMachine { execute( executor: StateMachineExecutable, context: MongoCryptContext, - callback: Callback + callback: Callback ) { const keyVaultNamespace = executor._keyVaultNamespace; const keyVaultClient = executor._keyVaultClient; @@ -222,7 +234,7 @@ export class StateMachine { callback(new MongoCryptError(message)); return; } - callback(undefined, deserialize(finalizedContext, this.options) as { v: Document }); + callback(undefined, deserialize(finalizedContext, this.options)); return; } case MONGOCRYPT_CTX_ERROR: { @@ -263,6 +275,7 @@ export class StateMachine { return new Promise(async (resolve, reject) => { const buffer = new BufferPool(); + /* eslint-disable prefer-const */ let socket: net.Socket; let rawSocket: net.Socket; @@ -297,8 +310,8 @@ export class StateMachine { rawSocket.on('error', onerror); try { // eslint-disable-next-line @typescript-eslint/no-var-requires - const { once } = require('events') as typeof import('events'); - await once(rawSocket, 'connect'); + const events = require('events') as typeof import('events'); + await events.once(rawSocket, 'connect'); options.socket = ( await SocksClient.createConnection({ existing_socket: rawSocket, diff --git a/src/utils.ts b/src/utils.ts index aa9afc51a22..015637eada1 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 | undefined): MongoDBCollectionNamespace { + return super.fromString(namespace) as MongoDBCollectionNamespace; + } } /** @internal */ diff --git a/test/unit/client-side-encryption/clientEncryption.test.ts b/test/unit/client-side-encryption/clientEncryption.test.ts index b5c22dc789e..3e84479147d 100644 --- a/test/unit/client-side-encryption/clientEncryption.test.ts +++ b/test/unit/client-side-encryption/clientEncryption.test.ts @@ -96,7 +96,6 @@ describe('ClientEncryption', function () { }); describe('createEncryptedCollection()', () => { - /** @type {InstanceType} */ let clientEncryption; const client = new MockClient(); let db; 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/providers/credentialsProvider.test.ts b/test/unit/client-side-encryption/providers/credentialsProvider.test.ts index f8f735033bb..2f2115b6e09 100644 --- a/test/unit/client-side-encryption/providers/credentialsProvider.test.ts +++ b/test/unit/client-side-encryption/providers/credentialsProvider.test.ts @@ -77,9 +77,9 @@ 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; } }); @@ -106,9 +106,9 @@ 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; } }); @@ -139,9 +139,9 @@ 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; } }); @@ -163,9 +163,9 @@ 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; } }); @@ -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; } }); @@ -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; } }); From e6806f52db9d0b84b5cfc02433cabd2ba7c7b5a3 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 26 Jul 2023 15:08:11 -0600 Subject: [PATCH 22/45] remove namespace helpers --- src/client-side-encryption/autoEncrypter.ts | 8 +-- .../clientEncryption.ts | 51 ++++++++++++------- src/client-side-encryption/common.js | 19 ------- src/client-side-encryption/stateMachine.ts | 15 +++--- 4 files changed, 45 insertions(+), 48 deletions(-) diff --git a/src/client-side-encryption/autoEncrypter.ts b/src/client-side-encryption/autoEncrypter.ts index 6a52926471a..f8deb64a7dc 100644 --- a/src/client-side-encryption/autoEncrypter.ts +++ b/src/client-side-encryption/autoEncrypter.ts @@ -3,8 +3,7 @@ import { type ProxyOptions } from '../cmap/connection'; import { getMongoDBClientEncryption } from '../deps'; import { type AnyError, MongoError, MongoRuntimeError } from '../error'; import { MongoClient, type MongoClientOptions } from '../mongo_client'; -import { type Callback } from '../utils'; -import { databaseNamespace } from './common'; +import { type Callback, MongoDBCollectionNamespace } from '../utils'; import * as cryptoCallbacks from './cryptoCallbacks'; import { MongocryptdManager } from './mongocryptdManager'; import { type KMSProviders, loadCredentials } from './providers'; @@ -512,7 +511,10 @@ export class AutoEncrypter implements StateMachineExecutable { let context; try { - context = this._mongocrypt.makeEncryptionContext(databaseNamespace(ns), commandBuffer); + context = this._mongocrypt.makeEncryptionContext( + MongoDBCollectionNamespace.fromString(ns).db, + commandBuffer + ); } catch (err) { callback(err, undefined); return; diff --git a/src/client-side-encryption/clientEncryption.ts b/src/client-side-encryption/clientEncryption.ts index e04b6e258b1..77db458cc3a 100644 --- a/src/client-side-encryption/clientEncryption.ts +++ b/src/client-side-encryption/clientEncryption.ts @@ -11,8 +11,8 @@ 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 } from '../utils'; -import { collectionNamespace, databaseNamespace, maybeCallback, promiseOrCallback } from './common'; +import { type Callback, MongoDBCollectionNamespace } from '../utils'; +import { maybeCallback, promiseOrCallback } from './common'; import * as cryptoCallbacks from './cryptoCallbacks'; import { MongoCryptCreateDataKeyError, MongoCryptCreateEncryptedCollectionError } from './errors'; import { type KMSProvider, type KMSProviders, loadCredentials } from './providers/index'; @@ -205,8 +205,9 @@ export class ClientEncryption implements StateMachineExecutable { return; } - const dbName = databaseNamespace(this._keyVaultNamespace); - const collectionName = collectionNamespace(this._keyVaultNamespace); + const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString( + this._keyVaultNamespace + ); this._keyVaultClient .db(dbName) @@ -269,8 +270,10 @@ export class ClientEncryption implements StateMachineExecutable { return {}; } - const dbName = databaseNamespace(this._keyVaultNamespace); - const collectionName = collectionNamespace(this._keyVaultNamespace); + const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString( + this._keyVaultNamespace + ); + const replacements = dataKey.v.map((key: DataKey) => ({ updateOne: { filter: { _id: key._id }, @@ -312,8 +315,10 @@ export class ClientEncryption implements StateMachineExecutable { * */ async deleteKey(_id: Binary): Promise { - const dbName = databaseNamespace(this._keyVaultNamespace); - const collectionName = collectionNamespace(this._keyVaultNamespace); + const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString( + this._keyVaultNamespace + ); + return this._keyVaultClient .db(dbName) .collection(collectionName) @@ -333,8 +338,10 @@ export class ClientEncryption implements StateMachineExecutable { * ``` */ getKeys(): FindCursor { - const dbName = databaseNamespace(this._keyVaultNamespace); - const collectionName = collectionNamespace(this._keyVaultNamespace); + const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString( + this._keyVaultNamespace + ); + return this._keyVaultClient .db(dbName) .collection(collectionName) @@ -357,8 +364,10 @@ export class ClientEncryption implements StateMachineExecutable { * ``` */ async getKey(_id: Binary): Promise { - const dbName = databaseNamespace(this._keyVaultNamespace); - const collectionName = collectionNamespace(this._keyVaultNamespace); + const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString( + this._keyVaultNamespace + ); + return this._keyVaultClient .db(dbName) .collection(collectionName) @@ -382,8 +391,10 @@ export class ClientEncryption implements StateMachineExecutable { * ``` */ async getKeyByAltName(keyAltName: string) { - const dbName = databaseNamespace(this._keyVaultNamespace); - const collectionName = collectionNamespace(this._keyVaultNamespace); + const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString( + this._keyVaultNamespace + ); + return this._keyVaultClient .db(dbName) .collection(collectionName) @@ -411,8 +422,10 @@ export class ClientEncryption implements StateMachineExecutable { * ``` */ async addKeyAltName(_id: Binary, keyAltName: string) { - const dbName = databaseNamespace(this._keyVaultNamespace); - const collectionName = collectionNamespace(this._keyVaultNamespace); + const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString( + this._keyVaultNamespace + ); + const { value } = await this._keyVaultClient .db(dbName) .collection(collectionName) @@ -449,8 +462,10 @@ export class ClientEncryption implements StateMachineExecutable { * ``` */ async removeKeyAltName(_id: Binary, keyAltName: string) { - const dbName = databaseNamespace(this._keyVaultNamespace); - const collectionName = collectionNamespace(this._keyVaultNamespace); + const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString( + this._keyVaultNamespace + ); + const pipeline = [ { $set: { diff --git a/src/client-side-encryption/common.js b/src/client-side-encryption/common.js index 25d703a9d0f..1d7dd2fe328 100644 --- a/src/client-side-encryption/common.js +++ b/src/client-side-encryption/common.js @@ -1,24 +1,5 @@ /* eslint-disable */ -/** - * @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('.'); -} - export function maybeCallback(promiseFn, callback) { const promise = promiseFn(); if (callback == null) { diff --git a/src/client-side-encryption/stateMachine.ts b/src/client-side-encryption/stateMachine.ts index 425146ea5ec..36a968a246b 100644 --- a/src/client-side-encryption/stateMachine.ts +++ b/src/client-side-encryption/stateMachine.ts @@ -8,9 +8,8 @@ import { promisify } from 'util'; import { deserialize, type Document, serialize } from '../bson'; import { MongoNetworkTimeoutError } from '../error'; import { type MongoClient } from '../mongo_client'; -import { BufferPool, type Callback } from '../utils'; +import { BufferPool, type Callback, MongoDBCollectionNamespace } from '../utils'; import { type ClientEncryptionTlsOptions, type DataKey } from './clientEncryption'; -import { collectionNamespace, databaseNamespace } from './common'; import { MongoCryptError } from './errors'; import { type MongocryptdManager } from './mongocryptdManager'; import { type KMSProvider, type KMSProviders } from './providers'; @@ -423,10 +422,10 @@ export class StateMachine { filter: Document, callback: Callback ) { - const dbName = databaseNamespace(ns); + const { db } = MongoDBCollectionNamespace.fromString(ns); client - .db(dbName) + .db(db) .listCollections(filter, { promoteLongs: false, promoteValues: false @@ -458,11 +457,11 @@ export class StateMachine { 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 => { @@ -488,8 +487,8 @@ export class StateMachine { filter: Uint8Array, callback: Callback> ) { - const dbName = databaseNamespace(keyVaultNamespace); - const collectionName = collectionNamespace(keyVaultNamespace); + const { db: dbName, collection: collectionName } = + MongoDBCollectionNamespace.fromString(keyVaultNamespace); client .db(dbName) From 9c9353fab52f8ac05f6cbcaf885e1d1b64d66c5b Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 27 Jul 2023 11:53:42 -0600 Subject: [PATCH 23/45] add remaining types woohoo --- src/client-side-encryption/autoEncrypter.ts | 62 ++++++----------- .../clientEncryption.ts | 56 ++++----------- src/client-side-encryption/cryptoCallbacks.ts | 31 +++------ src/client-side-encryption/stateMachine.ts | 69 +++++++++++++++---- src/cmap/connection.ts | 3 +- src/index.ts | 5 +- .../client_side_encryption.prose.test.js | 24 +++---- 7 files changed, 111 insertions(+), 139 deletions(-) diff --git a/src/client-side-encryption/autoEncrypter.ts b/src/client-side-encryption/autoEncrypter.ts index f8deb64a7dc..3b684d82af2 100644 --- a/src/client-side-encryption/autoEncrypter.ts +++ b/src/client-side-encryption/autoEncrypter.ts @@ -1,5 +1,7 @@ +import { type MongoCryptOptions } from 'mongodb-client-encryption'; + import { deserialize, type Document, serialize } from '../bson'; -import { type ProxyOptions } from '../cmap/connection'; +import { type CommandOptions, type ProxyOptions } from '../cmap/connection'; import { getMongoDBClientEncryption } from '../deps'; import { type AnyError, MongoError, MongoRuntimeError } from '../error'; import { MongoClient, type MongoClientOptions } from '../mongo_client'; @@ -7,27 +9,7 @@ import { type Callback, MongoDBCollectionNamespace } from '../utils'; import * as cryptoCallbacks from './cryptoCallbacks'; import { MongocryptdManager } from './mongocryptdManager'; import { type KMSProviders, loadCredentials } from './providers'; -import { StateMachine, type StateMachineExecutable } from './stateMachine'; - -/** @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; -} +import { type CSFLEKMSTlsOptions, StateMachine, type StateMachineExecutable } from './stateMachine'; /** @public */ export interface AutoEncryptionOptions { @@ -190,13 +172,7 @@ export interface AutoEncryptionOptions { }; proxyOptions?: ProxyOptions; /** The TLS options to use connecting to the KMS provider */ - tlsOptions?: { - aws?: AutoEncryptionTlsOptions; - local?: AutoEncryptionTlsOptions; - azure?: AutoEncryptionTlsOptions; - gcp?: AutoEncryptionTlsOptions; - kmip?: AutoEncryptionTlsOptions; - }; + tlsOptions?: CSFLEKMSTlsOptions; } /** @@ -240,7 +216,7 @@ export class AutoEncrypter implements StateMachineExecutable { _keyVaultClient: MongoClient; _metaDataClient: MongoClient; _proxyOptions: ProxyOptions; - _tlsOptions: Record; + _tlsOptions: CSFLEKMSTlsOptions; _kmsProviders: KMSProviders; _bypassMongocryptdAndCryptShared: boolean; _contextCounter: number; @@ -317,22 +293,21 @@ export class AutoEncrypter implements StateMachineExecutable { this._tlsOptions = options.tlsOptions || {}; this._kmsProviders = options.kmsProviders || {}; - // TODO: Add proper type support here, once the bindings are finished. - const mongoCryptOptions: Document = {}; + const mongoCryptOptions: MongoCryptOptions = {}; if (options.schemaMap) { mongoCryptOptions.schemaMap = Buffer.isBuffer(options.schemaMap) ? options.schemaMap - : serialize(options.schemaMap); + : (serialize(options.schemaMap) as Buffer); } if (options.encryptedFieldsMap) { mongoCryptOptions.encryptedFieldsMap = Buffer.isBuffer(options.encryptedFieldsMap) ? options.encryptedFieldsMap - : serialize(options.encryptedFieldsMap); + : (serialize(options.encryptedFieldsMap) as Buffer); } mongoCryptOptions.kmsProviders = !Buffer.isBuffer(this._kmsProviders) - ? serialize(this._kmsProviders) + ? (serialize(this._kmsProviders) as Buffer) : this._kmsProviders; if (options.options?.logger) { @@ -475,13 +450,19 @@ export class AutoEncrypter implements StateMachineExecutable { } 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?: Document | Callback, + options?: CommandOptions | Callback, callback?: Callback ) { if (typeof ns !== 'string') { @@ -492,8 +473,7 @@ export class AutoEncrypter implements StateMachineExecutable { throw new TypeError('Parameter `cmd` must be an object'); } - callback = - typeof options === 'function' ? (options as Callback) : callback; + callback = typeof options === 'function' ? options : callback; if (callback == null) { throw new TypeError('Callback must be provided'); @@ -537,15 +517,13 @@ export class AutoEncrypter implements StateMachineExecutable { /** * Decrypt a command response - * - * TODO: type options */ decrypt( response: Uint8Array, - options: Document | Callback, + options: CommandOptions | Callback, callback?: Callback ) { - callback = typeof options === 'function' ? (options as Callback) : callback; + callback = typeof options === 'function' ? options : callback; if (callback == null) { throw new TypeError('Callback must be provided'); diff --git a/src/client-side-encryption/clientEncryption.ts b/src/client-side-encryption/clientEncryption.ts index 77db458cc3a..395041823fc 100644 --- a/src/client-side-encryption/clientEncryption.ts +++ b/src/client-side-encryption/clientEncryption.ts @@ -1,4 +1,8 @@ -import type { ExplicitEncryptionContextOptions, MongoCrypt } from 'mongodb-client-encryption'; +import type { + ExplicitEncryptionContextOptions, + MongoCrypt, + MongoCryptOptions +} from 'mongodb-client-encryption'; import { type Binary, type Document, type Long, serialize } from '../bson'; import { type BulkWriteResult } from '../bulk/common'; @@ -16,7 +20,7 @@ import { maybeCallback, promiseOrCallback } from './common'; import * as cryptoCallbacks from './cryptoCallbacks'; import { MongoCryptCreateDataKeyError, MongoCryptCreateEncryptedCollectionError } from './errors'; import { type KMSProvider, type KMSProviders, loadCredentials } from './providers/index'; -import { StateMachine, type StateMachineExecutable } from './stateMachine'; +import { type CSFLEKMSTlsOptions, StateMachine, type StateMachineExecutable } from './stateMachine'; /** * The schema for a DataKey in the key vault collection. @@ -40,7 +44,7 @@ export class ClientEncryption implements StateMachineExecutable { _keyVaultNamespace: string; _keyVaultClient: MongoClient; _proxyOptions: ProxyOptions; - _tlsOptions: Record; + _tlsOptions: CSFLEKMSTlsOptions; _kmsProviders: KMSProviders; _mongoCrypt: MongoCrypt; @@ -91,12 +95,13 @@ export class ClientEncryption implements StateMachineExecutable { throw new TypeError('Missing required option `keyVaultNamespace`'); } - // TODO: type this - const mongoCryptOptions: Document = { ...options, cryptoCallbacks }; - - mongoCryptOptions.kmsProviders = !Buffer.isBuffer(this._kmsProviders) - ? serialize(this._kmsProviders) - : this._kmsProviders; + 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; @@ -807,37 +812,6 @@ export interface RewrapManyDataKeyOptions { | undefined; } -/** - * TLS options to use when connecting. The spec specifically calls out which insecure - * tls options are not allowed: - * - * - tlsAllowInvalidCertificates - * - tlsAllowInvalidHostnames - * - tlsInsecure - * - tlsDisableOCSPEndpointCheck - * - tlsDisableCertificateRevocationCheck - */ -export interface ClientEncryptionTlsOptions { - /** - * Specifies the location of a local .pem file that contains - * either the client's TLS/SSL certificate and key or only the - * client's TLS/SSL key when tlsCertificateFile is used to - * provide the certificate. - */ - 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; -} - /** * Additional settings to provide when creating a new `ClientEncryption` instance. */ @@ -873,7 +847,7 @@ export interface ClientEncryptionOptions { /** * TLS options for kms providers to use. */ - tlsOptions?: { [kms in keyof KMSProviders]?: ClientEncryptionTlsOptions }; + tlsOptions?: CSFLEKMSTlsOptions; } /** diff --git a/src/client-side-encryption/cryptoCallbacks.ts b/src/client-side-encryption/cryptoCallbacks.ts index d3c04f28c2a..1e2f1f7f070 100644 --- a/src/client-side-encryption/cryptoCallbacks.ts +++ b/src/client-side-encryption/cryptoCallbacks.ts @@ -1,22 +1,12 @@ import * as crypto from 'crypto'; -type AES256Callback = ( - key: Uint8Array, - iv: Uint8Array, - input: Uint8Array, - output: Uint8Array -) => number | Error; +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: Uint8Array, - iv: Uint8Array, - input: Uint8Array, - output: Uint8Array - ): number | Error { + return function (key: Buffer, iv: Buffer, input: Buffer, output: Buffer): number | Error { let result; try { @@ -36,7 +26,7 @@ export function makeAES256Hook( }; } -export function randomHook(buffer: Uint8Array, count: number): number | Error { +export function randomHook(buffer: Buffer, count: number): number | Error { try { crypto.randomFillSync(buffer, 0, count); } catch (e) { @@ -45,7 +35,7 @@ export function randomHook(buffer: Uint8Array, count: number): number | Error { return count; } -export function sha256Hook(input: Uint8Array, output: Uint8Array): number | Error { +export function sha256Hook(input: Buffer, output: Buffer): number | Error { let result; try { result = crypto.createHash('sha256').update(input).digest(); @@ -57,9 +47,9 @@ export function sha256Hook(input: Uint8Array, output: Uint8Array): number | Erro return result.length; } -type HMACHook = (key: Uint8Array, input: Uint8Array, output: Uint8Array) => number | Error; +type HMACHook = (key: Buffer, input: Buffer, output: Buffer) => number | Error; export function makeHmacHook(algorithm: 'sha512' | 'sha256'): HMACHook { - return (key: Uint8Array, input: Uint8Array, output: Uint8Array): number | Error => { + return (key: Buffer, input: Buffer, output: Buffer): number | Error => { let result; try { result = crypto.createHmac(algorithm, key).update(input).digest(); @@ -72,17 +62,12 @@ export function makeHmacHook(algorithm: 'sha512' | 'sha256'): HMACHook { }; } -export function signRsaSha256Hook( - key: Uint8Array, - input: Uint8Array, - output: Uint8Array -): number | Error { +export function signRsaSha256Hook(key: Buffer, input: Buffer, output: Buffer): number | Error { let result; try { const signer = crypto.createSign('sha256WithRSAEncryption'); const privateKey = Buffer.from( - // TODO: check this - `-----BEGIN PRIVATE KEY-----\n${key.toString()}\n-----END PRIVATE KEY-----\n` + `-----BEGIN PRIVATE KEY-----\n${key.toString('base64')}\n-----END PRIVATE KEY-----\n` ); result = signer.update(input).end().sign(privateKey); diff --git a/src/client-side-encryption/stateMachine.ts b/src/client-side-encryption/stateMachine.ts index 36a968a246b..fe08c558a01 100644 --- a/src/client-side-encryption/stateMachine.ts +++ b/src/client-side-encryption/stateMachine.ts @@ -5,11 +5,18 @@ import { SocksClient } from 'socks'; import * as tls from 'tls'; import { promisify } from 'util'; -import { deserialize, type Document, serialize } from '../bson'; +import { + type BSONSerializeOptions, + deserialize, + type Document, + pluckBSONSerializeOptions, + serialize +} from '../bson'; +import { type CommandOptions, type ProxyOptions } from '../cmap/connection'; import { MongoNetworkTimeoutError } from '../error'; -import { type MongoClient } from '../mongo_client'; +import { type MongoClient, type MongoClientOptions } from '../mongo_client'; import { BufferPool, type Callback, MongoDBCollectionNamespace } from '../utils'; -import { type ClientEncryptionTlsOptions, type DataKey } from './clientEncryption'; +import { type DataKey } from './clientEncryption'; import { MongoCryptError } from './errors'; import { type MongocryptdManager } from './mongocryptdManager'; import { type KMSProvider, type KMSProviders } from './providers'; @@ -66,6 +73,29 @@ declare module 'mongodb-client-encryption' { } } +/** + * TLS options to use when connecting. The spec specifically calls out which insecure + * tls options are not allowed: + * + * - tlsAllowInvalidCertificates + * - tlsAllowInvalidHostnames + * - tlsInsecure + * - tlsDisableOCSPEndpointCheck + * - tlsDisableCertificateRevocationCheck + */ +export type CSFLETlsOptions = Pick< + MongoClientOptions, + 'tlsCAFile' | 'tlsCertificateKeyFile' | 'tlsCertificateKeyFilePassword' +>; + +export type CSFLEKMSTlsOptions = { + aws?: CSFLETlsOptions; + gcp?: CSFLETlsOptions; + kmip?: CSFLETlsOptions; + local?: CSFLETlsOptions; + azure?: CSFLETlsOptions; +}; + export interface StateMachineExecutable { _keyVaultNamespace: string; _keyVaultClient: MongoClient; @@ -75,14 +105,25 @@ export interface StateMachineExecutable { askForKMSCredentials: () => Promise; } +export type StateMachineOptions = { + /** socks5 proxy options, if set. */ + proxyOptions: ProxyOptions; + + /** TLS options for KMS requests, if set. */ + tlsOptions: CSFLEKMSTlsOptions; +} & Pick & + CommandOptions; + /** * @internal * An internal class that executes across a MongoCryptContext until either * a finishing state or an error is reached. Do not instantiate directly. */ export class StateMachine { - // TODO: figure out state machine options type - constructor(private options: Document = {}) {} + 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. @@ -265,8 +306,11 @@ export class StateMachine { kmsRequest(request: MongoCryptKMSRequest): Promise { const parsedUrl = request.endpoint.split(':'); const port = parsedUrl[1] != null ? Number.parseInt(parsedUrl[1], 10) : HTTPS_PORT; - // TODO: type these options - const options: Document = { 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 @@ -372,11 +416,7 @@ export class StateMachine { * * @returns An error if any option is invalid. */ - validateTlsOptions( - kmsProvider: KMSProvider, - // TODO: should this be `ClientEncryptionTlsOptions`? - tlsOptions: ClientEncryptionTlsOptions - ): MongoCryptError | void { + validateTlsOptions(kmsProvider: string, tlsOptions: CSFLETlsOptions): MongoCryptError | void { const tlsOptionNames = Object.keys(tlsOptions); for (const option of INSECURE_TLS_OPTIONS) { if (tlsOptionNames.includes(option)) { @@ -391,8 +431,7 @@ export class StateMachine { * @param tlsOptions - The client TLS options for the provider. * @param options - The existing connection options. */ - // TODO: should this be `ClientEncryptionTlsOptions`? - setTlsOptions(tlsOptions: ClientEncryptionTlsOptions, options: Document) { + setTlsOptions(tlsOptions: CSFLETlsOptions, options: tls.ConnectionOptions) { if (tlsOptions.tlsCertificateKeyFile) { const cert = fs.readFileSync(tlsOptions.tlsCertificateKeyFile); options.cert = options.key = cert; @@ -465,7 +504,7 @@ export class StateMachine { .command(rawCommand, options) .then( response => { - return callback(undefined, serialize(response, this.options)); + return callback(undefined, serialize(response, this.bsonOptions)); }, err => { callback(err); diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index e87006f075a..7fca67caed5 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -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/index.ts b/src/index.ts index 55bdc581d4f..78057ddcf03 100644 --- a/src/index.ts +++ b/src/index.ts @@ -202,10 +202,7 @@ export type { UpdateDescription } from './change_stream'; export type { AutoEncrypter } from './client-side-encryption/autoEncrypter'; -export type { - AutoEncryptionOptions, - AutoEncryptionTlsOptions -} from './client-side-encryption/autoEncrypter'; +export type { AutoEncryptionOptions } from './client-side-encryption/autoEncrypter'; export type { MongocryptdManager } from './client-side-encryption/mongocryptdManager'; export type { AuthContext } from './cmap/auth/auth_provider'; export type { 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..c15a3145705 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 @@ -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'); } }); }); From 1a4b75f446bb49c53c182234da40435da5cda02f Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 27 Jul 2023 14:37:37 -0600 Subject: [PATCH 24/45] misc fixups --- src/client-side-encryption/autoEncrypter.ts | 5 +++-- src/client-side-encryption/stateMachine.ts | 18 +++++++++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/client-side-encryption/autoEncrypter.ts b/src/client-side-encryption/autoEncrypter.ts index 3b684d82af2..0aa06edb71f 100644 --- a/src/client-side-encryption/autoEncrypter.ts +++ b/src/client-side-encryption/autoEncrypter.ts @@ -293,7 +293,9 @@ export class AutoEncrypter implements StateMachineExecutable { this._tlsOptions = options.tlsOptions || {}; this._kmsProviders = options.kmsProviders || {}; - const mongoCryptOptions: MongoCryptOptions = {}; + const mongoCryptOptions: MongoCryptOptions = { + cryptoCallbacks + }; if (options.schemaMap) { mongoCryptOptions.schemaMap = Buffer.isBuffer(options.schemaMap) ? options.schemaMap @@ -331,7 +333,6 @@ export class AutoEncrypter implements StateMachineExecutable { mongoCryptOptions.cryptSharedLibSearchPaths = ['$SYSTEM']; } - Object.assign(mongoCryptOptions, { cryptoCallbacks }); const MongoCrypt = AutoEncrypter.getMongoCrypt(); this._mongocrypt = new MongoCrypt(mongoCryptOptions); this._contextCounter = 0; diff --git a/src/client-side-encryption/stateMachine.ts b/src/client-side-encryption/stateMachine.ts index fe08c558a01..cef7d10d3eb 100644 --- a/src/client-side-encryption/stateMachine.ts +++ b/src/client-side-encryption/stateMachine.ts @@ -96,13 +96,22 @@ export type CSFLEKMSTlsOptions = { azure?: CSFLETlsOptions; }; +/** + * 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; + askForKMSCredentials: () => Promise; } export type StateMachineOptions = { @@ -231,9 +240,7 @@ export class StateMachine { executor .askForKMSCredentials() .then(kmsProviders => { - context.provideKMSProviders( - !Buffer.isBuffer(kmsProviders) ? serialize(kmsProviders) : kmsProviders - ); + context.provideKMSProviders(serialize(kmsProviders)); this.execute(executor, context, callback); }) .catch(err => { @@ -269,7 +276,8 @@ export 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 - if ((context.state as 0) === MONGOCRYPT_CTX_ERROR) { + // @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; From 78e8ae85c29e6f45c7a18f9e12e585ae8943b124 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 28 Jul 2023 09:37:33 -0600 Subject: [PATCH 25/45] fix unit tests --- .../client-side-encryption/stateMachine.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/unit/client-side-encryption/stateMachine.test.ts b/test/unit/client-side-encryption/stateMachine.test.ts index 2f3fbcf28e5..a5e02a8bda3 100644 --- a/test/unit/client-side-encryption/stateMachine.test.ts +++ b/test/unit/client-side-encryption/stateMachine.test.ts @@ -69,7 +69,7 @@ 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 = () => {}; @@ -110,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 @@ -154,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) { @@ -171,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; @@ -198,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; @@ -224,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; @@ -299,7 +299,7 @@ describe('StateMachine', function () { proxyHost: 'localhost', proxyPort: socks5srv.address().port } - }); + } as any); const request = new MockRequest(Buffer.from('foobar'), 500); try { @@ -322,7 +322,7 @@ describe('StateMachine', function () { proxyUsername: 'foo', proxyPassword: 'bar' } - }); + } as any); const request = new MockRequest(Buffer.from('foobar'), 500); try { From ac1c44b6220748c48cbaac473519fd2c201dce3a Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 28 Jul 2023 09:59:48 -0600 Subject: [PATCH 26/45] chore: bump fle dependency version --- package-lock.json | 10 +++++----- package.json | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index e601bf5af99..207e3086665 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "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", @@ -6593,9 +6593,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": { @@ -6604,7 +6604,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 12003ba28a2..1c5766492dc 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,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" }, "peerDependenciesMeta": { @@ -94,7 +94,7 @@ "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", From f25c0f6f5344923e1c8dadc9969b8eb2de74365e Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 28 Jul 2023 13:52:07 -0600 Subject: [PATCH 27/45] lint --- src/client-side-encryption/autoEncrypter.ts | 2 ++ src/client-side-encryption/stateMachine.ts | 5 +++++ src/index.ts | 8 ++++++++ 3 files changed, 15 insertions(+) diff --git a/src/client-side-encryption/autoEncrypter.ts b/src/client-side-encryption/autoEncrypter.ts index 0aa06edb71f..027d938ee21 100644 --- a/src/client-side-encryption/autoEncrypter.ts +++ b/src/client-side-encryption/autoEncrypter.ts @@ -176,6 +176,8 @@ export interface AutoEncryptionOptions { } /** + * @public + * * Extra options related to the mongocryptd process * \* _Available in MongoDB 6.0 or higher._ */ diff --git a/src/client-side-encryption/stateMachine.ts b/src/client-side-encryption/stateMachine.ts index cef7d10d3eb..25454ec7f93 100644 --- a/src/client-side-encryption/stateMachine.ts +++ b/src/client-side-encryption/stateMachine.ts @@ -74,6 +74,8 @@ declare module 'mongodb-client-encryption' { } /** + * @public + * * TLS options to use when connecting. The spec specifically calls out which insecure * tls options are not allowed: * @@ -88,6 +90,7 @@ export type CSFLETlsOptions = Pick< 'tlsCAFile' | 'tlsCertificateKeyFile' | 'tlsCertificateKeyFilePassword' >; +/** @public */ export type CSFLEKMSTlsOptions = { aws?: CSFLETlsOptions; gcp?: CSFLETlsOptions; @@ -97,6 +100,8 @@ export type CSFLEKMSTlsOptions = { }; /** + * @internal + * * An interface representing an object that can be passed to the `StateMachine.execute` method. * * Not all properties are required for all operations. diff --git a/src/index.ts b/src/index.ts index 78057ddcf03..2261c474798 100644 --- a/src/index.ts +++ b/src/index.ts @@ -93,6 +93,14 @@ export { UnorderedBulkOperation }; +export type { AutoEncryptionExtraOptions } from './client-side-encryption/autoEncrypter'; +export type { KMSProviders } from './client-side-encryption/providers/index'; +export type { + CSFLEKMSTlsOptions, + CSFLETlsOptions, + StateMachineExecutable +} from './client-side-encryption/stateMachine'; + // enums export { BatchType } from './bulk/common'; export { AutoEncryptionLoggerLevel } from './client-side-encryption/autoEncrypter'; From 9e3e0165990e412c7a9d5f10e280d08cba31dd8d Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 28 Jul 2023 14:53:32 -0600 Subject: [PATCH 28/45] Type StateMachine.execute properly --- src/client-side-encryption/autoEncrypter.ts | 2 +- .../clientEncryption.ts | 42 ++++++++++--------- src/client-side-encryption/stateMachine.ts | 8 ++-- .../autoEncrypter.test.ts | 25 +---------- 4 files changed, 28 insertions(+), 49 deletions(-) diff --git a/src/client-side-encryption/autoEncrypter.ts b/src/client-side-encryption/autoEncrypter.ts index 027d938ee21..b515dc58ebc 100644 --- a/src/client-side-encryption/autoEncrypter.ts +++ b/src/client-side-encryption/autoEncrypter.ts @@ -515,7 +515,7 @@ export class AutoEncrypter implements StateMachineExecutable { proxyOptions: this._proxyOptions, tlsOptions: this._tlsOptions }); - stateMachine.execute(this, context, callback); + stateMachine.execute(this, context, callback); } /** diff --git a/src/client-side-encryption/clientEncryption.ts b/src/client-side-encryption/clientEncryption.ts index 395041823fc..2ece16b26ce 100644 --- a/src/client-side-encryption/clientEncryption.ts +++ b/src/client-side-encryption/clientEncryption.ts @@ -5,7 +5,7 @@ import type { } from 'mongodb-client-encryption'; import { type Binary, type Document, type Long, serialize } from '../bson'; -import { type BulkWriteResult } from '../bulk/common'; +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'; @@ -204,7 +204,7 @@ export class ClientEncryption implements StateMachineExecutable { // @ts-expect-error We did not convert promiseOrCallback to TS return promiseOrCallback(callback, cb => { - stateMachine.execute(this, context, (err, dataKey) => { + stateMachine.execute(this, context, (err, dataKey) => { if (err || !dataKey) { cb(err, null); return; @@ -217,7 +217,7 @@ export class ClientEncryption implements StateMachineExecutable { this._keyVaultClient .db(dbName) .collection(collectionName) - .insertOne(dataKey as DataKey, { writeConcern: { w: 'majority' } }) + .insertOne(dataKey, { writeConcern: { w: 'majority' } }) .then( result => { return cb(null, result.insertedId); @@ -270,7 +270,7 @@ export class ClientEncryption implements StateMachineExecutable { tlsOptions: this._tlsOptions }); - const dataKey = await stateMachine.executeAsync(this, context); + const dataKey = await stateMachine.executeAsync<{ v: DataKey[] }>(this, context); if (!dataKey || dataKey.v.length === 0) { return {}; } @@ -279,20 +279,22 @@ export class ClientEncryption implements StateMachineExecutable { this._keyVaultNamespace ); - const replacements = dataKey.v.map((key: DataKey) => ({ - updateOne: { - filter: { _id: key._id }, - update: { - $set: { - masterKey: key.masterKey, - keyMaterial: key.keyMaterial - }, - $currentDate: { - updateDate: true + 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) @@ -667,7 +669,7 @@ export class ClientEncryption implements StateMachineExecutable { * } * ``` */ - decrypt(value: Binary, callback?: Callback): Promise | void { + decrypt(value: Binary, callback?: Callback): Promise | void { const valueBuffer = serialize({ v: value }); const context = this._mongoCrypt.makeExplicitDecryptionContext(valueBuffer); @@ -678,7 +680,7 @@ export class ClientEncryption implements StateMachineExecutable { // @ts-expect-error We did not convert promiseOrCallback to TS return promiseOrCallback(callback, cb => { - stateMachine.execute(this, context, (err, result) => { + stateMachine.execute<{ v: T }>(this, context, (err, result) => { if (err || !result) { cb(err, null); return; @@ -759,8 +761,8 @@ export class ClientEncryption implements StateMachineExecutable { }); const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions); - const result = await stateMachine.executeAsync(this, context); - return result.v as Binary; + const result = await stateMachine.executeAsync<{ v: Binary }>(this, context); + return result.v; } } diff --git a/src/client-side-encryption/stateMachine.ts b/src/client-side-encryption/stateMachine.ts index 25454ec7f93..ffc05e828a8 100644 --- a/src/client-side-encryption/stateMachine.ts +++ b/src/client-side-encryption/stateMachine.ts @@ -139,7 +139,7 @@ export class StateMachine { private bsonOptions = pluckBSONSerializeOptions(options) ) {} - executeAsync(executor: StateMachineExecutable, context: MongoCryptContext): Promise { + 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); } @@ -147,10 +147,10 @@ export class StateMachine { /** * Executes the state machine according to the specification */ - execute( + execute( executor: StateMachineExecutable, context: MongoCryptContext, - callback: Callback + callback: Callback ) { const keyVaultNamespace = executor._keyVaultNamespace; const keyVaultClient = executor._keyVaultClient; @@ -287,7 +287,7 @@ export class StateMachine { callback(new MongoCryptError(message)); return; } - callback(undefined, deserialize(finalizedContext, this.options)); + callback(undefined, deserialize(finalizedContext, this.options) as T); return; } case MONGOCRYPT_CTX_ERROR: { diff --git a/test/unit/client-side-encryption/autoEncrypter.test.ts b/test/unit/client-side-encryption/autoEncrypter.test.ts index 7efce0a8940..1e674eb6737 100644 --- a/test/unit/client-side-encryption/autoEncrypter.test.ts +++ b/test/unit/client-side-encryption/autoEncrypter.test.ts @@ -226,30 +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', - options: { - // eslint-disable-next-line @typescript-eslint/no-empty-function - 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'; From 4c803f33c92c3a94d2c763c893484cf42d45e7b0 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 31 Jul 2023 08:40:13 -0600 Subject: [PATCH 29/45] fix failing test --- .../clientEncryption.ts | 8 ---- .../autoEncrypter.test.ts | 38 ------------------- 2 files changed, 46 deletions(-) diff --git a/src/client-side-encryption/clientEncryption.ts b/src/client-side-encryption/clientEncryption.ts index 2ece16b26ce..692bb2d522e 100644 --- a/src/client-side-encryption/clientEncryption.ts +++ b/src/client-side-encryption/clientEncryption.ts @@ -833,14 +833,6 @@ export interface ClientEncryptionOptions { */ kmsProviders?: KMSProviders; - /** - * Optional callback to override KMS providers per-context. - * - * @deprecated Installing optional dependencies will automatically refresh kms - * provider credentials. - */ - onKmsProviderRefresh?: () => Promise; - /** * Options for specifying a Socks5 proxy to use for connecting to the KMS. */ diff --git a/test/unit/client-side-encryption/autoEncrypter.test.ts b/test/unit/client-side-encryption/autoEncrypter.test.ts index 1e674eb6737..dbec282a8d7 100644 --- a/test/unit/client-side-encryption/autoEncrypter.test.ts +++ b/test/unit/client-side-encryption/autoEncrypter.test.ts @@ -346,44 +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', - options: { - // eslint-disable-next-line @typescript-eslint/no-empty-function - 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 () { From 749d3ba6425725250a0a9c78218a4617b6e36e3a Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 31 Jul 2023 13:38:27 -0600 Subject: [PATCH 30/45] Apply suggestions from code review Co-authored-by: Neal Beeken --- src/client-side-encryption/stateMachine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client-side-encryption/stateMachine.ts b/src/client-side-encryption/stateMachine.ts index 4e0a083d06d..7441b221e99 100644 --- a/src/client-side-encryption/stateMachine.ts +++ b/src/client-side-encryption/stateMachine.ts @@ -68,7 +68,7 @@ const INSECURE_TLS_OPTIONS = [ * 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: unknown) { +function debug(msg: unknown) { if (process.env.MONGODB_CRYPT_DEBUG) { // eslint-disable-next-line no-console console.error(msg); From eafee6481abb6ab5fe1875758371f11a00fffd2b Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 31 Jul 2023 13:38:16 -0600 Subject: [PATCH 31/45] rename loadCredentials --- src/client-side-encryption/autoEncrypter.ts | 4 ++-- src/client-side-encryption/clientEncryption.ts | 4 ++-- src/client-side-encryption/providers/index.ts | 2 +- src/client-side-encryption/stateMachine.ts | 1 - 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/client-side-encryption/autoEncrypter.ts b/src/client-side-encryption/autoEncrypter.ts index b515dc58ebc..bdaa9f5277c 100644 --- a/src/client-side-encryption/autoEncrypter.ts +++ b/src/client-side-encryption/autoEncrypter.ts @@ -8,7 +8,7 @@ import { MongoClient, type MongoClientOptions } from '../mongo_client'; import { type Callback, MongoDBCollectionNamespace } from '../utils'; import * as cryptoCallbacks from './cryptoCallbacks'; import { MongocryptdManager } from './mongocryptdManager'; -import { type KMSProviders, loadCredentials } from './providers'; +import { type KMSProviders, refreshKMSCredentials } from './providers'; import { type CSFLEKMSTlsOptions, StateMachine, type StateMachineExecutable } from './stateMachine'; /** @public */ @@ -575,7 +575,7 @@ export class AutoEncrypter implements StateMachineExecutable { * the original ones. */ async askForKMSCredentials(): Promise { - return loadCredentials(this._kmsProviders); + return refreshKMSCredentials(this._kmsProviders); } /** diff --git a/src/client-side-encryption/clientEncryption.ts b/src/client-side-encryption/clientEncryption.ts index 692bb2d522e..6a95632d39e 100644 --- a/src/client-side-encryption/clientEncryption.ts +++ b/src/client-side-encryption/clientEncryption.ts @@ -19,7 +19,7 @@ import { type Callback, MongoDBCollectionNamespace } from '../utils'; import { maybeCallback, promiseOrCallback } from './common'; import * as cryptoCallbacks from './cryptoCallbacks'; import { MongoCryptCreateDataKeyError, MongoCryptCreateEncryptedCollectionError } from './errors'; -import { type KMSProvider, type KMSProviders, loadCredentials } from './providers/index'; +import { type KMSProvider, type KMSProviders, refreshKMSCredentials } from './providers/index'; import { type CSFLEKMSTlsOptions, StateMachine, type StateMachineExecutable } from './stateMachine'; /** @@ -699,7 +699,7 @@ export class ClientEncryption implements StateMachineExecutable { * the original ones. */ async askForKMSCredentials(): Promise { - return loadCredentials(this._kmsProviders); + return refreshKMSCredentials(this._kmsProviders); } static get libmongocryptVersion() { 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.ts b/src/client-side-encryption/stateMachine.ts index 7441b221e99..86e5e9b54a2 100644 --- a/src/client-side-encryption/stateMachine.ts +++ b/src/client-side-encryption/stateMachine.ts @@ -363,7 +363,6 @@ export class StateMachine { function onerror(err: Error) { destroySockets(); - // TODO: make note of this const mcError = new MongoCryptError('KMS request failed', { cause: err }); reject(mcError); } From a2c8d2ab00ddea654189e1217abd71eedce23056 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 31 Jul 2023 13:44:01 -0600 Subject: [PATCH 32/45] address more comments --- src/client-side-encryption/stateMachine.ts | 5 +++-- src/index.ts | 15 +++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/client-side-encryption/stateMachine.ts b/src/client-side-encryption/stateMachine.ts index 86e5e9b54a2..a928f6e762c 100644 --- a/src/client-side-encryption/stateMachine.ts +++ b/src/client-side-encryption/stateMachine.ts @@ -60,6 +60,9 @@ 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' ]; @@ -94,8 +97,6 @@ declare module 'mongodb-client-encryption' { * - tlsAllowInvalidCertificates * - tlsAllowInvalidHostnames * - tlsInsecure - * - tlsDisableOCSPEndpointCheck - * - tlsDisableCertificateRevocationCheck */ export type CSFLETlsOptions = Pick< MongoClientOptions, diff --git a/src/index.ts b/src/index.ts index 2261c474798..0c0258df4f3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -93,14 +93,6 @@ export { UnorderedBulkOperation }; -export type { AutoEncryptionExtraOptions } from './client-side-encryption/autoEncrypter'; -export type { KMSProviders } from './client-side-encryption/providers/index'; -export type { - CSFLEKMSTlsOptions, - CSFLETlsOptions, - StateMachineExecutable -} from './client-side-encryption/stateMachine'; - // enums export { BatchType } from './bulk/common'; export { AutoEncryptionLoggerLevel } from './client-side-encryption/autoEncrypter'; @@ -211,7 +203,14 @@ export type { } from './change_stream'; export type { AutoEncrypter } from './client-side-encryption/autoEncrypter'; export type { AutoEncryptionOptions } from './client-side-encryption/autoEncrypter'; +export type { AutoEncryptionExtraOptions } from './client-side-encryption/autoEncrypter'; export type { MongocryptdManager } from './client-side-encryption/mongocryptdManager'; +export type { KMSProviders } from './client-side-encryption/providers/index'; +export type { + CSFLEKMSTlsOptions, + CSFLETlsOptions, + StateMachineExecutable +} from './client-side-encryption/stateMachine'; export type { AuthContext } from './cmap/auth/auth_provider'; export type { AuthMechanismProperties, From 1c2db54069bdb5dfb842bdeb475b076ffaf6178b Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 1 Aug 2023 14:10:34 -0600 Subject: [PATCH 33/45] fix unused imports in rolledup file' --- .eslintrc.json | 11 +++++++ package-lock.json | 33 ++++++++++++++++++- package.json | 3 +- src/client-side-encryption/autoEncrypter.ts | 12 +++++-- .../clientEncryption.ts | 5 ++- 5 files changed, 58 insertions(+), 6 deletions(-) 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 4f7d1e228c5..d9a5f0d05a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "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", @@ -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", diff --git a/package.json b/package.json index 7bc09136ef3..815a924d6f6 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "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", @@ -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.ts b/src/client-side-encryption/autoEncrypter.ts index bdaa9f5277c..cdeff88456c 100644 --- a/src/client-side-encryption/autoEncrypter.ts +++ b/src/client-side-encryption/autoEncrypter.ts @@ -1,4 +1,8 @@ -import { type MongoCryptOptions } from 'mongodb-client-encryption'; +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'; @@ -226,9 +230,11 @@ export class AutoEncrypter implements StateMachineExecutable { _mongocryptdManager?: MongocryptdManager; _mongocryptdClient?: MongoClient; - _mongocrypt: import('mongodb-client-encryption').MongoCrypt; + /** @internal */ + _mongocrypt: any; - static getMongoCrypt(): import('mongodb-client-encryption').MongoCryptConstructor { + /** @internal */ + static getMongoCrypt(): MongoCryptConstructor { const encryption = getMongoDBClientEncryption(); if ('kModuleError' in encryption) { throw encryption.kModuleError; diff --git a/src/client-side-encryption/clientEncryption.ts b/src/client-side-encryption/clientEncryption.ts index 6a95632d39e..f9f6d3ec2b7 100644 --- a/src/client-side-encryption/clientEncryption.ts +++ b/src/client-side-encryption/clientEncryption.ts @@ -1,6 +1,7 @@ import type { ExplicitEncryptionContextOptions, MongoCrypt, + MongoCryptConstructor, MongoCryptOptions } from 'mongodb-client-encryption'; @@ -47,9 +48,11 @@ export class ClientEncryption implements StateMachineExecutable { _tlsOptions: CSFLEKMSTlsOptions; _kmsProviders: KMSProviders; + /** @internal */ _mongoCrypt: MongoCrypt; - static getMongoCrypt(): import('mongodb-client-encryption').MongoCryptConstructor { + /** @internal */ + static getMongoCrypt(): MongoCryptConstructor { const encryption = getMongoDBClientEncryption(); if ('kModuleError' in encryption) { throw encryption.kModuleError; From ef53d90164e2e5b9a70db35de1bdae019e90da14 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 1 Aug 2023 14:16:29 -0600 Subject: [PATCH 34/45] re-type internal FLE APIs --- src/client-side-encryption/autoEncrypter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client-side-encryption/autoEncrypter.ts b/src/client-side-encryption/autoEncrypter.ts index cdeff88456c..b961316ab00 100644 --- a/src/client-side-encryption/autoEncrypter.ts +++ b/src/client-side-encryption/autoEncrypter.ts @@ -231,7 +231,7 @@ export class AutoEncrypter implements StateMachineExecutable { _mongocryptdClient?: MongoClient; /** @internal */ - _mongocrypt: any; + _mongocrypt: MongoCrypt; /** @internal */ static getMongoCrypt(): MongoCryptConstructor { From e2d0454926afbb70e4524128d7583f1a1e5240b0 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 1 Aug 2023 19:46:28 -0600 Subject: [PATCH 35/45] address comments and fix tests --- src/client-side-encryption/autoEncrypter.ts | 37 +++++++++++----- .../providers/credentialsProvider.test.ts | 42 +++++++++---------- 2 files changed, 47 insertions(+), 32 deletions(-) diff --git a/src/client-side-encryption/autoEncrypter.ts b/src/client-side-encryption/autoEncrypter.ts index b961316ab00..200524e8d1c 100644 --- a/src/client-side-encryption/autoEncrypter.ts +++ b/src/client-side-encryption/autoEncrypter.ts @@ -211,6 +211,15 @@ export const AutoEncryptionLoggerLevel = Object.freeze({ 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. @@ -233,6 +242,16 @@ export class AutoEncrypter implements StateMachineExecutable { /** @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]?: boolean; + /** @internal */ static getMongoCrypt(): MongoCryptConstructor { const encryption = getMongoDBClientEncryption(); @@ -509,7 +528,6 @@ export class AutoEncrypter implements StateMachineExecutable { return; } - // TODO: should these be accessors from the addon? context.id = this._contextCounter++; context.ns = ns; context.document = cmd; @@ -550,7 +568,6 @@ export class AutoEncrypter implements StateMachineExecutable { return; } - // TODO: should this be an accessor from the addon? context.id = this._contextCounter++; const stateMachine = new StateMachine({ @@ -559,8 +576,7 @@ export class AutoEncrypter implements StateMachineExecutable { tlsOptions: this._tlsOptions }); - // @ts-expect-error unique symbol cannot be used as an index type - const decorateResult = this[Symbol.for('@@mdb.decorateDecryptionResult')]; + const decorateResult = this[kDecorateResult]; stateMachine.execute(this, context, function (error?: Error, result?: Document) { // Only for testing/internal usage if (!error && result && decorateResult) { @@ -607,11 +623,10 @@ export class AutoEncrypter implements StateMachineExecutable { * @internal */ function decorateDecryptionResult( - decrypted: Document, + decrypted: Document & { [kDecoratedKeys]?: Array }, original: Document, isTopLevelDecorateCall = true ): Error | void { - 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)) { @@ -629,17 +644,17 @@ function decorateDecryptionResult( // 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) { - // @ts-expect-error unique symbols cannot be used as index type - if (!decrypted[decryptedKeys]) { - Object.defineProperty(decrypted, decryptedKeys, { + if (!decrypted[kDecoratedKeys]) { + Object.defineProperty(decrypted, kDecoratedKeys, { value: [], configurable: true, enumerable: false, writable: false }); } - // @ts-expect-error unique symbols cannot be used as index type - decrypted[decryptedKeys].push(k); + // 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; diff --git a/test/unit/client-side-encryption/providers/credentialsProvider.test.ts b/test/unit/client-side-encryption/providers/credentialsProvider.test.ts index 2f2115b6e09..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; @@ -85,7 +85,7 @@ describe('#loadCredentials', function () { }); 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, @@ -114,7 +114,7 @@ describe('#loadCredentials', function () { }); 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) @@ -147,7 +147,7 @@ describe('#loadCredentials', function () { }); it('does not refresh credentials', async function () { - const providers = await loadCredentials(kmsProviders); + const providers = await refreshKMSCredentials(kmsProviders); expect(providers).to.deep.equal(kmsProviders); }); }); @@ -171,7 +171,7 @@ describe('#loadCredentials', function () { }); it('does not refresh credentials', async function () { - const providers = await loadCredentials(kmsProviders); + const providers = await refreshKMSCredentials(kmsProviders); expect(providers).to.deep.equal(kmsProviders); }); }); @@ -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); }); }); @@ -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'); }); From f5f22b48becee2b94637c7320438995609473cc2 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 2 Aug 2023 10:34:14 -0600 Subject: [PATCH 36/45] fix lint --- src/client-side-encryption/autoEncrypter.ts | 3 +-- src/client-side-encryption/mongocryptdManager.ts | 16 ++++++++++++++-- src/client-side-encryption/stateMachine.ts | 5 ++--- src/utils.ts | 2 +- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/client-side-encryption/autoEncrypter.ts b/src/client-side-encryption/autoEncrypter.ts index 200524e8d1c..726b2fa825b 100644 --- a/src/client-side-encryption/autoEncrypter.ts +++ b/src/client-side-encryption/autoEncrypter.ts @@ -250,7 +250,7 @@ export class AutoEncrypter implements StateMachineExecutable { * it will mark it as decrypted. Compass uses this to determine which * fields were decrypted. */ - [kDecorateResult]?: boolean; + [kDecorateResult] = false; /** @internal */ static getMongoCrypt(): MongoCryptConstructor { @@ -533,7 +533,6 @@ export class AutoEncrypter implements StateMachineExecutable { context.document = cmd; const stateMachine = new StateMachine({ - ...options, promoteValues: false, promoteLongs: false, proxyOptions: this._proxyOptions, diff --git a/src/client-side-encryption/mongocryptdManager.ts b/src/client-side-encryption/mongocryptdManager.ts index 26f90f6fec9..13ffb76b2d3 100644 --- a/src/client-side-encryption/mongocryptdManager.ts +++ b/src/client-side-encryption/mongocryptdManager.ts @@ -48,7 +48,7 @@ export class MongocryptdManager { // eslint-disable-next-line @typescript-eslint/no-var-requires const { spawn } = require('child_process') as typeof import('child_process'); - // Spawned with stdio: ignore and detatched:true + // Spawned with stdio: ignore and detached: true // to ensure child can outlive parent. this._child = spawn(cmdName, this.spawnArgs, { stdio: 'ignore', @@ -56,7 +56,19 @@ export class MongocryptdManager { }); this._child.on('error', () => { - // perhaps questionable, but we swallow mongocryptd spawn errors. + // 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 diff --git a/src/client-side-encryption/stateMachine.ts b/src/client-side-encryption/stateMachine.ts index a928f6e762c..a2c9f121c8b 100644 --- a/src/client-side-encryption/stateMachine.ts +++ b/src/client-side-encryption/stateMachine.ts @@ -11,7 +11,7 @@ import { pluckBSONSerializeOptions, serialize } from '../bson'; -import { type CommandOptions, type ProxyOptions } from '../cmap/connection'; +import { type ProxyOptions } from '../cmap/connection'; import { getSocks, type SocksLib } from '../deps'; import { MongoNetworkTimeoutError } from '../error'; import { type MongoClient, type MongoClientOptions } from '../mongo_client'; @@ -138,8 +138,7 @@ export type StateMachineOptions = { /** TLS options for KMS requests, if set. */ tlsOptions: CSFLEKMSTlsOptions; -} & Pick & - CommandOptions; +} & Pick; /** * @internal diff --git a/src/utils.ts b/src/utils.ts index 015637eada1..436c2049dac 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -347,7 +347,7 @@ export class MongoDBCollectionNamespace extends MongoDBNamespace { super(db, collection); } - static override fromString(namespace?: string | undefined): MongoDBCollectionNamespace { + static override fromString(namespace?: string): MongoDBCollectionNamespace { return super.fromString(namespace) as MongoDBCollectionNamespace; } } From 335d233c05691424e5a67160a00af8831033fc3a Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 2 Aug 2023 11:11:32 -0600 Subject: [PATCH 37/45] rename files to snake case --- .../{autoEncrypter.ts => auto_encrypter.ts} | 10 +++++++--- .../{clientEncryption.ts => client_encryption.ts} | 8 ++++++-- .../{cryptoCallbacks.ts => crypto_callbacks.ts} | 0 ...{mongocryptdManager.ts => mongocryptd_manager.ts} | 2 +- .../{stateMachine.ts => state_machine.ts} | 4 ++-- src/cmap/connection.ts | 2 +- src/encrypter.ts | 2 +- src/index.ts | 12 ++++++------ src/mongo_client.ts | 2 +- src/sdam/server.ts | 2 +- ...ide_encryption.prose.14.decryption_events.test.ts | 2 +- .../client_side_encryption.prose.test.js | 2 +- test/tools/unified-spec-runner/unified-utils.ts | 2 +- ...{autoEncrypter.test.ts => auto_encrypter.test.ts} | 6 +++--- ...tEncryption.test.ts => client_encryption.test.ts} | 6 +++--- ...tdManager.test.ts => mongocryptd_manager.test.ts} | 2 +- .../{stateMachine.test.ts => state_machine.test.ts} | 2 +- 17 files changed, 37 insertions(+), 29 deletions(-) rename src/client-side-encryption/{autoEncrypter.ts => auto_encrypter.ts} (99%) rename src/client-side-encryption/{clientEncryption.ts => client_encryption.ts} (99%) rename src/client-side-encryption/{cryptoCallbacks.ts => crypto_callbacks.ts} (100%) rename src/client-side-encryption/{mongocryptdManager.ts => mongocryptd_manager.ts} (97%) rename src/client-side-encryption/{stateMachine.ts => state_machine.ts} (99%) rename test/unit/client-side-encryption/{autoEncrypter.test.ts => auto_encrypter.test.ts} (99%) rename test/unit/client-side-encryption/{clientEncryption.test.ts => client_encryption.test.ts} (99%) rename test/unit/client-side-encryption/{mongocryptdManager.test.ts => mongocryptd_manager.test.ts} (98%) rename test/unit/client-side-encryption/{stateMachine.test.ts => state_machine.test.ts} (99%) diff --git a/src/client-side-encryption/autoEncrypter.ts b/src/client-side-encryption/auto_encrypter.ts similarity index 99% rename from src/client-side-encryption/autoEncrypter.ts rename to src/client-side-encryption/auto_encrypter.ts index 726b2fa825b..c2c1b978c03 100644 --- a/src/client-side-encryption/autoEncrypter.ts +++ b/src/client-side-encryption/auto_encrypter.ts @@ -10,10 +10,14 @@ import { getMongoDBClientEncryption } from '../deps'; import { type AnyError, MongoError, MongoRuntimeError } from '../error'; import { MongoClient, type MongoClientOptions } from '../mongo_client'; import { type Callback, MongoDBCollectionNamespace } from '../utils'; -import * as cryptoCallbacks from './cryptoCallbacks'; -import { MongocryptdManager } from './mongocryptdManager'; +import * as cryptoCallbacks from './crypto_callbacks'; +import { MongocryptdManager } from './mongocryptd_manager'; import { type KMSProviders, refreshKMSCredentials } from './providers'; -import { type CSFLEKMSTlsOptions, StateMachine, type StateMachineExecutable } from './stateMachine'; +import { + type CSFLEKMSTlsOptions, + StateMachine, + type StateMachineExecutable +} from './state_machine'; /** @public */ export interface AutoEncryptionOptions { diff --git a/src/client-side-encryption/clientEncryption.ts b/src/client-side-encryption/client_encryption.ts similarity index 99% rename from src/client-side-encryption/clientEncryption.ts rename to src/client-side-encryption/client_encryption.ts index f9f6d3ec2b7..c06557e9379 100644 --- a/src/client-side-encryption/clientEncryption.ts +++ b/src/client-side-encryption/client_encryption.ts @@ -18,10 +18,14 @@ 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 './cryptoCallbacks'; +import * as cryptoCallbacks from './crypto_callbacks'; import { MongoCryptCreateDataKeyError, MongoCryptCreateEncryptedCollectionError } from './errors'; import { type KMSProvider, type KMSProviders, refreshKMSCredentials } from './providers/index'; -import { type CSFLEKMSTlsOptions, StateMachine, type StateMachineExecutable } from './stateMachine'; +import { + type CSFLEKMSTlsOptions, + StateMachine, + type StateMachineExecutable +} from './state_machine'; /** * The schema for a DataKey in the key vault collection. diff --git a/src/client-side-encryption/cryptoCallbacks.ts b/src/client-side-encryption/crypto_callbacks.ts similarity index 100% rename from src/client-side-encryption/cryptoCallbacks.ts rename to src/client-side-encryption/crypto_callbacks.ts diff --git a/src/client-side-encryption/mongocryptdManager.ts b/src/client-side-encryption/mongocryptd_manager.ts similarity index 97% rename from src/client-side-encryption/mongocryptdManager.ts rename to src/client-side-encryption/mongocryptd_manager.ts index 13ffb76b2d3..cf69f0fd7b5 100644 --- a/src/client-side-encryption/mongocryptdManager.ts +++ b/src/client-side-encryption/mongocryptd_manager.ts @@ -1,7 +1,7 @@ import type { ChildProcess } from 'child_process'; import { type Callback } from '../utils'; -import { type AutoEncryptionExtraOptions } from './autoEncrypter'; +import { type AutoEncryptionExtraOptions } from './auto_encrypter'; /** * @internal diff --git a/src/client-side-encryption/stateMachine.ts b/src/client-side-encryption/state_machine.ts similarity index 99% rename from src/client-side-encryption/stateMachine.ts rename to src/client-side-encryption/state_machine.ts index a2c9f121c8b..121ee3fe8f0 100644 --- a/src/client-side-encryption/stateMachine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -16,9 +16,9 @@ import { getSocks, type SocksLib } from '../deps'; import { MongoNetworkTimeoutError } from '../error'; import { type MongoClient, type MongoClientOptions } from '../mongo_client'; import { BufferPool, type Callback, MongoDBCollectionNamespace } from '../utils'; -import { type DataKey } from './clientEncryption'; +import { type DataKey } from './client_encryption'; import { MongoCryptError } from './errors'; -import { type MongocryptdManager } from './mongocryptdManager'; +import { type MongocryptdManager } from './mongocryptd_manager'; import { type KMSProvider, type KMSProviders } from './providers'; let socks: SocksLib | null = null; diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index 7fca67caed5..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, diff --git a/src/encrypter.ts b/src/encrypter.ts index 5c9d0a64166..f03e8d6c1a9 100644 --- a/src/encrypter.ts +++ b/src/encrypter.ts @@ -1,4 +1,4 @@ -import { AutoEncrypter, type AutoEncryptionOptions } from './client-side-encryption/autoEncrypter'; +import { AutoEncrypter, type AutoEncryptionOptions } from './client-side-encryption/auto_encrypter'; import { MONGO_CLIENT_EVENTS } from './constants'; import { getMongoDBClientEncryption } from './deps'; import { MongoInvalidArgumentError, MongoMissingDependencyError } from './error'; diff --git a/src/index.ts b/src/index.ts index 0c0258df4f3..38e5fdf9644 100644 --- a/src/index.ts +++ b/src/index.ts @@ -95,7 +95,7 @@ export { // enums export { BatchType } from './bulk/common'; -export { AutoEncryptionLoggerLevel } from './client-side-encryption/autoEncrypter'; +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'; @@ -201,16 +201,16 @@ export type { ResumeToken, UpdateDescription } from './change_stream'; -export type { AutoEncrypter } from './client-side-encryption/autoEncrypter'; -export type { AutoEncryptionOptions } from './client-side-encryption/autoEncrypter'; -export type { AutoEncryptionExtraOptions } 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/stateMachine'; +} from './client-side-encryption/state_machine'; export type { AuthContext } from './cmap/auth/auth_provider'; export type { AuthMechanismProperties, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 4c4e778c0c3..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, AutoEncryptionOptions } from './client-side-encryption/autoEncrypter'; +import type { AutoEncrypter, AutoEncryptionOptions } from './client-side-encryption/auto_encrypter'; import { type AuthMechanismProperties, DEFAULT_ALLOWED_HOSTS, 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/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.test.js b/test/integration/client-side-encryption/client_side_encryption.prose.test.js index c15a3145705..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 || '{}'); 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.ts b/test/unit/client-side-encryption/auto_encrypter.test.ts similarity index 99% rename from test/unit/client-side-encryption/autoEncrypter.test.ts rename to test/unit/client-side-encryption/auto_encrypter.test.ts index dbec282a8d7..965bce6e3d5 100644 --- a/test/unit/client-side-encryption/autoEncrypter.test.ts +++ b/test/unit/client-side-encryption/auto_encrypter.test.ts @@ -3,11 +3,11 @@ 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/autoEncrypter'; +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/mongocryptdManager'; +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/stateMachine'; +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'; diff --git a/test/unit/client-side-encryption/clientEncryption.test.ts b/test/unit/client-side-encryption/client_encryption.test.ts similarity index 99% rename from test/unit/client-side-encryption/clientEncryption.test.ts rename to test/unit/client-side-encryption/client_encryption.test.ts index 3e84479147d..5e2586990ac 100644 --- a/test/unit/client-side-encryption/clientEncryption.test.ts +++ b/test/unit/client-side-encryption/client_encryption.test.ts @@ -4,16 +4,16 @@ 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/clientEncryption'; +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/cryptoCallbacks'; +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/stateMachine'; +import { StateMachine } from '../../../src/client-side-encryption/state_machine'; import { Binary, BSON, deserialize } from '../../mongodb'; const { EJSON } = BSON; diff --git a/test/unit/client-side-encryption/mongocryptdManager.test.ts b/test/unit/client-side-encryption/mongocryptd_manager.test.ts similarity index 98% rename from test/unit/client-side-encryption/mongocryptdManager.test.ts rename to test/unit/client-side-encryption/mongocryptd_manager.test.ts index a4e2ec7a14e..8122841e3b5 100644 --- a/test/unit/client-side-encryption/mongocryptdManager.test.ts +++ b/test/unit/client-side-encryption/mongocryptd_manager.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports -import { MongocryptdManager } from '../../../src/client-side-encryption/mongocryptdManager'; +import { MongocryptdManager } from '../../../src/client-side-encryption/mongocryptd_manager'; describe('MongocryptdManager', function () { it('should default to having spawnArgs of --idleShutdownTimeoutSecs=60', function () { diff --git a/test/unit/client-side-encryption/stateMachine.test.ts b/test/unit/client-side-encryption/state_machine.test.ts similarity index 99% rename from test/unit/client-side-encryption/stateMachine.test.ts rename to test/unit/client-side-encryption/state_machine.test.ts index a5e02a8bda3..f1a7042d46e 100644 --- a/test/unit/client-side-encryption/stateMachine.test.ts +++ b/test/unit/client-side-encryption/state_machine.test.ts @@ -8,7 +8,7 @@ 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/stateMachine'; +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 From 791750df499d7179a8331ad63fb4ba6ed794210b Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 2 Aug 2023 11:27:33 -0600 Subject: [PATCH 38/45] fix tests --- .../client_side_encryption.prose.06.corpus.test.js | 2 +- .../client_side_encryption.prose.12.deadlock.test.ts | 2 +- .../client_side_encryption.prose.17.on_demand_gcp.test.ts | 2 +- .../client_side_encryption.prose.19.on_demand_azure.test.ts | 2 +- ...e_encryption.prose.21.automatic_data_encryption_keys.test.ts | 2 +- ...t_side_encryption.prose.22.range_explicit_encryption.test.ts | 2 +- test/integration/client-side-encryption/driver.test.ts | 2 +- test/integration/node-specific/client_encryption.test.ts | 2 +- test/integration/node-specific/crypto_callbacks.test.ts | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) 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.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/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/client_encryption.test.ts b/test/integration/node-specific/client_encryption.test.ts index 0b9a27f25a1..e08c4d74a3b 100644 --- a/test/integration/node-specific/client_encryption.test.ts +++ b/test/integration/node-specific/client_encryption.test.ts @@ -6,7 +6,7 @@ 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 { Binary, type Collection, Int32, Long, type MongoClient } from '../../mongodb'; diff --git a/test/integration/node-specific/crypto_callbacks.test.ts b/test/integration/node-specific/crypto_callbacks.test.ts index 3390ae01f5a..ec749f6ffaf 100644 --- a/test/integration/node-specific/crypto_callbacks.test.ts +++ b/test/integration/node-specific/crypto_callbacks.test.ts @@ -2,7 +2,7 @@ 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 { type MongoClient } from '../../mongodb'; From 83105362b716fc391408c3e88adc34bbdc1d5afe Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 2 Aug 2023 11:27:57 -0600 Subject: [PATCH 39/45] consolidate error handling and get rid of argument checking in internal APIs --- src/client-side-encryption/auto_encrypter.ts | 23 ++++++++----------- .../client_encryption.ts | 18 ++++++++++----- src/client-side-encryption/errors.ts | 15 ++++++++++++ 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/client-side-encryption/auto_encrypter.ts b/src/client-side-encryption/auto_encrypter.ts index c2c1b978c03..5bcbd1c683c 100644 --- a/src/client-side-encryption/auto_encrypter.ts +++ b/src/client-side-encryption/auto_encrypter.ts @@ -7,10 +7,11 @@ import { import { deserialize, type Document, serialize } from '../bson'; import { type CommandOptions, type ProxyOptions } from '../cmap/connection'; import { getMongoDBClientEncryption } from '../deps'; -import { type AnyError, MongoError, MongoRuntimeError } from '../error'; +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 { @@ -373,7 +374,9 @@ export class AutoEncrypter implements StateMachineExecutable { options.extraOptions.cryptSharedLibRequired && !this.cryptSharedLibVersionInfo ) { - throw new MongoError('`cryptSharedLibRequired` set but no crypt_shared library loaded'); + throw new MongoCryptInvalidArgumentError( + '`cryptSharedLibRequired` set but no crypt_shared library loaded' + ); } // Only instantiate mongocryptd manager/client once we know for sure @@ -422,7 +425,7 @@ export class AutoEncrypter implements StateMachineExecutable { (err.message.match(/timed out after/) || err.message.match(/ENOTFOUND/)) ) { callback( - new MongoError( + new MongoRuntimeError( 'Unable to connect to `mongocryptd`, please make sure it is running or in your PATH for auto-spawn' ) ); @@ -497,18 +500,10 @@ export class AutoEncrypter implements StateMachineExecutable { options?: CommandOptions | Callback, callback?: 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'); - } - callback = typeof options === 'function' ? options : callback; if (callback == null) { - throw new TypeError('Callback must be provided'); + throw new MongoCryptInvalidArgumentError('Callback must be provided'); } options = typeof options === 'function' ? {} : options; @@ -556,7 +551,7 @@ export class AutoEncrypter implements StateMachineExecutable { callback = typeof options === 'function' ? options : callback; if (callback == null) { - throw new TypeError('Callback must be provided'); + throw new MongoCryptInvalidArgumentError('Callback must be provided'); } options = typeof options === 'function' ? {} : options; @@ -636,7 +631,7 @@ function decorateDecryptionResult( original = deserialize(original); } if (Buffer.isBuffer(decrypted)) { - return new Error('Expected result of decryption to be deserialized BSON object'); + return new MongoRuntimeError('Expected result of decryption to be deserialized BSON object'); } } diff --git a/src/client-side-encryption/client_encryption.ts b/src/client-side-encryption/client_encryption.ts index c06557e9379..5d06134c226 100644 --- a/src/client-side-encryption/client_encryption.ts +++ b/src/client-side-encryption/client_encryption.ts @@ -19,7 +19,11 @@ 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 } from './errors'; +import { + MongoCryptCreateDataKeyError, + MongoCryptCreateEncryptedCollectionError, + MongoCryptInvalidArgumentError +} from './errors'; import { type KMSProvider, type KMSProviders, refreshKMSCredentials } from './providers/index'; import { type CSFLEKMSTlsOptions, @@ -99,7 +103,7 @@ export class ClientEncryption implements StateMachineExecutable { this._kmsProviders = options.kmsProviders || {}; if (options.keyVaultNamespace == null) { - throw new TypeError('Missing required option `keyVaultNamespace`'); + throw new MongoCryptInvalidArgumentError('Missing required option `keyVaultNamespace`'); } const mongoCryptOptions: MongoCryptOptions = { @@ -176,7 +180,7 @@ export class ClientEncryption implements StateMachineExecutable { const dataKey = Object.assign({ provider }, options.masterKey); if (options.keyAltNames && !Array.isArray(options.keyAltNames)) { - throw new TypeError( + throw new MongoCryptInvalidArgumentError( `Option "keyAltNames" must be an array of strings, but was of type ${typeof options.keyAltNames}.` ); } @@ -185,7 +189,7 @@ export class ClientEncryption implements StateMachineExecutable { if (options.keyAltNames && options.keyAltNames.length > 0) { keyAltNames = options.keyAltNames.map((keyAltName, i) => { if (typeof keyAltName !== 'string') { - throw new TypeError( + throw new MongoCryptInvalidArgumentError( `Option "keyAltNames" must be an array of strings, but item at index ${i} was of type ${typeof keyAltName}` ); } @@ -740,10 +744,12 @@ export class ClientEncryption implements StateMachineExecutable { } if (keyAltName) { if (keyId) { - throw new TypeError(`"options" cannot contain both "keyId" and "keyAltName"`); + throw new MongoCryptInvalidArgumentError( + `"options" cannot contain both "keyId" and "keyAltName"` + ); } if (typeof keyAltName !== 'string') { - throw new TypeError( + throw new MongoCryptInvalidArgumentError( `"options.keyAltName" must be of type string, but was of type ${typeof keyAltName}` ); } 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 From 261ee117b5789fe1f24671d3c1559df219564dc9 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 2 Aug 2023 12:58:22 -0600 Subject: [PATCH 40/45] fix naming issues --- test/integration/node-specific/auto_encrypter.test.ts | 6 +++--- test/integration/node-specific/client_encryption.test.ts | 2 +- test/integration/node-specific/crypto_callbacks.test.ts | 2 +- test/tools/fixtures/shared_library_test.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) 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 e08c4d74a3b..cb2ccfbad0b 100644 --- a/test/integration/node-specific/client_encryption.test.ts +++ b/test/integration/node-specific/client_encryption.test.ts @@ -8,7 +8,7 @@ import { type DataKey } from '../../../src/client-side-encryption/client_encryption'; /* 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 { Binary, type Collection, Int32, Long, type MongoClient } from '../../mongodb'; function readHttpResponse(path) { diff --git a/test/integration/node-specific/crypto_callbacks.test.ts b/test/integration/node-specific/crypto_callbacks.test.ts index ec749f6ffaf..e3564164eb7 100644 --- a/test/integration/node-specific/crypto_callbacks.test.ts +++ b/test/integration/node-specific/crypto_callbacks.test.ts @@ -4,7 +4,7 @@ import * as sinon from 'sinon'; /* eslint-disable @typescript-eslint/no-restricted-imports */ 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 { From 95c686d95002dcc35ecba42b2b847c2a553f37e7 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 2 Aug 2023 14:08:54 -0600 Subject: [PATCH 41/45] remove reliance on TypeErrors --- ...on.prose.21.automatic_data_encryption_keys.test.ts | 4 +++- ...ryption.prose.22.range_explicit_encryption.test.ts | 4 +++- .../node-specific/client_encryption.test.ts | 11 ++++++----- 3 files changed, 12 insertions(+), 7 deletions(-) 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 9990d773767..0463d194758 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,5 +1,7 @@ import { expect } from 'chai'; +/* eslint-disable @typescript-eslint/no-restricted-imports */ +import { MongoCryptInvalidArgumentError } from '../../../lib/client-side-encryption/errors'; /* eslint-disable @typescript-eslint/no-restricted-imports */ import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption'; /* eslint-disable @typescript-eslint/no-restricted-imports */ @@ -90,7 +92,7 @@ describe('21. Automatic Data Encryption Keys', () => { }) .catch(error => error); - expect(result).to.be.instanceOf(TypeError); + expect(result).to.be.instanceOf(MongoCryptInvalidArgumentError); }); it('Case 3: Invalid keyId', metadata, async () => { 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 af5fbf9fe1a..d4452f928c6 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 @@ -3,6 +3,8 @@ import { expect } from 'chai'; import { readFile } from 'fs/promises'; import { join } from 'path'; +/* eslint-disable @typescript-eslint/no-restricted-imports */ +import { MongoCryptInvalidArgumentError } from '../../../lib/client-side-encryption/errors'; import { Decimal128, type Document, Double, Long, type MongoClient } from '../../../src'; /* eslint-disable @typescript-eslint/no-restricted-imports */ import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption'; @@ -532,7 +534,7 @@ describe('Range Explicit Encryption', function () { }) .catch(e => e); - expect(resultOrError).to.be.instanceOf(TypeError); + expect(resultOrError).to.be.instanceOf(MongoCryptInvalidArgumentError); } ); }); diff --git a/test/integration/node-specific/client_encryption.test.ts b/test/integration/node-specific/client_encryption.test.ts index cb2ccfbad0b..49c543eb38a 100644 --- a/test/integration/node-specific/client_encryption.test.ts +++ b/test/integration/node-specific/client_encryption.test.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { readFileSync } from 'fs'; import * as sinon from 'sinon'; +import { MongoCryptInvalidArgumentError } from '../../../lib/client-side-encryption/errors'; /* eslint-disable @typescript-eslint/no-restricted-imports */ import { ClientEncryption, @@ -440,7 +441,7 @@ describe('ClientEncryption integration tests', function () { .encryptExpression(expression, completeOptions) .catch(e => e); - expect(errorOrResult).to.be.instanceof(TypeError); + expect(errorOrResult).to.be.instanceof(MongoCryptInvalidArgumentError); }); it('throws if algorithm is not provided', metadata, async function () { @@ -449,7 +450,7 @@ describe('ClientEncryption integration tests', function () { .encryptExpression(expression, completeOptions) .catch(e => e); - expect(errorOrResult).to.be.instanceof(TypeError); + expect(errorOrResult).to.be.instanceof(MongoCryptInvalidArgumentError); }); it(`throws if algorithm does not equal 'rangePreview'`, metadata, async function () { @@ -458,7 +459,7 @@ describe('ClientEncryption integration tests', function () { .encryptExpression(expression, completeOptions) .catch(e => e); - expect(errorOrResult).to.be.instanceof(TypeError); + expect(errorOrResult).to.be.instanceof(MongoCryptInvalidArgumentError); }); it( @@ -538,7 +539,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 +548,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 ); }); }); From d5100e823ece47f0eba0dcdbae14ac8ff939bd23 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 2 Aug 2023 14:45:13 -0600 Subject: [PATCH 42/45] fix tests --- ...encryption.prose.22.range_explicit_encryption.test.ts | 2 +- test/integration/node-specific/client_encryption.test.ts | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) 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 d4452f928c6..36f449c7fc4 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 @@ -534,7 +534,7 @@ describe('Range Explicit Encryption', function () { }) .catch(e => e); - expect(resultOrError).to.be.instanceOf(MongoCryptInvalidArgumentError); + expect(resultOrError).to.be.instanceOf(TypeError); } ); }); diff --git a/test/integration/node-specific/client_encryption.test.ts b/test/integration/node-specific/client_encryption.test.ts index 49c543eb38a..561fdde95d5 100644 --- a/test/integration/node-specific/client_encryption.test.ts +++ b/test/integration/node-specific/client_encryption.test.ts @@ -2,13 +2,14 @@ import { expect } from 'chai'; import { readFileSync } from 'fs'; import * as sinon from 'sinon'; -import { MongoCryptInvalidArgumentError } from '../../../lib/client-side-encryption/errors'; /* eslint-disable @typescript-eslint/no-restricted-imports */ import { ClientEncryption, type DataKey } from '../../../src/client-side-encryption/client_encryption'; /* eslint-disable @typescript-eslint/no-restricted-imports */ +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'; @@ -441,7 +442,7 @@ describe('ClientEncryption integration tests', function () { .encryptExpression(expression, completeOptions) .catch(e => e); - expect(errorOrResult).to.be.instanceof(MongoCryptInvalidArgumentError); + expect(errorOrResult).to.be.instanceof(TypeError); }); it('throws if algorithm is not provided', metadata, async function () { @@ -450,7 +451,7 @@ describe('ClientEncryption integration tests', function () { .encryptExpression(expression, completeOptions) .catch(e => e); - expect(errorOrResult).to.be.instanceof(MongoCryptInvalidArgumentError); + expect(errorOrResult).to.be.instanceof(TypeError); }); it(`throws if algorithm does not equal 'rangePreview'`, metadata, async function () { @@ -459,7 +460,7 @@ describe('ClientEncryption integration tests', function () { .encryptExpression(expression, completeOptions) .catch(e => e); - expect(errorOrResult).to.be.instanceof(MongoCryptInvalidArgumentError); + expect(errorOrResult).to.be.instanceof(TypeError); }); it( From efd07b3477080e63871e4389d1fdb091b2b920d4 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 3 Aug 2023 08:10:32 -0600 Subject: [PATCH 43/45] fix lint and test --- ...encryption.prose.21.automatic_data_encryption_keys.test.ts | 4 +--- ...side_encryption.prose.22.range_explicit_encryption.test.ts | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) 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 0463d194758..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,5 @@ import { expect } from 'chai'; -/* eslint-disable @typescript-eslint/no-restricted-imports */ -import { MongoCryptInvalidArgumentError } from '../../../lib/client-side-encryption/errors'; /* eslint-disable @typescript-eslint/no-restricted-imports */ import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption'; /* eslint-disable @typescript-eslint/no-restricted-imports */ @@ -92,7 +90,7 @@ describe('21. Automatic Data Encryption Keys', () => { }) .catch(error => error); - expect(result).to.be.instanceOf(MongoCryptInvalidArgumentError); + expect(result).to.be.instanceOf(TypeError); }); it('Case 3: Invalid keyId', metadata, async () => { 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 36f449c7fc4..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 @@ -3,8 +3,6 @@ import { expect } from 'chai'; import { readFile } from 'fs/promises'; import { join } from 'path'; -/* eslint-disable @typescript-eslint/no-restricted-imports */ -import { MongoCryptInvalidArgumentError } from '../../../lib/client-side-encryption/errors'; import { Decimal128, type Document, Double, Long, type MongoClient } from '../../../src'; /* eslint-disable @typescript-eslint/no-restricted-imports */ import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption'; From 805ac5c3d1f2caf80f2dd5c5a6e10b87605afd46 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 3 Aug 2023 12:02:05 -0600 Subject: [PATCH 44/45] remove last spread --- src/client-side-encryption/auto_encrypter.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client-side-encryption/auto_encrypter.ts b/src/client-side-encryption/auto_encrypter.ts index 5bcbd1c683c..ddb4d93cb22 100644 --- a/src/client-side-encryption/auto_encrypter.ts +++ b/src/client-side-encryption/auto_encrypter.ts @@ -569,7 +569,6 @@ export class AutoEncrypter implements StateMachineExecutable { context.id = this._contextCounter++; const stateMachine = new StateMachine({ - ...options, proxyOptions: this._proxyOptions, tlsOptions: this._tlsOptions }); From 6198bd7c2b208a6f3eee60a56d36e749922d2647 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 3 Aug 2023 12:57:39 -0600 Subject: [PATCH 45/45] Revert "remove last spread" This reverts commit 805ac5c3d1f2caf80f2dd5c5a6e10b87605afd46. --- src/client-side-encryption/auto_encrypter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client-side-encryption/auto_encrypter.ts b/src/client-side-encryption/auto_encrypter.ts index ddb4d93cb22..5bcbd1c683c 100644 --- a/src/client-side-encryption/auto_encrypter.ts +++ b/src/client-side-encryption/auto_encrypter.ts @@ -569,6 +569,7 @@ export class AutoEncrypter implements StateMachineExecutable { context.id = this._contextCounter++; const stateMachine = new StateMachine({ + ...options, proxyOptions: this._proxyOptions, tlsOptions: this._tlsOptions });