-
Notifications
You must be signed in to change notification settings - Fork 929
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
Conversation
Binary Size ReportAffected SDKs
Test Logs |
|
||
// Polyfill for IE and WebWorker | ||
// eslint-disable-next-line no-restricted-globals | ||
const crypto = |
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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!
There was a problem hiding this 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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']);
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
packages/firestore/src/util/misc.ts
Outdated
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)) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@@ -72,4 +74,22 @@ export class BrowserPlatform implements Platform { | |||
btoa(raw: string): string { | |||
return btoa(raw); | |||
} | |||
|
|||
randomBytes(nBytes: number): Uint8Array { | |||
validatePositiveNumber('randomBytes', 1, nBytes); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 = |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DebugAssert here as well.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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}`);
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
packages/firestore/src/util/misc.ts
Outdated
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. |
There was a problem hiding this comment.
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.
packages/firestore/src/util/misc.ts
Outdated
@@ -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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
packages/firestore/src/util/misc.ts
Outdated
// 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; |
There was a problem hiding this comment.
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.
packages/firestore/src/util/misc.ts
Outdated
// 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; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM++
There was a problem hiding this 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.
Manually verified on IE11 and web-workers.