Skip to content

Reland "Use crypo RNG for auto ID generation to reduce conflicts (#2764)" #3012

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 7, 2020

Conversation

wu-hui
Copy link
Contributor

@wu-hui wu-hui commented May 4, 2020

Manually verified on IE11 and web-workers.

@wu-hui wu-hui changed the title Reland SRNG Reland "Use crypo RNG for auto ID generation to reduce conflicts (#2764)" May 4, 2020
@wu-hui wu-hui requested a review from schmidt-sebastian May 4, 2020 21:14
@google-oss-bot
Copy link
Contributor

google-oss-bot commented May 4, 2020

Binary Size Report

Affected SDKs

  • @firebase/firestore

    Type Base (df44d12) Head (66cc9ee) Diff
    browser 247 kB 248 kB +655 B (+0.3%)
    esm2017 193 kB 194 kB +352 B (+0.2%)
    main 488 kB 489 kB +987 B (+0.2%)
    module 245 kB 246 kB +633 B (+0.3%)
  • @firebase/firestore/memory

    Type Base (df44d12) Head (66cc9ee) Diff
    browser 188 kB 189 kB +216 B (+0.1%)
    esm2017 148 kB 148 kB +210 B (+0.1%)
    main 365 kB 365 kB +496 B (+0.1%)
    module 187 kB 187 kB +216 B (+0.1%)
  • firebase

    Type Base (df44d12) Head (66cc9ee) Diff
    firebase-firestore.js 286 kB 287 kB +632 B (+0.2%)
    firebase-firestore.memory.js 229 kB 229 kB +217 B (+0.1%)
    firebase.js 820 kB 820 kB +634 B (+0.1%)

Test Logs


// Polyfill for IE and WebWorker
// eslint-disable-next-line no-restricted-globals
const crypto =
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian May 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might also consider adding support for this shim on ReactNative: https://stackoverflow.com/questions/45301900/howto-patch-shim-crypto-getrandomvalues-for-react-native

While the fallback will make sure these environments don't break, we can possibly extend our support for crypto.

Edit: I missed that you added self && self.crypto. I blame it on the inline Linter warning which you can suppress by moving your no-any line two down :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we don't need it, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am ok with not adding support for ReactNative until the ReactNative Firebase wrapper bundles "crypto", but note that without getting "crypto" from globals, we will always fall back to Math.random() for RN.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I am filing a ticket to get crypto added for react native firestore.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

react-native-firebase participant here, just cross-linking invertase/react-native-firebase#3618

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thought I'd quickly summerize; React Native Firebase doesn't use the JS SDK, we're a thin JS wrapper around the native Android & iOS SDKs so this change has no direct affect on us.

However our JS wrapper does use an identical random id generator - so I think we may need to port this change across in some form - we'll pick this up separately, thanks!

Copy link
Contributor

@dconeybe dconeybe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of drive-by comments.

