Spaces:
Runtime error
Runtime error
| ; | |
| Object.defineProperty(exports, "__esModule", { value: true }); | |
| exports.ClientEncryption = void 0; | |
| const bson_1 = require("../bson"); | |
| const deps_1 = require("../deps"); | |
| const utils_1 = require("../utils"); | |
| const cryptoCallbacks = require("./crypto_callbacks"); | |
| 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 || {}; | |
| if (options.keyVaultNamespace == null) { | |
| throw new errors_1.MongoCryptInvalidArgumentError('Missing required option `keyVaultNamespace`'); | |
| } | |
| const mongoCryptOptions = { | |
| ...options, | |
| cryptoCallbacks, | |
| kmsProviders: !Buffer.isBuffer(this._kmsProviders) | |
| ? (0, bson_1.serialize)(this._kmsProviders) | |
| : this._kmsProviders | |
| }; | |
| 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 | |
| }); | |
| const dataKey = await stateMachine.execute(this, context); | |
| 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' } }); | |
| 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 | |
| }); | |
| const { v: dataKeys } = await stateMachine.execute(this, context); | |
| 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' } | |
| }); | |
| 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' } }); | |
| } | |
| /** | |
| * 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' } }); | |
| } | |
| /** | |
| * 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' } }); | |
| } | |
| /** | |
| * 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' } }); | |
| } | |
| /** | |
| * 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' }); | |
| 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' | |
| }); | |
| 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; | |
| 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 }) | |
| }); | |
| 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 | |
| }); | |
| 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 "rangePreview" and algorithm is "RangePreview". | |
| * | |
| * @experimental The Range algorithm is experimental only. It is not intended for production use. It is subject to breaking changes. | |
| * | |
| * @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 | |
| }); | |
| const { v } = await stateMachine.execute(this, context); | |
| 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); | |
| } | |
| 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 } = 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); | |
| } | |
| const valueBuffer = (0, bson_1.serialize)({ v: value }); | |
| const stateMachine = new state_machine_1.StateMachine({ | |
| proxyOptions: this._proxyOptions, | |
| tlsOptions: this._tlsOptions | |
| }); | |
| const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions); | |
| const result = await stateMachine.execute(this, context); | |
| return result.v; | |
| } | |
| } | |
| exports.ClientEncryption = ClientEncryption; | |
| //# sourceMappingURL=client_encryption.js.map |