Skip to content

Commit 76e1826

Browse files
authored
feat: UUID convenience class (#425)
Adds a class that encapsulates generating and interacting with UUIDs stored in the BSON Binary type. Adds `to/from` UUID Binary helpers. Adds cross platform crypto API usage for secure random number generation when available.
1 parent 1576fd0 commit 76e1826

10 files changed

+457
-60
lines changed

package-lock.json

+11-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@
6363
"ts-node": "^9.0.0",
6464
"typedoc": "^0.18.0",
6565
"typescript": "^4.0.2",
66-
"typescript-cached-transpile": "0.0.6"
66+
"typescript-cached-transpile": "0.0.6",
67+
"uuid": "^8.3.2"
6768
},
6869
"config": {
6970
"native": false

src/binary.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Buffer } from 'buffer';
22
import { ensureBuffer } from './ensure_buffer';
3+
import { uuidHexStringToBuffer } from './uuid_utils';
4+
import { UUID, UUIDExtended } from './uuid';
35
import type { EJSONOptions } from './extended_json';
4-
import { parseUUID, UUIDExtended } from './uuid';
56

67
/** @public */
78
export type BinarySequence = Uint8Array | Buffer | number[];
@@ -230,6 +231,17 @@ export class Binary {
230231
};
231232
}
232233

234+
/** @internal */
235+
toUUID(): UUID {
236+
if (this.sub_type === Binary.SUBTYPE_UUID) {
237+
return new UUID(this.buffer.slice(0, this.position));
238+
}
239+
240+
throw new Error(
241+
`Binary sub_type "${this.sub_type}" is not supported for converting to UUID. Only "${Binary.SUBTYPE_UUID}" is currently supported.`
242+
);
243+
}
244+
233245
/** @internal */
234246
static fromExtendedJSON(
235247
doc: BinaryExtendedLegacy | BinaryExtended | UUIDExtended,
@@ -250,7 +262,7 @@ export class Binary {
250262
}
251263
} else if ('$uuid' in doc) {
252264
type = 4;
253-
data = Buffer.from(parseUUID(doc.$uuid));
265+
data = uuidHexStringToBuffer(doc.$uuid);
254266
}
255267
if (!data) {
256268
throw new TypeError(`Unexpected Binary Extended JSON format ${JSON.stringify(doc)}`);

src/bson.ts

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Map } from './map';
1212
import { MaxKey } from './max_key';
1313
import { MinKey } from './min_key';
1414
import { ObjectId } from './objectid';
15+
import { UUID } from './uuid';
1516
import { calculateObjectSize as internalCalculateObjectSize } from './parser/calculate_size';
1617
// Parts of the parser
1718
import { deserialize as internalDeserialize, DeserializeOptions } from './parser/deserializer';
@@ -81,6 +82,7 @@ export {
8182
DBRef,
8283
Binary,
8384
ObjectId,
85+
UUID,
8486
Long,
8587
Timestamp,
8688
Double,

src/objectid.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ export class ObjectId {
229229
typeof otherId === 'string' &&
230230
ObjectId.isValid(otherId) &&
231231
otherId.length === 12 &&
232-
this.id instanceof Buffer
232+
Buffer.isBuffer(this.id)
233233
) {
234234
return otherId === this.id.toString('binary');
235235
}
@@ -315,7 +315,7 @@ export class ObjectId {
315315
return true;
316316
}
317317

318-
if (id instanceof Buffer && id.length === 12) {
318+
if (Buffer.isBuffer(id) && id.length === 12) {
319319
return true;
320320
}
321321

src/parser/utils.ts

+34-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Buffer } from 'buffer';
22

3+
type RandomBytesFunction = (size: number) => Uint8Array;
4+
35
/**
46
* Normalizes our expected stringified form of a function across versions of node
57
* @param fn - The function to stringify
@@ -8,11 +10,20 @@ export function normalizedFunctionString(fn: Function): string {
810
return fn.toString().replace('function(', 'function (');
911
}
1012

11-
function insecureRandomBytes(size: number): Uint8Array {
13+
const isReactNative =
14+
typeof global.navigator === 'object' && global.navigator.product === 'ReactNative';
15+
16+
const insecureWarning = isReactNative
17+
? 'BSON: For React Native please polyfill crypto.getRandomValues, e.g. using: https://www.npmjs.com/package/react-native-get-random-values.'
18+
: 'BSON: No cryptographic implementation for random bytes present, falling back to a less secure implementation.';
19+
20+
const insecureRandomBytes: RandomBytesFunction = function insecureRandomBytes(size: number) {
21+
console.warn(insecureWarning);
22+
1223
const result = Buffer.alloc(size);
1324
for (let i = 0; i < size; ++i) result[i] = Math.floor(Math.random() * 256);
1425
return result;
15-
}
26+
};
1627

1728
/* We do not want to have to include DOM types just for this check */
1829
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -22,22 +33,34 @@ declare let require: Function;
2233
declare let global: any;
2334
declare const self: unknown;
2435

25-
export let randomBytes = insecureRandomBytes;
26-
if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) {
27-
randomBytes = size => window.crypto.getRandomValues(Buffer.alloc(size));
28-
} else {
36+
const detectRandomBytes = (): RandomBytesFunction => {
37+
if (typeof window !== 'undefined') {
38+
// browser crypto implementation(s)
39+
const target = window.crypto || window.msCrypto; // allow for IE11
40+
if (target && target.getRandomValues) {
41+
return size => target.getRandomValues(Buffer.alloc(size));
42+
}
43+
}
44+
45+
if (typeof global !== 'undefined' && global.crypto && global.crypto.getRandomValues) {
46+
// allow for RN packages such as https://www.npmjs.com/package/react-native-get-random-values to populate global
47+
return size => global.crypto.getRandomValues(Buffer.alloc(size));
48+
}
49+
50+
let requiredRandomBytes: RandomBytesFunction | null | undefined;
2951
try {
3052
// eslint-disable-next-line @typescript-eslint/no-var-requires
31-
randomBytes = require('crypto').randomBytes;
53+
requiredRandomBytes = require('crypto').randomBytes;
3254
} catch (e) {
3355
// keep the fallback
3456
}
3557

3658
// NOTE: in transpiled cases the above require might return null/undefined
37-
if (randomBytes == null) {
38-
randomBytes = insecureRandomBytes;
39-
}
40-
}
59+
60+
return requiredRandomBytes || insecureRandomBytes;
61+
};
62+
63+
export const randomBytes = detectRandomBytes();
4164

4265
export function isUint8Array(value: unknown): value is Uint8Array {
4366
return Object.prototype.toString.call(value) === '[object Uint8Array]';

0 commit comments

Comments
 (0)