Skip to content

Commit 631a0ec

Browse files
authored
Reland "Use crypo RNG for auto ID generation to reduce conflicts (#2764)" (#3012)
1 parent 8143c83 commit 631a0ec

File tree

5 files changed

+60
-3
lines changed

5 files changed

+60
-3
lines changed

packages/firestore/src/platform/platform.ts

+7
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ export interface Platform {
4343
/** Converts a binary string to a Base64 encoded string. */
4444
btoa(raw: string): string;
4545

46+
/**
47+
* Generates `nBytes` of random bytes.
48+
*
49+
* If `nBytes < 0` , an error will be thrown.
50+
*/
51+
randomBytes(nBytes: number): Uint8Array;
52+
4653
/** The Platform's 'window' implementation or null if not available. */
4754
readonly window: Window | null;
4855

packages/firestore/src/platform_browser/browser_platform.ts

+21
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ import { Platform } from '../platform/platform';
2020
import { Connection } from '../remote/connection';
2121
import { JsonProtoSerializer } from '../remote/serializer';
2222
import { ConnectivityMonitor } from './../remote/connectivity_monitor';
23+
2324
import { NoopConnectivityMonitor } from '../remote/connectivity_monitor_noop';
2425
import { BrowserConnectivityMonitor } from './browser_connectivity_monitor';
2526
import { WebChannelConnection } from './webchannel_connection';
27+
import { debugAssert } from '../util/assert';
2628

2729
// Implements the Platform API for browsers and some browser-like environments
2830
// (including ReactNative).
@@ -72,4 +74,23 @@ export class BrowserPlatform implements Platform {
7274
btoa(raw: string): string {
7375
return btoa(raw);
7476
}
77+
78+
randomBytes(nBytes: number): Uint8Array {
79+
debugAssert(nBytes >= 0, `Expecting non-negative nBytes, got: ${nBytes}`);
80+
81+
// Polyfills for IE and WebWorker by using `self` and `msCrypto` when `crypto` is not available.
82+
const crypto =
83+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
84+
typeof self !== 'undefined' && (self.crypto || (self as any)['msCrypto']);
85+
const bytes = new Uint8Array(nBytes);
86+
if (crypto) {
87+
crypto.getRandomValues(bytes);
88+
} else {
89+
// Falls back to Math.random
90+
for (let i = 0; i < nBytes; i++) {
91+
bytes[i] = Math.floor(Math.random() * 256);
92+
}
93+
}
94+
return bytes;
95+
}
7596
}

packages/firestore/src/platform_node/node_platform.ts

+8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18+
import { randomBytes } from 'crypto';
1819
import { inspect } from 'util';
1920

2021
import { DatabaseId, DatabaseInfo } from '../core/database_info';
@@ -27,6 +28,7 @@ import { NoopConnectivityMonitor } from './../remote/connectivity_monitor_noop';
2728

2829
import { GrpcConnection } from './grpc_connection';
2930
import { loadProtos } from './load_protos';
31+
import { debugAssert } from '../util/assert';
3032

3133
export class NodePlatform implements Platform {
3234
readonly base64Available = true;
@@ -75,4 +77,10 @@ export class NodePlatform implements Platform {
7577
btoa(raw: string): string {
7678
return new Buffer(raw, 'binary').toString('base64');
7779
}
80+
81+
randomBytes(nBytes: number): Uint8Array {
82+
debugAssert(nBytes >= 0, `Expecting non-negative nBytes, got: ${nBytes}`);
83+
84+
return randomBytes(nBytes);
85+
}
7886
}

packages/firestore/src/util/misc.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717

1818
import { debugAssert } from './assert';
19+
import { PlatformSupport } from '../platform/platform';
1920

2021
export type EventHandler<E> = (value: E) => void;
2122
export interface Indexable {
@@ -27,11 +28,27 @@ export class AutoId {
2728
// Alphanumeric characters
2829
const chars =
2930
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
31+
// The largest byte value that is a multiple of `char.length`.
32+
const maxMultiple = Math.floor(256 / chars.length) * chars.length;
33+
debugAssert(
34+
0 < maxMultiple && maxMultiple < 256,
35+
`Expect maxMultiple to be (0, 256), but got ${maxMultiple}`
36+
);
37+
3038
let autoId = '';
31-
for (let i = 0; i < 20; i++) {
32-
autoId += chars.charAt(Math.floor(Math.random() * chars.length));
39+
const targetLength = 20;
40+
while (autoId.length < targetLength) {
41+
const bytes = PlatformSupport.getPlatform().randomBytes(40);
42+
for (let i = 0; i < bytes.length; ++i) {
43+
// Only accept values that are [0, maxMultiple), this ensures they can
44+
// be evenly mapped to indices of `chars` via a modulo operation.
45+
if (autoId.length < targetLength && bytes[i] < maxMultiple) {
46+
autoId += chars.charAt(bytes[i] % chars.length);
47+
}
48+
}
3349
}
34-
debugAssert(autoId.length === 20, 'Invalid auto ID: ' + autoId);
50+
debugAssert(autoId.length === targetLength, 'Invalid auto ID: ' + autoId);
51+
3552
return autoId;
3653
}
3754
}

packages/firestore/test/util/test_platform.ts

+4
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,10 @@ export class TestPlatform implements Platform {
266266
btoa(raw: string): string {
267267
return this.basePlatform.btoa(raw);
268268
}
269+
270+
randomBytes(nBytes: number): Uint8Array {
271+
return this.basePlatform.randomBytes(nBytes);
272+
}
269273
}
270274

271275
/** Returns true if we are running under Node. */

0 commit comments

Comments
 (0)