expo: Regression in support for react-native-get-random-values

Summary

react-native-get-random-values uses NativeModules.ExpoRandom.getRandomBase64String(byteLength) to polyfill crypto.getRandomValues() in Expo Go:

https://github.com/LinusU/react-native-get-random-values/blob/455f9dbbedad370c094090615b4c88031a09191b/index.js#L26-L34

In converting expo-random to use JSI, we break this polyfill. We should fix this because it is documented in the extremely popular uuid package.

Managed or bare workflow? If you have ios/ or android/ directories in your project, the answer is bare!

managed

What platform(s) does this occur on?

iOS

SDK Version (managed workflow only)

45

Environment

  expo-env-info 1.0.3 environment info:
    System:
      OS: macOS 12.2.1
      Shell: 5.8 - /bin/zsh
    Binaries:
      Node: 16.13.0 - ~/.nvm/versions/node/v16.13.0/bin/node
      Yarn: 1.22.10 - /usr/local/bin/yarn
      npm: 8.1.0 - ~/.nvm/versions/node/v16.13.0/bin/npm
      Watchman: 2022.01.24.00 - /usr/local/bin/watchman
    Managers:
      CocoaPods: 1.11.2 - /usr/local/bin/pod
    SDKs:
      iOS SDK:
        Platforms: DriverKit 21.4, iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 8.5
      Android SDK:
        Android NDK: 21.0.6113669
    IDEs:
      Android Studio: 4.1 AI-201.8743.12.41.7042882
      Xcode: 13.3.1/13E500a - /usr/bin/xcodebuild
    npmPackages:
      expo: ~45.0.0-beta.9 => 45.0.0-beta.9
      react: 17.0.2 => 17.0.2
      react-dom: 17.0.2 => 17.0.2
      react-native: 0.68.1 => 0.68.1
      react-native-web: 0.17.1 => 0.17.1
    npmGlobalPackages:
      eas-cli: 0.50.0
      expo-cli: 5.4.2
    Expo Workflow: managed

Reproducible demo

https://github.com/brentvatne/secure-random-example-45

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 44 (25 by maintainers)

Commits related to this issue

Most upvoted comments

Hello! Could it be that this was re-introduced as a regression in SDK48 on Android?

I’m seeing undefined is not a function pointing to the line:

return global.ExpoModules.ExpoRandom.getRandomBase64String(byteLength..)

coming from getRandomValues -> getRandomBase64

I have tried:

  • nuking node_modules, yarn cache clean
  • upgrading to android sdk 33 in the emulator and clean installing expo
  • installing expo-random and react-native-get-random-values

With no luck. Am I forgetting something? Thank you!

 expo-env-info 1.0.5 environment info:
    System:
      OS: macOS 13.0.1
      Shell: 5.9 - /opt/homebrew/bin/zsh
    Binaries:
      Node: 18.12.1 - ~/Library/Caches/fnm_multishells/1240_1676304359638/bin/node
      Yarn: 1.22.19 - ~/.yarn/bin/yarn
      npm: 8.19.2 - ~/Library/Caches/fnm_multishells/1240_1676304359638/bin/npm
      Watchman: 2023.02.06.00 - /opt/homebrew/bin/watchman
    Managers:
      CocoaPods: 1.11.3 - /opt/homebrew/bin/pod
    SDKs:
      iOS SDK:
        Platforms: DriverKit 22.2, iOS 16.2, macOS 13.1, tvOS 16.1, watchOS 9.1
    IDEs:
      Android Studio: 2022.1 AI-221.6008.13.2211.9477386
      Xcode: 14.2/14C18 - /usr/bin/xcodebuild
    npmPackages:
      expo: ~48.0.0-beta.2 => 48.0.0-beta.2
      react: 18.2.0 => 18.2.0
      react-dom: 18.2.0 => 18.2.0
      react-native: 0.71.2 => 0.71.2
      react-native-web: ~0.18.7 => 0.18.10
    Expo Workflow: managed

As a workaround, I made a polyfill considering that expo-standard-web-crypto depends on expo-random which is now deprecated in Expo SDK v48.

Here is my code based on the expo-standard-web-crypto shim:

// crypto-shim.ts

import { getRandomValues as expoCryptoGetRandomValues } from "expo-crypto";

class Crypto {
  getRandomValues = expoCryptoGetRandomValues;
}

const webCrypto = typeof crypto !== "undefined" ? crypto : new Crypto();


(() => {
  if (typeof crypto === "undefined") {
    Object.defineProperty(window, "crypto", {
      configurable: true,
      enumerable: true,
      get: () => webCrypto
    });
  }
})();

