Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package org.ntqqrev.acidify.common.android

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.ntqqrev.acidify.common.AppInfo

@Serializable
class AndroidAppInfo(
@SerialName("Os") val os: String,
@SerialName("Kernel") val kernel: String = "",
@SerialName("VendorOs") val vendorOs: String,
@SerialName("Qua") val qua: String,
@SerialName("CurrentVersion") val currentVersion: String,
@SerialName("PtVersion") val ptVersion: String,
@SerialName("SsoVersion") val ssoVersion: Int,
@SerialName("PackageName") val packageName: String,
@SerialName("ApkSignatureMd5") val apkSignatureMd5: ByteArray,
@SerialName("SdkInfo") val sdkInfo: WtLoginSdkInfo,
@SerialName("AppId") val appId: Int,
@SerialName("SubAppId") val subAppId: Int,
@SerialName("AppClientVersion") val appClientVersion: Int,
) {
@Serializable
class WtLoginSdkInfo(
@SerialName("SdkBuildTime") val sdkBuildTime: Long,
@SerialName("SdkVersion") val sdkVersion: String,
@SerialName("MiscBitMap") val miscBitMap: Long,
@SerialName("SubSigMap") val subSigMap: Long,
@SerialName("MainSigMap") val mainSigMap: Long,
)

object Bundled {
val AndroidPhone = AndroidAppInfo(
os = "Android",
vendorOs = "android",
qua = "V1_AND_SQ_9.1.60_11520_YYB_D",
currentVersion = "9.1.60.045f5d19",
ptVersion = "9.1.60",
ssoVersion = 22,
packageName = "com.tencent.mobileqq",
apkSignatureMd5 = "a6b745bf24a2c277527716f6f36eb68d".hexToByteArray(),
sdkInfo = WtLoginSdkInfo(
sdkBuildTime = 1740483688,
sdkVersion = "6.0.0.2568",
miscBitMap = 150470524,
subSigMap = 66560,
mainSigMap = Sig.WLOGIN_A5 or Sig.WLOGIN_RESERVED or Sig.WLOGIN_STWEB or Sig.WLOGIN_A2 or Sig.WLOGIN_ST
or Sig.WLOGIN_LSKEY or Sig.WLOGIN_SKEY or Sig.WLOGIN_SIG64 or Sig.WLOGIN_VKEY or Sig.WLOGIN_D2
or Sig.WLOGIN_SID or Sig.WLOGIN_PSKEY or Sig.WLOGIN_AQSIG or Sig.WLOGIN_LHSIG or Sig.WLOGIN_PAYTOKEN
or 65536L
),
appId = 16,
subAppId = 537275636,
appClientVersion = 0
)

val AndroidPad = AndroidAppInfo(
os = "ANDROID",
vendorOs = "android",
qua = "V1_AND_SQ_9.2.20_11650_YYB_D",
currentVersion = "9.2.20.777b5929",
ptVersion = "9.2.20",
ssoVersion = 22,
packageName = "com.tencent.mobileqq",
apkSignatureMd5 = "a6b745bf24a2c277527716f6f36eb68d".hexToByteArray(),
sdkInfo = WtLoginSdkInfo(
sdkBuildTime = 1757058014,
sdkVersion = "6.0.0.2589",
miscBitMap = 150470524,
subSigMap = 66560,
mainSigMap = Sig.WLOGIN_A5 or Sig.WLOGIN_RESERVED or Sig.WLOGIN_STWEB or Sig.WLOGIN_A2 or Sig.WLOGIN_ST
or Sig.WLOGIN_LSKEY or Sig.WLOGIN_SKEY or Sig.WLOGIN_SIG64 or Sig.WLOGIN_VKEY or Sig.WLOGIN_D2
or Sig.WLOGIN_SID or Sig.WLOGIN_PSKEY or Sig.WLOGIN_AQSIG or Sig.WLOGIN_LHSIG or Sig.WLOGIN_PAYTOKEN
or 65536L
),
appId = 16,
subAppId = 537315825,
appClientVersion = 0
)
}

object Sig {
const val WLOGIN_A5: Long = 1 shl 1
const val WLOGIN_RESERVED: Long = 1 shl 4
const val WLOGIN_STWEB: Long = 1 shl 5
const val WLOGIN_A2: Long = 1 shl 6
const val WLOGIN_ST: Long = 1 shl 7
const val WLOGIN_LSKEY: Long = 1 shl 9
const val WLOGIN_SKEY: Long = 1 shl 12
const val WLOGIN_SIG64: Long = 1 shl 13
const val WLOGIN_OPENKEY: Long = 1 shl 14
const val WLOGIN_TOKEN: Long = 1 shl 15
const val WLOGIN_VKEY: Long = 1 shl 17
const val WLOGIN_D2: Long = 1 shl 18
const val WLOGIN_SID: Long = 1 shl 19
const val WLOGIN_PSKEY: Long = 1 shl 20
const val WLOGIN_AQSIG: Long = 1 shl 21
const val WLOGIN_LHSIG: Long = 1 shl 22
const val WLOGIN_PAYTOKEN: Long = 1 shl 23
const val WLOGIN_PF: Long = 1 shl 24
const val WLOGIN_DA2: Long = 1 shl 25
const val WLOGIN_QRPUSH: Long = 1 shl 26
const val WLOGIN_PT4Token: Long = 1 shl 27
}

companion object {
private val jsonModule = Json { ignoreUnknownKeys = true }

fun fromJson(json: String): AppInfo = jsonModule.decodeFromString(json)
}

fun toJson() = jsonModule.encodeToString(this)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package org.ntqqrev.acidify.common.android

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlin.random.Random

@Serializable
class AndroidSessionStore(
var uin: Long,
var uid: String,
var state: State,
var wloginSigs: WLoginSigs,
var guid: ByteArray,
var androidId: String,
var qimei: String,
var deviceName: String,
) {
@Serializable
class WLoginSigs(
var a2: ByteArray,
var a2Key: ByteArray,
var d2: ByteArray,
var d2Key: ByteArray,
var a1: ByteArray,
var a1Key: ByteArray,
var noPicSig: ByteArray,
var tgtgtKey: ByteArray,
var ksid: ByteArray,
var superKey: ByteArray,
var stKey: ByteArray,
var stWeb: ByteArray,
var st: ByteArray,
var wtSessionTicket: ByteArray,
var wtSessionTicketKey: ByteArray,
var randomKey: ByteArray,
var sKey: ByteArray,
var psKey: Map<String, String>,
) {
fun clear() {
a2 = ByteArray(0)
d2 = ByteArray(0)
d2Key = ByteArray(16)
a1 = ByteArray(0)
tgtgtKey = ByteArray(0)
randomKey = ByteArray(16)
}
}

@Serializable
class State(
var tlv104: ByteArray? = null,
var tlv547: ByteArray? = null,
var tlv174: ByteArray? = null,
var keyExchangeSession: KeyExchangeSession? = null,
var cookie: String? = null,
var qrSig: ByteArray? = null,
) {
@Serializable
class KeyExchangeSession(
var sessionTicket: ByteArray,
var sessionKey: ByteArray,
)
}

companion object {
fun empty(): AndroidSessionStore {
return AndroidSessionStore(
uin = 0,
uid = "",
state = State(),
wloginSigs = WLoginSigs(
a2 = ByteArray(0),
a2Key = ByteArray(16),
d2 = ByteArray(0),
d2Key = ByteArray(16),
a1 = ByteArray(0),
a1Key = ByteArray(16),
noPicSig = ByteArray(0),
tgtgtKey = ByteArray(0),
ksid = ByteArray(0),
superKey = ByteArray(0),
stKey = ByteArray(0),
stWeb = ByteArray(0),
st = ByteArray(0),
wtSessionTicket = ByteArray(0),
wtSessionTicketKey = ByteArray(0),
randomKey = ByteArray(16),
sKey = ByteArray(0),
psKey = emptyMap(),
),
guid = Random.nextBytes(16),
androidId = Random.nextBytes(8).toHexString(),
qimei = "",
deviceName = "Lagrange-${Random.nextBytes(3).toHexString()}",
)
}

fun fromJson(json: String): AndroidSessionStore = Json.decodeFromString(json)
}

fun toJson() = Json.encodeToString(this)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.ntqqrev.acidify.common.android

import org.ntqqrev.acidify.common.SignResult

/**
* Android 签名提供者接口,实现 [sign] 方法以提供签名功能
*/
fun interface AndroidSignProvider {
suspend fun sign(
uin: Long,
cmd: String,
buffer: ByteArray,
guid: String,
seq: Int,
version: String,
qua: String,
): SignResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package org.ntqqrev.acidify.common.android

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.ntqqrev.acidify.common.SignResult

/**
* 通过 HTTP 接口进行签名的 [AndroidSignProvider] 实现
* @param url 签名服务的 URL 地址
* @param httpProxy 可选的 HTTP 代理地址,例如 `http://127.0.0.1:7890`
*/
class AndroidUrlSignProvider(val url: String, val httpProxy: String? = null) : AndroidSignProvider {
private val client = HttpClient {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
engine {
if (!httpProxy.isNullOrEmpty()) {
proxy = ProxyBuilder.http(httpProxy)
}
}
}

override suspend fun sign(
uin: Long,
cmd: String,
buffer: ByteArray,
guid: String,
seq: Int,
version: String,
qua: String
): SignResult {
val data = client.post(url) {
contentType(ContentType.Application.Json)
setBody(
AndroidUrlSignRequest(
uin = uin,
cmd = cmd,
buffer = buffer.toHexString(),
guid = guid,
seq = seq,
version = version,
qua = qua,
)
)
}.body<AndroidUrlSignResponse>().data
return SignResult(
sign = data.sign.hexToByteArray(),
token = data.token.hexToByteArray(),
extra = data.extra.hexToByteArray(),
)
}
}

@Serializable
private data class AndroidUrlSignRequest(
val uin: Long,
val cmd: String,
val buffer: String,
val guid: String,
val seq: Int,
val version: String,
val qua: String
)

@Serializable
private data class AndroidUrlSignResponse(
val data: AndroidUrlSignValue
)

@Serializable
private data class AndroidUrlSignValue(
val sign: String,
val token: String,
val extra: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.ntqqrev.acidify.internal

import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract

/**
* Marks a service or operation as only supported for [LagrangeClient].
*/
@OptIn(ExperimentalContracts::class)
internal fun IClient.ensureIsLagrange() {
contract {
returns() implies (this@ensureIsLagrange is LagrangeClient)
}
if (this !is LagrangeClient) {
throw IllegalStateException("This operation is only supported for LagrangeClient")
}
}

/**
* Marks a service or operation as only supported for [KuromeClient].
*/
@OptIn(ExperimentalContracts::class)
internal fun IClient.ensureIsKurome() {
contract {
returns() implies (this@ensureIsKurome is KuromeClient)
}
if (this !is KuromeClient) {
throw IllegalStateException("This operation is only supported for KuromeClient")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.ntqqrev.acidify.internal

import kotlinx.coroutines.CoroutineScope
import org.ntqqrev.acidify.internal.service.Service
import org.ntqqrev.acidify.logging.Logger

internal sealed interface IClient : CoroutineScope {
val os: String
val uin: Long
val uid: String

fun createLogger(forObject: Any): Logger
suspend fun doPostOnlineLogic()
suspend fun doPreOfflineLogic()
suspend fun <T, R> callService(service: Service<T, R>, payload: T, timeout: Long = 10_000L): R
suspend fun <R> callService(service: Service<Unit, R>, timeout: Long = 10_000L): R =
callService(service, Unit, timeout)
}
Loading