React Native

Managing environment variables securely in React Native

Managing the configuration of your mobile application is a critical task that can make or break your project's security. A single mistake can lead to unauthorized access to confidential information, putting your users' data and your reputation at risk.

In this article, we'll explore the various types of configurations typically handled in mobile applications, and the tools available in the React Native ecosystem to handle them securely. We'll also cover the essential security rules that you must follow to protect your sensitive data, and provide examples of common configurations and their security levels.

By the end of this article, you'll have a clear understanding of how to organize and secure the configurations of your mobile app, minimizing your security risks and ensuring the smooth operation of your project.

Config

According to The Twelve-Factor App:

An app’s config is everything that is likely to vary between deploys (staging, production, developer environments, etc)

In a regular mobile app, there are three types of configs:

  1. App config: These settings affect how the project runs and are embedded in the APK/IPA for use at runtime (e.g; your backend URL, some feature flags, an API Key of a SaaS you use).
  2. Build config: These settings affect how the project is built. In an android app, most of them are defined in the ++code>app/build.gradle++/code> through multiple flavors and build types. In iOS, they are defined through multiple targets and Info.plist files. Most of the time, we’ll have secrets related to the digital signature of our artifacts. Another use case could be some credentials to access a private maven repository in Android.
  3. Deployment config: These settings affect how the project is deployed. These settings will be used in your fastlane scripts and combined with actions that deploy your artifacts to stores like upload_to_testflight, upload_to_play_store, appcenter_upload, and more.

One essential aspect of configurations is secrets. Secrets are sensitive pieces of information, such as API keys, passwords, or access tokens, that should be kept hidden from anyone who doesn’t have permission to access them. Secrets are typically included in the configs and must be handled securely to prevent unauthorized access.

In the following sections, we'll explore the different tools and best practices available in the React Native ecosystem to handle configurations securely, including how to protect secrets depending on the configuration type.

App Config

Managing Configurations on the JavaScript Side

To manage configurations on the JavaScript side, we can create a ++code>config++/code> object at the root of our project, as shown below:

However, hardcoding secrets directly in JavaScript code is not a secure practice. Once we build our application, React Native uses Metro to bundle our JavaScript/TypeScript files into a single JavaScript file. This JS bundle is included with the rest of our application in the APK.

To demonstrate how easy it is to retrieve secrets from the bundled JavaScript code, we can use the ++code>apktool++/code>  (++code>brew install apktool++/code>) to unzip the APK and find the bundled file.

To unzip an APK, run the following command:

Once the APK is unzipped, we can open the ++code>assets/index.android.bundle++/code> file and search for our ++code>config++/code> object. As shown in the screenshot below, the object can be easily located.

To prevent unauthorized access to our secrets, we must follow the first following rule ⬇️

Rule No. 1: Never hardcode secrets in your JavaScript code.

The JavaScript bundle created by React Native is consumed by a JavaScript engine. The default engine used in React Native is JavascriptCore, but a new engine, Hermes, has been available for a few years. In our tech radar, we recommend using Hermes as it offers improved start-up time, decreased memory usage, and smaller app size compared to JavascriptCore.

To leverage the benefits of Hermes, we no longer store our code in Javascript format in the APK. Instead, we pre-compile it into bytecode at build time, resulting in a binary format ++code>index.android.bundle++/code> file, as shown in the screenshot below.

Have we found a solution to our problem? Even though the binary format provides some level of security, it's not a foolproof solution. Reverse engineering tools like hermes-dec can decompile the compiled files with Hermes VM bytecode (HBC) format, as shown in the screenshot below.

Therefore, it's still not secure to hardcode secrets in the JavaScript code.

Rule No. 1 bis: Please, really, never hardcode secrets in your JS.

Managing Configurations on the Native Side

At BAM, we extensively use react-native-config to handle our configs in bare workflows. react-native-config reads configs from dotenv files, injects them at build time on the native side, and makes them available on the JavaScript side through a bridge at runtime, as shown in the diagram below.

On the native side, you can access your variables thanks to the resources API or the ++code>BuildConfig++/code> class. On the JS side, you can find them in the exported object from the react-native-config module, but you won’t be able to find them by analyzing your JavaScript bundle. Can you manipulate secrets with react-native-config though? Let’s analyze an APK 🔎

  1. Open android studio
  2. Go To build > Analyze APK and select your APK
  3. Open ++code>resources.arsc++/code> file. It’s a binary file that contains the compiled resources of your app
  4. Select the ++code>string++/code> section
  1. Get access to all your variables 😰 And it shouldn’t be a surprise if you read the documentation 😉
