You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

555 lines
23 KiB
JavaScript

"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