This Android-focused Gradle plugin provides developers with a comprehensive tool to securely hide and obfuscate secrets within their Android projects.
The Secrets Vault Plugin employs multiple security-enhancing strategies, including:
- Reversible XOR Operator: Utilizing the reversible XOR operator to obfuscate secrets, thereby ensuring they are never exposed in plain text.
- Obfuscated Storage in NDK Binary: Safely storing the obfuscated secret in an NDK binary as a hexadecimal array, making it extremely difficult to identify or reconstruct from disassembly.
- App Signature Check: Performs an app signature check, which validates the authenticity of the application. This check prevents unauthorized access to the generated .so file, adding an additional layer of protection against tampering or unauthorized usage.
- Different Source Set and Flavor Support: 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 handle secrets, especially for larger projects or apps with multiple distribution channels and variations.
- Custom Encoding/Decoding Algorithm: Providing an option for users to implement their own encoding/decoding algorithm, thereby introducing an additional layer of security.
Please remember 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, this method is our best recommendation for concealing them.
To use the Secrets Vault Plugin in your Android project, follow these steps:
In your root build.gradle[.kts]
file, add the following plugin configuration:
plugins {
...
id "com.commencis.secretsvaultplugin" version "[write the latest version]" apply false
}
Apply the plugin in the module build.gradle[.kts]
file:
plugins {
...
id 'com.commencis.secretsvaultplugin'
}
To keep secrets in your project, you can add them to a JSON file located in the root folder of the module where you've applied the plugin. Follow the format below:
[
{ "key": "apiKey1", "value": "API_VALUE_1" },
{ "key": "apiKey2", "value": "API_VALUE_2" },
...
]
You can obfuscate and hide your secret keys in your project using the following command:
./gradlew keepSecrets
You can also configure the plugin by defining the secretsVault block in your module-level build.gradle[.kts]
file:
secretsVault {
secretsFile = file("YourSecretsJsonFile.json") // Optional. secrets.json file in the module by default.
packageName = "YourPackageName" // Optional. Uses the namespace of the module where the plugin is applied by default.
appSignatures = ["YourAppSignatureForDebug", "YourAppSignatureForRelease"] // Optional. Empty by default.
obfuscationKey = "YourObfuscationKey" // Optional. A randomly generated alphanumeric string of length 32 by default.
sourceSetSecretsMappingFile = file("YourSourceSetMappingFile.json") // Optional. Used for multi-dimensional flavors mapping.
makeInjectable = false // Optional. Defaults to 'false'. Makes the generated Kotlin class injectable.
cmake { // Optional
projectName = "YourCMakeProjectName" // Optional. Defaults to the module name.
version = "YourCMakeVersion" // Optional. Default CMake version used otherwise.
}
}
- The
secretsFile
parameter is optional and usessecrets.json
file by default in the root folder of the module where you've applied the plugin. You can provide your own JSON file if desired. - The
packageName
parameter is optional and uses the namespace in the module where the plugin applied by default. You can specify a different package name if needed. - The
appSignatures
parameter is optional, but it is highly recommended to add an app signature check for better security. Use the./gradlew signingReport
command to retrieve the MD5 hash of your debug and release signings. Add these values to theappSignatures
option in thesecretsVault
configuration in thebuild.gradle[.kts]
file. - The
obfuscationKey
parameter is optional and defaults to a randomly generated alphanumeric string of length 32. - The
sourceSetSecretsMappingFile
parameter is optional and is used to map specific secrets files to distinct source sets, beneficial for multi-dimensional flavors. - The
makeInjectable
parameter is optional and determines if the generated Kotlin class should include the @Inject annotation for its constructor. By default, it is set tofalse
. - The
cmake
block is optional and contains additional optional parameters:- The
projectName
parameter is optional and uses the module's name where the plugin is applied by default. You can specify a different project name if needed. - The
version
parameter is optional. If not provided, a default CMake version will be used.
- The
To enable the compilation of C++ files, add these lines in the Module level build.gradle[.kts]
:
android {
...
// Configure NDK build for C++ files
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}
}
Access your secret key by calling :
val key = MainSecrets().getYourSecretKeyName()
The names of the methods in the generated files (MainSecrets.kt
& Secrets.kt
) are replaced with dummy strings such as a0
on the JVM
side for extra security. This makes accessing these methods inside Java
classes a bit tricky since those names are not "readable" and are dependent on the order in secrets.json
file.
As as solution, a helper/wrapper Kotlin
class can be created instead of accessing these methods directly inside a Java
class:
MySecretsHelper.kt
:
internal class MySecretsHelper {
val secrets = MainSecrets() // Suppose contains "external fun getYourSecretKeyName(): String" with "@JvmName("a0")" annotation.
val mySecret: String get() = secrets.getYourSecretKeyName()
}
Inside your Java
class, instead of doing:
class MySecretsConsumer {
final MainSecrets secrets = new MainSecrets();
void someMethod() {
final String key = secrets.a0();
...
}
}
You can do:
class MySecretsConsumer {
final MySecretsHelper secretsHelper = new MySecretsHelper();
void someMethod() {
final String key = secretsHelper.getMySecret();
...
}
}
If you are working on multi-flavor projects and have flavor-specific secrets, you need to pass arguments to CMake in your build.gradle[.kts]
file. Follow the steps below:
android {
...
productFlavors {
flavorName {
...
externalNativeBuild {
cmake {
// Pass arguments to CMake
arguments "-DsourceSet=flavorName"
}
}
}
}
}
}
Once you have set up the flavor-specific configuration, you can include the secrets in your secrets.json
file by specifying the source set or flavor name using the sourceSet
parameter. If a secret is independent of source sets, you can omit the sourceSet
parameter, and it will be accessible from all source sets.
Here is an example of the secrets.json
file structure:
[
{ "key": "apiKey1", "value": "API_VALUE_1_DEVELOPMENT", "sourceSet": "dev" },
{ "key": "apiKey1", "value": "API_VALUE_1_PRODUCTION", "sourceSet": "prod" },
{ "key": "apiKey2", "value": "API_VALUE_2_DEVELOPMENT", "sourceSet": "dev" },
{ "key": "apiKey2", "value": "API_VALUE_2_PRODUCTION", "sourceSet": "prod" },
{ "key": "apiKey4", "value": "API_VALUE_4_GENERAL" }
]
To access the flavor-specific secrets within your app, you can call the corresponding methods from the Secrets()
class. For example, use Secrets().getApiKey1()
to retrieve the value of apiKey1
that changes based on the current flavor.
For global secrets that are accessible from all source sets and flavours, you can use the methods from the MainSecrets()
class, such as MainSecrets().getApiKey4()
.
Note: Make sure that you update the
secrets.json
file with the relevant secrets for each source set in your Android project.
In complex projects with multiple build variants or distribution channels, managing secrets across these dimensions can be a challenge. The Secrets Vault Plugin simplifies this by introducing support for Multi-Dimensional Flavors.
This feature allows developers to map specific secrets to distinct source sets, ensuring a streamlined approach to managing secrets based on various build configurations.
To make use of Multi-Dimensional Flavors, specify a mapping using the following JSON format:
Here is an example of the sourceSetMapping.json
file structure:
[
{
"secretsFileName" : "MobileServicesSecrets",
"cmakeArgument": "mobileServices",
"sourceSets" : [ "google", "huawei" ]
},
{
"secretsFileName" : "Secrets",
"cmakeArgument": "version",
"sourceSets" : [ "dev", "qa" ]
}
]
secretsFileName
: Specifies the name of the generated secrets file, ensuring each set of secrets is distinctly accessible.
cmakeArgument
: Represents the specific argument name that will be passed to CMake during the native build process.
sourceSets
: A list of source sets that will use a particular set of secrets, ensuring precise mapping and access.
Firstly, provide the mapping file to the Secrets Vault Plugin in secretsVault block:
secretsVault {
...
sourceSetSecretsMappingFile = file("sourceSetMapping.json") // Point to the JSON file that contains your mapping configuration.
}
To correctly configure the multi-dimensional flavors, ensure you update the argument in your build configuration to match the cmakeArgument
specified in the JSON:
android {
...
productFlavors {
flavorName {
...
externalNativeBuild {
cmake {
// Pass arguments to CMake. Match the 'cmakeArgument' from your JSON configuration.
arguments "-Dversion=flavorName" // For example, for 'cmakeArgument': 'version'
}
}
}
}
}
Remember to replace version in -Dversion=flavorName with the appropriate cmakeArgument from your JSON. The flavorName should correspond to the specific product flavor you're building for.
To enhance the security of your secrets, you can create a custom encoding/decoding algorithm. The secrets will be stored in C++ and further secured by applying your custom encoding algorithm. Additionally, the decoding algorithm will be compiled, making it more challenging for an attacker to reverse-engineer and obtain your keys.
Encode all values in your secrets.json
file.
Then, implement the decoding logic, add your custom decoding code to the customDecode
method in secrets.cpp
.
void customDecode(char *str) {
// Implement your custom logic here.
}
The customDecode
method is automatically invoked when calling:
Secrets().getYourSecretKeyName()
This project is inspired by Hidden Secrets Gradle Plugin developed by Klaxit. It shares some common code with the Hidden Secrets Gradle Plugin, which served as a valuable reference during the development of this project.
This project is licensed under the MIT License. You are free to modify, distribute, and use the code in your projects. Please refer to the LICENSE file for more details.