There’s no way to be 100% secure in software development because software is developed by humans, and humans tend to make mistakes. Malicious actors exploit those mistakes to gain certain advantages. So, if you want to keep secrets safe, the goal is to eliminate the potential threats as much as possible.
This article explains how to keep secrets safe in Android projects.
What Are Application Secrets?
Some of the secrets in Android projects are: API keys, KeyStore credentials, licenses for the 3rd party SDK, etc.
Usually, secrets, like credentials, are used inside the build.gradle file because they are environment-specific features. In case we have staging and production flavors, here is what it could look like:
productFlavors {
staging {
…
buildConfigField STRING, API_KEY, ‘aRwAAAW51bS…’
…
}
production {
...
buildConfigField STRING, API_KEY, ‘aRwCCCdE23S...’
...
}
}
We want to remove secrets from the build to improve application security.gradle file and our repository to prevent unauthorized access.
Removing Secrets from the build.gradle File and Repository
The first step in hiding secrets would be to create a separate file named something like secrets.properties, and fill it with all the existing credentials and licenses.
Next, we should update the build.gradle file by removing the old values and adding values from the file.
Here is what our secrets.properties file would look:
apiKey.staging=aRwAAAW51bS...
apiKey.production=‘aRwCCCdE23S...
and how the build.gradle file would look after changes:
Properties secretProperties = new Properties()
secretProperties.load(new FileInputStream(file('../secrets.properties')))
...
android {
...
productFlavors {
staging {
...
buildConfigField STRING, API_KEY, secretProperties['apiKey.staging']
...
}
production {
...
buildConfigField STRING, API_KEY, secretProperties['apiKey.production']
...
}
}
}
To avoid pushing secrets to the repository, we should change the .gitignore file to add the secrets.properties file to ignore the list.
Now, the person who performs reverse engineering to access this data will neither be able to find it within the APK file nor will it be available in the repository.
Secrets and CI
When running the build on CI, we can’t use the secrets.properties file to inject secrets because it is not pushed to the repository. To support it, we should also add secrets to CI. Here’s an example when using Bitrise CI/CD Platform: in the Bitrise dashboard, we should add secrets in Project -> Workflow -> Secrets. In our case, we’ll add the API_KEY_STAGING and API_KEY_PRODUCTION secrets. After that, the adjusted build.gradle file looks like this:
productFlavors {
staging {
...
buildConfigField STRING, API_KEY, System.getenv("API_KEY_STAGING") ?: secretProperties['apiKey.staging']
...
}
production {
...
buildConfigField STRING, API_KEY, System.getenv("API_KEY_PRODUCTION") ?: secretProperties['apiKey.production]
...
}
}
Now, if we run the build on CI, it will receive secrets from Bitrise Secrets, and if we run the app locally, it will get secrets from the secrets.properties file.
Sharing Secrets Between Team Members with Vault
When a new member joins the team, you need to share secrets with them. Also, if anyone deletes the project files (computer reinstall, for example), they would need to get secrets from somewhere. You can use a password manager to share secrets, but whoever joins the project must add a new file and copy-paste all the credentials.
Using Vault is a better way to share secrets. This tool lets you have secrets for all your projects in one place and has a command-line tool to integrate it with your continuous integration script. That way, you don’t expose secrets on your CI as they will be fetched only when you need them.
Any colleague who joins the project can access Vault and fetch one or more secrets, as requested.
Using Github Actions
Github Actions Secrets are encrypted environment variables. Anyone with collaborator access to this repository can use these secrets for Actions.
Secrets are not passed to workflows triggered by a pull request from a fork; they can be stored at the organization or environment levels.
For secrets stored at the environment level, you can enable the required reviewers to control access to the secrets. A workflow job can’t access the environment secrets until the required approvers grant approval.
To add a new secret to GitHub, go to the repository Settings, select Secrets, then choose New Secret. Now, you can add a new secret, with a name & value.
After that, load your secrets into a new secrets.properties file inside the build.yml file. Then, load secrets.properties in build.gradle and use it, just like we did in the Bitrise case.
Conclusion
As I mentioned at the beginning, we should always reduce the security risk to a minimum. When storing secrets in an Android project, you should adapt the solution to the given context because there is no silver bullet for every situation. The final goal should be minimizing the number of people who can lay their hands on your secrets altogether.
About Author
Zoran Stanimirović – an Android developer with more than eight years of experience, possessing vast knowledge in the field of bank applications. His special interests are mobile architecture and Kotlin MultiPlatform.
Resources
- Gist for this post: https://infinum.com/blog/secrets-android-projects/
- Bitrise secrets: https://devcenter.bitrise.io/en/code-signing/android-code-signing/android-code-signing-in-gradle.html
- Github Actions Secrets: https://docs.github.com/en/actions/security-guides/encrypted-secrets
- Github Actions in action: https://blog.jakelee.co.uk/accessing-android-app-secret-from-github-actions-using-gradle/