| | |

Secure API keys and Secrets with Gradle build config in Android projects

Secure API keys and Secrets with Gradle build config in Android projects

When building Android apps you often need sensitive values like API keys, client IDs, identity providers. Committing these into your version control or having public visibility risks security concerns and is a recipe for disaster. We’ll walk through a simple, maintainable pattern which could help you.

Since those values are required in runtime while building/testing applications they need to be made available locally for build.

One solution is , for this purpose we can rely on local.properties to have that sensitive information for below reasons :

  1. By default ignored by git it becomes already convenient. (If not, add it to .gitignore)
  2. Available for both your Gradle scripts and IDE since they are in project root.
  3. Requires no additional plugins.
  4. Extends into CI/CD via GitHub Secrets so your tests run uninterrupted.

1. Keeping track of secrets and managing them

The Onboarding Pain Point

Imagine a new teammate clones your repo and builds the project.

gradlew assembleDebug

And only to run into build failures.

They check build files, but can’t find where API_KEY is supposed to come from. This hidden dependency slows everyone down, leads to increases the risk that someone might accidentally commit their real keys just to make things work.

Defining a Clear Contract

To avoid these pitfalls, you need a single, versioned contract that tells everyone exactly which secrets your build requires. That contract should:

  1. Be discoverable for everyone who uses the repository.
  2. Never contain real values, so it’s safe to share publicly.
  3. Human readable, so no one has to guess or reverse engineer to find keys.

Introducing keys.properties.md

At the project root, create a file called keys.properties.md, which will be in version control which documents only placeholders but no values.

# keys.properties.md

> This file lists all the keys you need to define in your `local.properties` file.  
> To avoid committing real `local.properties` into version control we use this as template.

# API key from TMDB
API_KEY=

With these placeholders, others immediately know which keys to supply to the build.

Now once they are aware these, after they get the required keys, they can insert those to their local file local.properties which is not tracked in version control. And it could look like :

sdk.dir=C\:\\Users\\name\\AppData\\Local\\Android\\Sdk
# API key
API_KEY=whateverkeyyouhave

Now we will functions which helps us inject these keys via Gradle.

2. Injecting Secrets with Gradle

At this point, you have keys in your properties file and now we need to access it and allow Gradle to provide it in places required in project.

This function in your build.gradle.kts will allow us to read those keys from properties file.

import java.util.Properties

fun getLocalProperties(): Properties {
    return Properties().apply {
        val file = rootProject.file("local.properties")
        if (file.exists()) {
            file.inputStream().use { load(it) }
        }
    }
}

Below extension function will access those keys from our properties function and emits into BuildConfig file which is a build generated file.

fun ApplicationDefaultConfig.setupBuildConfigFields(
    properties: Properties,
) {
    fun secret(key: String): String = properties.getProperty(key, "")

    buildConfigField(type = "String", name = "API_KEY", value = "\"${secret("API_KEY")}\"")
}

Call this in your android defaultConfig block :

android {
    defaultConfig {
        val properties = getLocalProperties()
        setupBuildConfigFields(properties = properties)
    }
}

After sync you can verify those values will be available in BuildConfig file. You can access it everywhere in project, example :

object TmdbApiKey {
    const val API_KEY = BuildConfig.API_KEY
}

3. Validation for secrets and fail builds

You can take advantage of current Gradle setup and throw errors to Devs when keys are missing or not provided.

In the same function, you can validate keys and warn with failing builds.

fun ApplicationDefaultConfig.setupBuildConfigFields(
    properties: Properties,
) {
    fun secret(key: String): String = properties.getProperty(key, "")

    if (secret("API_KEY").isEmpty()) {
        error("API_KEY not set in local.properties")
    }

    buildConfigField(type = "String", name = "API_KEY", value = "\"${secret("API_KEY")}\"")
}

If the key is missing build will fail with Exception as below :

API_KEY not set in local.properties

* Exception is:
java.lang.IllegalStateException: API_KEY not set in local.properties

4. Wiring Up CI/CD with Secrets

Locally you use local.properties, but your CI runner starts with a clean workspace. Here’s how to ensure builds pass as we will try to provide the API_KEY as an example.

In your repository,

  1. Open settings
  2. Secrets and variables
  3. Actions
  4. Add the secret key attribute and value.

Now we need to provide that same attribute name to receive it’s value in our workflow environment.

jobs:
  build:
    runs-on: ubuntu-latest
    
    env:
      API_KEY: ${{ secrets.API_KEY }}

Failing to do these steps will result your actions to fail since it can’t find the key/value since properties file in only available locally in your project setup.

  • No need to generate a fake properties file on the server.
  • Secrets remain encrypted at rest in GitHub, never revealed in your code.

Conclusion :

With this approach we solve below things :

  • Security risk: Anyone cloning your public repository could extract keys.
  • Rotation pains: If you need to rotate a key, hunting through code is error-prone.
  • CI/CD concerns: Build servers shouldn’t embed real secrets in artifacts.

Reference :

Project with working examples
Workflow file with mentioned setup

Author

Similar Posts

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.