Skip to content

Commit

Permalink
feat(NODE-6136): parse cursor responses on demand (#4112)
Browse files Browse the repository at this point in the history
Co-authored-by: Bailey Pearson <[email protected]>
  • Loading branch information
nbbeeken and baileympearson authored Jun 13, 2024
1 parent 0c687a5 commit 3ed6a2a
Show file tree
Hide file tree
Showing 42 changed files with 767 additions and 721 deletions.
1 change: 1 addition & 0 deletions src/bson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export {
UUID
} from 'bson';

/** @internal */
export type BSONElement = BSON.OnDemand['BSONElement'];

export function parseToElementsToArray(bytes: Uint8Array, offset?: number): BSONElement[] {
Expand Down
79 changes: 9 additions & 70 deletions src/client-side-encryption/auto_encrypter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {

import { deserialize, type Document, serialize } from '../bson';
import { type CommandOptions, type ProxyOptions } from '../cmap/connection';
import { kDecorateResult } from '../constants';
import { getMongoDBClientEncryption } from '../deps';
import { MongoRuntimeError } from '../error';
import { MongoClient, type MongoClientOptions } from '../mongo_client';
Expand Down Expand Up @@ -212,15 +213,6 @@ export const AutoEncryptionLoggerLevel = Object.freeze({
export type AutoEncryptionLoggerLevel =
(typeof AutoEncryptionLoggerLevel)[keyof typeof AutoEncryptionLoggerLevel];

// Typescript errors if we index objects with `Symbol.for(...)`, so
// to avoid TS errors we pull them out into variables. Then we can type
// the objects (and class) that we expect to see them on and prevent TS
// errors.
/** @internal */
const kDecorateResult = Symbol.for('@@mdb.decorateDecryptionResult');
/** @internal */
const kDecoratedKeys = Symbol.for('@@mdb.decryptedKeys');

/**
* @internal An internal class to be used by the driver for auto encryption
* **NOTE**: Not meant to be instantiated directly, this is for internal use only.
Expand Down Expand Up @@ -467,16 +459,18 @@ export class AutoEncrypter {
proxyOptions: this._proxyOptions,
tlsOptions: this._tlsOptions
});
return await stateMachine.execute<Document>(this, context);

return deserialize(await stateMachine.execute(this, context), {
promoteValues: false,
promoteLongs: false
});
}

/**
* Decrypt a command response
*/
async decrypt(response: Uint8Array | Document, options: CommandOptions = {}): Promise<Document> {
const buffer = Buffer.isBuffer(response) ? response : serialize(response, options);

const context = this._mongocrypt.makeDecryptionContext(buffer);
async decrypt(response: Uint8Array, options: CommandOptions = {}): Promise<Uint8Array> {
const context = this._mongocrypt.makeDecryptionContext(response);

context.id = this._contextCounter++;

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

const decorateResult = this[kDecorateResult];
const result = await stateMachine.execute<Document>(this, context);
if (decorateResult) {
decorateDecryptionResult(result, response);
}
return result;
return await stateMachine.execute(this, context);
}

/**
Expand All @@ -518,53 +507,3 @@ export class AutoEncrypter {
return AutoEncrypter.getMongoCrypt().libmongocryptVersion;
}
}

/**
* Recurse through the (identically-shaped) `decrypted` and `original`
* objects and attach a `decryptedKeys` property on each sub-object that
* contained encrypted fields. Because we only call this on BSON responses,
* we do not need to worry about circular references.
*
* @internal
*/
function decorateDecryptionResult(
decrypted: Document & { [kDecoratedKeys]?: Array<string> },
original: Document,
isTopLevelDecorateCall = true
): void {
if (isTopLevelDecorateCall) {
// The original value could have been either a JS object or a BSON buffer
if (Buffer.isBuffer(original)) {
original = deserialize(original);
}
if (Buffer.isBuffer(decrypted)) {
throw new MongoRuntimeError('Expected result of decryption to be deserialized BSON object');
}
}

if (!decrypted || typeof decrypted !== 'object') return;
for (const k of Object.keys(decrypted)) {
const originalValue = original[k];

// An object was decrypted by libmongocrypt if and only if it was
// a BSON Binary object with subtype 6.
if (originalValue && originalValue._bsontype === 'Binary' && originalValue.sub_type === 6) {
if (!decrypted[kDecoratedKeys]) {
Object.defineProperty(decrypted, kDecoratedKeys, {
value: [],
configurable: true,
enumerable: false,
writable: false
});
}
// this is defined in the preceding if-statement
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
decrypted[kDecoratedKeys]!.push(k);
// Do not recurse into this decrypted value. It could be a sub-document/array,
// in which case there is no original value associated with its subfields.
continue;
}

decorateDecryptionResult(decrypted[k], originalValue, false);
}
}
12 changes: 6 additions & 6 deletions src/client-side-encryption/client_encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
MongoCryptOptions
} from 'mongodb-client-encryption';

import { type Binary, type Document, type Long, serialize, type UUID } from '../bson';
import { type Binary, deserialize, 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';
Expand Down Expand Up @@ -202,7 +202,7 @@ export class ClientEncryption {
tlsOptions: this._tlsOptions
});

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

const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString(
this._keyVaultNamespace
Expand Down Expand Up @@ -259,7 +259,7 @@ export class ClientEncryption {
tlsOptions: this._tlsOptions
});

const { v: dataKeys } = await stateMachine.execute<{ v: DataKey[] }>(this, context);
const { v: dataKeys } = deserialize(await stateMachine.execute(this, context));
if (dataKeys.length === 0) {
return {};
}
Expand Down Expand Up @@ -640,7 +640,7 @@ export class ClientEncryption {
tlsOptions: this._tlsOptions
});

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

return v;
}
Expand Down Expand Up @@ -719,8 +719,8 @@ export class ClientEncryption {
});
const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions);

const result = await stateMachine.execute<{ v: Binary }>(this, context);
return result.v;
const { v } = deserialize(await stateMachine.execute(this, context));
return v;
}
}

