This commit is contained in:
PLBXNebulia-Formation 2025-11-21 09:23:11 +01:00
commit d1c8cae2c1
1417 changed files with 326736 additions and 0 deletions

View file

@ -0,0 +1,272 @@
"use strict";
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AutoEncrypter = exports.AutoEncryptionLoggerLevel = void 0;
const net = require("net");
const bson_1 = require("../bson");
const constants_1 = require("../constants");
const deps_1 = require("../deps");
const error_1 = require("../error");
const mongo_client_1 = require("../mongo_client");
const utils_1 = require("../utils");
const client_encryption_1 = require("./client_encryption");
const errors_1 = require("./errors");
const mongocryptd_manager_1 = require("./mongocryptd_manager");
const providers_1 = require("./providers");
const state_machine_1 = require("./state_machine");
/** @public */
exports.AutoEncryptionLoggerLevel = Object.freeze({
FatalError: 0,
Error: 1,
Warning: 2,
Info: 3,
Trace: 4
});
/**
* @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.
*/
class AutoEncrypter {
static { _a = constants_1.kDecorateResult; }
/** @internal */
static getMongoCrypt() {
const encryption = (0, deps_1.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, options) {
/**
* 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.
*/
this[_a] = false;
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 && !(0, providers_1.isEmptyCredentials)('aws', this._kmsProviders)) {
throw new errors_1.MongoCryptInvalidArgumentError('Can only provide a custom AWS credential provider when the state machine is configured for automatic AWS credential fetching');
}
const mongoCryptOptions = {
errorWrapper: errors_1.defaultErrorWrapper
};
if (options.schemaMap) {
mongoCryptOptions.schemaMap = Buffer.isBuffer(options.schemaMap)
? options.schemaMap
: (0, bson_1.serialize)(options.schemaMap);
}
if (options.encryptedFieldsMap) {
mongoCryptOptions.encryptedFieldsMap = Buffer.isBuffer(options.encryptedFieldsMap)
? options.encryptedFieldsMap
: (0, bson_1.serialize)(options.encryptedFieldsMap);
}
mongoCryptOptions.kmsProviders = !Buffer.isBuffer(this._kmsProviders)
? (0, bson_1.serialize)(this._kmsProviders)
: 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;
}
if (options.keyExpirationMS != null) {
mongoCryptOptions.keyExpirationMS = options.keyExpirationMS;
}
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 errors_1.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 mongocryptd_manager_1.MongocryptdManager(options.extraOptions);
const clientOptions = {
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, (0, client_encryption_1.autoSelectSocketOptions)(this._client.s?.options ?? {}));
}
this._mongocryptdClient = new mongo_client_1.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() {
if (this._bypassMongocryptdAndCryptShared || this.cryptSharedLibVersionInfo) {
return;
}
if (!this._mongocryptdManager) {
throw new error_1.MongoRuntimeError('Reached impossible state: mongocryptdManager is undefined when neither bypassSpawn nor the shared lib are specified.');
}
if (!this._mongocryptdClient) {
throw new error_1.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 error_1.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 close() {
await this._mongocryptdClient?.close();
}
/**
* Encrypt a command for a given namespace.
*/
async encrypt(ns, cmd, options = {}) {
options.signal?.throwIfAborted();
if (this._bypassEncryption) {
// If `bypassAutoEncryption` has been specified, don't encrypt
return cmd;
}
const commandBuffer = Buffer.isBuffer(cmd) ? cmd : (0, bson_1.serialize)(cmd, options);
const context = this._mongocrypt.makeEncryptionContext(utils_1.MongoDBCollectionNamespace.fromString(ns).db, commandBuffer);
context.id = this._contextCounter++;
context.ns = ns;
context.document = cmd;
const stateMachine = new state_machine_1.StateMachine({
promoteValues: false,
promoteLongs: false,
proxyOptions: this._proxyOptions,
tlsOptions: this._tlsOptions,
socketOptions: (0, client_encryption_1.autoSelectSocketOptions)(this._client.s.options)
});
return (0, bson_1.deserialize)(await stateMachine.execute(this, context, options), {
promoteValues: false,
promoteLongs: false
});
}
/**
* Decrypt a command response
*/
async decrypt(response, options = {}) {
options.signal?.throwIfAborted();
const context = this._mongocrypt.makeDecryptionContext(response);
context.id = this._contextCounter++;
const stateMachine = new state_machine_1.StateMachine({
...options,
proxyOptions: this._proxyOptions,
tlsOptions: this._tlsOptions,
socketOptions: (0, client_encryption_1.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() {
return await (0, providers_1.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() {
return this._mongocrypt.cryptSharedLibVersionInfo;
}
static get libmongocryptVersion() {
return AutoEncrypter.getMongoCrypt().libmongocryptVersion;
}
}
exports.AutoEncrypter = AutoEncrypter;
//# sourceMappingURL=auto_encrypter.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,609 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ClientEncryption = void 0;
exports.autoSelectSocketOptions = autoSelectSocketOptions;
const bson_1 = require("../bson");
const deps_1 = require("../deps");
const timeout_1 = require("../timeout");
const utils_1 = require("../utils");
const errors_1 = require("./errors");
const index_1 = require("./providers/index");
const state_machine_1 = require("./state_machine");
/**
* @public
* The public interface for explicit in-use encryption
*/
class ClientEncryption {
/** @internal */
static getMongoCrypt() {
const encryption = (0, deps_1.getMongoDBClientEncryption)();
if ('kModuleError' in encryption) {
throw encryption.kModuleError;
}
return encryption.MongoCrypt;
}
/**
* Create a new encryption instance
*
* @example
* ```ts
* new ClientEncryption(mongoClient, {
* keyVaultNamespace: 'client.encryption',
* kmsProviders: {
* local: {
* key: masterKey // The master key used for encryption/decryption. A 96-byte long Buffer
* }
* }
* });
* ```
*
* @example
* ```ts
* new ClientEncryption(mongoClient, {
* keyVaultNamespace: 'client.encryption',
* kmsProviders: {
* aws: {
* accessKeyId: AWS_ACCESS_KEY,
* secretAccessKey: AWS_SECRET_KEY
* }
* }
* });
* ```
*/
constructor(client, options) {
this._client = client;
this._proxyOptions = options.proxyOptions ?? {};
this._tlsOptions = options.tlsOptions ?? {};
this._kmsProviders = options.kmsProviders || {};
const { timeoutMS } = (0, utils_1.resolveTimeoutOptions)(client, options);
this._timeoutMS = timeoutMS;
this._credentialProviders = options.credentialProviders;
if (options.credentialProviders?.aws && !(0, index_1.isEmptyCredentials)('aws', this._kmsProviders)) {
throw new errors_1.MongoCryptInvalidArgumentError('Can only provide a custom AWS credential provider when the state machine is configured for automatic AWS credential fetching');
}
if (options.keyVaultNamespace == null) {
throw new errors_1.MongoCryptInvalidArgumentError('Missing required option `keyVaultNamespace`');
}
const mongoCryptOptions = {
...options,
kmsProviders: !Buffer.isBuffer(this._kmsProviders)
? (0, bson_1.serialize)(this._kmsProviders)
: this._kmsProviders,
errorWrapper: errors_1.defaultErrorWrapper
};
this._keyVaultNamespace = options.keyVaultNamespace;
this._keyVaultClient = options.keyVaultClient || client;
const MongoCrypt = ClientEncryption.getMongoCrypt();
this._mongoCrypt = new MongoCrypt(mongoCryptOptions);
}
/**
* Creates a data key used for explicit encryption and inserts it into the key vault namespace
*
* @example
* ```ts
* // Using async/await to create a local key
* const dataKeyId = await clientEncryption.createDataKey('local');
* ```
*
* @example
* ```ts
* // Using async/await to create an aws key
* const dataKeyId = await clientEncryption.createDataKey('aws', {
* masterKey: {
* region: 'us-east-1',
* key: 'xxxxxxxxxxxxxx' // CMK ARN here
* }
* });
* ```
*
* @example
* ```ts
* // Using async/await to create an aws key with a keyAltName
* const dataKeyId = await clientEncryption.createDataKey('aws', {
* masterKey: {
* region: 'us-east-1',
* key: 'xxxxxxxxxxxxxx' // CMK ARN here
* },
* keyAltNames: [ 'mySpecialKey' ]
* });
* ```
*/
async createDataKey(provider, options = {}) {
if (options.keyAltNames && !Array.isArray(options.keyAltNames)) {
throw new errors_1.MongoCryptInvalidArgumentError(`Option "keyAltNames" must be an array of strings, but was of type ${typeof options.keyAltNames}.`);
}
let keyAltNames = undefined;
if (options.keyAltNames && options.keyAltNames.length > 0) {
keyAltNames = options.keyAltNames.map((keyAltName, i) => {
if (typeof keyAltName !== 'string') {
throw new errors_1.MongoCryptInvalidArgumentError(`Option "keyAltNames" must be an array of strings, but item at index ${i} was of type ${typeof keyAltName}`);
}
return (0, bson_1.serialize)({ keyAltName });
});
}
let keyMaterial = undefined;
if (options.keyMaterial) {
keyMaterial = (0, bson_1.serialize)({ keyMaterial: options.keyMaterial });
}
const dataKeyBson = (0, bson_1.serialize)({
provider,
...options.masterKey
});
const context = this._mongoCrypt.makeDataKeyContext(dataKeyBson, {
keyAltNames,
keyMaterial
});
const stateMachine = new state_machine_1.StateMachine({
proxyOptions: this._proxyOptions,
tlsOptions: this._tlsOptions,
socketOptions: autoSelectSocketOptions(this._client.s.options)
});
const timeoutContext = options?.timeoutContext ??
timeout_1.TimeoutContext.create((0, utils_1.resolveTimeoutOptions)(this._client, { timeoutMS: this._timeoutMS }));
const dataKey = (0, bson_1.deserialize)(await stateMachine.execute(this, context, { timeoutContext }));
const { db: dbName, collection: collectionName } = utils_1.MongoDBCollectionNamespace.fromString(this._keyVaultNamespace);
const { insertedId } = await this._keyVaultClient
.db(dbName)
.collection(collectionName)
.insertOne(dataKey, {
writeConcern: { w: 'majority' },
timeoutMS: timeoutContext?.csotEnabled()
? timeoutContext?.getRemainingTimeMSOrThrow()
: undefined
});
return insertedId;
}
/**
* Searches the keyvault for any data keys matching the provided filter. If there are matches, rewrapManyDataKey then attempts to re-wrap the data keys using the provided options.
*
* If no matches are found, then no bulk write is performed.
*
* @example
* ```ts
* // rewrapping all data data keys (using a filter that matches all documents)
* const filter = {};
*
* const result = await clientEncryption.rewrapManyDataKey(filter);
* if (result.bulkWriteResult != null) {
* // keys were re-wrapped, results will be available in the bulkWrite object.
* }
* ```
*
* @example
* ```ts
* // attempting to rewrap all data keys with no matches
* const filter = { _id: new Binary() } // assume _id matches no documents in the database
* const result = await clientEncryption.rewrapManyDataKey(filter);
*
* if (result.bulkWriteResult == null) {
* // no keys matched, `bulkWriteResult` does not exist on the result object
* }
* ```
*/
async rewrapManyDataKey(filter, options) {
let keyEncryptionKeyBson = undefined;
if (options) {
const keyEncryptionKey = Object.assign({ provider: options.provider }, options.masterKey);
keyEncryptionKeyBson = (0, bson_1.serialize)(keyEncryptionKey);
}
const filterBson = (0, bson_1.serialize)(filter);
const context = this._mongoCrypt.makeRewrapManyDataKeyContext(filterBson, keyEncryptionKeyBson);
const stateMachine = new state_machine_1.StateMachine({
proxyOptions: this._proxyOptions,
tlsOptions: this._tlsOptions,
socketOptions: autoSelectSocketOptions(this._client.s.options)
});
const timeoutContext = timeout_1.TimeoutContext.create((0, utils_1.resolveTimeoutOptions)(this._client, { timeoutMS: this._timeoutMS }));
const { v: dataKeys } = (0, bson_1.deserialize)(await stateMachine.execute(this, context, { timeoutContext }));
if (dataKeys.length === 0) {
return {};
}
const { db: dbName, collection: collectionName } = utils_1.MongoDBCollectionNamespace.fromString(this._keyVaultNamespace);
const replacements = dataKeys.map((key) => ({
updateOne: {
filter: { _id: key._id },
update: {
$set: {
masterKey: key.masterKey,
keyMaterial: key.keyMaterial
},
$currentDate: {
updateDate: true
}
}
}
}));
const result = await this._keyVaultClient
.db(dbName)
.collection(collectionName)
.bulkWrite(replacements, {
writeConcern: { w: 'majority' },
timeoutMS: timeoutContext.csotEnabled() ? timeoutContext?.remainingTimeMS : undefined
});
return { bulkWriteResult: result };
}
/**
* Deletes the key with the provided id from the keyvault, if it exists.
*
* @example
* ```ts
* // delete a key by _id
* const id = new Binary(); // id is a bson binary subtype 4 object
* const { deletedCount } = await clientEncryption.deleteKey(id);
*
* if (deletedCount != null && deletedCount > 0) {
* // successful deletion
* }
* ```
*
*/
async deleteKey(_id) {
const { db: dbName, collection: collectionName } = utils_1.MongoDBCollectionNamespace.fromString(this._keyVaultNamespace);
return await this._keyVaultClient
.db(dbName)
.collection(collectionName)
.deleteOne({ _id }, { writeConcern: { w: 'majority' }, timeoutMS: this._timeoutMS });
}
/**
* Finds all the keys currently stored in the keyvault.
*
* This method will not throw.
*
* @returns a FindCursor over all keys in the keyvault.
* @example
* ```ts
* // fetching all keys
* const keys = await clientEncryption.getKeys().toArray();
* ```
*/
getKeys() {
const { db: dbName, collection: collectionName } = utils_1.MongoDBCollectionNamespace.fromString(this._keyVaultNamespace);
return this._keyVaultClient
.db(dbName)
.collection(collectionName)
.find({}, { readConcern: { level: 'majority' }, timeoutMS: this._timeoutMS });
}
/**
* Finds a key in the keyvault with the specified _id.
*
* Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents
* match the id. The promise rejects with an error if an error is thrown.
* @example
* ```ts
* // getting a key by id
* const id = new Binary(); // id is a bson binary subtype 4 object
* const key = await clientEncryption.getKey(id);
* if (!key) {
* // key is null if there was no matching key
* }
* ```
*/
async getKey(_id) {
const { db: dbName, collection: collectionName } = utils_1.MongoDBCollectionNamespace.fromString(this._keyVaultNamespace);
return await this._keyVaultClient
.db(dbName)
.collection(collectionName)
.findOne({ _id }, { readConcern: { level: 'majority' }, timeoutMS: this._timeoutMS });
}
/**
* Finds a key in the keyvault which has the specified keyAltName.
*
* @param keyAltName - a keyAltName to search for a key
* @returns Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents
* match the keyAltName. The promise rejects with an error if an error is thrown.
* @example
* ```ts
* // get a key by alt name
* const keyAltName = 'keyAltName';
* const key = await clientEncryption.getKeyByAltName(keyAltName);
* if (!key) {
* // key is null if there is no matching key
* }
* ```
*/
async getKeyByAltName(keyAltName) {
const { db: dbName, collection: collectionName } = utils_1.MongoDBCollectionNamespace.fromString(this._keyVaultNamespace);
return await this._keyVaultClient
.db(dbName)
.collection(collectionName)
.findOne({ keyAltNames: keyAltName }, { readConcern: { level: 'majority' }, timeoutMS: this._timeoutMS });
}
/**
* Adds a keyAltName to a key identified by the provided _id.
*
* This method resolves to/returns the *old* key value (prior to adding the new altKeyName).
*
* @param _id - The id of the document to update.
* @param keyAltName - a keyAltName to search for a key
* @returns Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents
* match the id. The promise rejects with an error if an error is thrown.
* @example
* ```ts
* // adding an keyAltName to a data key
* const id = new Binary(); // id is a bson binary subtype 4 object
* const keyAltName = 'keyAltName';
* const oldKey = await clientEncryption.addKeyAltName(id, keyAltName);
* if (!oldKey) {
* // null is returned if there is no matching document with an id matching the supplied id
* }
* ```
*/
async addKeyAltName(_id, keyAltName) {
const { db: dbName, collection: collectionName } = utils_1.MongoDBCollectionNamespace.fromString(this._keyVaultNamespace);
const value = await this._keyVaultClient
.db(dbName)
.collection(collectionName)
.findOneAndUpdate({ _id }, { $addToSet: { keyAltNames: keyAltName } }, { writeConcern: { w: 'majority' }, returnDocument: 'before', timeoutMS: this._timeoutMS });
return value;
}
/**
* Adds a keyAltName to a key identified by the provided _id.
*
* This method resolves to/returns the *old* key value (prior to removing the new altKeyName).
*
* If the removed keyAltName is the last keyAltName for that key, the `altKeyNames` property is unset from the document.
*
* @param _id - The id of the document to update.
* @param keyAltName - a keyAltName to search for a key
* @returns Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents
* match the id. The promise rejects with an error if an error is thrown.
* @example
* ```ts
* // removing a key alt name from a data key
* const id = new Binary(); // id is a bson binary subtype 4 object
* const keyAltName = 'keyAltName';
* const oldKey = await clientEncryption.removeKeyAltName(id, keyAltName);
*
* if (!oldKey) {
* // null is returned if there is no matching document with an id matching the supplied id
* }
* ```
*/
async removeKeyAltName(_id, keyAltName) {
const { db: dbName, collection: collectionName } = utils_1.MongoDBCollectionNamespace.fromString(this._keyVaultNamespace);
const pipeline = [
{
$set: {
keyAltNames: {
$cond: [
{
$eq: ['$keyAltNames', [keyAltName]]
},
'$$REMOVE',
{
$filter: {
input: '$keyAltNames',
cond: {
$ne: ['$$this', keyAltName]
}
}
}
]
}
}
}
];
const value = await this._keyVaultClient
.db(dbName)
.collection(collectionName)
.findOneAndUpdate({ _id }, pipeline, {
writeConcern: { w: 'majority' },
returnDocument: 'before',
timeoutMS: this._timeoutMS
});
return value;
}
/**
* A convenience method for creating an encrypted collection.
* This method will create data keys for any encryptedFields that do not have a `keyId` defined
* and then create a new collection with the full set of encryptedFields.
*
* @param db - A Node.js driver Db object with which to create the collection
* @param name - The name of the collection to be created
* @param options - Options for createDataKey and for createCollection
* @returns created collection and generated encryptedFields
* @throws MongoCryptCreateDataKeyError - If part way through the process a createDataKey invocation fails, an error will be rejected that has the partial `encryptedFields` that were created.
* @throws MongoCryptCreateEncryptedCollectionError - If creating the collection fails, an error will be rejected that has the entire `encryptedFields` that were created.
*/
async createEncryptedCollection(db, name, options) {
const { provider, masterKey, createCollectionOptions: { encryptedFields: { ...encryptedFields }, ...createCollectionOptions } } = options;
const timeoutContext = this._timeoutMS != null
? timeout_1.TimeoutContext.create((0, utils_1.resolveTimeoutOptions)(this._client, { timeoutMS: this._timeoutMS }))
: undefined;
if (Array.isArray(encryptedFields.fields)) {
const createDataKeyPromises = encryptedFields.fields.map(async (field) => field == null || typeof field !== 'object' || field.keyId != null
? field
: {
...field,
keyId: await this.createDataKey(provider, {
masterKey,
// clone the timeoutContext
// in order to avoid sharing the same timeout for server selection and connection checkout across different concurrent operations
timeoutContext: timeoutContext?.csotEnabled() ? timeoutContext?.clone() : undefined
})
});
const createDataKeyResolutions = await Promise.allSettled(createDataKeyPromises);
encryptedFields.fields = createDataKeyResolutions.map((resolution, index) => resolution.status === 'fulfilled' ? resolution.value : encryptedFields.fields[index]);
const rejection = createDataKeyResolutions.find((result) => result.status === 'rejected');
if (rejection != null) {
throw new errors_1.MongoCryptCreateDataKeyError(encryptedFields, { cause: rejection.reason });
}
}
try {
const collection = await db.createCollection(name, {
...createCollectionOptions,
encryptedFields,
timeoutMS: timeoutContext?.csotEnabled()
? timeoutContext?.getRemainingTimeMSOrThrow()
: undefined
});
return { collection, encryptedFields };
}
catch (cause) {
throw new errors_1.MongoCryptCreateEncryptedCollectionError(encryptedFields, { cause });
}
}
/**
* Explicitly encrypt a provided value. Note that either `options.keyId` or `options.keyAltName` must
* be specified. Specifying both `options.keyId` and `options.keyAltName` is considered an error.
*
* @param value - The value that you wish to serialize. Must be of a type that can be serialized into BSON
* @param options -
* @returns a Promise that either resolves with the encrypted value, or rejects with an error.
*
* @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' });
* }
* ```
*/
async encrypt(value, options) {
return await this._encrypt(value, false, options);
}
/**
* Encrypts a Match Expression or Aggregate Expression to query a range index.
*
* Only supported when queryType is "range" and algorithm is "Range".
*
* @param expression - a BSON document of one of the following forms:
* 1. A Match Expression of this form:
* `{$and: [{<field>: {$gt: <value1>}}, {<field>: {$lt: <value2> }}]}`
* 2. An Aggregate Expression of this form:
* `{$and: [{$gt: [<fieldpath>, <value1>]}, {$lt: [<fieldpath>, <value2>]}]}`
*
* `$gt` may also be `$gte`. `$lt` may also be `$lte`.
*
* @param options -
* @returns Returns a Promise that either resolves with the encrypted value or rejects with an error.
*/
async encryptExpression(expression, options) {
return await this._encrypt(expression, true, options);
}
/**
* Explicitly decrypt a provided encrypted value
*
* @param value - An encrypted value
* @returns a Promise that either resolves with the decrypted value, or rejects with an error
*
* @example
* ```ts
* // Decrypting value with async/await API
* async function decryptMyValue(value) {
* return clientEncryption.decrypt(value);
* }
* ```
*/
async decrypt(value) {
const valueBuffer = (0, bson_1.serialize)({ v: value });
const context = this._mongoCrypt.makeExplicitDecryptionContext(valueBuffer);
const stateMachine = new state_machine_1.StateMachine({
proxyOptions: this._proxyOptions,
tlsOptions: this._tlsOptions,
socketOptions: autoSelectSocketOptions(this._client.s.options)
});
const timeoutContext = this._timeoutMS != null
? timeout_1.TimeoutContext.create((0, utils_1.resolveTimeoutOptions)(this._client, { timeoutMS: this._timeoutMS }))
: undefined;
const { v } = (0, bson_1.deserialize)(await stateMachine.execute(this, context, { timeoutContext }));
return v;
}
/**
* @internal
* Ask the user for KMS credentials.
*
* This returns anything that looks like the kmsProviders original input
* option. It can be empty, and any provider specified here will override
* the original ones.
*/
async askForKMSCredentials() {
return await (0, index_1.refreshKMSCredentials)(this._kmsProviders, this._credentialProviders);
}
static get libmongocryptVersion() {
return ClientEncryption.getMongoCrypt().libmongocryptVersion;
}
/**
* @internal
* A helper that perform explicit encryption of values and expressions.
* Explicitly encrypt a provided value. Note that either `options.keyId` or `options.keyAltName` must
* be specified. Specifying both `options.keyId` and `options.keyAltName` is considered an error.
*
* @param value - The value that you wish to encrypt. Must be of a type that can be serialized into BSON
* @param expressionMode - a boolean that indicates whether or not to encrypt the value as an expression
* @param options - options to pass to encrypt
* @returns the raw result of the call to stateMachine.execute(). When expressionMode is set to true, the return
* value will be a bson document. When false, the value will be a BSON Binary.
*
*/
async _encrypt(value, expressionMode, options) {
const { algorithm, keyId, keyAltName, contentionFactor, queryType, rangeOptions, textOptions } = options;
const contextOptions = {
expressionMode,
algorithm
};
if (keyId) {
contextOptions.keyId = keyId.buffer;
}
if (keyAltName) {
if (keyId) {
throw new errors_1.MongoCryptInvalidArgumentError(`"options" cannot contain both "keyId" and "keyAltName"`);
}
if (typeof keyAltName !== 'string') {
throw new errors_1.MongoCryptInvalidArgumentError(`"options.keyAltName" must be of type string, but was of type ${typeof keyAltName}`);
}
contextOptions.keyAltName = (0, bson_1.serialize)({ keyAltName });
}
if (typeof contentionFactor === 'number' || typeof contentionFactor === 'bigint') {
contextOptions.contentionFactor = contentionFactor;
}
if (typeof queryType === 'string') {
contextOptions.queryType = queryType;
}
if (typeof rangeOptions === 'object') {
contextOptions.rangeOptions = (0, bson_1.serialize)(rangeOptions);
}
if (typeof textOptions === 'object') {
contextOptions.textOptions = (0, bson_1.serialize)(textOptions);
}
const valueBuffer = (0, bson_1.serialize)({ v: value });
const stateMachine = new state_machine_1.StateMachine({
proxyOptions: this._proxyOptions,
tlsOptions: this._tlsOptions,
socketOptions: autoSelectSocketOptions(this._client.s.options)
});
const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions);
const timeoutContext = this._timeoutMS != null
? timeout_1.TimeoutContext.create((0, utils_1.resolveTimeoutOptions)(this._client, { timeoutMS: this._timeoutMS }))
: undefined;
const { v } = (0, bson_1.deserialize)(await stateMachine.execute(this, context, { timeoutContext }));
return v;
}
}
exports.ClientEncryption = ClientEncryption;
/**
* Get the socket options from the client.
* @param baseOptions - The mongo client options.
* @returns ClientEncryptionSocketOptions
*/
function autoSelectSocketOptions(baseOptions) {
const options = { autoSelectFamily: true };
if ('autoSelectFamily' in baseOptions) {
options.autoSelectFamily = baseOptions.autoSelectFamily;
}
if ('autoSelectFamilyAttemptTimeout' in baseOptions) {
options.autoSelectFamilyAttemptTimeout = baseOptions.autoSelectFamilyAttemptTimeout;
}
return options;
}
//# sourceMappingURL=client_encryption.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,138 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MongoCryptKMSRequestNetworkTimeoutError = exports.MongoCryptAzureKMSRequestError = exports.MongoCryptCreateEncryptedCollectionError = exports.MongoCryptCreateDataKeyError = exports.MongoCryptInvalidArgumentError = exports.defaultErrorWrapper = exports.MongoCryptError = void 0;
const error_1 = require("../error");
/**
* @public
* An error indicating that something went wrong specifically with MongoDB Client Encryption
*/
class MongoCryptError extends error_1.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, options = {}) {
super(message, options);
}
get name() {
return 'MongoCryptError';
}
}
exports.MongoCryptError = MongoCryptError;
const defaultErrorWrapper = (error) => new MongoCryptError(error.message, { cause: error });
exports.defaultErrorWrapper = defaultErrorWrapper;
/**
* @public
*
* An error indicating an invalid argument was provided to an encryption API.
*/
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) {
super(message);
}
get name() {
return 'MongoCryptInvalidArgumentError';
}
}
exports.MongoCryptInvalidArgumentError = MongoCryptInvalidArgumentError;
/**
* @public
* An error indicating that `ClientEncryption.createEncryptedCollection()` failed to create data keys
*/
class MongoCryptCreateDataKeyError 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(encryptedFields, { cause }) {
super(`Unable to complete creating data keys: ${cause.message}`, { cause });
this.encryptedFields = encryptedFields;
}
get name() {
return 'MongoCryptCreateDataKeyError';
}
}
exports.MongoCryptCreateDataKeyError = MongoCryptCreateDataKeyError;
/**
* @public
* An error indicating that `ClientEncryption.createEncryptedCollection()` failed to create a collection
*/
class MongoCryptCreateEncryptedCollectionError 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(encryptedFields, { cause }) {
super(`Unable to create collection: ${cause.message}`, { cause });
this.encryptedFields = encryptedFields;
}
get name() {
return 'MongoCryptCreateEncryptedCollectionError';
}
}
exports.MongoCryptCreateEncryptedCollectionError = MongoCryptCreateEncryptedCollectionError;
/**
* @public
* An error indicating that mongodb-client-encryption failed to auto-refresh Azure KMS credentials.
*/
class MongoCryptAzureKMSRequestError 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, body) {
super(message);
this.body = body;
}
get name() {
return 'MongoCryptAzureKMSRequestError';
}
}
exports.MongoCryptAzureKMSRequestError = MongoCryptAzureKMSRequestError;
/** @public */
class MongoCryptKMSRequestNetworkTimeoutError extends MongoCryptError {
get name() {
return 'MongoCryptKMSRequestNetworkTimeoutError';
}
}
exports.MongoCryptKMSRequestNetworkTimeoutError = MongoCryptKMSRequestNetworkTimeoutError;
//# sourceMappingURL=errors.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/client-side-encryption/errors.ts"],"names":[],"mappings":";;;AACA,oCAAsC;AAEtC;;;GAGG;AACH,MAAa,eAAgB,SAAQ,kBAAU;IAC7C;;;;;;;;;;QAUI;IACJ,YAAY,OAAe,EAAE,UAA6B,EAAE;QAC1D,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC1B,CAAC;IAED,IAAa,IAAI;QACf,OAAO,iBAAiB,CAAC;IAC3B,CAAC;CACF;AAnBD,0CAmBC;AAEM,MAAM,mBAAmB,GAAG,CAAC,KAAY,EAAE,EAAE,CAClD,IAAI,eAAe,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;AAD1C,QAAA,mBAAmB,uBACuB;AAEvD;;;;GAIG;AACH,MAAa,8BAA+B,SAAQ,eAAe;IACjE;;;;;;;;;;QAUI;IACJ,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;IACjB,CAAC;IAED,IAAa,IAAI;QACf,OAAO,gCAAgC,CAAC;IAC1C,CAAC;CACF;AAnBD,wEAmBC;AACD;;;GAGG;AACH,MAAa,4BAA6B,SAAQ,eAAe;IAE/D;;;;;;;;;;QAUI;IACJ,YAAY,eAAyB,EAAE,EAAE,KAAK,EAAoB;QAChE,KAAK,CAAC,0CAA0C,KAAK,CAAC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5E,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;IACzC,CAAC;IAED,IAAa,IAAI;QACf,OAAO,8BAA8B,CAAC;IACxC,CAAC;CACF;AArBD,oEAqBC;AAED;;;GAGG;AACH,MAAa,wCAAyC,SAAQ,eAAe;IAE3E;;;;;;;;;;QAUI;IACJ,YAAY,eAAyB,EAAE,EAAE,KAAK,EAAoB;QAChE,KAAK,CAAC,gCAAgC,KAAK,CAAC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAClE,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;IACzC,CAAC;IAED,IAAa,IAAI;QACf,OAAO,0CAA0C,CAAC;IACpD,CAAC;CACF;AArBD,4FAqBC;AAED;;;GAGG;AACH,MAAa,8BAA+B,SAAQ,eAAe;IAGjE;;;;;;;;;;QAUI;IACJ,YAAY,OAAe,EAAE,IAAe;QAC1C,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,IAAa,IAAI;QACf,OAAO,gCAAgC,CAAC;IAC1C,CAAC;CACF;AAtBD,wEAsBC;AAED,cAAc;AACd,MAAa,uCAAwC,SAAQ,eAAe;IAC1E,IAAa,IAAI;QACf,OAAO,yCAAyC,CAAC;IACnD,CAAC;CACF;AAJD,0FAIC"}

