Skip to content

Commit 50c44ee

Browse files
committed
remove crypto and use webcrypto
1 parent a04ec7b commit 50c44ee

3 files changed

Lines changed: 68 additions & 38 deletions

File tree

.eslintrc.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@
103103
{
104104
"name": "node:*",
105105
"message": "Don't use `node:*`; use bare Node core module names instead."
106+
},
107+
{
108+
"name": "crypto",
109+
"message": "Use standard Web Crypto API, globalThis.crypto."
106110
}
107111
]
108112
}
@@ -276,7 +280,8 @@
276280
"patterns": [
277281
"**/../lib/**",
278282
"mongodb-mock-server",
279-
"node:*"
283+
"node:*",
284+
"crypto"
280285
],
281286
"paths": [
282287
{

src/cmap/auth/scram.ts

Lines changed: 59 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 {
54
allocateBuffer,
@@ -164,22 +163,22 @@ async function continueScramConversation(
164163

165164
// Set up start of proof
166165
const withoutProof = `c=biws,r=${rnonce}`;
167-
const saltedPassword = HI(processedPassword, fromBase64(salt), iterations, cryptoMethod);
166+
const saltedPassword = await HI(processedPassword, fromBase64(salt), iterations, cryptoMethod);
168167

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

178-
const clientSignature = HMAC(cryptoMethod, storedKey, authMessage);
177+
const clientSignature = await HMAC(cryptoMethod, storedKey, authMessage);
179178
const clientProof = `p=${xor(clientKey, clientSignature)}`;
180179
const clientFinal = [withoutProof, clientProof].join(',');
181180

182-
const serverSignature = HMAC(cryptoMethod, serverKey, authMessage);
181+
const serverSignature = await HMAC(cryptoMethod, serverKey, authMessage);
183182
const saslContinueCmd = {
184183
saslContinue: 1,
185184
conversationId: response.conversationId,
@@ -231,19 +230,28 @@ function passwordDigest(username: string, password: string) {
231230
throw new MongoInvalidArgumentError('Password cannot be empty');
232231
}
233232

234-
let md5: crypto.Hash;
233+
let nodeCrypto;
235234
try {
236-
md5 = crypto.createHash('md5');
235+
// eslint-disable-next-line @typescript-eslint/no-require-imports
236+
nodeCrypto = require('crypto');
237+
} catch (e) {
238+
throw new MongoRuntimeError('Crypto support is required for SCRAM-SHA-1 authentication', {
239+
cause: e
240+
});
241+
}
242+
243+
try {
244+
const md5 = nodeCrypto.createHash('md5');
245+
md5.update(`${username}:mongo:${password}`, 'utf8');
246+
return md5.digest('hex');
237247
} catch (err) {
238-
if (crypto.getFips()) {
248+
if (nodeCrypto.getFips()) {
239249
// This error is (slightly) more helpful than what comes from OpenSSL directly, e.g.
240250
// 'Error: error:060800C8:digital envelope routines:EVP_DigestInit_ex:disabled for FIPS'
241251
throw new Error('Auth mechanism SCRAM-SHA-1 is not supported in FIPS mode');
242252
}
243253
throw err;
244254
}
245-
md5.update(`${username}:mongo:${password}`, 'utf8');
246-
return md5.digest('hex');
247255
}
248256

249257
// XOR two buffers
@@ -258,12 +266,28 @@ function xor(a: Uint8Array, b: Uint8Array) {
258266
return ByteUtils.toBase64(fromNumberArray(res));
259267
}
260268

261-
function H(method: CryptoMethod, text: Uint8Array): Uint8Array {
262-
return crypto.createHash(method).update(text).digest();
269+
async function H(method: CryptoMethod, text: Uint8Array): Promise<Uint8Array> {
270+
const buffer = await crypto.subtle.digest(method === 'sha256' ? 'SHA-256' : 'SHA-1', text);
271+
return new Uint8Array(buffer);
263272
}
264273

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

269293
interface HICache {
@@ -282,21 +306,32 @@ const hiLengthMap = {
282306
sha1: 20
283307
};
284308

285-
function HI(data: string, salt: Uint8Array, iterations: number, cryptoMethod: CryptoMethod) {
309+
async function HI(data: string, salt: Uint8Array, iterations: number, cryptoMethod: CryptoMethod) {
286310
// omit the work if already generated
287311
const key = [data, ByteUtils.toBase64(salt), iterations].join('_');
288312
if (_hiCache[key] != null) {
289313
return _hiCache[key];
290314
}
291315

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

301336
// cache a copy to speed up the next lookup, but prevent unbounded cache growth
302337
if (_hiCacheCount >= 200) {
@@ -313,10 +348,6 @@ function compareDigest(lhs: Uint8Array, rhs: Uint8Array) {
313348
return false;
314349
}
315350

316-
if (typeof crypto.timingSafeEqual === 'function') {
317-
return crypto.timingSafeEqual(lhs, rhs);
318-
}
319-
320351
let result = 0;
321352
for (let i = 0; i < lhs.length; i++) {
322353
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';
@@ -308,7 +307,7 @@ export function* makeCounter(seed = 0): Generator<number> {
308307
* @internal
309308
*/
310309
export function uuidV4(): Uint8Array {
311-
const result = crypto.randomBytes(16);
310+
const result = crypto.getRandomValues(new Uint8Array(16));
312311
result[6] = (result[6] & 0x0f) | 0x40;
313312
result[8] = (result[8] & 0x3f) | 0x80;
314313
return result;
@@ -1228,13 +1227,8 @@ export function squashError(_error: unknown) {
12281227
return;
12291228
}
12301229

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

12401234
/**

0 commit comments

Comments
 (0)