123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555 |
- "use strict";
- 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 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 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 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 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 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 (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
|