Skip to content

Commit

Permalink
support for google-services.json
Browse files Browse the repository at this point in the history
  • Loading branch information
DatL4g committed Dec 10, 2024
1 parent 688ddff commit 170d2f6
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 7 deletions.
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ kase-change = { group = "net.pearx.kasechange", name = "kasechange", version.ref
ksp-api = { group = "com.google.devtools.ksp", name = "symbol-processing-api", version.ref = "ksp" }
reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" }
serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core", version.ref = "serialization" }
serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }

[plugins]
android-library = { id = "com.android.library", version.ref = "android" }
Expand Down
1 change: 1 addition & 0 deletions sekret-gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies {
implementation(libs.kotlin.poet)
implementation(libs.kase.change)
implementation(libs.serialization)
implementation(libs.serialization.json)
}

val generateVersion = tasks.create("generateVersion") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ open class PropertiesExtension(objectFactory: ObjectFactory) {
*/
open val propertiesFile: RegularFileProperty = objectFactory.fileProperty()

/**
* Change where the google-services.json file for your secrets is located.
* Default is "google-services.json" in the **androidMain** target of the module the plugin is applied.
*/
open val googleServicesFile: RegularFileProperty = objectFactory.fileProperty()

/**
* Configuration for handling copy process of native binaries.
*/
Expand Down Expand Up @@ -65,6 +71,7 @@ open class PropertiesExtension(objectFactory: ObjectFactory) {
})
encryptionKey.convention(packageName)
propertiesFile.convention(project.layout.projectDirectory.file(sekretFileName))
googleServicesFile.convention(project.layout.projectDirectory.file("src/androidMain/$googleServicesFileName"))

