Skip to content

Commit 872293c

Browse files
committed
fix: FLE
1 parent 4d9864f commit 872293c

File tree

8 files changed

+133
-95
lines changed

8 files changed

+133
-95
lines changed

src/client-side-encryption/auto_encrypter.ts

Lines changed: 9 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66

77
import { deserialize, type Document, serialize } from '../bson';
88
import { type CommandOptions, type ProxyOptions } from '../cmap/connection';
9+
import { kDecorateResult } from '../constants';
910
import { getMongoDBClientEncryption } from '../deps';
1011
import { MongoRuntimeError } from '../error';
1112
import { MongoClient, type MongoClientOptions } from '../mongo_client';
@@ -212,15 +213,6 @@ export const AutoEncryptionLoggerLevel = Object.freeze({
212213
export type AutoEncryptionLoggerLevel =
213214
(typeof AutoEncryptionLoggerLevel)[keyof typeof AutoEncryptionLoggerLevel];
214215

215-
// Typescript errors if we index objects with `Symbol.for(...)`, so
216-
// to avoid TS errors we pull them out into variables. Then we can type
217-
// the objects (and class) that we expect to see them on and prevent TS
218-
// errors.
219-
/** @internal */
220-
const kDecorateResult = Symbol.for('@@mdb.decorateDecryptionResult');
221-
/** @internal */
222-
const kDecoratedKeys = Symbol.for('@@mdb.decryptedKeys');
223-
224216
/**
225217
* @internal An internal class to be used by the driver for auto encryption
226218
* **NOTE**: Not meant to be instantiated directly, this is for internal use only.
@@ -467,16 +459,18 @@ export class AutoEncrypter {
467459
proxyOptions: this._proxyOptions,
468460
tlsOptions: this._tlsOptions
469461
});
470-
return await stateMachine.execute<Document>(this, context);
462+
463+
return deserialize(await stateMachine.execute(this, context), {
464+
promoteValues: false,
465+
promoteLongs: false
466+
});
471467
}
472468

473469
/**
474470
* Decrypt a command response
475471
*/
476-
async decrypt(response: Uint8Array | Document, options: CommandOptions = {}): Promise<Document> {
477-
const buffer = Buffer.isBuffer(response) ? response : serialize(response, options);
478-
479-
const context = this._mongocrypt.makeDecryptionContext(buffer);
472+
async decrypt(response: Uint8Array, options: CommandOptions = {}): Promise<Uint8Array> {
473+
const context = this._mongocrypt.makeDecryptionContext(response);
480474

481475
context.id = this._contextCounter++;
482476

@@ -486,12 +480,7 @@ export class AutoEncrypter {
486480
tlsOptions: this._tlsOptions
487481
});
488482

489-
const decorateResult = this[kDecorateResult];
490-
const result = await stateMachine.execute<Document>(this, context);
491-
if (decorateResult) {
492-
decorateDecryptionResult(result, response);
493-
}
494-
return result;
483+
return await stateMachine.execute(this, context);
495484
}
496485

497486
/**
@@ -518,53 +507,3 @@ export class AutoEncrypter {
518507
return AutoEncrypter.getMongoCrypt().libmongocryptVersion;
519508
}
520509
}
521-
522-
/**
523-
* Recurse through the (identically-shaped) `decrypted` and `original`
524-
* objects and attach a `decryptedKeys` property on each sub-object that
525-
* contained encrypted fields. Because we only call this on BSON responses,
526-
* we do not need to worry about circular references.
527-
*
528-
* @internal
529-
*/
530-
function decorateDecryptionResult(
531-
decrypted: Document & { [kDecoratedKeys]?: Array<string> },
532-
original: Document,
533-
isTopLevelDecorateCall = true
534-
): void {
535-
if (isTopLevelDecorateCall) {
536-
// The original value could have been either a JS object or a BSON buffer
537-
if (Buffer.isBuffer(original)) {
538-
original = deserialize(original);
539-
}
540-
if (Buffer.isBuffer(decrypted)) {
541-
throw new MongoRuntimeError('Expected result of decryption to be deserialized BSON object');
542-
}
543-
}
544-
545-
if (!decrypted || typeof decrypted !== 'object') return;
546-
for (const k of Object.keys(decrypted)) {
547-
const originalValue = original[k];
548-
549-
// An object was decrypted by libmongocrypt if and only if it was
550-
// a BSON Binary object with subtype 6.
551-
if (originalValue && originalValue._bsontype === 'Binary' && originalValue.sub_type === 6) {
552-
if (!decrypted[kDecoratedKeys]) {
553-
Object.defineProperty(decrypted, kDecoratedKeys, {
554-
value: [],
555-
configurable: true,
556-
enumerable: false,
557-
writable: false
558-
});
559-
}
560-
// this is defined in the preceding if-statement
561-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
562-
decrypted[kDecoratedKeys]!.push(k);
563-
// Do not recurse into this decrypted value. It could be a sub-document/array,
564-
// in which case there is no original value associated with its subfields.
565-
continue;
566-
}
567-
568-
decorateDecryptionResult(decrypted[k], originalValue, false);
569-
}
570-
}

src/client-side-encryption/client_encryption.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
MongoCryptOptions
66
} from 'mongodb-client-encryption';
77

8-
import { type Binary, type Document, type Long, serialize, type UUID } from '../bson';
8+
import { type Binary, deserialize, type Document, type Long, serialize, type UUID } from '../bson';
99
import { type AnyBulkWriteOperation, type BulkWriteResult } from '../bulk/common';
1010
import { type ProxyOptions } from '../cmap/connection';
1111
import { type Collection } from '../collection';
@@ -202,7 +202,7 @@ export class ClientEncryption {
202202
tlsOptions: this._tlsOptions
203203
});
204204

