Keeping Your Android Secrets Safer: Secrets Vault Plugin

Aziz Utku Kagitci
Commencis
Published in
6 min readSep 18, 2023
Photo by Franck on Unsplash

In the vast universe of Android development, ensuring app security, especially for sensitive data, has always been a paramount concern. If you’re a developer, you’ve likely faced the challenge of where to securely store secrets like API keys and tokens in your app.

Dive deeper into the challenges of safely storing secrets on Android with this article. Discover why relying only on the Native Development Kit (NDK) may not be enough and how the Secrets Vault Plugin can be a robust solution for safeguarding API keys and other confidential data.

TLDR: Ensuring app security in Android development is a major concern. While the NDK offers some protection, it’s not entirely foolproof. This article delves into the challenges with NDK and presents the Secrets Vault Plugin as a robust solution. It provides features like obfuscation, app signature checks, and more. However, developers should be wary and remember that no client-side security measure is completely invincible.

Some terminologies before we start:

NDK (Native Development Kit):
Allows developers to incorporate C and C++ code in Android apps, which can sometimes provide performance or security advantages over Java or Kotlin alone.

JNI (Java Native Interface):
Serves as a bridge that enables Java to interact with native code. In Android, it’s the tool that facilitates communication between Java or Kotlin and C or C++ code.

.so (Shared Object) files: Equivalent to Windows’ DLLs. In Android, .so files hold compiled C or C++ code and are found in the ‘lib’ directory of the APK.

The Problem with Plain NDK

The NDK allows developers to write portions of their apps using native-code languages like C and C++. On the surface, it might seem like the perfect place to hide secrets because, unlike Java or Kotlin bytecode, native code isn’t as easily decompiled. However, while native code offers a higher degree of security, it’s not invincible. Determined attackers can use tools to disassemble native libraries, revealing your sensitive strings. Also, manually setting up NDK requires writing a lot of boilerplate code, which can be cumbersome depending on the complexity and the number of secrets.

But then, what’s the solution?

We can take some measures to make reverse engineering harder:

1. Do not expose secrets as plain text

Storing secrets directly in your code can expose your application to numerous threats. Attackers, once they decompile or gain access to your source code, can easily extract these secrets. The best practice is never to expose secrets in plain text. This is where encoding and obfuscation come into play, turning your plain secrets into an obscured format.

You can obfuscate them using a reversible XOR operator.

A Kotlin code snippet for obfuscating secrets
Code snippet for obfuscating secrets

So we can store our secret in an obfuscated format within our native code.

Native code with obfuscated data

If we decompile the native code above, we cannot see our API key directly.

Decompiled native code that has obfuscated data

What happens if we store them as plain text?

If we store them in plain text, they can be easily captured from the decompiled native code.

Native code with plain text data

After decompiling the native code above using Ghidra, we can easily access our API key because it is stored in plain text. You can clearly see this on line 5: “YOUR_API_KEY”.

Decompiled native code that has plain text data

2. Use Obfuscated JNI Methods with @JvmName

The @JvmName annotation serves a dual purpose when obfuscating JNI methods. Firstly, it empowers developers to use meaningful method names during development, ensuring the readability and maintainability of the code. After compilation, these method names are obfuscated, adding a significant layer of obscurity to function calls between Java and native code. This means that even if malicious actors attempt reverse engineering, discerning the purpose of each method or understanding which secret or API key correlates to which function becomes a challenge. This dual nature ensures ease of development without compromising security.

class Secrets {
@JvmName("a0")
external fun getApiKey(): String

companion object {
init {
System.loadLibrary("secrets")
}
}
}

We also need to adjust the JNI method name in our native code to match the value set in the JvmName annotation. For instance, the new name should correspond to the JNI method shown in the previous examples:

Obfuscsted JNI method name

It disrupts the correlation between API key value and API key name. After decompiling the native code above, our JNI methods would be as follows:

Decompiled native code with obfuscated JNI method name

3. Perform app signature check

App signature check validates the authenticity of the application. This check prevents unauthorized access to the generated native .so file, adding an additional layer of protection against unauthorized usage.

Android apps are signed with a private key using a keystore. Therefore, you can check the MD5 signature of your signed application in your native code. If it doesn’t match the actual signature, it can simply return an empty string.

jstring getOriginalKey(
char *obfuscatedSecret,
int obfuscatedSecretSize,
JNIEnv *pEnv) {
const char appSignatures[][32] = { ..., ... };
if (!checkAppSignatures(appSignatures, 2, pEnv)) {
return pEnv->NewStringUTF("");
}
...
}

By performing an app signature check, if someone copies your native library file to their project, calling the JNI methods will return an empty string. For more details, you can refer to this comprehensive article: https://blog.mikepenz.dev/a/protect-secrets-p3/

Apply all measures easily with the Secrets Vault Plugin

Obfuscation transforms your code into an equivalent version that’s harder to understand. When combined with the NDK, you have a powerful plugin on your side. The Secrets Vault Plugin, designed for Android developers, enhances this combination by offering several features:

  1. Reversible XOR Operator and Obfuscated Storage in NDK Binary: Obfuscates secrets to ensure they aren’t exposed as plain text. It stores the obfuscated secret deep within the NDK binary as a hexadecimal array, making reconstruction as challenging as finding a needle in a haystack.
  2. Obfuscated JNI Methods: Utilizes the JvmName annotation to obfuscate JNI method names, adding a layer of obscurity to function calls between Java, Kotlin, and native code.
  3. App Signature Check: This allows you to perform an app signature check easily. Just pass your signatures to the plugin setup.
  4. Custom Encoding/Decoding Algorithm: Provides an advanced feature that lets you add your own layer of security.

The plugin also allows developers to apply this whole measurement easily with minimal effort. It generates whole boilerplate code for you. Just put your credentials in the JSON file.

[
{ "key": "apiKeyFlavorSpesific", "value": "API_VALUE_1_INTERNAL", "sourceSet": "internal" },
{ "key": "apiKeyFlavorSpesific", "value": "API_VALUE_1_EXTERNAL", "sourceSet": "external" },
{ "key": "apiKeyMain", "value": "API_VALUE_2_MAIN" }
]

Then you can call keepSecrets task.

./gradlew keepSecrets

Secrets Vault Plugin has also support for different source sets and flavors.

Developers can differentiate and categorize their secrets based on unique source sets or flavors, such as ‘google’ or ‘prod’. This feature provides an organized, efficient, and modular approach to handling secrets, especially for larger projects or apps with multiple distribution channels and variations.

For those of you who are interested in diving deeper into the specifics and functionalities of the plugin, I’ve provided a link to its GitHub repository below. This will give you comprehensive insights into its usage and more:

Please note that no client-side security measures are invincible. As a rule of thumb, storing secrets in a mobile app is not considered best practice. However, when there’s no other option, these methods are the best recommendation for concealing them.

--

--