Skip to content

Storing Secret Keys in Android

Mitchell Bryson edited this page Aug 5, 2026 · 39 revisions

Overview

Often your app will have secret credentials or API keys that you need to have in your app to function but you'd rather not have easily extracted from your app.

Before choosing a strategy, understand the ground rule: no key shipped inside your app is safe. Anything embedded in the APK — a BuildConfig field, a string resource, even a value obfuscated in native code — can be recovered by decompiling the app. For anything truly sensitive, keep the secret on a backend server you control and have the app call your backend instead; that is the only approach that keeps the key out of the shipped binary entirely. See this StackOverflow post for a detailed breakdown of why client-side hiding schemes only raise the reverse-engineering effort.

With that in mind, there are three distinct problems, each with a different tool:

You should not store secrets in shared preferences without encrypting this data first because they can be extracted when performing a backup of your data.

The alternative is to disable backups by setting android:allowBackup to false in your AndroidManifest.xml file:

<application ...
    android:allowBackup="false">
</application>

You can also specify which files to exclude from backups by reviewing this doc.

Keeping Keys Out of Version Control with the Secrets Gradle Plugin

Google's Secrets Gradle Plugin for Android is the current first-party tooling for keeping fixed keys out of your repository. It reads keys from a properties file that stays on your machine — by default local.properties, which Android Studio projects already exclude from Git — and exposes each entry to your code as a BuildConfig field and to AndroidManifest.xml as a manifest placeholder. The project README is explicit about the limits of this approach: "This plugin is primarily for hiding your keys from version control. Since your key is part of the static binary, your API keys are still recoverable by decompiling an APK."

Register the plugin in your root-level build.gradle:

buildscript {
    dependencies {
        classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
    }
}

Apply it in your app-level app/build.gradle:

plugins {
    id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}

Then add your key to local.properties:

apiKey=XXXXXXX

After a Gradle sync, the value is available in code as BuildConfig.apiKey and in the manifest as ${apiKey}. The plugin supports a checked-in defaults file and per-file overrides via its secrets { } configuration block — see the README for the options. (On AGP 8.0+, BuildConfig generation is opt-in; see the note in the next section.)

The sections below show how to accomplish the same goal by hand, which is useful to understand what the plugin automates — and Google's build documentation recommends the same properties-file pattern for keeping signing credentials out of build files.

Hidden in BuildConfigs

First, create a file apikey.properties in your root directory with the values for different secret keys:

CONSUMER_KEY="XXXXXXXXXXX"
CONSUMER_SECRET="XXXXXXX"

Double quotes are required.

To avoid these keys showing up in your repository, make sure to exclude the file from being checked in by adding to your .gitignore file:

apikey.properties

Next, add this section to read from this file in your app/build.gradle file. You'll also create compile-time options that will be generated from this file by using the buildConfigField definition:

def apikeyPropertiesFile = rootProject.file("apikey.properties")
def apikeyProperties = new Properties()
apikeyProperties.load(new FileInputStream(apikeyPropertiesFile))
 
android {

  // Required when using AGP 8.0+, which no longer generates BuildConfig by default
  buildFeatures {
    buildConfig = true
  }

  defaultConfig {
     
    // should correspond to key/value pairs inside the file   
    buildConfigField("String", "CONSUMER_KEY", apikeyProperties['CONSUMER_KEY'])
    buildConfigField("String", "CONSUMER_SECRET", apikeyProperties['CONSUMER_SECRET'])
  }
}

Note: Starting with Android Gradle Plugin 8.0, the BuildConfig class is no longer generated by default. You must opt in by adding the buildFeatures.buildConfig flag inside the module's android { ... } block. The Groovy DSL snippet above (buildConfig = true inside buildFeatures { ... }) is identical in the Kotlin DSL — both use the assignment form:

// app/build.gradle.kts
android {
    buildFeatures {
        buildConfig = true
    }
}

Alternatively, opt in for the whole project by setting android.defaults.buildfeatures.buildconfig=true in gradle.properties. See the AGP 8.0 release notes for details.

You can now access these two fields anywhere within your source code with the BuildConfig object provided by Gradle:

// inside of any of your application's code
String consumerKey = BuildConfig.CONSUMER_KEY;
String consumerSecret = BuildConfig.CONSUMER_SECRET;

Now you have access to as many secret values as you need within your app, but will avoid checking in the actual values into your git repository. To read more about this approach, check out this article or this other article.

Secrets in Resource Files

Start by creating a resource file for your secrets called res/values/secrets.xml with a string pair per secret value:

<!-- Inside of `res/values/secrets.xml` -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="parse_application_id">xxxxxx</string>
    <string name="google_maps_api_key">zzzzzz</string>
</resources>

Once these keys are in the file, Android will automatically merge it into your resources, where you can access them exactly as you would your normal strings. You can access the secret values in your Java code with:

// inside of an Activity, `getString` is called directly
String secretValue = getString(R.string.parse_application_id);
// inside of another class (requires a context object to exist)
String secretValue = context.getString(R.string.parse_application_id);

If you need your keys in another XML file such as in AndroidManifest.xml, you can just use the XML notation for accessing string resources:

