From 2ae4d728eee5c8914a8d22d34fd0875eadcc79a7 Mon Sep 17 00:00:00 2001 From: HuanCheng65 <22636177+HuanCheng65@users.noreply.github.com> Date: Wed, 19 Jul 2023 16:46:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=8E=B7=E5=8F=96=20`z=5Fid`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/assets/litepal.xml | 2 +- .../com/huanchengfly/tieba/post/Extensions.kt | 2 + .../tieba/post/api/models/SofireResponse.kt | 19 +++ .../post/api/retrofit/RetrofitTiebaApi.kt | 16 +++ .../post/api/retrofit/interfaces/SofireApi.kt | 21 ++++ .../tieba/post/models/database/Account.kt | 2 + .../tieba/post/utils/AccountUtil.kt | 110 +++++++++--------- .../tieba/post/utils/GZIPUtils.kt | 10 ++ .../huanchengfly/tieba/post/utils/RC442.kt | 67 +++++++++++ .../tieba/post/utils/SofireUtils.kt | 92 +++++++++++++++ 10 files changed, 285 insertions(+), 56 deletions(-) create mode 100644 app/src/main/java/com/huanchengfly/tieba/post/api/models/SofireResponse.kt create mode 100644 app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/interfaces/SofireApi.kt create mode 100644 app/src/main/java/com/huanchengfly/tieba/post/utils/GZIPUtils.kt create mode 100644 app/src/main/java/com/huanchengfly/tieba/post/utils/RC442.kt create mode 100644 app/src/main/java/com/huanchengfly/tieba/post/utils/SofireUtils.kt diff --git a/app/src/main/assets/litepal.xml b/app/src/main/assets/litepal.xml index 2ee086c1..88cee974 100644 --- a/app/src/main/assets/litepal.xml +++ b/app/src/main/assets/litepal.xml @@ -1,7 +1,7 @@ - + diff --git a/app/src/main/java/com/huanchengfly/tieba/post/Extensions.kt b/app/src/main/java/com/huanchengfly/tieba/post/Extensions.kt index 4647e3eb..286dc43d 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/Extensions.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/Extensions.kt @@ -75,6 +75,8 @@ fun Any.toJson(): String = Gson().toJson(this) fun String.toMD5(): String = MD5Util.toMd5(this) +fun ByteArray.toMD5(): String = MD5Util.toMd5(this) + fun Context.getColorCompat(@ColorRes id: Int): Int { return ContextCompat.getColor(this, id) } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/api/models/SofireResponse.kt b/app/src/main/java/com/huanchengfly/tieba/post/api/models/SofireResponse.kt new file mode 100644 index 00000000..8e704501 --- /dev/null +++ b/app/src/main/java/com/huanchengfly/tieba/post/api/models/SofireResponse.kt @@ -0,0 +1,19 @@ +package com.huanchengfly.tieba.post.api.models + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SofireResponse( + val data: String, + @SerialName("request_id") + @SerializedName("request_id") + val requestId: Long, + val skey: String +) + +@Serializable +data class SofireResponseData( + val token: String +) \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/RetrofitTiebaApi.kt b/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/RetrofitTiebaApi.kt index d08aa675..228c618b 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/RetrofitTiebaApi.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/RetrofitTiebaApi.kt @@ -25,6 +25,7 @@ import com.huanchengfly.tieba.post.api.retrofit.interfaces.MiniTiebaApi import com.huanchengfly.tieba.post.api.retrofit.interfaces.NewTiebaApi import com.huanchengfly.tieba.post.api.retrofit.interfaces.OfficialProtobufTiebaApi import com.huanchengfly.tieba.post.api.retrofit.interfaces.OfficialTiebaApi +import com.huanchengfly.tieba.post.api.retrofit.interfaces.SofireApi import com.huanchengfly.tieba.post.api.retrofit.interfaces.WebTiebaApi import com.huanchengfly.tieba.post.toJson import com.huanchengfly.tieba.post.utils.AccountUtil @@ -224,6 +225,21 @@ object RetrofitTiebaApi { ) } + val SOFIRE_API: SofireApi by lazy { + Retrofit.Builder() + .baseUrl("https://sofire.baidu.com/") + .addCallAdapterFactory(DeferredCallAdapterFactory()) + .addCallAdapterFactory(FlowCallAdapterFactory.create()) + .addConverterFactory(NullOnEmptyConverterFactory()) + .addConverterFactory(gsonConverterFactory) + .client(OkHttpClient.Builder().apply { +// addInterceptor() + connectionPool(connectionPool) + }.build()) + .build() + .create(SofireApi::class.java) + } + private inline fun createJsonApi( baseUrl: String, vararg interceptors: Interceptor diff --git a/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/interfaces/SofireApi.kt b/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/interfaces/SofireApi.kt new file mode 100644 index 00000000..4db7ba40 --- /dev/null +++ b/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/interfaces/SofireApi.kt @@ -0,0 +1,21 @@ +package com.huanchengfly.tieba.post.api.retrofit.interfaces + +import com.huanchengfly.tieba.post.api.models.SofireResponse +import kotlinx.coroutines.flow.Flow +import okhttp3.RequestBody +import retrofit2.http.Body +import retrofit2.http.HeaderMap +import retrofit2.http.POST +import retrofit2.http.Query +import retrofit2.http.Url + +interface SofireApi { + @POST +// @FormUrlEncoded + fun post( + @Url url: String, + @Query("skey") skey: String, + @Body body: RequestBody, + @HeaderMap headers: Map, + ): Flow +} \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/models/database/Account.kt b/app/src/main/java/com/huanchengfly/tieba/post/models/database/Account.kt index a396c45e..dbf0bf79 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/models/database/Account.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/models/database/Account.kt @@ -25,6 +25,8 @@ data class Account @JvmOverloads constructor( var birthdayTime: String? = null, var constellation: String? = null, var loadSuccess: Boolean = false, + var uuid: String? = "", + var zid: String? = "" ) : LitePalSupport() { val id: Int = 0 } \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/utils/AccountUtil.kt b/app/src/main/java/com/huanchengfly/tieba/post/utils/AccountUtil.kt index 0058074c..5a6e596c 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/utils/AccountUtil.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/utils/AccountUtil.kt @@ -12,13 +12,20 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.staticCompositionLocalOf import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.api.TiebaApi +import com.huanchengfly.tieba.post.api.models.InitNickNameBean +import com.huanchengfly.tieba.post.api.models.LoginBean import com.huanchengfly.tieba.post.arch.GlobalEvent import com.huanchengfly.tieba.post.arch.emitGlobalEvent import com.huanchengfly.tieba.post.models.database.Account import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.zip +import org.litepal.LitePal import org.litepal.LitePal.findAll import org.litepal.LitePal.where +import org.litepal.extension.findAllAsync +import org.litepal.extension.findFirst +import java.util.UUID object AccountUtil { const val TAG = "AccountUtil" @@ -67,6 +74,11 @@ object AccountUtil { return currentAccount } + @JvmStatic + fun getAccountInfo(getter: Account.() -> T): T? { + return currentAccount?.getter() + } + fun newAccount(uid: String, account: Account, callback: (Boolean) -> Unit) { account.saveOrUpdateAsync("uid = ?", uid).listen { mutableAllAccountsState.value = findAll(Account::class.java) @@ -80,7 +92,7 @@ object AccountUtil { @JvmStatic fun getAccountInfoByUid(uid: String): Account? { - return where("uid = ?", uid).findFirst(Account::class.java) + return where("uid = ?", uid).findFirst() } @JvmStatic @@ -103,44 +115,23 @@ object AccountUtil { .putInt("now", id).commit() } - fun fetchAccountFlow(): Flow { - return TiebaApi.getInstance() - .initNickNameFlow() - .zip(TiebaApi.getInstance().loginFlow()) { initNickNameBean, loginBean -> - getLoginInfo()!!.apply { - uid = loginBean.user.id - name = loginBean.user.name - nameShow = initNickNameBean.userInfo.nameShow - portrait = loginBean.user.portrait - tbs = loginBean.anti.tbs - saveOrUpdate("uid = ?", loginBean.user.id) - mutableAllAccountsState.value = findAll(Account::class.java) - } - } + private fun updateAccount( + account: Account, + initNickNameBean: InitNickNameBean, + loginBean: LoginBean, + ) { + account.apply { + uid = loginBean.user.id + name = loginBean.user.name + nameShow = initNickNameBean.userInfo.nameShow + portrait = loginBean.user.portrait + tbs = loginBean.anti.tbs + if (uuid.isNullOrBlank()) uuid = UUID.randomUUID().toString() + } } - fun fetchAccountFlow(account: Account): Flow { - return TiebaApi.getInstance() - .initNickNameFlow( - account.bduss, - account.sToken - ) - .zip( - TiebaApi.getInstance().loginFlow( - account.bduss, - account.sToken - ) - ) { initNickNameBean, loginBean -> - account.apply { - uid = loginBean.user.id - name = loginBean.user.name - nameShow = initNickNameBean.userInfo.nameShow - portrait = loginBean.user.portrait - tbs = loginBean.anti.tbs - saveOrUpdate("uid = ?", loginBean.user.id) - mutableAllAccountsState.value = findAll(Account::class.java) - } - } + fun fetchAccountFlow(account: Account = getLoginInfo()!!): Flow { + return fetchAccountFlow(account.bduss, account.sToken, account.cookie) } fun fetchAccountFlow( @@ -154,25 +145,34 @@ object AccountUtil { getAccountInfoByUid(loginBean.user.id)?.apply { this.bduss = bduss this.sToken = sToken - this.tbs = loginBean.anti.tbs - this.name = loginBean.user.name - this.nameShow = initNickNameBean.userInfo.nameShow - this.portrait = loginBean.user.portrait this.cookie = cookie ?: getBdussCookie(bduss) - saveOrUpdate("uid = ?", loginBean.user.id) - } - ?: Account( - loginBean.user.id, - loginBean.user.name, - bduss, - loginBean.anti.tbs, - loginBean.user.portrait, - sToken, - cookie ?: getBdussCookie(bduss), - initNickNameBean.userInfo.nameShow, - "", - "0" - ) + updateAccount(this, initNickNameBean, loginBean) + } ?: Account( + loginBean.user.id, + loginBean.user.name, + bduss, + loginBean.anti.tbs, + loginBean.user.portrait, + sToken, + cookie ?: getBdussCookie(bduss), + initNickNameBean.userInfo.nameShow, + "", + "0" + ) + } + .zip(SofireUtils.fetchZid()) { account, zid -> + account.apply { this.zid = zid } + } + .onEach { account -> + account.updateAllAsync("uid = ?", account.uid) + .listen { rowAffected -> + if (rowAffected > 0) { + LitePal.findAllAsync() + .listen { + mutableAllAccountsState.value = it + } + } + } } } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/utils/GZIPUtils.kt b/app/src/main/java/com/huanchengfly/tieba/post/utils/GZIPUtils.kt new file mode 100644 index 00000000..1507239e --- /dev/null +++ b/app/src/main/java/com/huanchengfly/tieba/post/utils/GZIPUtils.kt @@ -0,0 +1,10 @@ +package com.huanchengfly.tieba.post.utils + +import java.io.ByteArrayOutputStream +import java.util.zip.GZIPOutputStream + +fun String.gzipCompress(): ByteArray { + val bos = ByteArrayOutputStream() + GZIPOutputStream(bos).bufferedWriter(Charsets.UTF_8).use { it.write(this) } + return bos.toByteArray() +} \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/utils/RC442.kt b/app/src/main/java/com/huanchengfly/tieba/post/utils/RC442.kt new file mode 100644 index 00000000..d0bcfc31 --- /dev/null +++ b/app/src/main/java/com/huanchengfly/tieba/post/utils/RC442.kt @@ -0,0 +1,67 @@ +package com.huanchengfly.tieba.post.utils + +import kotlin.experimental.xor + +class RC442 { + private var x: Int = 0 + private var y: Int = 0 + private var m: ByteArray = ByteArray(256) + + fun setup(key: ByteArray, keyLen: Int = key.size) { + var i: Int + var j: Int + var a: Int + + for (i in 0 until 256) { + m[i] = i.toByte() + } + + j = 0 + var k = 0 + + for (i in 0 until 256) { + if (k >= keyLen) + k = 0 + + a = m[i].toInt() + j = (j + a + key[k].toInt()) and 0xFF + m[i] = m[j] + m[j] = a.toByte() + k++ + } + } + + fun crypt(src: ByteArray, srcLen: Int = src.size): ByteArray { + val dst = ByteArray(src.size) + var x = this.x + var y = this.y + + for (i in 0 until srcLen) { + x = (x + 1) and 0xFF + val a = m[x].toInt() + y = (y + a) and 0xFF + val b = m[y].toInt() + + m[x] = b.toByte() + m[y] = a.toByte() + + dst[i] = (src[i] xor m[(a + b) and 0xFF]) xor 42.toByte() + } + + this.x = x + this.y = y + + return dst + } +} + +fun rc442Crypt( + src: ByteArray, + key: ByteArray, + srcLen: Int = src.size, + keyLen: Int = key.size +): ByteArray { + val rc442 = RC442() + rc442.setup(key, keyLen) + return rc442.crypt(src, srcLen) +} \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/utils/SofireUtils.kt b/app/src/main/java/com/huanchengfly/tieba/post/utils/SofireUtils.kt new file mode 100644 index 00000000..991968fc --- /dev/null +++ b/app/src/main/java/com/huanchengfly/tieba/post/utils/SofireUtils.kt @@ -0,0 +1,92 @@ +package com.huanchengfly.tieba.post.utils + +import android.util.Base64 +import com.huanchengfly.tieba.post.api.models.SofireResponseData +import com.huanchengfly.tieba.post.api.retrofit.RetrofitTiebaApi +import com.huanchengfly.tieba.post.toMD5 +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import java.security.MessageDigest +import java.util.Locale +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import kotlin.random.Random + +@Serializable +data class SofireRequestBody( + @SerialName("module_section") + val moduleSection: List> +) + +fun generateRandomString(length: Int): String { + val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') + return (1..length) + .map { Random.nextInt(0, charPool.size) } + .map(charPool::get) + .joinToString("") +} + +object SofireUtils { + const val DEFAULT_APP_KEY = "200033" + const val DEFAULT_SECRET_KEY = "ea737e4f435b53786043369d2e5ace4f" + + fun fetchZid(): Flow { + val appKey = DEFAULT_APP_KEY + val secKey = DEFAULT_SECRET_KEY + val cuid = "${UIDUtil.uUID.toMD5().uppercase()}|0" + val cuidMd5 = cuid.toMD5().lowercase() + val currTime = "${System.currentTimeMillis() / 1000}" + val reqBody = + Json.encodeToString(SofireRequestBody(listOf(mapOf("zid" to cuid)))).gzipCompress() + val randomKey = generateRandomString(16) + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + val iv = IvParameterSpec(0.toChar().toString().repeat(16).encodeToByteArray()) + cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(randomKey.toByteArray(), "AES"), iv) + val encBody = cipher.doFinal(reqBody) + val reqBodyMd5Digest = MessageDigest.getInstance("MD5").apply { update(reqBody) }.digest() + val finalBody = + (encBody + reqBodyMd5Digest).toRequestBody("application/x-www-form-urlencoded".toMediaType()) + val headers = mapOf( + "Pragma" to "no-cache", + "Accept" to "*/*", + "Accept-Language" to Locale.getDefault().language, + "x-device-id" to cuidMd5, + "x-client-src" to "src", + "User-Agent" to "x6/$appKey/12.35.1.0/4.4.1.3", + "x-sdk-ver" to "sofire/3.5.9.6", + "x-plu-ver" to "x6/4.4.1.3", + "x-app-ver" to "com.baidu.tieba/12.35.1.0", + "x-api-ver" to "33" + ) + val pathMd5 = listOf(appKey, currTime, secKey).joinToString("").toMD5().lowercase() + val skey = Base64.encodeToString( + rc442Crypt(randomKey.encodeToByteArray(), cuidMd5.encodeToByteArray(), 16, 32), + Base64.DEFAULT + ) + val url = "https://sofire.baidu.com/c/11/z/100/$appKey/$currTime/$pathMd5" + return RetrofitTiebaApi.SOFIRE_API + .post(url, skey, finalBody, headers) + .map { + val resSkey = rc442Crypt( + Base64.decode(it.skey, Base64.DEFAULT), + cuidMd5.encodeToByteArray(), + 16, + 32 + ) + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(resSkey, "AES"), iv) + val decode = Base64.decode(it.data, Base64.DEFAULT).dropLast(16).toByteArray() + val json = Json { ignoreUnknownKeys = true } + val decryptData = json.decodeFromString( + cipher.doFinal(decode).decodeToString() + ) + decryptData.token + } + } +} \ No newline at end of file