View file

@ -0,0 +1,85 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MongocryptdManager = void 0;
const error_1 = require("../error");
/**
* @internal
* An internal class that handles spawning a mongocryptd.
*/
class MongocryptdManager {
static { this.DEFAULT_MONGOCRYPTD_URI = 'mongodb://localhost:27020'; }
constructor(extraOptions = {}) {
this.spawnPath = '';
this.spawnArgs = [];
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() {
const cmdName = this.spawnPath || 'mongocryptd';
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { spawn } = require('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(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 error_1.MongoNetworkTimeoutError && !this.bypassSpawn;
if (!shouldSpawn) {
throw err;
}
}
await this.spawn();
const result = await fn();
return result;
}
}
exports.MongocryptdManager = MongocryptdManager;
//# sourceMappingURL=mongocryptd_manager.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"mongocryptd_manager.js","sourceRoot":"","sources":["../../src/client-side-encryption/mongocryptd_manager.ts"],"names":[],"mappings":";;;AAEA,oCAAoD;AAGpD;;;GAGG;AACH,MAAa,kBAAkB;aACtB,4BAAuB,GAAG,2BAA2B,AAA9B,CAA+B;IAQ7D,YAAY,eAA2C,EAAE;QAJzD,cAAS,GAAG,EAAE,CAAC;QACf,cAAS,GAAkB,EAAE,CAAC;QAI5B,IAAI,CAAC,GAAG;YACN,OAAO,YAAY,CAAC,cAAc,KAAK,QAAQ,IAAI,YAAY,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC;gBACvF,CAAC,CAAC,YAAY,CAAC,cAAc;gBAC7B,CAAC,CAAC,kBAAkB,CAAC,uBAAuB,CAAC;QAEjD,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC,YAAY,CAAC,sBAAsB,CAAC;QAEzD,IAAI,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,sBAAsB,CAAC,IAAI,YAAY,CAAC,oBAAoB,EAAE,CAAC;YAC7F,IAAI,CAAC,SAAS,GAAG,YAAY,CAAC,oBAAoB,CAAC;QACrD,CAAC;QACD,IACE,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,sBAAsB,CAAC;YACnD,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,oBAAoB,CAAC,EAChD,CAAC;YACD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,YAAY,CAAC,oBAAoB,CAAC,CAAC;QAC5E,CAAC;QACD,IACE,IAAI,CAAC,SAAS;aACX,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC;aACtC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,2BAA2B,CAAC,GAAG,CAAC,CAAC,EAC7D,CAAC;YACD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,2BAA2B,EAAE,IAAI,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,KAAK;QACT,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,IAAI,aAAa,CAAC;QAEhD,iEAAiE;QACjE,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,eAAe,CAAmC,CAAC;QAE7E,gDAAgD;QAChD,sCAAsC;QACtC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,EAAE;YAC3C,KAAK,EAAE,QAAQ;YACf,QAAQ,EAAE,IAAI;SACf,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YAC3B,qBAAqB;YACrB,kFAAkF;YAClF,yEAAyE;YACzE,8EAA8E;YAC9E,8EAA8E;YAC9E,iFAAiF;YACjF,iFAAiF;YACjF,yEAAyE;YACzE,iFAAiF;YACjF,6EAA6E;YAC7E,yFAAyF;YACzF,+EAA+E;YAC/E,+DAA+D;QACjE,CAAC,CAAC,CAAC;QAEH,+CAA+C;QAC/C,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,WAAW,CAAI,EAAoB;QACvC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,EAAE,EAAE,CAAC;YAC1B,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,8GAA8G;YAC9G,MAAM,WAAW,GAAG,GAAG,YAAY,gCAAwB,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC;YACjF,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,MAAM,GAAG,CAAC;YACZ,CAAC;QACH,CAAC;QACD,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;QACnB,MAAM,MAAM,GAAG,MAAM,EAAE,EAAE,CAAC;QAC1B,OAAO,MAAM,CAAC;IAChB,CAAC;;AAzFH,gDA0FC"}

View file

@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadAWSCredentials = loadAWSCredentials;
const aws_temporary_credentials_1 = require("../../cmap/auth/aws_temporary_credentials");
/**
* @internal
*/
async function loadAWSCredentials(kmsProviders, provider) {
const credentialProvider = new aws_temporary_credentials_1.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 = {
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 };
}
//# sourceMappingURL=aws.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"aws.js","sourceRoot":"","sources":["../../../src/client-side-encryption/providers/aws.ts"],"names":[],"mappings":";;AASA,gDAuBC;AAhCD,yFAGmD;AAGnD;;GAEG;AACI,KAAK,UAAU,kBAAkB,CACtC,YAA0B,EAC1B,QAAgC;IAEhC,MAAM,kBAAkB,GAAG,IAAI,oDAAwB,CAAC,QAAQ,CAAC,CAAC;IAElE,8FAA8F;IAC9F,2FAA2F;IAC3F,0EAA0E;IAC1E,MAAM,EACJ,eAAe,GAAG,EAAE,EACpB,WAAW,GAAG,EAAE,EAChB,KAAK,EACN,GAAG,MAAM,kBAAkB,CAAC,cAAc,EAAE,CAAC;IAC9C,MAAM,GAAG,GAAqC;QAC5C,eAAe,EAAE,eAAe;QAChC,WAAW,EAAE,WAAW;KACzB,CAAC;IACF,4FAA4F;IAC5F,0DAA0D;IAC1D,KAAK,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,YAAY,GAAG,KAAK,CAAC,CAAC;IAE5C,OAAO,EAAE,GAAG,YAAY,EAAE,GAAG,EAAE,CAAC;AAClC,CAAC"}

View file

@ -0,0 +1,132 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.tokenCache = exports.AzureCredentialCache = exports.AZURE_BASE_URL = void 0;
exports.addAzureParams = addAzureParams;
exports.prepareRequest = prepareRequest;
exports.fetchAzureKMSToken = fetchAzureKMSToken;
exports.loadAzureCredentials = loadAzureCredentials;
const error_1 = require("../../error");
const utils_1 = require("../../utils");
const errors_1 = require("../errors");
const MINIMUM_TOKEN_REFRESH_IN_MILLISECONDS = 6000;
/** Base URL for getting Azure tokens. */
exports.AZURE_BASE_URL = 'http://169.254.169.254/metadata/identity/oauth2/token?';
/**
* @internal
*/
class AzureCredentialCache {
constructor() {
this.cachedToken = null;
}
async getToken() {
if (this.cachedToken == null || this.needsRefresh(this.cachedToken)) {
this.cachedToken = await this._getToken();
}
return { accessToken: this.cachedToken.accessToken };
}
needsRefresh(token) {
const timeUntilExpirationMS = token.expiresOnTimestamp - Date.now();
return timeUntilExpirationMS <= MINIMUM_TOKEN_REFRESH_IN_MILLISECONDS;
}
/**
* exposed for testing
*/
resetCache() {
this.cachedToken = null;
}
/**
* exposed for testing
*/
_getToken() {
return fetchAzureKMSToken();
}
}
exports.AzureCredentialCache = AzureCredentialCache;
/** @internal */
exports.tokenCache = new AzureCredentialCache();
/** @internal */
async function parseResponse(response) {
const { status, body: rawBody } = response;
const body = (() => {
try {
return JSON.parse(rawBody);
}
catch {
throw new errors_1.MongoCryptAzureKMSRequestError('Malformed JSON body in GET request.');
}
})();
if (status !== 200) {
throw new errors_1.MongoCryptAzureKMSRequestError('Unable to complete request.', body);
}
if (!body.access_token) {
throw new errors_1.MongoCryptAzureKMSRequestError('Malformed response body - missing field `access_token`.');
}
if (!body.expires_in) {
throw new errors_1.MongoCryptAzureKMSRequestError('Malformed response body - missing field `expires_in`.');
}
const expiresInMS = Number(body.expires_in) * 1000;
if (Number.isNaN(expiresInMS)) {
throw new errors_1.MongoCryptAzureKMSRequestError('Malformed response body - unable to parse int from `expires_in` field.');
}
return {
accessToken: body.access_token,
expiresOnTimestamp: Date.now() + expiresInMS
};
}
/**
* @internal
* Get the Azure endpoint URL.
*/
function addAzureParams(url, resource, username) {
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.
*/
function prepareRequest(options) {
const url = new URL(options.url?.toString() ?? exports.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)
*/
async function fetchAzureKMSToken(options = {}) {
const { headers, url } = prepareRequest(options);
try {
const response = await (0, utils_1.get)(url, { headers });
return await parseResponse(response);
}
catch (error) {
if (error instanceof error_1.MongoNetworkTimeoutError) {
throw new errors_1.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.
*/
async function loadAzureCredentials(kmsProviders) {
const azure = await exports.tokenCache.getToken();
return { ...kmsProviders, azure };
}
//# sourceMappingURL=azure.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"azure.js","sourceRoot":"","sources":["../../../src/client-side-encryption/providers/azure.ts"],"names":[],"mappings":"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"}

View file

@ -0,0 +1,16 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadGCPCredentials = loadGCPCredentials;
const deps_1 = require("../../deps");
/** @internal */
async function loadGCPCredentials(kmsProviders) {
const gcpMetadata = (0, deps_1.getGcpMetadata)();
if ('kModuleError' in gcpMetadata) {
return kmsProviders;
}
const { access_token: accessToken } = await gcpMetadata.instance({
property: 'service-accounts/default/token'
});
return { ...kmsProviders, gcp: { accessToken } };
}
//# sourceMappingURL=gcp.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"gcp.js","sourceRoot":"","sources":["../../../src/client-side-encryption/providers/gcp.ts"],"names":[],"mappings":";;AAIA,gDAWC;AAfD,qCAA4C;AAG5C,gBAAgB;AACT,KAAK,UAAU,kBAAkB,CAAC,YAA0B;IACjE,MAAM,WAAW,GAAG,IAAA,qBAAc,GAAE,CAAC;IAErC,IAAI,cAAc,IAAI,WAAW,EAAE,CAAC;QAClC,OAAO,YAAY,CAAC;IACtB,CAAC;IAED,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,GAAG,MAAM,WAAW,CAAC,QAAQ,CAA2B;QACzF,QAAQ,EAAE,gCAAgC;KAC3C,CAAC,CAAC;IACH,OAAO,EAAE,GAAG,YAAY,EAAE,GAAG,EAAE,EAAE,WAAW,EAAE,EAAE,CAAC;AACnD,CAAC"}

View file

@ -0,0 +1,43 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isEmptyCredentials = isEmptyCredentials;
exports.refreshKMSCredentials = refreshKMSCredentials;
const aws_1 = require("./aws");
const azure_1 = require("./azure");
const gcp_1 = require("./gcp");
/**
* 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
*/
function isEmptyCredentials(providerName, kmsProviders) {
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
*/
async function refreshKMSCredentials(kmsProviders, credentialProviders) {
let finalKMSProviders = kmsProviders;
if (isEmptyCredentials('aws', kmsProviders)) {
finalKMSProviders = await (0, aws_1.loadAWSCredentials)(finalKMSProviders, credentialProviders?.aws);
}
if (isEmptyCredentials('gcp', kmsProviders)) {
finalKMSProviders = await (0, gcp_1.loadGCPCredentials)(finalKMSProviders);
}
if (isEmptyCredentials('azure', kmsProviders)) {
finalKMSProviders = await (0, azure_1.loadAzureCredentials)(finalKMSProviders);
}
return finalKMSProviders;
}
//# sourceMappingURL=index.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/client-side-encryption/providers/index.ts"],"names":[],"mappings":";;AA0KA,gDASC;AASD,sDAkBC;AA5MD,+BAA2C;AAC3C,mCAA+C;AAC/C,+BAA2C;AA8J3C;;;;;;;GAOG;AACH,SAAgB,kBAAkB,CAChC,YAA6C,EAC7C,YAA0B;IAE1B,MAAM,QAAQ,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;IAC5C,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;QACrB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,OAAO,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC;AAC5E,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,qBAAqB,CACzC,YAA0B,EAC1B,mBAAyC;IAEzC,IAAI,iBAAiB,GAAG,YAAY,CAAC;IAErC,IAAI,kBAAkB,CAAC,KAAK,EAAE,YAAY,CAAC,EAAE,CAAC;QAC5C,iBAAiB,GAAG,MAAM,IAAA,wBAAkB,EAAC,iBAAiB,EAAE,mBAAmB,EAAE,GAAG,CAAC,CAAC;IAC5F,CAAC;IAED,IAAI,kBAAkB,CAAC,KAAK,EAAE,YAAY,CAAC,EAAE,CAAC;QAC5C,iBAAiB,GAAG,MAAM,IAAA,wBAAkB,EAAC,iBAAiB,CAAC,CAAC;IAClE,CAAC;IAED,IAAI,kBAAkB,CAAC,OAAO,EAAE,YAAY,CAAC,EAAE,CAAC;QAC9C,iBAAiB,GAAG,MAAM,IAAA,4BAAoB,EAAC,iBAAiB,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,iBAAiB,CAAC;AAC3B,CAAC"}

View file

@ -0,0 +1,426 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.StateMachine = void 0;
const fs = require("fs/promises");
const net = require("net");
const tls = require("tls");
const bson_1 = require("../bson");
const abstract_cursor_1 = require("../cursor/abstract_cursor");
const deps_1 = require("../deps");
const error_1 = require("../error");
const timeout_1 = require("../timeout");
const utils_1 = require("../utils");
const client_encryption_1 = require("./client_encryption");
const errors_1 = require("./errors");
let socks = null;
function loadSocks() {
if (socks == null) {
const socksImport = (0, deps_1.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'
];
/**
* Helper function for logging. Enabled by setting the environment flag MONGODB_CRYPT_DEBUG.
* @param msg - Anything you want to be logged.
*/
function debug(msg) {
if (process.env.MONGODB_CRYPT_DEBUG) {
// eslint-disable-next-line no-console
console.error(msg);
}
}
/**
* 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 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
class StateMachine {
constructor(options, bsonOptions = (0, bson_1.pluckBSONSerializeOptions)(options)) {
this.options = options;
this.bsonOptions = bsonOptions;
}
/**
* Executes the state machine according to the specification
*/
async execute(executor, context, options) {
const keyVaultNamespace = executor._keyVaultNamespace;
const keyVaultClient = executor._keyVaultClient;
const metaDataClient = executor._metaDataClient;
const mongocryptdClient = executor._mongocryptdClient;
const mongocryptdManager = executor._mongocryptdManager;
let result = 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 = (0, bson_1.deserialize)(context.nextMongoOperation());
if (!metaDataClient) {
throw new errors_1.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((0, bson_1.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 errors_1.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 = 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 ??= (0, bson_1.serialize)({ v: [] });
}
for (const key of keys) {
context.addMongoOperationResponse((0, bson_1.serialize)(key));
}
context.finishMongoOperation();
break;
}
case MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS: {
const kmsProviders = await executor.askForKMSCredentials();
context.provideKMSProviders((0, bson_1.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 errors_1.MongoCryptError(message);
}
result = finalizedContext;
break;
}
default:
throw new errors_1.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 errors_1.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, options) {
const parsedUrl = request.endpoint.split(':');
const port = parsedUrl[1] != null ? Number.parseInt(parsedUrl[1], 10) : HTTPS_PORT;
const socketOptions = {
host: parsedUrl[0],
servername: parsedUrl[0],
port,
...(0, client_encryption_1.autoSelectSocketOptions)(this.options.socketOptions || {})
};
const message = request.message;
const buffer = new utils_1.BufferPool();
let netSocket;
let socket;
function destroySockets() {
for (const sock of [socket, netSocket]) {
if (sock) {
sock.destroy();
}
}
}
function onerror(cause) {
return new errors_1.MongoCryptError('KMS request failed', { cause });
}
function onclose() {
return new errors_1.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 } = (0, utils_1.promiseWithResolvers)();
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 } = (0, utils_1.promiseWithResolvers)();
abortListener = (0, utils_1.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_1.Timeout.expires(options.timeoutContext?.remainingTimeMS)
])
: willResolveKmsRequest);
}
catch (error) {
if (error instanceof timeout_1.TimeoutError)
throw new error_1.MongoOperationTimeoutError('KMS request timed out');
throw error;
}
finally {
// There's no need for any more activity on this socket at this point.
destroySockets();
abortListener?.[utils_1.kDispose]();
}
}
*requests(context, options) {
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, tlsOptions) {
const tlsOptionNames = Object.keys(tlsOptions);
for (const option of INSECURE_TLS_OPTIONS) {
if (tlsOptionNames.includes(option)) {
return new errors_1.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, options) {
// If a secureContext is provided, ensure it is set.
if (tlsOptions.secureContext) {
options.secureContext = tlsOptions.secureContext;
}
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, ns, filter, options) {
const { db } = utils_1.MongoDBCollectionNamespace.fromString(ns);
const cursor = client.db(db).listCollections(filter, {
promoteLongs: false,
promoteValues: false,
timeoutContext: options?.timeoutContext && new abstract_cursor_1.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, ns, command, options) {
const { db } = utils_1.MongoDBCollectionNamespace.fromString(ns);
const bsonOptions = { promoteLongs: false, promoteValues: false };
const rawCommand = (0, bson_1.deserialize)(command, bsonOptions);
const commandOptions = {
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 (0, bson_1.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, keyVaultNamespace, filter, options) {
const { db: dbName, collection: collectionName } = utils_1.MongoDBCollectionNamespace.fromString(keyVaultNamespace);
const commandOptions = {
timeoutContext: undefined,
signal: undefined
};
if (options?.timeoutContext != null) {
commandOptions.timeoutContext = new abstract_cursor_1.CursorTimeoutContext(options.timeoutContext, Symbol());
}
if (options?.signal != null) {
commandOptions.signal = options.signal;
}
return client
.db(dbName)
.collection(collectionName, { readConcern: { level: 'majority' } })
.find((0, bson_1.deserialize)(filter), commandOptions)
.toArray();
}
}
exports.StateMachine = StateMachine;
//# sourceMappingURL=state_machine.js.map

File diff suppressed because one or more lines are too long