diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt
index 978af6ccd..bdfcda989 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt
@@ -29,7 +29,6 @@ import net.mamoe.mirai.qqandroid.utils.toRichTextElems
 import net.mamoe.mirai.utils.MiraiInternalAPI
 import net.mamoe.mirai.utils.cryptor.contentToString
 import net.mamoe.mirai.utils.currentTimeSeconds
-import net.mamoe.mirai.utils.io.hexToBytes
 import kotlin.math.absoluteValue
 import kotlin.random.Random
 
@@ -228,11 +227,15 @@ internal class MessageSvc {
                             elems = message.toRichTextElems()
                         )
                     ),
+
+                    //.apply { add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(
+                    //                                pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes()
+                    //                            ))) }
                     msgSeq = seq,
-                    msgRand = Random.nextInt().absoluteValue,
-                    syncCookie = "08 A0 C2 C4 F1 05 10 A0 C2 C4 F1 05 18 E6 ED B9 C3 02 20 89 FE BE A4 06 28 E4 C2 B1 95 03 48 A1 9F E0 C7 08 58 D3 C2 8F A0 09 60 1D 68 A0 C2 C4 F1 05 70 00".hexToBytes()
-                        ?: SyncCookie(time = currentTimeSeconds + client.timeDifference).toByteArray(SyncCookie.serializer()),
-                    msgVia = 0
+                    msgRand = Random.nextInt().absoluteValue//,
+                    //      syncCookie = ByteArray(0)
+                    //  ?: SyncCookie(time = currentTimeSeconds + client.timeDifference).toByteArray(SyncCookie.serializer()),
+                    // msgVia = 1
                 )
             )
         }
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/MessageQQA.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/MessageQQA.kt
index f6d5ff9fc..80e969fd7 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/MessageQQA.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/MessageQQA.kt
@@ -19,7 +19,64 @@ internal fun NotOnlineImageFromFile.toJceData(): ImMsgBody.NotOnlineImage {
     )
 }
 
