diff --git a/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt b/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt index bd86475..f5d1b3a 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt @@ -61,6 +61,10 @@ object XuqmSDK { @Volatile var bugCollectEnabled: Boolean = false private set + /** 请求签名密钥,从配置文件读取。 */ + @Volatile var signingKey: String? = null + private set + private val pendingInitCallbacks = mutableListOf<() -> Unit>() /** @@ -99,6 +103,7 @@ object XuqmSDK { "Please download the correct config file for this app." ) } + signingKey = configFile.signingKey initialize(context, configFile.appKey, configFile.serverUrl, logLevel) } @@ -119,6 +124,7 @@ object XuqmSDK { Log.w("XuqmSDK", "autoInitializeAsync: package name mismatch, skipping") return } + signingKey = configFile.signingKey // 异步:TokenStore / DeviceId / ApiClient / 远端配置 sdkScope.launch { runCatching { @@ -155,6 +161,8 @@ object XuqmSDK { return@launch } + signingKey = configFile.signingKey + // 2. 初始化 sdk-core initialize(ctx, configFile.appKey, configFile.serverUrl, logLevel) diff --git a/sdk-core/src/main/java/com/xuqm/sdk/core/SDKConfig.kt b/sdk-core/src/main/java/com/xuqm/sdk/core/SDKConfig.kt index dda4a3e..be9b47d 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/core/SDKConfig.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/core/SDKConfig.kt @@ -3,6 +3,7 @@ package com.xuqm.sdk.core data class SDKConfig( val appKey: String, val logLevel: LogLevel = LogLevel.WARN, + val signingKey: String? = null, ) enum class LogLevel { DEBUG, INFO, WARN, ERROR, NONE } diff --git a/sdk-core/src/main/java/com/xuqm/sdk/internal/ConfigFile.kt b/sdk-core/src/main/java/com/xuqm/sdk/internal/ConfigFile.kt index 9ba910d..6e1a49a 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/internal/ConfigFile.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/internal/ConfigFile.kt @@ -16,6 +16,7 @@ internal data class ConfigFile( @SerializedName(value = "harmonyBundleName", alternate = ["harmony_bundle_name"]) val harmonyBundleName: String? = null, @SerializedName(value = "baseUrl", alternate = ["base_url"]) val baseUrl: String? = null, @SerializedName(value = "serverUrl", alternate = ["server_url"]) val serverUrl: String? = null, + @SerializedName(value = "signingKey", alternate = ["signing_key"]) val signingKey: String? = null, @SerializedName(value = "issuedAt", alternate = ["issued_at"]) val issuedAt: String? = null, @SerializedName(value = "expiresAt", alternate = ["expires_at"]) val expiresAt: String? = null, ) diff --git a/sdk-core/src/main/java/com/xuqm/sdk/network/ApiClient.kt b/sdk-core/src/main/java/com/xuqm/sdk/network/ApiClient.kt index 6d4dbba..3e8f1ce 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/network/ApiClient.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/network/ApiClient.kt @@ -1,6 +1,7 @@ package com.xuqm.sdk.network import android.util.Log +import com.xuqm.sdk.XuqmSDK import com.xuqm.sdk.auth.TokenStore import com.xuqm.sdk.core.ServiceEndpointRegistry import com.xuqm.sdk.core.LogLevel @@ -22,7 +23,10 @@ import java.time.Instant import java.time.LocalDateTime import java.time.ZoneOffset import java.time.format.DateTimeFormatter +import java.util.UUID import java.util.concurrent.TimeUnit +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec object ApiClient { @@ -82,6 +86,32 @@ object ApiClient { } else chain.request() chain.proceed(req) } + .addInterceptor { chain -> + val request = chain.request() + val signingKey = XuqmSDK.signingKey + if (signingKey.isNullOrBlank()) { + chain.proceed(request) + } else { + val appKey = XuqmSDK.config.appKey + val timestamp = Instant.now().epochSecond.toString() + val nonce = UUID.randomUUID().toString() + val userId = XuqmSDK.getUserId() ?: "" + val payload = "$appKey\n$userId\n$timestamp\n$nonce" + val signature = hmacSha256(signingKey, payload) + val signedRequest = request.newBuilder() + .header("X-App-Key", appKey) + .header("X-Timestamp", timestamp) + .header("X-Nonce", nonce) + .header("X-Signature", signature) + .apply { + if (userId.isNotBlank()) { + header("X-User-Id", userId) + } + } + .build() + chain.proceed(signedRequest) + } + } .build() synchronized(this) { @@ -89,6 +119,13 @@ object ApiClient { } } + private fun hmacSha256(secret: String, data: String): String { + val mac = Mac.getInstance("HmacSHA256") + mac.init(SecretKeySpec(secret.toByteArray(Charsets.UTF_8), "HmacSHA256")) + val digest = mac.doFinal(data.toByteArray(Charsets.UTF_8)) + return android.util.Base64.encodeToString(digest, android.util.Base64.URL_SAFE or android.util.Base64.NO_PADDING or android.util.Base64.NO_WRAP) + } + fun create(service: Class, baseUrl: String = ServiceEndpointRegistry.controlBaseUrl): T { val retrofit = synchronized(this) { retrofitCache[baseUrl] ?: Retrofit.Builder() diff --git a/sdk-update/src/main/java/com/xuqm/sdk/update/UpdateSDK.kt b/sdk-update/src/main/java/com/xuqm/sdk/update/UpdateSDK.kt index 6f5d92c..7d7bf0c 100644 --- a/sdk-update/src/main/java/com/xuqm/sdk/update/UpdateSDK.kt +++ b/sdk-update/src/main/java/com/xuqm/sdk/update/UpdateSDK.kt @@ -185,6 +185,9 @@ object UpdateSDK { runCatching { val deviceInfo = com.xuqm.sdk.utils.DeviceUtils.getDeviceInfo() + val versionName = try { + context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "" + } catch (_: Exception) { "" } val response = api.checkUpdate( appKey = XuqmSDK.appKey, platform = "ANDROID", @@ -194,6 +197,7 @@ object UpdateSDK { model = deviceInfo["model"], osVersion = deviceInfo["osVersion"], vendor = deviceInfo["vendor"], + currentVersionName = versionName, ) if (response.code == 40404) { throw IllegalStateException("更新服务未开通 (code=40404) [appKey=${XuqmSDK.appKey}]") diff --git a/sdk-update/src/main/java/com/xuqm/sdk/update/api/UpdateApi.kt b/sdk-update/src/main/java/com/xuqm/sdk/update/api/UpdateApi.kt index dc520f2..4cd34e3 100644 --- a/sdk-update/src/main/java/com/xuqm/sdk/update/api/UpdateApi.kt +++ b/sdk-update/src/main/java/com/xuqm/sdk/update/api/UpdateApi.kt @@ -44,5 +44,6 @@ internal interface UpdateApi { @Query("model") model: String? = null, @Query("osVersion") osVersion: String? = null, @Query("vendor") vendor: String? = null, + @Query("currentVersionName") currentVersionName: String? = null, ): ApiResponse }