205-
const dataKey = await stateMachine.execute<DataKey>(this, context);
205+
const dataKey = deserialize(await stateMachine.execute(this, context)) as DataKey;
206206

207207
const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString(
208208
this._keyVaultNamespace
@@ -259,7 +259,7 @@ export class ClientEncryption {
259259
tlsOptions: this._tlsOptions
260260
});
261261

262-
const { v: dataKeys } = await stateMachine.execute<{ v: DataKey[] }>(this, context);
262+
const { v: dataKeys } = deserialize(await stateMachine.execute(this, context));
263263
if (dataKeys.length === 0) {
264264
return {};
265265
}
@@ -640,7 +640,7 @@ export class ClientEncryption {
640640
tlsOptions: this._tlsOptions
641641
});
642642

643-
const { v } = await stateMachine.execute<{ v: T }>(this, context);
643+
const { v } = deserialize(await stateMachine.execute(this, context));
644644

645645
return v;
646646
}
@@ -719,7 +719,7 @@ export class ClientEncryption {
719719
});
720720
const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions);
721721

722-
const result = await stateMachine.execute<{ v: Binary }>(this, context);
722+
const result = deserialize(await stateMachine.execute(this, context));
723723
return result.v;
724724
}
725725
}

src/client-side-encryption/state_machine.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ export type CSFLEKMSTlsOptions = {
112112
azure?: ClientEncryptionTlsOptions;
113113
};
114114

