'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 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 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 || client.topology.bson;
this._mongocryptdManager = new MongocryptdManager(options.extraOptions);
this._mongocryptdClient = new MongoClient(this._mongocryptdManager.uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
serverSelectionTimeoutMS: 1000
});
this._keyVaultNamespace = options.keyVaultNamespace || 'admin.datakeys';
this._keyVaultClient = options.keyVaultClient || client;
this._metaDataClient = options.metadataClient || client;
this._bypassEncryption =
typeof options.bypassAutoEncryption === 'boolean' ? options.bypassAutoEncryption : false;
const mongoCryptOptions = {};
if (options.schemaMap) {
mongoCryptOptions.schemaMap = Buffer.isBuffer(options.schemaMap)
? options.schemaMap
: this._bson.serialize(options.schemaMap);
}
if (options.kmsProviders) {
mongoCryptOptions.kmsProviders = !Buffer.isBuffer(options.kmsProviders)
? this._bson.serialize(options.kmsProviders)
: options.kmsProviders;
}
if (options.logger) {
mongoCryptOptions.logger = options.logger;
}
Object.assign(mongoCryptOptions, { cryptoCallbacks });
this._mongocrypt = new mc.MongoCrypt(mongoCryptOptions);
this._contextCounter = 0;
}
/**
* @ignore
* @param {Function} callback Invoked when the mongocryptd client either successfully connects or errors
*/
init(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(_callback);
}
this._mongocryptdManager.spawn(() => this._mongocryptdClient.connect(_callback));
}
/**
* @ignore
* @param {Function} callback Invoked when the mongocryptd client either successfully disconnects or errors
*/
teardown(force, callback) {
this._mongocryptdClient.close(force, 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(Object.assign({ bson }, options));
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(Object.assign({ bson }, options));
stateMachine.execute(this, context, callback);
}
}
return { AutoEncrypter };
};