Skip to content

Commit e798fd8

Browse files
authored
Merge branch 'main' into NODE-7470/test-backpressure-retries-are-nonblocking
2 parents 1b14e86 + ac98f4a commit e798fd8

14 files changed

Lines changed: 232 additions & 202 deletions

.eslintrc.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,8 @@
279279
"**/../lib/**",
280280
"mongodb-mock-server",
281281
"node:*",
282-
"os"
282+
"os",
283+
"crypto"
283284
],
284285
"paths": [
285286
{
@@ -346,4 +347,4 @@
346347
}
347348
}
348349
]
349-
}
350+
}

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ If you run into any unexpected compiler failures against our supported TypeScrip
101101

102102
Additionally, our Typescript types are compatible with the ECMAScript standard for our minimum supported Node version. Currently, our Typescript targets es2023.
103103

104+
#### Running in Custom Runtimes
105+
106+
We are working on removing Node.js as a dependency of the driver, so that in the future it will be possible to use the driver in non-Node environments.
107+
This work is currently in progress, and if you're curious, this is [our first runtime adapter commit](https://github.com/mongodb/node-mongodb-native/commit/d2ad07f20903d86334da81222a6df9717f76faaa).
108+
109+
Some things to keep in mind if you are using a non-Node runtime:
110+
111+
1. Users of Webpack/Vite may need to prevent `crypto` polyfill injection.
112+
2. Auth mechanism `SCRAM-SHA-1` has a hard dependency on Node.js.
113+
3. Auth mechanism `SCRAM-SHA-1` is not supported in FIPS mode.
114+
104115
## Installation
105116

106117
The recommended way to get started using the Node.js driver is by using the `npm` (Node Package Manager) to install the dependency in your project.

