From 8e344851119c6847a147ee78426e5c2256319b4e Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Wed, 1 Jun 2022 18:42:40 +0200 Subject: [PATCH 1/5] feat: support indexKeyId, contentionFactor, queryType, keyMaterial MONGOSH-1209 --- .../src/field-level-encryption.spec.ts | 28 ++++- .../shell-api/src/field-level-encryption.ts | 103 ++++++++++++++---- 2 files changed, 109 insertions(+), 22 deletions(-) diff --git a/packages/shell-api/src/field-level-encryption.spec.ts b/packages/shell-api/src/field-level-encryption.spec.ts index e73469023a..f7cbddaa4c 100644 --- a/packages/shell-api/src/field-level-encryption.spec.ts +++ b/packages/shell-api/src/field-level-encryption.spec.ts @@ -1,5 +1,5 @@ import { MongoshInvalidInputError } from '@mongosh/errors'; -import { bson, ClientEncryption as FLEClientEncryption, ClientEncryptionTlsOptions, ServiceProvider } from '@mongosh/service-provider-core'; +import { bson, ClientEncryption as FLEClientEncryption, ClientEncryptionTlsOptions, ServiceProvider, ClientEncryptionEncryptOptions } from '@mongosh/service-provider-core'; import { expect } from 'chai'; import { EventEmitter } from 'events'; import { promises as fs } from 'fs'; @@ -47,6 +47,12 @@ const AWS_KMS = { }; const ALGO = 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'; +const ENCRYPT_OPTIONS = { + algorithm: ALGO as ClientEncryptionEncryptOptions['algorithm'], + indexKeyId: new bson.Binary(Buffer.from('12345678123498761234123456789012', 'hex'), 4), + contentionFactor: 10, + queryType: 'Equality' +}; const RAW_CLIENT = { client: 1 } as any; @@ -161,12 +167,18 @@ describe('Field Level Encryption', () => { }); }); describe('encrypt', () => { - it('calls encrypt on libmongoc', async() => { + it('calls encrypt with algorithm on libmongoc', async() => { const value = new bson.ObjectId(); libmongoc.encrypt.resolves(); await clientEncryption.encrypt(KEY_ID, value, ALGO); expect(libmongoc.encrypt).calledOnceWithExactly(value, { keyId: KEY_ID, algorithm: ALGO }); }); + it('calls encrypt with algorithm, indexKeyId, contentionFactor, and queryType on libmongoc', async() => { + const value = new bson.ObjectId(); + libmongoc.encrypt.resolves(); + await clientEncryption.encrypt(KEY_ID, value, ENCRYPT_OPTIONS); + expect(libmongoc.encrypt).calledOnceWithExactly(value, { keyId: KEY_ID, ...ENCRYPT_OPTIONS }); + }); it('throw if failed', async() => { const value = new bson.ObjectId(); const expectedError = new Error(); @@ -384,6 +396,18 @@ describe('Field Level Encryption', () => { ]); expect(result).to.deep.equal(r2.value); }); + it('reads keyAltNames and keyMaterial from DataKeyEncryptionKeyOptions', async() => { + const rawResult = { result: 1 }; + const keyVault = await mongo.getKeyVault(); + const options = { + keyAltNames: ['b'], + keyMaterial: new bson.Binary(Buffer.from('12345678123498761234123456789012', 'hex'), 4) + }; + + libmongoc.createDataKey.resolves(rawResult); + await keyVault.createKey('local', options); + expect(libmongoc.createDataKey).calledOnceWithExactly('local', options); + }); }); }); describe('Mongo constructor FLE options', () => { diff --git a/packages/shell-api/src/field-level-encryption.ts b/packages/shell-api/src/field-level-encryption.ts index 09ff412693..669de16f04 100644 --- a/packages/shell-api/src/field-level-encryption.ts +++ b/packages/shell-api/src/field-level-encryption.ts @@ -47,6 +47,39 @@ export interface ClientSideFieldLevelEncryptionOptions { bypassQueryAnalysis?: boolean; } +type MasterKey = AWSEncryptionKeyOptions | AzureEncryptionKeyOptions | GCPEncryptionKeyOptions | undefined; +type AltNames = string[] | undefined; + +type DataKeyEncryptionKeyOptions = { + masterKey?: MasterKey; + keyAltNames?: AltNames; + keyMaterial?: Buffer | BinaryType +}; + +type MasterKeyOrAltNamesOrDataKeyOptions = MasterKey | DataKeyEncryptionKeyOptions | AltNames | string | undefined; + +const isDataKeyEncryptionKeyOptions = (options: MasterKeyOrAltNamesOrDataKeyOptions): options is DataKeyEncryptionKeyOptions => { + const dataKeyOptions = options as DataKeyEncryptionKeyOptions; + + return ( + !Array.isArray(dataKeyOptions) && + typeof dataKeyOptions === 'object' && + ('masterKey' in dataKeyOptions || 'keyAltNames' in dataKeyOptions || 'keyMaterial' in dataKeyOptions) + ); +}; + +const isMasterKey = (options: MasterKeyOrAltNamesOrDataKeyOptions): options is MasterKey => { + const masterKeyOptions = options as MasterKey; + + return ( + !Array.isArray(masterKeyOptions) && + typeof masterKeyOptions === 'object' && + !('masterKey' in masterKeyOptions) && + !('keyAltNames' in masterKeyOptions) && + !('masterKey' in masterKeyOptions) + ); +}; + @shellApiClassDefault @classPlatforms([ ReplPlatform.CLI ] ) export class ClientEncryption extends ShellApiWithMongoClass { @@ -80,15 +113,24 @@ export class ClientEncryption extends ShellApiWithMongoClass { async encrypt( encryptionId: BinaryType, value: any, - encryptionAlgorithm: ClientEncryptionEncryptOptions['algorithm'] + algorithmOrEncryptionOptions: ClientEncryptionEncryptOptions['algorithm'] | ClientEncryptionEncryptOptions ): Promise { - assertArgsDefinedType([encryptionId, value, encryptionAlgorithm], [true, true, true], 'ClientEncryption.encrypt'); + let encryptionOptions: ClientEncryptionEncryptOptions; + if (typeof algorithmOrEncryptionOptions === 'object') { + encryptionOptions = { + keyId: encryptionId, + ...algorithmOrEncryptionOptions + }; + } else { + encryptionOptions = { + keyId: encryptionId, + algorithm: algorithmOrEncryptionOptions + }; + } + assertArgsDefinedType([encryptionId, value, encryptionOptions], [true, true, true], 'ClientEncryption.encrypt'); return await this._libmongocrypt.encrypt( value, - { - keyId: encryptionId, - algorithm: encryptionAlgorithm - } + encryptionOptions ); } @@ -160,29 +202,46 @@ export class KeyVault extends ShellApiWithMongoClass { createKey(kms: 'local', keyAltNames?: string[]): Promise createKey(kms: ClientEncryptionDataKeyProvider, legacyMasterKey: string, keyAltNames?: string[]): Promise - createKey(kms: ClientEncryptionDataKeyProvider, options: AWSEncryptionKeyOptions | AzureEncryptionKeyOptions | GCPEncryptionKeyOptions | undefined): Promise - createKey(kms: ClientEncryptionDataKeyProvider, options: AWSEncryptionKeyOptions | AzureEncryptionKeyOptions | GCPEncryptionKeyOptions | undefined, keyAltNames: string[]): Promise + createKey(kms: ClientEncryptionDataKeyProvider, options: MasterKey | DataKeyEncryptionKeyOptions | undefined): Promise + createKey(kms: ClientEncryptionDataKeyProvider, options: MasterKey | DataKeyEncryptionKeyOptions | undefined, keyAltNames: string[]): Promise @returnsPromise @apiVersions([1]) + // eslint-disable-next-line complexity async createKey( kms: ClientEncryptionDataKeyProvider, - masterKeyOrAltNames?: AWSEncryptionKeyOptions | AzureEncryptionKeyOptions | GCPEncryptionKeyOptions | string | undefined | string[], - keyAltNames?: string[] + masterKeyOrAltNamesOrDataKeyOptions?: MasterKeyOrAltNamesOrDataKeyOptions, + legacyKeyAltNames?: string[] ): Promise { + let masterKey: MasterKey; + let keyAltNames; + let keyMaterial; + + if (isDataKeyEncryptionKeyOptions(masterKeyOrAltNamesOrDataKeyOptions)) { + masterKey = masterKeyOrAltNamesOrDataKeyOptions?.masterKey; + keyAltNames = masterKeyOrAltNamesOrDataKeyOptions?.keyAltNames; + keyMaterial = masterKeyOrAltNamesOrDataKeyOptions?.keyMaterial; + } else if (isMasterKey(masterKeyOrAltNamesOrDataKeyOptions)) { + masterKey = masterKeyOrAltNamesOrDataKeyOptions; + } + + if (legacyKeyAltNames) { + keyAltNames = legacyKeyAltNames; + } + assertArgsDefinedType([kms], [true], 'KeyVault.createKey'); - if (typeof masterKeyOrAltNames === 'string') { - if (kms === 'local' && masterKeyOrAltNames === '') { + if (typeof masterKeyOrAltNamesOrDataKeyOptions === 'string') { + if (kms === 'local' && masterKeyOrAltNamesOrDataKeyOptions === '') { // allowed in the old shell - even enforced prior to 4.2.3 // https://docs.mongodb.com/manual/reference/method/KeyVault.createKey/ - masterKeyOrAltNames = undefined; + masterKey = undefined; } else { throw new MongoshInvalidInputError( 'KeyVault.createKey does not support providing masterKey as string anymore. For AWS please use createKey("aws", { region: ..., key: ... })', CommonErrors.Deprecated ); } - } else if (Array.isArray(masterKeyOrAltNames)) { + } else if (Array.isArray(masterKeyOrAltNamesOrDataKeyOptions)) { // old signature - one could immediately provide an array of key alt names // not documented but visible in code: https://github.com/mongodb/mongo/blob/eb2b72cf9c0269f086223d499ac9be8a270d268c/src/mongo/shell/keyvault.js#L19 if (kms !== 'local') { @@ -198,16 +257,14 @@ export class KeyVault extends ShellApiWithMongoClass { ); } - keyAltNames = masterKeyOrAltNames; - masterKeyOrAltNames = undefined; + keyAltNames = masterKeyOrAltNamesOrDataKeyOptions; + masterKey = undefined; } } let options: ClientEncryptionCreateDataKeyProviderOptions | undefined; - if (masterKeyOrAltNames) { - options = { - masterKey: masterKeyOrAltNames as ClientEncryptionCreateDataKeyProviderOptions['masterKey'] - }; + if (masterKey) { + options = { masterKey }; } if (keyAltNames) { options = { @@ -215,6 +272,12 @@ export class KeyVault extends ShellApiWithMongoClass { keyAltNames }; } + if (keyMaterial) { + options = { + ...(options ?? {}), + keyMaterial + }; + } return await this._clientEncryption._libmongocrypt.createDataKey(kms, options as ClientEncryptionCreateDataKeyProviderOptions); } From 116492ffbbcdc5addff4f348ba91e92ed05a0d62 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Wed, 1 Jun 2022 18:51:10 +0200 Subject: [PATCH 2/5] refactor: remove type change --- .../shell-api/src/field-level-encryption.ts | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/shell-api/src/field-level-encryption.ts b/packages/shell-api/src/field-level-encryption.ts index 669de16f04..dc2641b330 100644 --- a/packages/shell-api/src/field-level-encryption.ts +++ b/packages/shell-api/src/field-level-encryption.ts @@ -59,24 +59,20 @@ type DataKeyEncryptionKeyOptions = { type MasterKeyOrAltNamesOrDataKeyOptions = MasterKey | DataKeyEncryptionKeyOptions | AltNames | string | undefined; const isDataKeyEncryptionKeyOptions = (options: MasterKeyOrAltNamesOrDataKeyOptions): options is DataKeyEncryptionKeyOptions => { - const dataKeyOptions = options as DataKeyEncryptionKeyOptions; - return ( - !Array.isArray(dataKeyOptions) && - typeof dataKeyOptions === 'object' && - ('masterKey' in dataKeyOptions || 'keyAltNames' in dataKeyOptions || 'keyMaterial' in dataKeyOptions) + !Array.isArray(options) && + typeof options === 'object' && + ('masterKey' in options || 'keyAltNames' in options || 'keyMaterial' in options) ); }; const isMasterKey = (options: MasterKeyOrAltNamesOrDataKeyOptions): options is MasterKey => { - const masterKeyOptions = options as MasterKey; - return ( - !Array.isArray(masterKeyOptions) && - typeof masterKeyOptions === 'object' && - !('masterKey' in masterKeyOptions) && - !('keyAltNames' in masterKeyOptions) && - !('masterKey' in masterKeyOptions) + !Array.isArray(options) && + typeof options === 'object' && + !('masterKey' in options) && + !('keyAltNames' in options) && + !('masterKey' in options) ); }; From 58554c6dd0dfd3ab1ffdc999980022da45bc04af Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Wed, 1 Jun 2022 19:47:40 +0200 Subject: [PATCH 3/5] refactor: expect to fail type --- .../shell-api/src/field-level-encryption.ts | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/shell-api/src/field-level-encryption.ts b/packages/shell-api/src/field-level-encryption.ts index dc2641b330..6d177dafc0 100644 --- a/packages/shell-api/src/field-level-encryption.ts +++ b/packages/shell-api/src/field-level-encryption.ts @@ -47,8 +47,8 @@ export interface ClientSideFieldLevelEncryptionOptions { bypassQueryAnalysis?: boolean; } -type MasterKey = AWSEncryptionKeyOptions | AzureEncryptionKeyOptions | GCPEncryptionKeyOptions | undefined; -type AltNames = string[] | undefined; +type MasterKey = AWSEncryptionKeyOptions | AzureEncryptionKeyOptions | GCPEncryptionKeyOptions; +type AltNames = string[]; type DataKeyEncryptionKeyOptions = { masterKey?: MasterKey; @@ -56,9 +56,9 @@ type DataKeyEncryptionKeyOptions = { keyMaterial?: Buffer | BinaryType }; -type MasterKeyOrAltNamesOrDataKeyOptions = MasterKey | DataKeyEncryptionKeyOptions | AltNames | string | undefined; +type MasterKeyOrAltNamesOrDataKeyOptions = MasterKey | DataKeyEncryptionKeyOptions | AltNames | string; -const isDataKeyEncryptionKeyOptions = (options: MasterKeyOrAltNamesOrDataKeyOptions): options is DataKeyEncryptionKeyOptions => { +const isDataKeyEncryptionKeyOptions = (options?: MasterKeyOrAltNamesOrDataKeyOptions): options is DataKeyEncryptionKeyOptions => { return ( !Array.isArray(options) && typeof options === 'object' && @@ -66,13 +66,11 @@ const isDataKeyEncryptionKeyOptions = (options: MasterKeyOrAltNamesOrDataKeyOpti ); }; -const isMasterKey = (options: MasterKeyOrAltNamesOrDataKeyOptions): options is MasterKey => { +const isMasterKey = (options?: MasterKeyOrAltNamesOrDataKeyOptions): options is MasterKey => { return ( !Array.isArray(options) && typeof options === 'object' && - !('masterKey' in options) && - !('keyAltNames' in options) && - !('masterKey' in options) + !isDataKeyEncryptionKeyOptions(options) ); }; @@ -208,7 +206,7 @@ export class KeyVault extends ShellApiWithMongoClass { masterKeyOrAltNamesOrDataKeyOptions?: MasterKeyOrAltNamesOrDataKeyOptions, legacyKeyAltNames?: string[] ): Promise { - let masterKey: MasterKey; + let masterKey: MasterKey | undefined; let keyAltNames; let keyMaterial; @@ -258,21 +256,16 @@ export class KeyVault extends ShellApiWithMongoClass { } } - let options: ClientEncryptionCreateDataKeyProviderOptions | undefined; + const options: ClientEncryptionCreateDataKeyProviderOptions = {}; if (masterKey) { - options = { masterKey }; + options.masterKey = masterKey; } if (keyAltNames) { - options = { - ...(options ?? {}), - keyAltNames - }; + options.keyAltNames = keyAltNames; } if (keyMaterial) { - options = { - ...(options ?? {}), - keyMaterial - }; + // @ts-expect-error waiting for driver release + options.keyMaterial = keyMaterial; } return await this._clientEncryption._libmongocrypt.createDataKey(kms, options as ClientEncryptionCreateDataKeyProviderOptions); From cc8d7794eab07ed58f6c59067151eee1195a8e32 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Wed, 1 Jun 2022 19:50:55 +0200 Subject: [PATCH 4/5] refactor: remove extra as --- packages/shell-api/src/field-level-encryption.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/shell-api/src/field-level-encryption.ts b/packages/shell-api/src/field-level-encryption.ts index 6d177dafc0..f4d91ef40d 100644 --- a/packages/shell-api/src/field-level-encryption.ts +++ b/packages/shell-api/src/field-level-encryption.ts @@ -257,6 +257,7 @@ export class KeyVault extends ShellApiWithMongoClass { } const options: ClientEncryptionCreateDataKeyProviderOptions = {}; + if (masterKey) { options.masterKey = masterKey; } @@ -268,7 +269,7 @@ export class KeyVault extends ShellApiWithMongoClass { options.keyMaterial = keyMaterial; } - return await this._clientEncryption._libmongocrypt.createDataKey(kms, options as ClientEncryptionCreateDataKeyProviderOptions); + return await this._clientEncryption._libmongocrypt.createDataKey(kms, options); } @returnType('Cursor') From 7dca09f12e57240dffc0c601bbcfaffe67608563 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Wed, 1 Jun 2022 20:42:29 +0200 Subject: [PATCH 5/5] refactor: revert --- packages/shell-api/src/field-level-encryption.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/shell-api/src/field-level-encryption.ts b/packages/shell-api/src/field-level-encryption.ts index f4d91ef40d..8702f02075 100644 --- a/packages/shell-api/src/field-level-encryption.ts +++ b/packages/shell-api/src/field-level-encryption.ts @@ -256,20 +256,20 @@ export class KeyVault extends ShellApiWithMongoClass { } } - const options: ClientEncryptionCreateDataKeyProviderOptions = {}; + let options: ClientEncryptionCreateDataKeyProviderOptions | undefined; if (masterKey) { - options.masterKey = masterKey; + options = { ...(options ?? {}), masterKey }; } if (keyAltNames) { - options.keyAltNames = keyAltNames; + options = { ...(options ?? {}), keyAltNames }; } if (keyMaterial) { // @ts-expect-error waiting for driver release - options.keyMaterial = keyMaterial; + options = { ...(options ?? {}), keyMaterial }; } - return await this._clientEncryption._libmongocrypt.createDataKey(kms, options); + return await this._clientEncryption._libmongocrypt.createDataKey(kms, options as ClientEncryptionCreateDataKeyProviderOptions); } @returnType('Cursor')