Expand Down
34 changes: 18 additions & 16 deletions src/client-side-encryption/state_machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,19 @@ export type CSFLEKMSTlsOptions = {
[key: string]: ClientEncryptionTlsOptions | undefined;
};

/**
* This is kind of a hack. For `rewrapManyDataKey`, we have tests that
* guarantee that when there are no matching keys, `rewrapManyDataKey` returns
* nothing. We also have tests for auto encryption that guarantee for `encrypt`
* we return an error when there are no matching keys. This error is generated in
* subsequent iterations of the state machine.
* Some apis (`encrypt`) throw if there are no filter matches and others (`rewrapManyDataKey`)
* do not. We set the result manually here, and let the state machine continue. `libmongocrypt`
* will inform us if we need to error by setting the state to `MONGOCRYPT_CTX_ERROR` but
* otherwise we'll return `{ v: [] }`.
*/
let EMPTY_V;

/**
* @internal
*
Expand Down Expand Up @@ -156,16 +169,13 @@ export class StateMachine {
/**
* Executes the state machine according to the specification
*/
async execute<T extends Document>(
executor: StateMachineExecutable,
context: MongoCryptContext
): Promise<T> {
async execute(executor: StateMachineExecutable, context: MongoCryptContext): Promise<Uint8Array> {
const keyVaultNamespace = executor._keyVaultNamespace;
const keyVaultClient = executor._keyVaultClient;
const metaDataClient = executor._metaDataClient;
const mongocryptdClient = executor._mongocryptdClient;
const mongocryptdManager = executor._mongocryptdManager;
let result: T | null = null;
let result: Uint8Array | null = null;

while (context.state !== MONGOCRYPT_CTX_DONE && context.state !== MONGOCRYPT_CTX_ERROR) {
debug(`[context#${context.id}] ${stateToString.get(context.state) || context.state}`);
Expand Down Expand Up @@ -213,16 +223,8 @@ export class StateMachine {
const keys = await this.fetchKeys(keyVaultClient, keyVaultNamespace, filter);

if (keys.length === 0) {
// This is kind of a hack. For `rewrapManyDataKey`, we have tests that
// guarantee that when there are no matching keys, `rewrapManyDataKey` returns
// nothing. We also have tests for auto encryption that guarantee for `encrypt`
// we return an error when there are no matching keys. This error is generated in
// subsequent iterations of the state machine.
// Some apis (`encrypt`) throw if there are no filter matches and others (`rewrapManyDataKey`)
// do not. We set the result manually here, and let the state machine continue. `libmongocrypt`
// will inform us if we need to error by setting the state to `MONGOCRYPT_CTX_ERROR` but
// otherwise we'll return `{ v: [] }`.
result = { v: [] } as any as T;
// See docs on EMPTY_V
result = EMPTY_V ??= serialize({ v: [] });
}
for await (const key of keys) {
context.addMongoOperationResponse(serialize(key));
Expand Down Expand Up @@ -254,7 +256,7 @@ export class StateMachine {
const message = context.status.message || 'Finalization error';
throw new MongoCryptError(message);
}
result = deserialize(finalizedContext, this.options) as T;
result = finalizedContext;
break;
}

Expand Down
Loading

0 comments on commit 3ed6a2a

Please sign in to comment.