diff --git a/CHANGELOG.md b/CHANGELOG.md index eb4362d..bea4b9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# 0.2.0 +### Improvements +* Ability to re-generate secrets from .properties file +* Group Gradle tasks and their descriptions to convenience command `tasks` +* Add a blank space after all // in line comments +#### Contributors +@valydia +@marius-m +@michpohl # 0.1.5 ### Fixes * Fix `UnsatisfiedLinkError` when package name has underscores. Reported in https://github.com/klaxit/hidden-secrets-gradle-plugin/issues/52 @@ -17,7 +26,7 @@ * Fix call to `customDecode()` in C++ code ### Improvements * jcenter dependency removed -* Moving up to gradle 6.8.3 +* Moving up to gradle 6.8.3 * Various libraries update # 0.1.1 ### Fixes @@ -37,4 +46,4 @@ 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) # 0.1.0 -* First release +* First release \ No newline at end of file diff --git a/README.md b/README.md index 6fe4486..7ddf4af 100644 --- a/README.md +++ b/README.md @@ -119,23 +119,54 @@ This method is automatically called and will revert the rot13 applied on your ke Secrets().getYourSecretKeyName(packageName) ``` -## Other available commands +# Going further +## Generate secrets from properties file +You can generate secrets from properties file as well. -### Copy files +### Use case +If you are using CI system to provide secrets, that are not hard-coded into the repository itself. It will re-generate those secrets in the app and build it. + +This is useful if you want to split production keys from repository itself, thus increasing security in your project repository. + +### Setting up +1. Create a new properties file in root project directory. + +``` shell +credentials.properties +``` + +2. Fill in wanted secrets. For ex.: + +``` java-properties +keyName1=yourKeyToObfuscate1 +keyName2=yourKeyToObfuscate2 +``` + +3. Run + +``` shell +./gradlew hideSecretFromPropertiesFile -PpropertiesFileName=credentials.properties +``` + +It will regenerate all secret files in the project and update all secrets from the properties file. + +# Other available commands + +## Copy files Copy required files to your project : ```shell ./gradlew copyCpp ./gradlew copyKotlin [-Ppackage=your.package.name] ``` -### Obfuscate +## Obfuscate Create an obfuscated key and display it : ```shell ./gradlew obfuscate -Pkey=yourKeyToObfuscate [-Ppackage=com.your.package] ``` This command can be useful if you modify your app's package name based on `buildTypes` configuration. With this command you can get the obfuscated key for a different package name and manually integrate it in another function in `secrets.cpp`. -## Development +# Development Pull Requests are very welcome! @@ -146,10 +177,10 @@ Before opening a PR : - `./gradlew test` must succeed - `./gradlew detekt` must succeed to avoid any style issue -## Authors +# Authors See the list of [contributors](https://github.com/klaxit/hidden-secrets-gradle-plugin/contributors) who participated in this project. -## License +# License Please see [LICENSE](https://github.com/klaxit/hidden-secrets-gradle-plugin/blob/master/LICENSE) diff --git a/settings.gradle.kts b/settings.gradle.kts index f8e245a..e2236bb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,6 @@ rootProject.name = "HiddenSecretsPlugin" gradle.allprojects { - group = "com.klaxit.hiddensecrets" - version = "0.1.5" -} + group = "com.klaxit.hiddensecrets" + version = "0.2.0" +} \ No newline at end of file diff --git a/src/main/kotlin/com/klaxit/hiddensecrets/HiddenSecretsPlugin.kt b/src/main/kotlin/com/klaxit/hiddensecrets/HiddenSecretsPlugin.kt index 388525d..c94e5db 100644 --- a/src/main/kotlin/com/klaxit/hiddensecrets/HiddenSecretsPlugin.kt +++ b/src/main/kotlin/com/klaxit/hiddensecrets/HiddenSecretsPlugin.kt @@ -11,6 +11,7 @@ import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction import java.io.File import java.nio.charset.Charset +import java.util.Properties /** * Available gradle tasks from HiddenSecretsPlugin @@ -24,22 +25,28 @@ open class HiddenSecretsPlugin : Plugin { private const val KOTLIN_FILE_NAME = "Secrets.kt" // Tasks + const val TASK_GROUP = "Hide secrets" const val TASK_UNZIP_HIDDEN_SECRETS = "unzipHiddenSecrets" const val TASK_COPY_CPP = "copyCpp" const val TASK_COPY_KOTLIN = "copyKotlin" const val TASK_HIDE_SECRET = "hideSecret" + const val TASK_HIDE_SECRET_FROM_PROPERTIES_FILE = "hideSecretFromPropertiesFile" const val TASK_OBFUSCATE = "obfuscate" const val TASK_PACKAGE_NAME = "packageName" const val TASK_FIND_KOTLIN_FILE = "findKotlinFile" // Properties - private const val KEY = "key" - private const val KEY_NAME = "keyName" - private const val PACKAGE = "package" + private const val PROP_KEY = "key" + private const val PROP_KEY_NAME = "keyName" + private const val PROP_PACKAGE = "package" + private const val PROP_FILE_NAME = "propertiesFileName" // 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'" + + // Sample usage + private const val SAMPLE_FROM_PROPS = "-P${PROP_FILE_NAME}=credentials.properties" } override fun apply(project: Project) { @@ -64,9 +71,9 @@ open class HiddenSecretsPlugin : Plugin { @Input fun getKeyParam(): String { val key: String - if (project.hasProperty(KEY)) { + if (project.hasProperty(PROP_KEY)) { // From command line - key = project.property(KEY) as String + key = project.property(PROP_KEY) as String } else { throw InvalidUserDataException(ERROR_EMPTY_KEY) } @@ -79,9 +86,9 @@ open class HiddenSecretsPlugin : Plugin { @Input fun getPackageNameParam(): String { var packageName: String? = null - if (project.hasProperty(PACKAGE)) { + if (project.hasProperty(PROP_PACKAGE)) { // From command line - packageName = project.property(PACKAGE) as String? + packageName = project.property(PROP_PACKAGE) as String? } if (packageName.isNullOrEmpty()) { // From Android app @@ -93,6 +100,48 @@ open class HiddenSecretsPlugin : Plugin { return packageName } + /** + * Get properties file path to hide secrets from + */ + @Input + fun getPropertiesFile(): File { + return if (project.hasProperty(PROP_FILE_NAME)) { + val propsPathRaw = project.property(PROP_FILE_NAME) + if (propsPathRaw != null) { + File(project.rootDir, propsPathRaw as String) + } else { + throw IllegalArgumentException( + "Cannot find properties (${propsPathRaw})!" + + " Use: '${SAMPLE_FROM_PROPS}'" + ) + } + } else { + throw IllegalArgumentException( + "Properties file is not defined!" + + " Use: '${SAMPLE_FROM_PROPS}'" + ) + } + } + + /** + * Get properties from the provided file + * @throws IllegalArgumentException no props found in project + */ + @Throws(IllegalArgumentException::class) + fun getPropertiesFromFile(propsFile: File): Properties { + if (!propsFile.exists()) { + throw IllegalArgumentException( + "Cannot find properties (${propsFile.absolutePath})!" + + " Use: '${SAMPLE_FROM_PROPS}'" + ) + } + return Properties().apply { + propsFile.inputStream().use { + load(it) // Does not support UTF-8 characters + } + } + } + /** * Get key name param from command line */ @@ -101,9 +150,9 @@ open class HiddenSecretsPlugin : Plugin { 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)) { + if (project.hasProperty(PROP_KEY_NAME)) { // From command line - keyName = project.property(KEY_NAME) as String + keyName = project.property(PROP_KEY_NAME) as String } else { println("Key name has been randomized, chose your own key name by adding argument '-PkeyName=yourName'") } @@ -145,6 +194,15 @@ open class HiddenSecretsPlugin : Plugin { return project.file(path) } + /** + * @return empty template file for [KOTLIN_FILE_NAME] + */ + fun tmpKotlinFile(): File { + return project.file("$tmpFolder/kotlin/").listFiles() + ?.first { it.name == KOTLIN_FILE_NAME } + ?: throw IllegalStateException("Did not find temporary template for secrets!") + } + /** * If found, returns the Secrets.kt file in the Android app */ @@ -153,12 +211,13 @@ open class HiddenSecretsPlugin : Plugin { } /** - * Copy Cpp files from the lib to the Android project if they don't exist yet + * Copy Cpp files from the lib to the Android project + * @param overwrite whether to overwrite existing files */ - fun copyCppFiles() { + fun copyCppFiles(overwrite: Boolean = false) { project.file("$tmpFolder/cpp/").listFiles()?.forEach { val destination = getCppDestination(it.name) - if (destination.exists()) { + if (!overwrite && destination.exists()) { println("${it.name} already exists") } else { println("Copy $it.name to\n$destination") @@ -168,23 +227,92 @@ open class HiddenSecretsPlugin : Plugin { } /** - * Copy Kotlin file Secrets.kt from the lib to the Android project if it does not exist yet + * Copy Kotlin file Secrets.kt from the lib to the Android project + * @param overwrite whether to overwrite existing files */ - fun copyKotlinFile() { - getKotlinFile()?.let { - println("$KOTLIN_FILE_NAME already exists") - return + fun copyKotlinFile(overwrite: Boolean = false) { + val existingKotlinFile: File? = getKotlinFile() + if (existingKotlinFile != null) { + if (overwrite) { + println("Overwriting existing $KOTLIN_FILE_NAME.") + tmpKotlinFile().copyTo(existingKotlinFile, true) + } else { + println("$KOTLIN_FILE_NAME already exists") + return + } + } else { + val packageName = getPackageNameParam() + project.file("$tmpFolder/kotlin/").listFiles()?.forEach { + val destination = getKotlinDestination(packageName, it.name) + if (destination.exists()) { + println("${it.name} already exists") + } else { + println("Copy $it.name to\n$destination") + it.copyTo(destination, true) + } + } } - val packageName = getPackageNameParam() - project.file("$tmpFolder/kotlin/").listFiles()?.forEach { - val destination = getKotlinDestination(packageName, it.name) - if (destination.exists()) { - println("${it.name} already exists") + } + + /** + * Main method of the project: add Cpp and Kotlin files to your project if necessary, + * obfuscate your secret key and add it to your project. + */ + fun hideSecret( + keyName: String, + packageName: String, + obfuscatedKey: String + ) { + // 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++ !") + } + if (text.contains(KEY_PLACEHOLDER)) { + // Edit placeholder key + // Replace package name + text = text.replace(PACKAGE_PLACEHOLDER, Utils.getCppPackageName(kotlinPackage)) + // Replace key name + text = text.replace("YOUR_KEY_NAME_GOES_HERE", keyName) + // Replace demo key + text = text.replace(KEY_PLACEHOLDER, obfuscatedKey) + secretsCpp.writeText(text) } else { - println("Copy $it.name to\n$destination") - it.copyTo(destination, true) + // Add new key + text += CodeGenerator.getCppCode(kotlinPackage, keyName, obfuscatedKey) + secretsCpp.writeText(text) } + } else { + error("Missing C++ file, please run gradle task : $TASK_COPY_CPP") } + println("✅ You can now get your secret key by calling : Secrets().get$keyName(packageName)") } /** @@ -199,13 +327,18 @@ open class HiddenSecretsPlugin : Plugin { println("Unzip jar to $tmpFolder") copy.into(tmpFolder) } - }) + }).apply { + this.group = TASK_GROUP + this.description = "Unzip plugin into tmp directory" + } /** * Copy C++ files to your project */ project.task(TASK_COPY_CPP) { + this.group = TASK_GROUP + this.description = "Copy C++ files to your project" doLast { copyCppFiles() } @@ -216,6 +349,8 @@ open class HiddenSecretsPlugin : Plugin { */ project.task(TASK_COPY_KOTLIN) { + this.group = TASK_GROUP + this.description = "Copy Kotlin file to your project" doLast { copyKotlinFile() } @@ -226,6 +361,8 @@ open class HiddenSecretsPlugin : Plugin { */ project.task(TASK_OBFUSCATE) { + this.group = TASK_GROUP + this.description = "Get an obfuscated key from command line" doLast { getObfuscatedKey() } @@ -236,6 +373,8 @@ open class HiddenSecretsPlugin : Plugin { */ project.task(TASK_HIDE_SECRET) { + this.group = TASK_GROUP + this.description = "Obfuscate a key and add it to your Android project" dependsOn(TASK_UNZIP_HIDDEN_SECRETS) doLast { @@ -249,56 +388,33 @@ open class HiddenSecretsPlugin : Plugin { 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 - } + hideSecret(keyName, packageName, obfuscatedKey) + } + } - // 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++ !") - } - if (text.contains(KEY_PLACEHOLDER)) { - // Edit placeholder key - // Replace package name - text = text.replace(PACKAGE_PLACEHOLDER, Utils.getCppPackageName(kotlinPackage)) - // Replace key name - text = text.replace("YOUR_KEY_NAME_GOES_HERE", keyName) - // Replace demo key - text = text.replace(KEY_PLACEHOLDER, obfuscatedKey) - secretsCpp.writeText(text) - } else { - // Add new key - text += CodeGenerator.getCppCode(kotlinPackage, keyName, obfuscatedKey) - secretsCpp.writeText(text) - } - } else { - error("Missing C++ file, please run gradle task : $TASK_COPY_CPP") + /** + * Clean all secret hidden keys in your project and obfuscate all keys from the properties file. + */ + project.task(TASK_HIDE_SECRET_FROM_PROPERTIES_FILE) + { + this.group = TASK_GROUP + this.description = "Re-generate and obfuscate keys from properties file and add it to your Android project" + dependsOn(TASK_UNZIP_HIDDEN_SECRETS) + + doLast { + // Create a clean copy of dependency files + copyCppFiles(true) + copyKotlinFile(true) + + 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) + hideSecret(keyName, packageName, obfuscatedKey) } - println("✅ You can now get your secret key by calling : Secrets().get$keyName(packageName)") } } @@ -307,6 +423,8 @@ open class HiddenSecretsPlugin : Plugin { */ project.task(TASK_PACKAGE_NAME) { + this.group = TASK_GROUP + this.description = "Print the package name of the app" doLast { println("APP PACKAGE NAME = " + getPackageNameParam()) } @@ -316,6 +434,8 @@ open class HiddenSecretsPlugin : Plugin { * Find Secrets.kt file in the project */ project.task(TASK_FIND_KOTLIN_FILE) { + this.group = TASK_GROUP + this.description = "Find Secrets.kt file in the project" doLast { getKotlinFile() } diff --git a/src/test/kotlin/CommandNamesTest.kt b/src/test/kotlin/CommandNamesTest.kt index 27dd1e0..d7fb569 100644 --- a/src/test/kotlin/CommandNamesTest.kt +++ b/src/test/kotlin/CommandNamesTest.kt @@ -10,6 +10,7 @@ class CommandNamesTest : StringSpec({ HiddenSecretsPlugin.TASK_COPY_CPP shouldBe "copyCpp" HiddenSecretsPlugin.TASK_COPY_KOTLIN shouldBe "copyKotlin" HiddenSecretsPlugin.TASK_HIDE_SECRET shouldBe "hideSecret" + HiddenSecretsPlugin.TASK_HIDE_SECRET_FROM_PROPERTIES_FILE shouldBe "hideSecretFromPropertiesFile" HiddenSecretsPlugin.TASK_OBFUSCATE shouldBe "obfuscate" HiddenSecretsPlugin.TASK_PACKAGE_NAME shouldBe "packageName" HiddenSecretsPlugin.TASK_FIND_KOTLIN_FILE shouldBe "findKotlinFile"