diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index a21475f3ac..8f29e05609 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -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() + timeline.addListener(object : Timeline.Listener { + override fun onTimelineUpdated(snapshot: List) { + 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()?.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() + timeline.addListener(object : Timeline.Listener { + override fun onTimelineUpdated(snapshot: List) { + 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 { + val timeline = room.timelineService().createTimeline(null, TimelineSettings(60)) + timeline.start() + val messageSent = CompletableDeferred() + timeline.addListener(object : Timeline.Listener { + override fun onTimelineUpdated(snapshot: List) { + 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 */ diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt index 472446ca9d..c43f98fe67 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt @@ -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() 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() - timeline.addListener(object : Timeline.Listener { - override fun onTimelineUpdated(snapshot: List) { - 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()?.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 diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeTestVerificationTestDirty.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeTestVerificationTestDirty.kt new file mode 100644 index 0000000000..2f7ad5861c --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeTestVerificationTestDirty.kt @@ -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 + } + } +} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index d0bd65e9bb..bc0263cc43 100755 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -189,7 +189,7 @@ internal class DefaultCryptoService @Inject constructor( private val liveEventManager: Lazy, 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) { + cryptoSessionInfoProvider.markMessageVerificationStateAsDirty(userIds) + } /* ========================================================================================== * DEBUG INFO * ========================================================================================== */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 40c69ceb66..4dfa8b097d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -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) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt index 1eaf1a8085..e26ca2f86a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt @@ -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) { + 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() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 45bcd792c2..c5ececcddb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -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() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt index 0f0a847c78..2f243dd855 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt @@ -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") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo051.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo051.kt new file mode 100644 index 0000000000..b6eb87c600 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo051.kt @@ -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) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt index 9574e443ad..0583ae5b9c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt @@ -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) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index d04b98ef76..917b019196 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -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 + ) + ) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 2a1be93a92..2e3707b7ad 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -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) { diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt index 0119af3bdf..77fd9b3ea3 100644 --- a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt +++ b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt @@ -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) = launch { + private fun CoroutineScope.updateShields(olmMachine: OlmMachine, userIds: List) = 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) = launch(coroutineDispatchers.computation) { + cryptoSessionInfoProvider.markMessageVerificationStateAsDirty(userIds) + } + private suspend fun sendToDevice(olmMachine: OlmMachine, request: Request.ToDevice): Boolean { return try { requestSender.sendToDevice(request)