mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-16 12:00:03 +03:00
Mark verification state as dirty
to avoid false positive decrypted by deleted session
This commit is contained in:
parent
ebed3195bf
commit
9d75a66aea
13 changed files with 293 additions and 64 deletions
|
@ -20,6 +20,7 @@ import android.content.Context
|
|||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
@ -29,6 +30,7 @@ import kotlinx.coroutines.delay
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
|
@ -183,6 +185,88 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
|
|||
return sentEvents
|
||||
}
|
||||
|
||||
suspend fun sendMessageInRoom(room: Room, text: String): String {
|
||||
room.sendService().sendTextMessage(text)
|
||||
|
||||
val timeline = room.timelineService().createTimeline(null, TimelineSettings(60))
|
||||
timeline.start()
|
||||
|
||||
val messageSent = CompletableDeferred<String>()
|
||||
timeline.addListener(object : Timeline.Listener {
|
||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
val decryptedMsg = timeline.getSnapshot()
|
||||
.filter { it.root.getClearType() == EventType.MESSAGE }
|
||||
.also { list ->
|
||||
val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" }
|
||||
Log.v("#E2E TEST", "Timeline snapshot is $message")
|
||||
}
|
||||
.filter { it.root.sendState == SendState.SYNCED }
|
||||
.firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true }
|
||||
if (decryptedMsg != null) {
|
||||
timeline.dispose()
|
||||
messageSent.complete(decryptedMsg.eventId)
|
||||
}
|
||||
}
|
||||
})
|
||||
return withTimeout(TestConstants.timeOutMillis) { messageSent.await() }
|
||||
}
|
||||
|
||||
suspend fun ensureMessage(room: Room, eventId: String, block: ((event: TimelineEvent) -> Boolean)) {
|
||||
val timeline = room.timelineService().createTimeline(null, TimelineSettings(60))
|
||||
timeline.start()
|
||||
val messageSent = CompletableDeferred<Unit>()
|
||||
timeline.addListener(object : Timeline.Listener {
|
||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
val success = timeline.getSnapshot()
|
||||
.filter { it.root.getClearType() == EventType.MESSAGE }
|
||||
.also { list ->
|
||||
val message = list.joinToString(",", "[", "]") {
|
||||
"${it.root.type}|${it.root.getClearType()}|${it.root.sendState}|${it.root.mxDecryptionResult?.verificationState}"
|
||||
}
|
||||
Log.v("#E2E TEST", "Timeline snapshot is $message")
|
||||
}
|
||||
.firstOrNull { it.eventId == eventId }
|
||||
?.let {
|
||||
block(it)
|
||||
} ?: false
|
||||
if (success) {
|
||||
messageSent.complete(Unit)
|
||||
timeline.dispose()
|
||||
}
|
||||
}
|
||||
})
|
||||
return withTimeout(TestConstants.timeOutMillis) {
|
||||
messageSent.await()
|
||||
}
|
||||
}
|
||||
|
||||
fun ensureMessagePromise(room: Room, eventId: String, block: ((event: TimelineEvent) -> Boolean)): CompletableDeferred<Unit> {
|
||||
val timeline = room.timelineService().createTimeline(null, TimelineSettings(60))
|
||||
timeline.start()
|
||||
val messageSent = CompletableDeferred<Unit>()
|
||||
timeline.addListener(object : Timeline.Listener {
|
||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
val success = timeline.getSnapshot()
|
||||
.filter { it.root.getClearType() == EventType.MESSAGE }
|
||||
.also { list ->
|
||||
val message = list.joinToString(",", "[", "]") {
|
||||
"${it.root.type}|${it.root.getClearType()}|${it.root.sendState}|${it.root.mxDecryptionResult?.verificationState}"
|
||||
}
|
||||
Log.v("#E2E TEST", "Promise Timeline snapshot is $message")
|
||||
}
|
||||
.firstOrNull { it.eventId == eventId }
|
||||
?.let {
|
||||
block(it)
|
||||
} ?: false
|
||||
if (success) {
|
||||
messageSent.complete(Unit)
|
||||
timeline.dispose()
|
||||
}
|
||||
}
|
||||
})
|
||||
return messageSent
|
||||
}
|
||||
|
||||
/**
|
||||
* Will send nb of messages provided by count parameter but waits every 10 messages to avoid gap in sync
|
||||
*/
|
||||
|
|
|
@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.crypto
|
|||
|
||||
import android.util.Log
|
||||
import androidx.test.filters.LargeTest
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.delay
|
||||
import org.amshove.kluent.fail
|
||||
import org.amshove.kluent.internal.assertEquals
|
||||
|
@ -36,18 +35,14 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
|||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MessageVerificationState
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
|
||||
|
@ -94,22 +89,16 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
Log.v("#E2E TEST", "Alice is sending the message")
|
||||
|
||||
val text = "This is my message"
|
||||
val sentEventId: String? = sendMessageInRoom(aliceRoomPOV, text)
|
||||
Assert.assertTrue("Message should be sent", sentEventId != null)
|
||||
val sentEventId: String = testHelper.sendMessageInRoom(aliceRoomPOV, text)
|
||||
Log.v("#E2E TEST", "Alice just sent message with id:$sentEventId")
|
||||
|
||||
// All should be able to decrypt
|
||||
otherAccounts.forEach { otherSession ->
|
||||
testHelper.retryWithBackoff(
|
||||
onFail = {
|
||||
fail("${otherSession.myUserId.take(10)} should be able to decrypt")
|
||||
}) {
|
||||
val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also {
|
||||
Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}|${it?.root?.mxDecryptionResult?.isSafe}")
|
||||
}
|
||||
timeLineEvent != null &&
|
||||
timeLineEvent.isEncrypted() &&
|
||||
timeLineEvent.root.getClearType() == EventType.MESSAGE &&
|
||||
timeLineEvent.root.mxDecryptionResult?.isSafe == true
|
||||
val room = otherSession.getRoom(e2eRoomID)!!
|
||||
testHelper.ensureMessage(room, sentEventId) {
|
||||
it.isEncrypted() &&
|
||||
it.root.getClearType() == EventType.MESSAGE &&
|
||||
it.root.mxDecryptionResult?.verificationState == MessageVerificationState.UN_SIGNED_DEVICE
|
||||
}
|
||||
}
|
||||
Log.v("#E2E TEST", "Everybody received the encrypted message and could decrypt")
|
||||
|
@ -143,7 +132,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
Log.v("#E2E TEST", "Alice sends a new message")
|
||||
|
||||
val secondMessage = "2 This is my message"
|
||||
val secondSentEventId: String? = sendMessageInRoom(aliceRoomPOV, secondMessage)
|
||||
val secondSentEventId: String = testHelper.sendMessageInRoom(aliceRoomPOV, secondMessage)
|
||||
|
||||
// new members should be able to decrypt it
|
||||
newAccount.forEach { otherSession ->
|
||||
|
@ -206,7 +195,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
val sentEventIds = mutableListOf<String>()
|
||||
val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning")
|
||||
messagesText.forEach { text ->
|
||||
val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
|
||||
val sentEventId = testHelper.sendMessageInRoom(aliceRoomPOV, text).also {
|
||||
sentEventIds.add(it)
|
||||
}
|
||||
|
||||
|
@ -297,7 +286,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
sentEventIds.forEach { sentEventId ->
|
||||
val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)!!
|
||||
val result = newBobSession.cryptoService().decryptEvent(timelineEvent.root, "")
|
||||
assertEquals("Keys from history should be deniable", false, result.isSafe)
|
||||
assertEquals("Keys from history should be deniable", MessageVerificationState.UNSAFE_SOURCE, result.messageVerificationState)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -321,7 +310,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
|
||||
Log.v("#E2E TEST", "Alice sends some messages")
|
||||
messagesText.forEach { text ->
|
||||
val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
|
||||
val sentEventId = testHelper.sendMessageInRoom(aliceRoomPOV, text).also {
|
||||
sentEventIds.add(it)
|
||||
}
|
||||
|
||||
|
@ -404,7 +393,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
|
||||
Log.v("#E2E TEST", "Alice sends some messages")
|
||||
firstMessage.let { text ->
|
||||
firstEventId = sendMessageInRoom(aliceRoomPOV, text)!!
|
||||
firstEventId = testHelper.sendMessageInRoom(aliceRoomPOV, text)
|
||||
|
||||
testHelper.retryWithBackoff {
|
||||
val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
|
||||
|
@ -430,7 +419,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
|
||||
Log.v("#E2E TEST", "Alice sends some messages")
|
||||
secondMessage.let { text ->
|
||||
secondEventId = sendMessageInRoom(aliceRoomPOV, text)!!
|
||||
secondEventId = testHelper.sendMessageInRoom(aliceRoomPOV, text)
|
||||
|
||||
testHelper.retryWithBackoff {
|
||||
val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
|
||||
|
@ -493,32 +482,6 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? {
|
||||
aliceRoomPOV.sendService().sendTextMessage(text)
|
||||
|
||||
val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60))
|
||||
timeline.start()
|
||||
|
||||
val messageSent = CompletableDeferred<String>()
|
||||
timeline.addListener(object : Timeline.Listener {
|
||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
val decryptedMsg = timeline.getSnapshot()
|
||||
.filter { it.root.getClearType() == EventType.MESSAGE }
|
||||
.also { list ->
|
||||
val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" }
|
||||
Log.v("#E2E TEST", "Timeline snapshot is $message")
|
||||
}
|
||||
.filter { it.root.sendState == SendState.SYNCED }
|
||||
.firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true }
|
||||
if (decryptedMsg != null) {
|
||||
timeline.dispose()
|
||||
messageSent.complete(decryptedMsg.eventId)
|
||||
}
|
||||
}
|
||||
})
|
||||
return messageSent.await()
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that if a better key is forwared (lower index, it is then used)
|
||||
*/
|
||||
|
@ -632,7 +595,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
|
||||
val roomFromAlicePOV = aliceSession.getRoom(cryptoTestData.roomId)!!
|
||||
Timber.v("#TEST: Send a first message that should be withheld")
|
||||
val sentEvent = sendMessageInRoom(roomFromAlicePOV, "Hello")!!
|
||||
val sentEvent = testHelper.sendMessageInRoom(roomFromAlicePOV, "Hello")
|
||||
|
||||
// wait for it to be synced back the other side
|
||||
Timber.v("#TEST: Wait for message to be synced back")
|
||||
|
@ -655,7 +618,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
|
||||
Timber.v("#TEST: Send a second message, outbound session should have rotated and only bob 1rst session should decrypt")
|
||||
|
||||
val secondEvent = sendMessageInRoom(roomFromAlicePOV, "World")!!
|
||||
val secondEvent = testHelper.sendMessageInRoom(roomFromAlicePOV, "World")
|
||||
Timber.v("#TEST: Wait for message to be synced back")
|
||||
testHelper.retryWithBackoff {
|
||||
bobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(secondEvent) != null
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import android.util.Log
|
||||
import androidx.test.filters.LargeTest
|
||||
import junit.framework.Assert.fail
|
||||
import kotlinx.coroutines.delay
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MessageVerificationState
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
@LargeTest
|
||||
class E2eeTestVerificationTestDirty : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun testVerificationStateRefreshedAfterKeyDownload() = CommonTestHelper.runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession!!
|
||||
val e2eRoomID = cryptoTestData.roomId
|
||||
|
||||
// We are going to setup a second session for bob that will send a message while alice session
|
||||
// has stopped syncing.
|
||||
|
||||
aliceSession.syncService().stopSync()
|
||||
aliceSession.syncService().stopAnyBackgroundSync()
|
||||
// wait a bit for session to be really closed
|
||||
delay(1_000)
|
||||
|
||||
Log.v("#E2E TEST", "Create a new session for Bob")
|
||||
val newBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
|
||||
|
||||
Log.v("#E2E TEST", "New bob session will send a message")
|
||||
val eventId = testHelper.sendMessageInRoom(newBobSession.getRoom(e2eRoomID)!!, "I am unknown")
|
||||
|
||||
aliceSession.syncService().startSync(true)
|
||||
|
||||
// Check without starting a timeline so that it doesn't update itself
|
||||
testHelper.retryWithBackoff(
|
||||
onFail = {
|
||||
fail("${aliceSession.myUserId.take(10)} should not have downloaded the device at time of decryption")
|
||||
}) {
|
||||
val timeLineEvent = aliceSession.getRoom(e2eRoomID)?.getTimelineEvent(eventId).also {
|
||||
Log.v("#E2E TEST", "Verification state is ${it?.root?.mxDecryptionResult?.verificationState}")
|
||||
}
|
||||
timeLineEvent != null &&
|
||||
timeLineEvent.isEncrypted() &&
|
||||
timeLineEvent.root.getClearType() == EventType.MESSAGE &&
|
||||
timeLineEvent.root.mxDecryptionResult?.verificationState == MessageVerificationState.UNKNOWN_DEVICE
|
||||
}
|
||||
|
||||
// After key download it should be dirty (that will happen after sync completed)
|
||||
testHelper.retryWithBackoff(
|
||||
onFail = {
|
||||
fail("${aliceSession.myUserId.take(10)} should be dirty")
|
||||
}) {
|
||||
val timeLineEvent = aliceSession.getRoom(e2eRoomID)?.getTimelineEvent(eventId).also {
|
||||
Log.v("#E2E TEST", "Is verification state dirty ${it?.root?.verificationStateIsDirty}")
|
||||
}
|
||||
timeLineEvent?.root?.verificationStateIsDirty.orFalse()
|
||||
}
|
||||
|
||||
Log.v("#E2E TEST", "Start timeline and check that verification state is updated")
|
||||
// eventually should be marked as dirty then have correct state when a timeline is started
|
||||
testHelper.ensureMessage(aliceSession.getRoom(e2eRoomID)!!, eventId) {
|
||||
it.isEncrypted() &&
|
||||
it.root.getClearType() == EventType.MESSAGE &&
|
||||
it.root.mxDecryptionResult?.verificationState == MessageVerificationState.UN_SIGNED_DEVICE
|
||||
}
|
||||
}
|
||||
}
|
|
@ -189,7 +189,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
private val liveEventManager: Lazy<StreamEventsManager>,
|
||||
private val unrequestedForwardManager: UnRequestedForwardManager,
|
||||
private val cryptoSyncHandler: CryptoSyncHandler,
|
||||
) : CryptoService {
|
||||
) : CryptoService, DeviceListManager.UserDevicesUpdateListener {
|
||||
|
||||
private val isStarting = AtomicBoolean(false)
|
||||
private val isStarted = AtomicBoolean(false)
|
||||
|
@ -312,6 +312,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
fetchDevicesList()
|
||||
}
|
||||
cryptoStore.tidyUpDataBase()
|
||||
deviceListManager.addListener(this@DefaultCryptoService)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -375,6 +376,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
* Close the crypto.
|
||||
*/
|
||||
override fun close() = runBlocking(coroutineDispatchers.crypto) {
|
||||
deviceListManager.removeListener(this@DefaultCryptoService)
|
||||
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
|
||||
incomingKeyRequestManager.close()
|
||||
outgoingKeyRequestManager.close()
|
||||
|
@ -1309,6 +1311,10 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
override fun removeSessionListener(listener: NewSessionListener) {
|
||||
roomDecryptorProvider.removeSessionListener(listener)
|
||||
}
|
||||
|
||||
override fun onUsersDeviceUpdate(userIds: List<String>) {
|
||||
cryptoSessionInfoProvider.markMessageVerificationStateAsDirty(userIds)
|
||||
}
|
||||
/* ==========================================================================================
|
||||
* DEBUG INFO
|
||||
* ========================================================================================== */
|
||||
|
|
|
@ -108,6 +108,9 @@ data class Event(
|
|||
@Transient
|
||||
var threadDetails: ThreadDetails? = null
|
||||
|
||||
@Transient
|
||||
var verificationStateIsDirty: Boolean? = null
|
||||
|
||||
fun sendStateError(): MatrixError? {
|
||||
return sendStateDetails?.let {
|
||||
val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java)
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto
|
|||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.internal.database.mapper.EventMapper
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
|
||||
|
@ -100,6 +101,23 @@ internal class CryptoSessionInfoProvider @Inject constructor(
|
|||
return roomIds.orEmpty()
|
||||
}
|
||||
|
||||
fun markMessageVerificationStateAsDirty(userList: List<String>) {
|
||||
monarchy.writeAsync { sessionRealm ->
|
||||
sessionRealm.where(EventEntity::class.java)
|
||||
.`in`(EventEntityFields.SENDER, userList.toTypedArray())
|
||||
.equalTo(EventEntityFields.TYPE, EventType.ENCRYPTED)
|
||||
.isNotNull(EventEntityFields.DECRYPTION_RESULT_JSON)
|
||||
// // A bit annoying to have to do that like that and it could break :/
|
||||
// .contains(EventEntityFields.DECRYPTION_RESULT_JSON, "\"verification_state\":\"UNKNOWN_DEVICE\"")
|
||||
.findAll()
|
||||
.onEach {
|
||||
it.isVerificationStateDirty = true
|
||||
}
|
||||
.map { EventMapper.map(it) }
|
||||
.also { Timber.v("## VerificationState refresh - ... impacted events ${it.joinToString{ it.eventId.orEmpty() }}") }
|
||||
}
|
||||
}
|
||||
|
||||
fun updateShieldForRoom(roomId: String, shield: RoomEncryptionTrustLevel?) {
|
||||
monarchy.writeAsync { realm ->
|
||||
val summary = RoomSummaryEntity.where(realm, roomId = roomId).findFirst()
|
||||
|
|
|
@ -67,6 +67,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo047
|
|||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo048
|
||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo049
|
||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo050
|
||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo051
|
||||
import org.matrix.android.sdk.internal.util.Normalizer
|
||||
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
|
||||
import javax.inject.Inject
|
||||
|
@ -75,7 +76,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||
private val normalizer: Normalizer
|
||||
) : MatrixRealmMigration(
|
||||
dbName = "Session",
|
||||
schemaVersion = 50L,
|
||||
schemaVersion = 51L,
|
||||
) {
|
||||
/**
|
||||
* Forces all RealmSessionStoreMigration instances to be equal.
|
||||
|
@ -135,5 +136,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||
if (oldVersion < 48) MigrateSessionTo048(realm).perform()
|
||||
if (oldVersion < 49) MigrateSessionTo049(realm).perform()
|
||||
if (oldVersion < 50) MigrateSessionTo050(realm).perform()
|
||||
if (oldVersion < 51) MigrateSessionTo051(realm).perform()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ internal object EventMapper {
|
|||
eventEntity.decryptionResultJson = event.mxDecryptionResult?.let {
|
||||
MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(it)
|
||||
}
|
||||
eventEntity.isVerificationStateDirty = event.verificationStateIsDirty
|
||||
eventEntity.decryptionErrorReason = event.mCryptoErrorReason
|
||||
eventEntity.decryptionErrorCode = event.mCryptoError?.name
|
||||
eventEntity.isRootThread = event.threadDetails?.isRootThread ?: false
|
||||
|
@ -93,6 +94,7 @@ internal object EventMapper {
|
|||
eventEntity.decryptionResultJson?.let { json ->
|
||||
try {
|
||||
it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json)
|
||||
it.verificationStateIsDirty = eventEntity.isVerificationStateDirty
|
||||
} catch (t: JsonDataException) {
|
||||
Timber.e(t, "Failed to parse decryption result")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (c) 2023 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.database.migration
|
||||
|
||||
import io.realm.DynamicRealm
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
||||
import org.matrix.android.sdk.internal.util.database.RealmMigrator
|
||||
|
||||
/**
|
||||
* As we compute message e2ee verification state at decryption time, it might get outdated.
|
||||
* Adding a new field to mark a decryption state as dirty
|
||||
*/
|
||||
internal class MigrateSessionTo051(realm: DynamicRealm) : RealmMigrator(realm, 51) {
|
||||
|
||||
override fun doMigrate(realm: DynamicRealm) {
|
||||
realm.schema.create("EventEntity")
|
||||
.addField(EventEntityFields.IS_VERIFICATION_STATE_DIRTY, Boolean::class.java)
|
||||
.setNullable(EventEntityFields.IS_VERIFICATION_STATE_DIRTY, true)
|
||||
}
|
||||
}
|
|
@ -47,7 +47,8 @@ internal open class EventEntity(
|
|||
@Index var rootThreadEventId: String? = null,
|
||||
// Number messages within the thread
|
||||
var numberOfThreads: Int = 0,
|
||||
var threadSummaryLatestMessage: TimelineEventEntity? = null
|
||||
var threadSummaryLatestMessage: TimelineEventEntity? = null,
|
||||
var isVerificationStateDirty: Boolean? = null,
|
||||
) : RealmObject() {
|
||||
|
||||
private var sendStateStr: String = SendState.UNKNOWN.name
|
||||
|
@ -94,6 +95,7 @@ internal open class EventEntity(
|
|||
decryptionResultJson = adapter.toJson(decryptionResult)
|
||||
decryptionErrorCode = null
|
||||
decryptionErrorReason = null
|
||||
isVerificationStateDirty = false
|
||||
|
||||
// If we have an EventInsertEntity for the eventId we make sures it can be processed now.
|
||||
realm.where(EventInsertEntity::class.java)
|
||||
|
|
|
@ -25,6 +25,7 @@ import io.realm.Sort
|
|||
import kotlinx.coroutines.CompletableDeferred
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MessageVerificationState
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.isReply
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
|
@ -420,13 +421,22 @@ internal class TimelineChunk(
|
|||
}
|
||||
|
||||
fun decryptIfNeeded(timelineEvent: TimelineEvent) {
|
||||
if (timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.mxDecryptionResult == null) {
|
||||
timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) }
|
||||
}
|
||||
if (!timelineEvent.isEncrypted() && !lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
// Thread aware for not encrypted events
|
||||
if (!timelineEvent.isEncrypted()) return
|
||||
val mxDecryptionResult = timelineEvent.root.mxDecryptionResult
|
||||
if (mxDecryptionResult == null) {
|
||||
timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) }
|
||||
} else if (timelineEvent.root.verificationStateIsDirty.orFalse() &&
|
||||
mxDecryptionResult.verificationState == MessageVerificationState.UNKNOWN_DEVICE
|
||||
) {
|
||||
// The goal is to catch late download of devices
|
||||
timelineEvent.root.eventId?.also {
|
||||
eventDecryptor.requestDecryption(
|
||||
TimelineEventDecryptor.DecryptionRequest(
|
||||
timelineEvent.root,
|
||||
timelineId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -414,7 +414,7 @@ internal class RoomSyncHandler @Inject constructor(
|
|||
// It's annoying roomId is not there, but lot of code rely on it.
|
||||
// And had to do it now as copy would delete all decryption results..
|
||||
val ageLocalTs = syncLocalTimestampMillis - (rawEvent.unsignedData?.age ?: 0)
|
||||
val event = rawEvent.copy(roomId = roomId).also {
|
||||
val event = rawEvent.copyAll(roomId = roomId).also {
|
||||
it.ageLocalTs = ageLocalTs
|
||||
}
|
||||
if (event.eventId == null || event.senderId == null || event.type == null) {
|
||||
|
|
|
@ -24,6 +24,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.internal.crypto.ComputeShieldForGroupUseCase
|
||||
|
@ -44,6 +45,7 @@ internal class OutgoingRequestsProcessor @Inject constructor(
|
|||
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
||||
private val computeShieldForGroup: ComputeShieldForGroupUseCase,
|
||||
private val matrixConfiguration: MatrixConfiguration,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
) {
|
||||
|
||||
private val lock: Mutex = Mutex()
|
||||
|
@ -136,6 +138,7 @@ internal class OutgoingRequestsProcessor @Inject constructor(
|
|||
val response = requestSender.queryKeys(request)
|
||||
olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_QUERY, response)
|
||||
coroutineScope.updateShields(olmMachine, request.users)
|
||||
coroutineScope.markMessageVerificationStatesAsDirty(request.users)
|
||||
true
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.tag(loggerTag.value).e(throwable, "## queryKeys(): error")
|
||||
|
@ -143,7 +146,7 @@ internal class OutgoingRequestsProcessor @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.updateShields(olmMachine: OlmMachine, userIds: List<String>) = launch {
|
||||
private fun CoroutineScope.updateShields(olmMachine: OlmMachine, userIds: List<String>) = launch(coroutineDispatchers.computation) {
|
||||
cryptoSessionInfoProvider.getRoomsWhereUsersAreParticipating(userIds).forEach { roomId ->
|
||||
if (cryptoSessionInfoProvider.isRoomEncrypted(roomId)) {
|
||||
val userGroup = cryptoSessionInfoProvider.getUserListForShieldComputation(roomId)
|
||||
|
@ -155,6 +158,10 @@ internal class OutgoingRequestsProcessor @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.markMessageVerificationStatesAsDirty(userIds: List<String>) = launch(coroutineDispatchers.computation) {
|
||||
cryptoSessionInfoProvider.markMessageVerificationStateAsDirty(userIds)
|
||||
}
|
||||
|
||||
private suspend fun sendToDevice(olmMachine: OlmMachine, request: Request.ToDevice): Boolean {
|
||||
return try {
|
||||
requestSender.sendToDevice(request)
|
||||
|
|
Loading…
Add table
Reference in a new issue