Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,13 @@
}
]
}
],
"no-restricted-globals": [
"error",
{
"name": "Buffer",
"message": "Use Uint8Array instead"
}
]
}
},
Expand Down
55 changes: 51 additions & 4 deletions src/bson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export {
BSONRegExp,
BSONSymbol,
BSONType,
ByteUtils,
calculateObjectSize,
Code,
DBRef,
Expand Down Expand Up @@ -38,10 +39,56 @@ export function parseToElementsToArray(bytes: Uint8Array, offset?: number): BSON
return Array.isArray(res) ? res : [...res];
}

export const getInt32LE = BSON.onDemand.NumberUtils.getInt32LE;
export const getFloat64LE = BSON.onDemand.NumberUtils.getFloat64LE;
export const getBigInt64LE = BSON.onDemand.NumberUtils.getBigInt64LE;
export const toUTF8 = BSON.onDemand.ByteUtils.toUTF8;
export const getInt32LE = BSON.NumberUtils.getInt32LE;
export const getFloat64LE = BSON.NumberUtils.getFloat64LE;
export const getBigInt64LE = BSON.NumberUtils.getBigInt64LE;
export const toUTF8 = BSON.ByteUtils.toUTF8;
export const fromUTF8 = BSON.ByteUtils.fromUTF8;
Copy link
Contributor

@addaleax addaleax Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's too late to change but this PR made me realize that the names toUTF8 and fromUTF8 are reversed semantically – fromUTF8 encodes a string into UTF-8 and toUTF8 decodes a string from UTF-8 😢

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, great spotted! I feel like we can only change that in future major version, I will create ticket for this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ByteUtils is experimental - we can make changes outside of semver if we want to fix it now

Copy link
Member

@tadjik1 tadjik1 Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the driver already rely on toUTF8 method (this one is old, fromUTF8 has been added just recently) , so if we change its' behaviour I believe it will break existing driver functionality, npm install will download newer version of bson.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nodejs nealjs return type
Buffer.from('...', 'utf8') ByteUtils.fromUTF8('...') Buffer
buffer.toString('utf8') ByteUtils.toUTF8(buffer) string

Not saying it's right or semantically makes sense, but this was the inspo for the naming. There are only two hard things in Computer Science: cache invalidation and naming things.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the Buffer.from() and buffer.toString() APIs are a bit weird in that they can either do conversion from/to character encodings (like UTF-8 or UTF-16) or from/to binary-to-text encodings (like Hex or Base64), since those "happen" to have the same signatures. The ByteUtils names make sense for the latter, not the former

export const fromBase64 = BSON.ByteUtils.fromBase64;
export const fromNumberArray = BSON.ByteUtils.fromNumberArray;
export const concatBuffers = BSON.ByteUtils.concat;
export const allocateBuffer = BSON.ByteUtils.allocate;
export const allocateUnsafeBuffer = BSON.ByteUtils.allocateUnsafe;

// writeInt32LE, same order of arguments as Buffer.writeInt32LE
export const writeInt32LE = (destination: Uint8Array, value: number, offset: number) =>
BSON.NumberUtils.setInt32LE(destination, offset, value);

// copyBuffer: copies from source buffer to target buffer, returns number of bytes copied
// inputs are explicitly named to avoid confusion
export const copyBuffer = (input: {
source: Uint8Array;
target: Uint8Array;
targetStart?: number;
sourceStart?: number;
sourceEnd?: number;
}): number => {
const { source, target, targetStart = 0, sourceStart = 0, sourceEnd } = input;
const sourceEndActual = sourceEnd ?? source.length;
const srcSlice = source.subarray(sourceStart, sourceEndActual);
const maxLen = Math.min(srcSlice.length, target.length - targetStart);
if (maxLen <= 0) {
return 0;
}
target.set(srcSlice.subarray(0, maxLen), targetStart);
return maxLen;
};

// validates buffer inputs, used for read operations
const validateBufferInputs = (buffer: Uint8Array, offset: number, length: number) => {
if (offset < 0 || offset + length > buffer.length) {
throw new RangeError(
`Attempt to access memory outside buffer bounds: buffer length: ${buffer.length}, offset: ${offset}, length: ${length}`
);
}
};