Here’s the current state of this issue for anyone searching for resolutions:

  1. The react-native-get-random-values package has been migrated to using expo-random so it should not error, but it will throw deprecation warnings.
  2. Error: Native module not found issue is unexpected at this point and might indicate you need to update react-native-get-random-values.
  3. The recommended way to get random values in an Expo project is to use install expo-crypto and use the getRandomValues function from it – it closely matches the Web API
  4. If you need a polyfill for a 3rd party package expecting window.crypto.getRandomValues, you can use expo-standard-web-crypto or the crypto-shim.js file posted above. You need to make sure it’s run or imported in the top of the App.ts file.

Note: you can work around this right now by using expo-standard-web-crypto instead of react-native-get-random-values:

expo install expo-random expo-standard-web-crypto
import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View } from "react-native";
import { v4 } from "uuid";
import { polyfillWebCrypto } from "expo-standard-web-crypto";

polyfillWebCrypto();

export default function App() {
  return (
    <View style={styles.container}>
      <Text>Open up App.tsx to start working on your app!</Text>
      <Text>{v4()}</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});

@tsapeta I don’t know if you have read #7209, but there is some more background in there.

Usually when people are using react-native-get-random-values it’s because they are using some package which weren’t made specifically for React Native/Expo, and depend on the global crypto.getRandomValues to be available. This is available in the browsers, Deno, Node.js, etc.

