Skip to main content

Protecting secrets in an Android project

Updated:

Would you like to include secrets such as API keys into your Android project, but also want to protect them and keep them safe? We can use features of Gradle, environment variables, obfuscation and encryption, to keep them out of Git and to keep them secure.

When developing apps we will often need to use secret values that we don't want anyone to get access to, such as tokens, IDs and API keys. There are many reasons they may be needed in our source code and in Gradle scripts, the most common being when we are asked to provide one to authenticate with a third-party API.

We will examine a selection of techniques that we can apply, providing protection for our secrets and preventing them from sitting in plaintext, in plain sight!

Why

When following the set up instructions to integrate a new library, we are usually told to put the API key in the AndroidManifest.xml, in the source code or in a Gradle file. These suggestions will result in the secrets being added to source control and to be easily obtainable in plaintext by decompiling our app.

There are more secure ways of managing our secrets and through these tips we can make them significantly harder to obtain. It is worth remembering that our app is published and installed, meaning people will be able to take it apart and try and find secret values within it. All we can do as developers is to apply an appropriate level of security and do our best to keep these secrets safe. When it comes to API keys and tokens, there are also techniques that can be applied on the backend-side to detect fraudulent use and block access using those credentials.

Where to store them

Gradle allows values to be passed in via Gradle properties, these can be passed on the command line or stored in a project-level or user-level properties file. A great way to handle our secrets is to use the user-level file on our filesystem, keeping them out of the project and out of source control. If we were to remove the project from our system and then re-clone it the secrets would still be there and we also have the possibility to include the same secrets into multiple projects without requiring extra set up.

By storing them at a user-level and keeping them out of Git, it means everyone with read-access to the source code doesn't automatically receive the secrets and if our source control system was compromised the attackers wouldn't obtain all of our secrets alongside the source code.

The file is stored within our user directory:

  • On Mac or Linux: /Users/<you>/.gradle/gradle.properties
  • On Windows: C:\Users\<you>\.gradle\gradle.properties

We add a property for each secret to the file, keeping in mind it is user-level and so we will want something to signify which app or project the secret corresponds to.

GameCatalogueApp_UploadKeystore_KeyPassword=aaaabbbbcccc
GameCatalogueApp_AuthClientSecret=123456789
GameCatalogueApp_Pusher_APIKey=ksldjalksdjskald

Using values

Accessing the values within our Android project is as simple as reading them as a Gradle property and using them how we wish. If the value is needed within a Gradle script, such as to pass in a keystore password, it can just be read and used as it is. We are using a handy extension to get the property and return an empty string if it isn't present, to avoid the null value. If our secrets are set up correctly, they won't be missing.

signingConfigs {
  create("upload") {
    storePassword = propertyOrEmpty(
      "GameCatalogueApp_UploadKeystore_KeyPassword"
    )
  }
}

fun Project.propertyOrEmpty(name: String): String {
    val property = findProperty(name) as String?
    return property ?: ""
}

Using the values from our source code requires them to be passed through using either resValue or buildConfigField, depending on whether we want them as an Android resource or as a property on the BuildConfig object. One quirk with buildConfigField is the String containing the property value needs to have quotes within it, in order for BuildConfig to be correctly generated.

defaultConfig {
  buildConfigField(
    "String",
    "AUTH_CLIENT_SECRET",
    buildConfigProperty("GameCatalogueApp_AuthClientSecret")
  )

  resValue(
    "string",
    "pusher_key",
    propertyOrEmpty("GameCatalogueApp_Pusher_APIKey")
  )
}

fun Project.buildConfigProperty(name: String) = "\"${propertyOrEmpty(name)}\""

What about CI

It would be very common for our project to be built on a continuous integration (CI) system or service, such as Bitrise or Jenkins. If this is the case we will need our secrets to be available on CI and passed through to the build environment. A handy trick here is that we can set the secrets as environment variables that use the same names as the Gradle properties. The functions we use to read their values within Gradle can then check for both a Gradle property or an environment variable and use whichever is found.

fun Project.propertyOrEmpty(name: String): String {
    val property = findProperty(name) as String?
    return property ?: environmentVariable(name)
}

fun environmentVariable(name: String) = System.getenv(name) ?: ""

Of course, this means environment variables can also be used locally if we would prefer, however, using Gradle properties is a very simple process.

Encrypt them

