client_encryption.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.ClientEncryption = void 0;
  4. const bson_1 = require("../bson");
  5. const deps_1 = require("../deps");
  6. const utils_1 = require("../utils");
  7. const cryptoCallbacks = require("./crypto_callbacks");
  8. const errors_1 = require("./errors");
  9. const index_1 = require("./providers/index");
  10. const state_machine_1 = require("./state_machine");
  11. /**
  12. * @public
  13. * The public interface for explicit in-use encryption
  14. */
  15. class ClientEncryption {
  16. /** @internal */
  17. static getMongoCrypt() {
  18. const encryption = (0, deps_1.getMongoDBClientEncryption)();
  19. if ('kModuleError' in encryption) {
  20. throw encryption.kModuleError;
  21. }
  22. return encryption.MongoCrypt;
  23. }
  24. /**
  25. * Create a new encryption instance
  26. *
  27. * @example
  28. * ```ts
  29. * new ClientEncryption(mongoClient, {
  30. * keyVaultNamespace: 'client.encryption',
  31. * kmsProviders: {
  32. * local: {
  33. * key: masterKey // The master key used for encryption/decryption. A 96-byte long Buffer
  34. * }
  35. * }
  36. * });
  37. * ```
  38. *
  39. * @example
  40. * ```ts
  41. * new ClientEncryption(mongoClient, {
  42. * keyVaultNamespace: 'client.encryption',
  43. * kmsProviders: {
  44. * aws: {
  45. * accessKeyId: AWS_ACCESS_KEY,
  46. * secretAccessKey: AWS_SECRET_KEY
  47. * }
  48. * }
  49. * });
  50. * ```
  51. */
  52. constructor(client, options) {
  53. this._client = client;
  54. this._proxyOptions = options.proxyOptions ?? {};
  55. this._tlsOptions = options.tlsOptions ?? {};
  56. this._kmsProviders = options.kmsProviders || {};
  57. if (options.keyVaultNamespace == null) {
  58. throw new errors_1.MongoCryptInvalidArgumentError('Missing required option `keyVaultNamespace`');
  59. }
  60. const mongoCryptOptions = {
  61. ...options,
  62. cryptoCallbacks,
  63. kmsProviders: !Buffer.isBuffer(this._kmsProviders)
  64. ? (0, bson_1.serialize)(this._kmsProviders)
  65. : this._kmsProviders
  66. };
  67. this._keyVaultNamespace = options.keyVaultNamespace;
  68. this._keyVaultClient = options.keyVaultClient || client;
  69. const MongoCrypt = ClientEncryption.getMongoCrypt();
  70. this._mongoCrypt = new MongoCrypt(mongoCryptOptions);
  71. }
  72. /**
  73. * Creates a data key used for explicit encryption and inserts it into the key vault namespace
  74. *
  75. * @example
  76. * ```ts
  77. * // Using async/await to create a local key
  78. * const dataKeyId = await clientEncryption.createDataKey('local');
  79. * ```
  80. *
  81. * @example
  82. * ```ts
  83. * // Using async/await to create an aws key
  84. * const dataKeyId = await clientEncryption.createDataKey('aws', {
  85. * masterKey: {
  86. * region: 'us-east-1',
  87. * key: 'xxxxxxxxxxxxxx' // CMK ARN here
  88. * }
  89. * });
  90. * ```
  91. *
  92. * @example
  93. * ```ts
  94. * // Using async/await to create an aws key with a keyAltName
  95. * const dataKeyId = await clientEncryption.createDataKey('aws', {
  96. * masterKey: {
  97. * region: 'us-east-1',
  98. * key: 'xxxxxxxxxxxxxx' // CMK ARN here
  99. * },
  100. * keyAltNames: [ 'mySpecialKey' ]
  101. * });
  102. * ```
  103. */
  104. async createDataKey(provider, options = {}) {
  105. if (options.keyAltNames && !Array.isArray(options.keyAltNames)) {
  106. throw new errors_1.MongoCryptInvalidArgumentError(`Option "keyAltNames" must be an array of strings, but was of type ${typeof options.keyAltNames}.`);
  107. }
  108. let keyAltNames = undefined;
  109. if (options.keyAltNames && options.keyAltNames.length > 0) {
  110. keyAltNames = options.keyAltNames.map((keyAltName, i) => {
  111. if (typeof keyAltName !== 'string') {
  112. throw new errors_1.MongoCryptInvalidArgumentError(`Option "keyAltNames" must be an array of strings, but item at index ${i} was of type ${typeof keyAltName}`);
  113. }
  114. return (0, bson_1.serialize)({ keyAltName });
  115. });
  116. }
  117. let keyMaterial = undefined;
  118. if (options.keyMaterial) {
  119. keyMaterial = (0, bson_1.serialize)({ keyMaterial: options.keyMaterial });
  120. }
  121. const dataKeyBson = (0, bson_1.serialize)({
  122. provider,
  123. ...options.masterKey
  124. });
  125. const context = this._mongoCrypt.makeDataKeyContext(dataKeyBson, {
  126. keyAltNames,
  127. keyMaterial
  128. });
  129. const stateMachine = new state_machine_1.StateMachine({
  130. proxyOptions: this._proxyOptions,
  131. tlsOptions: this._tlsOptions
  132. });
  133. const dataKey = await stateMachine.execute(this, context);
  134. const { db: dbName, collection: collectionName } = utils_1.MongoDBCollectionNamespace.fromString(this._keyVaultNamespace);
  135. const { insertedId } = await this._keyVaultClient
  136. .db(dbName)
  137. .collection(collectionName)
  138. .insertOne(dataKey, { writeConcern: { w: 'majority' } });
  139. return insertedId;
  140. }
  141. /**
  142. * 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.
  143. *
  144. * If no matches are found, then no bulk write is performed.
  145. *
  146. * @example
  147. * ```ts
  148. * // rewrapping all data data keys (using a filter that matches all documents)
  149. * const filter = {};
  150. *
  151. * const result = await clientEncryption.rewrapManyDataKey(filter);
  152. * if (result.bulkWriteResult != null) {
  153. * // keys were re-wrapped, results will be available in the bulkWrite object.
  154. * }
  155. * ```
  156. *
  157. * @example
  158. * ```ts
  159. * // attempting to rewrap all data keys with no matches
  160. * const filter = { _id: new Binary() } // assume _id matches no documents in the database
  161. * const result = await clientEncryption.rewrapManyDataKey(filter);
  162. *
  163. * if (result.bulkWriteResult == null) {
  164. * // no keys matched, `bulkWriteResult` does not exist on the result object
  165. * }
  166. * ```
  167. */
  168. async rewrapManyDataKey(filter, options) {
  169. let keyEncryptionKeyBson = undefined;
  170. if (options) {
  171. const keyEncryptionKey = Object.assign({ provider: options.provider }, options.masterKey);
  172. keyEncryptionKeyBson = (0, bson_1.serialize)(keyEncryptionKey);
  173. }
  174. const filterBson = (0, bson_1.serialize)(filter);
  175. const context = this._mongoCrypt.makeRewrapManyDataKeyContext(filterBson, keyEncryptionKeyBson);
  176. const stateMachine = new state_machine_1.StateMachine({
  177. proxyOptions: this._proxyOptions,
  178. tlsOptions: this._tlsOptions
  179. });
  180. const { v: dataKeys } = await stateMachine.execute(this, context);
  181. if (dataKeys.length === 0) {
  182. return {};
  183. }
  184. const { db: dbName, collection: collectionName } = utils_1.MongoDBCollectionNamespace.fromString(this._keyVaultNamespace);
  185. const replacements = dataKeys.map((key) => ({
  186. updateOne: {
  187. filter: { _id: key._id },
  188. update: {
  189. $set: {
  190. masterKey: key.masterKey,
  191. keyMaterial: key.keyMaterial
  192. },
  193. $currentDate: {
  194. updateDate: true
  195. }
  196. }
  197. }
  198. }));
  199. const result = await this._keyVaultClient
  200. .db(dbName)
  201. .collection(collectionName)
  202. .bulkWrite(replacements, {
  203. writeConcern: { w: 'majority' }
  204. });
  205. return { bulkWriteResult: result };
  206. }
  207. /**
  208. * Deletes the key with the provided id from the keyvault, if it exists.
  209. *
  210. * @example
  211. * ```ts
  212. * // delete a key by _id
  213. * const id = new Binary(); // id is a bson binary subtype 4 object
  214. * const { deletedCount } = await clientEncryption.deleteKey(id);
  215. *
  216. * if (deletedCount != null && deletedCount > 0) {
  217. * // successful deletion
  218. * }
  219. * ```
  220. *
  221. */
  222. async deleteKey(_id) {
  223. const { db: dbName, collection: collectionName } = utils_1.MongoDBCollectionNamespace.fromString(this._keyVaultNamespace);
  224. return this._keyVaultClient
  225. .db(dbName)
  226. .collection(collectionName)
  227. .deleteOne({ _id }, { writeConcern: { w: 'majority' } });
  228. }
  229. /**
  230. * Finds all the keys currently stored in the keyvault.
  231. *
  232. * This method will not throw.
  233. *
  234. * @returns a FindCursor over all keys in the keyvault.
  235. * @example
  236. * ```ts
  237. * // fetching all keys
  238. * const keys = await clientEncryption.getKeys().toArray();
  239. * ```
  240. */
  241. getKeys() {
  242. const { db: dbName, collection: collectionName } = utils_1.MongoDBCollectionNamespace.fromString(this._keyVaultNamespace);
  243. return this._keyVaultClient
  244. .db(dbName)
  245. .collection(collectionName)
  246. .find({}, { readConcern: { level: 'majority' } });
  247. }
  248. /**
  249. * Finds a key in the keyvault with the specified _id.
  250. *
  251. * Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents
  252. * match the id. The promise rejects with an error if an error is thrown.
  253. * @example
  254. * ```ts
  255. * // getting a key by id
  256. * const id = new Binary(); // id is a bson binary subtype 4 object
  257. * const key = await clientEncryption.getKey(id);
  258. * if (!key) {
  259. * // key is null if there was no matching key
  260. * }
  261. * ```
  262. */
  263. async getKey(_id) {
  264. const { db: dbName, collection: collectionName } = utils_1.MongoDBCollectionNamespace.fromString(this._keyVaultNamespace);
  265. return this._keyVaultClient
  266. .db(dbName)
  267. .collection(collectionName)
  268. .findOne({ _id }, { readConcern: { level: 'majority' } });
  269. }
  270. /**
  271. * Finds a key in the keyvault which has the specified keyAltName.
  272. *
  273. * @param keyAltName - a keyAltName to search for a key
  274. * @returns Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents
  275. * match the keyAltName. The promise rejects with an error if an error is thrown.
  276. * @example
  277. * ```ts
  278. * // get a key by alt name
  279. * const keyAltName = 'keyAltName';
  280. * const key = await clientEncryption.getKeyByAltName(keyAltName);
  281. * if (!key) {
  282. * // key is null if there is no matching key
  283. * }
  284. * ```
  285. */
  286. async getKeyByAltName(keyAltName) {
  287. const { db: dbName, collection: collectionName } = utils_1.MongoDBCollectionNamespace.fromString(this._keyVaultNamespace);
  288. return this._keyVaultClient
  289. .db(dbName)
  290. .collection(collectionName)
  291. .findOne({ keyAltNames: keyAltName }, { readConcern: { level: 'majority' } });
  292. }
  293. /**
  294. * Adds a keyAltName to a key identified by the provided _id.
  295. *
  296. * This method resolves to/returns the *old* key value (prior to adding the new altKeyName).
  297. *
  298. * @param _id - The id of the document to update.
  299. * @param keyAltName - a keyAltName to search for a key
  300. * @returns Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents
  301. * match the id. The promise rejects with an error if an error is thrown.
  302. * @example
  303. * ```ts
  304. * // adding an keyAltName to a data key
  305. * const id = new Binary(); // id is a bson binary subtype 4 object
  306. * const keyAltName = 'keyAltName';
  307. * const oldKey = await clientEncryption.addKeyAltName(id, keyAltName);
  308. * if (!oldKey) {
  309. * // null is returned if there is no matching document with an id matching the supplied id
  310. * }
  311. * ```
  312. */
  313. async addKeyAltName(_id, keyAltName) {
  314. const { db: dbName, collection: collectionName } = utils_1.MongoDBCollectionNamespace.fromString(this._keyVaultNamespace);
  315. const value = await this._keyVaultClient
  316. .db(dbName)
  317. .collection(collectionName)
  318. .findOneAndUpdate({ _id }, { $addToSet: { keyAltNames: keyAltName } }, { writeConcern: { w: 'majority' }, returnDocument: 'before' });
  319. return value;
  320. }
  321. /**
  322. * Adds a keyAltName to a key identified by the provided _id.
  323. *
  324. * This method resolves to/returns the *old* key value (prior to removing the new altKeyName).
  325. *
  326. * If the removed keyAltName is the last keyAltName for that key, the `altKeyNames` property is unset from the document.
  327. *
  328. * @param _id - The id of the document to update.
  329. * @param keyAltName - a keyAltName to search for a key
  330. * @returns Returns a promise that either resolves to a {@link DataKey} if a document matches the key or null if no documents
  331. * match the id. The promise rejects with an error if an error is thrown.
  332. * @example
  333. * ```ts
  334. * // removing a key alt name from a data key
  335. * const id = new Binary(); // id is a bson binary subtype 4 object
  336. * const keyAltName = 'keyAltName';
  337. * const oldKey = await clientEncryption.removeKeyAltName(id, keyAltName);
  338. *
  339. * if (!oldKey) {
  340. * // null is returned if there is no matching document with an id matching the supplied id
  341. * }
  342. * ```
  343. */
  344. async removeKeyAltName(_id, keyAltName) {
  345. const { db: dbName, collection: collectionName } = utils_1.MongoDBCollectionNamespace.fromString(this._keyVaultNamespace);
  346. const pipeline = [
  347. {
  348. $set: {
  349. keyAltNames: {
  350. $cond: [
  351. {
  352. $eq: ['$keyAltNames', [keyAltName]]
  353. },
  354. '$$REMOVE',
  355. {
  356. $filter: {
  357. input: '$keyAltNames',
  358. cond: {
  359. $ne: ['$$this', keyAltName]
  360. }
  361. }
  362. }
  363. ]
  364. }
  365. }
  366. }
  367. ];
  368. const value = await this._keyVaultClient
  369. .db(dbName)
  370. .collection(collectionName)
  371. .findOneAndUpdate({ _id }, pipeline, {
  372. writeConcern: { w: 'majority' },
  373. returnDocument: 'before'
  374. });
  375. return value;
  376. }
  377. /**
  378. * A convenience method for creating an encrypted collection.
  379. * This method will create data keys for any encryptedFields that do not have a `keyId` defined
  380. * and then create a new collection with the full set of encryptedFields.
  381. *
  382. * @param db - A Node.js driver Db object with which to create the collection
  383. * @param name - The name of the collection to be created
  384. * @param options - Options for createDataKey and for createCollection
  385. * @returns created collection and generated encryptedFields
  386. * @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.
  387. * @throws MongoCryptCreateEncryptedCollectionError - If creating the collection fails, an error will be rejected that has the entire `encryptedFields` that were created.
  388. */
  389. async createEncryptedCollection(db, name, options) {
  390. const { provider, masterKey, createCollectionOptions: { encryptedFields: { ...encryptedFields }, ...createCollectionOptions } } = options;
  391. if (Array.isArray(encryptedFields.fields)) {
  392. const createDataKeyPromises = encryptedFields.fields.map(async (field) => field == null || typeof field !== 'object' || field.keyId != null
  393. ? field
  394. : {
  395. ...field,
  396. keyId: await this.createDataKey(provider, { masterKey })
  397. });
  398. const createDataKeyResolutions = await Promise.allSettled(createDataKeyPromises);
  399. encryptedFields.fields = createDataKeyResolutions.map((resolution, index) => resolution.status === 'fulfilled' ? resolution.value : encryptedFields.fields[index]);
  400. const rejection = createDataKeyResolutions.find((result) => result.status === 'rejected');
  401. if (rejection != null) {
  402. throw new errors_1.MongoCryptCreateDataKeyError(encryptedFields, { cause: rejection.reason });
  403. }
  404. }
  405. try {
  406. const collection = await db.createCollection(name, {
  407. ...createCollectionOptions,
  408. encryptedFields
  409. });
  410. return { collection, encryptedFields };
  411. }
  412. catch (cause) {
  413. throw new errors_1.MongoCryptCreateEncryptedCollectionError(encryptedFields, { cause });
  414. }
  415. }
  416. /**
  417. * Explicitly encrypt a provided value. Note that either `options.keyId` or `options.keyAltName` must
  418. * be specified. Specifying both `options.keyId` and `options.keyAltName` is considered an error.
  419. *
  420. * @param value - The value that you wish to serialize. Must be of a type that can be serialized into BSON
  421. * @param options -
  422. * @returns a Promise that either resolves with the encrypted value, or rejects with an error.
  423. *
  424. * @example
  425. * ```ts
  426. * // Encryption with async/await api
  427. * async function encryptMyData(value) {
  428. * const keyId = await clientEncryption.createDataKey('local');
  429. * return clientEncryption.encrypt(value, { keyId, algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' });
  430. * }
  431. * ```
  432. *
  433. * @example
  434. * ```ts
  435. * // Encryption using a keyAltName
  436. * async function encryptMyData(value) {
  437. * await clientEncryption.createDataKey('local', { keyAltNames: 'mySpecialKey' });
  438. * return clientEncryption.encrypt(value, { keyAltName: 'mySpecialKey', algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' });
  439. * }
  440. * ```
  441. */
  442. async encrypt(value, options) {
  443. return this._encrypt(value, false, options);
  444. }
  445. /**
  446. * Encrypts a Match Expression or Aggregate Expression to query a range index.
  447. *
  448. * Only supported when queryType is "rangePreview" and algorithm is "RangePreview".
  449. *
  450. * @experimental The Range algorithm is experimental only. It is not intended for production use. It is subject to breaking changes.
  451. *
  452. * @param expression - a BSON document of one of the following forms:
  453. * 1. A Match Expression of this form:
  454. * `{$and: [{<field>: {$gt: <value1>}}, {<field>: {$lt: <value2> }}]}`
  455. * 2. An Aggregate Expression of this form:
  456. * `{$and: [{$gt: [<fieldpath>, <value1>]}, {$lt: [<fieldpath>, <value2>]}]}`
  457. *
  458. * `$gt` may also be `$gte`. `$lt` may also be `$lte`.
  459. *
  460. * @param options -
  461. * @returns Returns a Promise that either resolves with the encrypted value or rejects with an error.
  462. */
  463. async encryptExpression(expression, options) {
  464. return this._encrypt(expression, true, options);
  465. }
  466. /**
  467. * Explicitly decrypt a provided encrypted value
  468. *
  469. * @param value - An encrypted value
  470. * @returns a Promise that either resolves with the decrypted value, or rejects with an error
  471. *
  472. * @example
  473. * ```ts
  474. * // Decrypting value with async/await API
  475. * async function decryptMyValue(value) {
  476. * return clientEncryption.decrypt(value);
  477. * }
  478. * ```
  479. */
  480. async decrypt(value) {
  481. const valueBuffer = (0, bson_1.serialize)({ v: value });
  482. const context = this._mongoCrypt.makeExplicitDecryptionContext(valueBuffer);
  483. const stateMachine = new state_machine_1.StateMachine({
  484. proxyOptions: this._proxyOptions,
  485. tlsOptions: this._tlsOptions
  486. });
  487. const { v } = await stateMachine.execute(this, context);
  488. return v;
  489. }
  490. /**
  491. * @internal
  492. * Ask the user for KMS credentials.
  493. *
  494. * This returns anything that looks like the kmsProviders original input
  495. * option. It can be empty, and any provider specified here will override
  496. * the original ones.
  497. */
  498. async askForKMSCredentials() {
  499. return (0, index_1.refreshKMSCredentials)(this._kmsProviders);
  500. }
  501. static get libmongocryptVersion() {
  502. return ClientEncryption.getMongoCrypt().libmongocryptVersion;
  503. }
  504. /**
  505. * @internal
  506. * A helper that perform explicit encryption of values and expressions.
  507. * Explicitly encrypt a provided value. Note that either `options.keyId` or `options.keyAltName` must
  508. * be specified. Specifying both `options.keyId` and `options.keyAltName` is considered an error.
  509. *
  510. * @param value - The value that you wish to encrypt. Must be of a type that can be serialized into BSON
  511. * @param expressionMode - a boolean that indicates whether or not to encrypt the value as an expression
  512. * @param options - options to pass to encrypt
  513. * @returns the raw result of the call to stateMachine.execute(). When expressionMode is set to true, the return
  514. * value will be a bson document. When false, the value will be a BSON Binary.
  515. *
  516. */
  517. async _encrypt(value, expressionMode, options) {
  518. const { algorithm, keyId, keyAltName, contentionFactor, queryType, rangeOptions } = options;
  519. const contextOptions = {
  520. expressionMode,
  521. algorithm
  522. };
  523. if (keyId) {
  524. contextOptions.keyId = keyId.buffer;
  525. }
  526. if (keyAltName) {
  527. if (keyId) {
  528. throw new errors_1.MongoCryptInvalidArgumentError(`"options" cannot contain both "keyId" and "keyAltName"`);
  529. }
  530. if (typeof keyAltName !== 'string') {
  531. throw new errors_1.MongoCryptInvalidArgumentError(`"options.keyAltName" must be of type string, but was of type ${typeof keyAltName}`);
  532. }
  533. contextOptions.keyAltName = (0, bson_1.serialize)({ keyAltName });
  534. }
  535. if (typeof contentionFactor === 'number' || typeof contentionFactor === 'bigint') {
  536. contextOptions.contentionFactor = contentionFactor;
  537. }
  538. if (typeof queryType === 'string') {
  539. contextOptions.queryType = queryType;
  540. }
  541. if (typeof rangeOptions === 'object') {
  542. contextOptions.rangeOptions = (0, bson_1.serialize)(rangeOptions);
  543. }
  544. const valueBuffer = (0, bson_1.serialize)({ v: value });
  545. const stateMachine = new state_machine_1.StateMachine({
  546. proxyOptions: this._proxyOptions,
  547. tlsOptions: this._tlsOptions
  548. });
  549. const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions);
  550. const result = await stateMachine.execute(this, context);
  551. return result.v;
  552. }
  553. }
  554. exports.ClientEncryption = ClientEncryption;
  555. //# sourceMappingURL=client_encryption.js.map