// readInt32LE, reads a 32-bit integer from buffer at given offset
// throws if offset is out of bounds
export const readInt32LE = (buffer: Uint8Array, offset: number): number => {
validateBufferInputs(buffer, offset, 4);
return getInt32LE(buffer, offset);
};

/**
* BSON Serialization options.
Expand Down
28 changes: 17 additions & 11 deletions src/client-side-encryption/auto_encrypter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type MongoCrypt, type MongoCryptOptions } from 'mongodb-client-encryption';
import * as net from 'net';

import { deserialize, type Document, serialize } from '../bson';
import { ByteUtils, deserialize, type Document, serialize } from '../bson';
import { type CommandOptions, type ProxyOptions } from '../cmap/connection';
import { kDecorateResult } from '../constants';
import { getMongoDBClientEncryption } from '../deps';
Expand Down Expand Up @@ -256,20 +256,26 @@ export class AutoEncrypter {
errorWrapper: defaultErrorWrapper
};
if (options.schemaMap) {
mongoCryptOptions.schemaMap = Buffer.isBuffer(options.schemaMap)
? options.schemaMap
: (serialize(options.schemaMap) as Buffer);
if (ByteUtils.isUint8Array(options.schemaMap)) {
mongoCryptOptions.schemaMap = options.schemaMap;
} else {
mongoCryptOptions.schemaMap = serialize(options.schemaMap);
}
}

if (options.encryptedFieldsMap) {
mongoCryptOptions.encryptedFieldsMap = Buffer.isBuffer(options.encryptedFieldsMap)
? options.encryptedFieldsMap
: (serialize(options.encryptedFieldsMap) as Buffer);
if (ByteUtils.isUint8Array(options.encryptedFieldsMap)) {
mongoCryptOptions.encryptedFieldsMap = options.encryptedFieldsMap;
} else {
mongoCryptOptions.encryptedFieldsMap = serialize(options.encryptedFieldsMap);
}
}

mongoCryptOptions.kmsProviders = !Buffer.isBuffer(this._kmsProviders)
? (serialize(this._kmsProviders) as Buffer)
: this._kmsProviders;
if (ByteUtils.isUint8Array(this._kmsProviders)) {
mongoCryptOptions.kmsProviders = this._kmsProviders;
} else {
mongoCryptOptions.kmsProviders = serialize(this._kmsProviders);
}

if (options.options?.logger) {
mongoCryptOptions.logger = options.options.logger;
Expand Down Expand Up @@ -396,7 +402,7 @@ export class AutoEncrypter {
return cmd;
}

const commandBuffer = Buffer.isBuffer(cmd) ? cmd : serialize(cmd, options);
const commandBuffer: Uint8Array = serialize(cmd, options);
const context = this._mongocrypt.makeEncryptionContext(
MongoDBCollectionNamespace.fromString(ns).db,
commandBuffer
Expand Down
4 changes: 1 addition & 3 deletions src/client-side-encryption/client_encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,7 @@ export class ClientEncryption {

const mongoCryptOptions: MongoCryptOptions = {
...options,
kmsProviders: !Buffer.isBuffer(this._kmsProviders)
? (serialize(this._kmsProviders) as Buffer)
: this._kmsProviders,
kmsProviders: serialize(this._kmsProviders),
errorWrapper: defaultErrorWrapper
};

Expand Down
2 changes: 1 addition & 1 deletion src/cmap/auth/auth_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class AuthContext {
/** A response from an initial auth attempt, only some mechanisms use this (e.g, SCRAM) */
response?: Document;
/** A random nonce generated for use in an authentication conversation */
nonce?: Buffer;
nonce?: Uint8Array;

