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.
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:
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.
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.
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 🔎
⭐ 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:
✔️ 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.
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.
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.
One approach is to hardcode the configurations directly into the application source code or Fastlane scripts. However, this method has significant drawbacks.
💬 Rule No. 1 (final form): We do not store secrets in plain text in a Git repository.
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.
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.
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.
To summarize what we have learned so far, let's analyze common configurations we always encounter in a React Native project.
All these variables can be declared in a dotenv file and injected in the native & JS runtimes with react-native-config.
When setting up Over the air updates with CodePush, several configurations come into play at different stages of the flow:
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:
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.