diff --git a/.evergreen/run-azure-kms-tests.sh b/.evergreen/run-azure-kms-tests.sh index 741b7135b8f..87292d49d6f 100644 --- a/.evergreen/run-azure-kms-tests.sh +++ b/.evergreen/run-azure-kms-tests.sh @@ -9,8 +9,6 @@ source ".evergreen/init-node-and-npm-env.sh" set -o xtrace -npm install mongodb-client-encryption@alpha - export MONGODB_URI="mongodb://localhost:27017" export EXPECTED_AZUREKMS_OUTCOME=${EXPECTED_AZUREKMS_OUTCOME:-omitted} diff --git a/.evergreen/run-custom-csfle-tests.sh b/.evergreen/run-custom-csfle-tests.sh index a10f4af5b61..b71d0904e6e 100644 --- a/.evergreen/run-custom-csfle-tests.sh +++ b/.evergreen/run-custom-csfle-tests.sh @@ -57,8 +57,6 @@ bash ./etc/build-static.sh npm run rebuild # just in case this is necessary? -ls -ls lib BINDINGS_DIR=$(pwd) popd # libmongocrypt/bindings/node popd # ../csfle-deps-tmp diff --git a/.evergreen/run-gcp-kms-tests.sh b/.evergreen/run-gcp-kms-tests.sh index 35cb203b073..510894f3201 100644 --- a/.evergreen/run-gcp-kms-tests.sh +++ b/.evergreen/run-gcp-kms-tests.sh @@ -9,7 +9,6 @@ source ".evergreen/init-node-and-npm-env.sh" set -o xtrace -npm install mongodb-client-encryption@alpha npm install gcp-metadata export MONGODB_URI="mongodb://localhost:27017" diff --git a/.evergreen/run-serverless-tests.sh b/.evergreen/run-serverless-tests.sh index df95d818448..b0c58637b19 100755 --- a/.evergreen/run-serverless-tests.sh +++ b/.evergreen/run-serverless-tests.sh @@ -10,8 +10,6 @@ if [ -z ${MONGODB_URI+omitted} ]; then echo "MONGODB_URI is unset" && exit 1; fi if [ -z ${SERVERLESS_ATLAS_USER+omitted} ]; then echo "SERVERLESS_ATLAS_USER is unset" && exit 1; fi if [ -z ${SERVERLESS_ATLAS_PASSWORD+omitted} ]; then echo "SERVERLESS_ATLAS_PASSWORD is unset" && exit 1; fi -npm install mongodb-client-encryption@alpha - npx mocha \ --config test/mocha_mongodb.json \ test/integration/crud/crud.spec.test.js \ diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index 5daf11d3569..080857cd381 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -52,7 +52,6 @@ else source "$DRIVERS_TOOLS"/.evergreen/csfle/set-temp-creds.sh fi -npm install mongodb-client-encryption@alpha npm install @mongodb-js/zstd npm install snappy diff --git a/package-lock.json b/package-lock.json index d9a5f0d05a9..56f25df94f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "js-yaml": "^4.1.0", "mocha": "^10.2.0", "mocha-sinon": "^2.1.2", - "mongodb-client-encryption": "^6.0.0-alpha.1", + "mongodb-client-encryption": "6.0.0-alpha.1", "mongodb-legacy": "^5.0.0", "nyc": "^15.1.0", "prettier": "^2.8.8", diff --git a/package.json b/package.json index db79bde634e..ed97baf16b2 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "js-yaml": "^4.1.0", "mocha": "^10.2.0", "mocha-sinon": "^2.1.2", - "mongodb-client-encryption": "^6.0.0-alpha.1", + "mongodb-client-encryption": "6.0.0-alpha.1", "mongodb-legacy": "^5.0.0", "nyc": "^15.1.0", "prettier": "^2.8.8", diff --git a/src/bson.ts b/src/bson.ts index 7f335f1c131..80c54360d7b 100644 --- a/src/bson.ts +++ b/src/bson.ts @@ -19,7 +19,8 @@ export { MinKey, ObjectId, serialize, - Timestamp + Timestamp, + UUID } from 'bson'; /** diff --git a/src/client-side-encryption/client_encryption.ts b/src/client-side-encryption/client_encryption.ts index 641ffbcd6f1..66e15aaa493 100644 --- a/src/client-side-encryption/client_encryption.ts +++ b/src/client-side-encryption/client_encryption.ts @@ -5,7 +5,7 @@ import type { MongoCryptOptions } from 'mongodb-client-encryption'; -import { type Binary, type Document, type Long, serialize } from '../bson'; +import { type Binary, type Document, type Long, serialize, type UUID } from '../bson'; import { type AnyBulkWriteOperation, type BulkWriteResult } from '../bulk/common'; import { type ProxyOptions } from '../cmap/connection'; import { type Collection } from '../collection'; @@ -16,8 +16,7 @@ import { type MongoClient } from '../mongo_client'; import { type Filter } from '../mongo_types'; import { type CreateCollectionOptions } from '../operations/create_collection'; import { type DeleteResult } from '../operations/delete'; -import { type Callback, MongoDBCollectionNamespace } from '../utils'; -import { maybeCallback, promiseOrCallback } from './common'; +import { MongoDBCollectionNamespace } from '../utils'; import * as cryptoCallbacks from './crypto_callbacks'; import { MongoCryptCreateDataKeyError, @@ -36,7 +35,7 @@ import { type CSFLEKMSTlsOptions, StateMachine } from './state_machine'; * The schema for a DataKey in the key vault collection. */ export interface DataKey { - _id: Binary; + _id: UUID; version?: number; keyAltNames?: string[]; keyMaterial: Binary; @@ -133,18 +132,6 @@ export class ClientEncryption { * * @example * ```ts - * // 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 - * ```ts * // Using async/await to create a local key * const dataKeyId = await clientEncryption.createDataKey('local'); * ``` @@ -172,21 +159,10 @@ export class ClientEncryption { * }); * ``` */ - createDataKey( + async createDataKey( provider: ClientEncryptionDataKeyProvider, - options?: ClientEncryptionCreateDataKeyProviderOptions, - callback?: Callback - ) { - if (typeof options === 'function') { - callback = options; - options = {}; - } - if (options == null) { - options = {}; - } - - const dataKey = Object.assign({ provider }, options.masterKey); - + options: ClientEncryptionCreateDataKeyProviderOptions = {} + ): Promise { if (options.keyAltNames && !Array.isArray(options.keyAltNames)) { throw new MongoCryptInvalidArgumentError( `Option "keyAltNames" must be an array of strings, but was of type ${typeof options.keyAltNames}.` @@ -211,42 +187,33 @@ export class ClientEncryption { keyMaterial = serialize({ keyMaterial: options.keyMaterial }); } - const dataKeyBson = serialize(dataKey); + const dataKeyBson = serialize({ + provider, + ...options.masterKey + }); + const context = this._mongoCrypt.makeDataKeyContext(dataKeyBson, { keyAltNames, keyMaterial }); + const stateMachine = new StateMachine({ proxyOptions: this._proxyOptions, tlsOptions: this._tlsOptions }); - // @ts-expect-error We did not convert promiseOrCallback to TS - return promiseOrCallback(callback, cb => { - stateMachine.execute(this, context, (err, dataKey) => { - if (err || !dataKey) { - cb(err, null); - return; - } + const dataKey = await stateMachine.executeAsync(this, context); - const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString( - this._keyVaultNamespace - ); + const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString( + this._keyVaultNamespace + ); - this._keyVaultClient - .db(dbName) - .collection(collectionName) - .insertOne(dataKey, { writeConcern: { w: 'majority' } }) - .then( - result => { - return cb(null, result.insertedId); - }, - err => { - cb(err, null); - } - ); - }); - }); + const { insertedId } = await this._keyVaultClient + .db(dbName) + .collection(collectionName) + .insertOne(dataKey, { writeConcern: { w: 'majority' } }); + + return insertedId; } /** @@ -601,21 +568,7 @@ export class ClientEncryption { * * @param value - The value that you wish to serialize. Must be of a type that can be serialized into BSON * @param options - - * @param callback - Optional callback to invoke when value is encrypted - * @returns 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 - * ```ts - * // 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); - * }); - * } - * ``` + * @returns a Promise that either resolves with the encrypted value, or rejects with an error. * * @example * ```ts @@ -635,12 +588,8 @@ export class ClientEncryption { * } * ``` */ - encrypt( - value: unknown, - options: ClientEncryptionEncryptOptions, - callback: Callback - ): Promise | void { - return maybeCallback(() => this._encrypt(value, false, options), callback); + async encrypt(value: unknown, options: ClientEncryptionEncryptOptions): Promise { + return this._encrypt(value, false, options); } /** @@ -672,16 +621,7 @@ export class ClientEncryption { * Explicitly decrypt a provided encrypted value * * @param value - An encrypted value - * @param callback - Optional callback to invoke when value is decrypted - * @returns If no callback is provided, returns a Promise that either resolves with the decrypted value, or rejects with an error. If a callback is provided, returns nothing. - * - * ```ts - * @example - * // Decrypting value with callback API - * function decryptMyValue(value, callback) { - * clientEncryption.decrypt(value, callback); - * } - * ``` + * @returns a Promise that either resolves with the decrypted value, or rejects with an error * * @example * ```ts @@ -691,7 +631,7 @@ export class ClientEncryption { * } * ``` */ - decrypt(value: Binary, callback?: Callback): Promise | void { + async decrypt(value: Binary): Promise { const valueBuffer = serialize({ v: value }); const context = this._mongoCrypt.makeExplicitDecryptionContext(valueBuffer); @@ -700,17 +640,9 @@ export class ClientEncryption { tlsOptions: this._tlsOptions }); - // @ts-expect-error We did not convert promiseOrCallback to TS - return promiseOrCallback(callback, cb => { - stateMachine.execute<{ v: T }>(this, context, (err, result) => { - if (err || !result) { - cb(err, null); - return; - } + const { v } = await stateMachine.executeAsync<{ v: T }>(this, context); - cb(null, result.v); - }); - }); + return v; } /** diff --git a/src/index.ts b/src/index.ts index 11ac502fcef..436cc08d33c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,8 @@ export { MaxKey, MinKey, ObjectId, - Timestamp + Timestamp, + UUID } from './bson'; export { AnyBulkWriteOperation, BulkWriteOptions, MongoBulkWriteError } from './bulk/common'; export { ClientEncryption } from './client-side-encryption/client_encryption'; diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.12.deadlock.test.ts b/test/integration/client-side-encryption/client_side_encryption.prose.12.deadlock.test.ts index edde58f2fbb..4f2320c2612 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.12.deadlock.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.prose.12.deadlock.test.ts @@ -2,7 +2,6 @@ import * as BSON from 'bson'; import { expect } from 'chai'; import { readFileSync } from 'fs'; import * as path from 'path'; -import * as util from 'util'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption'; @@ -138,11 +137,8 @@ describe('Connection Pool Deadlock Prevention', function () { keyVaultClient: this.keyVaultClient, extraOptions: getEncryptExtraOptions() }); - this.clientEncryption.encryptPromisified = util.promisify( - this.clientEncryption.encrypt.bind(this.clientEncryption) - ); - this.ciphertext = await this.clientEncryption.encryptPromisified('string0', { + this.ciphertext = await this.clientEncryption.encrypt('string0', { algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic', keyAltName: 'local' }); diff --git a/test/integration/node-specific/client_encryption.test.ts b/test/integration/node-specific/client_encryption.test.ts index c01fe0d0c32..351c5256b19 100644 --- a/test/integration/node-specific/client_encryption.test.ts +++ b/test/integration/node-specific/client_encryption.test.ts @@ -11,7 +11,7 @@ import { import { MongoCryptInvalidArgumentError } from '../../../src/client-side-encryption/errors'; /* eslint-disable @typescript-eslint/no-restricted-imports */ import { StateMachine } from '../../../src/client-side-encryption/state_machine'; -import { Binary, type Collection, Int32, Long, type MongoClient } from '../../mongodb'; +import { Binary, type Collection, Int32, Long, type MongoClient, UUID } from '../../mongodb'; function readHttpResponse(path) { let data = readFileSync(path, 'utf8').toString(); @@ -160,7 +160,7 @@ describe('ClientEncryption integration tests', function () { } ); - it('should fail to create a data key if keyMaterial is wrong', metadata, function (done) { + it('should fail to create a data key if keyMaterial is wrong', metadata, async function () { const encryption = new ClientEncryption(client, { keyVaultNamespace: 'client.encryption', kmsProviders: { local: { key: 'A'.repeat(128) } } @@ -169,13 +169,8 @@ describe('ClientEncryption integration tests', function () { const dataKeyOptions = { keyMaterial: new Binary(Buffer.alloc(97)) }; - try { - encryption.createDataKey('local', dataKeyOptions); - expect.fail('missed exception'); - } catch (err) { - expect(err.message).to.equal('keyMaterial should have length 96, but has length 97'); - done(); - } + const error = await encryption.createDataKey('local', dataKeyOptions).catch(error => error); + expect(error.message).to.equal('keyMaterial should have length 96, but has length 97'); }); it( @@ -496,6 +491,26 @@ describe('ClientEncryption integration tests', function () { }); }); + describe('createDataKey()', () => { + let clientEncryption; + + beforeEach(function () { + clientEncryption = new ClientEncryption(client, { + keyVaultNamespace: 'client.encryption', + kmsProviders: { local: { key: Buffer.alloc(96) } } + }); + }); + + it('returns a UUID instance', async () => { + const dataKey = await clientEncryption.createDataKey('local', { + name: 'local', + kmsProviders: { local: { key: Buffer.alloc(96) } } + }); + + expect(dataKey).to.be.instanceOf(UUID); + }); + }); + describe('ClientEncryptionKeyAltNames', function () { let client: MongoClient; let clientEncryption: ClientEncryption; @@ -539,23 +554,21 @@ describe('ClientEncryption integration tests', function () { } describe('errors', function () { - [42, 'hello', { keyAltNames: 'foobar' }, /foobar/].forEach(val => { - it(`should fail if typeof keyAltNames = ${typeof val}`, metadata, function () { + for (const val of [42, 'hello', { keyAltNames: 'foobar' }, /foobar/]) { + it(`should fail if typeof keyAltNames = ${typeof val}`, metadata, async function () { const options = makeOptions(val); - expect(() => clientEncryption.createDataKey('aws', options, () => undefined)).to.throw( - MongoCryptInvalidArgumentError - ); + const error = await clientEncryption.createDataKey('aws', options).catch(error => error); + expect(error).to.be.instanceOf(MongoCryptInvalidArgumentError); }); - }); + } - [undefined, null, 42, { keyAltNames: 'foobar' }, ['foobar'], /foobar/].forEach(val => { - it(`should fail if typeof keyAltNames[x] = ${typeof val}`, metadata, function () { + for (const val of [undefined, null, 42, { keyAltNames: 'foobar' }, ['foobar'], /foobar/]) { + it(`should fail if typeof keyAltNames[x] = ${typeof val}`, metadata, async function () { const options = makeOptions([val]); - expect(() => clientEncryption.createDataKey('aws', options, () => undefined)).to.throw( - MongoCryptInvalidArgumentError - ); + const error = await clientEncryption.createDataKey('aws', options).catch(error => error); + expect(error).to.be.instanceOf(MongoCryptInvalidArgumentError); }); - }); + } }); it('should create a key with keyAltNames', metadata, async function () { diff --git a/test/integration/node-specific/crypto_callbacks.test.ts b/test/integration/node-specific/crypto_callbacks.test.ts index e3564164eb7..f9cb4d78e60 100644 --- a/test/integration/node-specific/crypto_callbacks.test.ts +++ b/test/integration/node-specific/crypto_callbacks.test.ts @@ -25,9 +25,15 @@ const SKIP_AWS_TESTS = [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AW describe('cryptoCallbacks', function () { let client: MongoClient; let sandbox; - before(function () { - sandbox = sinon.createSandbox(); - }); + + const hookNames = new Set([ + 'aes256CbcEncryptHook', + 'aes256CbcDecryptHook', + 'randomHook', + 'hmacSha512Hook', + 'hmacSha256Hook', + 'sha256Hook' + ]); beforeEach(function () { if (SKIP_AWS_TESTS) { @@ -35,14 +41,16 @@ describe('cryptoCallbacks', function () { return; } - sandbox.restore(); + sandbox = sinon.createSandbox(); + sandbox.spy(cryptoCallbacks); + client = this.configuration.newClient(); return client.connect(); }); afterEach(async function () { - sandbox.restore(); + sandbox?.restore(); await client?.close(); }); @@ -69,20 +77,7 @@ describe('cryptoCallbacks', function () { expect(output).to.deep.equal(expectedOutput); }).skipReason = 'TODO(NODE-3370): fix key formatting error "asn1_check_tlen:wrong tag"'; - const hookNames = new Set([ - 'aes256CbcEncryptHook', - 'aes256CbcDecryptHook', - 'randomHook', - 'hmacSha512Hook', - 'hmacSha256Hook', - 'sha256Hook' - ]); - - it('should invoke crypto callbacks when doing encryption', function (done) { - for (const name of hookNames) { - sandbox.spy(cryptoCallbacks, name); - } - + it('should invoke crypto callbacks when doing encryption', async function () { function assertCertainHooksCalled(expectedSet?) { expectedSet = expectedSet || new Set([]); for (const name of hookNames) { @@ -102,50 +97,32 @@ describe('cryptoCallbacks', function () { kmsProviders }); - try { - assertCertainHooksCalled(); - } catch (e) { - return done(e); - } + assertCertainHooksCalled(); - encryption.createDataKey('aws', dataKeyOptions, (err, dataKey) => { - try { - expect(err).to.not.exist; - assertCertainHooksCalled(new Set(['hmacSha256Hook', 'sha256Hook', 'randomHook'])); - } catch (e) { - return done(e); - } + const dataKeyId = await encryption.createDataKey('aws', dataKeyOptions); + assertCertainHooksCalled(new Set(['hmacSha256Hook', 'sha256Hook', 'randomHook'])); - const encryptOptions = { - keyId: dataKey, - algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' - }; - - encryption.encrypt('hello', encryptOptions, (err, encryptedValue) => { - try { - expect(err).to.not.exist; - assertCertainHooksCalled( - new Set(['aes256CbcEncryptHook', 'hmacSha512Hook', 'hmacSha256Hook', 'sha256Hook']) - ); - } catch (e) { - return done(e); - } - encryption.decrypt(encryptedValue, err => { - try { - expect(err).to.not.exist; - assertCertainHooksCalled(new Set(['aes256CbcDecryptHook', 'hmacSha512Hook'])); - } catch (e) { - return done(e); - } - done(); - }); - }); - }); + const encryptOptions = { + keyId: dataKeyId, + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' + }; + + const encryptedValue = await encryption.encrypt('hello', encryptOptions); + assertCertainHooksCalled( + new Set(['aes256CbcEncryptHook', 'hmacSha512Hook', 'hmacSha256Hook', 'sha256Hook']) + ); + + await encryption.decrypt(encryptedValue); + assertCertainHooksCalled(new Set(['aes256CbcDecryptHook', 'hmacSha512Hook'])); }); describe('error testing', function () { - ['aes256CbcEncryptHook', 'aes256CbcDecryptHook', 'hmacSha512Hook'].forEach(hookName => { - it(`should properly propagate an error when ${hookName} fails`, function (done) { + beforeEach(async function () { + sandbox?.restore(); + }); + + for (const hookName of ['aes256CbcEncryptHook', 'aes256CbcDecryptHook', 'hmacSha512Hook']) { + it(`should properly propagate an error when ${hookName} fails`, async function () { const error = new Error('some random error text'); sandbox.stub(cryptoCallbacks, hookName).returns(error); @@ -154,40 +131,28 @@ describe('cryptoCallbacks', function () { kmsProviders }); - function finish(err) { - try { - expect(err, 'Expected an error to exist').to.exist; - expect(err).to.have.property('message', error.message); - done(); - } catch (e) { - done(e); - } - } + const result = await (async () => { + const dataKeyId = await encryption.createDataKey('aws', dataKeyOptions); + const encryptOptions = { + keyId: dataKeyId, + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' + }; + + const encryptedValue = await encryption.encrypt('hello', encryptOptions); + await encryption.decrypt(encryptedValue); + })().then( + () => null, + error => error + ); - try { - encryption.createDataKey('aws', dataKeyOptions, (err, dataKey) => { - if (err) return finish(err); - - const encryptOptions = { - keyId: dataKey, - algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' - }; - - encryption.encrypt('hello', encryptOptions, (err, encryptedValue) => { - if (err) return finish(err); - encryption.decrypt(encryptedValue, err => finish(err)); - }); - }); - } catch (e) { - done(new Error('We should not be here')); - } + expect(result).to.be.instanceOf(Error); }); - }); + } // These ones will fail with an error, but that error will get overridden // with "failed to create KMS message" in mongocrypt-kms-ctx.c - ['hmacSha256Hook', 'sha256Hook'].forEach(hookName => { - it(`should error with a specific kms error when ${hookName} fails`, function () { + for (const hookName of ['hmacSha256Hook', 'sha256Hook']) { + it(`should error with a specific kms error when ${hookName} fails`, async function () { const error = new Error('some random error text'); sandbox.stub(cryptoCallbacks, hookName).returns(error); @@ -196,13 +161,12 @@ describe('cryptoCallbacks', function () { kmsProviders }); - expect(() => encryption.createDataKey('aws', dataKeyOptions, () => undefined)).to.throw( - 'failed to create KMS message' - ); + const result = await encryption.createDataKey('aws', dataKeyOptions).catch(error => error); + expect(result).to.match(/failed to create KMS message/); }); - }); + } - it('should error synchronously with error when randomHook fails', function (done) { + it('should error asynchronously with error when randomHook fails', async function () { const error = new Error('some random error text'); sandbox.stub(cryptoCallbacks, 'randomHook').returns(error); @@ -211,18 +175,8 @@ describe('cryptoCallbacks', function () { kmsProviders }); - try { - encryption.createDataKey('aws', dataKeyOptions, () => { - done(new Error('We should not be here')); - }); - } catch (err) { - try { - expect(err).to.have.property('message', 'some random error text'); - done(); - } catch (e) { - done(e); - } - } + const result = await encryption.createDataKey('aws', dataKeyOptions).catch(error => error); + expect(result).to.have.property('message', 'some random error text'); }); }); }); diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 8a2ef6ee59a..cc5184f24e5 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -128,6 +128,7 @@ const EXPECTED_EXPORTS = [ 'TopologyOpeningEvent', 'TopologyType', 'UnorderedBulkOperation', + 'UUID', 'WriteConcern' ];