@string/google_maps_api_key

Since your secrets are now in an individual file, they're simple to ignore in your source control system (for example, in Git, you would add this to the '.gitignore' file in your repository) by appending the pattern on the command line within your project git repository:

echo "**/*/res/values/secrets.xml" >> .gitignore

Verification: To make sure this worked, check the .gitignore file within your git repository, and make sure that this line referencing secrets.xml exists. Now, go to commit files to Git and make sure that you do not see the secrets.xml file in the staging area. You do not want to commit this file to Git.

This process is not bulletproof. As resources, they are somewhat more vulnerable to decompilation of your application package, and so they are discoverable if somebody really wants to know them. This solution does, however, prevent your secrets just sitting in plaintext in source control waiting for someone to use, and also has the advantage of being simple to use, leveraging Android's resource management system, and requiring no extra libraries.

As with the plugin and BuildConfig approaches, none of these strategies will ensure the protection of your keys — they keep secrets out of source control, not out of the APK. Per the overview above, anything truly sensitive belongs on your own backend.

Secrets in native libraries with NDK

Another way to make your keys harder (not impossible) to reverse engineer is to compile them into a native library with the NDK — an obfuscation tactic, not a security boundary. One implementation of this idea was the hidden-secrets-gradle-plugin (note: its repository was archived in March 2024 and is no longer maintained, so treat it as a reference for the technique rather than a dependency to adopt):

  • secret is obfuscated using the reversible XOR operator so it never appears in plain sight,
  • obfuscated secret is stored in a NDK binary as an hexadecimal array, so it is really hard to spot / put together from a disassembly,
  • the obfuscating string is not persisted in the binary to force runtime evaluation (ie : prevent the compiler from disclosing the secret by optimizing the de-obfuscation logic),
  • optionally, anyone can provide its own encoding / decoding algorithm when using the plugin to add an additional security layer.

Using the Android Keystore API

For dynamically generated secrets, the Android Keystore system is the recommended at-rest store. Key material is held in a system process — never in your app's address space — and on devices with a Trusted Execution Environment or StrongBox Secure Element the key is bound to secure hardware, so it cannot be extracted even on a rooted device. See the Android Keystore system training article for an overview.

The AndroidKeyStore provider has supported AES symmetric keys directly since API level 23 (Android 6.0), so there is no need to layer an RSA-wrapped AES scheme on top for app-local secret storage. Generate an AES-256 key with KeyGenParameterSpec and use it with AES/GCM/NoPadding for authenticated encryption — the algorithm pair the cryptography guide currently recommends for symmetric ciphers.

import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec

private const val PROVIDER = "AndroidKeyStore"
private const val TRANSFORMATION = "AES/GCM/NoPadding"
private const val GCM_IV_LENGTH = 12
private const val GCM_TAG_BITS = 128

private fun getOrCreateKey(alias: String): SecretKey {
    val keyStore = KeyStore.getInstance(PROVIDER).apply { load(null) }
    (keyStore.getKey(alias, null) as? SecretKey)?.let { return it }

    val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, PROVIDER)
    generator.init(
        KeyGenParameterSpec.Builder(
            alias,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
        )
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setKeySize(256)
            .build(),
    )
    return generator.generateKey()
}

fun encrypt(alias: String, plaintext: ByteArray): ByteArray {
    val cipher = Cipher.getInstance(TRANSFORMATION)
    cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey(alias))
    // Persist the IV alongside the ciphertext so decrypt() can recover it.
    return cipher.iv + cipher.doFinal(plaintext)
}

fun decrypt(alias: String, blob: ByteArray): ByteArray {
    val iv = blob.copyOfRange(0, GCM_IV_LENGTH)
    val ciphertext = blob.copyOfRange(GCM_IV_LENGTH, blob.size)
    val cipher = Cipher.getInstance(TRANSFORMATION)
    cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(alias), GCMParameterSpec(GCM_TAG_BITS, iv))
    return cipher.doFinal(ciphertext)
}

GCM generates a fresh 12-byte IV per Cipher.init(ENCRYPT_MODE, ...) call, so prepend it to the ciphertext on write and split it back off on read — never reuse an IV with the same key. Store the resulting blob in SharedPreferences, a file, or your database; the value at rest is opaque without access to the keystore.

About androidx.security:security-crypto. This Jetpack wrapper around the Keystore (EncryptedSharedPreferences, EncryptedFile, MasterKey) was the recommended replacement for hand-rolled Keystore code from 2020 through 2025, but all of its APIs were deprecated in version 1.1.0 (stable on 2025-07-30) with the release note "Deprecated all APIs in favour of existing platform APIs and direct use of Android Keystore." The cryptography overview confirms there will be no subsequent release of the library. New code should target the platform AndroidKeyStore APIs shown above; existing EncryptedSharedPreferences callers will continue to work but should plan a migration.

Resources

Finding these guides helpful?

We need help from the broader community to improve these guides, add new topics and keep the topics up-to-date. See our contribution guidelines here and our topic issues list for great ways to help out.

Check these same guides through our standalone viewer for a better browsing experience and an improved search. Follow us on twitter @codepath for access to more useful Android development resources.

Clone this wiki locally