diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9ce550d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# v0.1.1 +###Fixes +* Fix sha256 generation : long keys are now decoded correctly. Reported in https://github.com/klaxit/hidden-secrets-gradle-plugin/issues/16 +* Random string generation was not including lower case characters +###Improvements +* The plugin search for `Secrets.kt` to add other keys instead of using package's name to access it. +* Resolve package from the Kotlin file to edit the C++ file to be able to use a different package than the package of Secret.kt to encode/decode keys. +* Clearer error message for `hideSecret` command +* Clearer logs +* Tasks name become public +* Use more constants in code to avoid regression +* Automatic tests added +* Detekt added to the project to ensure Kotlin coding style +### Migration from 0.1.0 +* To take advantage of the sha256 generation fix you need to : +1) Remove files : `secrets.cpp`, `sha256.cpp` and `Secrets.kt` from your project (that will delete all your keys previously added) +2) You need to re-add all your keys with `hideSecret` command (will copy new cpp files and encode your key) +# v0.1.0 +* First release diff --git a/README.md b/README.md index 50b9cc6..f448f2c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -![travis ci status](https://travis-ci.org/klaxit/hidden-secrets-gradle-plugin.svg?branch=master) +[![travis ci status](https://travis-ci.org/klaxit/hidden-secrets-gradle-plugin.svg?branch=master)](https://travis-ci.com/github/klaxit/hidden-secrets-gradle-plugin/branches) +[![MIT license](https://img.shields.io/github/license/klaxit/hidden-secrets-gradle-plugin)](https://github.com/klaxit/hidden-secrets-gradle-plugin/blob/master/LICENSE) # Gradle plugin to deeply hide secrets on Android @@ -14,9 +15,6 @@ This plugin is **used in production** at [Klaxit - Covoiturage quotidien](https: ⚠️ Nothing on the client-side is unbreakable. So generally speaking, **keeping a secret in a mobile package is not a smart idea**. But when you absolutely need to, this is the best method we have found to hide it. -## This is a kotlin gradle plugin -This project is also a demonstration on how to create a full Kotlin gradle plugin for Android projects. - ## Compatibility This gradle plugin can be used with any Android project in Java or Kotlin. diff --git a/build.gradle.kts b/build.gradle.kts index e62ffb5..71ed5b1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } group = "com.klaxit.hiddensecrets" -version = "0.1.0" +version = "0.1.1" repositories { mavenCentral() diff --git a/src/main/kotlin/com/klaxit/hiddensecrets/CodeGenerator.kt b/src/main/kotlin/com/klaxit/hiddensecrets/CodeGenerator.kt index bed5a0c..7e741ba 100644 --- a/src/main/kotlin/com/klaxit/hiddensecrets/CodeGenerator.kt +++ b/src/main/kotlin/com/klaxit/hiddensecrets/CodeGenerator.kt @@ -12,7 +12,7 @@ object CodeGenerator { return "\nextern \"C\"\n" + "JNIEXPORT jstring JNICALL\n" + - "Java_" + Utils.getUnderScoredPackageName(packageName) + "_Secrets_get$keyName(\n" + + "Java_" + Utils.getSnakeCasePackageName(packageName) + "_Secrets_get$keyName(\n" + " JNIEnv* pEnv,\n" + " jobject pThis,\n" + " jstring packageName) {\n" + diff --git a/src/main/kotlin/com/klaxit/hiddensecrets/HiddenSecretsPlugin.kt b/src/main/kotlin/com/klaxit/hiddensecrets/HiddenSecretsPlugin.kt index 50bf4d1..16e8072 100644 --- a/src/main/kotlin/com/klaxit/hiddensecrets/HiddenSecretsPlugin.kt +++ b/src/main/kotlin/com/klaxit/hiddensecrets/HiddenSecretsPlugin.kt @@ -19,6 +19,9 @@ open class HiddenSecretsPlugin : Plugin { companion object { 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 PACKAGE_PLACEHOLDER = "YOUR_PACKAGE_GOES_HERE" + private const val KOTLIN_FILE_NAME = "Secrets.kt" //Tasks const val TASK_UNZIP_HIDDEN_SECRETS = "unzipHiddenSecrets" @@ -27,6 +30,7 @@ open class HiddenSecretsPlugin : Plugin { const val TASK_HIDE_SECRET = "hideSecret" const val TASK_OBFUSCATE = "obfuscate" const val TASK_PACKAGE_NAME = "packageName" + const val TASK_FIND_KOTLIN_FILE = "findKotlinFile" //Properties private const val KEY = "key" @@ -74,7 +78,6 @@ open class HiddenSecretsPlugin : Plugin { */ @Input fun getPackageNameParam(): String { - //From config var packageName: String? = null if (project.hasProperty(PACKAGE)) { //From command line @@ -95,7 +98,7 @@ open class HiddenSecretsPlugin : Plugin { */ @Input fun getKeyNameParam(): String { - val chars = ('a'..'Z') + ('A'..'Z') + val chars = ('a'..'z') + ('A'..'Z') // Default random key name var keyName = List(DEFAULT_KEY_NAME_LENGTH) { chars.random() }.joinToString("") if (project.hasProperty(KEY_NAME)) { @@ -136,12 +139,19 @@ open class HiddenSecretsPlugin : Plugin { } val directory = project.file(path) if (!directory.exists()) { - error("Directory $path does not exist in the project, you might have selected a wrong package.") + println("Directory $path does not exist in the project, you might have selected a wrong package.") } path += fileName return project.file(path) } + /** + * If found, returns the Secrets.kt file in the Android app + */ + fun getKotlinFile(): File? { + return Utils.findFileInProject(project, APP_MAIN_FOLDER, KOTLIN_FILE_NAME) + } + /** * Copy Cpp files from the lib to the Android project if they don't exist yet */ @@ -149,7 +159,7 @@ open class HiddenSecretsPlugin : Plugin { project.file("$tmpFolder/cpp/").listFiles()?.forEach { val destination = getCppDestination(it.name) if (destination.exists()) { - println(it.name + " already exists") + println("${it.name} already exists") } else { println("Copy $it.name to\n$destination") it.copyTo(destination, true) @@ -158,14 +168,18 @@ open class HiddenSecretsPlugin : Plugin { } /** - * Copy Kotlin files from the lib to the Android project if they don't exist yet + * Copy Kotlin file Secrets.kt from the lib to the Android project if it does not exist yet */ - fun copyKotlinFiles() { + fun copyKotlinFile() { + if (getKotlinFile() != null) { + println("$KOTLIN_FILE_NAME already exists") + return + } val packageName = getPackageNameParam() project.file("$tmpFolder/kotlin/").listFiles()?.forEach { val destination = getKotlinDestination(packageName, it.name) if (destination.exists()) { - println(it.name + " already exists") + println("${it.name} already exists") } else { println("Copy $it.name to\n$destination") it.copyTo(destination, true) @@ -177,15 +191,15 @@ open class HiddenSecretsPlugin : Plugin { * Unzip plugin into tmp directory */ project.tasks.create(TASK_UNZIP_HIDDEN_SECRETS, Copy::class.java, - object : Action { - @TaskAction - override fun execute(copy: Copy) { - // in the case of buildSrc dir - copy.from(project.zipTree(javaClass.protectionDomain.codeSource.location!!.toExternalForm())) - println("Unzip jar to $tmpFolder") - copy.into(tmpFolder) - } - }) + object : Action { + @TaskAction + override fun execute(copy: Copy) { + // in the case of buildSrc dir + copy.from(project.zipTree(javaClass.protectionDomain.codeSource.location!!.toExternalForm())) + println("Unzip jar to $tmpFolder") + copy.into(tmpFolder) + } + }) /** * Copy C++ files to your project @@ -203,7 +217,7 @@ open class HiddenSecretsPlugin : Plugin { project.task(TASK_COPY_KOTLIN) { doLast { - copyKotlinFiles() + copyKotlinFile() } } @@ -225,54 +239,66 @@ open class HiddenSecretsPlugin : Plugin { dependsOn(TASK_UNZIP_HIDDEN_SECRETS) doLast { - //Copy files if they do not exist + //Assert that the key is present + getKeyParam() + //Copy files if they don't exist copyCppFiles() - copyKotlinFiles() + copyKotlinFile() val keyName = getKeyNameParam() val packageName = getPackageNameParam() val obfuscatedKey = getObfuscatedKey() + //Add method in Kotlin code + var secretsKotlin = getKotlinFile() + if (secretsKotlin == null) { + //File not found in project + secretsKotlin = getKotlinDestination(packageName, KOTLIN_FILE_NAME) + } + if (secretsKotlin.exists()) { + var text = secretsKotlin.readText(Charset.defaultCharset()) + text = text.replace(PACKAGE_PLACEHOLDER, packageName) + if (text.contains(keyName)) { + println("⚠️ Method already added in Kotlin !") + } + text = text.dropLast(1) + text += CodeGenerator.getKotlinCode(keyName) + secretsKotlin.writeText(text) + } else { + error("Missing Kotlin file, please run gradle task : $TASK_COPY_KOTLIN") + } + //Resolve package name for C++ from the one used in Kotlin file + var kotlinPackage = Utils.getKotlinFilePackage(secretsKotlin) + if (kotlinPackage.isEmpty()) { + println("Empty package in $KOTLIN_FILE_NAME") + kotlinPackage = packageName + } + //Add obfuscated key in C++ code val secretsCpp = getCppDestination("secrets.cpp") if (secretsCpp.exists()) { var text = secretsCpp.readText(Charset.defaultCharset()) if (text.contains(obfuscatedKey)) { - println("Key already added in C++ !") + println("⚠️ Key already added in C++ !") } - if (text.contains("YOUR_KEY_GOES_HERE")) { + if (text.contains(KEY_PLACEHOLDER)) { + //Edit placeholder key //Replace package name - text = text.replace("YOUR_PACKAGE_GOES_HERE", Utils.getUnderScoredPackageName(packageName)) + text = text.replace(PACKAGE_PLACEHOLDER, Utils.getSnakeCasePackageName(kotlinPackage)) //Replace key name text = text.replace("YOUR_KEY_NAME_GOES_HERE", keyName) //Replace demo key - text = text.replace("{YOUR_KEY_GOES_HERE}", obfuscatedKey) + text = text.replace(KEY_PLACEHOLDER, obfuscatedKey) secretsCpp.writeText(text) } else { //Add new key - text += CodeGenerator.getCppCode(packageName, keyName, obfuscatedKey) + text += CodeGenerator.getCppCode(kotlinPackage, keyName, obfuscatedKey) secretsCpp.writeText(text) } } else { error("Missing C++ file, please run gradle task : $TASK_COPY_CPP") } - - //Add method in Kotlin code - val secretsKotlin = getKotlinDestination(packageName, "Secrets.kt") - if (secretsKotlin.exists()) { - var text = secretsKotlin.readText(Charset.defaultCharset()) - text = text.replace("YOUR_PACKAGE_GOES_HERE", packageName) - if (text.contains(keyName)) { - println("Method already added in Kotlin !") - } - text = text.dropLast(1) - text += CodeGenerator.getKotlinCode(keyName) - secretsKotlin.writeText(text) - } else { - error("Missing Kotlin file, please run gradle task : $TASK_COPY_KOTLIN") - } - - println("You can now get your secret key by calling : Secrets().get$keyName(packageName)") + println("✅ You can now get your secret key by calling : Secrets().get$keyName(packageName)") } } @@ -285,5 +311,14 @@ open class HiddenSecretsPlugin : Plugin { println("APP PACKAGE NAME = " + getPackageNameParam()) } } + + /** + * Find Secrets.kt file in the project + */ + project.task(TASK_FIND_KOTLIN_FILE) { + doLast { + getKotlinFile() + } + } } } diff --git a/src/main/kotlin/com/klaxit/hiddensecrets/Utils.kt b/src/main/kotlin/com/klaxit/hiddensecrets/Utils.kt index 70ac150..e4053be 100644 --- a/src/main/kotlin/com/klaxit/hiddensecrets/Utils.kt +++ b/src/main/kotlin/com/klaxit/hiddensecrets/Utils.kt @@ -1,6 +1,8 @@ package com.klaxit.hiddensecrets import com.google.common.annotations.VisibleForTesting +import org.gradle.api.Project +import java.io.File import java.nio.charset.Charset import java.security.MessageDigest import kotlin.experimental.xor @@ -10,7 +12,7 @@ object Utils { /** * Transform package name com.klaxit.hidden to com_klaxit_hidden to ingrate in C++ code */ - fun getUnderScoredPackageName(packageName: String): String { + fun getSnakeCasePackageName(packageName: String): String { val packageComponents = packageName.split(".") var packageStr = "" val iterator: Iterator = packageComponents.iterator() @@ -63,4 +65,31 @@ object Utils { encoded += " }" return encoded } + + /** + * Search a file in the finale project, can provide a path to limit the search in some folders + */ + fun findFileInProject(project: Project, path: String, fileName: String): File? { + val directory = project.file(path) + directory.walkBottomUp().forEach { + if (it.name == fileName) { + println("$fileName found in ${it.absolutePath}\n") + return it + } + } + println("$fileName not found in $path") + return null + } + + /** + * Return package from first line of a kotlin file + */ + fun getKotlinFilePackage(file: File): String { + var text = file.readLines(Charset.defaultCharset())[0] + text = text.replace("package ", "") + // Handle package name using keywords + text = text.replace("`", "") + println("Package : $text found in ${file.name}") + return text + } } diff --git a/src/main/resources/cpp/secrets.cpp b/src/main/resources/cpp/secrets.cpp index 7b91198..55a04b7 100644 --- a/src/main/resources/cpp/secrets.cpp +++ b/src/main/resources/cpp/secrets.cpp @@ -44,9 +44,12 @@ jstring getOriginalKey( // Get the obfuscating string SHA256 as the obfuscator const char *obfuscatingStr = pEnv->GetStringUTFChars(obfuscatingJStr, NULL); - const char *obfuscator = sha256(obfuscatingStr); + char buffer[2*SHA256::DIGEST_SIZE + 1]; - // Apply a XOR between the obfuscated key and the obfuscating string to get original sting + sha256(obfuscatingStr, buffer); + const char* obfuscator = buffer; + + // Apply a XOR between the obfuscated key and the obfuscating string to get original string char out[obfuscatedSecretSize + 1]; for (int i = 0; i < obfuscatedSecretSize; i++) { out[i] = obfuscatedSecret[i] ^ obfuscator[i % strlen(obfuscator)]; @@ -67,6 +70,6 @@ Java_YOUR_PACKAGE_GOES_HERE_Secrets_getYOUR_KEY_NAME_GOES_HERE( JNIEnv *pEnv, jobject pThis, jstring packageName) { - char obfuscatedSecret[] = {YOUR_KEY_GOES_HERE}; + char obfuscatedSecret[] = YOUR_KEY_GOES_HERE; return getOriginalKey(obfuscatedSecret, sizeof(obfuscatedSecret), packageName, pEnv); } diff --git a/src/main/resources/cpp/sha256.cpp b/src/main/resources/cpp/sha256.cpp index f96aeff..b949a67 100644 --- a/src/main/resources/cpp/sha256.cpp +++ b/src/main/resources/cpp/sha256.cpp @@ -150,20 +150,19 @@ void SHA256::final(unsigned char *digest) } } -const char* sha256(const char* input) +void sha256(const char* input, char buf[2*SHA256::DIGEST_SIZE + 1]) { unsigned char digest[SHA256::DIGEST_SIZE]; - memset(digest,0,SHA256::DIGEST_SIZE); - + memset(digest, 0, SHA256::DIGEST_SIZE); + SHA256 ctx = SHA256(); ctx.init(); ctx.update( (unsigned char*)input, strlen(input)); ctx.final(digest); - - char buf[2*SHA256::DIGEST_SIZE+1]; + buf[2*SHA256::DIGEST_SIZE] = 0; - for (int i = 0; i < SHA256::DIGEST_SIZE; i++) - sprintf(buf+i*2, "%02x", digest[i]); - return buf; + for (int i = 0; i < SHA256::DIGEST_SIZE; i++) { + sprintf(buf + i * 2, "%02x", digest[i]); + } } diff --git a/src/main/resources/cpp/sha256.hpp b/src/main/resources/cpp/sha256.hpp index 8705e05..09641c2 100644 --- a/src/main/resources/cpp/sha256.hpp +++ b/src/main/resources/cpp/sha256.hpp @@ -8,12 +8,12 @@ class SHA256 typedef unsigned int uint32; const static uint32 sha256_k[]; - static const unsigned int SHA224_256_BLOCK_SIZE = (512/8); + static const unsigned int SHA224_256_BLOCK_SIZE = (512 / 8); public: void init(); void update(const unsigned char *message, unsigned int len); void final(unsigned char *digest); - static const unsigned int DIGEST_SIZE = ( 256 / 8); + static const unsigned int DIGEST_SIZE = (256 / 8); protected: void transform(const unsigned char *message, unsigned int block_nb); diff --git a/src/main/resources/kotlin/Secrets.kt b/src/main/resources/kotlin/Secrets.kt index bde86b2..679ffc4 100644 --- a/src/main/resources/kotlin/Secrets.kt +++ b/src/main/resources/kotlin/Secrets.kt @@ -2,8 +2,8 @@ package YOUR_PACKAGE_GOES_HERE class Secrets { - //Method calls will be added by gradle task addObfuscatedKey - //external fun getWellHiddenSecret(packageName: String): String + //Method calls will be added by gradle task hideSecret + //Example : external fun getWellHiddenSecret(packageName: String): String companion object { init { diff --git a/src/test/kotlin/CommandNamesTest.kt b/src/test/kotlin/CommandNamesTest.kt index cc1e3b1..22b5707 100644 --- a/src/test/kotlin/CommandNamesTest.kt +++ b/src/test/kotlin/CommandNamesTest.kt @@ -12,4 +12,5 @@ class CommandNamesTest : StringSpec({ HiddenSecretsPlugin.TASK_HIDE_SECRET shouldBe "hideSecret" HiddenSecretsPlugin.TASK_OBFUSCATE shouldBe "obfuscate" HiddenSecretsPlugin.TASK_PACKAGE_NAME shouldBe "packageName" + HiddenSecretsPlugin.TASK_FIND_KOTLIN_FILE shouldBe "findKotlinFile" }) \ No newline at end of file diff --git a/src/test/kotlin/HiddenSecretsTest.kt b/src/test/kotlin/HiddenSecretsTest.kt index 7ee17af..cb71ffe 100644 --- a/src/test/kotlin/HiddenSecretsTest.kt +++ b/src/test/kotlin/HiddenSecretsTest.kt @@ -52,5 +52,10 @@ class HiddenSecretsTest : WordSpec({ println(result.output) result.output shouldContain packageName } + + "Make command ${HiddenSecretsPlugin.TASK_FIND_KOTLIN_FILE} succeed" { + val result = gradleRunner.withArguments(HiddenSecretsPlugin.TASK_FIND_KOTLIN_FILE).build() + println(result.output) + } } }) \ No newline at end of file diff --git a/src/test/kotlin/UtilsTest.kt b/src/test/kotlin/UtilsTest.kt index 117d67d..2c3bf58 100644 --- a/src/test/kotlin/UtilsTest.kt +++ b/src/test/kotlin/UtilsTest.kt @@ -1,6 +1,7 @@ import com.klaxit.hiddensecrets.Utils import io.kotest.core.spec.style.WordSpec import io.kotest.matchers.shouldBe +import java.io.File /** * Test Utils methods. @@ -11,7 +12,7 @@ class UtilsTest : WordSpec({ "Using getUnderScoredPackageName()" should { "transform package separator" { - Utils.getUnderScoredPackageName(packageName) shouldBe "com_klaxit_test" + Utils.getSnakeCasePackageName(packageName) shouldBe "com_klaxit_test" } } @@ -25,11 +26,42 @@ class UtilsTest : WordSpec({ "Using encodeSecret()" should { "encode String with a seed" { val key = "keyToEncode" - Utils.encodeSecret(key, packageName) shouldBe "{ 0x5b, 0x6, 0x18, 0x31, 0xb, 0x72, 0x57, 0x5, 0x5d, 0x57, 0x3 }" + Utils.encodeSecret( + key, + packageName + ) shouldBe "{ 0x5b, 0x6, 0x18, 0x31, 0xb, 0x72, 0x57, 0x5, 0x5d, 0x57, 0x3 }" } "encode String with special characters" { val key = "@&é(§èçà)-ù,;:=#°_*%£?./+" - Utils.encodeSecret(key, packageName) shouldBe "{ 0x70, 0x45, 0xa2, 0xcc, 0x4c, 0xf5, 0x9e, 0xa5, 0x9a, 0xf0, 0xc1, 0xa6, 0x92, 0x4a, 0x4e, 0xa6, 0x8a, 0x1a, 0xc, 0x5e, 0x5, 0x14, 0xf7, 0x86, 0x6b, 0x13, 0x40, 0xf5, 0x9a, 0xc, 0x16, 0x16, 0x19 }" + Utils.encodeSecret( + key, + packageName + ) shouldBe "{ 0x70, 0x45, 0xa2, 0xcc, 0x4c, 0xf5, 0x9e, 0xa5, 0x9a, 0xf0, 0xc1, 0xa6, 0x92, 0x4a, 0x4e, 0xa6, 0x8a, 0x1a, 0xc, 0x5e, 0x5, 0x14, 0xf7, 0x86, 0x6b, 0x13, 0x40, 0xf5, 0x9a, 0xc, 0x16, 0x16, 0x19 }" + } + } + + "Using getKotlinFilePackage()" should { + "find package name" { + val kotlinFile = File("filename.kt") + kotlinFile.writeText( + "package com.test.activity\n" + + "\n" + + "import android.test.Intent\n" + + "import android.test.Bundle" + ) + val kotlinPackage = Utils.getKotlinFilePackage(kotlinFile) + kotlinFile.delete() + kotlinPackage shouldBe "com.test.activity" + } + "find package name with escaping characters" { + val kotlinFile = File("filename.kt") + kotlinFile.writeText("package com.test.`object`\n" + + "\n" + + "import com.test.Hidden\n" + + "import com.test.constant.NetworkConstants") + val kotlinPackage = Utils.getKotlinFilePackage(kotlinFile) + kotlinFile.delete() + kotlinPackage shouldBe "com.test.object" } } }) \ No newline at end of file