Saturday, September 5, 2015

Android Dev and API Keys - Keep It Secret, Keep It Safe

The Problem

You have an external API you must call which requires an API key.  This key must not be checked into source control.  Maybe you have other values which are different depending what environment you're in. Either way, you don't want these hard-coded into your source code.

The Solution

Disclaimer: The following solution was synthesized from a couple of disparate StackOverflow answers which I am now unable to find.

1. Create a properties file in which to store your sensitive security/environment values.  Properties in this file follow the standard format of key=value.  Lines may be commented out by prefixing them with the "#" symbol.  You'll want to create a properties file for each environment you'll be working in (e.g. debug, release, etc).

IMPORTANT: Add each of these files to your .gitignore file (or your version control equivalent).

Example properties file:
key.password=somepassword
#key.alias=somekeyalias This line is commented out
store.file=/home/jim/keystores/mykeystore.store
api.key=myapikey
store.password=mystorepassword

2. Update your build.gradle file pull the values out of your property file for each environment.

In app/build.gradle:
android {
    //Lots of other configuration stuff not shown here.
    buildTypes {
        debug {
            Properties properties = new Properties()
            properties.load(project.rootProject.file('local.properties').newDataInputStream())
            def apiKey = properties.getProperty('api.key')
            resValue "string", "api_key", apiKey
            // other debug config stuff not shown here            
        }
        release {
            Properties properties = new Properties()
            properties.load(project.rootProject.file('release.properties').newDataInputStream())
            def apiKey = properties.getProperty('api.key')
            resValue "string", "api_key", apiKey            
            // other release config stuff not shown here
        }
    }
}

Notice the lines where we are calling 'resValue "string, "api_key", apiKey'? This is essentially telling the build to replace the resource value "api_key" anywhere it is used with the value in the variable apiKey.

3. Add a string value to your res/values/strings.xml file to refer to your value.
In res/values/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>    
    <string name="api_key_string">@string/api_key</string>
    <!-- Other values not shown here -->
</resources>

4. Update your AndroidManifest.xml file to make your values available to the application via meta-data.

In app/src/main/AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.yourpackage">
    <!-- Other configuration not shown here -->
    <application
        android:name=".some.applicationName"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
            <!-- Other configuration not shown here -->
        <meta-data android:name="api-key"
            tools:replace="android:value"
            android:value="@string/api_key_string"></meta-data>
            <!-- Other configuration not shown here -->
    </application>
</manifest>

Notice that on our <meta-data> tag we have defined an android:name attribute with the value "api-key", and we are using tools:replace="android:value".  This allows us to point to the value being held in our strings.xml file, referred to as "@string/api_key_string".

5. Finally, we can now refer to our value in the production code by accessing it via a Bundle.

Production Code:

        //You'll need to get a hold of your ApplicationContext for this step.  
        String apiKey;
        try {
            ApplicationInfo applicationInfo = yourApplicationContext.getPackageManager()
                    .getApplicationInfo(yourApplicationContext.getPackageName(),
                            PackageManager.GET_META_DATA);
            Bundle bundle = applicationInfo.metaData;
            apiKey = bundle.getString("api-key");
            Log.d(TAG, "api key: " + apiKey);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();  //Do something more useful here!
        }

That's it! By having your production code access this value via Bundle you've effectively abstracted away any environment-specific concerns; neither do you need to hard-code values anywhere in your source tree. This allows you to safely develop code which is portable to any environment, and frees you from worrying about someone getting access to your security-sensitive values simply by checking out your code.

No comments:

Post a Comment