constructor(
connection: Connection,
Expand Down
8 changes: 4 additions & 4 deletions src/cmap/auth/aws4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export type SignedHeaders = {
const getHexSha256 = async (str: string): Promise<string> => {
const data = stringToBuffer(str);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashHex = BSON.onDemand.ByteUtils.toHex(new Uint8Array(hashBuffer));
const hashHex = BSON.ByteUtils.toHex(new Uint8Array(hashBuffer));
return hashHex;
};

Expand Down Expand Up @@ -81,8 +81,8 @@ const convertHeaderValue = (value: string | number) => {
* @returns Uint8Array containing the UTF-8 encoded string.
*/
function stringToBuffer(str: string): Uint8Array {
const data = new Uint8Array(BSON.onDemand.ByteUtils.utf8ByteLength(str));
BSON.onDemand.ByteUtils.encodeUTF8Into(data, str, 0);
const data = new Uint8Array(BSON.ByteUtils.utf8ByteLength(str));
BSON.ByteUtils.encodeUTF8Into(data, str, 0);
return data;
}

Expand Down Expand Up @@ -189,7 +189,7 @@ export async function aws4Sign(

// 5. Calculate the signature
const signatureBuffer = await getHmacSha256(signingKey, stringToSign);
const signature = BSON.onDemand.ByteUtils.toHex(signatureBuffer);
const signature = BSON.ByteUtils.toHex(signatureBuffer);

// 6. Add the signature to the request
// Calculate the Authorization header
Expand Down
6 changes: 3 additions & 3 deletions src/cmap/auth/mongodb_aws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
MongoMissingCredentialsError,
MongoRuntimeError
} from '../../error';
import { ByteUtils, maxWireVersion, ns, randomBytes } from '../../utils';
import { maxWireVersion, ns, randomBytes } from '../../utils';
import { type AuthContext, AuthProvider } from './auth_provider';
import {
type AWSCredentialProvider,
Expand Down Expand Up @@ -92,7 +92,7 @@ export class MongoDBAWS extends AuthProvider {
throw new MongoRuntimeError(`Invalid server nonce length ${serverNonce.length}, expected 64`);
}

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

Expand All @@ -115,7 +115,7 @@ export class MongoDBAWS extends AuthProvider {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': body.length,
'X-MongoDB-Server-Nonce': ByteUtils.toBase64(serverNonce),
'X-MongoDB-Server-Nonce': BSON.ByteUtils.toBase64(serverNonce),
'X-MongoDB-GS2-CB-Flag': 'n'
},
path: '/',
Expand Down
4 changes: 2 additions & 2 deletions src/cmap/auth/plain.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Binary } from '../../bson';
import { Binary, fromUTF8 } from '../../bson';
import { MongoMissingCredentialsError } from '../../error';
import { ns } from '../../utils';
import { type AuthContext, AuthProvider } from './auth_provider';
Expand All @@ -12,7 +12,7 @@ export class Plain extends AuthProvider {

const { username, password } = credentials;

const payload = new Binary(Buffer.from(`\x00${username}\x00${password}`));
const payload = new Binary(fromUTF8(`\x00${username}\x00${password}`));
const command = {
saslStart: 1,
mechanism: 'PLAIN',
Expand Down
68 changes: 31 additions & 37 deletions src/cmap/auth/scram.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { saslprep } from '@mongodb-js/saslprep';
import * as crypto from 'crypto';

import { Binary, type Document } from '../../bson';
import {
allocateBuffer,
Binary,
ByteUtils,
concatBuffers,
type Document,
fromBase64,
fromNumberArray,
fromUTF8
} from '../../bson';
import {
MongoInvalidArgumentError,
MongoMissingCredentialsError,
Expand Down Expand Up @@ -65,21 +74,21 @@ function cleanUsername(username: string) {
return username.replace('=', '=3D').replace(',', '=2C');
}

function clientFirstMessageBare(username: string, nonce: Buffer) {
function clientFirstMessageBare(username: string, nonce: Uint8Array) {
// NOTE: This is done b/c Javascript uses UTF-16, but the server is hashing in UTF-8.
// Since the username is not sasl-prep-d, we need to do this here.
return Buffer.concat([
Buffer.from('n=', 'utf8'),
Buffer.from(username, 'utf8'),
Buffer.from(',r=', 'utf8'),
Buffer.from(nonce.toString('base64'), 'utf8')
return concatBuffers([
fromUTF8('n='),
fromUTF8(username),
fromUTF8(',r='),
fromUTF8(ByteUtils.toBase64(nonce))
]);
}

function makeFirstMessage(
cryptoMethod: CryptoMethod,
credentials: MongoCredentials,
nonce: Buffer
nonce: Uint8Array
) {
const username = cleanUsername(credentials.username);
const mechanism =
Expand All @@ -90,9 +99,7 @@ function makeFirstMessage(
return {
saslStart: 1,
mechanism,
payload: new Binary(
Buffer.concat([Buffer.from('n,,', 'utf8'), clientFirstMessageBare(username, nonce)])
),
payload: new Binary(concatBuffers([fromUTF8('n,,'), clientFirstMessageBare(username, nonce)])),
autoAuthorize: 1,
options: { skipEmptyExchange: true }
};
Expand Down Expand Up @@ -136,7 +143,7 @@ async function continueScramConversation(
const processedPassword =
cryptoMethod === 'sha256' ? saslprep(password) : passwordDigest(username, password);

const payload: Binary = Buffer.isBuffer(response.payload)
const payload: Binary = ByteUtils.isUint8Array(response.payload)
? new Binary(response.payload)
: response.payload;

Expand All @@ -157,12 +164,7 @@ async function continueScramConversation(

// Set up start of proof
const withoutProof = `c=biws,r=${rnonce}`;
const saltedPassword = HI(
processedPassword,
Buffer.from(salt, 'base64'),
iterations,
cryptoMethod
);
const saltedPassword = HI(processedPassword, fromBase64(salt), iterations, cryptoMethod);

const clientKey = HMAC(cryptoMethod, saltedPassword, 'Client Key');
const serverKey = HMAC(cryptoMethod, saltedPassword, 'Server Key');
Expand All @@ -181,13 +183,13 @@ async function continueScramConversation(
const saslContinueCmd = {
saslContinue: 1,
conversationId: response.conversationId,
payload: new Binary(Buffer.from(clientFinal))
payload: new Binary(fromUTF8(clientFinal))
};

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

if (!compareDigest(Buffer.from(parsedResponse.v, 'base64'), serverSignature)) {
if (!compareDigest(fromBase64(parsedResponse.v), serverSignature)) {
throw new MongoRuntimeError('Server returned an invalid signature');
}

Expand All @@ -199,7 +201,7 @@ async function continueScramConversation(
const retrySaslContinueCmd = {
saslContinue: 1,
conversationId: r.conversationId,
payload: Buffer.alloc(0)
payload: allocateBuffer(0)
};

await connection.command(ns(`${db}.$cmd`), retrySaslContinueCmd, undefined);
Expand Down Expand Up @@ -245,35 +247,27 @@ function passwordDigest(username: string, password: string) {
}

// XOR two buffers
function xor(a: Buffer, b: Buffer) {
if (!Buffer.isBuffer(a)) {
a = Buffer.from(a);
}

if (!Buffer.isBuffer(b)) {
b = Buffer.from(b);
}

function xor(a: Uint8Array, b: Uint8Array) {
const length = Math.max(a.length, b.length);
const res = [];

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

return Buffer.from(res).toString('base64');
return ByteUtils.toBase64(fromNumberArray(res));
}

function H(method: CryptoMethod, text: Buffer) {
function H(method: CryptoMethod, text: Uint8Array): Uint8Array {
return crypto.createHash(method).update(text).digest();
}

function HMAC(method: CryptoMethod, key: Buffer, text: Buffer | string) {
function HMAC(method: CryptoMethod, key: Uint8Array, text: Uint8Array | string): Uint8Array {
return crypto.createHmac(method, key).update(text).digest();
}

interface HICache {
[key: string]: Buffer;
[key: string]: Uint8Array;
}

let _hiCache: HICache = {};
Expand All @@ -288,9 +282,9 @@ const hiLengthMap = {
sha1: 20
};

function HI(data: string, salt: Buffer, iterations: number, cryptoMethod: CryptoMethod) {
function HI(data: string, salt: Uint8Array, iterations: number, cryptoMethod: CryptoMethod) {
// omit the work if already generated
const key = [data, salt.toString('base64'), iterations].join('_');
const key = [data, ByteUtils.toBase64(salt), iterations].join('_');
if (_hiCache[key] != null) {
return _hiCache[key];
}
Expand All @@ -314,7 +308,7 @@ function HI(data: string, salt: Buffer, iterations: number, cryptoMethod: Crypto
return saltedData;
}

function compareDigest(lhs: Buffer, rhs: Uint8Array) {
function compareDigest(lhs: Uint8Array, rhs: Uint8Array) {
if (lhs.length !== rhs.length) {
return false;
}
Expand Down
Loading