⭐  Rule No. 2: Never put secrets in your app config.

As a best practice, sensitive information like secrets should never be used on the front-end. If you need to use a secret like an API key, you should consider implementing the following countermeasures:

  • Move the usage of the secret to your back-end. An attacker won't have access to the runtime of your back-end.
  • If it is mandatory to use the key on the front-end, add a usage limit so that an attacker cannot:

✔️ Increase your bill at the end of the month.

✔️ Delete your SaaS access by using SaaS in a way that violates the terms of the contract.

Best Practices for Handling Multiple Environments

In a React Native app, when you need to support multiple environments, it is important to consider how you manage your configurations. One approach is to use JavaScript objects that you can dynamically import at runtime.

However, bundling all your configurations, including your staging configurations, into your final bundle can pose a security risk. Your staging configuration may contain the URL of your staging backend, which is often less secure than production backends. Staging backends may also provide endpoints for accessing API documentation, work-in-progress endpoints, and other resources. By leaking access to this information, you increase your attack surface and make it easier for an attacker.

To reduce the amount of knowledge bundled in the app, it is recommended to use environment variables to inject your configuration at build time. This way, you can ensure that only one app configuration is included in your final artifact.

It is important to remember to protect your staging backend as if it were your production one. Implement an authentication mechanism and control who can access it through IP address whitelisting. This way, you can ensure that only trusted users can access your staging backend, reducing the risk of a security breach.

Rule No. 3: When you have multiple app configurations for multiple environments, inject only one app configuration in your final artifact.

Build Config & Deployment Config

When developing a mobile application, managing build and deployment configurations is crucial to ensure that your application can be built and deployed properly. In this section, we will examine multiple strategies to handle them.

1st solution: Hardcode configurations in the git repository

One approach is to hardcode the configurations directly into the application source code or Fastlane scripts. However, this method has significant drawbacks.

  • If your git repository is public, anyone can build and deploy your application, which is a major security risk.
  • Even if the repository is private, there's no control over who has access to these variables, so any developer can use them independently. If you decide to make your git repo public, you'll need to modify your entire git history to remove the values of these secrets.
💬 Rule No. 1 (final form): We do not store secrets in plain text in a Git repository.

2nd solution: Use encrypted dotenv files

A second option is to store the configurations in dotenv files, which can be encrypted using technologies such as Transcrypt. This method is better than hardcoding secrets because it allows you to store them in your git repository while protecting them with a password. However, sharing the password with all developers working on the project is still a security risk. Also, there is no fine-grained control over who has access to which variable and a mistake can lead to secrets being leaked in plain text in the git repo.

3rd solution: Use variables stored in the CI

A third option is to store the configurations in the variables of your continuous integration (CI) system. This method offers better security for secrets because they are not stored in the git repo or on developers' machines. However, if your CI system is unavailable, local deployment becomes problematic, and secrets are only accessible to users with specific rights. Moreover, if you have build secrets, you need to find another way to distribute them to your developers. Finally, there are potential security vulnerabilities, such as the recent data leak incident at CircleCI, that can expose your secrets.

4th solution: Use a secret manager

A fourth and recommended solution is to store sensitive configuration variables in a secret management service like AWS Secrets Manager or Google Cloud Secret Manager. These services enable secure and scalable storage of secrets and integrate well with many cloud-based environments.

The advantages of this solution are numerous. First, it offers finer control over who has access to which secret, enabling the separation of build and deployment secrets. Second, it is likely more secure than previous solutions since it allows secrets to be stored centrally and protected by strict security policies.

However, this solution can be more expensive to implement since it requires a third-party service. Additionally, it can be more complex to configure and manage, especially when managing secrets for multiple environments or applications.

In conclusion, the solution you choose depends on your specific context. We recommend using a secret manager, as it's now easier than ever to integrate them into our CI/CD workflows, and they offer superior security and control over secrets.

Analysis of variables commonly used for a RN project

To summarize what we have learned so far, let's analyze common configurations we always encounter in a React Native project.

App configuration

Name Description Usage Secret? Type
ENV Get access to the environment in which the app is running. The name of the app. App config
APP_NAME The name of the app. On the native side to configure the name of our app below the icon. App config
APP_ID The id of the app. On the native side to configure the id of our app. App config
VERSION The public key used to check the signature. On the native side to configure the id of our app. App config
BUILD_NUMBER The build number of the app. For more info, read this article. On the native side to set up the build number of our app. App config
API_BASE_URL The base URL of our backend. On the JS side to configure our HTTP client. App config

