feat(sdk-bugcollect): Gradle plugin 支持从 config.xuqm 自动解密读取配置

Mode A 项目放置了 assets/xuqm/config.xuqm(PBKDF2+AES-256-GCM 加密),
Gradle plugin 现可直接解密读取 appKey / platformUrl,无需额外的
xuqm.config.json。优先级:config.xuqm > xuqm.config.json > gradle.properties。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-06-16 19:14:49 +08:00
父节点 dff226ae71
当前提交 8db0d353de

查看文件

@ -4,9 +4,55 @@ import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import java.io.File
import java.util.Base64
import javax.crypto.Cipher
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
class XuqmBugCollectPlugin : Plugin<Project> {
private fun base64UrlDecode(s: String): ByteArray {
val padded = s.replace('-', '+').replace('_', '/')
.let { it + "=".repeat((4 - it.length % 4) % 4) }
return Base64.getDecoder().decode(padded)
}
/**
* Read appKey/platformUrl from the SDK Mode A encrypted config file:
* app/src/main/assets/xuqm/config.xuqm
* Format: XUQM-CONFIG-V1.{salt_b64url}.{iv_b64url}.{ciphertext_b64url}
* Decrypted JSON contains: appKey, baseUrl, serverUrl, ...
*/
private fun readEncryptedConfigFile(projectDir: File): Pair<String?, String?> {
val f = File(projectDir, "src/main/assets/xuqm/config.xuqm").takeIf { it.exists() }
?: return null to null
val content = runCatching { f.readText().trim() }.getOrElse { return null to null }
val parts = content.split(".")
if (parts.size != 4 || parts[0] != "XUQM-CONFIG-V1") return null to null
return runCatching {
val salt = base64UrlDecode(parts[1])
val iv = base64UrlDecode(parts[2])
val ciphertext = base64UrlDecode(parts[3])
val passphrase = "xuqm-config-file-v1.2026.internal"
val keySpec = PBEKeySpec(passphrase.toCharArray(), salt, 120_000, 256)
val keyBytes = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(keySpec).encoded
val aesKey = SecretKeySpec(keyBytes, "AES")
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, aesKey, GCMParameterSpec(128, iv))
val json = String(cipher.doFinal(ciphertext))
val appKey = Regex(""""appKey"\s*:\s*"([^"]+)"""").find(json)?.groupValues?.get(1)
val platformUrl = Regex(""""baseUrl"\s*:\s*"([^"]+)"""").find(json)?.groupValues?.get(1)
?: Regex(""""serverUrl"\s*:\s*"([^"]+)"""").find(json)?.groupValues?.get(1)
appKey to platformUrl
}.getOrElse { null to null }
}
/** Read appKey/platformUrl from xuqm.config.json at project root (optional). */
private fun readConfigFile(rootDir: File): Pair<String?, String?> {
val f = File(rootDir, "xuqm.config.json").takeIf { it.exists() } ?: return null to null
@ -19,7 +65,10 @@ class XuqmBugCollectPlugin : Plugin<Project> {
override fun apply(target: Project) {
val android = target.extensions.findByType(AppExtension::class.java) ?: return
val (fileAppKey, filePlatformUrl) = readConfigFile(target.rootDir)
// Priority: config.xuqm (Mode A encrypted) > xuqm.config.json > gradle.properties
val (encAppKey, encPlatformUrl) = readEncryptedConfigFile(target.projectDir)
val (fileAppKey, filePlatformUrl) = if (encAppKey != null) encAppKey to encPlatformUrl
else readConfigFile(target.rootDir)
@Suppress("DEPRECATION")
android.applicationVariants.all { variant ->
@ -30,7 +79,7 @@ class XuqmBugCollectPlugin : Plugin<Project> {
task.group = "xuqm"
task.description = "Upload ProGuard mapping to BugCollect service (${variant.name})"
// Priority: xuqm.config.json > gradle.properties XUQM_APP_KEY > gradle.properties xuqm.appKey
// Priority: config.xuqm > xuqm.config.json > gradle.properties XUQM_APP_KEY > gradle.properties xuqm.appKey
task.appKey.set(
fileAppKey
?: target.findProperty("XUQM_APP_KEY")?.toString()