Skip to content

Commit

Permalink
refactor(NODE-4859): move random bytes implementations to platform sp…
Browse files Browse the repository at this point in the history
…ecific byte utils (#533)

Co-authored-by: Bailey Pearson <bailey.pearson@mongodb.com>
  • Loading branch information
nbbeeken and baileympearson authored Dec 7, 2022
1 parent 5103e4d commit 19b0654
Show file tree
Hide file tree
Showing 10 changed files with 259 additions and 115 deletions.
4 changes: 2 additions & 2 deletions src/binary.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { bufferToUuidHexString, uuidHexStringToBuffer, uuidValidateString } from './uuid_utils';
import { isUint8Array, randomBytes } from './parser/utils';
import { isUint8Array } from './parser/utils';
import type { EJSONOptions } from './extended_json';
import { BSONError, BSONTypeError } from './error';
import { BSON_BINARY_SUBTYPE_UUID_NEW } from './constants';
Expand Down Expand Up @@ -419,7 +419,7 @@ export class UUID extends Binary {
* Generates a populated buffer containing a v4 uuid
*/
static generate(): Uint8Array {
const bytes = randomBytes(UUID_BYTE_LENGTH);
const bytes = ByteUtils.randomBytes(UUID_BYTE_LENGTH);

// Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
// Kindly borrowed from https://github.com/uuidjs/uuid/blob/master/src/v4.js
Expand Down
4 changes: 2 additions & 2 deletions src/objectid.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BSONTypeError } from './error';
import { isUint8Array, randomBytes } from './parser/utils';
import { isUint8Array } from './parser/utils';
import { BSONDataView, ByteUtils } from './utils/byte_utils';

// Regular expression that checks for hex value
Expand Down Expand Up @@ -154,7 +154,7 @@ export class ObjectId {

// set PROCESS_UNIQUE if yet not initialized
if (PROCESS_UNIQUE === null) {
PROCESS_UNIQUE = randomBytes(5);
PROCESS_UNIQUE = ByteUtils.randomBytes(5);
}

// 5-byte process unique
Expand Down
63 changes: 0 additions & 63 deletions src/parser/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
import { ByteUtils } from '../utils/byte_utils';
import { getGlobal } from '../utils/global';

type RandomBytesFunction = (size: number) => Uint8Array;
declare let console: { warn(...message: unknown[]): void };

/**
* Normalizes our expected stringified form of a function across versions of node
* @param fn - The function to stringify
Expand All @@ -12,63 +6,6 @@ export function normalizedFunctionString(fn: Function): string {
return fn.toString().replace('function(', 'function (');
}

function isReactNative() {
const g = getGlobal<{ navigator?: { product?: string } }>();
return typeof g.navigator === 'object' && g.navigator.product === 'ReactNative';
}

const insecureRandomBytes: RandomBytesFunction = function insecureRandomBytes(size: number) {
const insecureWarning = isReactNative()
? 'BSON: For React Native please polyfill crypto.getRandomValues, e.g. using: https://www.npmjs.com/package/react-native-get-random-values.'
: 'BSON: No cryptographic implementation for random bytes present, falling back to a less secure implementation.';
console.warn(insecureWarning);

const result = ByteUtils.allocate(size);
for (let i = 0; i < size; ++i) result[i] = Math.floor(Math.random() * 256);
return result;
};

/* We do not want to have to include DOM types just for this check */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
declare let window: any;
declare let require: Function;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
declare let global: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
declare let process: any; // Used by @rollup/plugin-replace

const detectRandomBytes = (): RandomBytesFunction => {
if (process.browser) {
if (typeof window !== 'undefined') {
// browser crypto implementation(s)
const target = window.crypto || window.msCrypto; // allow for IE11
if (target && target.getRandomValues) {
return size => target.getRandomValues(ByteUtils.allocate(size));
}
}

if (typeof global !== 'undefined' && global.crypto && global.crypto.getRandomValues) {
// allow for RN packages such as https://www.npmjs.com/package/react-native-get-random-values to populate global
return size => global.crypto.getRandomValues(ByteUtils.allocate(size));
}

return insecureRandomBytes;
} else {
let requiredRandomBytes: RandomBytesFunction | null | undefined;
try {
requiredRandomBytes = require('crypto').randomBytes;
} catch (e) {
// keep the fallback
}

// NOTE: in transpiled cases the above require might return null/undefined

return requiredRandomBytes || insecureRandomBytes;
}
};

export const randomBytes = detectRandomBytes();

export function isAnyArrayBuffer(value: unknown): value is ArrayBuffer {
return ['[object ArrayBuffer]', '[object SharedArrayBuffer]'].includes(
Object.prototype.toString.call(value)
Expand Down
4 changes: 3 additions & 1 deletion src/utils/byte_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ export type ByteUtils = {
toUTF8: (buffer: Uint8Array) => string;
/** Get the utf8 code unit count from a string if it were to be transformed to utf8 */
utf8ByteLength: (input: string) => number;
/** encode UTF8 bytes generated from `source` string into `destination` at byteOffset. Returns the number of bytes encoded. */
/** Encode UTF8 bytes generated from `source` string into `destination` at byteOffset. Returns the number of bytes encoded. */
encodeUTF8Into(destination: Uint8Array, source: string, byteOffset: number): number;
/** Generate a Uint8Array filled with random bytes with byteLength */
randomBytes(byteLength: number): Uint8Array;
};

declare const Buffer: { new (): unknown; prototype?: { _isBuffer?: boolean } } | undefined;
Expand Down
22 changes: 0 additions & 22 deletions src/utils/global.ts

This file was deleted.

23 changes: 22 additions & 1 deletion src/utils/node_byte_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ type NodeJsBufferConstructor = Omit<Uint8ArrayConstructor, 'from'> & {
// Node.js global
declare const Buffer: NodeJsBufferConstructor;

/** @internal */
export function nodejsMathRandomBytes(byteLength: number) {
return nodeJsByteUtils.fromNumberArray(
Array.from({ length: byteLength }, () => Math.floor(Math.random() * 256))
);
}

/** @internal */
const nodejsRandomBytes: (byteLength: number) => Uint8Array = (() => {
try {
// What about nodejs es module users.........
// @ts-expect-error: require does not exist in our type's globals, but it should in nodejs... most of the time
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require('crypto').randomBytes;
} catch {
return nodejsMathRandomBytes;
}
})();

/** @internal */
export const nodeJsByteUtils = {
toLocalBufferType(potentialBuffer: Uint8Array | NodeJsBuffer | ArrayBuffer): NodeJsBuffer {
Expand Down Expand Up @@ -104,5 +123,7 @@ export const nodeJsByteUtils = {

encodeUTF8Into(buffer: Uint8Array, source: string, byteOffset: number): number {
return nodeJsByteUtils.toLocalBufferType(buffer).write(source, byteOffset, undefined, 'utf8');
}
},

randomBytes: nodejsRandomBytes
};
41 changes: 40 additions & 1 deletion src/utils/web_byte_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,43 @@ type ArrayBufferViewWithTag = ArrayBufferView & {
[Symbol.toStringTag]?: string;
};

function isReactNative() {
const { navigator } = globalThis as { navigator?: { product?: string } };
return typeof navigator === 'object' && navigator.product === 'ReactNative';
}

/** @internal */
export function webMathRandomBytes(byteLength: number) {
if (byteLength < 0) {
throw new RangeError(`The argument 'byteLength' is invalid. Received ${byteLength}`);
}
return webByteUtils.fromNumberArray(
Array.from({ length: byteLength }, () => Math.floor(Math.random() * 256))
);
}

/** @internal */
const webRandomBytes: (byteLength: number) => Uint8Array = (() => {
const { crypto } = globalThis as {
crypto?: { getRandomValues?: (space: Uint8Array) => Uint8Array };
};
if (crypto != null && typeof crypto.getRandomValues === 'function') {
return (byteLength: number) => {
// @ts-expect-error: crypto.getRandomValues cannot actually be null here
// You cannot separate getRandomValues from crypto (need to have this === crypto)
return crypto.getRandomValues(webByteUtils.allocate(byteLength));
};
} else {
if (isReactNative()) {
const { console } = globalThis as { console?: { warn?: (message: string) => void } };
console?.warn?.(
'BSON: For React Native please polyfill crypto.getRandomValues, e.g. using: https://www.npmjs.com/package/react-native-get-random-values.'
);
}
return webMathRandomBytes;
}
})();

const HEX_DIGIT = /(\d|[a-f])/i;

/** @internal */
Expand Down Expand Up @@ -147,5 +184,7 @@ export const webByteUtils = {
const bytes = webByteUtils.fromUTF8(source);
buffer.set(bytes, byteOffset);
return bytes.byteLength;
}
},

randomBytes: webRandomBytes
};
37 changes: 37 additions & 0 deletions test/load_bson.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use strict';

const vm = require('node:vm');
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');

function loadBSONWithGlobal(globals) {
// TODO(NODE-4787): Node.js 16 was when the atob and btoa globals were introduced, so we need replacements for testing on 14
const shim_btoa = input => Buffer.prototype.toString.call(Buffer.from(input), 'base64');
const shim_atob = input => Buffer.from(input, 'base64').toString('binary');
// TODO(NODE-4713): Using the umd for now since it works well as a Node.js import
// Switch to the .cjs rollup planned for NODE-4713
const filename = path.resolve(__dirname, '../dist/bson.browser.umd.js');
const code = fs.readFileSync(filename, { encoding: 'utf8' });
// These are the only globals BSON strictly depends on
// an optional global is crypto
const context = vm.createContext({
TextEncoder,
TextDecoder,
btoa: typeof btoa !== 'undefined' ? btoa : shim_btoa,
atob: typeof atob !== 'undefined' ? atob : shim_atob,
crypto: {
getRandomValues(buffer) {
const random = crypto.randomBytes(buffer.byteLength);
buffer.set(random, 0);
return buffer;
}
},
// Putting this last to allow caller to override default globals
...globals
});
vm.runInContext(code, context, { filename });
return context;
}

module.exports = { loadBSONWithGlobal };
Loading

0 comments on commit 19b0654

Please sign in to comment.