115+
/** `{ v: [] }` */
116+
const EMPTY_V = Uint8Array.from([13, 0, 0, 0, 4, 118, 0, 5, 0, 0, 0, 0, 0]);
117+
115118
/**
116119
* @internal
117120
*
@@ -154,16 +157,13 @@ export class StateMachine {
154157
/**
155158
* Executes the state machine according to the specification
156159
*/
157-
async execute<T extends Document>(
158-
executor: StateMachineExecutable,
159-
context: MongoCryptContext
160-
): Promise<T> {
160+
async execute(executor: StateMachineExecutable, context: MongoCryptContext): Promise<Uint8Array> {
161161
const keyVaultNamespace = executor._keyVaultNamespace;
162162
const keyVaultClient = executor._keyVaultClient;
163163
const metaDataClient = executor._metaDataClient;
164164
const mongocryptdClient = executor._mongocryptdClient;
165165
const mongocryptdManager = executor._mongocryptdManager;
166-
let result: T | null = null;
166+
let result: Uint8Array | null = null;
167167

168168
while (context.state !== MONGOCRYPT_CTX_DONE && context.state !== MONGOCRYPT_CTX_ERROR) {
169169
debug(`[context#${context.id}] ${stateToString.get(context.state) || context.state}`);
@@ -220,7 +220,7 @@ export class StateMachine {
220220
// do not. We set the result manually here, and let the state machine continue. `libmongocrypt`
221221
// will inform us if we need to error by setting the state to `MONGOCRYPT_CTX_ERROR` but
222222
// otherwise we'll return `{ v: [] }`.
223-
result = { v: [] } as any as T;
223+
result = EMPTY_V;
224224
}
225225
for await (const key of keys) {
226226
context.addMongoOperationResponse(serialize(key));
@@ -252,7 +252,7 @@ export class StateMachine {
252252
const message = context.status.message || 'Finalization error';
253253
throw new MongoCryptError(message);
254254
}
255-
result = deserialize(finalizedContext, this.options) as T;
255+
result = finalizedContext;
256256
break;
257257
}
258258

src/cmap/connection.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { type Readable, Transform, type TransformCallback } from 'stream';
22
import { clearTimeout, setTimeout } from 'timers';
33

4-
import type { BSONSerializeOptions, Document, ObjectId } from '../bson';
5-
import type { AutoEncrypter } from '../client-side-encryption/auto_encrypter';
4+
import { type BSONSerializeOptions, deserialize, type Document, type ObjectId } from '../bson';
5+
import { type AutoEncrypter } from '../client-side-encryption/auto_encrypter';
66
import {
77
CLOSE,
88
CLUSTER_TIME_RECEIVED,
99
COMMAND_FAILED,
1010
COMMAND_STARTED,
1111
COMMAND_SUCCEEDED,
12+
kDecorateResult,
1213
PINNED,
1314
UNPINNED
1415
} from '../constants';
@@ -33,6 +34,7 @@ import {
3334
BufferPool,
3435
calculateDurationInMs,
3536
type Callback,
37+
decorateDecryptionResult,
3638
HostAddress,
3739
maxWireVersion,
3840
type MongoDBNamespace,
@@ -721,7 +723,7 @@ export class CryptoConnection extends Connection {
721723
ns: MongoDBNamespace,
722724
cmd: Document,
723725
options?: CommandOptions,
724-
_responseType?: T | undefined
726+
responseType?: T | undefined
725727
): Promise<Document> {
726728
const { autoEncrypter } = this;
727729
if (!autoEncrypter) {
@@ -735,7 +737,7 @@ export class CryptoConnection extends Connection {
735737
const serverWireVersion = maxWireVersion(this);
736738
if (serverWireVersion === 0) {
737739
// This means the initial handshake hasn't happened yet
738-
return await super.command<T>(ns, cmd, options, undefined);
740+
return await super.command<T>(ns, cmd, options, responseType);
739741
}
740742

741743
if (serverWireVersion < 8) {
@@ -769,8 +771,25 @@ export class CryptoConnection extends Connection {
769771
}
770772
}
771773

772-
const response = await super.command<T>(ns, encrypted, options, undefined);
774+
const encryptedResponse: MongoDBResponse = (await super.command<T>(
775+
ns,
776+
encrypted,
777+
options,
778+
(responseType ?? MongoDBResponse) as any
779+
)) as unknown as MongoDBResponse;
773780

774-
return await autoEncrypter.decrypt(response, options);
781+
const result = await autoEncrypter.decrypt(encryptedResponse.toBytes(), options);
782+
783+
const decryptedResponse = responseType?.make(result) ?? deserialize(result, options);
784+
785+
if (autoEncrypter[kDecorateResult]) {
786+
if (responseType == null) {
787+
decorateDecryptionResult(decryptedResponse, encryptedResponse.toObject(), true);
788+
} else {
789+
decryptedResponse.encryptedResponse = encryptedResponse;
790+
}
791+
}
792+
793+
return decryptedResponse;
775794
}
776795
}

src/cmap/wire_protocol/responses.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from '../../bson';
1111
import { MongoUnexpectedServerResponseError } from '../../error';
1212
import { type ClusterTime } from '../../sdam/common';
13-
import { ns } from '../../utils';
13+
import { decorateDecryptionResult, ns } from '../../utils';
1414
import { OnDemandDocument } from './on_demand/document';
1515

1616
// eslint-disable-next-line no-restricted-syntax
@@ -169,6 +169,9 @@ export class MongoDBResponse extends OnDemandDocument {
169169
}
170170
return { utf8: { writeErrors: false } };
171171
}
172+
173+
// TODO: Supports decorating result
174+
encryptedResponse?: MongoDBResponse;
172175
}
173176

174177
// Here's a litle blast from the past.
@@ -220,6 +223,21 @@ export class CursorResponse extends MongoDBResponse {
220223
return Math.max(this.batchSize - this.iterated, 0);
221224
}
222225

226+
private _encryptedBatch: OnDemandDocument | null = null;
227+
get encryptedBatch() {
228+
if (this.encryptedResponse == null) return null;
229+
if (this._encryptedBatch != null) return this._encryptedBatch;
230+
231+
const cursor = this.encryptedResponse?.get('cursor', BSONType.object);
232+
if (cursor?.has('firstBatch'))
233+
this._encryptedBatch = cursor.get('firstBatch', BSONType.array, true);
234+
else if (cursor?.has('nextBatch'))
235+
this._encryptedBatch = cursor.get('nextBatch', BSONType.array, true);
236+
else throw new MongoUnexpectedServerResponseError('Cursor document did not contain a batch');
237+
238+
return this._encryptedBatch;
239+
}
240+
223241
private get batch() {
224242
if (this._batch != null) return this._batch;
225243
const cursor = this.cursor;
@@ -249,12 +267,18 @@ export class CursorResponse extends MongoDBResponse {
249267
}
250268

251269
const result = this.batch.get(this.iterated, BSONType.object, true) ?? null;
270+
const encryptedResult = this.encryptedBatch?.get(this.iterated, BSONType.object, true) ?? null;
271+
252272
this.iterated += 1;
253273

254274
if (options?.raw) {
255275
return result.toBytes();
256276
} else {
257-
return result.toObject(options);
277+
const object = result.toObject(options);
278+
if (encryptedResult) {
279+
decorateDecryptionResult(object, encryptedResult.toObject(options), true);
280+
}
281+
return object;
258282
}
259283
}
260284

src/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,12 @@ export const LEGACY_HELLO_COMMAND = 'ismaster';
165165
* The legacy hello command that was deprecated in MongoDB 5.0.
166166
*/
167167
export const LEGACY_HELLO_COMMAND_CAMEL_CASE = 'isMaster';
168+
169+
// Typescript errors if we index objects with `Symbol.for(...)`, so
170+
// to avoid TS errors we pull them out into variables. Then we can type
171+
// the objects (and class) that we expect to see them on and prevent TS
172+
// errors.
173+
/** @internal */
174+
export const kDecorateResult = Symbol.for('@@mdb.decorateDecryptionResult');
175+
/** @internal */
176+
export const kDecoratedKeys = Symbol.for('@@mdb.decryptedKeys');

src/cursor/abstract_cursor.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -653,9 +653,6 @@ export abstract class AbstractCursor<
653653
const state = await this._initialize(this[kSession]);
654654
const response = state.response;
655655
this[kServer] = state.server;
656-
657-
if (!CursorResponse.is(response)) throw new Error('ah');
658-
659656
this[kId] = response.id;
660657
this[kNamespace] = response.ns ?? this[kNamespace];
661658
this[kDocuments] = response;

0 commit comments

Comments
 (0)