I’ve been trying for more than four years (https://github.com/facebook/react-native/pull/20686, #7209 😅) to get this into React Native or Expo so that all of these packages would just work. And if this is something that is of interest to Expo I would be very happy to submit a PR for this. @brentvatne also took a stab at it here #9716, so maybe you have some input?

Having crypto.getRandomValues just work out-of-the-box without any setup for the user would be such a great benefit to the ecosystem, since Math.random() shouldn’t be used for anything important, and all other javascript environments have moved over to crypto.getRandomValues.

react-native-get-random-values only polyfills if typeof global.crypto.getRandomValues !== 'function', so if e.g. SDK 49 would get support for it there wouldn’t be any conflicts for people already using it.


That being said, the part of the code that maintains different Expo SDK versions is here:

https://github.com/LinusU/react-native-get-random-values/blob/0a48c10a395c19affb9e24044a31e6e56afbf20a/index.js#L30-L34

So maybe we just need to add something new for SDK 48 support? Maybe global.ExpoModules.ExpoCrypto instead of global.ExpoModules.ExpoRandom?

edit: Just saw that you were the last one to edit those lines so you’re probably quite familiar with it 😅 I mostly tried to answer the “Maybe you could use them directly without react-native-get-random-values?” and then spun on

After trying all the above recommendations, nothing worked for my case

The only thing that FINALLY worked is merging the global.crypto with ‘expo-crypto’ in the polyfills

// polyfills.js

import * as crypto from 'expo-crypto';
global.crypto = { ...global.crypto, ...crypto };

@aleqsio My usecase falls under the 3rd category you mentioned:

If you need a polyfill for a 3rd party package expecting window.crypto.getRandomValues, you can use expo-standard-web-crypto or the crypto-shim.js file https://github.com/expo/expo/issues/17270#issuecomment-1445149952. You need to make sure it's run or imported in the top of the App.ts file.

I’m trying to create my own polyfill crypto-shim.js but the issue is that the 3rd party package import always runs before my crypto-shim.js. I’m on Expo SDK 49 using the new app/ based expo-router so I don’t have a App.ts file.

3rd party package:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.crypto = void 0;
console.log("in 3rd party");
exports.crypto = typeof globalThis === 'object' && 'crypto' in globalThis ? globalThis.crypto : undefined;
//# sourceMappingURL=crypto.js.map

The in 3rd party line always gets logged before any of my shims, no matter where I place it. Where can I run my polyfills so that they’re actually polyfilled in time for the imports?

If I edit node_modules/expo-router/entry.js, then it finally polyfills before the import. Is there a way I can insert my polyfills into node_modules/expo-router/entry.js?

// `@expo/metro-runtime` MUST be the first import to ensure Fast Refresh works
// on web.
import "@expo/metro-runtime";

// DO POLYFILL/SHIM CODE HERE
console.log("this will execute before 3rd party imports");

// This file should only import and register the root. No components or exports
// should be added here.
import { renderRootComponent } from "expo-router/src/renderRootComponent";

import { App } from "./_app";

renderRootComponent(App);

EDIT: solved by changing the "main": "index.js", in package.json then doing the polyfill code in there.

index.js:

// POLYFILLS/SHIMS
import {getRandomValues as expoCryptoGetRandomValues} from 'expo-crypto';

// getRandomValues polyfill
class Crypto {
  getRandomValues = expoCryptoGetRandomValues;
}

const webCrypto = typeof crypto !== 'undefined' ? crypto : new Crypto();

(() => {
  if (typeof crypto === 'undefined') {
    Object.defineProperty(window, 'crypto', {
      configurable: true,
      enumerable: true,
      get: () => webCrypto,
    });
  }
})();


import 'expo-router/entry';

I am trying to upgrade to expo 48, but new builds in expo go fail with

 ERROR  Error: crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported, js engine: hermes

We are using uuid and react-native-get-random-values in version 1.8.0, which is the latest version afaik and it worked until now. With expo 48 it does not throw a deprecation, it throws a hard error, so this is not compatible anymore. I tried using expo-standard-web-crypto, but it does not seem to do anything.

import { polyfillWebCrypto } from 'expo-standard-web-crypto'

polyfillWebCrypto()

I tried this in our App.tsx and in our index.js, where react-native-get-random-values was imported, but neither worked. It does not seem to kick in.

For context: We have a mono repo setup with a web app, a node api and the mobile app. We also have a utils package where we are using uuid. So it is not an explicit dependency of the mobile package. As I stated initially, all of this worked just fine with expo 47 and react-native-get-random-values.

To clarify, the regression wasn’t introduced by the migration to JSI, but by making expo-random an Expo module in general (it was a standard React Native module before). The polyfill assumes expo-random is accessible from NativeModules object, but now it should also look for it in global.ExpoModules. I’ll make a PR in react-native-get-random-values to fix this.

Thanks for the PR! This is fixed in the following release of react-native-get-random-values:

🚢 1.8.0 / 2022-05-02

@Michaelsulistio Thank you so much dude, anyone using expo router, you need to define a custom entrypoint and polyfill from there first. Make sure to clear your cache if it just drops you back to the expo start screen.

@Michaelsulistio this is the perfect solution for using expo router! Thanks!

Since the last message here was only a bit more than a month ago, I’m going to take a chance. I’m in Expo version 48, and was switching from using the old uuid + react-native-get-random-values to instead use only the “Crypto” package from expo directly. I only need to generate random UUIDs, so the:

import * as Crypto from 'expo-crypto'
// later
const uuid = Crypto.randomUUID(); // has an error

But I kept getting told

Possible Unhandled Promise Rejection (id: 0):
TypeError: _ExpoCrypto.default.randomUUID is not a function

The only way to get random UUIDs that I can figure out is to replicate @brentvatne’s solution from the first comment:

import { v4 } from 'uuid';
import { polyfillWebCrypto } from 'expo-standard-web-crypto';

polyfillWebCrypto();
// later
const uuid = v4(); // works fine

Have I missed something to get the Crypto package working correctly in Expo 48?

So, is there any path forward? I just tried upgrading to Expo 48 but I am encountering this issue. It’s b/c of an underlying dependency needs the getRandomValues and I can’t get past the Error: Native module not found. I tried the crypto-shim.ts to no avail.

I’m growing tired of the Expo approach of both trying to be the default runtime for react native but also saying “not our problem” when they upgrade and break things that have been a part of the ecosystem for years.

I’m afraid it’s not the thing that the JS engine should do. This is one of the Web APIs, which are implemented by the browser not the JS engine itself as they are not part of the language.

Would it be possible to move specifically global.expo.modules.ExpoCrypto.getRandomBase64String to some kind of Expo core, so that it would be available regardless of wether the expo-crypto module is installed or not?

I think this is exactly the same case as with the JS engine vs the browser. There are many other APIs that need polyfills in React Native, if we add one then we should add the others as well. I would rather be in favor of not bloating the JS runtime and not adding too much into the core if this is not necessary. That being said, polyfilling it automatically when expo-crypto is installed sounds like a good compromise. I’ll add this to our roadmap for SDK 49 😉

Or even better would be to expose a function that fills a typed array directly, instead of going via Base64!

This is exactly what the new getRandomValues does 🙂 Fully native, called through JSI (as every function of a native expo module).

Or maybe even better, register the function from the native code directly on global.crypto.getRandomValues.

Yes, I think we’ll go this way 👍

@peterpme After a quick look, I don’t think it could cause a regression, but we’ll take a look at that.

expo-random is generally deprecated in SDK 48, we merged it into expo-crypto which now has some new cool features, including its own getRandomValues and randomUUID functions. Maybe you could use them directly without react-native-get-random-values?

Not sure what was up with it but yarn cache clean and following CONTRIBUTING.md again seems to have sorted it.

@ramiel1999 - it is a good solution, i’d recommend it