Even though our secrets are now usable and kept separate to the source code, it would still be fairly simple to decompile the app and extract our secrets in plain text. We can take our solution further by encrypting the values before they are stored and then decrypting them at runtime.

There are various options for encryption, including built-in options or third-party libraries. One possibility is Themis, which is easy to use and provides strong cryptographic techniques, along with a unified API between Android and iOS. Unless there is a skilled cryptographer working on the project, it may be a good idea to use a higher-level encryption API to avoid mistakes being made which weaken applied security measures.

In order to encrypt our data we will need an encryption key.

GameCatalogueApp_EncryptionKey=super_secret_key

buildConfigField(
  "String",
  "ENCRYPTION_KEY",
  buildConfigProperty("GameCatalogueApp_EncryptionKey")
)

We can provide some obfuscation and protection to our key by applying runtime transformations to it, resulting in the "real" key we will actually use. By doing this an attacker would need to decompile the source code and work out from the obfuscated code which operations were applied to the key.

fun generateKey(): ByteArray {
  val rawKey = buildString(5) {
      append(byteArrayOf(0x12, 0x27, 0x42).base64EncodedString())
      append(500 + 6 / 7 * 89)
      append(BuildConfig.ENCRYPTION_KEY)
      append("pghy^%£aft")
  }
  return rawKey.toByteArray()
}

fun ByteArray.base64EncodedString() = Base64.encodeToString(this, Base64.NO_WRAP)

Here we have only applied some fairly simple operations onto the key to demonstrate the idea, the concept could be taken much further and make the key harder to crack.

Now that we have our encryption key ready to go, we can use a Themis SecureCell to turn our raw String data into an encrypted byte string.

fun encrypt(message: String): ByteArray {
    val encryptionKey = generateKey()
    val cell = SecureCell(encryptionKey, SecureCell.MODE_SEAL)
    val protectedData = cell.protect(
        encryptionContext, message.toByteArray()
    )
    return protectedData.protectedData
}

private val encryptionContext: ByteArray? = null

To store our encrypted secret we can encode the produced ByteArray into a base 64 encoded string. The same key generation code could be added to a script or we could just run the app, encrypt the secret and print out the value for us to copy.

val encypted = EncryptionEngine().encrypt("raw_secret_value")
Log.d("ENCRYPTED", encypted.base64EncodedString())

The Gradle properties or our CI environment variables can now be replaced with encrypted versions.

Decrypt them

At runtime we will first need to convert the base 64 encoded version into an encrypted ByteArray.

val encryptedDaya = Base64.decode(secretAsBase64, Base64.NO_WRAP)

The encrypted ByteArray now needs to be turned into the raw string versions, using the same encryption key as was used for the encryption process earlier. We will need to handle a failed decryption in some way, which would mean something had been set up incorrectly or an invalid value was passed in.

fun decrypt(encryptedData: ByteArray): String? {
    val encryptionKey = generateKey()
    val cell = SecureCell(encryptionKey, SecureCell.MODE_SEAL)
    return try {
        val cellData = SecureCellData(encryptedData, null)
        val decodedData = cell.unprotect(encryptionContext, cellData)
        String(decodedData)
    } catch (error: SecureCellException) {
        Log.e("EncryptionEngine", "Failed to decrypt message, error)
        null
    }
}

Our unencrypted secrets can now be used as they were before. It can be a good practice to decrypt them when they need to be used rather than storing them in an unencrypted state during an app session.

Wrap up

Protecting our API keys and other secrets is a good practice to avoid someone accessing them and causing us harm. This is particularly true if the keys are used for authentication or for accessing our own API that serves up user data. Using various techniques, we have removed the secrets from source control, kept them out of the project, encrypted them and applied protection to our encryption key.

On top of what has been discussed there are further practices that can be applied, some requiring more significant changes. It is best to evaluate the security needs of a particular application, based on what type of data it uses and the level of security a user would expect. Clearly a banking app will need to be much more security concious than a timer app, however, a good-level of security should be applied regardless to protect access to a user's data.

I hope the article was useful. If you have any feedback or questions please feel free to reach out.

Thanks for reading!

Like what you read? Please share the article.

Avatar of Andrew Lord

WRITTEN BY

Andrew Lord

A software developer and tech leader from the UK. Writing articles that focus on all aspects of Android and iOS development using Kotlin and Swift.