src/cmap/auth/gssapi.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export async function performGSSAPICanonicalizeHostName(
169169

170170
try {
171171
// Perform a reverse ptr lookup on the ip address.
172-
const results = await dns.promises.resolvePtr(address);
172+
const results = await dns.promises.resolve(address, 'PTR');
173173
// If the ptr did not error but had no results, return the host.
174174
return results.length > 0 ? results[0] : host;
175175
} catch {
@@ -188,7 +188,7 @@ export async function performGSSAPICanonicalizeHostName(
188188
export async function resolveCname(host: string): Promise<string> {
189189
// Attempt to resolve the host name
190190
try {
191-
const results = await dns.promises.resolveCname(host);
191+
const results = await dns.promises.resolve(host, 'CNAME');
192192
// Get the first resolved host id
193193
return results.length > 0 ? results[0] : host;
194194
} catch {

src/cmap/auth/scram.ts

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { saslprep } from '@mongodb-js/saslprep';
2-
import * as crypto from 'crypto';
32

43
import { Binary, ByteUtils, type Document } from '../../bson';
54
import {
@@ -157,27 +156,27 @@ async function continueScramConversation(
157156

158157
// Set up start of proof
159158
const withoutProof = `c=biws,r=${rnonce}`;
160-
const saltedPassword = HI(
159+
const saltedPassword = await HI(
161160
processedPassword,
162161
ByteUtils.fromBase64(salt),
163162
iterations,
164163
cryptoMethod
165164
);
166165

167-
const clientKey = HMAC(cryptoMethod, saltedPassword, 'Client Key');
168-
const serverKey = HMAC(cryptoMethod, saltedPassword, 'Server Key');
169-
const storedKey = H(cryptoMethod, clientKey);
166+
const clientKey = await HMAC(cryptoMethod, saltedPassword, 'Client Key');
167+
const serverKey = await HMAC(cryptoMethod, saltedPassword, 'Server Key');
168+
const storedKey = await H(cryptoMethod, clientKey);
170169
const authMessage = [
171170
clientFirstMessageBare(username, nonce),
172171
payload.toString('utf8'),
173172
withoutProof
174173
].join(',');
175174

176-
const clientSignature = HMAC(cryptoMethod, storedKey, authMessage);
175+
const clientSignature = await HMAC(cryptoMethod, storedKey, authMessage);
177176
const clientProof = `p=${xor(clientKey, clientSignature)}`;
178177
const clientFinal = [withoutProof, clientProof].join(',');
179178

180-
const serverSignature = HMAC(cryptoMethod, serverKey, authMessage);
179+
const serverSignature = await HMAC(cryptoMethod, serverKey, authMessage);
181180
const saslContinueCmd = {
182181
saslContinue: 1,
183182
conversationId: response.conversationId,
@@ -229,19 +228,32 @@ function passwordDigest(username: string, password: string) {
229228
throw new MongoInvalidArgumentError('Password cannot be empty');
230229
}
231230

232-
let md5: crypto.Hash;
231+
let nodeCrypto;
233232
try {
234-
md5 = crypto.createHash('md5');
233+
// TODO: NODE-7424 - remove dependency on 'crypto' for SCRAM-SHA-1 authentication
234+
// eslint-disable-next-line @typescript-eslint/no-require-imports
235+
nodeCrypto = require('crypto');
236+
} catch (e) {
237+
throw new MongoRuntimeError(
238+
'Node.js crypto module is required for SCRAM-SHA-1 authentication',
239+
{
240+
cause: e
241+
}
242+
);
243+
}
244+
245+
try {
246+
const md5 = nodeCrypto.createHash('md5');
247+
md5.update(`${username}:mongo:${password}`, 'utf8');
248+
return md5.digest('hex');
235249
} catch (err) {
236-
if (crypto.getFips()) {
250+
if (nodeCrypto.getFips()) {
237251
// This error is (slightly) more helpful than what comes from OpenSSL directly, e.g.
238252
// 'Error: error:060800C8:digital envelope routines:EVP_DigestInit_ex:disabled for FIPS'
239253
throw new Error('Auth mechanism SCRAM-SHA-1 is not supported in FIPS mode');
240254
}
241255
throw err;
242256
}
243-
md5.update(`${username}:mongo:${password}`, 'utf8');
244-
return md5.digest('hex');
245257
}
246258

247259
// XOR two buffers
@@ -256,12 +268,28 @@ function xor(a: Uint8Array, b: Uint8Array) {
256268
return ByteUtils.toBase64(ByteUtils.fromNumberArray(res));
257269
}
258270

259-
function H(method: CryptoMethod, text: Uint8Array): Uint8Array {
260-
return crypto.createHash(method).update(text).digest();
271+
async function H(method: CryptoMethod, text: Uint8Array): Promise<Uint8Array> {
272+
const buffer = await crypto.subtle.digest(method === 'sha256' ? 'SHA-256' : 'SHA-1', text);
273+
return new Uint8Array(buffer);
261274
}
262275

263-
function HMAC(method: CryptoMethod, key: Uint8Array, text: Uint8Array | string): Uint8Array {
264-
return crypto.createHmac(method, key).update(text).digest();
276+
async function HMAC(
277+
method: CryptoMethod,
278+
key: Uint8Array,
279+
text: Uint8Array | string
280+
): Promise<Uint8Array> {
281+
const keyBuffer = ByteUtils.toLocalBufferType(key);
282+
const cryptoKey = await crypto.subtle.importKey(
283+
'raw',
284+
keyBuffer,
285+
{ name: 'HMAC', hash: { name: method === 'sha256' ? 'SHA-256' : 'SHA-1' } },
286+
false,
287+
['sign', 'verify']
288+
);
289+
const textData: Uint8Array = typeof text === 'string' ? new TextEncoder().encode(text) : text;
290+
const textBuffer = ByteUtils.toLocalBufferType(textData);
291+
const signature = await crypto.subtle.sign('HMAC', cryptoKey, textBuffer);
292+
return new Uint8Array(signature);
265293
}
266294

267295
interface HICache {
@@ -280,21 +308,32 @@ const hiLengthMap = {
280308
sha1: 20
281309
};
282310

283-
function HI(data: string, salt: Uint8Array, iterations: number, cryptoMethod: CryptoMethod) {
311+
async function HI(data: string, salt: Uint8Array, iterations: number, cryptoMethod: CryptoMethod) {
284312
// omit the work if already generated
285313
const key = [data, ByteUtils.toBase64(salt), iterations].join('_');
286314
if (_hiCache[key] != null) {
287315
return _hiCache[key];
288316
}
289317

290-
// generate the salt
291-
const saltedData = crypto.pbkdf2Sync(
292-
data,
293-
salt,
294-
iterations,
295-
hiLengthMap[cryptoMethod],
296-
cryptoMethod
318+
const keyMaterial = await crypto.subtle.importKey(
319+
'raw',
320+
new TextEncoder().encode(data),
321+
{ name: 'PBKDF2' },
322+
false,
323+
['deriveBits']
324+
);
325+
const params = {
326+
name: 'PBKDF2',
327+
salt: salt,
328+
iterations: iterations,
329+
hash: { name: cryptoMethod === 'sha256' ? 'SHA-256' : 'SHA-1' }
330+
};
331+
const derivedBits = await crypto.subtle.deriveBits(
332+
params,
333+
keyMaterial,
334+
hiLengthMap[cryptoMethod] * 8
297335
);
336+
const saltedData = new Uint8Array(derivedBits);
298337

299338
// cache a copy to speed up the next lookup, but prevent unbounded cache growth
300339
if (_hiCacheCount >= 200) {
@@ -311,10 +350,6 @@ function compareDigest(lhs: Uint8Array, rhs: Uint8Array) {
311350
return false;
312351
}
313352

314-
if (typeof crypto.timingSafeEqual === 'function') {
315-
return crypto.timingSafeEqual(lhs, rhs);
316-
}
317-
318353
let result = 0;
319354
for (let i = 0; i < lhs.length; i++) {
320355
result |= lhs[i] ^ rhs[i];

src/connection_string.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,26 +42,30 @@ const LB_REPLICA_SET_ERROR = 'loadBalanced option not supported with a replicaSe
4242
const LB_DIRECT_CONNECTION_ERROR =
4343
'loadBalanced option not supported when directConnection is provided';
4444

45-
function retryDNSTimeoutFor(api: 'resolveSrv'): (a: string) => Promise<dns.SrvRecord[]>;
46-
function retryDNSTimeoutFor(api: 'resolveTxt'): (a: string) => Promise<string[][]>;
45+
function retryDNSTimeoutFor(rrtype: 'SRV'): (lookupAddress: string) => Promise<dns.SrvRecord[]>;
46+
function retryDNSTimeoutFor(rrtype: 'TXT'): (lookupAddress: string) => Promise<string[][]>;
4747
function retryDNSTimeoutFor(
48-
api: 'resolveSrv' | 'resolveTxt'
49-
): (a: string) => Promise<dns.SrvRecord[] | string[][]> {
48+
rrtype: 'SRV' | 'TXT'
49+
): (lookupAddress: string) => Promise<dns.SrvRecord[] | string[][]> {
50+
const resolve =
51+
rrtype === 'SRV'
52+
? (address: string) => dns.promises.resolve(address, 'SRV')
53+
: (address: string) => dns.promises.resolve(address, 'TXT');
5054
return async function dnsReqRetryTimeout(lookupAddress: string) {
5155
try {
52-
return await dns.promises[api](lookupAddress);
56+
return await resolve(lookupAddress);
5357
} catch (firstDNSError) {
5458
if (firstDNSError.code === dns.TIMEOUT) {
55-
return await dns.promises[api](lookupAddress);
59+
return await resolve(lookupAddress);
5660
} else {
5761
throw firstDNSError;
5862
}
5963
}
6064
};
6165
}
6266

63-
const resolveSrv = retryDNSTimeoutFor('resolveSrv');
64-
const resolveTxt = retryDNSTimeoutFor('resolveTxt');
67+
const resolveSrv = retryDNSTimeoutFor('SRV');
68+
const resolveTxt = retryDNSTimeoutFor('TXT');
6569

6670
/**
6771
* Lookup a `mongodb+srv` connection string, combine the parts and reparse it as a normal

src/sdam/srv_polling.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export class SrvPoller extends TypedEventEmitter<SrvPollerEvents> {
116116
let srvRecords;
117117

118118
try {
119-
srvRecords = await dns.promises.resolveSrv(this.srvAddress);
119+
srvRecords = await dns.promises.resolve(this.srvAddress, 'SRV');
120120
} catch {
121121
this.failure();
122122
return;

src/utils.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as crypto from 'crypto';
21
import type { SrvRecord } from 'dns';
32
import { type EventEmitter } from 'events';
43
import { promises as fs } from 'fs';
@@ -306,7 +305,7 @@ export function* makeCounter(seed = 0): Generator<number> {
306305
* @internal
307306
*/
308307
export function uuidV4(): Uint8Array {
309-
const result = crypto.randomBytes(16);
308+
const result = crypto.getRandomValues(new Uint8Array(16));
310309
result[6] = (result[6] & 0x0f) | 0x40;
311310
result[8] = (result[8] & 0x3f) | 0x80;
312311
return result;
@@ -1226,13 +1225,8 @@ export function squashError(_error: unknown) {
12261225
return;
12271226
}
12281227

1229-
export const randomBytes = (size: number) => {
1230-
return new Promise<Uint8Array>((resolve, reject) => {
1231-
crypto.randomBytes(size, (error: Error | null, buf: Uint8Array) => {
1232-
if (error) return reject(error);
1233-
resolve(buf);
1234-
});
1235-
});
1228+
export const randomBytes = (size: number): Promise<Uint8Array> => {
1229+
return Promise.resolve(crypto.getRandomValues(new Uint8Array(size)));
12361230
};
12371231

12381232
/**

0 commit comments

Comments
 (0)