+internal fun CustomFaceFromFile.toJceData(): ImMsgBody.CustomFace {
+    return ImMsgBody.CustomFace(
+        filePath = this.filepath,
+        fileId = this.fileId,
+        serverIp = this.serverIp,
+        serverPort = this.serverPort,
+        fileType = this.fileType,
+        signature = this.signature,
+        useful = this.useful,
+        md5 = this.md5,
+        bizType = this.bizType,
+        imageType = this.imageType,
+        width = this.width,
+        height = this.height,
+        source = this.source,
+        size = this.size,
+        pbReserve = this.pbReserve
+    )
+}
+
 /*
+customFace=CustomFace#2050019814 {
+        guid=<Empty ByteArray>
+        filePath=5F6C522DEAC4F36C0ED8EF362660EFD6.png
+        shortcut=
+        buffer=<Empty ByteArray>
+        flag=<Empty ByteArray>
+        oldData=<Empty ByteArray>
+        fileId=0xB40AF10E(-1274351346)
+        serverIp=0xB703E13A(-1224482502)
+        serverPort=0x00000050(80)
+        fileType=0x00000042(66)
+        signature=6B 44 61 76 72 79 68 79 57 67 70 52 41 45 78 49
+        useful=0x00000001(1)
+        md5=5F 6C 52 2D EA C4 F3 6C 0E D8 EF 36 26 60 EF D6
+        thumbUrl=
+        bigUrl=
+        origUrl=
+        bizType=0x00000005(5)
+        repeatIndex=0x00000000(0)
+        repeatImage=0x00000000(0)
+        imageType=0x000003E9(1001)
+        index=0x00000000(0)
+        width=0x0000005F(95)
+        height=0x00000054(84)
+        source=0x00000067(103)
+        size=0x000006E2(1762)
+        origin=0x00000000(0)
+        thumbWidth=0x00000000(0)
+        thumbHeight=0x00000000(0)
+        showLen=0x00000000(0)
+        downloadLen=0x00000000(0)
+        _400Url=
+        _400Width=0x00000000(0)
+        _400Height=0x00000000(0)
+        pbReserve=08 01 10 00 32 00 4A 0E 5B E5 8A A8 E7 94 BB E8 A1 A8 E6 83 85 5D 50 00 78 05
+}
+
 notOnlineImage=NotOnlineImage#2050019814 {
         filePath=41AEF2D4B5BD24CF3791EFC5FEB67D60.jpg
         fileLen=0x00000350(848)
@@ -57,26 +114,39 @@ internal fun MessageChain.toRichTextElems(): MutableList<ImMsgBody.Elem> {
 
     this.forEach {
         when (it) {
-            is PlainText -> {
-                elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = it.stringValue)))
-            }
+            is PlainText -> elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = it.stringValue)))
             is At -> {
 
             }
-            is NotOnlineImageFromServer -> {
-                elements.add(ImMsgBody.Elem(notOnlineImage = it.delegate))
-             //   elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "78 00 90 01 01 F8 01 00 A0 02 00 C8 02 00".hexToBytes())))
-            }
-            is NotOnlineImageFromFile -> {
-                elements.add(ImMsgBody.Elem(notOnlineImage = it.toJceData()))
-                //  elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "78 00 90 01 01 F8 01 00 A0 02 00 C8 02 00".hexToBytes())))
-            }
+            is CustomFaceFromFile -> elements.add(ImMsgBody.Elem(customFace = it.toJceData()))
+            is CustomFaceFromServer -> elements.add(ImMsgBody.Elem(customFace = it.delegate))
+            is NotOnlineImageFromServer -> elements.add(ImMsgBody.Elem(notOnlineImage = it.delegate))
+            is NotOnlineImageFromFile -> elements.add(ImMsgBody.Elem(notOnlineImage = it.toJceData()))
         }
     }
 
     return elements
 }
 
+internal class CustomFaceFromServer(
+    internal val delegate: ImMsgBody.CustomFace
+) : CustomFace() {
+    override val filepath: String get() = delegate.filePath
+    override val fileId: Int get() = delegate.fileId
+    override val serverIp: Int get() = delegate.serverIp
+    override val serverPort: Int get() = delegate.serverPort
+    override val fileType: Int get() = delegate.fileType
+    override val signature: ByteArray get() = delegate.signature
+    override val useful: Int get() = delegate.useful
+    override val md5: ByteArray get() = delegate.md5
+    override val bizType: Int get() = delegate.bizType
+    override val imageType: Int get() = delegate.imageType
+    override val width: Int get() = delegate.width
+    override val height: Int get() = delegate.height
+    override val source: Int get() = delegate.source
+    override val size: Int get() = delegate.size
+    override val pbReserve: ByteArray get() = delegate.pbReserve
+}
 
 internal class NotOnlineImageFromServer(
     internal val delegate: ImMsgBody.NotOnlineImage
@@ -107,22 +177,8 @@ internal fun ImMsgBody.RichText.toMessageChain(): MessageChain {
 
     elems.forEach {
         when {
-            it.notOnlineImage != null -> message.add(
-                NotOnlineImageFromServer(it.notOnlineImage)
-            )
-            it.customFace != null -> message.add(
-                NotOnlineImageFromFile(
-                    it.customFace.filePath,
-                    it.customFace.md5,
-                    it.customFace.origUrl,
-                    it.customFace.downloadLen,
-                    it.customFace.height,
-                    it.customFace.width,
-                    it.customFace.bizType,
-                    it.customFace.imageType,
-                    it.customFace.filePath
-                )
-            )
+            it.notOnlineImage != null -> message.add(NotOnlineImageFromServer(it.notOnlineImage))
+            it.customFace != null -> message.add(CustomFaceFromServer(it.customFace))
             it.text != null -> message.add(it.text.str.toMessage())
         }
     }
diff --git a/mirai-core-qqandroid/src/jvmTest/kotlin/androidPacketTests/clientToServer.kt b/mirai-core-qqandroid/src/jvmTest/kotlin/androidPacketTests/clientToServer.kt
index 0159bb6a7..f6616b841 100644
--- a/mirai-core-qqandroid/src/jvmTest/kotlin/androidPacketTests/clientToServer.kt
+++ b/mirai-core-qqandroid/src/jvmTest/kotlin/androidPacketTests/clientToServer.kt
@@ -68,10 +68,12 @@ internal val t163 = "2C 7A 7B 23 4E 24 3F 24 24 47 62 6B 69 2E 47 50".hexToBytes
 var ecdhPrivateKeyS = "97a52992cb7a2110413629af94a3c249c68a3b731510caa8"
 
 internal val shareKeyCalculatedByConstPubKey
-    get() = ECDH.calculateShareKey(
-        loadPrivateKey(ecdhPrivateKeyS),
-        initialPublicKey
-    )
+        by lazy {
+            ECDH.calculateShareKey(
+                loadPrivateKey(ecdhPrivateKeyS),
+                initialPublicKey
+            )
+        }
 
 var passwordMd5: ByteArray = byteArrayOf()
 var uin: Long = 0L
@@ -137,132 +139,141 @@ fun ByteReadPacket.analysisOneFullPacket(): ByteReadPacket = debugIfFail("Failed
     println("uin=" + readString(readInt() - 4))
 
     println("// 解密 body")
-    readRemainingBytes().tryDecrypt().toReadPacket().debugPrintThis("outer body decrypted").apply {
-        when (flag1) {
-            0x0A -> decodeSso()
-            0x0B -> decodeUni()
-            else -> error("unknown flag1: $flag1")
-        }
+    val encrypted = readRemainingBytes()
 
-        when (flag2) {
+    val decrypted = encrypted.tryDecryptOrNull()
+    if (decrypted == null) {
+        println("cannot decrypt: ${encrypted.toUHexString()}")
+        error("cannot decrypt: ${encrypted.toUHexString()}")
+    } else {
+        decrypted.toReadPacket().debugPrintThis("outer body decrypted").apply {
+            when (flag1) {
+                0x0A -> decodeSso()
+                0x0B -> decodeUni()
+                else -> error("unknown flag1: $flag1")
+            }
 
-            2 -> {
+            when (flag2) {
 
-                this.debugPrintThis("Oicq Request").apply {
-                    /*
-                   byte     2 // head flag
-                   short    27 + 2 + remaining.length
-                   ushort   client.protocolVersion // const 8001
-                   ushort   0x0001 // const0
-                   uint     client.uin
-                   byte     3 // const1
-                   ubyte    encryptMethod.value // [EncryptMethod]
-                   byte     0 // const2
-                   int      2 // const3
-                   int      client.appClientVersion
-                   int      0 // const4
-                    */
-                    discardExact(3)
-                    readShort().toInt().takeIf { it != 8001 }?.let {
-                        println("这个包不是 oicqRequest")
-                        return@debugIfFail this
-                        println("  got new protocolVersion=$it")
-                    }
-                    val commandId = readUShort().toInt()
-                    println("  commandId=0x${commandId.toShort().toUHexString()}")
-                    readUShort().toInt().takeIf { it != 1 }?.let {
-                        println("  got new const0=$it")
-                    }
-                    println("  uin=${readUInt()}")
-                    readByte().toInt().takeIf { it != 3 }?.let {
-                        println("  got new const1=$it")
-                    }
-                    val encryptionMethod = readUByte().toInt()
-                    readByte().toInt().takeIf { it != 0 }?.let {
-                        println("  got new const2=$it")
-                    }
-                    readInt().takeIf { it != 2 }?.let {
-                        println("  got new const3=$it")
-                    }
-                    readInt().takeIf { it != 0 }?.let {
-                        println("  got new appClientVersion=$it")
-                    }
-                    readInt().takeIf { it != 0 }?.let {
-                        println("  got new const4=$it")
-                    }
+                2 -> {
 
-
-                    discardExact(1)
-                    discardExact(1)
-                    val randomKey = readBytes(16)
-                    println("randomKey= ${randomKey.toUHexString()}")
-                    readUShort().toInt().takeIf { it != 258 }?.let {
-                        println("  got new const in ECDH head(originally=258)=$it")
-                    }
-                    val publicKey = readBytes(readShort().toInt())
-                    println("ecdh publicKey=" + publicKey.toUHexString())
-
-
-                    val encrypt = when (encryptionMethod) {
-                        135, 7 -> {
-                            ECDH.calculateShareKey(
-                                loadPrivateKey(ecdhPrivateKeyS),
-                                //"04cb366698561e936e80c157e074cab13b0bb68ddeb2824548a1b18dd4fb6122afe12fe48c5266d8d7269d7651a8eb6fe7".chunkedHexToBytes().adjustToPublicKey() // QQ: 04cb366698561e936e80c157e074cab13b0bb68ddeb2824548a1b18dd4fb6122afe12fe48c5266d8d7269d7651a8eb6fe7
-                                ECDH.constructPublicKey("30 46 30 10 06 07 2A 86 48 CE 3D 02 01 06 05 2B 81 04 00 1F 03 32 00".hexToBytes() + publicKey)
-                            )
+                    this.debugPrintThis("Oicq Request").apply {
+                        /*
+                       byte     2 // head flag
+                       short    27 + 2 + remaining.length
+                       ushort   client.protocolVersion // const 8001
+                       ushort   0x0001 // const0
+                       uint     client.uin
+                       byte     3 // const1
+                       ubyte    encryptMethod.value // [EncryptMethod]
+                       byte     0 // const2
+                       int      2 // const3
+                       int      client.appClientVersion
+                       int      0 // const4
+                        */
+                        discardExact(3)
+                        readShort().toInt().takeIf { it != 8001 }?.let {
+                            println("这个包不是 oicqRequest")
+                            return@debugIfFail this
+                            println("  got new protocolVersion=$it")
+                        }
+                        val commandId = readUShort().toInt()
+                        println("  commandId=0x${commandId.toShort().toUHexString()}")
+                        readUShort().toInt().takeIf { it != 1 }?.let {
+                            println("  got new const0=$it")
+                        }
+                        println("  uin=${readUInt()}")
+                        readByte().toInt().takeIf { it != 3 }?.let {
+                            println("  got new const1=$it")
+                        }
+                        val encryptionMethod = readUByte().toInt()
+                        readByte().toInt().takeIf { it != 0 }?.let {
+                            println("  got new const2=$it")
+                        }
+                        readInt().takeIf { it != 2 }?.let {
+                            println("  got new const3=$it")
+                        }
+                        readInt().takeIf { it != 0 }?.let {
+                            println("  got new appClientVersion=$it")
+                        }
+                        readInt().takeIf { it != 0 }?.let {
+                            println("  got new const4=$it")
                         }
 
-                        69 -> {
-                            error("encryptionMethod 69")
+
+                        discardExact(1)
+                        discardExact(1)
+                        val randomKey = readBytes(16)
+                        println("randomKey= ${randomKey.toUHexString()}")
+                        readUShort().toInt().takeIf { it != 258 }?.let {
+                            println("  got new const in ECDH head(originally=258)=$it")
                         }
-                        else -> error("unknown encryptionMethod=$encryptionMethod")
-                    }
+                        val publicKey = readBytes(readShort().toInt())
+                        println("ecdh publicKey=" + publicKey.toUHexString())
 
-                    val encryptedBody = readBytes((remaining - 1).toInt())
 
-                    val decrypted = kotlin.runCatching {
-                        encryptedBody.decryptBy(encrypt).also { println("first by calculatedShareKey or sessionKey(method=7)") }
-                    }.getOrElse {
-                        encryptedBody.decryptBy(shareKeyCalculatedByConstPubKey).also { println("first by shareKeyCalculatedByConstPubKey") }
-                    }.let { firstDecrypted ->
-                        runCatching {
-                            firstDecrypted.decryptBy(encrypt).also { println("second by calculatedShareKey") }
+                        val encrypt = when (encryptionMethod) {
+                            135, 7 -> {
+                                ECDH.calculateShareKey(
+                                    loadPrivateKey(ecdhPrivateKeyS),
+                                    //"04cb366698561e936e80c157e074cab13b0bb68ddeb2824548a1b18dd4fb6122afe12fe48c5266d8d7269d7651a8eb6fe7".chunkedHexToBytes().adjustToPublicKey() // QQ: 04cb366698561e936e80c157e074cab13b0bb68ddeb2824548a1b18dd4fb6122afe12fe48c5266d8d7269d7651a8eb6fe7
+                                    ECDH.constructPublicKey("30 46 30 10 06 07 2A 86 48 CE 3D 02 01 06 05 2B 81 04 00 1F 03 32 00".hexToBytes() + publicKey)
+                                )
+                            }
+
+                            69 -> {
+                                error("encryptionMethod 69")
+                            }
+                            else -> error("unknown encryptionMethod=$encryptionMethod")
+                        }
+
+                        val encryptedBody = readBytes((remaining - 1).toInt())
+
+                        val decrypted = kotlin.runCatching {
+                            encryptedBody.decryptBy(encrypt).also { println("first by calculatedShareKey or sessionKey(method=7)") }
                         }.getOrElse {
-                            kotlin.runCatching {
-                                firstDecrypted.decryptBy(shareKeyCalculatedByConstPubKey)
-                            }.getOrDefault(firstDecrypted)
-                        }
-                    }
-
-                    PacketLogger.info("Real body=" + decrypted.toUHexString())
-                    decrypted.toReadPacket().apply {
-                        if (commandId == 0x0810) {
-                            DebugLogger.info("发送 login!! 正在获取 tgtgtKey")
-                            try {
-                                discardExact(4)
-                                val tlvMap = readTLVMap()
-                                tlvMap.printTLVMap()
-                                tlvMap[0x106]
-                                    ?.also { DebugLogger.info("找到了 0x106") }
-                                    ?.decryptBy(md5(passwordMd5 + ByteArray(4) + uin.toInt().toByteArray()))
-                                    ?.read {
-                                        discardExact(2 + 4 * 4 + 8 + 4 + 4 + 1 + 16)
-                                        tgtgtKey = readBytes(16)
-                                        DebugLogger.info("获取 tgtgtKey=${tgtgtKey.toUHexString()}")
-                                    } ?: DebugLogger.info("找不到 0x106")
-                            } catch (e: Exception) {
-                                e.printStackTrace()
+                            encryptedBody.decryptBy(shareKeyCalculatedByConstPubKey).also { println("first by shareKeyCalculatedByConstPubKey") }
+                        }.let { firstDecrypted ->
+                            runCatching {
+                                firstDecrypted.decryptBy(encrypt).also { println("second by calculatedShareKey") }
+                            }.getOrElse {
+                                kotlin.runCatching {
+                                    firstDecrypted.decryptBy(shareKeyCalculatedByConstPubKey)
+                                }.getOrDefault(firstDecrypted)
                             }
                         }
 
+                        PacketLogger.info("Real body=" + decrypted.toUHexString())
+                        decrypted.toReadPacket().apply {
+                            if (commandId == 0x0810) {
+                                DebugLogger.info("发送 login!! 正在获取 tgtgtKey")
+                                try {
+                                    discardExact(4)
+                                    val tlvMap = readTLVMap()
+                                    tlvMap.printTLVMap()
+                                    tlvMap[0x106]
+                                        ?.also { DebugLogger.info("找到了 0x106") }
+                                        ?.decryptBy(md5(passwordMd5 + ByteArray(4) + uin.toInt().toByteArray()))
+                                        ?.read {
+                                            discardExact(2 + 4 * 4 + 8 + 4 + 4 + 1 + 16)
+                                            tgtgtKey = readBytes(16)
+                                            DebugLogger.info("获取 tgtgtKey=${tgtgtKey.toUHexString()}")
+                                        } ?: DebugLogger.info("找不到 0x106")
+                                } catch (e: Exception) {
+                                    e.printStackTrace()
+                                }
+                            }
+
+                        }
                     }
                 }
-            }
-            else -> {
-                this.debugPrintThis("uni packet")
+                else -> {
+                    this.debugPrintThis("uni packet")
+                }
             }
         }
     }
+
 }
 
 fun ByteReadPacket.decodeUni() {
@@ -354,8 +365,8 @@ val keys: Map<String, ByteArray>
         "shareKeyCalculatedByConstPubKey" to shareKeyCalculatedByConstPubKey,
         "t108" to t108,
         "t10c" to t10c,
-    "t163" to t163
-)
+        "t163" to t163
+    )
 
 fun ByteArray.tryDecrypt(): ByteArray {
     return this.tryDecryptOrNull() ?: error("Cannot decrypt. Encrypted data=" + this.toUHexString())
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt
index b3a9a9be6..a5c0f436f 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt
@@ -2,19 +2,110 @@
 
 package net.mamoe.mirai.message.data
 
+import kotlinx.serialization.Serializable
+
 sealed class Image : Message {
-    abstract val resourceId: String
+    abstract val md5: ByteArray
 
     abstract override fun toString(): String
 
-    final companion object Key : Message.Key<Image>
+    companion object Key : Message.Key<Image>
 
     abstract override fun eq(other: Message): Boolean
 }
 
+abstract class CustomFace : Image() {
+    abstract val filepath: String
+    abstract val fileId: Int
+    abstract val serverIp: Int
+    abstract val serverPort: Int
+    abstract val fileType: Int
+    abstract val signature: ByteArray
+    abstract val useful: Int
+    abstract override val md5: ByteArray
+    abstract val bizType: Int
+    abstract val imageType: Int
+    abstract val width: Int
+    abstract val height: Int
+    abstract val source: Int
+    abstract val size:Int
+    abstract val pbReserve: ByteArray
+
+    override fun toString(): String {
+        return "[CustomFace]"
+    }
+
+    override fun eq(other: Message): Boolean {
+        return this.toString() == other.toString()
+    }
+}
+
+@Serializable
+data class CustomFaceFromFile(
+    override val filepath: String,
+    override val fileId: Int,
+    override val serverIp: Int,
+    override val serverPort: Int,
+    override val fileType: Int,
+    override val signature: ByteArray,
+    override val useful: Int,
+    override val md5: ByteArray,
+    override val bizType: Int,
+    override val imageType: Int,
+    override val width: Int,
+    override val height: Int,
+    override val source: Int,
+    override val size: Int,
+    override val pbReserve: ByteArray
+) : CustomFace() {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || this::class != other::class) return false
+
+        other as CustomFaceFromFile
+
+        if (filepath != other.filepath) return false
+        if (fileId != other.fileId) return false
+        if (serverIp != other.serverIp) return false
+        if (serverPort != other.serverPort) return false
+        if (fileType != other.fileType) return false
+        if (!signature.contentEquals(other.signature)) return false
+        if (useful != other.useful) return false
+        if (!md5.contentEquals(other.md5)) return false
+        if (bizType != other.bizType) return false
+        if (imageType != other.imageType) return false
+        if (width != other.width) return false
+        if (height != other.height) return false
+        if (source != other.source) return false
+        if (size != other.size) return false
+        if (!pbReserve.contentEquals(other.pbReserve)) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = filepath.hashCode()
+        result = 31 * result + fileId
+        result = 31 * result + serverIp
+        result = 31 * result + serverPort
+        result = 31 * result + fileType
+        result = 31 * result + signature.contentHashCode()
+        result = 31 * result + useful
+        result = 31 * result + md5.contentHashCode()
+        result = 31 * result + bizType
+        result = 31 * result + imageType
+        result = 31 * result + width
+        result = 31 * result + height
+        result = 31 * result + source
+        result = 31 * result + size
+        result = 31 * result + pbReserve.contentHashCode()
+        return result
+    }
+}
+
 abstract class NotOnlineImage : Image() {
-    abstract override val resourceId: String
-    abstract val md5: ByteArray
+    abstract val resourceId: String
+    abstract override val md5: ByteArray
     abstract val filepath: String
     abstract val fileLength: Int
     abstract val height: Int
@@ -24,7 +115,7 @@ abstract class NotOnlineImage : Image() {
     open val downloadPath: String get() = resourceId
 
     override fun toString(): String {
-        return "[$resourceId]"
+        return "[NotOnlineImage $resourceId]"
     }
 
     override fun eq(other: Message): Boolean {
@@ -32,7 +123,7 @@ abstract class NotOnlineImage : Image() {
     }
 }
 
-open class NotOnlineImageFromFile(
+data class NotOnlineImageFromFile(
     override val resourceId: String,
     override val md5: ByteArray,
     override val filepath: String,
@@ -42,4 +133,36 @@ open class NotOnlineImageFromFile(
     override val bizType: Int = 0,
     override val imageType: Int = 1000,
     override val downloadPath: String = resourceId
-) : NotOnlineImage()
\ No newline at end of file
+) : NotOnlineImage() {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || this::class != other::class) return false
+
+        other as NotOnlineImageFromFile
+
+        if (resourceId != other.resourceId) return false
+        if (!md5.contentEquals(other.md5)) return false
+        if (filepath != other.filepath) return false
+        if (fileLength != other.fileLength) return false
+        if (height != other.height) return false
+        if (width != other.width) return false
+        if (bizType != other.bizType) return false
+        if (imageType != other.imageType) return false
+        if (downloadPath != other.downloadPath) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = resourceId.hashCode()
+        result = 31 * result + md5.contentHashCode()
+        result = 31 * result + filepath.hashCode()
+        result = 31 * result + fileLength
+        result = 31 * result + height
+        result = 31 * result + width
+        result = 31 * result + bizType
+        result = 31 * result + imageType
+        result = 31 * result + downloadPath.hashCode()
+        return result
+    }
+}
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/ECDH.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/ECDH.kt
index e93a21355..6e49c700b 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/ECDH.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/ECDH.kt
@@ -1,7 +1,6 @@
 package net.mamoe.mirai.utils.cryptor
 
 import net.mamoe.mirai.utils.io.chunkedHexToBytes
-import net.mamoe.mirai.utils.io.toUHexString
 
 expect interface ECDHPrivateKey {
     fun getEncoded(): ByteArray
@@ -65,8 +64,11 @@ private val commonHeadForNot02 = "3046301006072A8648CE3D020106052B8104001F033200
 private const val constantHead = "3046301006072A8648CE3D020106052B8104001F03320004"
 private val byteArray_04 = byteArrayOf(0x04)
 
-fun ByteArray.adjustToPublicKey(): ECDHPublicKey {
-    val head = if(this.size<30) "302E301006072A8648CE3D020106052B8104001F031A00" else "3046301006072A8648CE3D020106052B8104001F03320004"
 
-    return ECDH.constructPublicKey((head + this.toUHexString("")).chunkedHexToBytes())
+private val head1 = "302E301006072A8648CE3D020106052B8104001F031A00".chunkedHexToBytes()
+private val head2 = "3046301006072A8648CE3D020106052B8104001F03320004".chunkedHexToBytes()
+fun ByteArray.adjustToPublicKey(): ECDHPublicKey {
+    val head = if (this.size < 30) head1 else head2
+
+    return ECDH.constructPublicKey(head + this)
 }
\ No newline at end of file
diff --git a/mirai-demos/mirai-demo-gentleman/src/main/kotlin/demo/gentleman/Main.kt b/mirai-demos/mirai-demo-gentleman/src/main/kotlin/demo/gentleman/Main.kt
index cca3e8f43..422ae5956 100644
--- a/mirai-demos/mirai-demo-gentleman/src/main/kotlin/demo/gentleman/Main.kt
+++ b/mirai-demos/mirai-demo-gentleman/src/main/kotlin/demo/gentleman/Main.kt
@@ -23,6 +23,7 @@ import net.mamoe.mirai.message.data.buildXMLMessage
 import net.mamoe.mirai.message.data.getValue
 import net.mamoe.mirai.message.sendAsImageTo
 import net.mamoe.mirai.utils.ContextImpl
+import net.mamoe.mirai.utils.io.toUHexString
 import java.io.File
 import java.util.*
 import javax.swing.filechooser.FileSystemView
@@ -130,7 +131,7 @@ suspend fun main() {
 
                 try {
                     image.downloadTo(newTestTempFile(suffix = ".png").also { reply("Temp file: ${it.absolutePath}") })
-                    reply(image.resourceId + " downloaded")
+                    reply(image.md5.toUHexString() + " downloaded")
                 } catch (e: Exception) {
                     e.printStackTrace()
                     reply(e.message ?: e::class.java.simpleName)