On the security of Google Secrets

@cryptax
5 min readJul 11, 2024

--

Google Secrets Gradle plugin is “for providing your secrets securely to your Android project”. I would like to make it clear in this article that it does not make your secrets safe to reverse engineering and that they remain very easy to recover. The intent is only to deport the secrets in a file that you do not commit in your versioning system. If this is clear to you, skip to the last section “how can I keep my secrets confidential”.

The disclaimer on the Google Secrets GitHub page is explicit:

DISCLAIMER: 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. So, securing your key using other measures like adding restrictions (if possible) are recommended.

However, titles such as “How to Hide API and Secret Keys in Android Studio”, or “Hide your API keys on Android” can mislead developers and make them think this is sort of a secure storage facility. Don’t misunderstand me: I am not saying those links are wrong/bad, just that someone who reads them quickly will probably think Google Secrets is more than it is really.

Testing Google Secrets

I tested Google Secrets in a simple Android application. The secrets are stored in an external file, e.g secrets.properties, which should not be committed to git. That’s the whole and unique purpose of Google Secrets. The filename is configurable in your module build gradle. Follow this link to setup your Android project, and this link for a working example.

secrets {
// Optionally specify a different file name containing your secrets.
// The plugin defaults to "local.properties"
propertiesFileName = "secrets.properties"
...
}

There are two way to access the secrets: via Manifest meta-data, or via BuildConfig.

Access through Manifest Meta Data

If you are using Manifest meta-data, you should add an entry for each secret you want to access. In the example below, MANIFEST_KEY is defined in my secrets.properties, and will be accessible through the Android Manifest meta-data manifestKey.

<application>
...
<meta-data
android:name = "manifestKey"
android:value = "${MANIFEST_KEY}"/>
</application>

Then, you access the secret by getting application info’s metadata (getApplicationInfo) and getting the specific name (getString).

try {
ApplicationInfo appInfo;
appInfo = context.getPackageManager().getApplicationInfo(
this.getPackageName(),
PackageManager.GET_META_DATA);
this.manifestSecretKey = appInfo.metaData.getString("manifestKey");
} catch(PackageManager.NameNotFoundException e){
Log.e("My Secret", "PackageManager exception");
}

Access through BuildConfig

If you are using BuildConfig, there is nothing to do, I find this preferable. However, don’t forget to enable buildConfig.
In the following example, MY_SECRET_KEY is stored in secrets.properties, and the source code directly retrieves it.

this.buildSecretKey = BuildConfig.MY_SECRET_KEY;

Running the app

My test application reads two secrets from secrets.properties, one using Manifest meta-data, and the other from buildConfig.

MY_SECRET_KEY=CryptaxVerySecret
MANIFEST_KEY=CryptaxManifestSecret

The source code will merely toast and log the secrets.

package com.example.mysecret;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class MainActivity extends AppCompatActivity {
private String buildSecretKey;
private String manifestSecretKey;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
Context context = this.getApplicationContext();

try {
ApplicationInfo appInfo;
appInfo = context.getPackageManager().getApplicationInfo(this.getPackageName(),
PackageManager.GET_META_DATA);
initSecret(appInfo.metaData);
useSecret(context);
} catch(PackageManager.NameNotFoundException e){
Log.e("My Secret", "PackageManager exception");
}

}

void initSecret(Bundle bundle) {
Log.d("My Secret", "initSecret()");
this.buildSecretKey = BuildConfig.MY_SECRET_KEY;

this.manifestSecretKey = bundle.getString("manifestKey");

// Log an error if apiKey is not set.
if (TextUtils.isEmpty(this.buildSecretKey)) {
Log.e("My Secret", "No build secret key");
finish();
}

if (TextUtils.isEmpty(this.manifestSecretKey)) {
Log.e("My Secret", "No manifest key");
finish();
}
}

void useSecret(Context context) {
Log.d("My Secret", "useSecret()");
// we can use the secret
// DEMO: we toast it
Toast.makeText(context, this.buildSecretKey + " " + this.manifestSecretKey, Toast.LENGTH_LONG ).show();
Log.d("My Secret", "Toasting build secret: "+this.buildSecretKey);
Log.d("My Secret", "Toasting manifest secret: "+this.manifestSecretKey);
}
}
The application works and displays both secrets. All good.

Disassembling the APK

How easy is it to recover the secrets through disassembly? Very, very simple. I’ll use DroidLysis (which can be seen as a baksmali/apktool wrapper) on the APK.

$ python3 ~/dev/droidlysis/droidlysis3.py --input app-release.apk --output . --conf ~/dev/droidlysis/conf/general.conf 
Processing file: ./app-release.apk ...
...

Then, let’s search for your secrets with a simple grep!

$ grep -r Cryptax *
AndroidManifest.xml: <meta-data android:name="manifestKey" android:value="CryptaxManifestSecret"/>
smali/com/example/mysecret/BuildConfig.smali:.field public static final MANIFEST_KEY:Ljava/lang/String; = "CryptaxManifestSecret"
smali/com/example/mysecret/BuildConfig.smali:.field public static final MY_SECRET_KEY:Ljava/lang/String; = "CryptaxVerySecret"
smali/com/example/mysecret/MainActivity.smali: const-string v0, "CryptaxVerySecret"
...

At build time, we see that the Google Secrets Gradle plugin replaces the secrets in theAndroidManifest.xml

...
<meta-data android:name="manifestKey" android:value="CryptaxManifestSecret"/>

And it creates a BuildConfig entry for each secret:

.field public static final MANIFEST_KEY:Ljava/lang/String; = "CryptaxManifestSecret"

.field public static final MY_SECRET_KEY:Ljava/lang/String; = "CryptaxVerySecret"

Conclusion: do not use Google Secrets if you are looking for a mechanism to keep your secrets confidential. Once again, the Google Secrets plugin only helps you read “secrets” from an external configuration file. The ultimate goal being that this file is not committed to a versioning system.

So, how can I keep my secrets confidential in an Android app?

Keeping your secrets confidential against the prying eyes of disassembly is something difficult with no perfect solution.

Whatever solution (encryption, obfuscation, packing…), at some point, your code needs to access the secret, and that’s when it becomes particularly vulnerable to reverse engineering, e.g hooking techniques.

The best you can do is make it difficult. In some cases, it can be really difficult. Many RASPs are painful to reverse. If you are looking for a custom / simple solution, I see 2 strategies:

  1. Native. Implement native code that decrypts or de-obfuscates your secret, and access through JNI.
  2. Flutter. Put your secret in Flutter code and access it from Flutter or from Dalvik using Platform Channels. Pay attention that all strings are dumped in an Object Pool in Flutter and easy to find with strings on the binary. So, if your secret is a string, you should store it encrypted and/or obfuscated. Flutter is currently so difficult to reverse that it will keep your secrets relatively safe. In all cases, it’s only “relative security”, nothing is bullet-proof.

— Cryptax

--

--

@cryptax

Mobile and IoT malware researcher. The postings on this account are solely my own opinion and do not represent my employer.