Parcourir la source

QRCode login support & Introduce new authorization factory (#2502)

* [core] process `wtlogin.trans_emp` to support qrcode login

* [core] fix `wtlogin.trans_emp` protocol

* [core] optimize QRCodeLoginProcessor logic

* [core] fix `wtlogin.trans_emp` outgoing packet

* [core] cancel login when logging a bot which is inconsistent from bot factory

* [core] ignore `flag3` check on ANDROID_WATCH & name `flag1` and `flag2`

* [core] provide default `QRCodeLoginListener` for jvm

* [core] don't catch IllegalStateException in QRCodeLoginProcessor

* [core] Use `LoginSolver.createQRCodeLoginListener()` instead of property; Rename configuration name

* [core] Code improvement

* [core] remove qrcode state lock

* [core] ignore `flag3` when command is `wtlogin.trans_emp` in packet codec

* [core] enable qrcode login for macos

* [core] remove debug property in log

* [core] reformat code

* [core] rename `TransEmpResponse` to `Response`

* [core] assert `flag3Exception` not null first

* [core] remove arg client

* [core] update qrcode login notes

* [core] set custom qrcode size

* [core] Draft BotAuthorization

* [core] make SecretsProtection mpp

* [core] BotAuthorization.byXXX

* [core] Move QRCodeLoginListener to `.auth`

* [core] Protect data of BotAccount

* [core] Add SelectorRequireReconnectException

* [core] Implementation of BotAuthorization

* Revert changes of BotConfiguration

* api dump

* [core] remove passwordMd5 in `BotAccount`

* [mock] Add new bot factory function to mock bot factory

* Delete LoginCommandTest

* [core] Improve QRCode render

* [core] Introduce UnsupportedCaptchaMethodException & UnsupportedQRCodeCaptchaException

* api dump

* update docs

* [core] update `DebugRunHelper`

* [core] add simple block for BotAuthorization

* api dump

* Rename `canDoQRLogin` to `supportsQRLogin`, and specify argument names for MiraiProtocolInternal

* Remove `phoneNumber` parameter from BotAccount

* Make `BotAccount.<init>` with String password parameter TestOnly

* Rename `InconsistentBotException` to `InconsistentBotIdException`

* Rename `QRCodeLoginListener.onStatusChanged` to `QRCodeLoginListener.onStateChanged`

* Rename `BotAuthorizationResult` to `BotAuthResult`

* Rename BotAuthComponent, move internal APIs to internal module

* Logic fixup

* doc update

* QRCodeLoginListener.qrCodeStateUpdateInterval & onIntervalLoop

* console login with BotAuthorization

* update testing

* Update mirai-core-api/src/jvmMain/kotlin/utils/LoginSolver.jvm.kt

* Move AuthControl outside SsoProcessor

* Redesign auth

* Add initialTicket to producerCoroutine

* Revert protocol changes of MACOS

* Fix latch death locking

* Fix CoroutineOnDemandValueScope.receiveOrNull exceptional finish

* Fix exception collecting

* Fix DefaultBotAuthorizationFactory loading

* [core] qrcode login for IPAD protocol

* Revert "[core] qrcode login for IPAD protocol"

This reverts commit c1136a8798917f9fbf96d304c16c02c57d3eb607.

---------

Co-authored-by: Karlatemp <kar@kasukusakura.com>
Co-authored-by: Him188 <Him188@mamoe.net>
StageGuard il y a 2 ans
Parent
commit
78d0b4fd54
54 fichiers modifiés avec 2816 ajouts et 481 suppressions
  1. 1 0
      mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api
  2. 30 5
      mirai-console/backend/mirai-console/src/MiraiConsole.kt
  3. 8 0
      mirai-console/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt
  4. 53 0
      mirai-console/backend/mirai-console/src/internal/auth/ConsoleBotAuthorization.kt
  5. 53 0
      mirai-console/backend/mirai-console/src/internal/auth/ConsoleSecretsCalculator.kt
  6. 0 157
      mirai-console/backend/mirai-console/test/command/LoginCommandTest.kt
  7. 76 2
      mirai-core-api/compatibility-validation/android/api/android.api
  8. 77 2
      mirai-core-api/compatibility-validation/jvm/api/jvm.api
  9. 52 0
      mirai-core-api/src/commonMain/kotlin/BotFactory.kt
  10. 128 0
      mirai-core-api/src/commonMain/kotlin/auth/BotAuthorization.kt
  11. 95 0
      mirai-core-api/src/commonMain/kotlin/auth/QRCodeLoginListener.kt
  12. 37 3
      mirai-core-api/src/commonMain/kotlin/network/LoginFailedException.kt
  13. 18 1
      mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt
  14. 156 2
      mirai-core-api/src/jvmMain/kotlin/utils/LoginSolver.jvm.kt
  15. 8 0
      mirai-core-mock/src/internal/MockBotFactoryImpl.kt
  16. 91 0
      mirai-core-utils/src/commonMain/kotlin/SecretsProtection.kt
  17. 26 49
      mirai-core-utils/src/jvmBaseMain/kotlin/SecretsProtection.kt
  18. 2 2
      mirai-core-utils/src/jvmTest/kotlin/SecretsProtectionTest.kt
  19. 47 0
      mirai-core-utils/src/nativeMain/kotlin/SecretsProtection.kt
  20. 19 9
      mirai-core/src/commonMain/kotlin/BotAccount.kt
  21. 7 2
      mirai-core/src/commonMain/kotlin/BotFactory.kt
  22. 4 0
      mirai-core/src/commonMain/kotlin/QQAndroidBot.kt
  23. 1 1
      mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt
  24. 116 0
      mirai-core/src/commonMain/kotlin/network/auth/AuthControl.kt
  25. 51 0
      mirai-core/src/commonMain/kotlin/network/auth/BotAuthSession.kt
  26. 203 0
      mirai-core/src/commonMain/kotlin/network/auth/CoroutineOnDemandValueScope.kt
  27. 51 0
      mirai-core/src/commonMain/kotlin/network/auth/DefaultBotAuthorizationFactoryImpl.kt
  28. 51 0
      mirai-core/src/commonMain/kotlin/network/auth/Latch.kt
  29. 86 0
      mirai-core/src/commonMain/kotlin/network/auth/OnDemandValueScope.kt
  30. 176 0
      mirai-core/src/commonMain/kotlin/network/auth/ProducerState.kt
  31. 2 2
      mirai-core/src/commonMain/kotlin/network/components/AccountSecretsManager.kt
  32. 6 0
      mirai-core/src/commonMain/kotlin/network/components/BotClientHolder.kt
  33. 65 36
      mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt
  34. 167 0
      mirai-core/src/commonMain/kotlin/network/components/QRCodeLoginProcessor.kt
  35. 161 37
      mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt
  36. 8 1
      mirai-core/src/commonMain/kotlin/network/handler/CommonNetworkHandler.kt
  37. 15 0
      mirai-core/src/commonMain/kotlin/network/handler/selector/SelectorRequireReconnectException.kt
  38. 11 0
      mirai-core/src/commonMain/kotlin/network/keys.kt
  39. 4 2
      mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacket.kt
  40. 1 0
      mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt
  41. 94 2
      mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt
  42. 243 3
      mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt
  43. 2 2
      mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin15.kt
  44. 55 2
      mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin9.kt
  45. 6 1
      mirai-core/src/commonMain/kotlin/utils/MiraiCoreServices.kt
  46. 59 52
      mirai-core/src/commonMain/kotlin/utils/MiraiProtocolInternal.kt
  47. 10 0
      mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.auth.DefaultBotAuthorizationFactory
  48. 108 0
      mirai-core/src/commonTest/kotlin/network/component/BotAuthControlTest.kt
  49. 29 1
      mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt
  50. 0 63
      mirai-core/src/jvmBaseMain/kotlin/BotAccount.kt
  51. 29 0
      mirai-core/src/jvmBaseMain/kotlin/network/handler/selector/SelectorRequireReconnectException.kt
  52. 6 1
      mirai-core/src/jvmTest/kotlin/directboot/DebugRunHelper.kt
  53. 0 41
      mirai-core/src/nativeMain/kotlin/BotAccount.kt
  54. 12 0
      mirai-core/src/nativeMain/kotlin/network/handler/selector/SelectorRequireReconnectException.kt

+ 1 - 0
mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api

@@ -19,6 +19,7 @@ public abstract interface class net/mamoe/mirai/console/MiraiConsole : kotlinx/c
 
 public final class net/mamoe/mirai/console/MiraiConsole$INSTANCE : net/mamoe/mirai/console/MiraiConsole {
 	public static synthetic fun addBot$default (Lnet/mamoe/mirai/console/MiraiConsole$INSTANCE;JLjava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/mamoe/mirai/Bot;
+	public static synthetic fun addBot$default (Lnet/mamoe/mirai/console/MiraiConsole$INSTANCE;JLnet/mamoe/mirai/auth/BotAuthorization;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/mamoe/mirai/Bot;
 	public static synthetic fun addBot$default (Lnet/mamoe/mirai/console/MiraiConsole$INSTANCE;J[BLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/mamoe/mirai/Bot;
 	public fun getBuildDate ()Ljava/time/Instant;
 	public fun getBuiltInPluginLoaders ()Ljava/util/List;

+ 30 - 5
mirai-console/backend/mirai-console/src/MiraiConsole.kt

@@ -16,6 +16,7 @@ import kotlinx.coroutines.*
 import me.him188.kotlin.dynamic.delegation.dynamicDelegation
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.BotFactory
+import net.mamoe.mirai.auth.BotAuthorization
 import net.mamoe.mirai.console.MiraiConsole.INSTANCE
 import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start
 import net.mamoe.mirai.console.extensions.BotConfigurationAlterer
@@ -191,8 +192,31 @@ public interface MiraiConsole : CoroutineScope {
         public fun addBot(id: Long, password: ByteArray, configuration: BotConfiguration.() -> Unit = {}): Bot =
             addBotImpl(id, password, configuration)
 
+        /**
+         * 添加一个 [Bot] 实例到全局 Bot 列表, 但不登录.
+         *
+         * 调用 [Bot.login] 可登录.
+         *
+         * @see Bot.instances 获取现有 [Bot] 实例列表
+         * @see BotConfigurationAlterer ExtensionPoint
+         */
+        @ConsoleExperimentalApi("This is a low-level API and might be removed in the future.")
+        public fun addBot(
+            id: Long,
+            authorization: BotAuthorization,
+            configuration: BotConfiguration.() -> Unit = {}
+        ): Bot = addBotImpl(id, authorization, configuration)
+
         @Suppress("UNREACHABLE_CODE")
-        private fun addBotImpl(id: Long, password: Any, configuration: BotConfiguration.() -> Unit = {}): Bot {
+        private fun addBotImpl(id: Long, authorization: Any, configuration: BotConfiguration.() -> Unit = {}): Bot {
+            when (authorization) {
+                is String -> {}
+                is ByteArray -> {}
+                is BotAuthorization -> {}
+
+                else -> throw IllegalArgumentException("Bad authorization type: `${authorization.javaClass.name}`. Require String, ByteArray or BotAuthorization")
+            }
+
             var config = BotConfiguration().apply {
 
                 workingDir = MiraiConsole.rootDir
@@ -239,10 +263,11 @@ public interface MiraiConsole : CoroutineScope {
                 extension.alterConfiguration(id, acc)
             }
 
-            return when (password) {
-                is ByteArray -> BotFactory.newBot(id, password, config)
-                is String -> BotFactory.newBot(id, password, config)
-                else -> throw IllegalArgumentException("Bad password type: `${password.javaClass.name}`. Require ByteArray or String")
+            return when (authorization) {
+                is ByteArray -> BotFactory.newBot(id, authorization, config)            // pwd md5
+                is String -> BotFactory.newBot(id, authorization, config)               // pwd
+                is BotAuthorization -> BotFactory.newBot(id, authorization, config)     // authorization
+                else -> error("assert")
             }
         }
 

+ 8 - 0
mirai-console/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt

@@ -33,6 +33,7 @@ import net.mamoe.mirai.console.extensions.CommandCallParserProvider
 import net.mamoe.mirai.console.extensions.CommandCallResolverProvider
 import net.mamoe.mirai.console.extensions.PermissionServiceProvider
 import net.mamoe.mirai.console.extensions.PostStartupExtension
+import net.mamoe.mirai.console.internal.auth.ConsoleSecretsCalculator
 import net.mamoe.mirai.console.internal.command.CommandConfig
 import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig
 import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig.Account.ConfigurationKey
@@ -100,6 +101,9 @@ internal class MiraiConsoleImplementationBridge(
     @Volatile
     var permissionSeviceLoaded: Boolean = false
 
+    // For protect account.secrets in console with non-password login
+    lateinit var consoleSecretsCalculator: ConsoleSecretsCalculator
+
     // MiraiConsoleImplementation define: get() = LoggerControllerImpl()
     // Need to cache it or else created every call.
     //      It caused config/Console/Logger.yml ignored.
@@ -290,6 +294,10 @@ ___  ____           _   _____                       _
         phase("initialize all plugins") {
             pluginManager // init
 
+            consoleSecretsCalculator = ConsoleSecretsCalculator(
+                pluginManager.pluginsDataPath.resolve("Console/console-secrets.key")
+            ).also { it.consoleKey }
+
             mainLogger.verbose { "Loading JVM plugins..." }
             pluginManager.loadAllPluginsUsingBuiltInLoaders()
             pluginManager.initExternalPluginLoaders().let { count ->

+ 53 - 0
mirai-console/backend/mirai-console/src/internal/auth/ConsoleBotAuthorization.kt

@@ -0,0 +1,53 @@
+/*
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.console.internal.auth
+
+import net.mamoe.mirai.auth.BotAuthInfo
+import net.mamoe.mirai.auth.BotAuthResult
+import net.mamoe.mirai.auth.BotAuthSession
+import net.mamoe.mirai.auth.BotAuthorization
+import net.mamoe.mirai.console.MiraiConsoleImplementation
+import java.io.ByteArrayOutputStream
+
+internal class ConsoleBotAuthorization(
+    private val delegate: suspend (BotAuthSession, BotAuthInfo) -> BotAuthResult,
+) : BotAuthorization {
+
+    override suspend fun authorize(session: BotAuthSession, info: BotAuthInfo): BotAuthResult {
+        return delegate.invoke(session, info)
+    }
+
+    override fun calculateSecretsKey(bot: BotAuthInfo): ByteArray {
+        val calc = MiraiConsoleImplementation.getBridge().consoleSecretsCalculator
+
+        val writer = ByteArrayOutputStream()
+
+        writer += calc.consoleKey.asByteArray
+
+        writer += bot.deviceInfo.apn
+        writer += bot.deviceInfo.device
+        writer += bot.deviceInfo.bootId
+        writer += bot.deviceInfo.imsiMd5
+
+        return writer.toByteArray()
+    }
+
+
+    private operator fun ByteArrayOutputStream.plusAssign(data: ByteArray) {
+        write(data)
+    }
+
+    companion object {
+        fun byQRCode(): ConsoleBotAuthorization = ConsoleBotAuthorization { session, _ ->
+            session.authByQRCode()
+        }
+    }
+}
+

+ 53 - 0
mirai-console/backend/mirai-console/src/internal/auth/ConsoleSecretsCalculator.kt

@@ -0,0 +1,53 @@
+/*
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.console.internal.auth
+
+import net.mamoe.mirai.utils.SecretsProtection
+import net.mamoe.mirai.utils.lateinitMutableProperty
+import java.io.ByteArrayOutputStream
+import java.io.DataOutputStream
+import java.nio.file.Path
+import java.util.*
+import kotlin.io.path.createDirectories
+import kotlin.io.path.isRegularFile
+import kotlin.io.path.readBytes
+import kotlin.io.path.writeBytes
+
+internal class ConsoleSecretsCalculator(
+    private val file: Path,
+) {
+    internal val consoleKey: SecretsProtection.EscapedByteBuffer get() = _consoleKey
+
+    private var _consoleKey: SecretsProtection.EscapedByteBuffer by lateinitMutableProperty {
+        loadOrCreate()
+    }
+
+    fun loadOrCreate(): SecretsProtection.EscapedByteBuffer {
+        if (file.isRegularFile()) {
+            return SecretsProtection.EscapedByteBuffer(file.readBytes())
+        }
+
+        file.parent?.createDirectories()
+        val dataStream = ByteArrayOutputStream()
+        val dataWriter = DataOutputStream(dataStream)
+
+        repeat(3) {
+            dataWriter.writeUTF(UUID.randomUUID().toString())
+        }
+
+        val data = dataStream.toByteArray()
+        file.writeBytes(data)
+        return SecretsProtection.EscapedByteBuffer(data)
+    }
+
+    fun reloadOrCreate() {
+        _consoleKey = loadOrCreate()
+    }
+}

+ 0 - 157
mirai-console/backend/mirai-console/test/command/LoginCommandTest.kt

@@ -1,157 +0,0 @@
-/*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/dev/LICENSE
- */
-
-@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
-
-package net.mamoe.mirai.console.command
-
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.test.runTest
-import net.mamoe.mirai.Bot
-import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register
-import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
-import net.mamoe.mirai.console.internal.command.builtin.LoginCommandImpl
-import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig
-import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig.Account
-import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig.Account.PasswordKind
-import net.mamoe.mirai.internal.QQAndroidBot
-import net.mamoe.mirai.utils.BotConfiguration
-import net.mamoe.mirai.utils.md5
-import net.mamoe.mirai.utils.toUHexString
-import kotlin.test.Test
-import kotlin.test.assertContentEquals
-import kotlin.test.assertEquals
-import kotlin.test.assertNotNull
-
-@OptIn(ExperimentalCommandDescriptors::class)
-internal class LoginCommandTest : AbstractCommandTest() {
-
-    @Test
-    fun `login with provided password`() = runTest {
-        val myId = 123L
-        val myPwd = "password001"
-
-        val bot = awaitDeferred { cont ->
-            val command = object : LoginCommandImpl() {
-                override suspend fun doLogin(bot: Bot) {
-                    cont.complete(bot as QQAndroidBot)
-                }
-            }
-            command.register(true)
-            command.execute(consoleSender, "$myId $myPwd")
-        }
-
-        val account = bot.account
-        assertContentEquals(myPwd.md5(), account.passwordMd5)
-        assertEquals(myId, account.id)
-    }
-
-    @Test
-    fun `login with saved plain password`() = runTest {
-        val myId = 123L
-        val myPwd = "password001"
-
-        dataScope.set(AutoLoginConfig().apply {
-            accounts.add(
-                Account(
-                    account = myId.toString(),
-                    password = Account.Password(PasswordKind.PLAIN, myPwd)
-                )
-            )
-        })
-
-        val bot = awaitDeferred { cont ->
-            val command = object : LoginCommandImpl() {
-                override suspend fun doLogin(bot: Bot) {
-                    cont.complete(bot as QQAndroidBot)
-                }
-            }
-            command.register(true)
-            command.execute(consoleSender, "$myId")
-        }
-
-        val account = bot.account
-        assertContentEquals(myPwd.md5(), account.passwordMd5)
-        assertEquals(myId, account.id)
-    }
-
-    @Test
-    fun `login with saved md5 password`() = runTest {
-        val myId = 123L
-        val myPwd = "password001"
-
-        dataScope.set(AutoLoginConfig().apply {
-            accounts.add(
-                Account(
-                    account = myId.toString(),
-                    password = Account.Password(PasswordKind.MD5, myPwd.md5().toUHexString(""))
-                )
-            )
-        })
-
-        val bot = awaitDeferred<QQAndroidBot> { cont ->
-            val command = object : LoginCommandImpl() {
-                override suspend fun doLogin(bot: Bot) {
-                    cont.complete(bot as QQAndroidBot)
-                }
-            }
-            command.register(true)
-            command.execute(consoleSender, "$myId")
-        }
-
-        val account = bot.account
-        assertContentEquals(myPwd.md5(), account.passwordMd5)
-        assertEquals(myId, account.id)
-    }
-
-    @Test
-    fun `login with saved configuration`() = runTest {
-        val myId = 123L
-        val myPwd = "password001"
-
-        dataScope.set(AutoLoginConfig().apply {
-            accounts.add(
-                Account(
-                    account = myId.toString(),
-                    password = Account.Password(PasswordKind.MD5, myPwd.md5().toUHexString("")),
-                    configuration = mapOf(
-                        Account.ConfigurationKey.protocol to BotConfiguration.MiraiProtocol.ANDROID_PAD.name,
-                        Account.ConfigurationKey.device to "device.new.json",
-                        Account.ConfigurationKey.heartbeatStrategy to BotConfiguration.HeartbeatStrategy.REGISTER.name
-                    )
-                )
-            )
-        })
-
-        val bot = awaitDeferred<QQAndroidBot> { cont ->
-            val command = object : LoginCommandImpl() {
-                override suspend fun doLogin(bot: Bot) {
-                    cont.complete(bot as QQAndroidBot)
-                }
-            }
-            command.register(true)
-            command.execute(consoleSender, "$myId")
-        }
-
-        val configuration = bot.configuration
-        assertEquals(BotConfiguration.MiraiProtocol.ANDROID_PAD, configuration.protocol)
-        assertEquals(BotConfiguration.HeartbeatStrategy.REGISTER, configuration.heartbeatStrategy)
-        assertNotNull(configuration.deviceInfo)
-    }
-}
-
-@BuilderInference
-internal suspend inline fun <T> awaitDeferred(
-    @BuilderInference
-    crossinline block: suspend (CompletableDeferred<T>) -> Unit
-): T {
-    val deferred = CompletableDeferred<T>()
-    block(deferred)
-    return deferred.await()
-}

+ 76 - 2
mirai-core-api/compatibility-validation/android/api/android.api

@@ -53,6 +53,9 @@ public abstract interface class net/mamoe/mirai/BotFactory {
 	public fun newBot (JLjava/lang/String;)Lnet/mamoe/mirai/Bot;
 	public fun newBot (JLjava/lang/String;Lnet/mamoe/mirai/BotFactory$BotConfigurationLambda;)Lnet/mamoe/mirai/Bot;
 	public abstract fun newBot (JLjava/lang/String;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
+	public fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;)Lnet/mamoe/mirai/Bot;
+	public fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;Lnet/mamoe/mirai/BotFactory$BotConfigurationLambda;)Lnet/mamoe/mirai/Bot;
+	public abstract fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
 	public fun newBot (J[B)Lnet/mamoe/mirai/Bot;
 	public fun newBot (J[BLnet/mamoe/mirai/BotFactory$BotConfigurationLambda;)Lnet/mamoe/mirai/Bot;
 	public abstract fun newBot (J[BLnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
@@ -65,6 +68,7 @@ public abstract interface class net/mamoe/mirai/BotFactory$BotConfigurationLambd
 public final class net/mamoe/mirai/BotFactory$INSTANCE : net/mamoe/mirai/BotFactory {
 	public final synthetic fun newBot (JLjava/lang/String;Lkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/Bot;
 	public fun newBot (JLjava/lang/String;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
+	public fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
 	public final synthetic fun newBot (J[BLkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/Bot;
 	public fun newBot (J[BLnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
 }
@@ -165,6 +169,58 @@ public final class net/mamoe/mirai/_MiraiInstance {
 	public static final fun set (Lnet/mamoe/mirai/IMirai;)V
 }
 
+public abstract interface class net/mamoe/mirai/auth/BotAuthInfo {
+	public abstract fun getConfiguration ()Lnet/mamoe/mirai/utils/BotConfiguration;
+	public abstract fun getDeviceInfo ()Lnet/mamoe/mirai/utils/DeviceInfo;
+	public abstract fun getId ()J
+}
+
+public abstract interface class net/mamoe/mirai/auth/BotAuthResult {
+}
+
+public abstract interface class net/mamoe/mirai/auth/BotAuthSession {
+	public abstract fun authByPassword (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public abstract fun authByPassword ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public abstract fun authByQRCode (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public abstract interface class net/mamoe/mirai/auth/BotAuthorization {
+	public static final field Companion Lnet/mamoe/mirai/auth/BotAuthorization$Companion;
+	public abstract fun authorize (Lnet/mamoe/mirai/auth/BotAuthSession;Lnet/mamoe/mirai/auth/BotAuthInfo;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public static fun byPassword (Ljava/lang/String;)Lnet/mamoe/mirai/auth/BotAuthorization;
+	public static fun byPassword ([B)Lnet/mamoe/mirai/auth/BotAuthorization;
+	public static fun byQRCode ()Lnet/mamoe/mirai/auth/BotAuthorization;
+	public fun calculateSecretsKey (Lnet/mamoe/mirai/auth/BotAuthInfo;)[B
+}
+
+public final class net/mamoe/mirai/auth/BotAuthorization$Companion {
+	public final fun byPassword (Ljava/lang/String;)Lnet/mamoe/mirai/auth/BotAuthorization;
+	public final fun byPassword ([B)Lnet/mamoe/mirai/auth/BotAuthorization;
+	public final fun byQRCode ()Lnet/mamoe/mirai/auth/BotAuthorization;
+	public final fun invoke (Lkotlin/jvm/functions/Function3;)Lnet/mamoe/mirai/auth/BotAuthorization;
+}
+
+public abstract interface class net/mamoe/mirai/auth/QRCodeLoginListener {
+	public fun getQrCodeEcLevel ()I
+	public fun getQrCodeMargin ()I
+	public fun getQrCodeSize ()I
+	public fun getQrCodeStateUpdateInterval ()J
+	public abstract fun onFetchQRCode (Lnet/mamoe/mirai/Bot;[B)V
+	public fun onIntervalLoop ()V
+	public abstract fun onStateChanged (Lnet/mamoe/mirai/Bot;Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;)V
+}
+
+public final class net/mamoe/mirai/auth/QRCodeLoginListener$State : java/lang/Enum {
+	public static final field CANCELLED Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field CONFIRMED Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field DEFAULT Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field TIMEOUT Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field WAITING_FOR_CONFIRM Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field WAITING_FOR_SCAN Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static fun valueOf (Ljava/lang/String;)Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static fun values ()[Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+}
+
 public abstract interface class net/mamoe/mirai/contact/AnonymousMember : net/mamoe/mirai/contact/Member {
 	public abstract fun getAnonymousId ()Ljava/lang/String;
 	public fun nudge ()Lnet/mamoe/mirai/message/action/MemberNudge;
@@ -5355,6 +5411,12 @@ public final class net/mamoe/mirai/network/ForceOfflineException : java/util/con
 	public fun getMessage ()Ljava/lang/String;
 }
 
+public final class net/mamoe/mirai/network/InconsistentBotIdException : net/mamoe/mirai/network/LoginFailedException {
+	public synthetic fun <init> (JJLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public final fun getActual ()J
+	public final fun getExpected ()J
+}
+
 public abstract class net/mamoe/mirai/network/LoginFailedException : java/lang/RuntimeException {
 	public synthetic fun <init> (ZLjava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
 	public synthetic fun <init> (ZLjava/lang/String;Ljava/lang/Throwable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
@@ -5374,11 +5436,22 @@ public final class net/mamoe/mirai/network/RetryLaterException : net/mamoe/mirai
 	public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
 }
 
-public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/LoginFailedException {
+public class net/mamoe/mirai/network/UnsupportedCaptchaMethodException : net/mamoe/mirai/network/LoginFailedException {
+	public fun <init> (Z)V
+	public fun <init> (ZLjava/lang/String;)V
+	public fun <init> (ZLjava/lang/String;Ljava/lang/Throwable;)V
+	public fun <init> (ZLjava/lang/Throwable;)V
+}
+
+public final class net/mamoe/mirai/network/UnsupportedQRCodeCaptchaException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException {
+	public fun <init> (Ljava/lang/String;)V
+}
+
+public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException {
 	public fun <init> (Ljava/lang/String;)V
 }
 
-public final class net/mamoe/mirai/network/UnsupportedSmsLoginException : net/mamoe/mirai/network/LoginFailedException {
+public final class net/mamoe/mirai/network/UnsupportedSmsLoginException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException {
 	public fun <init> (Ljava/lang/String;)V
 }
 
@@ -5825,6 +5898,7 @@ public abstract class net/mamoe/mirai/utils/LoginSolver {
 	public static final field Companion Lnet/mamoe/mirai/utils/LoginSolver$Companion;
 	public static final field Default Lnet/mamoe/mirai/utils/LoginSolver;
 	public fun <init> ()V
+	public fun createQRCodeLoginListener (Lnet/mamoe/mirai/Bot;)Lnet/mamoe/mirai/auth/QRCodeLoginListener;
 	public fun isSliderCaptchaSupported ()Z
 	public fun onSolveDeviceVerification (Lnet/mamoe/mirai/Bot;Lnet/mamoe/mirai/utils/DeviceVerificationRequests;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public abstract fun onSolvePicCaptcha (Lnet/mamoe/mirai/Bot;[BLkotlin/coroutines/Continuation;)Ljava/lang/Object;

+ 77 - 2
mirai-core-api/compatibility-validation/jvm/api/jvm.api

@@ -53,6 +53,9 @@ public abstract interface class net/mamoe/mirai/BotFactory {
 	public fun newBot (JLjava/lang/String;)Lnet/mamoe/mirai/Bot;
 	public fun newBot (JLjava/lang/String;Lnet/mamoe/mirai/BotFactory$BotConfigurationLambda;)Lnet/mamoe/mirai/Bot;
 	public abstract fun newBot (JLjava/lang/String;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
+	public fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;)Lnet/mamoe/mirai/Bot;
+	public fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;Lnet/mamoe/mirai/BotFactory$BotConfigurationLambda;)Lnet/mamoe/mirai/Bot;
+	public abstract fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
 	public fun newBot (J[B)Lnet/mamoe/mirai/Bot;
 	public fun newBot (J[BLnet/mamoe/mirai/BotFactory$BotConfigurationLambda;)Lnet/mamoe/mirai/Bot;
 	public abstract fun newBot (J[BLnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
@@ -65,6 +68,7 @@ public abstract interface class net/mamoe/mirai/BotFactory$BotConfigurationLambd
 public final class net/mamoe/mirai/BotFactory$INSTANCE : net/mamoe/mirai/BotFactory {
 	public final synthetic fun newBot (JLjava/lang/String;Lkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/Bot;
 	public fun newBot (JLjava/lang/String;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
+	public fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
 	public final synthetic fun newBot (J[BLkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/Bot;
 	public fun newBot (J[BLnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
 }
@@ -165,6 +169,58 @@ public final class net/mamoe/mirai/_MiraiInstance {
 	public static final fun set (Lnet/mamoe/mirai/IMirai;)V
 }
 
+public abstract interface class net/mamoe/mirai/auth/BotAuthInfo {
+	public abstract fun getConfiguration ()Lnet/mamoe/mirai/utils/BotConfiguration;
+	public abstract fun getDeviceInfo ()Lnet/mamoe/mirai/utils/DeviceInfo;
+	public abstract fun getId ()J
+}
+
+public abstract interface class net/mamoe/mirai/auth/BotAuthResult {
+}
+
+public abstract interface class net/mamoe/mirai/auth/BotAuthSession {
+	public abstract fun authByPassword (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public abstract fun authByPassword ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public abstract fun authByQRCode (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public abstract interface class net/mamoe/mirai/auth/BotAuthorization {
+	public static final field Companion Lnet/mamoe/mirai/auth/BotAuthorization$Companion;
+	public abstract fun authorize (Lnet/mamoe/mirai/auth/BotAuthSession;Lnet/mamoe/mirai/auth/BotAuthInfo;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public static fun byPassword (Ljava/lang/String;)Lnet/mamoe/mirai/auth/BotAuthorization;
+	public static fun byPassword ([B)Lnet/mamoe/mirai/auth/BotAuthorization;
+	public static fun byQRCode ()Lnet/mamoe/mirai/auth/BotAuthorization;
+	public fun calculateSecretsKey (Lnet/mamoe/mirai/auth/BotAuthInfo;)[B
+}
+
+public final class net/mamoe/mirai/auth/BotAuthorization$Companion {
+	public final fun byPassword (Ljava/lang/String;)Lnet/mamoe/mirai/auth/BotAuthorization;
+	public final fun byPassword ([B)Lnet/mamoe/mirai/auth/BotAuthorization;
+	public final fun byQRCode ()Lnet/mamoe/mirai/auth/BotAuthorization;
+	public final fun invoke (Lkotlin/jvm/functions/Function3;)Lnet/mamoe/mirai/auth/BotAuthorization;
+}
+
+public abstract interface class net/mamoe/mirai/auth/QRCodeLoginListener {
+	public fun getQrCodeEcLevel ()I
+	public fun getQrCodeMargin ()I
+	public fun getQrCodeSize ()I
+	public fun getQrCodeStateUpdateInterval ()J
+	public abstract fun onFetchQRCode (Lnet/mamoe/mirai/Bot;[B)V
+	public fun onIntervalLoop ()V
+	public abstract fun onStateChanged (Lnet/mamoe/mirai/Bot;Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;)V
+}
+
+public final class net/mamoe/mirai/auth/QRCodeLoginListener$State : java/lang/Enum {
+	public static final field CANCELLED Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field CONFIRMED Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field DEFAULT Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field TIMEOUT Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field WAITING_FOR_CONFIRM Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field WAITING_FOR_SCAN Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static fun valueOf (Ljava/lang/String;)Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static fun values ()[Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+}
+
 public abstract interface class net/mamoe/mirai/contact/AnonymousMember : net/mamoe/mirai/contact/Member {
 	public abstract fun getAnonymousId ()Ljava/lang/String;
 	public fun nudge ()Lnet/mamoe/mirai/message/action/MemberNudge;
@@ -5355,6 +5411,12 @@ public final class net/mamoe/mirai/network/ForceOfflineException : java/util/con
 	public fun getMessage ()Ljava/lang/String;
 }
 
+public final class net/mamoe/mirai/network/InconsistentBotIdException : net/mamoe/mirai/network/LoginFailedException {
+	public synthetic fun <init> (JJLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public final fun getActual ()J
+	public final fun getExpected ()J
+}
+
 public abstract class net/mamoe/mirai/network/LoginFailedException : java/lang/RuntimeException {
 	public synthetic fun <init> (ZLjava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
 	public synthetic fun <init> (ZLjava/lang/String;Ljava/lang/Throwable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
@@ -5374,11 +5436,22 @@ public final class net/mamoe/mirai/network/RetryLaterException : net/mamoe/mirai
 	public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
 }
 
-public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/LoginFailedException {
+public class net/mamoe/mirai/network/UnsupportedCaptchaMethodException : net/mamoe/mirai/network/LoginFailedException {
+	public fun <init> (Z)V
+	public fun <init> (ZLjava/lang/String;)V
+	public fun <init> (ZLjava/lang/String;Ljava/lang/Throwable;)V
+	public fun <init> (ZLjava/lang/Throwable;)V
+}
+
+public final class net/mamoe/mirai/network/UnsupportedQRCodeCaptchaException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException {
+	public fun <init> (Ljava/lang/String;)V
+}
+
+public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException {
 	public fun <init> (Ljava/lang/String;)V
 }
 
-public final class net/mamoe/mirai/network/UnsupportedSmsLoginException : net/mamoe/mirai/network/LoginFailedException {
+public final class net/mamoe/mirai/network/UnsupportedSmsLoginException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException {
 	public fun <init> (Ljava/lang/String;)V
 }
 
@@ -5825,6 +5898,7 @@ public abstract class net/mamoe/mirai/utils/LoginSolver {
 	public static final field Companion Lnet/mamoe/mirai/utils/LoginSolver$Companion;
 	public static final field Default Lnet/mamoe/mirai/utils/LoginSolver;
 	public fun <init> ()V
+	public fun createQRCodeLoginListener (Lnet/mamoe/mirai/Bot;)Lnet/mamoe/mirai/auth/QRCodeLoginListener;
 	public fun isSliderCaptchaSupported ()Z
 	public fun onSolveDeviceVerification (Lnet/mamoe/mirai/Bot;Lnet/mamoe/mirai/utils/DeviceVerificationRequests;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public abstract fun onSolvePicCaptcha (Lnet/mamoe/mirai/Bot;[BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
@@ -6190,6 +6264,7 @@ public final class net/mamoe/mirai/utils/StandardCharImageLoginSolver : net/mamo
 	public synthetic fun <init> (Lkotlin/jvm/functions/Function1;Lnet/mamoe/mirai/utils/MiraiLogger;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
 	public static final fun createBlocking (Lkotlin/jvm/functions/Function0;)Lnet/mamoe/mirai/utils/StandardCharImageLoginSolver;
 	public static final fun createBlocking (Lkotlin/jvm/functions/Function0;Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/StandardCharImageLoginSolver;
+	public fun createQRCodeLoginListener (Lnet/mamoe/mirai/Bot;)Lnet/mamoe/mirai/auth/QRCodeLoginListener;
 	public fun isSliderCaptchaSupported ()Z
 	public fun onSolveDeviceVerification (Lnet/mamoe/mirai/Bot;Lnet/mamoe/mirai/utils/DeviceVerificationRequests;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public fun onSolvePicCaptcha (Lnet/mamoe/mirai/Bot;[BLkotlin/coroutines/Continuation;)Ljava/lang/Object;

+ 52 - 0
mirai-core-api/src/commonMain/kotlin/BotFactory.kt

@@ -11,6 +11,7 @@
 
 package net.mamoe.mirai
 
+import net.mamoe.mirai.auth.BotAuthorization
 import net.mamoe.mirai.utils.BotConfiguration
 import kotlin.jvm.JvmSynthetic
 
@@ -111,6 +112,53 @@ public interface BotFactory {
      */
     public fun newBot(qq: Long, passwordMd5: ByteArray): Bot = newBot(qq, passwordMd5, BotConfiguration.Default)
 
+    ///////////////////////////////////////////////////////////////////////////
+    // BotAuthorization
+    ///////////////////////////////////////////////////////////////////////////
+
+    /**
+     * 使用 [默认配置][BotConfiguration.Default] 构造 [Bot] 实例
+     *
+     * @since 2.15
+     */
+    public fun newBot(qq: Long, authorization: BotAuthorization): Bot =
+        newBot(qq, authorization, BotConfiguration.Default)
+
+    /**
+     * 使用指定的 [配置][configuration] 构造 [Bot] 实例
+     *
+     * @since 2.15
+     */
+    public fun newBot(qq: Long, authorization: BotAuthorization, configuration: BotConfiguration): Bot
+
+
+    /**
+     * 使用指定的 [配置][configuration] 构造 [Bot] 实例
+     *
+     * Kotlin:
+     * ```
+     * newBot(123, password) {
+     *     // this: BotConfiguration
+     *     fileBasedDeviceInfo()
+     * }
+     * ```
+     *
+     * Java:
+     * ```java
+     * newBot(123, password, configuration -> {
+     *     configuration.fileBasedDeviceInfo()
+     * })
+     * ```
+     *
+     * @since 2.15
+     */
+    public fun newBot(
+        qq: Long,
+        authorization: BotAuthorization,
+        configuration: BotConfigurationLambda /* = BotConfiguration.() -> Unit */
+    ): Bot = newBot(qq, authorization, configuration.run { BotConfiguration().apply { invoke() } })
+
+
     public companion object INSTANCE : BotFactory {
         override fun newBot(qq: Long, password: String, configuration: BotConfiguration): Bot {
             return Mirai.BotFactory.newBot(qq, password, configuration)
@@ -160,5 +208,9 @@ public interface BotFactory {
             passwordMd5: ByteArray,
             configuration: BotConfiguration.() -> Unit /* = BotConfiguration.() -> Unit */
         ): Bot = newBot(qq, passwordMd5, BotConfiguration().apply(configuration))
+
+        override fun newBot(qq: Long, authorization: BotAuthorization, configuration: BotConfiguration): Bot {
+            return Mirai.BotFactory.newBot(qq, authorization, configuration)
+        }
     }
 }

+ 128 - 0
mirai-core-api/src/commonMain/kotlin/auth/BotAuthorization.kt

@@ -0,0 +1,128 @@
+/*
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.auth
+
+import net.mamoe.mirai.BotFactory
+import net.mamoe.mirai.Mirai
+import net.mamoe.mirai.network.LoginFailedException
+import net.mamoe.mirai.network.RetryLaterException
+import net.mamoe.mirai.utils.*
+import kotlin.jvm.JvmStatic
+
+/**
+ * Bot 的登录鉴权方式
+ *
+ * @see BotFactory.newBot
+ *
+ * @since 2.15
+ */
+public interface BotAuthorization {
+    /**
+     * 此方法控制 Bot 如何进行登录.
+     *
+     * Bot 只能使用一种登录方式, 但是可以在一种登录方式失败的时候尝试其他登录方式
+     *
+     * ## 异常类型
+     *
+     * 抛出一个 [LoginFailedException] 以正常地终止登录, 并可建议系统进行重连或停止 bot (通过 [LoginFailedException.killBot]).
+     * 例如抛出 [RetryLaterException] 可让 bot 重新进行一次登录.
+     *
+     * 抛出任意其他 [Throwable] 将视为鉴权选择器的自身错误.
+     *
+     * ## 示例代码
+     * ```kotlin
+     * override suspend fun authorize(
+     *      authComponent: BotAuthSession,
+     *      bot: BotAuthInfo,
+     * ) {
+     *      return kotlin.runCatching {
+     *          authComponent.authByQRCode()
+     *      }.recover {
+     *          authComponent.authByPassword("...")
+     *      }.getOrThrow()
+     * }
+     * ```
+     */
+    public suspend fun authorize(
+        session: BotAuthSession,
+        info: BotAuthInfo,
+    ): BotAuthResult
+
+
+    /**
+     * 计算 `cache/account.secrets` 的加密秘钥
+     */
+    public fun calculateSecretsKey(
+        bot: BotAuthInfo,
+    ): ByteArray = bot.deviceInfo.guid + bot.id.toByteArray()
+
+    public companion object {
+        @JvmStatic
+        public fun byPassword(password: String): BotAuthorization = byPassword(password.md5())
+
+        @JvmStatic
+        public fun byPassword(passwordMd5: ByteArray): BotAuthorization = factory.byPassword(passwordMd5)
+
+        @JvmStatic
+        public fun byQRCode(): BotAuthorization = factory.byQRCode()
+
+        public operator fun invoke(
+            block: suspend (BotAuthSession, BotAuthInfo) -> BotAuthResult
+        ): BotAuthorization {
+            return object : BotAuthorization {
+                override suspend fun authorize(
+                    session: BotAuthSession,
+                    info: BotAuthInfo
+                ): BotAuthResult {
+                    return block(session, info)
+                }
+            }
+        }
+
+        private val factory: DefaultBotAuthorizationFactory by lazy {
+            Mirai // Ensure services loaded
+            loadService()
+        }
+    }
+}
+
+@NotStableForInheritance
+public interface BotAuthResult
+
+@NotStableForInheritance
+public interface BotAuthInfo {
+    public val id: Long
+    public val deviceInfo: DeviceInfo
+    public val configuration: BotConfiguration
+}
+
+@NotStableForInheritance
+public interface BotAuthSession {
+    /**
+     * @throws LoginFailedException
+     */
+    public suspend fun authByPassword(password: String): BotAuthResult
+
+    /**
+     * @throws LoginFailedException
+     */
+    public suspend fun authByPassword(passwordMd5: ByteArray): BotAuthResult
+
+    /**
+     * @throws LoginFailedException
+     */
+    public suspend fun authByQRCode(): BotAuthResult
+}
+
+
+internal interface DefaultBotAuthorizationFactory {
+    fun byPassword(passwordMd5: ByteArray): BotAuthorization
+    fun byQRCode(): BotAuthorization
+}

+ 95 - 0
mirai-core-api/src/commonMain/kotlin/auth/QRCodeLoginListener.kt

@@ -0,0 +1,95 @@
+/*
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.auth
+
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.network.LoginFailedException
+
+/**
+ * 二维码扫描登录监听器
+ *
+ * @since 2.15
+ */
+public interface QRCodeLoginListener {
+
+    /**
+     * 使用二维码登录时获取的二维码图片大小字节数.
+     */
+    public val qrCodeSize: Int get() = 3
+
+    /**
+     * 使用二维码登录时获取的二维码边框宽度像素.
+     */
+    public val qrCodeMargin: Int get() = 4
+
+    /**
+     * 使用二维码登录时获取的二维码校正等级,必须为 1-3 之间.
+     */
+    public val qrCodeEcLevel: Int get() = 2
+
+    /**
+     * 每隔 [qrCodeStateUpdateInterval] 毫秒更新一次[二维码状态][State]
+     */
+    public val qrCodeStateUpdateInterval: Long get() = 5000
+
+    /**
+     * 从服务器获取二维码时调用,在下级显示二维码并扫描.
+     *
+     * @param data 二维码图像数据 (文件)
+     */
+    public fun onFetchQRCode(bot: Bot, data: ByteArray)
+
+    /**
+     * 当二维码状态变化时调用.
+     * @see State
+     */
+    public fun onStateChanged(bot: Bot, state: State)
+
+    /**
+     * 每隔一段时间会调用一次此函数
+     *
+     * 在此函数抛出 [LoginFailedException] 以中断登录
+     */
+    public fun onIntervalLoop() {
+    }
+
+    public enum class State {
+        /**
+         * 等待扫描中,请在此阶段请扫描二维码.
+         * @see QRCodeLoginListener.onFetchQRCode
+         */
+        WAITING_FOR_SCAN,
+
+        /**
+         * 二维码已扫描,等待扫描端确认登录.
+         */
+        WAITING_FOR_CONFIRM,
+
+        /**
+         * 扫描后取消了确认.
+         */
+        CANCELLED,
+
+        /**
+         * 二维码超时,必须重新获取二维码.
+         */
+        TIMEOUT,
+
+        /**
+         * 二维码已确认,将会继续登录.
+         */
+        CONFIRMED,
+
+        /**
+         * 默认状态,在登录前通常为此状态.
+         */
+        DEFAULT,
+    }
+}

+ 37 - 3
mirai-core-api/src/commonMain/kotlin/network/LoginFailedException.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
  *
  * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
  * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@@ -37,6 +37,21 @@ public class WrongPasswordException @MiraiInternalApi constructor(
     message: String?
 ) : LoginFailedException(true, message)
 
+/**
+ * 二维码扫码账号与 BOT 账号不一致。
+ *
+ * @since 2.15
+ */
+public class InconsistentBotIdException @MiraiInternalApi constructor(
+    public val expected: Long,
+    public val actual: Long,
+    message: String? = null
+) : LoginFailedException(
+    true,
+    message
+        ?: "trying to logging in a bot whose id is different from the one provided to BotFactory.newBot, expected=$expected, actual=$actual."
+)
+
 /**
  * 无可用服务器
  */
@@ -60,16 +75,35 @@ public class NoStandardInputForCaptchaException @MiraiInternalApi constructor(
     public override val cause: Throwable? = null
 ) : LoginFailedException(true, "no standard input for captcha")
 
+/**
+ * 当前 [LoginSolver] 不支持此验证方式
+ *
+ * @since 2.15
+ */
+public open class UnsupportedCaptchaMethodException : LoginFailedException {
+    public constructor(killBot: Boolean) : super(killBot)
+    public constructor(killBot: Boolean, message: String?) : super(killBot, message)
+    public constructor(killBot: Boolean, message: String?, cause: Throwable?) : super(killBot, message, cause)
+    public constructor(killBot: Boolean, cause: Throwable?) : super(killBot, cause = cause)
+}
+
 /**
  * 需要强制短信验证, 且当前 [LoginSolver] 不支持时抛出.
  * @since 2.13
  */
-public class UnsupportedSmsLoginException(message: String?) : LoginFailedException(true, message)
+public class UnsupportedSmsLoginException(message: String?) : UnsupportedCaptchaMethodException(true, message)
 
 /**
  * 无法完成滑块验证
  */
-public class UnsupportedSliderCaptchaException(message: String?) : LoginFailedException(true, message)
+public class UnsupportedSliderCaptchaException(message: String?) : UnsupportedCaptchaMethodException(true, message)
+
+/**
+ * 需要二维码登录, 且当前 [LoginSolver] 不支持时抛出
+ *
+ * @since 2.15
+ */
+public class UnsupportedQRCodeCaptchaException(message: String?) : UnsupportedCaptchaMethodException(true, message)
 
 /**
  * 非 mirai 实现的异常

+ 18 - 1
mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
  *
  * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
  * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@@ -13,8 +13,12 @@ package net.mamoe.mirai.utils
 
 import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.Bot
+import net.mamoe.mirai.auth.BotAuthSession
+import net.mamoe.mirai.auth.BotAuthorization
+import net.mamoe.mirai.auth.QRCodeLoginListener
 import net.mamoe.mirai.network.LoginFailedException
 import net.mamoe.mirai.network.RetryLaterException
+import net.mamoe.mirai.network.UnsupportedQRCodeCaptchaException
 import net.mamoe.mirai.network.UnsupportedSmsLoginException
 import net.mamoe.mirai.utils.LoginSolver.Companion.Default
 import kotlin.jvm.JvmField
@@ -49,6 +53,19 @@ public abstract class LoginSolver {
      */
     public open val isSliderCaptchaSupported: Boolean get() = PlatformLoginSolverImplementations.isSliderCaptchaSupported
 
+    /**
+     * 当使用二维码登录时会通过此方法创建二维码登录监听器
+     *
+     * @see QRCodeLoginListener
+     * @see BotAuthorization
+     * @see BotAuthSession.authByQRCode
+     *
+     * @since 2.15
+     */
+    public open fun createQRCodeLoginListener(bot: Bot): QRCodeLoginListener {
+        throw UnsupportedQRCodeCaptchaException("This login session requires QRCode login, but current LoginSolver($this) does not support it. Please override `LoginSolver.createQRCodeLoginListener`.")
+    }
+
     /**
      * 处理滑动验证码.
      *

+ 156 - 2
mirai-core-api/src/jvmMain/kotlin/utils/LoginSolver.jvm.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
  *
  * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
  * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@@ -17,6 +17,7 @@ import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
 import kotlinx.coroutines.withContext
 import net.mamoe.mirai.Bot
+import net.mamoe.mirai.auth.QRCodeLoginListener
 import net.mamoe.mirai.network.NoStandardInputForCaptchaException
 import net.mamoe.mirai.utils.StandardCharImageLoginSolver.Companion.createBlocking
 import java.awt.Image
@@ -57,6 +58,105 @@ public class StandardCharImageLoginSolver @JvmOverloads constructor(
     }
 
     override val isSliderCaptchaSupported: Boolean get() = true
+    override fun createQRCodeLoginListener(bot: Bot): QRCodeLoginListener {
+        return object : QRCodeLoginListener {
+            private var tmpFile: File? = null
+
+            override val qrCodeMargin: Int get() = 1
+            override val qrCodeSize: Int get() = 1
+
+            override fun onFetchQRCode(bot: Bot, data: ByteArray) {
+                val logger = loggerSupplier(bot)
+
+                logger.info { "[QRCodeLogin] 已获取登录二维码,请在手机 QQ 使用账号 ${bot.id} 扫码" }
+                logger.info { "[QRCodeLogin] Fetched login qrcode, please scan via qq android with account ${bot.id}." }
+
+                try {
+                    val tempFile: File
+                    if (tmpFile == null) {
+                        tempFile = File.createTempFile(
+                            "mirai-qrcode-${bot.id}-${currentTimeSeconds()}",
+                            ".png"
+                        ).apply { deleteOnExit() }
+
+                        tempFile.createNewFile()
+
+                        tmpFile = tempFile
+                    } else {
+                        tempFile = tmpFile!!
+                    }
+
+                    tempFile.writeBytes(data)
+                    logger.info { "[QRCodeLogin] 将会显示二维码图片,若看不清图片,请查看文件 ${tempFile.absolutePath}" }
+                    logger.info { "[QRCodeLogin] Displaying qrcode image. If not clear, view file ${tempFile.absolutePath}." }
+                } catch (e: Exception) {
+                    logger.warning("[QRCodeLogin] 无法写出二维码图片. 请尽量关闭终端个性化样式后扫描二维码字符图片", e)
+                    logger.warning(
+                        "[QRCodeLogin] Failed to export qrcode image. Please try to scan the char-image after disabling custom terminal style.",
+                        e
+                    )
+                }
+
+                data.inputStream().use { stream ->
+                    try {
+                        val isCacheEnabled = ImageIO.getUseCache()
+
+                        try {
+                            ImageIO.setUseCache(false)
+                            val img = ImageIO.read(stream)
+                            if (img == null) {
+                                logger.warning { "[QRCodeLogin] 无法创建字符图片. 请查看文件" }
+                                logger.warning { "[QRCodeLogin] Failed to create char-image. Please see the file." }
+                            } else {
+                                logger.info { "[QRCodeLogin] \n" + img.renderQRCode() }
+                            }
+                        } finally {
+                            ImageIO.setUseCache(isCacheEnabled)
+                        }
+
+                    } catch (throwable: Throwable) {
+                        logger.warning("[QRCodeLogin] 创建字符图片时出错. 请查看文件.", throwable)
+                        logger.warning("[QRCodeLogin] Failed to create char-image. Please see the file.", throwable)
+                    }
+                }
+            }
+
+            override fun onStateChanged(bot: Bot, state: QRCodeLoginListener.State) {
+                val logger = loggerSupplier(bot)
+                logger.info {
+                    buildString {
+                        append("[QRCodeLogin] ")
+                        when (state) {
+                            QRCodeLoginListener.State.WAITING_FOR_SCAN -> append("等待扫描二维码中")
+                            QRCodeLoginListener.State.WAITING_FOR_CONFIRM -> append("扫描完成,请在手机 QQ 确认登录")
+                            QRCodeLoginListener.State.CANCELLED -> append("已取消登录,将会重新获取二维码")
+                            QRCodeLoginListener.State.TIMEOUT -> append("扫描超时,将会重新获取二维码")
+                            QRCodeLoginListener.State.CONFIRMED -> append("已确认登录")
+                            else -> append("default state")
+                        }
+                    }
+                }
+                logger.info {
+                    buildString {
+                        append("[QRCodeLogin] ")
+                        when (state) {
+                            QRCodeLoginListener.State.WAITING_FOR_SCAN -> append("Waiting for scanning qrcode.")
+                            QRCodeLoginListener.State.WAITING_FOR_CONFIRM -> append("Scan complete. Please confirm login.")
+                            QRCodeLoginListener.State.CANCELLED -> append("Login cancelled, we will try to fetch qrcode again.")
+                            QRCodeLoginListener.State.TIMEOUT -> append("Timeout scanning, we will try to fetch qrcode again.")
+                            QRCodeLoginListener.State.CONFIRMED -> append("Login confirmed.")
+                            else -> append("default state")
+                        }
+                    }
+                }
+
+                if (state == QRCodeLoginListener.State.CONFIRMED) {
+                    kotlin.runCatching { tmpFile?.delete() }.onFailure { logger.warning(it) }
+                }
+            }
+
+        }
+    }
 
     override suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String? = loginSolverLock.withLock {
         val logger = loggerSupplier(bot)
@@ -68,7 +168,7 @@ public class StandardCharImageLoginSolver @JvmOverloads constructor(
             try {
                 tempFile.writeBytes(data)
                 logger.info { "[PicCaptcha] 将会显示字符图片. 若看不清字符图片, 请查看文件 ${tempFile.absolutePath}" }
-                logger.info { "[PicCaptcha] Displaying char-image. If not clear, view file ${tempFile.absolutePath}" }
+                logger.info { "[PicCaptcha] Displaying char-image. If not clear, view file ${tempFile.absolutePath}." }
             } catch (e: Exception) {
                 logger.warning("[PicCaptcha] 无法写出验证码文件, 请尝试查看以上字符图片", e)
                 logger.warning("[PicCaptcha] Failed to export captcha image. Please see the char-image.", e)
@@ -282,3 +382,57 @@ private fun BufferedImage.createCharImg(outputWidth: Int = 100, ignoreRate: Doub
         }
     }
 }
+
+private fun BufferedImage.renderQRCode(
+    blackPlaceholder: String = "   ",
+    whitePlaceholder: String = "   ",
+    doColorSwitch: Boolean = true,
+): String {
+    var lastStatus: Boolean? = null
+
+    fun isBlackBlock(rgb: Int): Boolean {
+        val r = rgb and 0xff0000 shr 16
+        val g = rgb and 0x00ff00 shr 8
+        val b = rgb and 0x0000ff
+
+        return r < 10 && g < 10 && b < 10
+    }
+
+    val sb = StringBuilder()
+    sb.append("\n")
+
+    val BLACK = "\u001b[30;40m"
+    val WHITE = "\u001b[97;107m"
+    val RESET = "\u001b[0m"
+
+    for (y in 0 until height) {
+        for (x in 0 until width) {
+            val rgbcolor = getRGB(x, y)
+            val crtStatus = isBlackBlock(rgbcolor)
+
+            if (doColorSwitch && crtStatus != lastStatus) {
+                lastStatus = crtStatus
+                sb.append(
+                    if (crtStatus) BLACK else WHITE
+                )
+            }
+
+            sb.append(
+                if (crtStatus) blackPlaceholder else whitePlaceholder
+            )
+        }
+
+        if (doColorSwitch) {
+            sb.append(RESET)
+        }
+
+        sb.append("\n")
+        lastStatus = null
+    }
+
+    if (doColorSwitch) {
+        sb.append(RESET)
+    }
+
+    return sb.toString()
+}

+ 8 - 0
mirai-core-mock/src/internal/MockBotFactoryImpl.kt

@@ -10,6 +10,7 @@
 package net.mamoe.mirai.mock.internal
 
 import net.mamoe.mirai.Bot
+import net.mamoe.mirai.auth.BotAuthorization
 import net.mamoe.mirai.message.data.Image
 import net.mamoe.mirai.mock.MockBot
 import net.mamoe.mirai.mock.MockBotFactory
@@ -115,4 +116,11 @@ internal class MockBotFactoryImpl : MockBotFactory {
             .configuration(configuration)
             .create()
     }
+
+    override fun newBot(qq: Long, authorization: BotAuthorization, configuration: BotConfiguration): Bot {
+        return newMockBotBuilder()
+            .id(qq)
+            .configuration(configuration)
+            .create()
+    }
 }

+ 91 - 0
mirai-core-utils/src/commonMain/kotlin/SecretsProtection.kt

@@ -0,0 +1,91 @@
+/*
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.utils
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlin.jvm.JvmInline
+import kotlin.jvm.JvmStatic
+
+/**
+ * 核心数据保护器
+ *
+ * ### Why
+ *
+ * 有时候可能会发生 `OutOfMemoryError`, 如果存在 `-XX:+HeapDumpOnOutOfMemoryError`, 则 JVM 会生成一份系统内存打印以供 debug.
+ * 该报告包含全部内存信息, 包括各种数据, 核心数据以及, 机密数据 (如密码)
+ *
+ * 该内存报告唯一没有包含的数据就是 Native层数据, 包括且不限于
+ *
+ * - `sun.misc.Unsafe.allocate()`
+ * - `java.nio.ByteBuffer.allocateDirect()` (Named `DirectByteBuffer`)
+ * - C/C++ (或其他语言) 的数据
+ *
+ * *试验数据来源 `openjdk version "17" 2021-09-14, 64-Bit Server VM (build 17+35-2724, mixed mode, sharing)`*
+ *
+ * ### How it works
+ *
+ * 因为 Heap Dump 不存在 `DirectByteBuffer` 的实际数据, 所以可以通过该类隐藏关键数据. 等需要的时候在读取出来.
+ * 因为数据并没有直接存在于某个类字段中, 缺少数据关联, 也很难分析相关数据是什么数据
+ */
+@Suppress("NOTHING_TO_INLINE", "UsePropertyAccessSyntax")
+//@MiraiExperimentalApi
+public object SecretsProtection {
+
+    @JvmInline
+    @Serializable(EscapedStringSerializer::class)
+    public value class EscapedString(
+        public val data: Any,
+    ) {
+        public val asString: String
+            get() = SecretsProtectionPlatform.impl_asString(data)
+
+        public constructor(data: ByteArray) : this(escape(data))
+        public constructor(data: String) : this(escape(data.encodeToByteArray()))
+    }
+
+    @JvmInline
+    @Serializable(EscapedByteBufferSerializer::class)
+    public value class EscapedByteBuffer(
+        public val data: Any,
+    ) {
+        public val size: Int get() = SecretsProtectionPlatform.impl_getSize(data)
+
+        public val asByteArray: ByteArray
+            get() = SecretsProtectionPlatform.impl_asByteArray(data)
+
+        public constructor(data: ByteArray) : this(escape(data))
+    }
+
+    @JvmStatic
+    public fun escape(data: ByteArray): Any {
+        return SecretsProtectionPlatform.escape(data)
+    }
+
+
+    public object EscapedStringSerializer :
+        KSerializer<EscapedString> by SecretsProtectionPlatform.EscapedStringSerializer
+
+    public object EscapedByteBufferSerializer :
+        KSerializer<EscapedByteBuffer> by SecretsProtectionPlatform.EscapedByteBufferSerializer
+}
+
+
+internal expect object SecretsProtectionPlatform {
+    fun impl_asString(data: Any): String
+    fun impl_asByteArray(data: Any): ByteArray
+    fun impl_getSize(data: Any): Int
+
+    fun escape(data: ByteArray): Any
+
+    object EscapedStringSerializer : KSerializer<SecretsProtection.EscapedString>
+
+    object EscapedByteBufferSerializer : KSerializer<SecretsProtection.EscapedByteBuffer>
+}

+ 26 - 49
mirai-core-utils/src/jvmBaseMain/kotlin/SecretsProtection.kt

@@ -10,39 +10,15 @@
 package net.mamoe.mirai.utils
 
 import kotlinx.serialization.KSerializer
-import kotlinx.serialization.Serializable
 import kotlinx.serialization.builtins.ByteArraySerializer
 import kotlinx.serialization.builtins.serializer
 import java.nio.ByteBuffer
-import java.util.concurrent.ConcurrentLinkedDeque
 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater
 import java.util.concurrent.locks.Lock
 import java.util.concurrent.locks.ReentrantLock
 
-/**
- * 核心数据保护器
- *
- * ### Why
- *
- * 有时候可能会发生 `OutOfMemoryError`, 如果存在 `-XX:+HeapDumpOnOutOfMemoryError`, 则 JVM 会生成一份系统内存打印以供 debug.
- * 该报告包含全部内存信息, 包括各种数据, 核心数据以及, 机密数据 (如密码)
- *
- * 该内存报告唯一没有包含的数据就是 Native层数据, 包括且不限于
- *
- * - `sun.misc.Unsafe.allocate()`
- * - `java.nio.ByteBuffer.allocateDirect()` (Named `DirectByteBuffer`)
- * - C/C++ (或其他语言) 的数据
- *
- * *试验数据来源 `openjdk version "17" 2021-09-14, 64-Bit Server VM (build 17+35-2724, mixed mode, sharing)`*
- *
- * ### How it works
- *
- * 因为 Heap Dump 不存在 `DirectByteBuffer` 的实际数据, 所以可以通过该类隐藏关键数据. 等需要的时候在读取出来.
- * 因为数据并没有直接存在于某个类字段中, 缺少数据关联, 也很难分析相关数据是什么数据
- */
-@Suppress("NOTHING_TO_INLINE", "UsePropertyAccessSyntax")
-//@MiraiExperimentalApi
-public object SecretsProtection {
+internal actual object SecretsProtectionPlatform {
+
     private class NativeBufferWithLock(
         @JvmField val buffer: ByteBuffer,
         val lock: Lock = ReentrantLock(),
@@ -106,7 +82,7 @@ public object SecretsProtection {
 
      */
     @JvmStatic
-    public fun allocate(size: Int): ByteBuffer {
+    fun allocate(size: Int): ByteBuffer {
         if (size >= bufferSize) {
             return ByteBuffer.allocateDirect(size)
         }
@@ -171,39 +147,40 @@ public object SecretsProtection {
     }
 
     @JvmStatic
-    public fun escape(data: ByteArray): ByteBuffer {
+    actual fun escape(data: ByteArray): Any {
         return allocate(data.size).also {
             it.put(data)
             it.pos = 0
         }
     }
 
-    @JvmInline
-    @Serializable(EscapedStringSerializer::class)
-    public value class EscapedString(
-        public val data: ByteBuffer,
-    ) {
-        public val asString: String
-            get() = data.duplicate().readString()
+    actual fun impl_asString(data: Any): String {
+        data as ByteBuffer
+
+        return data.duplicate().readString()
     }
 
-    @JvmInline
-    @Serializable(EscapedByteBufferSerializer::class)
-    public value class EscapedByteBuffer(
-        public val data: ByteBuffer,
-    )
+    actual fun impl_asByteArray(data: Any): ByteArray {
+        data as ByteBuffer
+        return data.duplicate().readBytes()
+    }
+
+    actual fun impl_getSize(data: Any): Int {
+        return (data as ByteBuffer).remaining
+    }
 
-    public object EscapedStringSerializer : KSerializer<EscapedString> by String.serializer().map(
+    actual object EscapedStringSerializer : KSerializer<SecretsProtection.EscapedString> by String.serializer().map(
         String.serializer().descriptor.copy("EscapedString"),
-        deserialize = { EscapedString(escape(it.toByteArray())) },
-        serialize = { it.data.duplicate().readString() }
+        deserialize = { SecretsProtection.EscapedString(escape(it.toByteArray())) },
+        serialize = { it.data.cast<ByteBuffer>().duplicate().readString() }
     )
 
-    public object EscapedByteBufferSerializer : KSerializer<EscapedByteBuffer> by ByteArraySerializer().map(
-        ByteArraySerializer().descriptor.copy("EscapedByteBuffer"),
-        deserialize = { EscapedByteBuffer(escape(it)) },
-        serialize = { it.data.duplicate().readBytes() }
-    )
+    actual object EscapedByteBufferSerializer :
+        KSerializer<SecretsProtection.EscapedByteBuffer> by ByteArraySerializer().map(
+            ByteArraySerializer().descriptor.copy("EscapedByteBuffer"),
+            deserialize = { SecretsProtection.EscapedByteBuffer(escape(it)) },
+            serialize = { it.data.cast<ByteBuffer>().duplicate().readBytes() }
+        )
 
 
-}
+}

+ 2 - 2
mirai-core-utils/src/jvmTest/kotlin/SecretsProtectionTest.kt

@@ -12,9 +12,9 @@ package net.mamoe.mirai.utils
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
+import java.nio.ByteBuffer
 import kotlin.test.Test
 import kotlin.test.assertContentEquals
-import kotlin.test.assertTrue
 
 internal class SecretsProtectionTest {
     @Test
@@ -22,7 +22,7 @@ internal class SecretsProtectionTest {
         repeat(500) {
             launch {
                 val data = ByteArray((1..255).random()) { (0..255).random().toByte() }
-                val buffer = SecretsProtection.escape(data)
+                val buffer = SecretsProtection.escape(data) as ByteBuffer
                 assertContentEquals(
                     data, buffer.duplicate().readBytes()
                 )

+ 47 - 0
mirai-core-utils/src/nativeMain/kotlin/SecretsProtection.kt

@@ -0,0 +1,47 @@
+/*
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.utils
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.builtins.ByteArraySerializer
+import kotlinx.serialization.builtins.serializer
+
+internal actual object SecretsProtectionPlatform {
+    actual fun impl_asString(data: Any): String {
+        return (data as ByteArray).decodeToString()
+    }
+
+    actual fun impl_asByteArray(data: Any): ByteArray {
+        return data as ByteArray
+    }
+
+    actual fun impl_getSize(data: Any): Int {
+        return data.cast<ByteArray>().size
+    }
+
+    actual fun escape(data: ByteArray): Any {
+        return data
+    }
+
+    actual object EscapedStringSerializer : KSerializer<SecretsProtection.EscapedString> by String.serializer().map(
+        String.serializer().descriptor.copy("EscapedString"),
+        deserialize = { SecretsProtection.EscapedString(it.encodeToByteArray()) },
+        serialize = { it.data.cast<ByteArray>().decodeToString() }
+    )
+
+    actual object EscapedByteBufferSerializer :
+        KSerializer<SecretsProtection.EscapedByteBuffer> by ByteArraySerializer().map(
+            ByteArraySerializer().descriptor.copy("EscapedByteBuffer"),
+            deserialize = { SecretsProtection.EscapedByteBuffer(it) },
+            serialize = { it.data.cast() }
+        )
+
+
+}

+ 19 - 9
mirai-core/src/commonMain/kotlin/BotAccount.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
  *
  * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
  * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@@ -10,16 +10,26 @@
 
 package net.mamoe.mirai.internal
 
+import net.mamoe.mirai.auth.BotAuthorization
+import net.mamoe.mirai.utils.SecretsProtection
+import net.mamoe.mirai.utils.TestOnly
 
-internal expect class BotAccount {
-    internal val id: Long
-    val phoneNumber: String
 
-    constructor(id: Long, passwordMd5: ByteArray, phoneNumber: String = "")
-    constructor(id: Long, passwordPlainText: String, phoneNumber: String = "")
+internal class BotAccount(
+    internal val id: Long,
+    val authorization: BotAuthorization,
+) {
+    @TestOnly // to be compatible with your local tests :)
+    constructor(
+        id: Long, pwd: String
+    ) : this(id, BotAuthorization.byPassword(pwd))
 
-    val passwordMd5: ByteArray
+    var accountSecretsKeyBuffer: SecretsProtection.EscapedByteBuffer? = null
+
+    val accountSecretsKey: ByteArray
+        get() {
+            accountSecretsKeyBuffer?.let { return it.asByteArray }
+            error("accountSecretsKey not yet available")
+        }
 
-    override fun equals(other: Any?): Boolean
-    override fun hashCode(): Int
 }

+ 7 - 2
mirai-core/src/commonMain/kotlin/BotFactory.kt

@@ -15,6 +15,7 @@ package net.mamoe.mirai.internal
 
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.BotFactory
+import net.mamoe.mirai.auth.BotAuthorization
 import net.mamoe.mirai.utils.BotConfiguration
 import net.mamoe.mirai.utils.DeprecatedSinceMirai
 
@@ -28,7 +29,7 @@ internal object BotFactoryImpl : BotFactory {
      * 使用指定的 [配置][configuration] 构造 [Bot] 实例
      */
     override fun newBot(qq: Long, password: String, configuration: BotConfiguration): Bot {
-        return QQAndroidBot(BotAccount(qq, password), configuration)
+        return QQAndroidBot(BotAccount(qq, BotAuthorization.byPassword(password)), configuration)
     }
 
     /**
@@ -38,5 +39,9 @@ internal object BotFactoryImpl : BotFactory {
         qq: Long,
         passwordMd5: ByteArray,
         configuration: BotConfiguration
-    ): Bot = QQAndroidBot(BotAccount(qq, passwordMd5), configuration)
+    ): Bot = QQAndroidBot(BotAccount(qq, BotAuthorization.byPassword(passwordMd5)), configuration)
+
+    override fun newBot(qq: Long, authorization: BotAuthorization, configuration: BotConfiguration): Bot {
+        return QQAndroidBot(BotAccount(qq, authorization), configuration)
+    }
 }

+ 4 - 0
mirai-core/src/commonMain/kotlin/QQAndroidBot.kt

@@ -209,6 +209,10 @@ internal open class QQAndroidBot constructor(
 
         set(SsoProcessorContext, SsoProcessorContextImpl(bot))
         set(SsoProcessor, SsoProcessorImpl(get(SsoProcessorContext)))
+        set(
+            QRCodeLoginProcessor,
+            QRCodeLoginProcessor.parse(get(SsoProcessorContext), networkLogger.subLogger("QRCodeLoginProcessor"))
+        )
 
         val cacheValidator = CacheValidatorImpl(
             get(SsoProcessorContext),

+ 1 - 1
mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt

@@ -169,7 +169,7 @@ internal open class QQAndroidClient(
     var t547: ByteArray? = null
 }
 
-internal val QQAndroidClient.apkId: ByteArray get() = "com.tencent.mobileqq".toByteArray()
+internal val QQAndroidClient.apkId: ByteArray get() = protocol.apkId.toByteArray()
 internal val QQAndroidClient.ssoVersion: Int get() = protocol.ssoVersion
 internal val QQAndroidClient.networkType: NetworkType get() = NetworkType.WIFI
 internal val QQAndroidClient.appClientVersion: Int get() = 0

+ 116 - 0
mirai-core/src/commonMain/kotlin/network/auth/AuthControl.kt

@@ -0,0 +1,116 @@
+/*
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.network.auth
+
+import net.mamoe.mirai.auth.BotAuthInfo
+import net.mamoe.mirai.auth.BotAuthResult
+import net.mamoe.mirai.auth.BotAuthorization
+import net.mamoe.mirai.internal.network.components.SsoProcessorImpl
+import net.mamoe.mirai.internal.utils.subLogger
+import net.mamoe.mirai.utils.*
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.cancellation.CancellationException
+
+
+/**
+ * Event sequence:
+ *
+ * 1. Starts a user coroutine [BotAuthorization.authorize].
+ * 2. User coroutine
+ */
+internal class AuthControl(
+    private val botAuthInfo: BotAuthInfo,
+    private val authorization: BotAuthorization,
+    private val logger: MiraiLogger,
+    parentCoroutineContext: CoroutineContext,
+) {
+    internal val exceptionCollector = ExceptionCollector()
+
+    private val userDecisions: OnDemandConsumer<Throwable?, SsoProcessorImpl.AuthMethod> =
+        CoroutineOnDemandValueScope(parentCoroutineContext, logger.subLogger("AuthControl/UserDecisions")) { _ ->
+            /**
+             * Implements [BotAuthSessionInternal] from API, to be called by the user, to receive user's decisions.
+             */
+            val sessionImpl = object : BotAuthSessionInternal() {
+                private val authResultImpl = object : BotAuthResult {}
+
+                override suspend fun authByPassword(passwordMd5: SecretsProtection.EscapedByteBuffer): BotAuthResult {
+                    runWrapInternalException {
+                        emit(SsoProcessorImpl.AuthMethod.Pwd(passwordMd5))
+                    }?.let { throw it }
+                    return authResultImpl
+                }
+
+                override suspend fun authByQRCode(): BotAuthResult {
+                    runWrapInternalException {
+                        emit(SsoProcessorImpl.AuthMethod.QRCode)
+                    }?.let { throw it }
+                    return authResultImpl
+                }
+
+                private inline fun <R> runWrapInternalException(block: () -> R): R {
+                    try {
+                        return block()
+                    } catch (e: IllegalProducerStateException) {
+                        if (e.lastStateWasSucceed) {
+                            throw IllegalStateException(
+                                "This login session has already completed. Please return the BotAuthResult you get from 'authBy*()' immediately",
+                                e
+                            )
+                        } else {
+                            throw e // internal bug
+                        }
+                    }
+                }
+            }
+
+            try {
+                logger.verbose { "[AuthControl/auth] Authorization started" }
+
+                authorization.authorize(sessionImpl, botAuthInfo)
+
+                logger.verbose { "[AuthControl/auth] Authorization exited" }
+                finish()
+            } catch (e: CancellationException) {
+                logger.verbose { "[AuthControl/auth] Authorization cancelled" }
+            } catch (e: Throwable) {
+                logger.verbose { "[AuthControl/auth] Authorization failed: $e" }
+                finishExceptionally(e)
+            }
+        }
+
+    init {
+        userDecisions.expectMore(null)
+    }
+
+    // Does not throw
+    suspend fun acquireAuth(): SsoProcessorImpl.AuthMethod {
+        logger.verbose { "[AuthControl/acquire] Acquiring auth method" }
+
+        val rsp = try {
+            userDecisions.receiveOrNull() ?: SsoProcessorImpl.AuthMethod.NotAvailable
+        } catch (e: ProducerFailureException) {
+            SsoProcessorImpl.AuthMethod.Error(e)
+        }
+
+        logger.debug { "[AuthControl/acquire] Authorization responded: $rsp" }
+        return rsp
+    }
+
+    fun actMethodFailed(cause: Throwable) {
+        logger.verbose { "[AuthControl/resume] Fire auth failed with cause: $cause" }
+        userDecisions.expectMore(cause)
+    }
+
+    fun actComplete() {
+        logger.verbose { "[AuthControl/resume] Fire auth completed" }
+        userDecisions.finish()
+    }
+}

+ 51 - 0
mirai-core/src/commonMain/kotlin/network/auth/BotAuthSession.kt

@@ -0,0 +1,51 @@
+/*
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.network.auth
+
+import net.mamoe.mirai.auth.BotAuthInfo
+import net.mamoe.mirai.auth.BotAuthResult
+import net.mamoe.mirai.auth.BotAuthSession
+import net.mamoe.mirai.auth.BotAuthorization
+import net.mamoe.mirai.utils.SecretsProtection
+import net.mamoe.mirai.utils.md5
+
+
+// With SecretsProtection support
+internal abstract class BotAuthSessionInternal : BotAuthSession {
+
+    final override suspend fun authByPassword(password: String): BotAuthResult {
+        return authByPassword(password.md5())
+    }
+
+    final override suspend fun authByPassword(passwordMd5: ByteArray): BotAuthResult {
+        return authByPassword(SecretsProtection.EscapedByteBuffer(passwordMd5))
+    }
+
+    abstract suspend fun authByPassword(passwordMd5: SecretsProtection.EscapedByteBuffer): BotAuthResult
+}
+
+// With SecretsProtection support
+internal abstract class BotAuthorizationWithSecretsProtection : BotAuthorization {
+    final override fun calculateSecretsKey(bot: BotAuthInfo): ByteArray {
+        return calculateSecretsKeyImpl(bot).asByteArray
+    }
+
+    abstract fun calculateSecretsKeyImpl(
+        bot: BotAuthInfo,
+    ): SecretsProtection.EscapedByteBuffer
+
+    abstract suspend fun authorize(session: BotAuthSessionInternal, bot: BotAuthInfo): BotAuthResult
+
+    final override suspend fun authorize(session: BotAuthSession, info: BotAuthInfo): BotAuthResult {
+        return authorize(session as BotAuthSessionInternal, info)
+    }
+}
+
+

+ 203 - 0
mirai-core/src/commonMain/kotlin/network/auth/CoroutineOnDemandValueScope.kt

@@ -0,0 +1,203 @@
+/*
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.network.auth
+
+import kotlinx.atomicfu.AtomicRef
+import kotlinx.atomicfu.atomic
+import kotlinx.atomicfu.loop
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.job
+import kotlinx.coroutines.launch
+import net.mamoe.mirai.utils.MiraiLogger
+import net.mamoe.mirai.utils.childScope
+import net.mamoe.mirai.utils.debug
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.cancellation.CancellationException
+
+
+internal class IllegalProducerStateException(
+    private val state: ProducerState<*, *>,
+    message: String? = state.toString(),
+    cause: Throwable? = null,
+) : IllegalStateException(message, cause) {
+    val lastStateWasSucceed get() = (state is ProducerState.Finished) && state.isSuccess
+}
+
+internal class CoroutineOnDemandValueScope<T, V>(
+    parentCoroutineContext: CoroutineContext,
+    private val logger: MiraiLogger,
+    private val producerCoroutine: suspend OnDemandProducerScope<T, V>.(initialTicket: T) -> Unit,
+) : OnDemandConsumer<T, V> {
+    private val coroutineScope = parentCoroutineContext.childScope("CoroutineOnDemandValueScope")
+
+    private val state: AtomicRef<ProducerState<T, V>> = atomic(ProducerState.JustInitialized())
+
+
+    inner class Producer(
+        private val initialTicket: T,
+    ) : OnDemandProducerScope<T, V> {
+        init {
+            coroutineScope.launch {
+                try {
+                    producerCoroutine(initialTicket)
+                } catch (_: CancellationException) {
+                    // ignored
+                } catch (e: Exception) {
+                    finishExceptionally(e)
+                }
+            }
+        }
+
+        override suspend fun emit(value: V): T {
+            state.loop { state ->
+                when (state) {
+                    is ProducerState.Finished -> throw state.createAlreadyFinishedException(null)
+                    is ProducerState.Producing -> {
+                        val deferred = state.deferred
+                        val consumingState = ProducerState.Consuming(
+                            state.producer,
+                            state.deferred,
+                            coroutineScope.coroutineContext
+                        )
+                        if (compareAndSetState(state, consumingState)) {
+                            deferred.complete(value) // produce a value
+                            return consumingState.producerLatch.acquire() // wait for producer to consume the previous value.
+                        }
+                    }
+
+                    else -> throw IllegalProducerStateException(state)
+                }
+            }
+        }
+
+        override fun finishExceptionally(exception: Throwable) {
+            finishImpl(exception)
+        }
+
+        override fun finish() {
+            state.loop { state ->
+                when (state) {
+                    is ProducerState.Finished -> throw state.createAlreadyFinishedException(null)
+                    else -> {
+                        if (compareAndSetState(state, ProducerState.Finished(state, null))) {
+                            return
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private fun finishImpl(exception: Throwable?) {
+        state.loop { state ->
+            when (state) {
+                is ProducerState.Finished -> throw state.createAlreadyFinishedException(exception)
+                else -> {
+                    if (compareAndSetState(state, ProducerState.Finished(state, exception))) {
+                        val cancellationException = kotlinx.coroutines.CancellationException("Finished", exception)
+                        coroutineScope.cancel(cancellationException)
+                        return
+                    }
+                }
+            }
+        }
+    }
+
+    private fun compareAndSetState(state: ProducerState<T, V>, newState: ProducerState<T, V>): Boolean {
+        return this.state.compareAndSet(state, newState).also {
+            logger.debug { "CAS: $state -> $newState: $it" }
+        }
+    }
+
+    override suspend fun receiveOrNull(): V? {
+        state.loop { state ->
+            when (state) {
+                is ProducerState.Producing -> {
+                    // still producing value
+
+                    state.deferred.await() // just wait for value, but does not return it.
+
+                    // The value will be completed in ProducerState.Consuming state,
+                    // but you cannot thread-safely assume current state is Consuming.
+
+                    // Here we will loop again, to atomically switch to Consumed state.
+                }
+
+                is ProducerState.Consuming -> {
+                    // value is ready, switch state to ProducerReady
+
+                    if (compareAndSetState(
+                            state,
+                            ProducerState.Consumed(state.producer, state.producerLatch)
+                        )
+                    ) {
+                        return try {
+                            state.value.await() // won't suspend, since value is already completed
+                        } catch (e: Exception) {
+                            throw ProducerFailureException(cause = e)
+                        }
+                    }
+                }
+
+                is ProducerState.Finished -> {
+                    state.exception?.let { err ->
+                        throw ProducerFailureException(cause = err)
+                    }
+                    return null
+                }
+                else -> throw IllegalProducerStateException(state)
+            }
+        }
+    }
+
+    override fun expectMore(ticket: T): Boolean {
+        state.loop { state ->
+            when (state) {
+                is ProducerState.JustInitialized -> {
+                    compareAndSetState(state, ProducerState.CreatingProducer { Producer(ticket) })
+                    // loop again
+                }
+
+                is ProducerState.CreatingProducer -> {
+                    compareAndSetState(state, ProducerState.ProducerReady(state.producer))
+                    // loop again
+                }
+
+                is ProducerState.ProducerReady -> {
+                    val deferred = CompletableDeferred<V>(coroutineScope.coroutineContext.job)
+                    if (!compareAndSetState(state, ProducerState.Producing(state.producer, deferred))) {
+                        deferred.cancel() // avoid leak
+                    }
+                    // loop again
+                }
+
+                is ProducerState.Producing -> return true // ok
+
+                is ProducerState.Consuming -> throw IllegalProducerStateException(state) // a value is already ready
+
+                is ProducerState.Consumed -> {
+                    if (compareAndSetState(state, ProducerState.ProducerReady(state.producer))) {
+                        // wake up producer async.
+                        state.producerLatch.resumeWith(Result.success(ticket))
+                        // loop again to switch state atomically to Producing. 
+                        // Do not do switch state directly here — async producer may race with you! 
+                    }
+                }
+
+                is ProducerState.Finished -> return false
+            }
+        }
+    }
+
+    override fun finish() {
+        finishImpl(null)
+    }
+}

+ 51 - 0
mirai-core/src/commonMain/kotlin/network/auth/DefaultBotAuthorizationFactoryImpl.kt

@@ -0,0 +1,51 @@
+/*
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.network.auth
+
+import net.mamoe.mirai.auth.BotAuthInfo
+import net.mamoe.mirai.auth.BotAuthResult
+import net.mamoe.mirai.auth.BotAuthSession
+import net.mamoe.mirai.auth.BotAuthorization
+import net.mamoe.mirai.utils.SecretsProtection.EscapedByteBuffer
+
+/**
+ * Provides default [BotAuthorization.byPassword] implementation.
+ * @see net.mamoe.mirai.auth.DefaultBotAuthorizationFactory
+ */
+@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "CANNOT_OVERRIDE_INVISIBLE_MEMBER")
+internal class DefaultBotAuthorizationFactoryImpl :
+    net.mamoe.mirai.auth.DefaultBotAuthorizationFactory {
+    override fun byPassword(passwordMd5: ByteArray): BotAuthorization {
+        val buffer = EscapedByteBuffer(passwordMd5)
+        return byPassword(buffer) // Avoid referring passwordMd5(ByteArray)
+    }
+
+    private fun byPassword(buffer: EscapedByteBuffer): BotAuthorization {
+        return object : BotAuthorizationWithSecretsProtection() {
+            override fun calculateSecretsKeyImpl(bot: BotAuthInfo): EscapedByteBuffer = buffer
+
+            override suspend fun authorize(
+                session: BotAuthSessionInternal,
+                bot: BotAuthInfo
+            ): BotAuthResult = session.authByPassword(buffer)
+
+            override fun toString(): String = "BotAuthorization.byPassword(<ERASED>)"
+        }
+    }
+
+    override fun byQRCode(): BotAuthorization {
+        return object : BotAuthorization {
+            override suspend fun authorize(session: BotAuthSession, info: BotAuthInfo): BotAuthResult =
+                session.authByQRCode()
+
+            override fun toString(): String = "BotAuthorization.byQRCode()"
+        }
+    }
+}

+ 51 - 0
mirai-core/src/commonMain/kotlin/network/auth/Latch.kt

@@ -0,0 +1,51 @@
+/*
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.network.auth
+
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.completeWith
+import kotlin.coroutines.CoroutineContext
+
+
+internal interface Latch<T> {
+    /**
+     * Suspends and waits to acquire the latch.
+     * @throws Throwable if [resumeWith] is called with [Result.Failure]
+     */
+    suspend fun acquire(): T
+
+    /**
+     * Release the latch, resuming the coroutines waiting for the latch.
+     *
+     * This function will return immediately unless a client is calling [acquire] concurrently.
+     */
+    fun resumeWith(result: Result<T>)
+}
+
+
+internal fun <T> Latch(parentCoroutineContext: CoroutineContext): Latch<T> = LatchImpl(parentCoroutineContext)
+
+private class LatchImpl<T>(
+    parentCoroutineContext: CoroutineContext
+) : Latch<T> {
+    private val deferred: CompletableDeferred<T> = CompletableDeferred(parentCoroutineContext[Job])
+
+
+    override suspend fun acquire(): T = deferred.await()
+
+    override fun resumeWith(result: Result<T>) {
+        if (!deferred.completeWith(result)) {
+            error("$this was already resumed")
+        }
+    }
+
+    override fun toString(): String = "LatchImpl($deferred)"
+}

+ 86 - 0
mirai-core/src/commonMain/kotlin/network/auth/OnDemandValueScope.kt

@@ -0,0 +1,86 @@
+/*
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.network.auth
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.ReceiveChannel
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.cancellation.CancellationException
+
+
+/**
+ * 按需供给的值制造器.
+ */
+internal interface OnDemandProducerScope<T, V> {
+    /**
+     * 挂起协程, 直到 [OnDemandConsumer] 期望接收一个 [V], 届时将 [value] 传递给 [OnDemandConsumer.receiveOrNull], 成为其返回值.
+     *
+     * 若在调用 [emit] 时已经有 [OnDemandConsumer] 正在等待, 则该 [OnDemandConsumer] 协程会立即[恢复][Continuation.resumeWith].
+     *
+     * 若 [OnDemandConsumer] 已经[完结][OnDemandConsumer.finish], [OnDemandProducerScope.emit] 会抛出 [IllegalProducerStateException].
+     */
+    suspend fun emit(value: V): T
+
+    /**
+     * 标记此 [OnDemandProducerScope] 在生产 [V] 的过程中出现错误.
+     *
+     * 这也会终止此 [OnDemandProducerScope], 随后 [OnDemandConsumer.receiveOrNull] 将会抛出 [ProducerFailureException].
+     */
+    fun finishExceptionally(exception: Throwable)
+
+    /**
+     * 标记此 [OnDemandProducerScope] 已经没有更多 [V] 可生产.
+     *
+     * 随后 [OnDemandConsumer.receiveOrNull] 将会抛出 [IllegalStateException].
+     */
+    fun finish()
+}
+
+/**
+ * 按需消费者.
+ *
+ * 与 [ReceiveChannel] 不同, [OnDemandConsumer] 只有在调用 [expectMore] 后才会期待[生产者][OnDemandProducerScope] 生产下一个 [V].
+ */
+internal interface OnDemandConsumer<T, V> {
+    /**
+     * 挂起协程并等待从 [OnDemandProducerScope] [接收][OnDemandProducerScope.emit]一个 [V].
+     *
+     * 当此函数被多个线程 (协程) 同时调用时, 只有一个线程挂起并获得 [V], 其他线程将会
+     *
+     * @throws ProducerFailureException 当 [OnDemandProducerScope.finishExceptionally] 时抛出.
+     * @throws CancellationException 当协程被取消时抛出
+     * @throws IllegalProducerStateException 当状态异常, 如未调用 [expectMore] 时抛出
+     */
+    @Throws(ProducerFailureException::class, CancellationException::class)
+    suspend fun receiveOrNull(): V?
+
+    /**
+     * 期待 [OnDemandProducerScope] 再生产一个 [V]. 期望生产后必须在之后调用 [receiveOrNull] 或 [finish] 来消耗生产的 [V].
+     *
+     * 在成功发起期待后返回 `true`; 在 [OnDemandProducerScope] 已经[完结][OnDemandProducerScope.finish] 时返回 `false`.
+     *
+     * @throws IllegalProducerStateException 当 [expectMore] 被调用后, 没有调用 [receiveOrNull] 就又调用了 [expectMore] 时抛出
+     */
+    fun expectMore(ticket: T): Boolean
+
+    /**
+     * 标记此 [OnDemandConsumer] 已经完结.
+     *
+     * 如果 [OnDemandProducerScope] 仍在运行, 将会 (正常地) 取消 [OnDemandProducerScope].
+     *
+     * 随后 [OnDemandProducerScope.emit] 将会抛出 [IllegalStateException].
+     */
+    fun finish()
+}
+
+internal class ProducerFailureException(
+    override val message: String? = null,
+    override val cause: Throwable?
+) : Exception()

+ 176 - 0
mirai-core/src/commonMain/kotlin/network/auth/ProducerState.kt

@@ -0,0 +1,176 @@
+/*
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.network.auth
+
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * Producer states.
+ */
+internal sealed interface ProducerState<T, V> {
+    /*
+     * 可变更状态的函数: [emit], [receiveOrNull], [expectMore], [finish], [finishExceptionally]
+     * 
+     * [emit] 和 [receiveOrNull] 为 suspend 函数, 在图中 "(suspend)" 表示挂起它们的协程, "(resume)" 表示恢复它们的协程.
+     * 
+     * "A ~~~~~~> B" 表示在切换为状态 A 后, 会挂起或恢复协程 B.
+     * 
+     * 
+     * 
+     *
+     *                                  JustInitialized 
+     *                                         |
+     *                                         | 调用 [expectMore]
+     *                                         |
+     *                                         V 
+     *                                  CreatingProducer 
+     *                                         |
+     *                                         | 
+     *                                         |                                                     
+     *                                         V                                                      
+     *                                   ProducerReady (从此用户协程作为 producer 在后台运行)             
+     *                                         |                                                      
+     *                                         |                                                      
+     *                                         |  <--------------------------------------------------
+     *                                         |                                                     \
+     *                                         V                                                      |
+     *                                     Producing   ([expectMore] 结束)                             |
+     *                                     |       \                                                  |
+     *                        调用          |       \                                                  |
+     *                    [receiveOrNull]  |        \ 调用 [emit]                                      |
+     *                                     /         \                                                |
+     *                                    /           \                                               |
+     *                                   /             \                                              |
+     *                                   |               \                                            |
+     *                                   |                \                                           |
+     *                                   |                 |-------------                             |
+     *                                   |                 |             \                            |
+     *                                   |                 |              |                           |
+     *                                   |                  \             |                           |
+     *                                   |                   \            |                           |
+     *                                   |                    \           |                           | 
+     *                                   |                     |          |                           |
+     *                                   V       (resume)      V          |                           |            
+     *              ([receiveOrNull] suspend) <~~~~~~~~~~~~ Consuming     |                           |                  
+     *                                   |                     /          |                           |
+     *                                   |                    /           |                           |
+     *                                   |   /---------------/            |                           |
+     *                                   |  / 调用 [receiveOrNull]         |                           |
+     *                                   | /                              |                           |
+     *                                   |/                               |                           |
+     *                                   |                                |                           |
+     *                                   |                                |                           |
+     *                                   V                                |                           |  
+     *         ([receiveOrNull] 结束)  Consumed                            |                           |
+     *                                   |                                |                           |
+     *                                   | 调用 [expectMore]               |                           |
+     *                                   |                                |                           |
+     *                                   V            (resume)            V                           |
+     *                             ProducerReady ~~~~~~~~~~~~~~~~> ([emit] suspend)                   |
+     *                                   |                                |                           |
+     *                                   |                                |                           |
+     *                                   |                                V                           |
+     *                                   |                           ([emit] 结束)                     |
+     *                                   |                                                            |
+     *                                   |------------------------------------------------------------+
+     *                                                             (返回顶部 Producing)
+     * 
+     * 
+     * 
+     * 在任意状态调用 [finish] 以及 [finishExceptionally], 可将状态转移到最终状态 [Finished].
+     * 
+     * 在一个状态中调用图中未说明的函数会抛出 [IllegalProducerStateException].
+     */
+
+    /**
+     * Override this function to produce good debug information
+     */
+    abstract override fun toString(): String
+
+    class JustInitialized<T, V> : ProducerState<T, V> {
+        override fun toString(): String = "JustInitialized"
+    }
+
+    sealed interface HasProducer<T, V> : ProducerState<T, V> {
+        val producer: OnDemandProducerScope<T, V>
+    }
+
+    // This is need — to ensure [launchProducer] is called exactly once.
+    class CreatingProducer<T, V>(
+        launchProducer: () -> OnDemandProducerScope<T, V>
+    ) : HasProducer<T, V> {
+        override val producer: OnDemandProducerScope<T, V> by lazy(launchProducer)
+        override fun toString(): String = "CreatingProducer"
+    }
+
+    class ProducerReady<T, V>(
+        override val producer: OnDemandProducerScope<T, V>,
+    ) : HasProducer<T, V> {
+        override fun toString(): String = "ProducerReady"
+    }
+
+    class Producing<T, V>(
+        override val producer: OnDemandProducerScope<T, V>,
+        val deferred: CompletableDeferred<V>,
+    ) : HasProducer<T, V> {
+        override fun toString(): String = "Producing(deferred.completed=${deferred.isCompleted})"
+    }
+
+    class Consuming<T, V>(
+        override val producer: OnDemandProducerScope<T, V>,
+        val value: Deferred<V>,
+        parentCoroutineContext: CoroutineContext,
+    ) : HasProducer<T, V> {
+        val producerLatch = Latch<T>(parentCoroutineContext)
+
+        override fun toString(): String {
+            val completed =
+                value.runCatching { getCompleted().toString() }.getOrNull() // getCompleted() is experimental
+            return "Consuming(value=$completed)"
+        }
+    }
+
+    class Consumed<T, V>(
+        override val producer: OnDemandProducerScope<T, V>,
+        val producerLatch: Latch<T>
+    ) : HasProducer<T, V> {
+        override fun toString(): String = "Consumed($producerLatch)"
+    }
+
+    class Finished<T, V>(
+        val previousState: ProducerState<T, V>,
+        val exception: Throwable?,
+    ) : ProducerState<T, V> {
+        val isSuccess get() = exception == null
+
+        fun createAlreadyFinishedException(cause: Throwable?): IllegalProducerStateException {
+            val exception = exception
+            return if (exception == null) {
+                IllegalProducerStateException(
+                    this,
+                    "Producer has already finished normally, but attempting to finish with the cause $cause. Previous state was: $previousState",
+                    cause = cause
+                )
+            } else {
+                IllegalProducerStateException(
+                    this,
+                    "Producer has already finished with the suppressed exception, but attempting to finish with the cause $cause. Previous state was: $previousState",
+                    cause = cause
+                ).apply {
+                    addSuppressed(exception)
+                }
+            }
+        }
+
+        override fun toString(): String = "Finished($previousState, $exception)"
+    }
+}

+ 2 - 2
mirai-core/src/commonMain/kotlin/network/components/AccountSecretsManager.kt

@@ -193,7 +193,7 @@ internal class FileCacheAccountSecretsManager(
     private fun getSecretsImpl(account: BotAccount): AccountSecrets? {
         if (!file.exists()) return null
         val loaded = kotlin.runCatching {
-            TEA.decrypt(file.readBytes(), account.passwordMd5).loadAs(AccountSecretsImpl.serializer())
+            TEA.decrypt(file.readBytes(), account.accountSecretsKey).loadAs(AccountSecretsImpl.serializer())
         }.getOrElse { e ->
             if (e.message == "Field 'ecdhInitialPublicKey' is required for type with serial name 'net.mamoe.mirai.internal.network.components.AccountSecretsImpl', but it was missing") {
                 logger.info { "Detected old account secrets, invalidating..." }
@@ -218,7 +218,7 @@ internal class FileCacheAccountSecretsManager(
             file.writeBytes(
                 TEA.encrypt(
                     AccountSecretsImpl(secrets).toByteArray(AccountSecretsImpl.serializer()),
-                    account.passwordMd5
+                    account.accountSecretsKey
                 )
             )
         }

+ 6 - 0
mirai-core/src/commonMain/kotlin/network/components/BotClientHolder.kt

@@ -18,6 +18,8 @@ import net.mamoe.mirai.utils.lateinitMutableProperty
 internal interface BotClientHolder {
     var client: QQAndroidClient
 
+    fun refreshClient()
+
     companion object : ComponentKey<BotClientHolder>
 }
 
@@ -27,6 +29,10 @@ internal class BotClientHolderImpl(
 ) : BotClientHolder {
     override var client: QQAndroidClient by lateinitMutableProperty { createClient(bot) }
 
+    override fun refreshClient() {
+        client = createClient(bot)
+    }
+
     private fun createClient(bot: QQAndroidBot): QQAndroidClient {
         val ssoContext = bot.components[SsoProcessorContext]
         val device = ssoContext.device

+ 65 - 36
mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt

@@ -17,6 +17,7 @@ import net.mamoe.mirai.internal.network.components.PacketCodec.Companion.PacketL
 import net.mamoe.mirai.internal.network.components.PacketCodecException.Kind.*
 import net.mamoe.mirai.internal.network.handler.selector.NetworkException
 import net.mamoe.mirai.internal.network.protocol.packet.*
+import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
 import net.mamoe.mirai.internal.utils.crypto.Ecdh
 import net.mamoe.mirai.internal.utils.crypto.TEA
 import net.mamoe.mirai.utils.*
@@ -96,58 +97,81 @@ internal class PacketCodecException(
 
 internal class PacketCodecImpl : PacketCodec {
 
-    override fun decodeRaw(client: SsoSession, input: ByteReadPacket): RawIncomingPacket = input.run {
-        // login
-        val flag1 = readInt()
+    override fun decodeRaw(
+        client: SsoSession,
+        input: ByteReadPacket
+    ): RawIncomingPacket = input.run {
+        // packet type
+        val type = readInt()
 
         PacketLogger.verbose { "开始处理一个包" }
 
-        val flag2 = readByte().toInt()
+        val encryptMethod = readByte().toInt()
         val flag3 = readByte().toInt()
-        if (flag3 != 0) {
-            throw PacketCodecException(
-                "Illegal flag3. Expected 0, whereas got $flag3. flag1=$flag1, flag2=$flag2. " +
-                        "Remaining=${this.readBytes().toUHexString()}",
+        val flag3Exception = if (flag3 != 0) {
+            PacketCodecException(
+                "Illegal flag3. Expected 0, whereas got $flag3. packet type=$type, encrypt method=$encryptMethod. ",
                 kind = PROTOCOL_UPDATED
             )
-        }
+        } else null
 
         readString(readInt() - 4)// uinAccount
 
         ByteArrayPool.useInstance(this.remaining.toInt()) { buffer ->
             val size = this.readAvailable(buffer)
 
-            when (flag2) {
-                2 -> TEA.decrypt(buffer, DECRYPTER_16_ZERO, size)
-                1 -> TEA.decrypt(buffer, client.wLoginSigInfo.d2Key, size)
-                0 -> buffer
-                else -> throw PacketCodecException("Unknown flag2=$flag2", PROTOCOL_UPDATED)
-            }.let { decryptedData ->
-                when (flag1) {
-                    0x0A -> parseSsoFrame(client, decryptedData)
-                    0x0B -> parseSsoFrame(client, decryptedData) // 这里可能是 uni?? 但测试时候发现结构跟 sso 一样.
-                    else -> throw PacketCodecException(
-                        "unknown flag1: ${flag1.toByte().toUHexString()}",
-                        PROTOCOL_UPDATED
-                    )
+            val raw = try {
+                when (encryptMethod) {
+                    2 -> TEA.decrypt(buffer, DECRYPTER_16_ZERO, size)
+                    1 -> TEA.decrypt(buffer, client.wLoginSigInfo.d2Key, size)
+                    0 -> buffer
+                    else -> throw PacketCodecException("Unknown encrypt type=$encryptMethod", PROTOCOL_UPDATED)
+                }.let { decryptedData ->
+                    when (type) {
+                        0x0A -> parseSsoFrame(client, decryptedData)
+                        0x0B -> parseSsoFrame(client, decryptedData) // 这里可能是 uni?? 但测试时候发现结构跟 sso 一样.
+                        else -> throw PacketCodecException(
+                            "unknown packet type: ${type.toByte().toUHexString()}",
+                            PROTOCOL_UPDATED
+                        )
+                    }
                 }
-            }.let { raw ->
-                when (flag2) {
-                    0, 1 -> RawIncomingPacket(raw.commandName, raw.sequenceId, raw.body.readBytes())
-                    2 -> RawIncomingPacket(
-                        raw.commandName,
-                        raw.sequenceId,
-                        raw.body.withUse {
-                            try {
-                                parseOicqResponse(client)
-                            } catch (e: Throwable) {
-                                throw PacketCodecException(e, PacketCodecException.Kind.OTHER)
-                            }
-                        }
+            } catch (e: Exception) {
+                throw e.also {
+                    if (flag3Exception != null) {
+                        it.addSuppressed(flag3Exception)
+                    }
+                }
+            }
+
+            if (flag3 != 0 && flag3Exception != null) {
+                if (raw.commandName == WtLogin.TransEmp.commandName) {
+                    PacketLogger.warning(
+                        "unknown flag3: $flag3 in packet ${WtLogin.TransEmp.commandName}, " +
+                                "which may means protocol is updated.",
+                        flag3Exception
                     )
-                    else -> error("unreachable")
+                } else {
+                    throw flag3Exception
                 }
             }
+
+            when (encryptMethod) {
+                0, 1 -> RawIncomingPacket(raw.commandName, raw.sequenceId, raw.body.readBytes())
+                2 -> RawIncomingPacket(
+                    raw.commandName,
+                    raw.sequenceId,
+                    raw.body.withUse {
+                        try {
+                            parseOicqResponse(client)
+                        } catch (e: Throwable) {
+                            throw PacketCodecException(e, PacketCodecException.Kind.OTHER)
+                        }
+                    }
+                )
+
+                else -> error("unreachable")
+            }
         }
     }
 
@@ -220,6 +244,7 @@ internal class PacketCodecImpl : PacketCodec {
                         }
                     }
                 }
+
                 1 -> {
                     input.discardExact(4)
                     input.inflateAllAvailable().let { bytes ->
@@ -231,6 +256,7 @@ internal class PacketCodecImpl : PacketCodec {
                         }
                     }
                 }
+
                 8 -> input
                 else -> throw PacketCodecException("Unknown dataCompressed flag: $dataCompressed", PROTOCOL_UPDATED)
             }
@@ -270,6 +296,7 @@ internal class PacketCodecImpl : PacketCodec {
                     qqEcdh.calculateQQShareKey(Ecdh.Instance.importPublicKey(readUShortLVByteArray()))
                 TEA.decrypt(data, peerShareKey)
             }
+
             3 -> {
                 val size = (this.remaining - 1).toInt()
                 // session
@@ -279,6 +306,7 @@ internal class PacketCodecImpl : PacketCodec {
                     length = size
                 )
             }
+
             0 -> {
                 if (client.loginState == 0) {
                     val size = (this.remaining - 1).toInt()
@@ -294,6 +322,7 @@ internal class PacketCodecImpl : PacketCodec {
                     TEA.decrypt(this.readBytes(), client.randomKey, length = size)
                 }
             }
+
             else -> error("Illegal encryption method. expected 0 or 4, got $encryptionMethod")
         }
     }

+ 167 - 0
mirai-core/src/commonMain/kotlin/network/components/QRCodeLoginProcessor.kt

@@ -0,0 +1,167 @@
+/*
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.network.components
+
+import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.delay
+import net.mamoe.mirai.auth.QRCodeLoginListener
+import net.mamoe.mirai.internal.network.QQAndroidClient
+import net.mamoe.mirai.internal.network.QRCodeLoginData
+import net.mamoe.mirai.internal.network.component.ComponentKey
+import net.mamoe.mirai.internal.network.handler.NetworkHandler
+import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
+import net.mamoe.mirai.internal.utils.MiraiProtocolInternal.Companion.asInternal
+import net.mamoe.mirai.utils.MiraiLogger
+import net.mamoe.mirai.utils.debug
+
+internal interface QRCodeLoginProcessor {
+    suspend fun process(handler: NetworkHandler, client: QQAndroidClient): QRCodeLoginData = error("Not implemented")
+
+    /**
+     * Allocate a special processor for once login request
+     */
+    fun prepareProcess(handler: NetworkHandler, client: QQAndroidClient): QRCodeLoginProcessor =
+        error("Not implemented")
+
+    companion object : ComponentKey<QRCodeLoginProcessor> {
+        internal val NOOP = object : QRCodeLoginProcessor {}
+
+        fun parse(ssoContext: SsoProcessorContext, logger: MiraiLogger): QRCodeLoginProcessor {
+            return QRCodeLoginProcessorPreLoaded(ssoContext, logger)
+        }
+    }
+}
+
+internal class QRCodeLoginProcessorPreLoaded(
+    private val ssoContext: SsoProcessorContext,
+    private val logger: MiraiLogger,
+) : QRCodeLoginProcessor {
+    override fun prepareProcess(handler: NetworkHandler, client: QQAndroidClient): QRCodeLoginProcessor {
+        check(ssoContext.bot.configuration.protocol.asInternal.supportsQRLogin) {
+            "The login protocol must be ANDROID_WATCH or MACOS while enabling qrcode login." +
+                    "Set it by `bot.configuration.protocol = BotConfiguration.MiraiProtocol.ANDROID_WATCH`."
+        }
+
+        val loginSolver = ssoContext.bot.configuration.loginSolver
+            ?: throw IllegalStateException(
+                "No LoginSolver found while enabling qrcode login. " +
+                        "Please provide by BotConfiguration.loginSolver. " +
+                        "For example use `BotFactory.newBot(...) { loginSolver = yourLoginSolver}` in Kotlin, " +
+                        "use `BotFactory.newBot(..., new BotConfiguration() {{ setLoginSolver(yourLoginSolver) }})` in Java."
+            )
+
+        val qrCodeLoginListener = loginSolver.createQRCodeLoginListener(client.bot)
+
+        return loginSolver.run {
+            QRCodeLoginProcessorImpl(qrCodeLoginListener, logger)
+        }
+    }
+}
+
+internal class QRCodeLoginProcessorImpl(
+    private val qrCodeLoginListener: QRCodeLoginListener,
+    private val logger: MiraiLogger,
+) : QRCodeLoginProcessor {
+
+    private var state = atomic(QRCodeLoginListener.State.DEFAULT)
+
+    private suspend fun requestQRCode(
+        handler: NetworkHandler,
+        client: QQAndroidClient
+    ): WtLogin.TransEmp.Response.FetchQRCode {
+        logger.debug { "requesting qrcode." }
+        val resp = handler.sendAndExpect(
+            WtLogin.TransEmp.FetchQRCode(
+                client,
+                size = qrCodeLoginListener.qrCodeSize,
+                margin = qrCodeLoginListener.qrCodeMargin,
+                ecLevel = qrCodeLoginListener.qrCodeEcLevel,
+            ),
+            attempts = 1
+        )
+        check(resp is WtLogin.TransEmp.Response.FetchQRCode) { "Cannot fetch qrcode, resp=$resp" }
+        qrCodeLoginListener.onFetchQRCode(handler.context.bot, resp.imageData)
+        return resp
+    }
+
+    private suspend fun queryQRCodeStatus(
+        handler: NetworkHandler,
+        client: QQAndroidClient,
+        sig: ByteArray
+    ): WtLogin.TransEmp.Response {
+        logger.debug { "querying qrcode state." }
+        val resp = handler.sendAndExpect(WtLogin.TransEmp.QueryQRCodeStatus(client, sig), attempts = 1, timeout = 500)
+        check(
+            resp is WtLogin.TransEmp.Response.QRCodeStatus || resp is WtLogin.TransEmp.Response.QRCodeConfirmed
+        ) { "Cannot query qrcode status, resp=$resp" }
+
+        val currentState = state.value
+        val newState = resp.mapProtocolState()
+        if (currentState != newState && state.compareAndSet(currentState, newState)) {
+            logger.debug { "qrcode state changed: $state" }
+            qrCodeLoginListener.onStateChanged(handler.context.bot, newState)
+        }
+        return resp
+    }
+
+    override suspend fun process(handler: NetworkHandler, client: QQAndroidClient): QRCodeLoginData {
+        main@ while (true) {
+            val qrCodeData = requestQRCode(handler, client)
+            state@ while (true) {
+                qrCodeLoginListener.onIntervalLoop()
+
+                when (val status = queryQRCodeStatus(handler, client, qrCodeData.sig)) {
+                    is WtLogin.TransEmp.Response.QRCodeConfirmed -> {
+                        return status.data
+                    }
+
+                    is WtLogin.TransEmp.Response.QRCodeStatus -> when (status.state) {
+                        WtLogin.TransEmp.Response.QRCodeStatus.State.TIMEOUT,
+                        WtLogin.TransEmp.Response.QRCodeStatus.State.CANCELLED -> {
+                            break@state
+                        }
+
+                        else -> {} // WAITING_FOR_SCAN or WAITING_FOR_CONFIRM
+                    }
+                    // status is FetchQRCode, which is unreachable.
+                    else -> {
+                        error("query qrcode status should not be FetchQRCode.")
+                    }
+                }
+
+                delay(qrCodeLoginListener.qrCodeStateUpdateInterval.coerceAtLeast(200L))
+            }
+        }
+    }
+
+    private fun WtLogin.TransEmp.Response.mapProtocolState(): QRCodeLoginListener.State {
+        return when (this) {
+            is WtLogin.TransEmp.Response.QRCodeStatus -> when (this.state) {
+                WtLogin.TransEmp.Response.QRCodeStatus.State.WAITING_FOR_SCAN ->
+                    QRCodeLoginListener.State.WAITING_FOR_SCAN
+
+                WtLogin.TransEmp.Response.QRCodeStatus.State.WAITING_FOR_CONFIRM ->
+                    QRCodeLoginListener.State.WAITING_FOR_CONFIRM
+
+                WtLogin.TransEmp.Response.QRCodeStatus.State.CANCELLED ->
+                    QRCodeLoginListener.State.CANCELLED
+
+                WtLogin.TransEmp.Response.QRCodeStatus.State.TIMEOUT ->
+                    QRCodeLoginListener.State.TIMEOUT
+            }
+
+            is WtLogin.TransEmp.Response.QRCodeConfirmed ->
+                QRCodeLoginListener.State.CONFIRMED
+
+            is WtLogin.TransEmp.Response.FetchQRCode ->
+                error("$this cannot be mapped to listener state.")
+        }
+    }
+}

+ 161 - 37
mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
  *
  * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
  * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@@ -11,13 +11,18 @@ package net.mamoe.mirai.internal.network.components
 
 import kotlinx.atomicfu.AtomicRef
 import kotlinx.atomicfu.atomic
-import net.mamoe.mirai.internal.QQAndroidBot
+import net.mamoe.mirai.auth.*
 import net.mamoe.mirai.internal.network.Packet
 import net.mamoe.mirai.internal.network.QQAndroidClient
+import net.mamoe.mirai.internal.network.QRCodeLoginData
 import net.mamoe.mirai.internal.network.WLoginSigInfo
+import net.mamoe.mirai.internal.network.auth.AuthControl
+import net.mamoe.mirai.internal.network.auth.BotAuthorizationWithSecretsProtection
 import net.mamoe.mirai.internal.network.component.ComponentKey
 import net.mamoe.mirai.internal.network.handler.NetworkHandler
+import net.mamoe.mirai.internal.network.handler.logger
 import net.mamoe.mirai.internal.network.handler.selector.NetworkException
+import net.mamoe.mirai.internal.network.handler.selector.SelectorRequireReconnectException
 import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType
 import net.mamoe.mirai.internal.network.protocol.packet.login.DeviceVerificationResultImpl
 import net.mamoe.mirai.internal.network.protocol.packet.login.SmsDeviceVerificationResult
@@ -30,10 +35,8 @@ import net.mamoe.mirai.network.LoginFailedException
 import net.mamoe.mirai.network.RetryLaterException
 import net.mamoe.mirai.network.UnsupportedSliderCaptchaException
 import net.mamoe.mirai.network.WrongPasswordException
+import net.mamoe.mirai.utils.*
 import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol
-import net.mamoe.mirai.utils.LoginSolver
-import net.mamoe.mirai.utils.info
-import net.mamoe.mirai.utils.withExceptionCollector
 import kotlin.coroutines.cancellation.CancellationException
 import kotlin.jvm.Volatile
 
@@ -142,37 +145,155 @@ internal class SsoProcessorImpl(
     override val ssoSession: SsoSession get() = client
     private val components get() = ssoContext.bot.components
 
+    private val botAuthInfo = object : BotAuthInfo {
+        override val id: Long
+            get() = ssoContext.bot.id
+        override val deviceInfo: DeviceInfo
+            get() = ssoContext.device
+        override val configuration: BotConfiguration
+            get() = ssoContext.bot.configuration
+    }
+
     /**
      * Do login. Throws [LoginFailedException] if failed
      */
-    override suspend fun login(handler: NetworkHandler) = withExceptionCollector {
-        components[CacheValidator].validate()
+    override suspend fun login(handler: NetworkHandler) {
+
+        fun initAuthControl() {
+            authControl = AuthControl(
+                botAuthInfo,
+                ssoContext.bot.account.authorization,
+                ssoContext.bot.network.logger,
+                ssoContext.bot.coroutineContext, // do not use network context because network may restart whilst auth control should keep alive
+            )
+        }
 
-        components[BdhSessionSyncer].loadServerListFromCache()
-        try {
+        suspend fun loginSuccess() {
+            components[AccountSecretsManager].saveSecrets(ssoContext.account, AccountSecretsImpl(client))
+            registerClientOnline(handler)
+            ssoContext.bot.logger.info { "Login successful." }
+        }
+
+        if (authControl == null) {
+            ssoContext.bot.account.let { account ->
+                if (account.accountSecretsKeyBuffer == null) {
+
+                    account.accountSecretsKeyBuffer = when (val authorization = account.authorization) {
+                        is BotAuthorizationWithSecretsProtection -> authorization.calculateSecretsKeyImpl(botAuthInfo)
+                        else -> SecretsProtection.EscapedByteBuffer(authorization.calculateSecretsKey(botAuthInfo))
+                    }
+                }
+            }
+
+            components[CacheValidator].validate()
+
+            components[BdhSessionSyncer].loadServerListFromCache()
+
+            // try fast login
             if (client.wLoginSigInfoInitialized) {
                 ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyEcdh()
                 kotlin.runCatching {
                     FastLoginImpl(handler).doLogin()
                 }.onFailure { e ->
-                    collectException(e)
-                    SlowLoginImpl(handler).doLogin()
+                    initAuthControl()
+                    authControl!!.exceptionCollector.collect(e)
+
+                    throw SelectorRequireReconnectException()
                 }
-            } else {
-                client = createClient(ssoContext.bot)
-                ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyEcdh()
-                SlowLoginImpl(handler).doLogin()
+
+                loginSuccess()
+
+                return
             }
-        } catch (e: Exception) {
-            // Failed to log in, invalidate secrets.
-            ssoContext.bot.components[AccountSecretsManager].invalidate()
-            throw e
         }
-        components[AccountSecretsManager].saveSecrets(ssoContext.account, AccountSecretsImpl(client))
-        registerClientOnline(handler)
-        ssoContext.bot.logger.info { "Login successful." }
+
+        if (authControl == null) initAuthControl()
+        val authControl0 = authControl!!
+
+
+        var nextAuthMethod: AuthMethod? = null
+        try {
+            ssoContext.bot.components[BotClientHolder].refreshClient()
+            ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyEcdh()
+
+            when (val authw = authControl0.acquireAuth().also { nextAuthMethod = it }) {
+                is AuthMethod.Error -> {
+                    authControl = null
+                    throw authw.exception
+                }
+
+                AuthMethod.NotAvailable -> {
+                    authControl = null
+                    error("No more auth method available")
+                }
+
+                is AuthMethod.Pwd -> {
+                    SlowLoginImpl(handler, LoginType.Password(authw.passwordMd5)).doLogin()
+                }
+
+                AuthMethod.QRCode -> {
+                    val rsp = ssoContext.bot.components[QRCodeLoginProcessor].prepareProcess(
+                        handler, client
+                    ).process(handler, client)
+
+                    SlowLoginImpl(handler, LoginType.QRCode(rsp)).doLogin()
+                }
+            }
+
+            authControl!!.actComplete()
+            authControl = null
+        } catch (exception: Throwable) {
+            if (exception is SelectorRequireReconnectException) {
+                throw exception
+            }
+
+            ssoContext.bot.network.logger.warning({ "Failed with auth method: $nextAuthMethod" }, exception)
+            authControl0.exceptionCollector.collectException(exception)
+
+            if (nextAuthMethod !is AuthMethod.Error && nextAuthMethod != null) {
+                authControl0.actMethodFailed(exception)
+            }
+
+            if (exception is NetworkException) {
+                if (exception.recoverable) throw exception
+            }
+
+            if (nextAuthMethod == null || nextAuthMethod is AuthMethod.NotAvailable || nextAuthMethod is AuthMethod.Error) {
+                authControl = null
+                authControl0.exceptionCollector.throwLast()
+            }
+
+            throw SelectorRequireReconnectException()
+        }
+
+        loginSuccess()
+
     }
 
+
+    sealed class AuthMethod {
+        object NotAvailable : AuthMethod() {
+            override fun toString(): String = "NotAvailable"
+        }
+
+        object QRCode : AuthMethod() {
+            override fun toString(): String = "QRCode"
+        }
+
+        class Pwd(val passwordMd5: SecretsProtection.EscapedByteBuffer) : AuthMethod() {
+            override fun toString(): String = "Password@${hashCode()}"
+        }
+
+        /**
+         * Exception in [BotAuthorization]
+         */
+        class Error(val exception: Throwable) : AuthMethod() {
+            override fun toString(): String = "Error[$exception]@${hashCode()}"
+        }
+    }
+
+    private var authControl: AuthControl? = null
+
     override suspend fun sendRegister(handler: NetworkHandler): StatSvc.Register.Response {
         return registerClientOnline(handler).also { registerResp = it }
     }
@@ -189,17 +310,6 @@ internal class SsoProcessorImpl(
         }
     }
 
-    private fun createClient(bot: QQAndroidBot): QQAndroidClient {
-        val device = ssoContext.device
-        return QQAndroidClient(
-            ssoContext.account,
-            device = device,
-            accountSecrets = bot.components[AccountSecretsManager].getSecretsOrCreate(ssoContext.account, device)
-        ).apply {
-            _bot = bot
-        }
-    }
-
     ///////////////////////////////////////////////////////////////////////////
     // login
     ///////////////////////////////////////////////////////////////////////////
@@ -219,7 +329,10 @@ internal class SsoProcessorImpl(
         abstract suspend fun doLogin()
     }
 
-    private inner class SlowLoginImpl(handler: NetworkHandler) : LoginStrategy(handler) {
+    private inner class SlowLoginImpl(
+        handler: NetworkHandler,
+        private val loginType: LoginType
+    ) : LoginStrategy(handler) {
 
         private fun loginSolverNotNull(): LoginSolver {
             fun LoginSolver?.notnull(): LoginSolver {
@@ -259,9 +372,15 @@ internal class SsoProcessorImpl(
 
         override suspend fun doLogin() = withExceptionCollector {
 
+            @Suppress("FunctionName")
+            fun SSOWtLogin9(allowSlider: Boolean) = when (loginType) {
+                is LoginType.Password -> WtLogin9.Password(client, loginType.passwordMd5.asByteArray, allowSlider)
+                is LoginType.QRCode -> WtLogin9.QRCode(client, loginType.qrCodeLoginData)
+            }
+
             var allowSlider = sliderSupported || bot.configuration.protocol == MiraiProtocol.ANDROID_PHONE
 
-            var response: LoginPacketResponse = WtLogin9(client, allowSlider).sendAndExpect()
+            var response: LoginPacketResponse = SSOWtLogin9(allowSlider).sendAndExpect()
 
             mainloop@ while (true) {
                 when (response) {
@@ -281,7 +400,7 @@ internal class SsoProcessorImpl(
                         check(result is DeviceVerificationResultImpl)
                         response = when (result) {
                             is UrlDeviceVerificationResult -> {
-                                WtLogin9(client, allowSlider).sendAndExpect()
+                                SSOWtLogin9(allowSlider).sendAndExpect()
                             }
 
                             is SmsDeviceVerificationResult -> {
@@ -308,7 +427,7 @@ internal class SsoProcessorImpl(
                                 collectThrow(error)
                             }
                             response = if (ticket == null) {
-                                WtLogin9(client, allowSlider).sendAndExpect()
+                                SSOWtLogin9(allowSlider).sendAndExpect()
                             } else {
                                 WtLogin2.SubmitSliderCaptcha(client, ticket).sendAndExpect()
                             }
@@ -358,6 +477,11 @@ internal class SsoProcessorImpl(
         }
     }
 
+    private sealed class LoginType {
+        class Password(val passwordMd5: SecretsProtection.EscapedByteBuffer) : LoginType()
+        class QRCode(val qrCodeLoginData: QRCodeLoginData) : LoginType()
+    }
+
     private inner class FastLoginImpl(handler: NetworkHandler) : LoginStrategy(handler) {
         override suspend fun doLogin() {
             val login10 = handler.sendAndExpect(WtLogin10(client))

+ 8 - 1
mirai-core/src/commonMain/kotlin/network/handler/CommonNetworkHandler.kt

@@ -17,6 +17,7 @@ import net.mamoe.mirai.internal.network.components.*
 import net.mamoe.mirai.internal.network.handler.NetworkHandler.Companion.runUnwrapCancellationException
 import net.mamoe.mirai.internal.network.handler.selector.NetworkException
 import net.mamoe.mirai.internal.network.handler.selector.NetworkHandlerSelector
+import net.mamoe.mirai.internal.network.handler.selector.SelectorRequireReconnectException
 import net.mamoe.mirai.internal.network.handler.state.StateObserver
 import net.mamoe.mirai.internal.network.impl.HeartbeatFailedException
 import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
@@ -253,7 +254,13 @@ internal abstract class CommonNetworkHandler<Conn>(
                     this@CommonNetworkHandler.launch { resumeConnection() } // go to next state.
                 } else {
                     // failed in SSO stage
-                    context[SsoProcessor].casFirstLoginResult(null, FirstLoginResult.OTHER_FAILURE)
+                    context[SsoProcessor].casFirstLoginResult(
+                        null,
+                        when (error) {
+                            is SelectorRequireReconnectException -> null
+                            else -> FirstLoginResult.OTHER_FAILURE
+                        }
+                    )
 
                     if (error is CancellationException) {
                         // CancellationException is either caused by parent cancellation or manual `connectResult.cancel`.

+ 15 - 0
mirai-core/src/commonMain/kotlin/network/handler/selector/SelectorRequireReconnectException.kt

@@ -0,0 +1,15 @@
+/*
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.network.handler.selector
+
+/**
+ * A special exception that instructs selector to restart network connection
+ */
+internal expect class SelectorRequireReconnectException() : NetworkException

+ 11 - 0
mirai-core/src/commonMain/kotlin/network/keys.kt

@@ -74,6 +74,17 @@ internal fun BytePacketBuilder.writeLoginExtraData(loginExtraData: LoginExtraDat
     }
 }
 
+@Serializable
+internal class QRCodeLoginData(
+    val tmpPwd: ByteArray, // get from wtlogin.trans_emp, don't use client.wLoginSigInfo.encryptA1
+    val noPicSig: ByteArray, // get from wtlogin.trans_emp, don't use client.wLoginSigInfo.encryptA1
+    val tgtQR: ByteArray,
+) {
+    override fun toString(): String {
+        return "QRCodeLoginData(tmpPwd=${tmpPwd.toUHexString()}, noPicSig=${noPicSig.toUHexString()}, tgtQR=${tgtQR.toUHexString()})"
+    }
+}
+
 @Suppress("ArrayInDataClass") // for `copy`
 @Serializable
 internal data class WLoginSigInfo(

+ 4 - 2
mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacket.kt

@@ -172,6 +172,7 @@ internal val NO_ENCRYPT: ByteArray = ByteArray(0)
 internal inline fun <R : Packet?> OutgoingPacketFactory<R>.buildLoginOutgoingPacket(
     client: QQAndroidClient,
     bodyType: Byte,
+    uin: String = client.uin.toString(),
     extraData: ByteArray = EMPTY_BYTE_ARRAY,
     remark: String? = null,
     commandName: String = this.commandName,
@@ -190,7 +191,7 @@ internal inline fun <R : Packet?> OutgoingPacketFactory<R>.buildLoginOutgoingPac
             }
             writeByte(0x00)
 
-            client.uin.toString().let {
+            uin.let {
                 writeInt(it.length + 4)
                 writeText(it)
             }
@@ -274,6 +275,7 @@ internal inline fun BytePacketBuilder.writeSsoPacket(
 
 internal fun BytePacketBuilder.writeOicqRequestPacket(
     client: QQAndroidClient,
+    uin: Long = client.uin,
     encryptMethod: EncryptMethod = EncryptMethodEcdh(client.bot.components[EcdhInitialPublicKeyUpdater].getQQEcdh()),
     commandId: Int,
     bodyBlock: BytePacketBuilder.() -> Unit
@@ -284,7 +286,7 @@ internal fun BytePacketBuilder.writeOicqRequestPacket(
     writeShort(8001)
     writeShort(commandId.toShort())
     writeShort(1) // const??
-    writeInt(client.uin.toInt())
+    writeInt(uin.toInt())
     writeByte(3) // originally const
     writeByte(encryptMethod.id.toByte())
     writeByte(0) // const8_always_0

+ 1 - 0
mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt

@@ -131,6 +131,7 @@ internal object KnownPacketFactories {
     object OutgoingFactories : List<OutgoingPacketFactory<*>> by mutableListOf(
         WtLogin.Login,
         WtLogin.ExchangeEmp,
+        WtLogin.TransEmp,
         StatSvc.Register,
         StatSvc.GetOnlineStatus,
         StatSvc.SimpleGet,

+ 94 - 2
mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt

@@ -82,6 +82,26 @@ internal fun BytePacketBuilder.t8(
     }
 }
 
+internal fun BytePacketBuilder.t16(
+    ssoVersion: Int,
+    subAppId: Long,
+    guid: ByteArray,
+    apkId: ByteArray,
+    apkVersionName: ByteArray,
+    apkSignatureMd5: ByteArray
+) {
+    writeShort(0x16)
+    writeShortLVPacket {
+        writeInt(ssoVersion)
+        writeInt(16)
+        writeInt(subAppId.toInt())
+        writeFully(guid)
+        writeShortLVByteArray(apkId)
+        writeShortLVByteArray(apkVersionName)
+        writeShortLVByteArray(apkSignatureMd5)
+    }
+}
+
 internal fun BytePacketBuilder.t18(
     appId: Long,
     appClientVersion: Int = 0,
@@ -100,9 +120,81 @@ internal fun BytePacketBuilder.t18(
     } shouldEqualsTo 22
 }
 
+internal fun BytePacketBuilder.t1b(
+    micro: Int = 0,
+    version: Int = 0,
+    size: Int = 3,
+    margin: Int = 4,
+    dpi: Int = 72,
+    ecLevel: Int = 2,
+    hint: Int = 2
+) {
+    writeShort(0x1b)
+    writeShortLVPacket {
+        writeInt(micro)
+        writeInt(version)
+        writeInt(size)
+        writeInt(margin)
+        writeInt(dpi)
+        writeInt(ecLevel)
+        writeInt(hint)
+        writeShort(0)
+    }
+}
+
+internal fun BytePacketBuilder.t1d(
+    miscBitmap: Int,
+) {
+    writeShort(0x1d)
+    writeShortLVPacket {
+        writeByte(1)
+        writeInt(miscBitmap)
+        writeInt(0)
+        writeByte(0)
+        writeInt(0)
+    }
+}
+
+internal fun BytePacketBuilder.t1f(
+    isRoot: Boolean = false,
+    osName: ByteArray,
+    osVersion: ByteArray,
+    simVendor: ByteArray,
+    apn: ByteArray,
+    networkType: Short = 2,
+) {
+    writeShort(0x1f)
+    writeShortLVPacket {
+        writeByte(if (isRoot) 1 else 0)
+        writeShortLVByteArray(osName)
+        writeShortLVByteArray(osVersion)
+        writeShort(networkType)
+        writeShortLVByteArray(simVendor)
+        writeShortLVByteArray(EMPTY_BYTE_ARRAY)
+        writeShortLVByteArray(apn)
+    }
+}
+
+internal fun BytePacketBuilder.t33(
+    guid: ByteArray,
+) {
+    writeShort(0x33)
+    writeShortLVByteArray(guid)
+}
+
+internal fun BytePacketBuilder.t35(
+    productType: Int
+) {
+    writeShort(0x35)
+    writeShortLVPacket {
+        writeInt(productType)
+    }
+}
+
 internal fun BytePacketBuilder.t106(
+    client: QQAndroidClient,
     appId: Long = 16L,
-    client: QQAndroidClient
+    passwordMd5: ByteArray,
 ) {
     return t106(
         appId,
@@ -110,7 +202,7 @@ internal fun BytePacketBuilder.t106(
         client.appClientVersion,
         client.uin,
         true,
-        client.account.passwordMd5,
+        passwordMd5,
         0,
         client.uin.toByteArray(),
         client.tgtgtKey,

+ 243 - 3
mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
  *
  * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
  * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@@ -25,7 +25,10 @@ import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLoginExt
 import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.analysisTlv0x531
 import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.orEmpty
 import net.mamoe.mirai.internal.utils.crypto.TEA
+import net.mamoe.mirai.internal.utils.io.writeShortLVByteArray
+import net.mamoe.mirai.internal.utils.io.writeShortLVPacket
 import net.mamoe.mirai.internal.utils.printStructure
+import net.mamoe.mirai.network.InconsistentBotIdException
 import net.mamoe.mirai.network.RetryLaterException
 import net.mamoe.mirai.network.WrongPasswordException
 import net.mamoe.mirai.utils.*
@@ -130,7 +133,8 @@ internal class WtLogin {
                         t142(client.apkId)
                         t145(client.device.guid)
                         t154(0)
-                        t112(client.account.phoneNumber.encodeToByteArray())
+                        // 需要 t112, 但在实现 QR 时删除了 phoneNumber
+//                        t112(client.account.phoneNumber.encodeToByteArray())
                         t116(client.miscBitMap, client.subSigMap)
                         t521()
                         t52c()
@@ -635,7 +639,7 @@ internal class WtLogin {
                             deviceToken = tlvMap119.getOrEmpty(0x322),
                             encryptedDownloadSession = tlvMap119[0x11d]?.let {
                                 client.analysisTlv11d(it)
-                            }
+                            },
                         )
                     }
                     //bot.network.logger.error(client.wLoginSigInfo.psKeyMap["qun.qq.com"]?.data?.encodeToString())
@@ -655,4 +659,240 @@ internal class WtLogin {
         }
 
     }
+
+    internal object TransEmp : OutgoingPacketFactory<TransEmp.Response>("wtlogin.trans_emp") {
+
+        fun FetchQRCode(
+            client: QQAndroidClient,
+            size: Int,
+            margin: Int,
+            ecLevel: Int
+        ) = TransEmp.buildLoginOutgoingPacket(client, bodyType = 2, uin = "") { sequenceId ->
+            writeSsoPacket(client, client.subAppId, TransEmp.commandName, sequenceId = sequenceId) {
+                writeOicqRequestPacket(client, uin = 0, commandId = 0x812) {
+                    val code2dPacket = buildCode2dPacket(0, 0, 0x31) {
+                        writeShort(0)
+                        writeInt(16)
+                        writeLong(0)
+                        writeByte(8)
+                        writeShortLVPacket { }
+
+                        writeShort(6)
+                        t16(
+                            client.ssoVersion,
+                            client.subAppId,
+                            client.device.guid,
+                            client.apkId,
+                            client.apkVersionName,
+                            client.apkSignatureMd5
+                        )
+                        t1b(
+                            size = size,
+                            margin = margin,
+                            ecLevel = ecLevel
+                        )
+                        t1d(client.miscBitMap)
+
+                        val protocol = client.bot.configuration.protocol
+                        when (protocol) {
+                            BotConfiguration.MiraiProtocol.MACOS -> t1f(
+                                false,
+                                "Mac OSX".toByteArray(),
+                                "10".toByteArray(),
+                                "mac carrier".toByteArray(),
+                                client.device.apn,
+                                2
+                            )
+
+                            BotConfiguration.MiraiProtocol.ANDROID_WATCH -> t1f(
+                                false,
+                                client.device.osType,
+                                "7.1.2".toByteArray(),
+                                "China Mobile GSM".toByteArray(),
+                                client.device.apn,
+                                2
+                            )
+
+                            else -> error("protocol $protocol doesn't support qrcode login.")
+                        }
+
+                        t33(client.device.guid)
+                        t35(
+                            when (protocol) {
+                                BotConfiguration.MiraiProtocol.MACOS -> 5
+                                BotConfiguration.MiraiProtocol.ANDROID_WATCH -> 8
+                                else -> error("assertion")
+                            }
+                        )
+                    }
+                    writeByte(0)
+                    writeShort(code2dPacket.remaining.toShort())
+                    writeInt(0x10) // appId, const 16
+                    writeInt(0x72) // 0x90
+                    writeFully(ByteArray(3) { 0x00 })
+                    writePacket(code2dPacket)
+                    code2dPacket.release()
+                }
+            }
+        }
+
+        fun QueryQRCodeStatus(
+            client: QQAndroidClient,
+            sig: ByteArray,
+        ) = TransEmp.buildLoginOutgoingPacket(client, bodyType = 2, uin = "") { sequenceId ->
+            writeSsoPacket(client, client.subAppId, TransEmp.commandName, sequenceId = sequenceId) {
+                writeOicqRequestPacket(client, uin = 0, commandId = 0x812) {
+                    val code2dPacket = buildCode2dPacket(1, 0, 0x12) {
+                        writeShort(5)
+                        writeByte(1)
+                        writeInt(8)
+                        writeInt(16)
+                        writeShortLVByteArray(sig)
+                        writeLong(0)
+                        writeByte(8)
+                        writeShortLVPacket { }
+                        writeShort(0)
+                    }
+                    writeByte(0)
+                    writeShort(code2dPacket.remaining.toShort())
+                    writeInt(0x10) // appId, const 16
+                    writeInt(0x72) // 0x90
+                    writeFully(ByteArray(3) { 0x00 })
+                    writePacket(code2dPacket)
+                    code2dPacket.release()
+                }
+            }
+        }
+
+        private fun buildCode2dPacket(
+            sequence: Int, uin: Long, command: Short, body: BytePacketBuilder.() -> Unit
+        ) = buildPacket {
+            writeInt(currentTimeSeconds().toInt())
+            writeByte(2)
+            val bodyPacket = buildPacket(body)
+            writeUShort((43 + bodyPacket.remaining + 1).toUShort())
+            writeUShort(command.toUShort())
+            writeFully(ByteArray(21) { 0 })
+            writeByte(3)
+            writeShort(0)
+            writeShort(50)
+            writeInt(sequence)
+            writeLong(uin)
+            writePacket(bodyPacket)
+            bodyPacket.release()
+            writeByte(3)
+        }
+
+        sealed class Response() : Packet {
+            class FetchQRCode(val imageData: ByteArray, val sig: ByteArray) : Response() {
+                override fun toString(): String {
+                    return "WtLogin.TransEmp.Response.FetchQRCode" +
+                            "(imageData=${imageData.toUHexString()}, sig=${sig.toUHexString()})"
+                }
+            }
+
+            class QRCodeStatus(val state: State) : Response() {
+                override fun toString(): String {
+                    return "WtLogin.TransEmp.Response.QRCodeStatus(state=$state)"
+                }
+
+                enum class State { WAITING_FOR_SCAN, WAITING_FOR_CONFIRM, CANCELLED, TIMEOUT }
+            }
+
+            class QRCodeConfirmed(val data: QRCodeLoginData) : Response() {
+                override fun toString(): String {
+                    return "WtLogin.TransEmp.Response.QRCodeConfirmed(data=$data)"
+                }
+            }
+        }
+
+        override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
+            check(remaining >= 48) { "remaining payload is too short, current is $remaining." }
+
+            discardExact(5)
+            readUByte()
+            readUShort()
+            val command = readUShort().toInt()
+            discardExact(21)
+            readUByte()
+            readUShort()
+            readUShort()
+            readInt()
+            readLong()
+
+            return when (command) {
+                0x31 -> { // qr code data
+                    readShort()
+                    readInt()
+
+                    val code = readByte().toInt()
+                    check(code == 0) { "code is not 0 while parsing wtlogin.trans_emp with command 0x31." }
+                    val sig = readUShortLVByteArray()
+                    readUShort()
+
+                    val tlv = _readTLVMap()
+                    val data =
+                        tlv.getOrFail(0x17) { "missing tlv 0x17 while parsing wtlogin.trans_emp with command 0x31." }
+
+                    Response.FetchQRCode(data, sig)
+                }
+
+                0x12 -> { // qr code state
+                    var length = readUShort().toInt()
+                    if (length != 0) {
+                        length--
+                        if (readUByte().toInt() == 2) {
+                            readLong()
+                            length -= 8
+                        }
+                    }
+
+                    if (length > 0) {
+                        discardExact(length)
+                    }
+                    readInt()
+
+                    val code = readUByte().toInt()
+                    if (code != 0) {
+                        when (code) { // code
+                            0x30 -> Response.QRCodeStatus(Response.QRCodeStatus.State.WAITING_FOR_SCAN)
+                            0x35 -> Response.QRCodeStatus(Response.QRCodeStatus.State.WAITING_FOR_CONFIRM)
+                            0x36 -> Response.QRCodeStatus(Response.QRCodeStatus.State.CANCELLED)
+                            0x11 -> Response.QRCodeStatus(Response.QRCodeStatus.State.TIMEOUT)
+                            else -> error("unknown code $code while parsing wtlogin.trans_emp with command 0x12.")
+                        }
+                    } else {
+                        val client = bot.client
+
+                        val uin = readLong()
+                        if (client.uin != uin) {
+                            throw InconsistentBotIdException(expected = client.uin, actual = uin)
+                        }
+                        readInt()
+                        readUShort()
+                        val tlv = _readTLVMap()
+
+                        val tmpPwd = tlv.getOrFail(0x18) {
+                            "missing tlv 0x18 while parsing wtlogin.trans_emp with command 0x12."
+                        }
+                        val noPicSig = tlv.getOrFail(0x19) {
+                            "missing tlv 0x19 while parsing wtlogin.trans_emp with command 0x12."
+                        }
+                        val tgtQR = tlv.getOrFail(0x65) {
+                            "missing tlv 0x65 while parsing wtlogin.trans_emp with command 0x12."
+                        }
+
+                        client.tgtgtKey = tlv.getOrFail(0x1e) {
+                            "missing tlv 0x1e while parsing wtlogin.trans_emp with command 0x12."
+                        }
+
+                        Response.QRCodeConfirmed(QRCodeLoginData(tmpPwd, noPicSig, tgtQR))
+                    }
+                }
+
+                else -> error("wtlogin.trans_emp received an unknown command: $command")
+            }
+        }
+
+    }
 }

+ 2 - 2
mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin15.kt

@@ -29,11 +29,11 @@ internal object WtLogin15 : WtLoginExt {
 //        writeSsoPacket(client, client.subAppId, WtLogin.ExchangeEmp.commandName, sequenceId = sequenceId) {
         writeOicqRequestPacket(
             client,
-            EncryptMethodSessionKeyNew(
+            encryptMethod = EncryptMethodSessionKeyNew(
                 client.wLoginSigInfo.wtSessionTicket.data,
                 client.wLoginSigInfo.wtSessionTicketKey
             ),
-            0x0810
+            commandId = 0x0810
         ) {
             writeShort(subCommand) // subCommand
             writeShort(24)

+ 55 - 2
mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin9.kt

@@ -17,8 +17,9 @@ import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
 internal object WtLogin9 : WtLoginExt {
     private const val appId = 16L
 
-    operator fun invoke(
+    fun Password(
         client: QQAndroidClient,
+        passwordMd5: ByteArray,
         allowSlider: Boolean
     ) = WtLogin.Login.buildLoginOutgoingPacket(
         client, bodyType = 2, remark = "9:password-login"
@@ -43,7 +44,7 @@ internal object WtLogin9 : WtLoginExt {
                 if (useEncryptA1AndNoPicSig) {
                     t106(client.wLoginSigInfo.encryptA1!!)
                 } else {
-                    t106(appId, client)
+                    t106(client, appId, passwordMd5)
                 }
 
                 /* // from GetStWithPasswd
@@ -118,4 +119,56 @@ internal object WtLogin9 : WtLoginExt {
             }
         }
     }
+
+    @Suppress("DuplicatedCode")
+    fun QRCode(
+        client: QQAndroidClient,
+        data: QRCodeLoginData,
+    ) = WtLogin.Login.buildLoginOutgoingPacket(
+        client, bodyType = 2, remark = "9:qrcode-login"
+    ) { sequenceId ->
+        writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
+            writeOicqRequestPacket(client, commandId = 0x0810) {
+                writeShort(9) // subCommand
+                writeShort(0x19) // count of TLVs, probably ignored by server?
+
+                t18(appId, client.appClientVersion, client.uin)
+                t1(client.uin, client.device.ipAddress)
+
+                t106(data.tmpPwd)
+
+                t116(client.miscBitMap, client.subSigMap)
+                t100(appId, client.subAppId, client.appClientVersion, client.ssoVersion, client.mainSigMap)
+                t107(0)
+                t108(client.device.imei.toByteArray())
+
+                t142(client.apkId)
+
+                t144(client)
+
+                t145(client.device.guid)
+                t147(appId, client.apkVersionName, client.apkSignatureMd5)
+
+                t16a(data.noPicSig)
+
+                t154(sequenceId)
+                t141(client.device.simInfo, client.networkType, client.device.apn)
+                t8(2052)
+
+                t511()
+
+                t187(client.device.macAddress)
+                t188(client.device.androidId)
+                t194(client.device.imsiMd5)
+                t191(0x00)
+
+                t202(client.device.wifiBSSID, client.device.wifiSSID)
+
+                t177(client.buildTime, client.sdkVersion)
+                t516()
+                t521(8)
+                t318(data.tgtQR)
+            }
+        }
+    }
 }

+ 6 - 1
mirai-core/src/commonMain/kotlin/utils/MiraiCoreServices.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
  *
  * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
  * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@@ -115,5 +115,10 @@ internal object MiraiCoreServices {
             "net.mamoe.mirai.message.data.OfflineAudio.Factory",
             "net.mamoe.mirai.internal.message.data.OfflineAudioFactoryImpl"
         ) { net.mamoe.mirai.internal.message.data.OfflineAudioFactoryImpl() }
+
+        Services.register(
+            "net.mamoe.mirai.auth.DefaultBotAuthorizationFactory",
+            "net.mamoe.mirai.internal.network.auth.DefaultBotAuthorizationFactoryImpl"
+        ) { net.mamoe.mirai.internal.network.auth.DefaultBotAuthorizationFactoryImpl() }
     }
 }

+ 59 - 52
mirai-core/src/commonMain/kotlin/utils/MiraiProtocolInternal.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
  *
  * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
  * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@@ -25,6 +25,7 @@ internal class MiraiProtocolInternal(
     @JvmField internal val sign: String,
     @JvmField internal val buildTime: Long,
     @JvmField internal val ssoVersion: Int,
+    @JvmField internal val supportsQRLogin: Boolean,
 ) {
     internal companion object {
         internal val protocols = EnumMap<MiraiProtocol, MiraiProtocolInternal>(MiraiProtocol::class)
@@ -35,67 +36,73 @@ internal class MiraiProtocolInternal(
         init {
             //Updated from MiraiGo (2023/3/7)
             protocols[MiraiProtocol.ANDROID_PHONE] = MiraiProtocolInternal(
-                "com.tencent.mobileqq",
-                537151682,
-                "8.9.33.10335",
-                "6.0.0.2534",
-                150470524,
-                0x10400,
-                16724722,
-                "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D",
-                1673599898L,
-                19,
+                apkId = "com.tencent.mobileqq",
+                id = 537151682,
+                ver = "8.9.33.10335",
+                sdkVer = "6.0.0.2534",
+                miscBitMap = 150470524,
+                subSigMap = 0x10400,
+                mainSigMap = 16724722,
+                sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D",
+                buildTime = 1673599898L,
+                ssoVersion = 19,
+                supportsQRLogin = false,
             )
             //Updated from MiraiGo (2023/3/7)
             protocols[MiraiProtocol.ANDROID_PAD] = MiraiProtocolInternal(
-                "com.tencent.mobileqq",
-                537151218,
-                "8.9.33.10335",
-                "6.0.0.2534",
-                150470524,
-                0x10400,
-                16724722,
-                "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D",
-                1673599898L,
-                19,
+                apkId = "com.tencent.mobileqq",
+                id = 537151218,
+                ver = "8.9.33.10335",
+                sdkVer = "6.0.0.2534",
+                miscBitMap = 150470524,
+                subSigMap = 0x10400,
+                mainSigMap = 16724722,
+                sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D",
+                buildTime = 1673599898L,
+                ssoVersion = 19,
+                supportsQRLogin = false,
             )
             protocols[MiraiProtocol.ANDROID_WATCH] = MiraiProtocolInternal(
-                "com.tencent.qqlite",
-                537064446,
-                "2.0.5",
-                "6.0.0.236",
-                16252796,
-                0x10400,
-                34869472,
-                "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D",
-                1559564731L,
-                5,
+                apkId = "com.tencent.qqlite",
+                id = 537064446,
+                ver = "2.0.5",
+                sdkVer = "6.0.0.236",
+                miscBitMap = 16252796,
+                subSigMap = 0x10400,
+                mainSigMap = 34869472,
+                sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D",
+                buildTime = 1559564731L,
+                ssoVersion = 5,
+                supportsQRLogin = true,
             )
             protocols[MiraiProtocol.IPAD] = MiraiProtocolInternal(
-                "com.tencent.minihd.qq",
-                537151363,
-                "8.9.33.614",
-                "6.0.0.2433",
-                150470524,
-                66560,
-                1970400,
-                "AA 39 78 F4 1F D9 6F F9 91 4A 66 9E 18 64 74 C7",
-                1640921786L,
-                12,
+                apkId = "com.tencent.minihd.qq",
+                id = 537151363,
+                ver = "8.9.33.614",
+                sdkVer = "6.0.0.2433",
+                miscBitMap = 150470524,
+                subSigMap = 66560,
+                mainSigMap = 1970400,
+                sign = "AA 39 78 F4 1F D9 6F F9 91 4A 66 9E 18 64 74 C7",
+                buildTime = 1640921786L,
+                ssoVersion = 12,
+                supportsQRLogin = false,
             )
-            //Updated from MiraiGo (2023/3/15)
             protocols[MiraiProtocol.MACOS] = MiraiProtocolInternal(
-                "com.tencent.qq",
-                537128930,
-                "5.8.9",
-                "6.0.0.2433",
-                150470524,
-                66560,
-                1970400,
-                "AA 39 78 F4 1F D9 6F F9 91 4A 66 9E 18 64 74 C7",
-                1595836208L,
-                12,
+                apkId = "com.tencent.qq",
+                id = 0x2003ca32,
+                ver = "6.7.9",
+                sdkVer = "6.2.0.1023",
+                miscBitMap = 0x7ffc,
+                subSigMap = 66560,
+                mainSigMap = 1970400,
+                sign = "com.tencent.qq".encodeToByteArray().toUHexString(" "),
+                buildTime = 0L,
+                ssoVersion = 7,
+                supportsQRLogin = true,
             )
         }
+
+        inline val MiraiProtocol.asInternal: MiraiProtocolInternal get() = get(this)
     }
 }

+ 10 - 0
mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.auth.DefaultBotAuthorizationFactory

@@ -0,0 +1,10 @@
+#
+# Copyright 2019-2023 Mamoe Technologies and contributors.
+#
+# 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+# Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+#
+# https://github.com/mamoe/mirai/blob/dev/LICENSE
+#
+
+net.mamoe.mirai.internal.network.auth.DefaultBotAuthorizationFactoryImpl

+ 108 - 0
mirai-core/src/commonTest/kotlin/network/component/BotAuthControlTest.kt

@@ -0,0 +1,108 @@
+/*
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.network.component
+
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.yield
+import net.mamoe.mirai.auth.BotAuthInfo
+import net.mamoe.mirai.auth.BotAuthResult
+import net.mamoe.mirai.auth.BotAuthSession
+import net.mamoe.mirai.auth.BotAuthorization
+import net.mamoe.mirai.internal.network.auth.AuthControl
+import net.mamoe.mirai.internal.network.components.SsoProcessorContext
+import net.mamoe.mirai.internal.network.components.SsoProcessorImpl
+import net.mamoe.mirai.internal.network.framework.AbstractCommonNHTest
+import net.mamoe.mirai.network.CustomLoginFailedException
+import net.mamoe.mirai.utils.BotConfiguration
+import net.mamoe.mirai.utils.DeviceInfo
+import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY
+import kotlin.reflect.KClass
+import kotlin.test.Test
+import kotlin.test.assertFailsWith
+import kotlin.test.fail
+
+internal class BotAuthControlTest : AbstractCommonNHTest() {
+    private val botAuthInfo = object : BotAuthInfo {
+        override val id: Long
+            get() = bot.id
+        override val deviceInfo: DeviceInfo
+            get() = bot.components[SsoProcessorContext].device
+        override val configuration: BotConfiguration
+            get() = bot.configuration
+    }
+
+    private suspend fun AuthControl.assertRequire(exceptedType: KClass<*>) {
+        println("Requiring auth method")
+        val nextAuth = acquireAuth()
+        println("Got $nextAuth")
+        yield()
+
+        if (nextAuth is SsoProcessorImpl.AuthMethod.Error) {
+            fail(cause = nextAuth.exception)
+        }
+        if (exceptedType.isInstance(nextAuth)) return
+        fail("Type not match, excepted $exceptedType but got ${nextAuth::class}")
+    }
+
+    @Test
+    fun `auth test`() = runTest {
+
+        val control = AuthControl(botAuthInfo, object : BotAuthorization {
+            override suspend fun authorize(session: BotAuthSession, info: BotAuthInfo): BotAuthResult {
+                return session.authByPassword(EMPTY_BYTE_ARRAY)
+            }
+        }, bot.logger, backgroundScope.coroutineContext)
+
+        control.assertRequire(SsoProcessorImpl.AuthMethod.Pwd::class)
+        control.actComplete()
+        control.assertRequire(SsoProcessorImpl.AuthMethod.NotAvailable::class)
+
+    }
+
+    @Test
+    fun `test auth failed and reselect`() = runTest {
+        class MyLoginFailedException : CustomLoginFailedException(killBot = false)
+
+        val control = AuthControl(botAuthInfo, object : BotAuthorization {
+            override suspend fun authorize(session: BotAuthSession, info: BotAuthInfo): BotAuthResult {
+                assertFailsWith<MyLoginFailedException> { session.authByPassword(EMPTY_BYTE_ARRAY); println("!") }
+                println("114514")
+                return session.authByPassword(EMPTY_BYTE_ARRAY)
+            }
+        }, bot.logger, backgroundScope.coroutineContext)
+
+        control.assertRequire(SsoProcessorImpl.AuthMethod.Pwd::class)
+        control.actMethodFailed(MyLoginFailedException())
+
+        control.assertRequire(SsoProcessorImpl.AuthMethod.Pwd::class)
+        control.actComplete()
+
+        control.assertRequire(SsoProcessorImpl.AuthMethod.NotAvailable::class)
+
+    }
+
+    @Test
+    fun `failed when login complete`() = runTest {
+        val control = AuthControl(botAuthInfo, object : BotAuthorization {
+            override suspend fun authorize(session: BotAuthSession, info: BotAuthInfo): BotAuthResult {
+                val rsp = session.authByPassword(EMPTY_BYTE_ARRAY)
+                assertFailsWith<IllegalStateException> { session.authByPassword(EMPTY_BYTE_ARRAY) }
+                assertFailsWith<IllegalStateException> { session.authByPassword(EMPTY_BYTE_ARRAY) }
+                assertFailsWith<IllegalStateException> { session.authByPassword(EMPTY_BYTE_ARRAY) }
+                return rsp
+            }
+        }, bot.logger, backgroundScope.coroutineContext)
+
+        control.assertRequire(SsoProcessorImpl.AuthMethod.Pwd::class)
+        control.actComplete()
+        control.assertRequire(SsoProcessorImpl.AuthMethod.NotAvailable::class)
+
+    }
+}

+ 29 - 1
mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
  *
  * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
  * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@@ -13,12 +13,15 @@ package net.mamoe.mirai.internal.network.framework
 
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.SupervisorJob
+import net.mamoe.mirai.auth.BotAuthInfo
+import net.mamoe.mirai.auth.BotAuthResult
 import net.mamoe.mirai.internal.*
 import net.mamoe.mirai.internal.contact.uin
 import net.mamoe.mirai.internal.network.KeyWithCreationTime
 import net.mamoe.mirai.internal.network.KeyWithExpiry
 import net.mamoe.mirai.internal.network.WLoginSigInfo
 import net.mamoe.mirai.internal.network.WLoginSimpleInfo
+import net.mamoe.mirai.internal.network.auth.BotAuthSessionInternal
 import net.mamoe.mirai.internal.network.component.ComponentKey
 import net.mamoe.mirai.internal.network.component.ConcurrentComponentStorage
 import net.mamoe.mirai.internal.network.component.setAll
@@ -113,6 +116,31 @@ internal abstract class AbstractRealNetworkHandlerTest<H : NetworkHandler> : Abs
         set(SsoProcessorContext, SsoProcessorContextImpl(bot))
         set(SsoProcessor, object : TestSsoProcessor(bot) {
             override suspend fun login(handler: NetworkHandler) {
+                val botAuthInfo = object : BotAuthInfo {
+                    override val id: Long get() = bot.id
+                    override val deviceInfo: DeviceInfo
+                        get() = get(SsoProcessorContext).device
+                    override val configuration: BotConfiguration
+                        get() = bot.configuration
+                }
+                val rsp = object : BotAuthResult {}
+
+                val session = object : BotAuthSessionInternal() {
+                    override suspend fun authByPassword(passwordMd5: SecretsProtection.EscapedByteBuffer): BotAuthResult {
+                        return rsp
+                    }
+
+                    override suspend fun authByQRCode(): BotAuthResult {
+                        return rsp
+                    }
+
+                }
+
+                bot.account.authorization.authorize(session, botAuthInfo)
+                bot.account.accountSecretsKeyBuffer = SecretsProtection.EscapedByteBuffer(
+                    bot.account.authorization.calculateSecretsKey(botAuthInfo)
+                )
+
                 nhEvents.add(NHEvent.Login)
                 super.login(handler)
             }

+ 0 - 63
mirai-core/src/jvmBaseMain/kotlin/BotAccount.kt

@@ -1,63 +0,0 @@
-/*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/dev/LICENSE
- */
-
-package net.mamoe.mirai.internal
-
-import net.mamoe.mirai.utils.*
-import java.nio.ByteBuffer
-
-internal actual data class BotAccount(
-    internal actual val id: Long,
-
-    val passwordMd5Buffer: ByteBuffer, // md5
-
-    actual val phoneNumber: String = ""
-) {
-    init {
-        check(passwordMd5Buffer.remaining == 16) {
-            "Invalid passwordMd5: size must be 16 but got ${passwordMd5Buffer.remaining}. passwordMd5=${passwordMd5.toUHexString()}"
-        }
-    }
-
-    actual constructor(id: Long, passwordMd5: ByteArray, phoneNumber: String) : this(
-        id, SecretsProtection.escape(passwordMd5), phoneNumber
-    )
-
-    actual constructor(id: Long, passwordPlainText: String, phoneNumber: String) : this(
-        id,
-        passwordPlainText.md5(),
-        phoneNumber
-    ) {
-        require(passwordPlainText.length <= 16) { "Password length must be at most 16." }
-    }
-
-    actual val passwordMd5: ByteArray
-        get() {
-            return passwordMd5Buffer.duplicate().readBytes()
-        }
-
-    actual override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other == null || this::class != other::class) return false
-
-        other as BotAccount
-
-        if (id != other.id) return false
-        if (passwordMd5Buffer != other.passwordMd5Buffer) return false
-
-        return true
-    }
-
-
-    actual override fun hashCode(): Int {
-        var result = id.hashCode()
-        result = 31 * result + passwordMd5Buffer.hashCode()
-        return result
-    }
-}

+ 29 - 0
mirai-core/src/jvmBaseMain/kotlin/network/handler/selector/SelectorRequireReconnectException.kt

@@ -0,0 +1,29 @@
+/*
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.network.handler.selector
+
+internal actual class SelectorRequireReconnectException(
+    withStackTrace: Boolean
+) : NetworkException(true) {
+    actual constructor() : this(false)
+
+    private companion object {
+        val EMPTY = arrayOf<StackTraceElement>()
+    }
+
+    override fun fillInStackTrace(): Throwable {
+        stackTrace = EMPTY
+        return this
+    }
+
+    init {
+        if (withStackTrace) super.fillInStackTrace()
+    }
+}

+ 6 - 1
mirai-core/src/jvmTest/kotlin/directboot/DebugRunHelper.kt

@@ -11,13 +11,18 @@ package net.mamoe.mirai.internal.directboot
 
 import kotlinx.coroutines.Dispatchers
 import net.mamoe.mirai.BotFactory
+import net.mamoe.mirai.auth.BotAuthorization
 import net.mamoe.mirai.internal.QQAndroidBot
 import net.mamoe.mirai.utils.BotConfiguration
 import java.io.File
 
 internal object DebugRunHelper {
     fun newBot(id: Long, pwd: String, conf: BotConfiguration.(botid: Long) -> Unit): QQAndroidBot {
-        val bot = BotFactory.newBot(id, pwd) {
+        return newBot(id, BotAuthorization.byPassword(pwd), conf)
+    }
+
+    fun newBot(id: Long, authorization: BotAuthorization, conf: BotConfiguration.(botid: Long) -> Unit): QQAndroidBot {
+        val bot = BotFactory.newBot(id, authorization) {
             parentCoroutineContext = Dispatchers.IO
 
             workingDir = File("test/session/$id").also { it.mkdirs() }.absoluteFile

+ 0 - 41
mirai-core/src/nativeMain/kotlin/BotAccount.kt

@@ -1,41 +0,0 @@
-/*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/dev/LICENSE
- */
-
-package net.mamoe.mirai.internal
-
-import net.mamoe.mirai.utils.isSameClass
-import net.mamoe.mirai.utils.md5
-
-internal actual class BotAccount actual constructor(
-    internal actual val id: Long,
-    actual val passwordMd5: ByteArray,
-    actual val phoneNumber: String,
-) {
-    actual constructor(id: Long, passwordPlainText: String, phoneNumber: String) : this(
-        id,
-        passwordPlainText.md5(),
-        phoneNumber
-    )
-
-    actual override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is BotAccount || !isSameClass(this, other)) return false
-
-        if (id != other.id) return false
-        if (!passwordMd5.contentEquals(other.passwordMd5)) return false
-
-        return true
-    }
-
-    actual override fun hashCode(): Int {
-        var result = id.hashCode()
-        result = 31 * result + passwordMd5.hashCode()
-        return result
-    }
-}

+ 12 - 0
mirai-core/src/nativeMain/kotlin/network/handler/selector/SelectorRequireReconnectException.kt

@@ -0,0 +1,12 @@
+/*
+ * Copyright 2019-2023 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.network.handler.selector
+
+internal actual class SelectorRequireReconnectException : NetworkException(true)