'use strict'; module.exports = function (modules) { const mc = require('bindings')('mongocrypt'); const common = require('./common'); const databaseNamespace = common.databaseNamespace; const collectionNamespace = common.collectionNamespace; const promiseOrCallback = common.promiseOrCallback; const StateMachine = modules.stateMachine.StateMachine; const BSON = modules.mongodb.BSON; const { loadCredentials } = require('./credentialsProvider'); const cryptoCallbacks = require('./cryptoCallbacks'); const { promisify } = require('util'); /** * @typedef {object} KMSProviders Configuration options that are used by specific KMS providers during key generation, encryption, and decryption. * @property {object} [aws] Configuration options for using 'aws' as your KMS provider * @property {string} [aws.accessKeyId] The access key used for the AWS KMS provider * @property {string} [aws.secretAccessKey] The secret access key used for the AWS KMS provider * @property {object} [local] Configuration options for using 'local' as your KMS provider * @property {Buffer} [local.key] The master key used to encrypt/decrypt data keys. A 96-byte long Buffer. * @property {object} [azure] Configuration options for using 'azure' as your KMS provider * @property {string} [azure.tenantId] The tenant ID identifies the organization for the account * @property {string} [azure.clientId] The client ID to authenticate a registered application * @property {string} [azure.clientSecret] The client secret to authenticate a registered application * @property {string} [azure.identityPlatformEndpoint] If present, a host with optional port. E.g. "example.com" or "example.com:443". This is optional, and only needed if customer is using a non-commercial Azure instance (e.g. a government or China account, which use different URLs). Defaults to "login.microsoftonline.com" * @property {object} [gcp] Configuration options for using 'gcp' as your KMS provider * @property {string} [gcp.email] The service account email to authenticate * @property {string|Binary} [gcp.privateKey] A PKCS#8 encrypted key. This can either be a base64 string or a binary representation * @property {string} [gcp.endpoint] If present, a host with optional port. E.g. "example.com" or "example.com:443". Defaults to "oauth2.googleapis.com" */ /** * @typedef {object} DataKey A data key as stored in the database. * @property {UUID} _id A unique identifier for the key. * @property {number} version A numeric identifier for the schema version of this document. Implicitly 0 if unset. * @property {string[]} [keyAltNames] Alternate names to search for keys by. Used for a per-document key scenario in support of GDPR scenarios. * @property {Binary} keyMaterial Encrypted data key material, BinData type General. * @property {Date} creationDate The datetime the wrapped data key material was imported into the Key Database. * @property {Date} updateDate The datetime the wrapped data key material was last modified. On initial import, this value will be set to creationDate. * @property {number} status 0 = enabled, 1 = disabled * @property {object} masterKey the encrypted master key */ /** * @typedef {string} KmsProvider A string containing the name of a kms provider. Valid options are 'aws', 'azure', 'gcp', 'kmip', or 'local' */ /** * @typedef {object} ClientSession The ClientSession class from the MongoDB Node driver (see https://mongodb.github.io/node-mongodb-native/4.8/classes/ClientSession.html) */ /** * @typedef {object} DeleteResult The result of a delete operation from the MongoDB Node driver (see https://mongodb.github.io/node-mongodb-native/4.8/interfaces/DeleteResult.html) * @property {boolean} acknowledged Indicates whether this write result was acknowledged. If not, then all other members of this result will be undefined. * @property {number} deletedCount The number of documents that were deleted */ /** * @typedef {object} BulkWriteResult The BulkWriteResult class from the MongoDB Node driver (https://mongodb.github.io/node-mongodb-native/4.8/classes/BulkWriteResult.html) */ /** * @typedef {object} FindCursor The FindCursor class from the MongoDB Node driver (see https://mongodb.github.io/node-mongodb-native/4.8/classes/FindCursor.html) */ /** * The public interface for explicit in-use encryption */ class ClientEncryption { /** * Create a new encryption instance * * @param {MongoClient} client The client used for encryption * @param {object} options Additional settings * @param {string} options.keyVaultNamespace The namespace of the key vault, used to store encryption keys * @param {object} options.tlsOptions An object that maps KMS provider names to TLS options. * @param {MongoClient} [options.keyVaultClient] A `MongoClient` used to fetch keys from a key vault. Defaults to `client` * @param {KMSProviders} [options.kmsProviders] options for specific KMS providers to use * * @example * new ClientEncryption(mongoClient, { * keyVaultNamespace: 'client.encryption', * kmsProviders: { * local: { * key: masterKey // The master key used for encryption/decryption. A 96-byte long Buffer * } * } * }); * * @example * new ClientEncryption(mongoClient, { * keyVaultNamespace: 'client.encryption', * kmsProviders: { * aws: { * accessKeyId: AWS_ACCESS_KEY, * secretAccessKey: AWS_SECRET_KEY * } * } * }); */ constructor(client, options) { this._client = client; this._bson = options.bson || BSON || client.topology.bson; this._proxyOptions = options.proxyOptions; this._tlsOptions = options.tlsOptions; this._kmsProviders = options.kmsProviders || {}; if (options.keyVaultNamespace == null) { throw new TypeError('Missing required option `keyVaultNamespace`'); } const mongoCryptOptions = { ...options, cryptoCallbacks }; mongoCryptOptions.kmsProviders = !Buffer.isBuffer(this._kmsProviders) ? this._bson.serialize(this._kmsProviders) : this._kmsProviders; this._onKmsProviderRefresh = options.onKmsProviderRefresh; this._keyVaultNamespace = options.keyVaultNamespace; this._keyVaultClient = options.keyVaultClient || client; this._mongoCrypt = new mc.MongoCrypt(mongoCryptOptions); } /** * @typedef {Binary} ClientEncryptionDataKeyId * The id of an existing dataKey. Is a bson Binary value. * Can be used for {@link ClientEncryption.encrypt}, and can be used to directly * query for the data key itself against the key vault namespace. */ /** * @callback ClientEncryptionCreateDataKeyCallback * @param {Error} [error] If present, indicates an error that occurred in the creation of the data key * @param {ClientEncryption~dataKeyId} [dataKeyId] If present, returns the id of the created data key */ /** * @typedef {object} AWSEncryptionKeyOptions Configuration options for making an AWS encryption key * @property {string} region The AWS region of the KMS * @property {string} key The Amazon Resource Name (ARN) to the AWS customer master key (CMK) * @property {string} [endpoint] An alternate host to send KMS requests to. May include port number */ /** * @typedef {object} GCPEncryptionKeyOptions Configuration options for making a GCP encryption key * @property {string} projectId GCP project id * @property {string} location Location name (e.g. "global") * @property {string} keyRing Key ring name * @property {string} keyName Key name * @property {string} [keyVersion] Key version * @property {string} [endpoint] KMS URL, defaults to `https://www.googleapis.com/auth/cloudkms` */ /** * @typedef {object} AzureEncryptionKeyOptions Configuration options for making an Azure encryption key * @property {string} keyName Key name * @property {string} keyVaultEndpoint Key vault URL, typically `.vault.azure.net` * @property {string} [keyVersion] Key version */ /** * Creates a data key used for explicit encryption and inserts it into the key vault namespace * * @param {string} provider The KMS provider used for this data key. Must be `'aws'`, `'azure'`, `'gcp'`, or `'local'` * @param {object} [options] Options for creating the data key * @param {AWSEncryptionKeyOptions|AzureEncryptionKeyOptions|GCPEncryptionKeyOptions} [options.masterKey] Idenfities a new KMS-specific key used to encrypt the new data key * @param {string[]} [options.keyAltNames] An optional list of string alternate names used to reference a key. If a key is created with alternate names, then encryption may refer to the key by the unique alternate name instead of by _id. * @param {ClientEncryptionCreateDataKeyCallback} [callback] Optional callback to invoke when key is created * @returns {Promise|void} If no callback is provided, returns a Promise that either resolves with {@link ClientEncryption~dataKeyId the id of the created data key}, or rejects with an error. If a callback is provided, returns nothing. * @example * // Using callbacks to create a local key * clientEncryption.createDataKey('local', (err, dataKey) => { * if (err) { * // This means creating the key failed. * } else { * // key creation succeeded * } * }); * * @example * // Using async/await to create a local key * const dataKeyId = await clientEncryption.createDataKey('local'); * * @example * // 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 * // 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' ] * }); */ createDataKey(provider, options, callback) { if (typeof options === 'function') { callback = options; options = {}; } if (options == null) { options = {}; } const bson = this._bson; const dataKey = Object.assign({ provider }, options.masterKey); if (options.keyAltNames && !Array.isArray(options.keyAltNames)) { throw new TypeError( `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 TypeError( `Option "keyAltNames" must be an array of strings, but item at index ${i} was of type ${typeof keyAltName}` ); } return bson.serialize({ keyAltName }); }); } let keyMaterial = undefined; if (options.keyMaterial) { keyMaterial = bson.serialize({ keyMaterial: options.keyMaterial }); } const dataKeyBson = bson.serialize(dataKey); const context = this._mongoCrypt.makeDataKeyContext(dataKeyBson, { keyAltNames, keyMaterial }); const stateMachine = new StateMachine({ bson, proxyOptions: this._proxyOptions, tlsOptions: this._tlsOptions }); return promiseOrCallback(callback, cb => { stateMachine.execute(this, context, (err, dataKey) => { if (err) { cb(err, null); return; } const dbName = databaseNamespace(this._keyVaultNamespace); const collectionName = collectionNamespace(this._keyVaultNamespace); this._keyVaultClient .db(dbName) .collection(collectionName) .insertOne(dataKey, { writeConcern: { w: 'majority' } }) .then( result => { return cb(null, result.insertedId); }, err => { cb(err, null); } ); }); }); } /** * @typedef {object} RewrapManyDataKeyResult * @property {BulkWriteResult} [bulkWriteResult] An optional BulkWriteResult, if any keys were matched and attempted to be re-wrapped. */ /** * Searches the keyvault for any data keys matching the provided filter. If there are matches, rewrapManyDataKey then attempts to re-wrap the data keys using the provided options. * * If no matches are found, then no bulk write is performed. * * @param {object} filter A valid MongoDB filter. Any documents matching this filter will be re-wrapped. * @param {object} [options] * @param {KmsProvider} options.provider The KMS provider to use when re-wrapping the data keys. * @param {AWSEncryptionKeyOptions | AzureEncryptionKeyOptions | GCPEncryptionKeyOptions} [options.masterKey] * @returns {Promise} * * @example * // 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 * // 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) { const bson = this._bson; let keyEncryptionKeyBson = undefined; if (options) { const keyEncryptionKey = Object.assign({ provider: options.provider }, options.masterKey); keyEncryptionKeyBson = bson.serialize(keyEncryptionKey); } else { // Always make sure `options` is an object below. options = {}; } const filterBson = bson.serialize(filter); const context = this._mongoCrypt.makeRewrapManyDataKeyContext( filterBson, keyEncryptionKeyBson ); const stateMachine = new StateMachine({ bson, proxyOptions: this._proxyOptions, tlsOptions: this._tlsOptions }); const execute = promisify(stateMachine.execute.bind(stateMachine)); const dataKey = await execute(this, context); if (!dataKey || dataKey.v.length === 0) { return {}; } const dbName = databaseNamespace(this._keyVaultNamespace); const collectionName = collectionNamespace(this._keyVaultNamespace); const replacements = dataKey.v.map(key => ({ 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' } }); return { bulkWriteResult: result }; } /** * Deletes the key with the provided id from the keyvault, if it exists. * * @param {ClientEncryptionDataKeyId} _id - the id of the document to delete. * @returns {Promise} Returns a promise that either resolves to a {@link DeleteResult} or rejects with an error. * * @example * // 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 dbName = databaseNamespace(this._keyVaultNamespace); const collectionName = collectionNamespace(this._keyVaultNamespace); return await this._keyVaultClient .db(dbName) .collection(collectionName) .deleteOne({ _id }, { writeConcern: { w: 'majority' } }); } /** * Finds all the keys currently stored in the keyvault. * * This method will not throw. * * @returns {FindCursor} a FindCursor over all keys in the keyvault. * @example * // fetching all keys * const keys = await clientEncryption.getKeys().toArray(); */ getKeys() { const dbName = databaseNamespace(this._keyVaultNamespace); const collectionName = collectionNamespace(this._keyVaultNamespace); return this._keyVaultClient .db(dbName) .collection(collectionName) .find({}, { readConcern: { level: 'majority' } }); } /** * Finds a key in the keyvault with the specified _id. * * @param {ClientEncryptionDataKeyId} _id - the id of the document to delete. * @returns {Promise} Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents * match the id. The promise rejects with an error if an error is thrown. * @example * // 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 dbName = databaseNamespace(this._keyVaultNamespace); const collectionName = collectionNamespace(this._keyVaultNamespace); return await this._keyVaultClient .db(dbName) .collection(collectionName) .findOne({ _id }, { readConcern: { level: 'majority' } }); } /** * Finds a key in the keyvault which has the specified keyAltName. * * @param {string} keyAltName - a keyAltName to search for a key * @returns {Promise} Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents * match the keyAltName. The promise rejects with an error if an error is thrown. * @example * // 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 dbName = databaseNamespace(this._keyVaultNamespace); const collectionName = collectionNamespace(this._keyVaultNamespace); return await this._keyVaultClient .db(dbName) .collection(collectionName) .findOne({ keyAltNames: keyAltName }, { readConcern: { level: 'majority' } }); } /** * 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 {ClientEncryptionDataKeyId} _id The id of the document to update. * @param {string} keyAltName - a keyAltName to search for a key * @returns {Promise} Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents * match the id. The promise rejects with an error if an error is thrown. * @example * // 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 dbName = databaseNamespace(this._keyVaultNamespace); const collectionName = collectionNamespace(this._keyVaultNamespace); const { value } = await this._keyVaultClient .db(dbName) .collection(collectionName) .findOneAndUpdate( { _id }, { $addToSet: { keyAltNames: keyAltName } }, { writeConcern: { w: 'majority' }, returnDocument: 'before' } ); 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 {ClientEncryptionDataKeyId} _id The id of the document to update. * @param {string} keyAltName - a keyAltName to search for a key * @returns {Promise} Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents * match the id. The promise rejects with an error if an error is thrown. * @example * // 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 dbName = databaseNamespace(this._keyVaultNamespace); const collectionName = collectionNamespace(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' }); return value; } /** * @callback ClientEncryptionEncryptCallback * @param {Error} [err] If present, indicates an error that occurred in the process of encryption * @param {Buffer} [result] If present, is the encrypted result */ /** * 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 {object} options * @param {ClientEncryptionDataKeyId} [options.keyId] The id of the Binary dataKey to use for encryption * @param {string} [options.keyAltName] A unique string name corresponding to an already existing dataKey. * @param {} [options.algorithm] The algorithm to use for encryption. Must be either `'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'`, `'AEAD_AES_256_CBC_HMAC_SHA_512-Random'`, `'Indexed'` or `'Unindexed'` * @param {ClientEncryptionEncryptCallback} [callback] Optional callback to invoke when value is encrypted * @returns {Promise|void} If no callback is provided, returns a Promise that either resolves with the encrypted value, or rejects with an error. If a callback is provided, returns nothing. * * @example * // Encryption with callback API * function encryptMyData(value, callback) { * clientEncryption.createDataKey('local', (err, keyId) => { * if (err) { * return callback(err); * } * clientEncryption.encrypt(value, { keyId, algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' }, callback); * }); * } * * @example * // 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 * // Encryption using a keyAltName * async function encryptMyData(value) { * await clientEncryption.createDataKey('local', { keyAltNames: 'mySpecialKey' }); * return clientEncryption.encrypt(value, { keyAltName: 'mySpecialKey', algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' }); * } */ encrypt(value, options, callback) { const bson = this._bson; const valueBuffer = bson.serialize({ v: value }); const contextOptions = Object.assign({}, options); if (options.keyId) { contextOptions.keyId = options.keyId.buffer; } if (options.keyAltName) { const keyAltName = options.keyAltName; if (options.keyId) { throw new TypeError(`"options" cannot contain both "keyId" and "keyAltName"`); } const keyAltNameType = typeof keyAltName; if (keyAltNameType !== 'string') { throw new TypeError( `"options.keyAltName" must be of type string, but was of type ${keyAltNameType}` ); } contextOptions.keyAltName = bson.serialize({ keyAltName }); } const stateMachine = new StateMachine({ bson, proxyOptions: this._proxyOptions, tlsOptions: this._tlsOptions }); const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions); return promiseOrCallback(callback, cb => { stateMachine.execute(this, context, (err, result) => { if (err) { cb(err, null); return; } cb(null, result.v); }); }); } /** * @callback ClientEncryption~decryptCallback * @param {Error} [err] If present, indicates an error that occurred in the process of decryption * @param {object} [result] If present, is the decrypted result */ /** * Explicitly decrypt a provided encrypted value * * @param {Buffer | Binary} value An encrypted value * @param {ClientEncryption~decryptCallback} callback Optional callback to invoke when value is decrypted * @returns {Promise|void} If no callback is provided, returns a Promise that either resolves with the decryped value, or rejects with an error. If a callback is provided, returns nothing. * * @example * // Decrypting value with callback API * function decryptMyValue(value, callback) { * clientEncryption.decrypt(value, callback); * } * * @example * // Decrypting value with async/await API * async function decryptMyValue(value) { * return clientEncryption.decrypt(value); * } */ decrypt(value, callback) { const bson = this._bson; const valueBuffer = bson.serialize({ v: value }); const context = this._mongoCrypt.makeExplicitDecryptionContext(valueBuffer); const stateMachine = new StateMachine({ bson, proxyOptions: this._proxyOptions, tlsOptions: this._tlsOptions }); return promiseOrCallback(callback, cb => { stateMachine.execute(this, context, (err, result) => { if (err) { cb(err, null); return; } cb(null, result.v); }); }); } /** * 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); } static get libmongocryptVersion() { return mc.MongoCrypt.libmongocryptVersion; } } return { ClientEncryption }; };