nativeCopy = NativeCopyExtension(project.objects).also {
it.setupConvention(project)
Expand Down Expand Up @@ -100,5 +107,6 @@ open class PropertiesExtension(objectFactory: ObjectFactory) {
companion object {
internal const val sekretFileName = "sekret.properties"
internal const val sekretPackageName = "dev.datlag.sekret"
internal const val googleServicesFileName = "google-services.json"
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.datlag.sekret.gradle.helper

import dev.datlag.sekret.gradle.EncodedProperty
import dev.datlag.sekret.gradle.model.GoogleServices
import net.pearx.kasechange.toCamelCase
import net.pearx.kasechange.universalWordSplitter
import java.security.MessageDigest
Expand All @@ -10,17 +11,80 @@ import kotlin.experimental.xor
object Encoder {

fun encodeProperties(
properties: Properties,
properties: Properties?,
googleServices: GoogleServices?,
password: String
): Iterable<EncodedProperty> {
val props = mutableSetOf<EncodedProperty>()

properties.entries.forEach { entry ->
properties?.entries?.forEach { entry ->
val keyName = (entry.key as String).toCamelCase(universalWordSplitter(treatDigitsAsUppercase = false)).trim()
val secretValue = encode(entry.value as String, password)

props.add(EncodedProperty(keyName, secretValue))
}
googleServices?.let { json ->
props.add(EncodedProperty(
key = GoogleServices.PROJECT_ID_KEY.toCamelCase(universalWordSplitter(treatDigitsAsUppercase = false)).trim(),
secret = encode(json.project.id, password)
))
json.project.number?.let {
props.add(EncodedProperty(
key = GoogleServices.PROJECT_NUMBER_KEY.toCamelCase(universalWordSplitter(treatDigitsAsUppercase = false)).trim(),
secret = encode(it, password)
))
}
json.project.firebaseUrl?.let {
props.add(EncodedProperty(
key = GoogleServices.PROJECT_FIREBASE_URL_KEY.toCamelCase(universalWordSplitter(treatDigitsAsUppercase = false)).trim(),
secret = encode(it, password)
))
}
json.project.storageBucket?.let {
props.add(EncodedProperty(
key = GoogleServices.PROJECT_STORAGE_BUCKET_KEY.toCamelCase(universalWordSplitter(treatDigitsAsUppercase = false)).trim(),
secret = encode(it, password)
))
}
json.firebase?.let { firebase ->
firebase.info?.appId?.let {
props.add(EncodedProperty(
key = GoogleServices.FIREBASE_APP_ID_KEY.toCamelCase(universalWordSplitter(treatDigitsAsUppercase = false)).trim(),
secret = encode(it, password)
))
}
firebase.currentApiKey?.let {
props.add(EncodedProperty(
key = GoogleServices.FIREBASE_API_KEY_KEY.toCamelCase(universalWordSplitter(treatDigitsAsUppercase = false)).trim(),
secret = encode(it, password)
))
}
firebase.androidClient?.clientId?.let {
props.add(EncodedProperty(
key = GoogleServices.FIREBASE_ANDROID_ID_KEY.toCamelCase(universalWordSplitter(treatDigitsAsUppercase = false)).trim(),
secret = encode(it, password)
))
}
firebase.iOSClient?.clientId?.let {
props.add(EncodedProperty(
key = GoogleServices.FIREBASE_IOS_ID_KEY.toCamelCase(universalWordSplitter(treatDigitsAsUppercase = false)).trim(),
secret = encode(it, password)
))
}
firebase.webClientOrFirebaseAuth?.clientId?.let {
props.add(EncodedProperty(
key = GoogleServices.FIREBASE_WEB_OR_AUTH_ID_KEY.toCamelCase(universalWordSplitter(treatDigitsAsUppercase = false)).trim(),
secret = encode(it, password)
))
}
firebase.adminClient?.clientId?.let {
props.add(EncodedProperty(
key = GoogleServices.FIREBASE_ADMIN_ID_KEY.toCamelCase(universalWordSplitter(treatDigitsAsUppercase = false)).trim(),
secret = encode(it, password)
))
}
}
}

return props
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package dev.datlag.sekret.gradle.model

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.gradle.api.logging.LogLevel
import org.gradle.api.logging.Logger
import java.io.File

@Serializable
data class GoogleServices(
@SerialName("project_info") val project: Project,
@SerialName("client") val client: List<Client>
) {
@Transient
val firebase: Client? = client.maxByOrNull {
it.firebaseFactor
} ?: client.firstOrNull()

@Serializable
data class Project(
@SerialName("project_id") val id: String,
@SerialName("project_number") private val _number: String,
@SerialName("firebase_url") private val _firebaseUrl: String? = null,
@SerialName("storage_bucket") private val _storageBucket: String? = null,
) {
@Transient
val number = _number.ifBlank { null }

@Transient
val firebaseUrl = _firebaseUrl?.ifBlank { null }

@Transient
val storageBucket = _storageBucket?.ifBlank { null }
}

@Serializable
data class Client(
@SerialName("client_info") val info: Info? = null,
@SerialName("oauth_client") val oauthClient: List<OAuthClient> = emptyList(),
@SerialName("api_key") val apiKey: List<ApiKey> = emptyList(),
@SerialName("services") val services: Services? = null
) {
@Transient
val androidClient: OAuthClient? = clientOfType(1)

@Transient
val iOSClient: OAuthClient? = clientOfType(2)

@Transient
val webClientOrFirebaseAuth: OAuthClient? = clientOfType(3)

@Transient
val adminClient: OAuthClient? = clientOfType(4)

@Transient
val currentApiKey: String? = apiKey.firstNotNullOfOrNull { it.current }

@Transient
internal val firebaseFactor: Int = listOfNotNull(
androidClient?.let { 1 },
iOSClient?.let { 1 },
webClientOrFirebaseAuth?.let { 2 },
adminClient?.let { 1 }
).sum()

private fun clientOfType(type: Int): OAuthClient? = oauthClient.firstOrNull {
it.clientType == type
} ?: services?.appInvite?.otherPlatformOAuthClient?.firstOrNull {
it.clientType == type
}

@Serializable
data class Info(
@SerialName("mobilesdk_app_id") private val _appId: String?,
) {
@Transient
val appId: String? = _appId?.ifBlank { null }
}

@Serializable
data class OAuthClient(
@SerialName("client_id") private val _clientId: String?,
@SerialName("client_type") val clientType: Int = -1,
) {
@Transient
val clientId: String? = _clientId?.ifBlank { null }
}

@Serializable
data class ApiKey(
@SerialName("current_key") private val _current: String? = null,
) {
@Transient
val current: String? = _current?.ifBlank { null }
}

@Serializable
data class Services(
@SerialName("appinvite_service") val appInvite: AppInvite? = null,
) {
@Serializable
data class AppInvite(
@SerialName("other_platform_oauth_client") val otherPlatformOAuthClient: List<OAuthClient> = emptyList(),
)
}
}

companion object {
internal const val PROJECT_ID_KEY = "PROJECT_ID"
internal const val PROJECT_NUMBER_KEY = "PROJECT_NUMBER"
internal const val PROJECT_FIREBASE_URL_KEY = "PROJECT_FIREBASE_URL"
internal const val PROJECT_STORAGE_BUCKET_KEY = "PROJECT_STORAGE_BUCKET"

internal const val FIREBASE_APP_ID_KEY = "FIREBASE_APP_ID"
internal const val FIREBASE_ANDROID_ID_KEY = "FIREBASE_ANDROID_ID"
internal const val FIREBASE_IOS_ID_KEY = "FIREBASE_IOS_ID"
internal const val FIREBASE_WEB_OR_AUTH_ID_KEY = "FIREBASE_WEB_OR_AUTH_ID"
internal const val FIREBASE_ADMIN_ID_KEY = "FIREBASE_ADMIN_ID"
internal const val FIREBASE_API_KEY_KEY = "FIREBASE_API_KEY"

private val json = Json {
ignoreUnknownKeys = true
isLenient = true
}

@OptIn(ExperimentalSerializationApi::class)
fun from(file: File, logger: Logger): GoogleServices? {
return file.inputStream().use { inputStream ->
runCatching {
json.decodeFromStream<GoogleServices>(inputStream)
}.onFailure {
logger.log(LogLevel.ERROR, "Seems like your google-services.json is malformed, could not parse.", it)
}.getOrNull()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ open class CreateSekretValueTask : DefaultTask() {

val name = key.orNull ?: throw IllegalArgumentException("Missing property 'key'")
val data = value.orNull ?: throw IllegalArgumentException("Missing property 'value'")
val propFile = propertiesFile.asFile.orNull ?: throw IllegalStateException("No sekret properties file found.")
val propFile = propertiesFile.orNull?.asFile ?: throw IllegalStateException("No sekret properties file found.")
val properties = Utils.propertiesFromFile(propFile)

properties[name] = data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import dev.datlag.sekret.gradle.generator.ModuleGenerator
import dev.datlag.sekret.gradle.generator.SekretGenerator
import dev.datlag.sekret.gradle.helper.Encoder
import dev.datlag.sekret.gradle.helper.Utils
import dev.datlag.sekret.gradle.model.GoogleServices
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
Expand Down Expand Up @@ -49,6 +50,9 @@ open class GenerateSekretTask : DefaultTask() {
@get:InputFile
open val propertiesFile: RegularFileProperty = project.objects.fileProperty()

@get:InputFile
open val googleServicesFile: RegularFileProperty = project.objects.fileProperty()

private val outputDir: File
get() = outputDirectory.asFile.orNull
?: projectLayout.projectDirectory.dir("sekret").asFile
Expand Down Expand Up @@ -89,15 +93,26 @@ open class GenerateSekretTask : DefaultTask() {
targets = requiredTargets
)

val propFile = propertiesFile.asFile.orNull ?: throw IllegalStateException("No sekret properties file found.")
val properties = Utils.propertiesFromFile(propFile)
val propFile = propertiesFile.orNull?.asFile
val googleServicesFile = googleServicesFile.orNull?.asFile

if (propFile == null && googleServicesFile == null) {
throw IllegalStateException("No sekret.properties file or google-services.json found.")
}

val properties = propFile?.let(Utils::propertiesFromFile)
val googleServices = googleServicesFile?.let { GoogleServices.from(it, logger) }

if (properties == null && googleServices == null) {
throw IllegalStateException("No sekret.properties file or google-services.json could not be parsed.")
}

val generator = SekretGenerator.createAllForTargets(
packageName = packageName.getOrElse(PropertiesExtension.sekretPackageName),
structure = structure
)

val encodedProperties = Encoder.encodeProperties(properties, encryptionKey.get())
val encodedProperties = Encoder.encodeProperties(properties, googleServices, encryptionKey.get())
SekretGenerator.generate(encodedProperties, *generator.toTypedArray())
}

Expand All @@ -118,10 +133,30 @@ open class GenerateSekretTask : DefaultTask() {
return null
}

return resolveFile(config.propertiesFile.asFile.getOrElse(project.file(PropertiesExtension.sekretFileName)))
return resolveFile(config.propertiesFile.orNull?.asFile ?: project.file(PropertiesExtension.sekretFileName))
?: resolveFile(project.projectDir)
}

private fun googleServicesFile(project: Project, config: PropertiesExtension): File? {
val defaultName = PropertiesExtension.googleServicesFileName

fun resolveFile(file: File): File? {
if (file.existsSafely() && file.canReadSafely()) {
val googleServicesFile = if (file.isDirectorySafely()) {
File(file, defaultName)
} else {
file
}
if (googleServicesFile.existsSafely() && googleServicesFile.canReadSafely()) {
return googleServicesFile
}
}
return null
}

return config.googleServicesFile.orNull?.asFile?.let(::resolveFile)
}

fun apply(project: Project, extension: SekretPluginExtension = project.sekretExtension) {
enabled.set(extension.properties.enabled)
packageName.set(extension.properties.packageName)
Expand All @@ -134,6 +169,7 @@ open class GenerateSekretTask : DefaultTask() {
encryptionKey.set(extension.properties.encryptionKey)
outputDirectory.set(project.findProject("sekret")?.projectDir ?: File(project.projectDir, "sekret"))
propertiesFile.set(propertiesFile(project, extension.properties))
googleServicesFile.set(googleServicesFile(project, extension.properties))
}

companion object {
Expand Down

0 comments on commit 170d2f6

Please sign in to comment.