Managing api keys in Android builds

Enrique Ramírez
Aug 22, 2017 · 6 min read

I find very important to differentiate between what belongs to codebase and what to configuration. In back-end development it seems more obvious as it is crucial to define how a service has to be deployed, having instructions that normally contain config files with the environment vars. In mobile app development, it could seem different in the first glance but it is necessary some configuration to determine how a build is gonna be made. In Android we have Gradle system that allows us to easily import libraries distributed using maven, but we can also define how our builds are gonna be made.

Most of our apps require API keys, base urls or other constants that might be specific to each type of build we want to generate. In order to achieve this things, the Android Gradle plugin give you the option of defining buildTypes as well as productFlavors that will help creating the different versions of your app. In these flavours and build types, but also in the default configuration block, we can add these keys/constants to be accessible from within our app or directly by a third party library we use via manifest file. There are basically 3 ways to inject keys or constants in our projects:

  • Create a BuildConfig field. BuildConfig is a generated class that contains a few useful constants like the build version, or if the app is debuggable. By adding buildConfigField “<type>”, “<name>”, “<value>” in one of the places named before, we are telling gradle to add a new field in the BuildConfig class.
  • Create a resource item with ResValue. This will create a resource item that can be accessed if we have a context or from another resource file like a layout. You create one by doing resValue “<type>”, “<name>”, “<value>” same way you would do the previous one.
  • Create a ManifestPlaceholder. You might be using a third party library that checks the manifest for some key and you don’t want to manually type that key in your manifest file, cause as we said before it might depend on you type of build. The way this one works is by writing a placeholder in the manifest file, then assigning a value in your build.gradle script.

This is very well explained in the Android documentation and other articles, but let’s set an example of build script for a simple app (simplified):

In this simple project we have defined the environments we could point to as product flavours, so depending on the build variant we choose it will have a different api url and a third party key for development, staging or production. If you have noticed, we haven’t specified anything for development, so it will fallback to the api url and third party api key we defined in the default config. So far, so good, and under this scheme it will scale up ok to a certain number of environments and keys/constants. Of course it can always be improved, reducing some boilerplate code, but let’s say this is good enough.

The problem comes when we not just only have different environments but we have also different skins or free/paid alternatives. At that point you might think in moving all the environment configuration to build types, but I strongly discourage that as the way I see build type is more about signing, shrinking and being debuggable, but that is only an opinion. In any case there is no need to move thinks as there is something called flavour dimensions that let you create build variants with more than one flavour, sp we could have something like:

Here all our possible build variants are gonna be a combination of a skin flavour and an environment flavour, and we would be resolving the problem if we just have a unique third party api key for each skin but being the same in all environments for each case, i.e. build variant [skin1][production] will have the same key as [skin1][staging] but different from build variant [skin2][production]. I have found many situations where that is not the case and we have a skin with keys that have to be different for each environment. The first solution that might come to your mind is to just create all the possible combinations of skin environment as flavours and get rid of the dimensions, i.e. skin1Devlopment, skin2Development, skin1Staging … . This is definitely a solution but it doesn’t scale very well, every time we want to add a new skin, we have to add 3 product flavours to the productFlavor block. There is a much cleaner way and it is by predefining the keys and then using afterEvaluate to generate the right build config field. Something like:

For each flavour we have set an empty map by doing flavor.ext.set(“thirdPartyApiKey”,[:]). Then for each skin flavour we set the keys we want and then we are injecting the buildConfigField in post evaluation, checking if the value exists and failing back to default value when not defined. Here it is very important the order of the dimensions, by doing flavorDimensions “skin”, “env” we are stating that for each build variant there is gonna be a flavor[0] of the type skin and a flavor[1] of the type env. So what is happening in the afterEvaluate block is that we check if the the skin flavour (flavor[0]) there is a value inside the thirdPartyApiKey map which key is the same as the name of the env flavour (flavor[1]). If we change the order in the flavorDimension, this specific piece of code won’t work as we expect.

We can do the same thing for other api keys and we can try to gentrify the way extract them even choosing whether or not we want to inject a build config field, a res value or a manifest placeholder. This last one might seem a bit problematic but instead of doing:

variant.manifestPlaceHolders[key:value]

we do:

variant.mergedFlavor.manifestPlaceholders[key:value]

I found that when developing a plugin to reduce boilerplate code having the common api keys/constants we use in the company I work for. This solution is the best I’ve found so far for this kind projects that are a bit more complex. Developing a gradle plugin containing similar code of what is shown in the last snippet code but for the keys of the third party services you commonly use, might be quite handy if you work as a freelancer or for an agency building many project with those libraries or services. At Nodes, we built the plugin I’ve mentioned at the beginning of this paragraph, just to make projects neater also adding some flexibility so you could add more custom keys, and being able to chose what to generate: buildConfigFields, resValues and/or ManifestPlaceholders.

Security issues

So far we have assumed that your code is public within the organisation you work for, or team, but private to the rest of the world and there is no risk in exposing those keys/constants, but there are case where that is not true and the keys or constants made to build the app must be kept away from development, or the codebase is open source and it you don’t want to include those keys in the public git project. A simple solution would be to have a file containing the keys loaded as a properties file from gradle. Then is up to the team or organisation to decide where to keep that file, and whether or not having more than one version of the file. By having a limited version of the keys file we could control what kind of build the developers can make and let the CI with access to the full keys file create releases. As a simple example:

Obviously this can be combined with the previous examples and even extract the keys automatically in the on afterEvaluate block we’ve seen before, and following a naming pattern instead of having to assign them manually. That could be material for the next article.

Please don’t hesitate to ask below if you have any question.

)

Enrique Ramírez

Written by

Software developer and music enthusiast.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade