mongo works

This commit is contained in:
counterweight 2025-06-01 23:23:55 +02:00
parent ae6ccd559f
commit 125107da50
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
937 changed files with 205033 additions and 2 deletions

View file

@ -0,0 +1,468 @@
import {
type MongoCrypt,
type MongoCryptConstructor,
type MongoCryptOptions
} from 'mongodb-client-encryption';
import * as net from 'net';
import { deserialize, type Document, serialize } from '../bson';
import { type CommandOptions, type ProxyOptions } from '../cmap/connection';
import { kDecorateResult } from '../constants';
import { getMongoDBClientEncryption } from '../deps';
import { MongoRuntimeError } from '../error';
import { MongoClient, type MongoClientOptions } from '../mongo_client';
import { type Abortable } from '../mongo_types';
import { MongoDBCollectionNamespace } from '../utils';
import { autoSelectSocketOptions } from './client_encryption';
import * as cryptoCallbacks from './crypto_callbacks';
import { MongoCryptInvalidArgumentError } from './errors';
import { MongocryptdManager } from './mongocryptd_manager';
import {
type CredentialProviders,
isEmptyCredentials,
type KMSProviders,
refreshKMSCredentials
} from './providers';
import { type CSFLEKMSTlsOptions, StateMachine } from './state_machine';
/** @public */
export interface AutoEncryptionOptions {
/** @internal client for metadata lookups */
metadataClient?: MongoClient;
/** A `MongoClient` used to fetch keys from a key vault */
keyVaultClient?: MongoClient;
/** The namespace where keys are stored in the key vault */
keyVaultNamespace?: string;
/** Configuration options that are used by specific KMS providers during key generation, encryption, and decryption. */
kmsProviders?: KMSProviders;
/** Configuration options for custom credential providers. */
credentialProviders?: CredentialProviders;
/**
* A map of namespaces to a local JSON schema for encryption
*
* **NOTE**: Supplying options.schemaMap provides more security than relying on JSON Schemas obtained from the server.
* It protects against a malicious server advertising a false JSON Schema, which could trick the client into sending decrypted data that should be encrypted.
* Schemas supplied in the schemaMap only apply to configuring automatic encryption for Client-Side Field Level Encryption.
* Other validation rules in the JSON schema will not be enforced by the driver and will result in an error.
*/
schemaMap?: Document;
/** Supply a schema for the encrypted fields in the document */
encryptedFieldsMap?: Document;
/** Allows the user to bypass auto encryption, maintaining implicit decryption */
bypassAutoEncryption?: boolean;
/** Allows users to bypass query analysis */
bypassQueryAnalysis?: boolean;
options?: {
/** An optional hook to catch logging messages from the underlying encryption engine */
logger?: (level: AutoEncryptionLoggerLevel, message: string) => void;
};
extraOptions?: {
/**
* A local process the driver communicates with to determine how to encrypt values in a command.
* Defaults to "mongodb://%2Fvar%2Fmongocryptd.sock" if domain sockets are available or "mongodb://localhost:27020" otherwise
*/
mongocryptdURI?: string;
/** If true, autoEncryption will not attempt to spawn a mongocryptd before connecting */
mongocryptdBypassSpawn?: boolean;
/** The path to the mongocryptd executable on the system */
mongocryptdSpawnPath?: string;
/** Command line arguments to use when auto-spawning a mongocryptd */
mongocryptdSpawnArgs?: string[];
/**
* Full path to a MongoDB Crypt shared library to be used (instead of mongocryptd).
*
* This needs to be the path to the file itself, not a directory.
* It can be an absolute or relative path. If the path is relative and
* its first component is `$ORIGIN`, it will be replaced by the directory
* containing the mongodb-client-encryption native addon file. Otherwise,
* the path will be interpreted relative to the current working directory.
*
* Currently, loading different MongoDB Crypt shared library files from different
* MongoClients in the same process is not supported.
*
* If this option is provided and no MongoDB Crypt shared library could be loaded
* from the specified location, creating the MongoClient will fail.
*
* If this option is not provided and `cryptSharedLibRequired` is not specified,
* the AutoEncrypter will attempt to spawn and/or use mongocryptd according
* to the mongocryptd-specific `extraOptions` options.
*
* Specifying a path prevents mongocryptd from being used as a fallback.
*
* Requires the MongoDB Crypt shared library, available in MongoDB 6.0 or higher.
*/
cryptSharedLibPath?: string;
/**
* If specified, never use mongocryptd and instead fail when the MongoDB Crypt
* shared library could not be loaded.
*
* This is always true when `cryptSharedLibPath` is specified.
*
* Requires the MongoDB Crypt shared library, available in MongoDB 6.0 or higher.
*/
cryptSharedLibRequired?: boolean;
/**
* Search paths for a MongoDB Crypt shared library to be used (instead of mongocryptd)
* Only for driver testing!
* @internal
*/
cryptSharedLibSearchPaths?: string[];
};
proxyOptions?: ProxyOptions;
/** The TLS options to use connecting to the KMS provider */
tlsOptions?: CSFLEKMSTlsOptions;
}
/**
* @public
*
* Extra options related to the mongocryptd process
* \* _Available in MongoDB 6.0 or higher._
*/
export type AutoEncryptionExtraOptions = NonNullable<AutoEncryptionOptions['extraOptions']>;
/** @public */
export const AutoEncryptionLoggerLevel = Object.freeze({
FatalError: 0,
Error: 1,
Warning: 2,
Info: 3,
Trace: 4
} as const);
/**
* @public
* The level of severity of the log message
*
* | Value | Level |
* |-------|-------|
* | 0 | Fatal Error |
* | 1 | Error |
* | 2 | Warning |
* | 3 | Info |
* | 4 | Trace |
*/
export type AutoEncryptionLoggerLevel =
(typeof AutoEncryptionLoggerLevel)[keyof typeof AutoEncryptionLoggerLevel];
/**
* @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 {
_client: MongoClient;
_bypassEncryption: boolean;
_keyVaultNamespace: string;
_keyVaultClient: MongoClient;
_metaDataClient: MongoClient;
_proxyOptions: ProxyOptions;
_tlsOptions: CSFLEKMSTlsOptions;
_kmsProviders: KMSProviders;
_bypassMongocryptdAndCryptShared: boolean;
_contextCounter: number;
_credentialProviders?: CredentialProviders;
_mongocryptdManager?: MongocryptdManager;
_mongocryptdClient?: MongoClient;
/** @internal */
_mongocrypt: MongoCrypt;
/**
* Used by devtools to enable decorating decryption results.
*
* When set and enabled, `decrypt` will automatically recursively
* traverse a decrypted document and if a field has been decrypted,
* it will mark it as decrypted. Compass uses this to determine which
* fields were decrypted.
*/
[kDecorateResult] = false;
/** @internal */
static getMongoCrypt(): MongoCryptConstructor {
const encryption = getMongoDBClientEncryption();
if ('kModuleError' in encryption) {
throw encryption.kModuleError;
}
return encryption.MongoCrypt;
}
/**
* Create an AutoEncrypter
*
* **Note**: Do not instantiate this class directly. Rather, supply the relevant options to a MongoClient
*
* **Note**: Supplying `options.schemaMap` provides more security than relying on JSON Schemas obtained from the server.
* It protects against a malicious server advertising a false JSON Schema, which could trick the client into sending unencrypted data that should be encrypted.
* Schemas supplied in the schemaMap only apply to configuring automatic encryption for Client-Side Field Level Encryption.
* Other validation rules in the JSON schema will not be enforced by the driver and will result in an error.
*
* @example <caption>Create an AutoEncrypter that makes use of mongocryptd</caption>
* ```ts
* // Enabling autoEncryption via a MongoClient using mongocryptd
* const { MongoClient } = require('mongodb');
* const client = new MongoClient(URL, {
* autoEncryption: {
* kmsProviders: {
* aws: {
* accessKeyId: AWS_ACCESS_KEY,
* secretAccessKey: AWS_SECRET_KEY
* }
* }
* }
* });
* ```
*
* await client.connect();
* // From here on, the client will be encrypting / decrypting automatically
* @example <caption>Create an AutoEncrypter that makes use of libmongocrypt's CSFLE shared library</caption>
* ```ts
* // Enabling autoEncryption via a MongoClient using CSFLE shared library
* const { MongoClient } = require('mongodb');
* const client = new MongoClient(URL, {
* autoEncryption: {
* kmsProviders: {
* aws: {}
* },
* extraOptions: {
* cryptSharedLibPath: '/path/to/local/crypt/shared/lib',
* cryptSharedLibRequired: true
* }
* }
* });
* ```
*
* await client.connect();
* // From here on, the client will be encrypting / decrypting automatically
*/
constructor(client: MongoClient, options: AutoEncryptionOptions) {
this._client = client;
this._bypassEncryption = options.bypassAutoEncryption === true;
this._keyVaultNamespace = options.keyVaultNamespace || 'admin.datakeys';
this._keyVaultClient = options.keyVaultClient || client;
this._metaDataClient = options.metadataClient || client;
this._proxyOptions = options.proxyOptions || {};
this._tlsOptions = options.tlsOptions || {};
this._kmsProviders = options.kmsProviders || {};
this._credentialProviders = options.credentialProviders;
if (options.credentialProviders?.aws && !isEmptyCredentials('aws', this._kmsProviders)) {
throw new MongoCryptInvalidArgumentError(
'Can only provide a custom AWS credential provider when the state machine is configured for automatic AWS credential fetching'
);
}
const mongoCryptOptions: MongoCryptOptions = {
enableMultipleCollinfo: true,
cryptoCallbacks
};
if (options.schemaMap) {
mongoCryptOptions.schemaMap = Buffer.isBuffer(options.schemaMap)
? options.schemaMap
: (serialize(options.schemaMap) as Buffer);
}
if (options.encryptedFieldsMap) {
mongoCryptOptions.encryptedFieldsMap = Buffer.isBuffer(options.encryptedFieldsMap)
? options.encryptedFieldsMap
: (serialize(options.encryptedFieldsMap) as Buffer);
}
mongoCryptOptions.kmsProviders = !Buffer.isBuffer(this._kmsProviders)
? (serialize(this._kmsProviders) as Buffer)
: this._kmsProviders;
if (options.options?.logger) {
mongoCryptOptions.logger = options.options.logger;
}
if (options.extraOptions && options.extraOptions.cryptSharedLibPath) {
mongoCryptOptions.cryptSharedLibPath = options.extraOptions.cryptSharedLibPath;
}
if (options.bypassQueryAnalysis) {
mongoCryptOptions.bypassQueryAnalysis = options.bypassQueryAnalysis;
}
this._bypassMongocryptdAndCryptShared = this._bypassEncryption || !!options.bypassQueryAnalysis;
if (options.extraOptions && options.extraOptions.cryptSharedLibSearchPaths) {
// Only for driver testing
mongoCryptOptions.cryptSharedLibSearchPaths = options.extraOptions.cryptSharedLibSearchPaths;
} else if (!this._bypassMongocryptdAndCryptShared) {
mongoCryptOptions.cryptSharedLibSearchPaths = ['$SYSTEM'];
}
const MongoCrypt = AutoEncrypter.getMongoCrypt();
this._mongocrypt = new MongoCrypt(mongoCryptOptions);
this._contextCounter = 0;
if (
options.extraOptions &&
options.extraOptions.cryptSharedLibRequired &&
!this.cryptSharedLibVersionInfo
) {
throw new MongoCryptInvalidArgumentError(
'`cryptSharedLibRequired` set but no crypt_shared library loaded'
);
}
// Only instantiate mongocryptd manager/client once we know for sure
// that we are not using the CSFLE shared library.
if (!this._bypassMongocryptdAndCryptShared && !this.cryptSharedLibVersionInfo) {
this._mongocryptdManager = new MongocryptdManager(options.extraOptions);
const clientOptions: MongoClientOptions = {
serverSelectionTimeoutMS: 10000
};
if (
(options.extraOptions == null || typeof options.extraOptions.mongocryptdURI !== 'string') &&
!net.getDefaultAutoSelectFamily
) {
// Only set family if autoSelectFamily options are not supported.
clientOptions.family = 4;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: TS complains as this always returns true on versions where it is present.
if (net.getDefaultAutoSelectFamily) {
// AutoEncrypter is made inside of MongoClient constructor while options are being parsed,
// we do not have access to the options that are in progress.
// TODO(NODE-6449): AutoEncrypter does not use client options for autoSelectFamily
Object.assign(clientOptions, autoSelectSocketOptions(this._client.s?.options ?? {}));
}
this._mongocryptdClient = new MongoClient(this._mongocryptdManager.uri, clientOptions);
}
}
/**
* Initializes the auto encrypter by spawning a mongocryptd and connecting to it.
*
* This function is a no-op when bypassSpawn is set or the crypt shared library is used.
*/
async init(): Promise<MongoClient | void> {
if (this._bypassMongocryptdAndCryptShared || this.cryptSharedLibVersionInfo) {
return;
}
if (!this._mongocryptdManager) {
throw new MongoRuntimeError(
'Reached impossible state: mongocryptdManager is undefined when neither bypassSpawn nor the shared lib are specified.'
);
}
if (!this._mongocryptdClient) {
throw new MongoRuntimeError(
'Reached impossible state: mongocryptdClient is undefined when neither bypassSpawn nor the shared lib are specified.'
);
}
if (!this._mongocryptdManager.bypassSpawn) {
await this._mongocryptdManager.spawn();
}
try {
const client = await this._mongocryptdClient.connect();
return client;
} catch (error) {
throw new MongoRuntimeError(
'Unable to connect to `mongocryptd`, please make sure it is running or in your PATH for auto-spawn',
{ cause: error }
);
}
}
/**
* Cleans up the `_mongocryptdClient`, if present.
*/
async teardown(force: boolean): Promise<void> {
await this._mongocryptdClient?.close(force);
}
/**
* Encrypt a command for a given namespace.
*/
async encrypt(
ns: string,
cmd: Document,
options: CommandOptions & Abortable = {}
): Promise<Document | Uint8Array> {
options.signal?.throwIfAborted();
if (this._bypassEncryption) {
// If `bypassAutoEncryption` has been specified, don't encrypt
return cmd;
}
const commandBuffer = Buffer.isBuffer(cmd) ? cmd : serialize(cmd, options);
const context = this._mongocrypt.makeEncryptionContext(
MongoDBCollectionNamespace.fromString(ns).db,
commandBuffer
);
context.id = this._contextCounter++;
context.ns = ns;
context.document = cmd;
const stateMachine = new StateMachine({
promoteValues: false,
promoteLongs: false,
proxyOptions: this._proxyOptions,
tlsOptions: this._tlsOptions,
socketOptions: autoSelectSocketOptions(this._client.s.options)
});
return deserialize(await stateMachine.execute(this, context, options), {
promoteValues: false,
promoteLongs: false
});
}
/**
* Decrypt a command response
*/
async decrypt(
response: Uint8Array,
options: CommandOptions & Abortable = {}
): Promise<Uint8Array> {
options.signal?.throwIfAborted();
const context = this._mongocrypt.makeDecryptionContext(response);
context.id = this._contextCounter++;
const stateMachine = new StateMachine({
...options,
proxyOptions: this._proxyOptions,
tlsOptions: this._tlsOptions,
socketOptions: autoSelectSocketOptions(this._client.s.options)
});
return await stateMachine.execute(this, context, options);
}
/**
* Ask the user for KMS credentials.
*
* This returns anything that looks like the kmsProviders original input
* option. It can be empty, and any provider specified here will override
* the original ones.
*/
async askForKMSCredentials(): Promise<KMSProviders> {
return await refreshKMSCredentials(this._kmsProviders, this._credentialProviders);
}
/**
* Return the current libmongocrypt's CSFLE shared library version
* as `{ version: bigint, versionStr: string }`, or `null` if no CSFLE
* shared library was loaded.
*/
get cryptSharedLibVersionInfo(): { version: bigint; versionStr: string } | null {
return this._mongocrypt.cryptSharedLibVersionInfo;
}
static get libmongocryptVersion(): string {
return AutoEncrypter.getMongoCrypt().libmongocryptVersion;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,87 @@
import * as crypto from 'crypto';
type AES256Callback = (key: Buffer, iv: Buffer, input: Buffer, output: Buffer) => number | Error;
export function makeAES256Hook(
method: 'createCipheriv' | 'createDecipheriv',
mode: 'aes-256-cbc' | 'aes-256-ctr'
): AES256Callback {
return function (key: Buffer, iv: Buffer, input: Buffer, output: Buffer): number | Error {
let result;
try {
const cipher = crypto[method](mode, key, iv);
cipher.setAutoPadding(false);
result = cipher.update(input);
const final = cipher.final();
if (final.length > 0) {
result = Buffer.concat([result, final]);
}
} catch (e) {
return e;
}
result.copy(output);
return result.length;
};
}
export function randomHook(buffer: Buffer, count: number): number | Error {
try {
crypto.randomFillSync(buffer, 0, count);
} catch (e) {
return e;
}
return count;
}
export function sha256Hook(input: Buffer, output: Buffer): number | Error {
let result;
try {
result = crypto.createHash('sha256').update(input).digest();
} catch (e) {
return e;
}
result.copy(output);
return result.length;
}
type HMACHook = (key: Buffer, input: Buffer, output: Buffer) => number | Error;
export function makeHmacHook(algorithm: 'sha512' | 'sha256'): HMACHook {
return (key: Buffer, input: Buffer, output: Buffer): number | Error => {
let result;
try {
result = crypto.createHmac(algorithm, key).update(input).digest();
} catch (e) {
return e;
}
result.copy(output);
return result.length;
};
}
export function signRsaSha256Hook(key: Buffer, input: Buffer, output: Buffer): number | Error {
let result;
try {
const signer = crypto.createSign('sha256WithRSAEncryption');
const privateKey = Buffer.from(
`-----BEGIN PRIVATE KEY-----\n${key.toString('base64')}\n-----END PRIVATE KEY-----\n`
);
result = signer.update(input).end().sign(privateKey);
} catch (e) {
return e;
}
result.copy(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');
export const aes256CtrDecryptHook = makeAES256Hook('createDecipheriv', 'aes-256-ctr');
export const hmacSha512Hook = makeHmacHook('sha512');
export const hmacSha256Hook = makeHmacHook('sha256');

View file

@ -0,0 +1,141 @@
import { type Document } from '../bson';
import { MongoError } from '../error';
/**
* @public
* An error indicating that something went wrong specifically with MongoDB Client Encryption
*/
export class MongoCryptError extends MongoError {
/**
* **Do not use this constructor!**
*
* Meant for internal use only.
*
* @remarks
* This class is only meant to be constructed within the driver. This constructor is
* not subject to semantic versioning compatibility guarantees and may change at any time.
*
* @public
**/
constructor(message: string, options: { cause?: Error } = {}) {
super(message, options);
}
override get name() {
return 'MongoCryptError';
}
}
/**
* @public
*
* An error indicating an invalid argument was provided to an encryption API.
*/
export class MongoCryptInvalidArgumentError extends MongoCryptError {
/**
* **Do not use this constructor!**
*
* Meant for internal use only.
*
* @remarks
* This class is only meant to be constructed within the driver. This constructor is
* not subject to semantic versioning compatibility guarantees and may change at any time.
*
* @public
**/
constructor(message: string) {
super(message);
}
override get name() {
return 'MongoCryptInvalidArgumentError';
}
}
/**
* @public
* An error indicating that `ClientEncryption.createEncryptedCollection()` failed to create data keys
*/
export class MongoCryptCreateDataKeyError extends MongoCryptError {
encryptedFields: Document;
/**
* **Do not use this constructor!**
*
* Meant for internal use only.
*
* @remarks
* This class is only meant to be constructed within the driver. This constructor is
* not subject to semantic versioning compatibility guarantees and may change at any time.
*
* @public
**/
constructor(encryptedFields: Document, { cause }: { cause: Error }) {
super(`Unable to complete creating data keys: ${cause.message}`, { cause });
this.encryptedFields = encryptedFields;
}
override get name() {
return 'MongoCryptCreateDataKeyError';
}
}
/**
* @public
* An error indicating that `ClientEncryption.createEncryptedCollection()` failed to create a collection
*/
export class MongoCryptCreateEncryptedCollectionError extends MongoCryptError {
encryptedFields: Document;
/**
* **Do not use this constructor!**
*
* Meant for internal use only.
*
* @remarks
* This class is only meant to be constructed within the driver. This constructor is
* not subject to semantic versioning compatibility guarantees and may change at any time.
*
* @public
**/
constructor(encryptedFields: Document, { cause }: { cause: Error }) {
super(`Unable to create collection: ${cause.message}`, { cause });
this.encryptedFields = encryptedFields;
}
override get name() {
return 'MongoCryptCreateEncryptedCollectionError';
}
}
/**
* @public
* An error indicating that mongodb-client-encryption failed to auto-refresh Azure KMS credentials.
*/
export class MongoCryptAzureKMSRequestError extends MongoCryptError {
/** The body of the http response that failed, if present. */
body?: Document;
/**
* **Do not use this constructor!**
*
* Meant for internal use only.
*
* @remarks
* This class is only meant to be constructed within the driver. This constructor is
* not subject to semantic versioning compatibility guarantees and may change at any time.
*
* @public
**/
constructor(message: string, body?: Document) {
super(message);
this.body = body;
}
override get name(): string {
return 'MongoCryptAzureKMSRequestError';
}
}
/** @public */
export class MongoCryptKMSRequestNetworkTimeoutError extends MongoCryptError {
override get name(): string {
return 'MongoCryptKMSRequestNetworkTimeoutError';
}
}

View file

@ -0,0 +1,100 @@
import type { ChildProcess } from 'child_process';
import { MongoNetworkTimeoutError } from '../error';
import { type AutoEncryptionExtraOptions } from './auto_encrypter';
/**
* @internal
* An internal class that handles spawning a mongocryptd.
*/
export class MongocryptdManager {
static DEFAULT_MONGOCRYPTD_URI = 'mongodb://localhost:27020';
uri: string;
bypassSpawn: boolean;
spawnPath = '';
spawnArgs: Array<string> = [];
_child?: ChildProcess;
constructor(extraOptions: AutoEncryptionExtraOptions = {}) {
this.uri =
typeof extraOptions.mongocryptdURI === 'string' && extraOptions.mongocryptdURI.length > 0
? extraOptions.mongocryptdURI
: MongocryptdManager.DEFAULT_MONGOCRYPTD_URI;
this.bypassSpawn = !!extraOptions.mongocryptdBypassSpawn;
if (Object.hasOwn(extraOptions, 'mongocryptdSpawnPath') && extraOptions.mongocryptdSpawnPath) {
this.spawnPath = extraOptions.mongocryptdSpawnPath;
}
if (
Object.hasOwn(extraOptions, 'mongocryptdSpawnArgs') &&
Array.isArray(extraOptions.mongocryptdSpawnArgs)
) {
this.spawnArgs = this.spawnArgs.concat(extraOptions.mongocryptdSpawnArgs);
}
if (
this.spawnArgs
.filter(arg => typeof arg === 'string')
.every(arg => arg.indexOf('--idleShutdownTimeoutSecs') < 0)
) {
this.spawnArgs.push('--idleShutdownTimeoutSecs', '60');
}
}
/**
* Will check to see if a mongocryptd is up. If it is not up, it will attempt
* to spawn a mongocryptd in a detached process, and then wait for it to be up.
*/
async spawn(): Promise<void> {
const cmdName = this.spawnPath || 'mongocryptd';
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { spawn } = require('child_process') as typeof import('child_process');
// Spawned with stdio: ignore and detached: true
// to ensure child can outlive parent.
this._child = spawn(cmdName, this.spawnArgs, {
stdio: 'ignore',
detached: true
});
this._child.on('error', () => {
// From the FLE spec:
// "The stdout and stderr of the spawned process MUST not be exposed in the driver
// (e.g. redirect to /dev/null). Users can pass the argument --logpath to
// extraOptions.mongocryptdSpawnArgs if they need to inspect mongocryptd logs.
// If spawning is necessary, the driver MUST spawn mongocryptd whenever server
// selection on the MongoClient to mongocryptd fails. If the MongoClient fails to
// connect after spawning, the server selection error is propagated to the user."
// The AutoEncrypter and MongoCryptdManager should work together to spawn
// mongocryptd whenever necessary. Additionally, the `mongocryptd` intentionally
// shuts down after 60s and gets respawned when necessary. We rely on server
// selection timeouts when connecting to the `mongocryptd` to inform users that something
// has been configured incorrectly. For those reasons, we suppress stderr from
// the `mongocryptd` process and immediately unref the process.
});
// unref child to remove handle from event loop
this._child.unref();
}
/**
* @returns the result of `fn` or rejects with an error.
*/
async withRespawn<T>(fn: () => Promise<T>): ReturnType<typeof fn> {
try {
const result = await fn();
return result;
} catch (err) {
// If we are not bypassing spawning, then we should retry once on a MongoTimeoutError (server selection error)
const shouldSpawn = err instanceof MongoNetworkTimeoutError && !this.bypassSpawn;
if (!shouldSpawn) {
throw err;
}
}
await this.spawn();
const result = await fn();
return result;
}
}

View file

@ -0,0 +1,33 @@
import {
type AWSCredentialProvider,
AWSSDKCredentialProvider
} from '../../cmap/auth/aws_temporary_credentials';
import { type KMSProviders } from '.';
/**
* @internal
*/
export async function loadAWSCredentials(
kmsProviders: KMSProviders,
provider?: AWSCredentialProvider
): Promise<KMSProviders> {
const credentialProvider = new AWSSDKCredentialProvider(provider);
// We shouldn't ever receive a response from the AWS SDK that doesn't have a `SecretAccessKey`
// or `AccessKeyId`. However, TS says these fields are optional. We provide empty strings
// and let libmongocrypt error if we're unable to fetch the required keys.
const {
SecretAccessKey = '',
AccessKeyId = '',
Token
} = await credentialProvider.getCredentials();
const aws: NonNullable<KMSProviders['aws']> = {
secretAccessKey: SecretAccessKey,
accessKeyId: AccessKeyId
};
// the AWS session token is only required for temporary credentials so only attach it to the
// result if it's present in the response from the aws sdk
Token != null && (aws.sessionToken = Token);
return { ...kmsProviders, aws };
}

View file

@ -0,0 +1,181 @@
import { type Document } from '../../bson';
import { MongoNetworkTimeoutError } from '../../error';
import { get } from '../../utils';
import { MongoCryptAzureKMSRequestError } from '../errors';
import { type KMSProviders } from './index';
const MINIMUM_TOKEN_REFRESH_IN_MILLISECONDS = 6000;
/** Base URL for getting Azure tokens. */
export const AZURE_BASE_URL = 'http://169.254.169.254/metadata/identity/oauth2/token?';
/**
* 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 {
cachedToken: AzureTokenCacheEntry | null = null;
async getToken(): Promise<AccessToken> {
if (this.cachedToken == null || this.needsRefresh(this.cachedToken)) {
this.cachedToken = await this._getToken();
}
return { accessToken: this.cachedToken.accessToken };
}
needsRefresh(token: AzureTokenCacheEntry): boolean {
const timeUntilExpirationMS = token.expiresOnTimestamp - Date.now();
return timeUntilExpirationMS <= MINIMUM_TOKEN_REFRESH_IN_MILLISECONDS;
}
/**
* exposed for testing
*/
resetCache() {
this.cachedToken = null;
}
/**
* exposed for testing
*/
_getToken(): Promise<AzureTokenCacheEntry> {
return fetchAzureKMSToken();
}
}
/** @internal */
export const tokenCache = new AzureCredentialCache();
/** @internal */
async function parseResponse(response: {
body: string;
status?: number;
}): Promise<AzureTokenCacheEntry> {
const { status, body: rawBody } = response;
const body: { expires_in?: number; access_token?: string } = (() => {
try {
return JSON.parse(rawBody);
} catch {
throw new MongoCryptAzureKMSRequestError('Malformed JSON body in GET request.');
}
})();
if (status !== 200) {
throw new MongoCryptAzureKMSRequestError('Unable to complete request.', body);
}
if (!body.access_token) {
throw new MongoCryptAzureKMSRequestError(
'Malformed response body - missing field `access_token`.'
);
}
if (!body.expires_in) {
throw new MongoCryptAzureKMSRequestError(
'Malformed response body - missing field `expires_in`.'
);
}
const expiresInMS = Number(body.expires_in) * 1000;
if (Number.isNaN(expiresInMS)) {
throw new MongoCryptAzureKMSRequestError(
'Malformed response body - unable to parse int from `expires_in` field.'
);
}
return {
accessToken: body.access_token,
expiresOnTimestamp: Date.now() + expiresInMS
};
}
/**
* @internal
*
* exposed for CSFLE
* [prose test 18](https://github.com/mongodb/specifications/tree/master/source/client-side-encryption/tests#azure-imds-credentials)
*/
export interface AzureKMSRequestOptions {
headers?: Document;
url?: URL | string;
}
/**
* @internal
* Get the Azure endpoint URL.
*/
export function addAzureParams(url: URL, resource: string, username?: string): URL {
url.searchParams.append('api-version', '2018-02-01');
url.searchParams.append('resource', resource);
if (username) {
url.searchParams.append('client_id', username);
}
return url;
}
/**
* @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 = new URL(options.url?.toString() ?? AZURE_BASE_URL);
addAzureParams(url, 'https://vault.azure.net');
const headers = { ...options.headers, 'Content-Type': 'application/json', Metadata: true };
return { headers, url };
}
/**
* @internal
*
* `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.
*
* 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 = {}
): Promise<AzureTokenCacheEntry> {
const { headers, url } = prepareRequest(options);
try {
const response = await get(url, { headers });
return await parseResponse(response);
} catch (error) {
if (error instanceof MongoNetworkTimeoutError) {
throw new MongoCryptAzureKMSRequestError(`[Azure KMS] ${error.message}`);
}
throw error;
}
}
/**
* @internal
*
* @throws Will reject with a `MongoCryptError` if the http request fails or the http response is malformed.
*/
export async function loadAzureCredentials(kmsProviders: KMSProviders): Promise<KMSProviders> {
const azure = await tokenCache.getToken();
return { ...kmsProviders, azure };
}

View file

@ -0,0 +1,16 @@
import { getGcpMetadata } from '../../deps';
import { type KMSProviders } from '.';
/** @internal */
export async function loadGCPCredentials(kmsProviders: KMSProviders): Promise<KMSProviders> {
const gcpMetadata = getGcpMetadata();
if ('kModuleError' in gcpMetadata) {
return kmsProviders;
}
const { access_token: accessToken } = await gcpMetadata.instance<{ access_token: string }>({
property: 'service-accounts/default/token'
});
return { ...kmsProviders, gcp: { accessToken } };
}

View file

@ -0,0 +1,207 @@
import type { Binary } from '../../bson';
import { type AWSCredentialProvider } from '../../cmap/auth/aws_temporary_credentials';
import { loadAWSCredentials } from './aws';
import { loadAzureCredentials } from './azure';
import { loadGCPCredentials } from './gcp';
/**
* @public
*
* A data key provider. Allowed values:
*
* - aws, gcp, local, kmip or azure
* - (`mongodb-client-encryption>=6.0.1` only) a named key, in the form of:
* `aws:<name>`, `gcp:<name>`, `local:<name>`, `kmip:<name>`, `azure:<name>`
* where `name` is an alphanumeric string, underscores allowed.
*/
export type ClientEncryptionDataKeyProvider = keyof KMSProviders;
/** @public */
export interface AWSKMSProviderConfiguration {
/**
* 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;
}
/** @public */
export interface LocalKMSProviderConfiguration {
/**
* The master key used to encrypt/decrypt data keys.
* A 96-byte long Buffer or base64 encoded string.
*/
key: Binary | Uint8Array | string;
}
/** @public */
export interface KMIPKMSProviderConfiguration {
/**
* 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;
}
/** @public */
export type AzureKMSProviderConfiguration =
| {
/**
* 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;
};
/** @public */
export type GCPKMSProviderConfiguration =
| {
/**
* 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;
};
/**
* @public
* Configuration options for custom credential providers for KMS requests.
*/
export interface CredentialProviders {
/* A custom AWS credential provider */
aws?: AWSCredentialProvider;
}
/**
* @public
* Configuration options that are used by specific KMS providers during key generation, encryption, and decryption.
*
* Named KMS providers _are not supported_ for automatic KMS credential fetching.
*/
export interface KMSProviders {
/**
* Configuration options for using 'aws' as your KMS provider
*/
aws?: AWSKMSProviderConfiguration | Record<string, never>;
[key: `aws:${string}`]: AWSKMSProviderConfiguration;
/**
* Configuration options for using 'local' as your KMS provider
*/
local?: LocalKMSProviderConfiguration;
[key: `local:${string}`]: LocalKMSProviderConfiguration;
/**
* Configuration options for using 'kmip' as your KMS provider
*/
kmip?: KMIPKMSProviderConfiguration;
[key: `kmip:${string}`]: KMIPKMSProviderConfiguration;
/**
* Configuration options for using 'azure' as your KMS provider
*/
azure?: AzureKMSProviderConfiguration | Record<string, never>;
[key: `azure:${string}`]: AzureKMSProviderConfiguration;
/**
* Configuration options for using 'gcp' as your KMS provider
*/
gcp?: GCPKMSProviderConfiguration | Record<string, never>;
[key: `gcp:${string}`]: GCPKMSProviderConfiguration;
}
/**
* 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.
*
* @internal - exposed for testing purposes only
*/
export function isEmptyCredentials(
providerName: ClientEncryptionDataKeyProvider,
kmsProviders: KMSProviders
): boolean {
const provider = kmsProviders[providerName];
if (provider == null) {
return false;
}
return typeof provider === 'object' && Object.keys(provider).length === 0;
}
/**
* Load cloud provider credentials for the user provided KMS providers.
* Credentials will only attempt to get loaded if they do not exist
* and no existing credentials will get overwritten.
*
* @internal
*/
export async function refreshKMSCredentials(
kmsProviders: KMSProviders,
credentialProviders?: CredentialProviders
): Promise<KMSProviders> {
let finalKMSProviders = kmsProviders;
if (isEmptyCredentials('aws', kmsProviders)) {
finalKMSProviders = await loadAWSCredentials(finalKMSProviders, credentialProviders?.aws);
}
if (isEmptyCredentials('gcp', kmsProviders)) {
finalKMSProviders = await loadGCPCredentials(finalKMSProviders);
}
if (isEmptyCredentials('azure', kmsProviders)) {
finalKMSProviders = await loadAzureCredentials(finalKMSProviders);
}
return finalKMSProviders;
}

View file

@ -0,0 +1,648 @@
import * as fs from 'fs/promises';
import { type MongoCryptContext, type MongoCryptKMSRequest } from 'mongodb-client-encryption';
import * as net from 'net';
import * as tls from 'tls';
import {
type BSONSerializeOptions,
deserialize,
type Document,
pluckBSONSerializeOptions,
serialize
} from '../bson';
import { type ProxyOptions } from '../cmap/connection';
import { CursorTimeoutContext } from '../cursor/abstract_cursor';
import { getSocks, type SocksLib } from '../deps';
import { MongoOperationTimeoutError } from '../error';
import { type MongoClient, type MongoClientOptions } from '../mongo_client';
import { type Abortable } from '../mongo_types';
import { type CollectionInfo } from '../operations/list_collections';
import { Timeout, type TimeoutContext, TimeoutError } from '../timeout';
import {
addAbortListener,
BufferPool,
kDispose,
MongoDBCollectionNamespace,
promiseWithResolvers
} from '../utils';
import { autoSelectSocketOptions, type DataKey } from './client_encryption';
import { MongoCryptError } from './errors';
import { type MongocryptdManager } from './mongocryptd_manager';
import { type KMSProviders } from './providers';
let socks: SocksLib | null = null;
function loadSocks(): SocksLib {
if (socks == null) {
const socksImport = getSocks();
if ('kModuleError' in socksImport) {
throw socksImport.kModuleError;
}
socks = socksImport;
}
return socks;
}
// libmongocrypt states
const MONGOCRYPT_CTX_ERROR = 0;
const MONGOCRYPT_CTX_NEED_MONGO_COLLINFO = 1;
const MONGOCRYPT_CTX_NEED_MONGO_MARKINGS = 2;
const MONGOCRYPT_CTX_NEED_MONGO_KEYS = 3;
const MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS = 7;
const MONGOCRYPT_CTX_NEED_KMS = 4;
const MONGOCRYPT_CTX_READY = 5;
const MONGOCRYPT_CTX_DONE = 6;
const HTTPS_PORT = 443;
const stateToString = new Map([
[MONGOCRYPT_CTX_ERROR, 'MONGOCRYPT_CTX_ERROR'],
[MONGOCRYPT_CTX_NEED_MONGO_COLLINFO, 'MONGOCRYPT_CTX_NEED_MONGO_COLLINFO'],
[MONGOCRYPT_CTX_NEED_MONGO_MARKINGS, 'MONGOCRYPT_CTX_NEED_MONGO_MARKINGS'],
[MONGOCRYPT_CTX_NEED_MONGO_KEYS, 'MONGOCRYPT_CTX_NEED_MONGO_KEYS'],
[MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS, 'MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS'],
[MONGOCRYPT_CTX_NEED_KMS, 'MONGOCRYPT_CTX_NEED_KMS'],
[MONGOCRYPT_CTX_READY, 'MONGOCRYPT_CTX_READY'],
[MONGOCRYPT_CTX_DONE, 'MONGOCRYPT_CTX_DONE']
]);
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'
];
/**
* Helper function for logging. Enabled by setting the environment flag MONGODB_CRYPT_DEBUG.
* @param msg - Anything you want to be logged.
*/
function debug(msg: unknown) {
if (process.env.MONGODB_CRYPT_DEBUG) {
// eslint-disable-next-line no-console
console.error(msg);
}
}
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;
}
}
/**
* @public
*
* TLS options to use when connecting. The spec specifically calls out which insecure
* tls options are not allowed:
*
* - tlsAllowInvalidCertificates
* - tlsAllowInvalidHostnames
* - tlsInsecure
*
* These options are not included in the type, and are ignored if provided.
*/
export type ClientEncryptionTlsOptions = Pick<
MongoClientOptions,
'tlsCAFile' | 'tlsCertificateKeyFile' | 'tlsCertificateKeyFilePassword'
>;
/** @public */
export type CSFLEKMSTlsOptions = {
aws?: ClientEncryptionTlsOptions;
gcp?: ClientEncryptionTlsOptions;
kmip?: ClientEncryptionTlsOptions;
local?: ClientEncryptionTlsOptions;
azure?: ClientEncryptionTlsOptions;
[key: string]: ClientEncryptionTlsOptions | undefined;
};
/**
* @public
*
* Socket options to use for KMS requests.
*/
export type ClientEncryptionSocketOptions = Pick<
MongoClientOptions,
'autoSelectFamily' | 'autoSelectFamilyAttemptTimeout'
>;
/**
* This is kind of a hack. For `rewrapManyDataKey`, we have tests that
* guarantee that when there are no matching keys, `rewrapManyDataKey` returns
* nothing. We also have tests for auto encryption that guarantee for `encrypt`
* we return an error when there are no matching keys. This error is generated in
* subsequent iterations of the state machine.
* Some apis (`encrypt`) throw if there are no filter matches and others (`rewrapManyDataKey`)
* do not. We set the result manually here, and let the state machine continue. `libmongocrypt`
* will inform us if we need to error by setting the state to `MONGOCRYPT_CTX_ERROR` but
* otherwise we'll return `{ v: [] }`.
*/
let EMPTY_V;
/**
* @internal
*
* An interface representing an object that can be passed to the `StateMachine.execute` method.
*
* Not all properties are required for all operations.
*/
export interface StateMachineExecutable {
_keyVaultNamespace: string;
_keyVaultClient: MongoClient;
askForKMSCredentials: () => Promise<KMSProviders>;
/** only used for auto encryption */
_metaDataClient?: MongoClient;
/** only used for auto encryption */
_mongocryptdClient?: MongoClient;
/** only used for auto encryption */
_mongocryptdManager?: MongocryptdManager;
}
export type StateMachineOptions = {
/** socks5 proxy options, if set. */
proxyOptions: ProxyOptions;
/** TLS options for KMS requests, if set. */
tlsOptions: CSFLEKMSTlsOptions;
/** Socket specific options we support. */
socketOptions: ClientEncryptionSocketOptions;
} & Pick<BSONSerializeOptions, 'promoteLongs' | 'promoteValues'>;
/**
* @internal
* An internal class that executes across a MongoCryptContext until either
* a finishing state or an error is reached. Do not instantiate directly.
*/
// TODO(DRIVERS-2671): clarify CSOT behavior for FLE APIs
export class StateMachine {
constructor(
private options: StateMachineOptions,
private bsonOptions = pluckBSONSerializeOptions(options)
) {}
/**
* Executes the state machine according to the specification
*/
async execute(
executor: StateMachineExecutable,
context: MongoCryptContext,
options: { timeoutContext?: TimeoutContext } & Abortable
): Promise<Uint8Array> {
const keyVaultNamespace = executor._keyVaultNamespace;
const keyVaultClient = executor._keyVaultClient;
const metaDataClient = executor._metaDataClient;
const mongocryptdClient = executor._mongocryptdClient;
const mongocryptdManager = executor._mongocryptdManager;
let result: Uint8Array | null = null;
// Typescript treats getters just like properties: Once you've tested it for equality
// it cannot change. Which is exactly the opposite of what we use state and status for.
// Every call to at least `addMongoOperationResponse` and `finalize` can change the state.
// These wrappers let us write code more naturally and not add compiler exceptions
// to conditions checks inside the state machine.
const getStatus = () => context.status;
const getState = () => context.state;
while (getState() !== MONGOCRYPT_CTX_DONE && getState() !== MONGOCRYPT_CTX_ERROR) {
options.signal?.throwIfAborted();
debug(`[context#${context.id}] ${stateToString.get(getState()) || getState()}`);
switch (getState()) {
case MONGOCRYPT_CTX_NEED_MONGO_COLLINFO: {
const filter = deserialize(context.nextMongoOperation());
if (!metaDataClient) {
throw new MongoCryptError(
'unreachable state machine state: entered MONGOCRYPT_CTX_NEED_MONGO_COLLINFO but metadata client is undefined'
);
}
const collInfoCursor = this.fetchCollectionInfo(
metaDataClient,
context.ns,
filter,
options
);
for await (const collInfo of collInfoCursor) {
context.addMongoOperationResponse(serialize(collInfo));
if (getState() === MONGOCRYPT_CTX_ERROR) break;
}
if (getState() === MONGOCRYPT_CTX_ERROR) break;
context.finishMongoOperation();
break;
}
case MONGOCRYPT_CTX_NEED_MONGO_MARKINGS: {
const command = context.nextMongoOperation();
if (getState() === MONGOCRYPT_CTX_ERROR) break;
if (!mongocryptdClient) {
throw new MongoCryptError(
'unreachable state machine state: entered MONGOCRYPT_CTX_NEED_MONGO_MARKINGS but mongocryptdClient is undefined'
);
}
// When we are using the shared library, we don't have a mongocryptd manager.
const markedCommand: Uint8Array = mongocryptdManager
? await mongocryptdManager.withRespawn(
this.markCommand.bind(this, mongocryptdClient, context.ns, command, options)
)
: await this.markCommand(mongocryptdClient, context.ns, command, options);
context.addMongoOperationResponse(markedCommand);
context.finishMongoOperation();
break;
}
case MONGOCRYPT_CTX_NEED_MONGO_KEYS: {
const filter = context.nextMongoOperation();
const keys = await this.fetchKeys(keyVaultClient, keyVaultNamespace, filter, options);
if (keys.length === 0) {
// See docs on EMPTY_V
result = EMPTY_V ??= serialize({ v: [] });
}
for await (const key of keys) {
context.addMongoOperationResponse(serialize(key));
}
context.finishMongoOperation();
break;
}
case MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS: {
const kmsProviders = await executor.askForKMSCredentials();
context.provideKMSProviders(serialize(kmsProviders));
break;
}
case MONGOCRYPT_CTX_NEED_KMS: {
await Promise.all(this.requests(context, options));
context.finishKMSRequests();
break;
}
case MONGOCRYPT_CTX_READY: {
const finalizedContext = context.finalize();
if (getState() === MONGOCRYPT_CTX_ERROR) {
const message = getStatus().message || 'Finalization error';
throw new MongoCryptError(message);
}
result = finalizedContext;
break;
}
default:
throw new MongoCryptError(`Unknown state: ${getState()}`);
}
}
if (getState() === MONGOCRYPT_CTX_ERROR || result == null) {
const message = getStatus().message;
if (!message) {
debug(
`unidentifiable error in MongoCrypt - received an error status from \`libmongocrypt\` but received no error message.`
);
}
throw new MongoCryptError(
message ??
'unidentifiable error in MongoCrypt - received an error status from `libmongocrypt` but received no error message.'
);
}
return result;
}
/**
* 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 A promise that resolves when the KMS reply has be fully parsed
*/
async kmsRequest(
request: MongoCryptKMSRequest,
options?: { timeoutContext?: TimeoutContext } & Abortable
): Promise<void> {
const parsedUrl = request.endpoint.split(':');
const port = parsedUrl[1] != null ? Number.parseInt(parsedUrl[1], 10) : HTTPS_PORT;
const socketOptions: tls.ConnectionOptions & {
host: string;
port: number;
autoSelectFamily?: boolean;
autoSelectFamilyAttemptTimeout?: number;
} = {
host: parsedUrl[0],
servername: parsedUrl[0],
port,
...autoSelectSocketOptions(this.options.socketOptions || {})
};
const message = request.message;
const buffer = new BufferPool();
let netSocket: net.Socket;
let socket: tls.TLSSocket;
function destroySockets() {
for (const sock of [socket, netSocket]) {
if (sock) {
sock.destroy();
}
}
}
function onerror(cause: Error) {
return new MongoCryptError('KMS request failed', { cause });
}
function onclose() {
return new MongoCryptError('KMS request closed');
}
const tlsOptions = this.options.tlsOptions;
if (tlsOptions) {
const kmsProvider = request.kmsProvider;
const providerTlsOptions = tlsOptions[kmsProvider];
if (providerTlsOptions) {
const error = this.validateTlsOptions(kmsProvider, providerTlsOptions);
if (error) {
throw error;
}
try {
await this.setTlsOptions(providerTlsOptions, socketOptions);
} catch (err) {
throw onerror(err);
}
}
}
let abortListener;
try {
if (this.options.proxyOptions && this.options.proxyOptions.proxyHost) {
netSocket = new net.Socket();
const {
promise: willConnect,
reject: rejectOnNetSocketError,
resolve: resolveOnNetSocketConnect
} = promiseWithResolvers<void>();
netSocket
.once('error', err => rejectOnNetSocketError(onerror(err)))
.once('close', () => rejectOnNetSocketError(onclose()))
.once('connect', () => resolveOnNetSocketConnect());
const netSocketOptions = {
...socketOptions,
host: this.options.proxyOptions.proxyHost,
port: this.options.proxyOptions.proxyPort || 1080
};
netSocket.connect(netSocketOptions);
await willConnect;
try {
socks ??= loadSocks();
socketOptions.socket = (
await socks.SocksClient.createConnection({
existing_socket: netSocket,
command: 'connect',
destination: { host: socketOptions.host, port: socketOptions.port },
proxy: {
// host and port are ignored because we pass existing_socket
host: 'iLoveJavaScript',
port: 0,
type: 5,
userId: this.options.proxyOptions.proxyUsername,
password: this.options.proxyOptions.proxyPassword
}
})
).socket;
} catch (err) {
throw onerror(err);
}
}
socket = tls.connect(socketOptions, () => {
socket.write(message);
});
const {
promise: willResolveKmsRequest,
reject: rejectOnTlsSocketError,
resolve
} = promiseWithResolvers<void>();
abortListener = addAbortListener(options?.signal, function () {
destroySockets();
rejectOnTlsSocketError(this.reason);
});
socket
.once('error', err => rejectOnTlsSocketError(onerror(err)))
.once('close', () => rejectOnTlsSocketError(onclose()))
.on('data', data => {
buffer.append(data);
while (request.bytesNeeded > 0 && buffer.length) {
const bytesNeeded = Math.min(request.bytesNeeded, buffer.length);
request.addResponse(buffer.read(bytesNeeded));
}
if (request.bytesNeeded <= 0) {
resolve();
}
});
await (options?.timeoutContext?.csotEnabled()
? Promise.all([
willResolveKmsRequest,
Timeout.expires(options.timeoutContext?.remainingTimeMS)
])
: willResolveKmsRequest);
} catch (error) {
if (error instanceof TimeoutError)
throw new MongoOperationTimeoutError('KMS request timed out');
throw error;
} finally {
// There's no need for any more activity on this socket at this point.
destroySockets();
abortListener?.[kDispose]();
}
}
*requests(context: MongoCryptContext, options?: { timeoutContext?: TimeoutContext } & Abortable) {
for (
let request = context.nextKMSRequest();
request != null;
request = context.nextKMSRequest()
) {
yield this.kmsRequest(request, options);
}
}
/**
* Validates the provided TLS options are secure.
*
* @param kmsProvider - The KMS provider name.
* @param tlsOptions - The client TLS options for the provider.
*
* @returns An error if any option is invalid.
*/
validateTlsOptions(
kmsProvider: string,
tlsOptions: ClientEncryptionTlsOptions
): MongoCryptError | void {
const tlsOptionNames = Object.keys(tlsOptions);
for (const option of INSECURE_TLS_OPTIONS) {
if (tlsOptionNames.includes(option)) {
return new MongoCryptError(`Insecure TLS options prohibited for ${kmsProvider}: ${option}`);
}
}
}
/**
* Sets only the valid secure TLS options.
*
* @param tlsOptions - The client TLS options for the provider.
* @param options - The existing connection options.
*/
async setTlsOptions(
tlsOptions: ClientEncryptionTlsOptions,
options: tls.ConnectionOptions
): Promise<void> {
if (tlsOptions.tlsCertificateKeyFile) {
const cert = await fs.readFile(tlsOptions.tlsCertificateKeyFile);
options.cert = options.key = cert;
}
if (tlsOptions.tlsCAFile) {
options.ca = await fs.readFile(tlsOptions.tlsCAFile);
}
if (tlsOptions.tlsCertificateKeyFilePassword) {
options.passphrase = tlsOptions.tlsCertificateKeyFilePassword;
}
}
/**
* 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 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: MongoClient,
ns: string,
filter: Document,
options?: { timeoutContext?: TimeoutContext } & Abortable
): AsyncIterable<CollectionInfo> {
const { db } = MongoDBCollectionNamespace.fromString(ns);
const cursor = client.db(db).listCollections(filter, {
promoteLongs: false,
promoteValues: false,
timeoutContext:
options?.timeoutContext && new CursorTimeoutContext(options?.timeoutContext, Symbol()),
signal: options?.signal,
nameOnly: false
});
return cursor;
}
/**
* Calls to the mongocryptd to provide markings for a command.
* Exposed for testing purposes. Do not directly invoke.
* @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
*/
async markCommand(
client: MongoClient,
ns: string,
command: Uint8Array,
options?: { timeoutContext?: TimeoutContext } & Abortable
): Promise<Uint8Array> {
const { db } = MongoDBCollectionNamespace.fromString(ns);
const bsonOptions = { promoteLongs: false, promoteValues: false };
const rawCommand = deserialize(command, bsonOptions);
const commandOptions: {
timeoutMS?: number;
signal?: AbortSignal;
} = {
timeoutMS: undefined,
signal: undefined
};
if (options?.timeoutContext?.csotEnabled()) {
commandOptions.timeoutMS = options.timeoutContext.remainingTimeMS;
}
if (options?.signal) {
commandOptions.signal = options.signal;
}
const response = await client.db(db).command(rawCommand, {
...bsonOptions,
...commandOptions
});
return serialize(response, this.bsonOptions);
}
/**
* Requests keys from the keyVault collection on the topology.
* Exposed for testing purposes. Do not directly invoke.
* @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: MongoClient,
keyVaultNamespace: string,
filter: Uint8Array,
options?: { timeoutContext?: TimeoutContext } & Abortable
): Promise<Array<DataKey>> {
const { db: dbName, collection: collectionName } =
MongoDBCollectionNamespace.fromString(keyVaultNamespace);
const commandOptions: {
timeoutContext?: CursorTimeoutContext;
signal?: AbortSignal;
} = {
timeoutContext: undefined,
signal: undefined
};
if (options?.timeoutContext != null) {
commandOptions.timeoutContext = new CursorTimeoutContext(options.timeoutContext, Symbol());
}
if (options?.signal != null) {
commandOptions.signal = options.signal;
}
return client
.db(dbName)
.collection<DataKey>(collectionName, { readConcern: { level: 'majority' } })
.find(deserialize(filter), commandOptions)
.toArray();
}
}