Skip to content

Commit 3d28ae5

Browse files
committed
lots of changes related to removing Buffer from internal uses:
1. using Uint8Array instead of Buffer in all internal code 2. enabling eslint rule to block Buffer use in src 3. using BSON's ByteUtils in places where we depended on Buffer operations 4. using ByteUtils.isUint8Array instead of Buffer.isBuffer 5. introduced writeInt32LE wrapper that uses the same order of variables, to avoid embarassing mistakes
1 parent 51efd36 commit 3d28ae5

20 files changed

Lines changed: 185 additions & 154 deletions

File tree

.eslintrc.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,13 @@
285285
}
286286
]
287287
}
288+
],
289+
"no-restricted-globals": [
290+
"error",
291+
{
292+
"name": "Buffer",
293+
"message": "Use Uin8Array instead"
294+
}
288295
]
289296
}
290297
},

src/bson.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable no-restricted-imports */
2-
import { BSON, type DeserializeOptions, type SerializeOptions } from 'bson';
2+
import { BSON, ByteUtils, type DeserializeOptions, type SerializeOptions } from 'bson';
33

44
export {
55
Binary,
@@ -8,6 +8,7 @@ export {
88
BSONRegExp,
99
BSONSymbol,
1010
BSONType,
11+
ByteUtils,
1112
calculateObjectSize,
1213
Code,
1314
DBRef,
@@ -32,13 +33,6 @@ export {
3233
/** @internal */
3334
export type BSONElement = BSON.OnDemand['BSONElement'];
3435

35-
/** @internal */
36-
export function toLocalBufferType(this: void, buffer: Buffer | Uint8Array): Buffer {
37-
return Buffer.isBuffer(buffer)
38-
? buffer
39-
: Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
40-
}
41-
4236
export function parseToElementsToArray(bytes: Uint8Array, offset?: number): BSONElement[] {
4337
const res = BSON.onDemand.parseToElements(bytes, offset);
4438
return Array.isArray(res) ? res : [...res];
@@ -48,22 +42,30 @@ export const getInt32LE = BSON.onDemand.NumberUtils.getInt32LE;
4842
export const getFloat64LE = BSON.onDemand.NumberUtils.getFloat64LE;
4943
export const getBigInt64LE = BSON.onDemand.NumberUtils.getBigInt64LE;
5044
export const toUTF8 = BSON.onDemand.ByteUtils.toUTF8;
51-
export const writeInt32LE = BSON.onDemand.NumberUtils.setInt32LE;
5245

53-
export const fromUTF8 = (text: string) => toLocalBufferType(BSON.onDemand.ByteUtils.fromUTF8(text));
46+
// BSON wrappers
5447

55-
export const concatBuffers = (list: Buffer[] | Uint8Array[]) => {
56-
return toLocalBufferType(BSON.onDemand.ByteUtils.concat(list));
48+
// writeInt32LE, same order of arguments as Buffer.writeInt32LE
49+
export const writeInt32LE = (destination: Uint8Array, value: number, offset: number) =>
50+
BSON.onDemand.NumberUtils.setInt32LE(destination, offset, value);
51+
52+
// various wrappers that consume and return local buffer types
53+
54+
export const fromUTF8 = (text: string) =>
55+
ByteUtils.toLocalBufferType(BSON.onDemand.ByteUtils.fromUTF8(text));
56+
export const fromBase64 = (b64: string) =>
57+
ByteUtils.toLocalBufferType(BSON.onDemand.ByteUtils.fromBase64(b64));
58+
export const fromNumberArray = (array: number[]) =>
59+
ByteUtils.toLocalBufferType(BSON.onDemand.ByteUtils.fromNumberArray(array));
60+
export const concatBuffers = (list: Uint8Array[]) => {
61+
return ByteUtils.toLocalBufferType(BSON.onDemand.ByteUtils.concat(list));
5762
};
5863
export const allocateBuffer = (size: number) =>
59-
toLocalBufferType(BSON.onDemand.ByteUtils.allocate(size));
64+
ByteUtils.toLocalBufferType(BSON.onDemand.ByteUtils.allocate(size));
6065
export const allocateUnsafeBuffer = (size: number) =>
61-
toLocalBufferType(BSON.onDemand.ByteUtils.allocateUnsafe(size));
62-
63-
export const utf8ByteLength = BSON.onDemand.ByteUtils.utf8ByteLength;
64-
export const toBase64 = BSON.onDemand.ByteUtils.toBase64;
65-
export const encodeUTF8Into = BSON.onDemand.ByteUtils.encodeUTF8Into;
66+
ByteUtils.toLocalBufferType(BSON.onDemand.ByteUtils.allocateUnsafe(size));
6667

68+
// validates buffer inputs, used for read operations
6769
const validateBufferInputs = (buffer: Uint8Array, offset: number, length: number) => {
6870
if (offset < 0 || offset + length > buffer.length) {
6971
throw new RangeError(

src/client-side-encryption/auto_encrypter.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type MongoCrypt, type MongoCryptOptions } from 'mongodb-client-encryption';
22
import * as net from 'net';
33

4-
import { deserialize, type Document, serialize } from '../bson';
4+
import { ByteUtils, deserialize, type Document, serialize } from '../bson';
55
import { type CommandOptions, type ProxyOptions } from '../cmap/connection';
66
import { kDecorateResult } from '../constants';
77
import { getMongoDBClientEncryption } from '../deps';
@@ -256,20 +256,26 @@ export class AutoEncrypter {
256256
errorWrapper: defaultErrorWrapper
257257
};
258258
if (options.schemaMap) {
259-
mongoCryptOptions.schemaMap = Buffer.isBuffer(options.schemaMap)
260-
? options.schemaMap
261-
: (serialize(options.schemaMap) as Buffer);
259+
if (ByteUtils.isUint8Array(options.schemaMap)) {
260+
mongoCryptOptions.schemaMap = options.schemaMap as Uint8Array;
261+
} else {
262+
mongoCryptOptions.schemaMap = serialize(options.schemaMap);
263+
}
262264
}
263265

264266
if (options.encryptedFieldsMap) {
265-
mongoCryptOptions.encryptedFieldsMap = Buffer.isBuffer(options.encryptedFieldsMap)
266-
? options.encryptedFieldsMap
267-
: (serialize(options.encryptedFieldsMap) as Buffer);
267+
if (ByteUtils.isUint8Array(options.encryptedFieldsMap)) {
268+
mongoCryptOptions.encryptedFieldsMap = options.encryptedFieldsMap as Uint8Array;
269+
} else {
270+
mongoCryptOptions.encryptedFieldsMap = serialize(options.encryptedFieldsMap);
271+
}
268272
}
269273

270-
mongoCryptOptions.kmsProviders = !Buffer.isBuffer(this._kmsProviders)
271-
? (serialize(this._kmsProviders) as Buffer)
272-
: this._kmsProviders;
274+
if (ByteUtils.isUint8Array(this._kmsProviders)) {
275+
mongoCryptOptions.kmsProviders = this._kmsProviders as any as Uint8Array;
276+
} else {
277+
mongoCryptOptions.kmsProviders = serialize(this._kmsProviders);
278+
}
273279

274280
if (options.options?.logger) {
275281
mongoCryptOptions.logger = options.options.logger;
@@ -396,7 +402,9 @@ export class AutoEncrypter {
396402
return cmd;
397403
}
398404

399-
const commandBuffer = Buffer.isBuffer(cmd) ? cmd : serialize(cmd, options);
405+
const commandBuffer: Uint8Array = ByteUtils.isUint8Array(cmd)
406+
? (cmd as Uint8Array)
407+
: serialize(cmd, options);
400408
const context = this._mongocrypt.makeEncryptionContext(
401409
MongoDBCollectionNamespace.fromString(ns).db,
402410
commandBuffer

src/client-side-encryption/client_encryption.ts

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

77
import {
88
type Binary,
9+
ByteUtils,
910
deserialize,
1011
type Document,
1112
type Int32,
@@ -141,11 +142,15 @@ export class ClientEncryption {
141142
throw new MongoCryptInvalidArgumentError('Missing required option `keyVaultNamespace`');
142143
}
143144

145+
let kmsProviders;
146+
if (!ByteUtils.isUint8Array(this._kmsProviders)) {
147+
kmsProviders = serialize(this._kmsProviders);
148+
} else {
149+
kmsProviders = this._kmsProviders as any as Uint8Array;
150+
}
144151
const mongoCryptOptions: MongoCryptOptions = {
145152
...options,
146-
kmsProviders: !Buffer.isBuffer(this._kmsProviders)
147-
? (serialize(this._kmsProviders) as Buffer)
148-
: this._kmsProviders,
153+
kmsProviders,
149154
errorWrapper: defaultErrorWrapper
150155
};
151156

src/cmap/auth/auth_provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export class AuthContext {
2121
/** A response from an initial auth attempt, only some mechanisms use this (e.g, SCRAM) */
2222
response?: Document;
2323
/** A random nonce generated for use in an authentication conversation */
24-
nonce?: Buffer;
24+
nonce?: Uint8Array;
2525

2626
constructor(
2727
connection: Connection,

src/cmap/auth/mongodb_aws.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
MongoMissingCredentialsError,
66
MongoRuntimeError
77
} from '../../error';
8-
import { ByteUtils, maxWireVersion, ns, randomBytes } from '../../utils';
8+
import { maxWireVersion, ns, randomBytes } from '../../utils';
99
import { type AuthContext, AuthProvider } from './auth_provider';
1010
import {
1111
type AWSCredentialProvider,
@@ -92,7 +92,7 @@ export class MongoDBAWS extends AuthProvider {
9292
throw new MongoRuntimeError(`Invalid server nonce length ${serverNonce.length}, expected 64`);
9393
}
9494

95-
if (!ByteUtils.equals(serverNonce.subarray(0, nonce.byteLength), nonce)) {
95+
if (!BSON.ByteUtils.equals(serverNonce.subarray(0, nonce.byteLength), nonce)) {
9696
// throw because the serverNonce's leading 32 bytes must equal the client nonce's 32 bytes
9797
// https://github.com/mongodb/specifications/blob/master/source/auth/auth.md#conversation-5
9898

@@ -115,7 +115,7 @@ export class MongoDBAWS extends AuthProvider {
115115
headers: {
116116
'Content-Type': 'application/x-www-form-urlencoded',
117117
'Content-Length': body.length,
118-
'X-MongoDB-Server-Nonce': ByteUtils.toBase64(serverNonce),
118+
'X-MongoDB-Server-Nonce': BSON.ByteUtils.toBase64(serverNonce),
119119
'X-MongoDB-GS2-CB-Flag': 'n'
120120
},
121121
path: '/',

src/cmap/auth/plain.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Binary } from '../../bson';
1+
import { Binary, fromUTF8 } from '../../bson';
22
import { MongoMissingCredentialsError } from '../../error';
33
import { ns } from '../../utils';
44
import { type AuthContext, AuthProvider } from './auth_provider';
@@ -12,7 +12,7 @@ export class Plain extends AuthProvider {
1212

1313
const { username, password } = credentials;
1414

15-
const payload = new Binary(Buffer.from(`\x00${username}\x00${password}`));
15+
const payload = new Binary(fromUTF8(`\x00${username}\x00${password}`));
1616
const command = {
1717
saslStart: 1,
1818
mechanism: 'PLAIN',

src/cmap/auth/scram.ts

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import * as crypto from 'crypto';
44
import {
55
allocateBuffer,
66
Binary,
7+
ByteUtils,
78
concatBuffers,
89
type Document,
9-
fromUTF8,
10-
toBase64
10+
fromBase64,
11+
fromNumberArray,
12+
fromUTF8
1113
} from '../../bson';
1214
import {
1315
MongoInvalidArgumentError,
@@ -72,21 +74,21 @@ function cleanUsername(username: string) {
7274
return username.replace('=', '=3D').replace(',', '=2C');
7375
}
7476

75-
function clientFirstMessageBare(username: string, nonce: Buffer) {
77+
function clientFirstMessageBare(username: string, nonce: Uint8Array) {
7678
// NOTE: This is done b/c Javascript uses UTF-16, but the server is hashing in UTF-8.
7779
// Since the username is not sasl-prep-d, we need to do this here.
7880
return concatBuffers([
7981
fromUTF8('n='),
8082
fromUTF8(username),
8183
fromUTF8(',r='),
82-
fromUTF8(toBase64(nonce))
84+
fromUTF8(ByteUtils.toBase64(nonce))
8385
]);
8486
}
8587

8688
function makeFirstMessage(
8789
cryptoMethod: CryptoMethod,
8890
credentials: MongoCredentials,
89-
nonce: Buffer
91+
nonce: Uint8Array
9092
) {
9193
const username = cleanUsername(credentials.username);
9294
const mechanism =
@@ -97,9 +99,7 @@ function makeFirstMessage(
9799
return {
98100
saslStart: 1,
99101
mechanism,
100-
payload: new Binary(
101-
concatBuffers([Buffer.from('n,,', 'utf8'), clientFirstMessageBare(username, nonce)])
102-
),
102+
payload: new Binary(concatBuffers([fromUTF8('n,,'), clientFirstMessageBare(username, nonce)])),
103103
autoAuthorize: 1,
104104
options: { skipEmptyExchange: true }
105105
};
@@ -143,7 +143,7 @@ async function continueScramConversation(
143143
const processedPassword =
144144
cryptoMethod === 'sha256' ? saslprep(password) : passwordDigest(username, password);
145145

146-
const payload: Binary = Buffer.isBuffer(response.payload)
146+
const payload: Binary = ByteUtils.isUint8Array(response.payload)
147147
? new Binary(response.payload)
148148
: response.payload;
149149

@@ -164,12 +164,7 @@ async function continueScramConversation(
164164

165165
// Set up start of proof
166166
const withoutProof = `c=biws,r=${rnonce}`;
167-
const saltedPassword = HI(
168-
processedPassword,
169-
Buffer.from(salt, 'base64'),
170-
iterations,
171-
cryptoMethod
172-
);
167+
const saltedPassword = HI(processedPassword, fromBase64(salt), iterations, cryptoMethod);
173168

174169
const clientKey = HMAC(cryptoMethod, saltedPassword, 'Client Key');
175170
const serverKey = HMAC(cryptoMethod, saltedPassword, 'Server Key');
@@ -188,13 +183,13 @@ async function continueScramConversation(
188183
const saslContinueCmd = {
189184
saslContinue: 1,
190185
conversationId: response.conversationId,
191-
payload: new Binary(Buffer.from(clientFinal))
186+
payload: new Binary(fromUTF8(clientFinal))
192187
};
193188

194189
const r = await connection.command(ns(`${db}.$cmd`), saslContinueCmd, undefined);
195190
const parsedResponse = parsePayload(r.payload);
196191

197-
if (!compareDigest(Buffer.from(parsedResponse.v, 'base64'), serverSignature)) {
192+
if (!compareDigest(fromBase64(parsedResponse.v), serverSignature)) {
198193
throw new MongoRuntimeError('Server returned an invalid signature');
199194
}
200195

@@ -252,35 +247,27 @@ function passwordDigest(username: string, password: string) {
252247
}
253248

254249
// XOR two buffers
255-
function xor(a: Buffer, b: Buffer) {
256-
if (!Buffer.isBuffer(a)) {
257-
a = Buffer.from(a);
258-
}
259-
260-
if (!Buffer.isBuffer(b)) {
261-
b = Buffer.from(b);
262-
}
263-
250+
function xor(a: Uint8Array, b: Uint8Array) {
264251
const length = Math.max(a.length, b.length);
265252
const res = [];
266253

267254
for (let i = 0; i < length; i += 1) {
268255
res.push(a[i] ^ b[i]);
269256
}
270257

271-
return Buffer.from(res).toString('base64');
258+
return ByteUtils.toBase64(fromNumberArray(res));
272259
}
273260

274-
function H(method: CryptoMethod, text: Buffer) {
261+
function H(method: CryptoMethod, text: Uint8Array): Uint8Array {
275262
return crypto.createHash(method).update(text).digest();
276263
}
277264

278-
function HMAC(method: CryptoMethod, key: Buffer, text: Buffer | string) {
265+
function HMAC(method: CryptoMethod, key: Uint8Array, text: Uint8Array | string): Uint8Array {
279266
return crypto.createHmac(method, key).update(text).digest();
280267
}
281268

282269
interface HICache {
283-
[key: string]: Buffer;
270+
[key: string]: Uint8Array;
284271
}
285272

286273
let _hiCache: HICache = {};
@@ -295,9 +282,9 @@ const hiLengthMap = {
295282
sha1: 20
296283
};
297284

298-
function HI(data: string, salt: Buffer, iterations: number, cryptoMethod: CryptoMethod) {
285+
function HI(data: string, salt: Uint8Array, iterations: number, cryptoMethod: CryptoMethod) {
299286
// omit the work if already generated
300-
const key = [data, salt.toString('base64'), iterations].join('_');
287+
const key = [data, ByteUtils.toBase64(salt), iterations].join('_');
301288
if (_hiCache[key] != null) {
302289
return _hiCache[key];
303290
}
@@ -321,7 +308,7 @@ function HI(data: string, salt: Buffer, iterations: number, cryptoMethod: Crypto
321308
return saltedData;
322309
}
323310

324-
function compareDigest(lhs: Buffer, rhs: Uint8Array) {
311+
function compareDigest(lhs: Uint8Array, rhs: Uint8Array) {
325312
if (lhs.length !== rhs.length) {
326313
return false;
327314
}

0 commit comments

Comments
 (0)