diff --git a/packages/firestore/src/platform/platform.ts b/packages/firestore/src/platform/platform.ts index f206ab1a77e..1ab582ea05f 100644 --- a/packages/firestore/src/platform/platform.ts +++ b/packages/firestore/src/platform/platform.ts @@ -43,6 +43,13 @@ export interface Platform { /** Converts a binary string to a Base64 encoded string. */ btoa(raw: string): string; + /** + * Generates `nBytes` of random bytes. + * + * If `nBytes < 0` , an error will be thrown. + */ + randomBytes(nBytes: number): Uint8Array; + /** The Platform's 'window' implementation or null if not available. */ readonly window: Window | null; diff --git a/packages/firestore/src/platform_browser/browser_platform.ts b/packages/firestore/src/platform_browser/browser_platform.ts index 48b19339e29..38133db4df5 100644 --- a/packages/firestore/src/platform_browser/browser_platform.ts +++ b/packages/firestore/src/platform_browser/browser_platform.ts @@ -20,9 +20,11 @@ import { Platform } from '../platform/platform'; import { Connection } from '../remote/connection'; import { JsonProtoSerializer } from '../remote/serializer'; import { ConnectivityMonitor } from './../remote/connectivity_monitor'; + import { NoopConnectivityMonitor } from '../remote/connectivity_monitor_noop'; import { BrowserConnectivityMonitor } from './browser_connectivity_monitor'; import { WebChannelConnection } from './webchannel_connection'; +import { debugAssert } from '../util/assert'; // Implements the Platform API for browsers and some browser-like environments // (including ReactNative). @@ -72,4 +74,23 @@ export class BrowserPlatform implements Platform { btoa(raw: string): string { return btoa(raw); } + + randomBytes(nBytes: number): Uint8Array { + debugAssert(nBytes >= 0, `Expecting non-negative nBytes, got: ${nBytes}`); + + // Polyfills for IE and WebWorker by using `self` and `msCrypto` when `crypto` is not available. + const crypto = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + typeof self !== 'undefined' && (self.crypto || (self as any)['msCrypto']); + const bytes = new Uint8Array(nBytes); + if (crypto) { + crypto.getRandomValues(bytes); + } else { + // Falls back to Math.random + for (let i = 0; i < nBytes; i++) { + bytes[i] = Math.floor(Math.random() * 256); + } + } + return bytes; + } } diff --git a/packages/firestore/src/platform_node/node_platform.ts b/packages/firestore/src/platform_node/node_platform.ts index 927209acd5a..2fe74f7d68b 100644 --- a/packages/firestore/src/platform_node/node_platform.ts +++ b/packages/firestore/src/platform_node/node_platform.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { randomBytes } from 'crypto'; import { inspect } from 'util'; import { DatabaseId, DatabaseInfo } from '../core/database_info'; @@ -27,6 +28,7 @@ import { NoopConnectivityMonitor } from './../remote/connectivity_monitor_noop'; import { GrpcConnection } from './grpc_connection'; import { loadProtos } from './load_protos'; +import { debugAssert } from '../util/assert'; export class NodePlatform implements Platform { readonly base64Available = true; @@ -75,4 +77,10 @@ export class NodePlatform implements Platform { btoa(raw: string): string { return new Buffer(raw, 'binary').toString('base64'); } + + randomBytes(nBytes: number): Uint8Array { + debugAssert(nBytes >= 0, `Expecting non-negative nBytes, got: ${nBytes}`); + + return randomBytes(nBytes); + } } diff --git a/packages/firestore/src/util/misc.ts b/packages/firestore/src/util/misc.ts index 8a46a11292c..35a7d40baf4 100644 --- a/packages/firestore/src/util/misc.ts +++ b/packages/firestore/src/util/misc.ts @@ -16,6 +16,7 @@ */ import { debugAssert } from './assert'; +import { PlatformSupport } from '../platform/platform'; export type EventHandler = (value: E) => void; export interface Indexable { @@ -27,11 +28,27 @@ export class AutoId { // Alphanumeric characters const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + // The largest byte value that is a multiple of `char.length`. + const maxMultiple = Math.floor(256 / chars.length) * chars.length; + debugAssert( + 0 < maxMultiple && maxMultiple < 256, + `Expect maxMultiple to be (0, 256), but got ${maxMultiple}` + ); + let autoId = ''; - for (let i = 0; i < 20; i++) { - autoId += chars.charAt(Math.floor(Math.random() * chars.length)); + const targetLength = 20; + while (autoId.length < targetLength) { + const bytes = PlatformSupport.getPlatform().randomBytes(40); + for (let i = 0; i < bytes.length; ++i) { + // Only accept values that are [0, maxMultiple), this ensures they can + // be evenly mapped to indices of `chars` via a modulo operation. + if (autoId.length < targetLength && bytes[i] < maxMultiple) { + autoId += chars.charAt(bytes[i] % chars.length); + } + } } - debugAssert(autoId.length === 20, 'Invalid auto ID: ' + autoId); + debugAssert(autoId.length === targetLength, 'Invalid auto ID: ' + autoId); + return autoId; } } diff --git a/packages/firestore/test/util/test_platform.ts b/packages/firestore/test/util/test_platform.ts index 1d1f8346d77..cd9d458f63d 100644 --- a/packages/firestore/test/util/test_platform.ts +++ b/packages/firestore/test/util/test_platform.ts @@ -266,6 +266,10 @@ export class TestPlatform implements Platform { btoa(raw: string): string { return this.basePlatform.btoa(raw); } + + randomBytes(nBytes: number): Uint8Array { + return this.basePlatform.randomBytes(nBytes); + } } /** Returns true if we are running under Node. */