Skip to content

Commit 4b9a723

Browse files
committed
feat(NODE-7379): Refactor Crypto to Web Crypto API
1 parent 5535a9c commit 4b9a723

File tree

4 files changed

+76
-39
lines changed

4 files changed

+76
-39
lines changed

.eslintrc.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,8 @@
277277
"**/../lib/**",
278278
"mongodb-mock-server",
279279
"node:*",
280-
"os"
280+
"os",
281+
"crypto"
281282
],
282283
"paths": [
283284
{
@@ -335,4 +336,4 @@
335336
}
336337
}
337338
]
338-
}
339+
}

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ 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 NodeJS as a dependency of the driver, so that in the future it will be possible to use the drive 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 NodeJS and is not supported in FIPS mode.
113+
104114
## Installation
105115

106116
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/scram.ts

Lines changed: 60 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,29 @@ 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('global crypto is required for SCRAM-SHA-1 authentication', {
238+
cause: e
239+
});
240+
}
241+
242+
try {
243+
const md5 = nodeCrypto.createHash('md5');
244+
md5.update(`${username}:mongo:${password}`, 'utf8');
245+
return md5.digest('hex');
235246
} catch (err) {
236-
if (crypto.getFips()) {
247+
if (nodeCrypto.getFips()) {
237248
// This error is (slightly) more helpful than what comes from OpenSSL directly, e.g.
238249
// 'Error: error:060800C8:digital envelope routines:EVP_DigestInit_ex:disabled for FIPS'
239250
throw new Error('Auth mechanism SCRAM-SHA-1 is not supported in FIPS mode');
240251
}
241252
throw err;
242253
}
243-
md5.update(`${username}:mongo:${password}`, 'utf8');
244-
return md5.digest('hex');
245254
}
246255

247256
// XOR two buffers
@@ -256,12 +265,28 @@ function xor(a: Uint8Array, b: Uint8Array) {
256265
return ByteUtils.toBase64(ByteUtils.fromNumberArray(res));
257266
}
258267

259-
function H(method: CryptoMethod, text: Uint8Array): Uint8Array {
260-
return crypto.createHash(method).update(text).digest();
268+
async function H(method: CryptoMethod, text: Uint8Array): Promise<Uint8Array> {
269+
const buffer = await crypto.subtle.digest(method === 'sha256' ? 'SHA-256' : 'SHA-1', text);
270+
return new Uint8Array(buffer);
261271
}
262272

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

267292
interface HICache {
@@ -280,21 +305,32 @@ const hiLengthMap = {
280305
sha1: 20
281306
};
282307

283-
function HI(data: string, salt: Uint8Array, iterations: number, cryptoMethod: CryptoMethod) {
308+
async function HI(data: string, salt: Uint8Array, iterations: number, cryptoMethod: CryptoMethod) {
284309
// omit the work if already generated
285310
const key = [data, ByteUtils.toBase64(salt), iterations].join('_');
286311
if (_hiCache[key] != null) {
287312
return _hiCache[key];
288313
}
289314

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

299335
// cache a copy to speed up the next lookup, but prevent unbounded cache growth
300336
if (_hiCacheCount >= 200) {
@@ -311,10 +347,6 @@ function compareDigest(lhs: Uint8Array, rhs: Uint8Array) {
311347
return false;
312348
}
313349

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

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)