@@ -43,6 +43,12 @@ export interface Platform {
/** Converts a binary string to a Base64 encoded string. */
btoa(raw: string): string;

/**
* Generates `nBytes` of random bytes. If `nBytes` is negative, an empty array
* will be returned.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the rationale for accepting negative numbers? This seems like it may be hiding bugs in the calling code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it to validate input, and reject anything <=0.

(this.window.crypto || (this.window as any)['msCrypto'])) ||
(self && self.crypto);
const v = new Uint8Array(nBytes);
if (!!crypto) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this double exclamation mark intentional? If not, you may want to add a unit test that would fail if Math.random() is used unintentionally.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can probably just be if(crypto) since an if statement just needs a "truthy" value and not a boolean.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

const crypto =
(this.window &&
(this.window.crypto || (this.window as any)['msCrypto'])) ||
(self && self.crypto);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to https://developer.mozilla.org/en-US/docs/Web/API/Window/self, you can just write:

const crypto = typeof self !=== 'undefined' && (self.crypto || (self as any)['msCrypto']);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

(this.window.crypto || (this.window as any)['msCrypto'])) ||
(self && self.crypto);
const v = new Uint8Array(nBytes);
if (!!crypto) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can probably just be if(crypto) since an if statement just needs a "truthy" value and not a boolean.

autoId += chars.charAt(Math.floor(Math.random() * chars.length));
while (autoId.length < 20) {
const bytes = PlatformSupport.getPlatform().randomBytes(40);
for (const b of Array.from(bytes)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: You can save one copy here if you do a for(let i = 0; i < bytes.length; ++i) loop.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@wu-hui wu-hui assigned schmidt-sebastian and dconeybe and unassigned wu-hui May 5, 2020
@@ -72,4 +74,22 @@ export class BrowserPlatform implements Platform {
btoa(raw: string): string {
return btoa(raw);
}

randomBytes(nBytes: number): Uint8Array {
validatePositiveNumber('randomBytes', 1, nBytes);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a debugAssert since we are not validating a public API call.

You should also allow calls with nBytes: 0 (unless you have to write special code for this).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


// Polyfill for IE and WebWorker
// eslint-disable-next-line no-restricted-globals
const crypto =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am ok with not adding support for ReactNative until the ReactNative Firebase wrapper bundles "crypto", but note that without getting "crypto" from globals, we will always fall back to Math.random() for RN.

@@ -75,4 +77,10 @@ export class NodePlatform implements Platform {
btoa(raw: string): string {
return new Buffer(raw, 'binary').toString('base64');
}

randomBytes(nBytes: number): Uint8Array {
validatePositiveNumber('randomBytes', 1, nBytes);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DebugAssert here as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, make sure that 0 is accepted as a valid value for nBytes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

/**
* Generates `nBytes` of random bytes.
*
* If `nBytes <= 0` , an error will be thrown.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: nBytes==0 is valid; please update this comment to state that it will only reject values that are strictly less than zero.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -72,4 +74,23 @@ export class BrowserPlatform implements Platform {
btoa(raw: string): string {
return btoa(raw);
}

randomBytes(nBytes: number): Uint8Array {
debugAssert(nBytes >= 0, 'Number of random bytes must not be negative');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider including the invalid value in the failure message; this could assist in debugging should the assert ever get triggered and all we have to use for debugging is the stack track and error message.

e.g.

debugAssert(nBytes >= 0, `invalid nBytes: ${nBytes}`);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

randomBytes(nBytes: number): Uint8Array {
debugAssert(nBytes >= 0, 'Number of random bytes must not be negative');

// Polyfill for IE and WebWorker
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you improve this comment to explain how it affects the reader? Is the polyfill being done by the code block that follows? Or is it instructing the reader to perform some polyfilling? Consider re-writing this comment as a sentence that would be either clearly informative or clearly actionable by the reader.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

const crypto =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
typeof self !== 'undefined' && (self.crypto || (self as any)['msCrypto']);
const v = new Uint8Array(nBytes);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Consider a more meaningful name than v (e.g. generatedBytes).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

for (let i = 0; i < bytes.length; ++i) {
// Length of `chars` is 62. We only take bytes between 0 and 62*4-1
// (both inclusive). The value is then evenly mapped to indices of `char`
// via a modulo operation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming this logic is used to ensure an even distribution between all of the characters in chars. Please update the comment to explain why this logic exists because it is non-obvious.

@@ -28,8 +29,17 @@ export class AutoId {
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let autoId = '';
for (let i = 0; i < 20; i++) {
autoId += chars.charAt(Math.floor(Math.random() * chars.length));
while (autoId.length < 20) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider re-writing this loop as while (true) and then breaking out of the loop with a break statement immediately after appending the 20th character to autoId. This would avoid hardcoding 20 in two different places.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewrote the section to reduce hardcoding numbers.

// Length of `chars` is 62. We only take bytes between 0 and 62*4-1
// (both inclusive). The value is then evenly mapped to indices of `char`
// via a modulo operation.
const maxValue = 62 * 4 - 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider moving the calculation of maxValue to immediately follow the declaration of chars. IMO, having it re-calculated on each loop iteration detracts from readability because it incorrectly implies that its value may change between iterations.

// Length of `chars` is 62. We only take bytes between 0 and 62*4-1
// (both inclusive). The value is then evenly mapped to indices of `char`
// via a modulo operation.
const maxValue = 62 * 4 - 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoding 62 in three different places (including in the comment) is a bit unnerving. It would be preferable to use chars.length, for robustness. How about something like this:

const chars = '...';
const maxValue = chars.length * 4 - 1;
debugAssert(maxValue < 256);

Then the hardcoded value of 62 in the modulo operation below can be safely replaced with chars.length.

@@ -75,4 +77,10 @@ export class NodePlatform implements Platform {
btoa(raw: string): string {
return new Buffer(raw, 'binary').toString('base64');
}

randomBytes(nBytes: number): Uint8Array {
validatePositiveNumber('randomBytes', 1, nBytes);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, make sure that 0 is accepted as a valid value for nBytes.

Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM++

@wu-hui wu-hui assigned dconeybe and unassigned wu-hui May 7, 2020
Copy link
Contributor

@dconeybe dconeybe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Thanks for refactoring the logic in newId(); it is now far more readable IMO.

@dconeybe dconeybe assigned wu-hui and unassigned dconeybe and wu-hui May 7, 2020
@wu-hui wu-hui merged commit 631a0ec into master May 7, 2020
@firebase firebase locked and limited conversation to collaborators Jun 7, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants