'use strict'; module.exports = function (modules) { const mc = require('bindings')('mongocrypt'); const common = require('./common'); const databaseNamespace = common.databaseNamespace; const StateMachine = modules.stateMachine.StateMachine; const MongocryptdManager = require('./mongocryptdManager').MongocryptdManager; const MongoClient = modules.mongodb.MongoClient; const MongoError = modules.mongodb.MongoError; const BSON = modules.mongodb.BSON; const { loadCredentials } = require('./credentialsProvider'); const cryptoCallbacks = require('./cryptoCallbacks'); /** * Configuration options for a automatic client encryption. * * @typedef {Object} AutoEncrypter~AutoEncryptionOptions * @property {MongoClient} [keyVaultClient] A `MongoClient` used to fetch keys from a key vault * @property {string} [keyVaultNamespace] The namespace where keys are stored in the key vault * @property {KMSProviders} [kmsProviders] Configuration options that are used by specific KMS providers during key generation, encryption, and decryption. * @property {object} [schemaMap] A map of namespaces to a local JSON schema for encryption * @property {boolean} [bypassAutoEncryption] Allows the user to bypass auto encryption, maintaining implicit decryption * @property {AutoEncrypter~logger} [options.logger] An optional hook to catch logging messages from the underlying encryption engine * @property {AutoEncrypter~AutoEncryptionExtraOptions} [extraOptions] Extra options related to the mongocryptd process */ /** * Extra options related to the mongocryptd process * @typedef {object} AutoEncrypter~AutoEncryptionExtraOptions * @property {string} [mongocryptdURI] A local process the driver communicates with to determine how to encrypt values in a command. Defaults to "mongodb://%2Fvar%2Fmongocryptd.sock" if domain sockets are available or "mongodb://localhost:27020" otherwise * @property {boolean} [mongocryptdBypassSpawn=false] If true, autoEncryption will not attempt to spawn a mongocryptd before connecting * @property {string} [mongocryptdSpawnPath] The path to the mongocryptd executable on the system * @property {string[]} [mongocryptdSpawnArgs] Command line arguments to use when auto-spawning a mongocryptd */ /** * @callback AutoEncrypter~logger * @description A callback that is invoked with logging information from * the underlying C++ Bindings. * @param {AutoEncrypter~logLevel} level The level of logging. * @param {string} message The message to log */ /** * @name AutoEncrypter~logLevel * @enum {number} * @description * The level of severity of the log message * * | Value | Level | * |-------|-------| * | 0 | Fatal Error | * | 1 | Error | * | 2 | Warning | * | 3 | Info | * | 4 | Trace | */ /** * @classdesc 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 { /** * Create an AutoEncrypter * * **Note**: Do not instantiate this class directly. Rather, supply the relevant options to a MongoClient * * **Note**: Supplying `options.schemaMap` provides more security than relying on JSON Schemas obtained from the server. * It protects against a malicious server advertising a false JSON Schema, which could trick the client into sending unencrypted data that should be encrypted. * Schemas supplied in the schemaMap only apply to configuring automatic encryption for Client-Side Field Level Encryption. * Other validation rules in the JSON schema will not be enforced by the driver and will result in an error. * @param {MongoClient} client The client autoEncryption is enabled on * @param {AutoEncrypter~AutoEncryptionOptions} [options] Optional settings * * @example * // Enabling autoEncryption via a MongoClient * 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 */ constructor(client, options) { this._client = client; this._bson = options.bson || BSON || client.topology.bson; this._bypassEncryption = options.bypassAutoEncryption === true; this._keyVaultNamespace = options.keyVaultNamespace || 'admin.datakeys'; this._keyVaultClient = options.keyVaultClient || client; this._metaDataClient = options.metadataClient || client; this._proxyOptions = options.proxyOptions || {}; this._tlsOptions = options.tlsOptions || {}; this._onKmsProviderRefresh = options.onKmsProviderRefresh; this._kmsProviders = options.kmsProviders || {}; const mongoCryptOptions = {}; if (options.schemaMap) { mongoCryptOptions.schemaMap = Buffer.isBuffer(options.schemaMap) ? options.schemaMap : this._bson.serialize(options.schemaMap); } if (options.encryptedFieldsMap) { mongoCryptOptions.encryptedFieldsMap = Buffer.isBuffer(options.encryptedFieldsMap) ? options.encryptedFieldsMap : this._bson.serialize(options.encryptedFieldsMap); } mongoCryptOptions.kmsProviders = !Buffer.isBuffer(this._kmsProviders) ? this._bson.serialize(this._kmsProviders) : this._kmsProviders; if (options.logger) { mongoCryptOptions.logger = options.logger; } if (options.extraOptions && options.extraOptions.cryptSharedLibPath) { mongoCryptOptions.cryptSharedLibPath = options.extraOptions.cryptSharedLibPath; } if (options.bypassQueryAnalysis) { mongoCryptOptions.bypassQueryAnalysis = options.bypassQueryAnalysis; } this._bypassMongocryptdAndCryptShared = this._bypassEncryption || options.bypassQueryAnalysis; if (options.extraOptions && options.extraOptions.cryptSharedLibSearchPaths) { // Only for driver testing mongoCryptOptions.cryptSharedLibSearchPaths = options.extraOptions.cryptSharedLibSearchPaths; } else if (!this._bypassMongocryptdAndCryptShared) { mongoCryptOptions.cryptSharedLibSearchPaths = ['$SYSTEM']; } Object.assign(mongoCryptOptions, { cryptoCallbacks }); this._mongocrypt = new mc.MongoCrypt(mongoCryptOptions); this._contextCounter = 0; if ( options.extraOptions && options.extraOptions.cryptSharedLibRequired && !this.cryptSharedLibVersionInfo ) { throw new MongoError('`cryptSharedLibRequired` set but no crypt_shared library loaded'); } // Only instantiate mongocryptd manager/client once we know for sure // that we are not using the CSFLE shared library. if (!this._bypassMongocryptdAndCryptShared && !this.cryptSharedLibVersionInfo) { this._mongocryptdManager = new MongocryptdManager(options.extraOptions); const clientOptions = { useNewUrlParser: true, useUnifiedTopology: true, serverSelectionTimeoutMS: 10000 }; if ( options.extraOptions == null || typeof options.extraOptions.mongocryptdURI !== 'string' ) { clientOptions.family = 4; } this._mongocryptdClient = new MongoClient(this._mongocryptdManager.uri, clientOptions); } } /** * @ignore * @param {Function} callback Invoked when the mongocryptd client either successfully connects or errors */ init(callback) { if (this._bypassMongocryptdAndCryptShared || this.cryptSharedLibVersionInfo) { return callback(); } const _callback = (err, res) => { if ( err && err.message && (err.message.match(/timed out after/) || err.message.match(/ENOTFOUND/)) ) { callback( new MongoError( 'Unable to connect to `mongocryptd`, please make sure it is running or in your PATH for auto-spawn' ) ); return; } callback(err, res); }; if (this._mongocryptdManager.bypassSpawn) { return this._mongocryptdClient.connect().then( result => { return _callback(null, result); }, error => { _callback(error, null); } ); } this._mongocryptdManager.spawn(() => { this._mongocryptdClient.connect().then( result => { return _callback(null, result); }, error => { _callback(error, null); } ); }); } /** * @ignore * @param {Function} callback Invoked when the mongocryptd client either successfully disconnects or errors */ teardown(force, callback) { if (this._mongocryptdClient) { this._mongocryptdClient.close(force).then( result => { return callback(null, result); }, error => { callback(error); } ); } else { callback(); } } /** * @ignore * Encrypt a command for a given namespace. * * @param {string} ns The namespace for this encryption context * @param {object} cmd The command to encrypt * @param {Function} callback */ encrypt(ns, cmd, options, callback) { if (typeof ns !== 'string') { throw new TypeError('Parameter `ns` must be a string'); } if (typeof cmd !== 'object') { throw new TypeError('Parameter `cmd` must be an object'); } if (typeof options === 'function' && callback == null) { callback = options; options = {}; } // If `bypassAutoEncryption` has been specified, don't encrypt if (this._bypassEncryption) { callback(undefined, cmd); return; } const bson = this._bson; const commandBuffer = Buffer.isBuffer(cmd) ? cmd : bson.serialize(cmd, options); let context; try { context = this._mongocrypt.makeEncryptionContext(databaseNamespace(ns), commandBuffer); } catch (err) { callback(err, null); return; } // TODO: should these be accessors from the addon? context.id = this._contextCounter++; context.ns = ns; context.document = cmd; const stateMachine = new StateMachine({ bson, ...options, promoteValues: false, promoteLongs: false, proxyOptions: this._proxyOptions, tlsOptions: this._tlsOptions }); stateMachine.execute(this, context, callback); } /** * @ignore * Decrypt a command response * * @param {Buffer} buffer * @param {Function} callback */ decrypt(response, options, callback) { if (typeof options === 'function' && callback == null) { callback = options; options = {}; } const bson = this._bson; const buffer = Buffer.isBuffer(response) ? response : bson.serialize(response, options); let context; try { context = this._mongocrypt.makeDecryptionContext(buffer); } catch (err) { callback(err, null); return; } // TODO: should this be an accessor from the addon? context.id = this._contextCounter++; const stateMachine = new StateMachine({ bson, ...options, proxyOptions: this._proxyOptions, tlsOptions: this._tlsOptions }); const decorateResult = this[Symbol.for('@@mdb.decorateDecryptionResult')]; stateMachine.execute(this, context, function (err, result) { // Only for testing/internal usage if (!err && result && decorateResult) { err = decorateDecryptionResult(result, response, bson); if (err) return callback(err); } callback(err, result); }); } /** * Ask the user for KMS credentials. * * This returns anything that looks like the kmsProviders original input * option. It can be empty, and any provider specified here will override * the original ones. */ async askForKMSCredentials() { return this._onKmsProviderRefresh ? this._onKmsProviderRefresh() : loadCredentials(this._kmsProviders); } /** * Return the current libmongocrypt's CSFLE shared library version * as `{ version: bigint, versionStr: string }`, or `null` if no CSFLE * shared library was loaded. */ get cryptSharedLibVersionInfo() { return this._mongocrypt.cryptSharedLibVersionInfo; } static get libmongocryptVersion() { return mc.MongoCrypt.libmongocryptVersion; } } return { AutoEncrypter }; }; /** * Recurse through the (identically-shaped) `decrypted` and `original` * objects and attach a `decryptedKeys` property on each sub-object that * contained encrypted fields. Because we only call this on BSON responses, * we do not need to worry about circular references. * * @internal * @ignore */ function decorateDecryptionResult(decrypted, original, bson, isTopLevelDecorateCall = true) { const decryptedKeys = Symbol.for('@@mdb.decryptedKeys'); if (isTopLevelDecorateCall) { // The original value could have been either a JS object or a BSON buffer if (Buffer.isBuffer(original)) { original = bson.deserialize(original); } if (Buffer.isBuffer(decrypted)) { return new Error('Expected result of decryption to be deserialized BSON object'); } } if (!decrypted || typeof decrypted !== 'object') return; for (const k of Object.keys(decrypted)) { const originalValue = original[k]; // An object was decrypted by libmongocrypt if and only if it was // a BSON Binary object with subtype 6. if (originalValue && originalValue._bsontype === 'Binary' && originalValue.sub_type === 6) { if (!decrypted[decryptedKeys]) { Object.defineProperty(decrypted, decryptedKeys, { value: [], configurable: true, enumerable: false, writable: false }); } decrypted[decryptedKeys].push(k); // Do not recurse into this decrypted value. It could be a subdocument/array, // in which case there is no original value associated with its subfields. continue; } decorateDecryptionResult(decrypted[k], originalValue, bson, false); } }