Skip to content
This repository was archived by the owner on Dec 24, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ For more details about the installation check the [plugin's page](https://plugin

Obfuscate and hide your key in your project :
```shell
./gradlew hideSecret -Pkey=yourKeyToObfuscate [-PkeyName=YourSecretKeyName] [-Ppackage=com.your.package]
./gradlew hideSecret -PkeyHash=YourKeystoreHash -Pkey=yourKeyToObfuscate [-PkeyName=YourSecretKeyName] [-Ppackage=com.your.package]
```
The parameter `keyName` is optional, by default the key name is randomly generated.
The parameter `package` is optional, by default the `applicationId` of your project will be used.
Expand Down Expand Up @@ -92,7 +92,7 @@ As an example, we will use a [rot13 algorithm](https://en.wikipedia.org/wiki/ROT
After a rot13 encoding your key `yourKeyToObfuscate` becomes `lbheXrlGbBoshfpngr`.
Add it in your app :
```shell
./gradlew hideSecret -Pkey=lbheXrlGbBoshfpngr -PkeyName=YourSecretKeyName
./gradlew hideSecret -Pkey=lbheXrlGbBoshfpngr -PkeyName=YourSecretKeyName -PkeyHash=YourKeystoreHash
```

Then in `secrets.cpp` you need to add your own decoding code in `customDecode` method:
Expand Down Expand Up @@ -145,7 +145,7 @@ keyName2=yourKeyToObfuscate2
3. Run

``` shell
./gradlew hideSecretFromPropertiesFile -PpropertiesFileName=credentials.properties
./gradlew hideSecretFromPropertiesFile -PpropertiesFileName=credentials.properties -PkeyHash=YourKeystoreHash
```

It will regenerate all secret files in the project and update all secrets from the properties file.
Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/com/klaxit/hiddensecrets/CodeGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ object CodeGenerator {
" JNIEnv* pEnv,\n" +
" jobject pThis,\n" +
" jstring packageName) {\n" +
" jstring obfuscatingJStr = getSignature(pEnv, packageName);\n" +
" char obfuscatedSecret[] = $obfuscatedKey;\n" +
" return getOriginalKey(obfuscatedSecret, sizeof(obfuscatedSecret), packageName, pEnv);\n" +
" return getOriginalKey(obfuscatedSecret, sizeof(obfuscatedSecret), obfuscatingJStr, pEnv);\n" +
"}\n"
}

Expand Down
32 changes: 28 additions & 4 deletions src/main/kotlin/com/klaxit/hiddensecrets/HiddenSecretsPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ open class HiddenSecretsPlugin : Plugin<Project> {
private const val APP_MAIN_FOLDER = "src/main/"
private const val DEFAULT_KEY_NAME_LENGTH = 8
private const val KEY_PLACEHOLDER = "YOUR_KEY_GOES_HERE"
private const val KEY_NAME_PLACEHOLDER = "YOUR_KEY_NAME_GOES_HERE"
private const val PACKAGE_PLACEHOLDER = "YOUR_PACKAGE_GOES_HERE"
private const val KOTLIN_FILE_NAME = "Secrets.kt"

Expand All @@ -40,10 +41,12 @@ open class HiddenSecretsPlugin : Plugin<Project> {
private const val PROP_KEY_NAME = "keyName"
private const val PROP_PACKAGE = "package"
private const val PROP_FILE_NAME = "propertiesFileName"
private const val PROP_KEY_HASH = "keyHash"

// Errors
private const val ERROR_EMPTY_KEY = "No key provided, use argument '-Pkey=yourKey'"
private const val ERROR_EMPTY_PACKAGE = "Empty package name, use argument '-Ppackage=your.package.name'"
private const val ERROR_EMPTY_KEY_HASH = "Empty keyHash, use argument '-PkeyHash=yourKeystoreHash'"

// Sample usage
private const val SAMPLE_FROM_PROPS = "-P${PROP_FILE_NAME}=credentials.properties"
Expand Down Expand Up @@ -160,17 +163,35 @@ open class HiddenSecretsPlugin : Plugin<Project> {
return keyName
}

/**
* Get key hash param from command line
*/
@Input
fun getKeyHash(): String {
val key: String
if (project.hasProperty(PROP_KEY_HASH)) {
// From command line
key = project.property(PROP_KEY_HASH) as String
} else {
throw InvalidUserDataException(ERROR_EMPTY_KEY_HASH)
}
return key
}

/**
* Generate en encoded key from command line params
*/
fun getObfuscatedKey(): String {
val key = getKeyParam()
println("### SECRET ###\n$key\n")

val keyHash = getKeyHash()
println("### KEY HASH ###\n$keyHash\n")

val packageName = getPackageNameParam()
println("### PACKAGE NAME ###\n$packageName\n")

val encodedKey = Utils.encodeSecret(key, packageName)
val encodedKey = Utils.encodeSecret(key, keyHash, packageName)
println("### OBFUSCATED SECRET ###\n$encodedKey")
return encodedKey
}
Expand Down Expand Up @@ -261,7 +282,7 @@ open class HiddenSecretsPlugin : Plugin<Project> {
fun hideSecret(
keyName: String,
packageName: String,
obfuscatedKey: String
obfuscatedKey: String,
) {
// Add method in Kotlin code
var secretsKotlin = getKotlinFile()
Expand Down Expand Up @@ -302,7 +323,7 @@ open class HiddenSecretsPlugin : Plugin<Project> {
// Replace package name
text = text.replace(PACKAGE_PLACEHOLDER, Utils.getCppName(kotlinPackage))
// Replace key name
text = text.replace("YOUR_KEY_NAME_GOES_HERE", cppKeyName)
text = text.replace(KEY_NAME_PLACEHOLDER, cppKeyName)
// Replace demo key
text = text.replace(KEY_PLACEHOLDER, obfuscatedKey)
secretsCpp.writeText(text)
Expand Down Expand Up @@ -408,13 +429,16 @@ open class HiddenSecretsPlugin : Plugin<Project> {
copyCppFiles(true)
copyKotlinFile(true)

val keyHash = getKeyHash()
val packageName = getPackageNameParam()
val propsFile = getPropertiesFile()
val props = getPropertiesFromFile(propsFile = propsFile)
println("Generating secrets from props: ${propsFile.path}")
props.entries.forEach { entry ->
val keyName = entry.key as String
val obfuscatedKey = Utils.encodeSecret(entry.value as String, packageName)
val obfuscatedKey = Utils.encodeSecret(
entry.value as String, keyHash, packageName
)
hideSecret(keyName, packageName, obfuscatedKey)
}
}
Expand Down
18 changes: 16 additions & 2 deletions src/main/kotlin/com/klaxit/hiddensecrets/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,26 @@ object Utils {
return digest.fold("") { str, it -> str + "%02x".format(it) }
}

/**
* Obfuscator is created using SHA-256 of (keyHash + package)
*/
fun createObfuscator(
keyHash: String,
packageName: String
): String {
return sha256(keyHash + packageName)
}

/**
* Encode a string key to and hex array using package name
*/
fun encodeSecret(key: String, packageName: String): String {
fun encodeSecret(
key: String,
keyHash: String,
packageName: String
): String {
// Generate the obfuscator as the SHA256 of the app package name
val obfuscator = sha256(packageName)
val obfuscator = createObfuscator(keyHash, packageName)
val obfuscatorBytes = obfuscator.toByteArray()

// Generate the obfuscated secret bytes array by applying a XOR between the secret and the obfuscator
Expand Down
6 changes: 5 additions & 1 deletion src/main/resources/cpp/secrets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
#include "sha256.hpp"
#include "sha256.cpp"

#include "signature.hpp"
#include "signature.cpp"

/* Copyright (c) 2020-present Klaxit SAS
*
* Permission is hereby granted, free of charge, to any person
Expand Down Expand Up @@ -69,6 +72,7 @@ Java_YOUR_PACKAGE_GOES_HERE_Secrets_getYOUR_KEY_NAME_GOES_HERE(
JNIEnv *pEnv,
jobject pThis,
jstring packageName) {
jstring obfuscatingJStr = getSignature(pEnv, packageName);
char obfuscatedSecret[] = YOUR_KEY_GOES_HERE;
return getOriginalKey(obfuscatedSecret, sizeof(obfuscatedSecret), packageName, pEnv);
return getOriginalKey(obfuscatedSecret, sizeof(obfuscatedSecret), obfuscatingJStr, pEnv);
}
129 changes: 129 additions & 0 deletions src/main/resources/cpp/signature.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#include "secrets.hpp"

#include <jni.h>

/* Copyright (c) 2024-present Dylan Kwon
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/

jobject getApplication(JNIEnv *pEnv) {
jclass appGlobalsClass = pEnv->FindClass("android/app/AppGlobals");
jmethodID jGetInitialApplication = pEnv->GetStaticMethodID(appGlobalsClass,"getInitialApplication","()Landroid/app/Application;");
return pEnv->CallStaticObjectMethod(appGlobalsClass, jGetInitialApplication);
}

jstring getPackageName(JNIEnv *pEnv) {
jobject applicationObject = getApplication(pEnv);
jclass contextClass = pEnv->FindClass("android/content/Context");
jmethodID getPackageNameMethodId = pEnv->GetMethodID(contextClass, "getPackageName", "()Ljava/lang/String;");
return static_cast<jstring>(pEnv->CallObjectMethod(applicationObject, getPackageNameMethodId));
}

jstring getKeyHash(JNIEnv *pEnv) {
// Application
jobject applicationObject = getApplication(pEnv);

// PackageName
jstring packageNameString = getPackageName(pEnv);

// Context
jclass contextClass = pEnv->FindClass("android/content/Context");

// PackageManager
jmethodID getPackageManagerMethodId = pEnv->GetMethodID(contextClass, "getPackageManager", "()Landroid/content/pm/PackageManager;");
jobject packageManagerObject = pEnv->CallObjectMethod(applicationObject, getPackageManagerMethodId);
jclass packageManagerClass = pEnv->GetObjectClass(packageManagerObject);

// PackageInfo
jmethodID getPackageInfoMethodId = pEnv->GetMethodID(packageManagerClass, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
jobject packageInfoObject = pEnv->CallObjectMethod(packageManagerObject, getPackageInfoMethodId, packageNameString, 0x08000000);
jclass packageInfoClass = pEnv->GetObjectClass(packageInfoObject);

// SDK Version
jclass buildClass = pEnv->FindClass("android/os/Build$VERSION");
jfieldID versionCodeFieldId = pEnv->GetStaticFieldID(buildClass, "SDK_INT", "I");
jint versionCode = pEnv->GetStaticIntField(buildClass, versionCodeFieldId);

// Signatures
jobjectArray signatures;

if (versionCode >= 28) {
// SigningInfo
jfieldID signingInfoFieldId = pEnv->GetFieldID(packageInfoClass, "signingInfo", "Landroid/content/pm/SigningInfo;");
jobject signingInfoObject = pEnv->GetObjectField(packageInfoObject, signingInfoFieldId);
jclass signingInfoClass = pEnv->GetObjectClass(signingInfoObject);

// SigningInfo.hasMultipleSignersBoolean
jmethodID hasMultipleSignersMethodId = pEnv->GetMethodID(signingInfoClass, "hasMultipleSigners", "()Z");
jboolean hasMultipleSignersBoolean = pEnv->CallBooleanMethod(signingInfoObject, hasMultipleSignersMethodId);

if (hasMultipleSignersBoolean) {
jmethodID getApkContentsSignersMethodId = pEnv->GetMethodID(signingInfoClass, "getApkContentsSigners", "()[Landroid/content/pm/Signature;");
signatures = reinterpret_cast<jobjectArray>(pEnv->CallObjectMethod(signingInfoObject, getApkContentsSignersMethodId));
} else {
jmethodID getSigningCertificateHistoryMethodId = pEnv->GetMethodID(signingInfoClass, "getSigningCertificateHistory", "()[Landroid/content/pm/Signature;");
signatures = reinterpret_cast<jobjectArray>(pEnv->CallObjectMethod(signingInfoObject, getSigningCertificateHistoryMethodId));
}
} else {
jfieldID signaturesFieldId = pEnv->GetFieldID(packageInfoClass, "signatures", "[Landroid/content/pm/Signature;");
signatures = reinterpret_cast<jobjectArray>(pEnv->GetObjectField(packageInfoObject, signaturesFieldId));
}

// First Signature
jobject signatureObject = pEnv->GetObjectArrayElement(signatures, 0);
jclass signatureClass = pEnv->GetObjectClass(signatureObject);

// First Signature ByteArray
jmethodID toByteArrayMethodId = pEnv->GetMethodID(signatureClass, "toByteArray", "()[B");
jbyteArray signatureByteArray = reinterpret_cast<jbyteArray>(pEnv->CallObjectMethod(signatureObject, toByteArrayMethodId));

// MessageDigest
jclass messageDigestClass = pEnv->FindClass("java/security/MessageDigest");
jmethodID getInstanceMethodId = pEnv->GetStaticMethodID(messageDigestClass,"getInstance","(Ljava/lang/String;)Ljava/security/MessageDigest;");
jobject messageDigestObject = pEnv->CallStaticObjectMethod(messageDigestClass,getInstanceMethodId, pEnv->NewStringUTF("SHA"));

// MessageDigest Update
jmethodID updateMethodId = pEnv->GetMethodID(messageDigestClass,"update","([B)V");
pEnv->CallVoidMethod(messageDigestObject, updateMethodId, signatureByteArray);

// MessageDigest Digest
jmethodID digestMethodId = pEnv->GetMethodID(messageDigestClass,"digest","()[B");
jbyteArray digestByteArray = reinterpret_cast<jbyteArray>(pEnv->CallObjectMethod(messageDigestObject, digestMethodId));

// Base64
jclass base64Class = pEnv->FindClass("android/util/Base64");
jmethodID encodeMethodId = pEnv->GetStaticMethodID(base64Class,"encodeToString","([BI)Ljava/lang/String;");
jstring base64String = static_cast<jstring>(pEnv->CallStaticObjectMethod(base64Class, encodeMethodId, digestByteArray, 2));

return base64String;
}

jstring getSignature(JNIEnv* pEnv, jstring packageName) {
jstring keyHash = getKeyHash(pEnv);

jclass signatureClass = pEnv->GetObjectClass(keyHash);
jmethodID concatMethodId = pEnv->GetMethodID(signatureClass, "concat", "(Ljava/lang/String;)Ljava/lang/String;");
jstring concat = reinterpret_cast<jstring>(pEnv->CallObjectMethod(keyHash, concatMethodId, packageName));

return concat;
}
5 changes: 5 additions & 0 deletions src/main/resources/cpp/signature.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#ifndef SIGNATURE_H
#define SIGNATURE_H


#endif // SIGNATURE_H
5 changes: 3 additions & 2 deletions src/test/kotlin/HiddenSecretsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class HiddenSecretsTest : WordSpec({
// Properties
val key = "thisIsATestKey"
val packageName = "com.package.test"
val keyHash = "5ptCUaVG+0JGgprlT1yKuyJrUI4="

"Make command ${HiddenSecretsPlugin.TASK_COPY_CPP} succeed" {
val result = gradleRunner.withArguments(HiddenSecretsPlugin.TASK_COPY_CPP).build()
Expand All @@ -42,10 +43,10 @@ class HiddenSecretsTest : WordSpec({
}

"Make command ${HiddenSecretsPlugin.TASK_OBFUSCATE} succeed" {
val result = gradleRunner.withArguments(HiddenSecretsPlugin.TASK_OBFUSCATE, "-Pkey=$key", "-Ppackage=$packageName").build()
val result = gradleRunner.withArguments(HiddenSecretsPlugin.TASK_OBFUSCATE, "-Pkey=$key", "-Ppackage=$packageName", "-PkeyHash=$keyHash").build()
println(result.output)
// Should contain obfuscated key
result.output shouldContain "{ 0x15, 0x58, 0xb, 0x43, 0x78, 0x4a, 0x23, 0x6d, 0x1, 0x4b, 0x46, 0x7c, 0x57, 0x41 }"
result.output shouldContain "{ 0x45, 0xd, 0x5e, 0x42, 0x7d, 0x10, 0x76, 0x36, 0x6, 0x15, 0x16, 0x73, 0x51, 0x18 }"
}

"Make command ${HiddenSecretsPlugin.TASK_PACKAGE_NAME} succeed" {
Expand Down
20 changes: 14 additions & 6 deletions src/test/kotlin/IntegrationTest.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import com.klaxit.hiddensecrets.HiddenSecretsPlugin
import io.kotest.core.spec.style.WordSpec
import io.kotest.data.Row4
import io.kotest.data.Row5
import io.kotest.datatest.withData
import io.kotest.matchers.shouldBe
import org.gradle.testkit.runner.GradleRunner
Expand Down Expand Up @@ -39,14 +39,21 @@ class IntegrationTest : WordSpec({
val test = this
"Make command ${HiddenSecretsPlugin.TASK_HIDE_SECRET} succeed" {
test.withData(
Row4("thisIsATestKey", "thisIsATestKeyName", "thisIsATestKeyName", "com.package.test"),
Row4(
Row5(
"thisIsATestKey",
"thisIsATestKeyName",
"thisIsATestKeyName",
"com.package.test",
"5ptCUaVG+0JGgprlT1yKuyJrUI4=",
),
Row5(
"this_is_a_test_key",
"this_is_a_test_key_name",
"this_1is_1a_1test_1key_1name",
"com.package.test"
"com.package.test",
"7IoCUPVG0IrfEB381+KSyn2kUa0=",
),
) { (key, keyName, cppKeyName, packageName) ->
) { (key, keyName, cppKeyName, packageName, keyHash) ->
testProjectDir.run {
val packagePath = packageName.replace('.', '/')
val packageDirJava = newFolder("src/main/java/$packagePath")
Expand All @@ -73,7 +80,8 @@ class IntegrationTest : WordSpec({
HiddenSecretsPlugin.TASK_HIDE_SECRET,
"-Pkey=$key",
"-PkeyName=$keyName",
"-Ppackage=$packageName"
"-Ppackage=$packageName",
"-PkeyHash=$keyHash",
)
.build()
println(result.output)
Expand Down
Loading