All these variables can be declared in a dotenv file and injected in the native & JS runtimes with react-native-config.

CodePush

When setting up Over the air updates with CodePush, several configurations come into play at different stages of the flow:

  1. Bundle your JS code to obtain the JS bundle that will be uploaded to App Center.
  2. Sign the bundle: Code signing ensures that only JS bundles created and signed by a person with the private key will be installed in our application.
  3. Upload your bundle to AppCenter by authenticating yourself with an API token.
  4. At runtime, check if there is an update available for a given deployment identified by a deployment key.
  5. Validate the signature using the public key associated with the private key previously used, which is embedded in the artifact. (See Code signing setup for more information.)
Name Description Usage Secret? Type
CODEPUSH_ DEPLOYMENT_KEY It identifies which deployment the CodePush SDK needs to query to know if updates are available. - On iOS, it’s defined in the ++code>Info.plist++/code> file, in an entry named ++code>CodePushDeploymentKey++/code>.
- On Android, it’s defined in the ++code>app/build.gradle++/code> file, we define a new res value ++code>CodePushDeploymentKey++/code>.
❌, it’s available in the artifact. App config
CODEPUSH_ API_TOKEN It authenticates the user on the AppCenter API. Injected on the CI during the deployment phase to upload a JS Bundle to App Center. ✅, if an attacker has access to this token, he can upload a corrupted JS bundle on App Center that can be redistributed to our end users depending on our configs. Deployment config
CODEPUSH_CODE_ SIGNING_PRIVATE_KEY It’s used to sign our JS bundle before uploading them on CodePush. Once a device downloads. Injected on the CI during the build phase of a CodePush release. The JS bundle will next be signed with it. ✅ , if an attacker has access to this key, he can bypass the code signing security. Therefore, the security of this key is essential to ensure the authenticity and integrity of our JS bundle. Build config
CODEPUSH_CODE_ SIGNING_PUBLIC_KEY The public key used to check the signature. - On iOS, it’s defined in the ++code>Info.plist++/code> file, in an entry named ++code>CodePushPublicKey++/code>.
- On android, it’s defined in a string resource, in an entry name ++code>CodePushPublicKey++/code>.
❌, it’s available in the artifact. App config

Android build & deployment

When setting up a CD to release an android app on the play store, several configurations come into play at different stages of the flow:

  1. Build your app (APK/AAB format) with gradle.
  2. Sign your app. It’s an important step to ensure its integrity and authenticity. We need to create signing configs and use keystores.
  3. Upload your artifact to the Play Console.
Name Description Usage Secret? Type
ANDROID_KEYSTORE_PASS Keystore password required to access the content of the keystore (signing keys). Used in our ++code>app/build.gradle++/code> file to configure our signing config. ✅, it gives access to the content of the keystore. Build config
ANDROID_KEYSTORE_ALIAS Alias of the key stored in the keystore used to sign the APK. Injected on the CI during the deployment phase to upload a JS Bundle to App Center. Build config
ANDROID_KEYSTORE_ALIAS_PASS Password required to access the key stored in the keystore used to sign the APK. Used in our ++code>app/build.gradle++/code> file to configure our signing config. ✅, it gives access to the key used to sign the app. Build config
PLAY_STORE_JSON_KEY A key that identifies the dev team on play store APIs. Injected on the CI during the deployment phase to upload the generated artifact on the play console ✅ If an attacker has access to this key, he can upload a corrupted AAB on Play Console. It can then be redistributed to our end users depending on our configs. Deployment config

I did not follow these guidelines on my project, and some secrets have leaked. What should I do?

Conclusion

In a mobile application, it is important to consider the security of your application, especially at runtime. To prevent reverse-engineering of the application and ensure its secrecy, it is recommended to minimize the configurations injected at runtime and migrate the use of secrets to the backend or the CI when possible. One way to manage your app config is to use libraries like react-native-config, which make it easier to manage and control your configurations.

When it comes to build and deployment configurations, it is important to store your secrets on secret managers to manage their access. By doing so, you can ensure that only authorized developers have access to these secrets and that they are managed securely. Give your developers access to the necessary secrets to build the application so that they can build them locally. Additionally, you should configure your CI to retrieve the secrets needed for build and deployment. This will help you avoid introducing secrets into your codebase or, worse, into artifacts distributed to your users.

By following these guidelines, you can ensure that your application is secure and that your users' data are protected. It is important to prioritize security in your development process to prevent security breaches and ensure the success of your application.

Développeur mobile ?

Rejoins nos équipes