From 28f670d1d88f6c66f0d76750af96fc26f2f9b821 Mon Sep 17 00:00:00 2001 From: Benoit Marty <benoit@matrix.org> Date: Wed, 28 Sep 2022 18:05:57 +0200 Subject: [PATCH] Import v1.5.1 from Element Android --- dependencies.gradle | 6 +- dependencies_groups.gradle | 2 - .../android/sdk/common/CommonTestHelper.kt | 12 +- .../android/sdk/common/CryptoTestHelper.kt | 3 +- .../sdk/internal/crypto/E2eeSanityTests.kt | 73 +++--- .../crypto/E2eeShareKeysHistoryTest.kt | 14 +- .../sdk/internal/crypto/UnwedgingTest.kt | 6 +- .../crypto/gossiping/KeyShareTests.kt | 82 ++++-- .../crypto/gossiping/WithHeldTests.kt | 11 +- .../android/sdk/api/crypto/MXCryptoConfig.kt | 5 +- .../android/sdk/api/query/QueryStringValue.kt | 5 + .../sdk/api/session/SessionExtensions.kt | 8 + .../crypto/model/MXEventDecryptionResult.kt | 4 +- .../crypto/model/OlmDecryptionResult.kt | 7 +- .../sdk/api/session/events/model/Event.kt | 18 +- .../android/sdk/api/session/room/Room.kt | 12 + .../sdk/api/session/room/RoomService.kt | 7 + .../session/room/RoomSummaryQueryParams.kt | 8 + .../room/model/LocalRoomCreationState.kt | 24 ++ .../session/room/model/LocalRoomSummary.kt | 46 ++++ .../api/session/room/model/SpaceChildInfo.kt | 1 - .../room/model/localecho/RoomLocalEcho.kt | 2 +- .../api/session/space/SpaceHierarchyData.kt | 4 +- .../sdk/api/session/space/SpaceService.kt | 6 +- .../space/model/SpaceChildSummaryEvent.kt | 30 +++ .../sdk/api/session/user/UserService.kt | 2 +- .../internal/crypto/DefaultCryptoService.kt | 30 ++- .../crypto/InboundGroupSessionStore.kt | 15 ++ .../sdk/internal/crypto/MXOlmDevice.kt | 66 +++-- .../sdk/internal/crypto/SecretShareManager.kt | 13 +- .../crypto/algorithms/IMXDecrypting.kt | 2 +- .../algorithms/megolm/MXMegolmDecryption.kt | 122 ++++++--- .../megolm/MXMegolmDecryptionFactory.kt | 24 +- .../algorithms/megolm/MXMegolmEncryption.kt | 3 +- .../megolm/UnRequestedForwardManager.kt | 150 +++++++++++ .../keysbackup/DefaultKeysBackupService.kt | 7 - .../crypto/model/InboundGroupSessionData.kt | 9 +- .../model/MXInboundMegolmSessionWrapper.kt | 1 + .../store/db/RealmCryptoStoreMigration.kt | 4 +- .../store/db/migration/MigrateCryptoTo018.kt | 52 ++++ .../internal/crypto/tasks/EncryptEventTask.kt | 3 +- .../database/RealmSessionStoreMigration.kt | 4 +- .../database/helper/ThreadSummaryHelper.kt | 3 +- .../database/mapper/LocalRoomSummaryMapper.kt | 36 +++ .../database/migration/MigrateSessionTo037.kt | 34 +++ .../internal/database/model/EventEntity.kt | 3 +- .../database/model/LocalRoomSummaryEntity.kt | 11 +- .../query/LocalRoomSummaryEntityQueries.kt | 8 +- .../query/ReadReceiptEntityQueries.kt | 5 + .../query/QueryStringValueProcessor.kt | 1 + .../sdk/internal/session/room/DefaultRoom.kt | 9 + .../session/room/DefaultRoomService.kt | 11 +- .../create/CreateRoomFromLocalRoomTask.kt | 135 +++++----- .../room/delete/DeleteLocalRoomTask.kt | 9 + .../threads/FetchThreadTimelineTask.kt | 3 +- .../room/summary/RoomSummaryDataSource.kt | 24 ++ .../session/room/timeline/GetEventTask.kt | 3 +- .../session/space/DefaultSpaceService.kt | 20 +- .../space/SpaceChildSummaryResponse.kt | 9 +- .../session/sync/handler/CryptoSyncHandler.kt | 50 +++- .../sync/handler/room/RoomSyncHandler.kt | 6 +- .../crypto/UnRequestedKeysManagerTest.kt | 248 ++++++++++++++++++ .../DefaultCreateRoomFromLocalRoomTaskTest.kt | 131 ++++++--- .../android/sdk/test/fakes/FakeMonarchy.kt | 5 + .../test/fakes/FakeRoomSummaryDataSource.kt | 36 +++ 65 files changed, 1376 insertions(+), 327 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomCreationState.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomSummary.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildSummaryEvent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo018.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LocalRoomSummaryMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo037.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomSummaryDataSource.kt diff --git a/dependencies.gradle b/dependencies.gradle index 3759763f..9641a63f 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -15,14 +15,14 @@ def gradle = "7.1.3" def kotlin = "1.6.21" def kotlinCoroutines = "1.6.4" def dagger = "2.42" -def appDistribution = "16.0.0-beta03" +def appDistribution = "16.0.0-beta04" def retrofit = "2.9.0" def arrow = "0.8.2" def markwon = "4.6.2" def moshi = "1.13.0" def lifecycle = "2.5.1" def flowBinding = "1.2.0" -def flipper = "0.163.0" +def flipper = "0.164.0" def epoxy = "4.6.2" def mavericks = "2.7.0" def glide = "4.13.2" @@ -86,7 +86,7 @@ ext.libs = [ 'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution", 'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution", // Phone number https://github.com/google/libphonenumber - 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.12.54" + 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.12.55" ], dagger : [ 'dagger' : "com.google.dagger:dagger:$dagger", diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index bcd737ac..433bc535 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -69,8 +69,6 @@ ext.groups = [ 'com.gabrielittner.threetenbp', 'com.getkeepsafe.relinker', 'com.github.bumptech.glide', - 'com.github.filippudak', - 'com.github.filippudak.progresspieview', 'com.github.javaparser', 'com.github.piasy', 'com.github.shyiko.klob', 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 a78953ca..43f42a3e 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 @@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.registration.RegistrationResult +import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel @@ -61,7 +62,7 @@ import java.util.concurrent.TimeUnit * This class exposes methods to be used in common cases * Registration, login, Sync, Sending messages... */ -class CommonTestHelper internal constructor(context: Context) { +class CommonTestHelper internal constructor(context: Context, val cryptoConfig: MXCryptoConfig? = null) { companion object { internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) { @@ -75,8 +76,10 @@ class CommonTestHelper internal constructor(context: Context) { } } - internal fun runCryptoTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CryptoTestHelper, CommonTestHelper) -> Unit) { - val testHelper = CommonTestHelper(context) + internal fun runCryptoTest(context: Context, autoSignoutOnClose: Boolean = true, + cryptoConfig: MXCryptoConfig? = null, + block: (CryptoTestHelper, CommonTestHelper) -> Unit) { + val testHelper = CommonTestHelper(context, cryptoConfig) val cryptoTestHelper = CryptoTestHelper(testHelper) return try { block(cryptoTestHelper, testHelper) @@ -103,7 +106,8 @@ class CommonTestHelper internal constructor(context: Context) { context, MatrixConfiguration( applicationFlavor = "TestFlavor", - roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider() + roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider(), + cryptoConfig = cryptoConfig ?: MXCryptoConfig() ) ) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index f36bfb62..210ce906 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -529,7 +529,8 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { payload = result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe ) } } catch (error: MXCryptoError) { 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 f8832954..410fb4f5 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 @@ -29,9 +29,9 @@ 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.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.RequestResult import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo @@ -45,7 +45,6 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction 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.content.WithHeldCode 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 @@ -134,7 +133,8 @@ class E2eeSanityTests : InstrumentedTest { val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!) timeLineEvent != null && timeLineEvent.isEncrypted() && - timeLineEvent.root.getClearType() == EventType.MESSAGE + timeLineEvent.root.getClearType() == EventType.MESSAGE && + timeLineEvent.root.mxDecryptionResult?.isSafe == true } } } @@ -331,6 +331,15 @@ class E2eeSanityTests : InstrumentedTest { // ensure bob can now decrypt cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) + + // Check key trust + sentEventIds.forEach { sentEventId -> + val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)!! + val result = testHelper.runBlockingTest { + newBobSession.cryptoService().decryptEvent(timelineEvent.root, "") + } + assertEquals("Keys from history should be deniable", false, result.isSafe) + } } /** @@ -379,10 +388,6 @@ class E2eeSanityTests : InstrumentedTest { Log.v("#E2E TEST", "check that new bob can't currently decrypt") cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) -// newBobSession.cryptoService().getOutgoingRoomKeyRequests() -// .firstOrNull { -// it.sessionId == -// } // Try to request sentEventIds.forEach { sentEventId -> @@ -390,33 +395,30 @@ class E2eeSanityTests : InstrumentedTest { newBobSession.cryptoService().requestRoomKeyForEvent(event) } - // wait a bit - // we need to wait a couple of syncs to let sharing occurs -// testHelper.waitFewSyncs(newBobSession, 6) - // Ensure that new bob still can't decrypt (keys must have been withheld) - sentEventIds.forEach { sentEventId -> - val megolmSessionId = newBobSession.getRoom(e2eRoomID)!! - .getTimelineEvent(sentEventId)!! - .root.content.toModel<EncryptedEventContent>()!!.sessionId - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests() - .first { - it.sessionId == megolmSessionId && - it.roomId == e2eRoomID - } - .results.also { - Log.w("##TEST", "result list is $it") - } - .firstOrNull { it.userId == aliceSession.myUserId } - ?.result - aliceReply != null && - aliceReply is RequestResult.Failure && - WithHeldCode.UNAUTHORISED == aliceReply.code - } - } - } + // as per new config we won't request to alice, so ignore following test +// sentEventIds.forEach { sentEventId -> +// val megolmSessionId = newBobSession.getRoom(e2eRoomID)!! +// .getTimelineEvent(sentEventId)!! +// .root.content.toModel<EncryptedEventContent>()!!.sessionId +// testHelper.waitWithLatch { latch -> +// testHelper.retryPeriodicallyWithLatch(latch) { +// val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests() +// .first { +// it.sessionId == megolmSessionId && +// it.roomId == e2eRoomID +// } +// .results.also { +// Log.w("##TEST", "result list is $it") +// } +// .firstOrNull { it.userId == aliceSession.myUserId } +// ?.result +// aliceReply != null && +// aliceReply is RequestResult.Failure && +// WithHeldCode.UNAUTHORISED == aliceReply.code +// } +// } +// } cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) @@ -438,7 +440,10 @@ class E2eeSanityTests : InstrumentedTest { * Test that if a better key is forwarded (lower index, it is then used) */ @Test - fun testForwardBetterKey() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + fun testForwardBetterKey() = runCryptoTest( + context(), + cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) + ) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = cryptoTestData.firstSession diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt index 32a95008..4b44aab1 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt @@ -77,6 +77,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { */ private fun testShareHistoryWithRoomVisibility(roomHistoryVisibility: RoomHistoryVisibility? = null) = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val aliceMessageText = "Hello Bob, I am Alice!" val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, roomHistoryVisibility) val e2eRoomID = cryptoTestData.roomId @@ -96,7 +97,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2) Log.v("#E2E TEST", "Alice and Bob are in roomId: $e2eRoomID") - val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, "Hello Bob, I am Alice!", testHelper) + val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, aliceMessageText, testHelper) Assert.assertTrue("Message should be sent", aliceMessageId != null) Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID") @@ -106,7 +107,8 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) (timelineEvent != null && timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE).also { + timelineEvent.root.getClearType() == EventType.MESSAGE && + timelineEvent.root.mxDecryptionResult?.isSafe == true).also { if (it) { Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") } @@ -142,7 +144,8 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) (timelineEvent != null && timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE + timelineEvent.root.getClearType() == EventType.MESSAGE && + timelineEvent.root.mxDecryptionResult?.isSafe == false ).also { if (it) { Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") @@ -377,7 +380,10 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { } private fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? { - return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.eventId + return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.let { + Log.v("#E2E TEST", "Message sent with session ${it.root.content?.get("session_id")}") + return it.eventId + } } private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String, testHelper: CommonTestHelper) { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt index 5fe73761..130c8d13 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt @@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.EventType @@ -82,7 +83,10 @@ class UnwedgingTest : InstrumentedTest { * -> This is automatically fixed after SDKs restarted the olm session */ @Test - fun testUnwedging() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + fun testUnwedging() = runCryptoTest( + context(), + cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) + ) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt index 7bb53e13..df0b10ea 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt @@ -22,15 +22,16 @@ import androidx.test.filters.LargeTest import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue import org.amshove.kluent.internal.assertEquals +import org.amshove.kluent.shouldBeEqualTo import org.junit.Assert import org.junit.Assert.assertNull import org.junit.FixMethodOrder -import org.junit.Ignore -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM +import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState import org.matrix.android.sdk.api.session.crypto.RequestResult import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel @@ -43,7 +44,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest -import org.matrix.android.sdk.common.RetryTestRule import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.mustFail @@ -51,16 +51,15 @@ import org.matrix.android.sdk.mustFail @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) @LargeTest -@Ignore class KeyShareTests : InstrumentedTest { - @get:Rule val rule = RetryTestRule(3) + // @get:Rule val rule = RetryTestRule(3) @Test fun test_DoNotSelfShareIfNotTrusted() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) - Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}") + Log.v("#TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}") // Create an encrypted room and add a message val roomId = commonTestHelper.runBlockingTest { @@ -86,7 +85,7 @@ class KeyShareTests : InstrumentedTest { aliceSession2.cryptoService().enableKeyGossiping(false) commonTestHelper.syncSession(aliceSession2) - Log.v("TEST", "=======> AliceSession 2 is ${aliceSession2.sessionParams.deviceId}") + Log.v("#TEST", "=======> AliceSession 2 is ${aliceSession2.sessionParams.deviceId}") val roomSecondSessionPOV = aliceSession2.getRoom(roomId) @@ -121,7 +120,7 @@ class KeyShareTests : InstrumentedTest { } } } - Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId") + Log.v("#TEST", "=======> Outgoing requet Id is $outGoingRequestId") val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequests() @@ -134,14 +133,17 @@ class KeyShareTests : InstrumentedTest { commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodicallyWithLatch(latch) { // DEBUG LOGS -// aliceSession.cryptoService().getIncomingRoomKeyRequests().let { -// Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)") -// Log.v("TEST", "=========================") -// it.forEach { keyRequest -> -// Log.v("TEST", "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") -// } -// Log.v("TEST", "=========================") -// } + aliceSession.cryptoService().getIncomingRoomKeyRequests().let { + Log.v("#TEST", "Incoming request Session 1 (looking for $outGoingRequestId)") + Log.v("#TEST", "=========================") + it.forEach { keyRequest -> + Log.v( + "#TEST", + "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}" + ) + } + Log.v("#TEST", "=========================") + } val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } incoming != null @@ -152,10 +154,10 @@ class KeyShareTests : InstrumentedTest { commonTestHelper.retryPeriodicallyWithLatch(latch) { // DEBUG LOGS aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest -> - Log.v("TEST", "=========================") - Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") - Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") - Log.v("TEST", "=========================") + Log.v("#TEST", "=========================") + Log.v("#TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") + Log.v("#TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") + Log.v("#TEST", "=========================") } val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } @@ -172,11 +174,24 @@ class KeyShareTests : InstrumentedTest { } // Mark the device as trusted + + Log.v("#TEST", "=======> Alice device 1 is ${aliceSession.sessionParams.deviceId}|${aliceSession.cryptoService().getMyDevice().identityKey()}") + val aliceSecondSession = aliceSession2.cryptoService().getMyDevice() + Log.v("#TEST", "=======> Alice device 2 is ${aliceSession2.sessionParams.deviceId}|${aliceSecondSession.identityKey()}") + aliceSession.cryptoService().setDeviceVerification( DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId, aliceSession2.sessionParams.deviceId ?: "" ) + // We only accept forwards from trusted session, so we need to trust on other side to + aliceSession2.cryptoService().setDeviceVerification( + DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId, + aliceSession.sessionParams.deviceId ?: "" + ) + + aliceSession.cryptoService().deviceWithIdentityKey(aliceSecondSession.identityKey()!!, MXCRYPTO_ALGORITHM_OLM)!!.isVerified shouldBeEqualTo true + // Re request aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root) @@ -193,7 +208,10 @@ class KeyShareTests : InstrumentedTest { * if the key was originally shared with him */ @Test - fun test_reShareIfWasIntendedToBeShared() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + fun test_reShareIfWasIntendedToBeShared() = runCryptoTest( + context(), + cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) + ) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = testData.firstSession @@ -224,7 +242,10 @@ class KeyShareTests : InstrumentedTest { * if the key was originally shared with him */ @Test - fun test_reShareToUnverifiedIfWasIntendedToBeShared() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + fun test_reShareToUnverifiedIfWasIntendedToBeShared() = runCryptoTest( + context(), + cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) + ) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true) val aliceSession = testData.firstSession @@ -242,7 +263,6 @@ class KeyShareTests : InstrumentedTest { } val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first() val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!! - // Let's try to request any how. // As it was share previously alice should accept to reshare aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root) @@ -261,7 +281,10 @@ class KeyShareTests : InstrumentedTest { * Tests that keys reshared with own verified session are done from the earliest known index */ @Test - fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() = runCryptoTest( + context(), + cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) + ) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = testData.firstSession @@ -333,6 +356,9 @@ class KeyShareTests : InstrumentedTest { aliceSession.cryptoService() .verificationService() .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!) + aliceNewSession.cryptoService() + .verificationService() + .markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!) // Let's now try to request aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root) @@ -381,7 +407,10 @@ class KeyShareTests : InstrumentedTest { * Tests that we don't cancel a request to early on first forward if the index is not good enough */ @Test - fun test_dontCancelToEarly() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + fun test_dontCancelToEarly() = runCryptoTest( + context(), + cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) + ) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = testData.firstSession val bobSession = testData.secondSession!! @@ -421,6 +450,9 @@ class KeyShareTests : InstrumentedTest { aliceSession.cryptoService() .verificationService() .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!) + aliceNewSession.cryptoService() + .verificationService() + .markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!) // /!\ Stop initial alice session syncing so that it can't reply aliceSession.cryptoService().enableKeyGossiping(false) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt index 0aac4297..910a349b 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt @@ -27,6 +27,7 @@ import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.NoOpMatrixCallback +import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.RequestResult @@ -153,7 +154,10 @@ class WithHeldTests : InstrumentedTest { } @Test - fun test_WithHeldNoOlm() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + fun test_WithHeldNoOlm() = runCryptoTest( + context(), + cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) + ) { cryptoTestHelper, testHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = testData.firstSession @@ -233,7 +237,10 @@ class WithHeldTests : InstrumentedTest { } @Test - fun test_WithHeldKeyRequest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + fun test_WithHeldKeyRequest() = runCryptoTest( + context(), + cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) + ) { cryptoTestHelper, testHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = testData.firstSession diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt index 015cb6a1..38f52258 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt @@ -35,8 +35,9 @@ data class MXCryptoConfig constructor( /** * Currently megolm keys are requested to the sender device and to all of our devices. - * You can limit request only to your sessions by turning this setting to `true` + * You can limit request only to your sessions by turning this setting to `true`. + * Forwarded keys coming from other users will also be ignored if set to true. */ - val limitRoomKeyRequestsToMyDevices: Boolean = false, + val limitRoomKeyRequestsToMyDevices: Boolean = true, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt index d3f6ec22..1d6e79c8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt @@ -68,6 +68,11 @@ sealed interface QueryStringValue { */ data class Contains(override val string: String, override val case: Case = Case.SENSITIVE) : ContentQueryStringValue + /** + * The tested field must not contain the [string]. + */ + data class NotContains(override val string: String, override val case: Case = Case.SENSITIVE) : ContentQueryStringValue + /** * Case enum for [ContentQueryStringValue]. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionExtensions.kt index a15e73eb..96dac276 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionExtensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionExtensions.kt @@ -32,5 +32,13 @@ fun Session.getRoomSummary(roomIdOrAlias: String): RoomSummary? = roomService(). /** * Get a user using the UserService of a Session. + * @param userId the userId to look for. + * @return a user with userId or null if the User is not known yet by the SDK. + * See [org.matrix.android.sdk.api.session.user.UserService.resolveUser] to ensure that a User is retrieved. */ fun Session.getUser(userId: String): User? = userService().getUser(userId) + +/** + * Similar to [getUser], but fallback to a User without details if the User is not known by the SDK, or if Session is null. + */ +fun Session?.getUserOrDefault(userId: String): User = this?.userService()?.getUser(userId) ?: User(userId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt index 0a0ccc2d..66d7558f 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt @@ -43,5 +43,7 @@ data class MXEventDecryptionResult( * List of curve25519 keys involved in telling us about the senderCurve25519Key and * claimedEd25519Key. See MXEvent.forwardingCurve25519KeyChain. */ - val forwardingCurve25519KeyChain: List<String> = emptyList() + val forwardingCurve25519KeyChain: List<String> = emptyList(), + + val isSafe: Boolean = false ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt index a26f6606..6d57318f 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt @@ -44,5 +44,10 @@ data class OlmDecryptionResult( /** * Devices which forwarded this session to us (normally empty). */ - @Json(name = "forwardingCurve25519KeyChain") val forwardingCurve25519KeyChain: List<String>? = null + @Json(name = "forwardingCurve25519KeyChain") val forwardingCurve25519KeyChain: List<String>? = null, + + /** + * True if the key used to decrypt is considered safe (trusted). + */ + @Json(name = "key_safety") val isSafe: Boolean? = null, ) 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 59dc6c43..f5d2c0d9 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 @@ -174,15 +174,29 @@ data class Event( * @return the event type */ fun getClearType(): String { - return mxDecryptionResult?.payload?.get("type")?.toString() ?: type ?: EventType.MISSING_TYPE + return getDecryptedType() ?: type ?: EventType.MISSING_TYPE + } + + /** + * @return The decrypted type, or null. Won't fallback to the wired type + */ + fun getDecryptedType(): String? { + return mxDecryptionResult?.payload?.get("type")?.toString() } /** * @return the event content */ fun getClearContent(): Content? { + return getDecryptedContent() ?: content + } + + /** + * @return the decrypted event content or null, Won't fallback to the wired content + */ + fun getDecryptedContent(): Content? { @Suppress("UNCHECKED_CAST") - return mxDecryptionResult?.payload?.get("content") as? Content ?: content + return mxDecryptionResult?.payload?.get("content") as? Content } fun toContentStringWithIndent(): String { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index 5d2769ac..8031fcae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.members.MembershipService +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.relation.RelationService import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService @@ -60,11 +61,22 @@ interface Room { */ fun getRoomSummaryLive(): LiveData<Optional<RoomSummary>> + /** + * A live [LocalRoomSummary] associated with the room. + * You can observe this summary to get dynamic data from this room. + */ + fun getLocalRoomSummaryLive(): LiveData<Optional<LocalRoomSummary>> + /** * A current snapshot of [RoomSummary] associated with the room. */ fun roomSummary(): RoomSummary? + /** + * A current snapshot of [LocalRoomSummary] associated with the room. + */ + fun localRoomSummary(): LocalRoomSummary? + /** * Use this room as a Space, if the type is correct. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt index ad8106c9..65383f10 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult import org.matrix.android.sdk.api.session.room.alias.RoomAliasDescription import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -117,6 +118,12 @@ interface RoomService { */ fun getRoomSummaryLive(roomId: String): LiveData<Optional<RoomSummary>> + /** + * A live [LocalRoomSummary] associated with the room with id [roomId]. + * You can observe this summary to get dynamic data from this room, even if the room is not joined yet + */ + fun getLocalRoomSummaryLive(roomId: String): LiveData<Optional<LocalRoomSummary>> + /** * Get a snapshot list of room summaries. * @return the immutable list of [RoomSummary] diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt index 60963ef2..d651f06e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt @@ -20,8 +20,10 @@ import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.query.RoomTagQueryFilter import org.matrix.android.sdk.api.query.SpaceFilter +import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams.Builder import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomType +import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams /** @@ -52,6 +54,10 @@ fun spaceSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = * [roomSummaryQueryParams] and [spaceSummaryQueryParams] can also be used to build an instance of this class. */ data class RoomSummaryQueryParams( + /** + * Query for the roomId. + */ + val roomId: QueryStringValue, /** * Query for the displayName of the room. The display name can be the value of the state event, * or a value returned by [org.matrix.android.sdk.api.RoomDisplayNameFallbackProvider]. @@ -94,6 +100,7 @@ data class RoomSummaryQueryParams( * [roomSummaryQueryParams] and [spaceSummaryQueryParams] can also be used to build an instance of [RoomSummaryQueryParams]. */ class Builder { + var roomId: QueryStringValue = QueryStringValue.NotContains(RoomLocalEcho.PREFIX) var displayName: QueryStringValue = QueryStringValue.NoCondition var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition var memberships: List<Membership> = Membership.all() @@ -104,6 +111,7 @@ data class RoomSummaryQueryParams( var spaceFilter: SpaceFilter = SpaceFilter.NoFilter fun build() = RoomSummaryQueryParams( + roomId = roomId, displayName = displayName, canonicalAlias = canonicalAlias, memberships = memberships, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomCreationState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomCreationState.kt new file mode 100644 index 00000000..4fc99225 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomCreationState.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 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.api.session.room.model + +enum class LocalRoomCreationState { + NOT_CREATED, + CREATING, + FAILURE, + CREATED +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomSummary.kt new file mode 100644 index 00000000..eced1dd5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomSummary.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2022 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.api.session.room.model + +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams + +/** + * This class holds some data of a local room. + * It can be retrieved by [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService] + */ +data class LocalRoomSummary( + /** + * The roomId of the room. + */ + val roomId: String, + /** + * The room summary of the room. + */ + val roomSummary: RoomSummary?, + /** + * The creation params attached to the room. + */ + val createRoomParams: CreateRoomParams?, + /** + * The roomId of the created room (ie. created on the server), if any. + */ + val replacementRoomId: String?, + /** + * The creation state of the room. + */ + val creationState: LocalRoomCreationState, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt index 7d3109fb..2388bee0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt @@ -34,5 +34,4 @@ data class SpaceChildInfo( val canonicalAlias: String?, val aliases: List<String>?, val worldReadable: Boolean - ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt index 7ef0d639..ec0e642a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt @@ -20,7 +20,7 @@ import java.util.UUID object RoomLocalEcho { - private const val PREFIX = "!local." + const val PREFIX = "!local." /** * Tell whether the provider room id is a local id. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceHierarchyData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceHierarchyData.kt index ecc3eb52..d03f4c42 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceHierarchyData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceHierarchyData.kt @@ -16,13 +16,13 @@ package org.matrix.android.sdk.api.session.space -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo +import org.matrix.android.sdk.api.session.space.model.SpaceChildSummaryEvent data class SpaceHierarchyData( val rootSummary: RoomSummary, val children: List<SpaceChildInfo>, - val childrenState: List<Event>, + val childrenState: List<SpaceChildSummaryEvent>, val nextToken: String? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt index c7a64050..5d2a9412 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt @@ -18,10 +18,10 @@ package org.matrix.android.sdk.api.session.space import android.net.Uri import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.space.model.SpaceChildSummaryEvent import org.matrix.android.sdk.api.session.space.peeking.SpacePeekResult typealias SpaceSummaryQueryParams = RoomSummaryQueryParams @@ -75,12 +75,12 @@ interface SpaceService { suggestedOnly: Boolean? = null, limit: Int? = null, from: String? = null, - knownStateList: List<Event>? = null + knownStateList: List<SpaceChildSummaryEvent>? = null ): SpaceHierarchyData /** * Get a live list of space summaries. This list is refreshed as soon as the data changes. - * @return the [LiveData] of List[SpaceSummary] + * @return the [LiveData] of List[RoomSummary] */ fun getSpaceSummariesLive( queryParams: SpaceSummaryQueryParams, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildSummaryEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildSummaryEvent.kt new file mode 100644 index 00000000..13aa0336 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildSummaryEvent.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 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.api.session.space.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content + +@JsonClass(generateAdapter = true) +data class SpaceChildSummaryEvent( + @Json(name = "type") val type: String? = null, + @Json(name = "state_key") val stateKey: String? = null, + @Json(name = "content") val content: Content? = null, + @Json(name = "sender") val senderId: String? = null, + @Json(name = "origin_server_ts") val originServerTs: Long? = null, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt index 0c5465e1..70750237 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt @@ -29,7 +29,7 @@ interface UserService { /** * Get a user from a userId. * @param userId the userId to look for. - * @return a user with userId or null + * @return a user with userId or null if the User is not known yet by the SDK. See [resolveUser] to ensure that a User is retrieved. */ fun getUser(userId: String): User? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 8dd7c309..322f297a 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -79,6 +79,7 @@ import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationActio import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory +import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService @@ -183,7 +184,8 @@ internal class DefaultCryptoService @Inject constructor( private val cryptoCoroutineScope: CoroutineScope, private val eventDecryptor: EventDecryptor, private val verificationMessageProcessor: VerificationMessageProcessor, - private val liveEventManager: Lazy<StreamEventsManager> + private val liveEventManager: Lazy<StreamEventsManager>, + private val unrequestedForwardManager: UnRequestedForwardManager, ) : CryptoService { private val isStarting = AtomicBoolean(false) @@ -399,6 +401,7 @@ internal class DefaultCryptoService @Inject constructor( cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) incomingKeyRequestManager.close() outgoingKeyRequestManager.close() + unrequestedForwardManager.close() olmDevice.release() cryptoStore.close() } @@ -485,6 +488,14 @@ internal class DefaultCryptoService @Inject constructor( // just for safety but should not throw Timber.tag(loggerTag.value).w("failed to process incoming room key requests") } + + unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(clock.epochMillis()) { events -> + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + events.forEach { + onRoomKeyEvent(it, true) + } + } + } } } } @@ -845,9 +856,9 @@ internal class DefaultCryptoService @Inject constructor( * * @param event the key event. */ - private fun onRoomKeyEvent(event: Event) { - val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return - Timber.tag(loggerTag.value).i("onRoomKeyEvent() from: ${event.senderId} type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>") + private fun onRoomKeyEvent(event: Event, acceptUnrequested: Boolean = false) { + val roomKeyContent = event.getDecryptedContent().toModel<RoomKeyContent>() ?: return + Timber.tag(loggerTag.value).i("onRoomKeyEvent(forceAccept:$acceptUnrequested) from: ${event.senderId} type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>") if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { Timber.tag(loggerTag.value).e("onRoomKeyEvent() : missing fields") return @@ -857,7 +868,7 @@ internal class DefaultCryptoService @Inject constructor( Timber.tag(loggerTag.value).e("GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") return } - alg.onRoomKeyEvent(event, keysBackupService) + alg.onRoomKeyEvent(event, keysBackupService, acceptUnrequested) } private fun onKeyWithHeldReceived(event: Event) { @@ -950,6 +961,15 @@ internal class DefaultCryptoService @Inject constructor( * @param event the membership event causing the change */ private fun onRoomMembershipEvent(roomId: String, event: Event) { + // because the encryption event can be after the join/invite in the same batch + event.stateKey?.let { _ -> + val roomMember: RoomMemberContent? = event.content.toModel() + val membership = roomMember?.membership + if (membership == Membership.INVITE) { + unrequestedForwardManager.onInviteReceived(roomId, event.senderId.orEmpty(), clock.epochMillis()) + } + } + roomEncryptorsStore.get(roomId) ?: /* No encrypting in this room */ return event.stateKey?.let { userId -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt index 39dfb721..6d197a09 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt @@ -91,6 +91,21 @@ internal class InboundGroupSessionStore @Inject constructor( internalStoreGroupSession(new, sessionId, senderKey) } + @Synchronized + fun updateToSafe(old: InboundGroupSessionHolder, sessionId: String, senderKey: String) { + Timber.tag(loggerTag.value).v("## updateToSafe for session ${old.wrapper.roomId}-${old.wrapper.senderKey}") + + store.storeInboundGroupSessions( + listOf( + old.wrapper.copy( + sessionData = old.wrapper.sessionData.copy(trusted = true) + ) + ) + ) + // will release it :/ + sessionCache.remove(CacheKey(sessionId, senderKey)) + } + @Synchronized fun storeInBoundGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) { internalStoreGroupSession(holder, sessionId, senderKey) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index 96ccba51..48b46523 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto import androidx.annotation.VisibleForTesting import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError @@ -612,7 +613,8 @@ internal class MXOlmDevice @Inject constructor( forwardingCurve25519KeyChain: List<String>, keysClaimed: Map<String, String>, exportFormat: Boolean, - sharedHistory: Boolean + sharedHistory: Boolean, + trusted: Boolean ): AddSessionResult { val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") { if (exportFormat) { @@ -620,6 +622,8 @@ internal class MXOlmDevice @Inject constructor( } else { OlmInboundGroupSession(sessionKey) } + } ?: return AddSessionResult.NotImported.also { + Timber.tag(loggerTag.value).d("## addInboundGroupSession() : failed to import key candidate $senderKey/$sessionId") } val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } @@ -631,31 +635,49 @@ internal class MXOlmDevice @Inject constructor( val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex } ?: return AddSessionResult.NotImported.also { // This is quite unexpected, could throw if native was released? Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session") - candidateSession?.releaseSession() + candidateSession.releaseSession() // Probably should discard it? } - val newKnownFirstIndex = tryOrNull("Failed to get candidate first known index") { candidateSession?.firstKnownIndex } - // If our existing session is better we keep it - if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { - Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId") - candidateSession?.releaseSession() - return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt()) + val newKnownFirstIndex = tryOrNull("Failed to get candidate first known index") { candidateSession.firstKnownIndex } + ?: return AddSessionResult.NotImported.also { + candidateSession.releaseSession() + Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Failed to get new session index") + } + + val keyConnects = existingSession.session.connects(candidateSession) + if (!keyConnects) { + Timber.tag(loggerTag.value) + .e("## addInboundGroupSession() Unconnected key") + if (!trusted) { + // Ignore the not connecting unsafe, keep existing + Timber.tag(loggerTag.value) + .e("## addInboundGroupSession() Received unsafe unconnected key") + return AddSessionResult.NotImported + } + // else if the new one is safe and does not connect with existing, import the new one + } else { + // If our existing session is better we keep it + if (existingFirstKnown <= newKnownFirstIndex) { + val shouldUpdateTrust = trusted && (existingSession.sessionData.trusted != true) + Timber.tag(loggerTag.value).d("## addInboundGroupSession() : updateTrust for $sessionId") + if (shouldUpdateTrust) { + // the existing as a better index but the new one is trusted so update trust + inboundGroupSessionStore.updateToSafe(existingSessionHolder, sessionId, senderKey) + } + Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId") + candidateSession.releaseSession() + return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt()) + } } } catch (failure: Throwable) { Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}") - candidateSession?.releaseSession() + candidateSession.releaseSession() return AddSessionResult.NotImported } } Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId") - // sanity check on the new session - if (null == candidateSession) { - Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>") - return AddSessionResult.NotImported - } - try { if (candidateSession.sessionIdentifier() != sessionId) { Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") @@ -674,6 +696,7 @@ internal class MXOlmDevice @Inject constructor( keysClaimed = keysClaimed, forwardingCurve25519KeyChain = forwardingCurve25519KeyChain, sharedHistory = sharedHistory, + trusted = trusted ) val wrapper = MXInboundMegolmSessionWrapper( @@ -689,6 +712,16 @@ internal class MXOlmDevice @Inject constructor( return AddSessionResult.Imported(candidateSession.firstKnownIndex.toInt()) } + fun OlmInboundGroupSession.connects(other: OlmInboundGroupSession): Boolean { + return try { + val lowestCommonIndex = this.firstKnownIndex.coerceAtLeast(other.firstKnownIndex) + this.export(lowestCommonIndex) == other.export(lowestCommonIndex) + } catch (failure: Throwable) { + // native error? key disposed? + false + } + } + /** * Import an inbound group sessions to the session store. * @@ -821,7 +854,8 @@ internal class MXOlmDevice @Inject constructor( payload, wrapper.sessionData.keysClaimed, senderKey, - wrapper.sessionData.forwardingCurve25519KeyChain + wrapper.sessionData.forwardingCurve25519KeyChain, + isSafe = sessionHolder.wrapper.sessionData.trusted.orFalse() ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt index a79e1a89..5691f24d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt @@ -267,13 +267,24 @@ internal class SecretShareManager @Inject constructor( Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event") return } + // no need to download keys, after a verification we already forced download + val sendingDevice = toDevice.getSenderKey()?.let { cryptoStore.deviceWithIdentityKey(it) } + if (sendingDevice == null) { + Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from unknown device ${toDevice.getSenderKey()}") + return + } // Was that sent by us? - if (toDevice.senderId != credentials.userId) { + if (sendingDevice.userId != credentials.userId) { Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}") return } + if (!sendingDevice.isVerified) { + Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from untrusted device ${toDevice.getSenderKey()}") + return + } + val secretContent = toDevice.getClearContent().toModel<SecretSendEventContent>() ?: return val existingRequest = verifMutex.withLock { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt index 6847a463..e2ddd5d1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt @@ -42,5 +42,5 @@ internal interface IMXDecrypting { * @param event the key event. * @param defaultKeysBackupService the keys backup service */ - fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {} + fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean = false) {} } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 410b74e1..5354cbff 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -17,7 +17,8 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import dagger.Lazy -import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.NewSessionListener @@ -34,16 +35,20 @@ import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.session.StreamEventsManager +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO) internal class MXMegolmDecryption( private val olmDevice: MXOlmDevice, + private val myUserId: String, private val outgoingKeyRequestManager: OutgoingKeyRequestManager, private val cryptoStore: IMXCryptoStore, - private val matrixConfiguration: MatrixConfiguration, - private val liveEventManager: Lazy<StreamEventsManager> + private val liveEventManager: Lazy<StreamEventsManager>, + private val unrequestedForwardManager: UnRequestedForwardManager, + private val cryptoConfig: MXCryptoConfig, + private val clock: Clock, ) : IMXDecrypting { var newSessionListener: NewSessionListener? = null @@ -94,7 +99,8 @@ internal class MXMegolmDecryption( senderCurve25519Key = olmDecryptionResult.senderKey, claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"), forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain - .orEmpty() + .orEmpty(), + isSafe = olmDecryptionResult.isSafe.orFalse() ).also { liveEventManager.get().dispatchLiveEventDecrypted(event, it) } @@ -182,12 +188,21 @@ internal class MXMegolmDecryption( * @param event the key event. * @param defaultKeysBackupService the keys backup service */ - override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) { - Timber.tag(loggerTag.value).v("onRoomKeyEvent()") + override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean) { + Timber.tag(loggerTag.value).v("onRoomKeyEvent(${event.getSenderKey()})") var exportFormat = false - val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return + val roomKeyContent = event.getDecryptedContent()?.toModel<RoomKeyContent>() ?: return + + val eventSenderKey: String = event.getSenderKey() ?: return Unit.also { + Timber.tag(loggerTag.value).e("onRoom Key/Forward Event() : event is missing sender_key field") + } + + // this device might not been downloaded now? + val fromDevice = cryptoStore.deviceWithIdentityKey(eventSenderKey) + + lateinit var sessionInitiatorSenderKey: String + val trusted: Boolean - var senderKey: String? = event.getSenderKey() var keysClaimed: MutableMap<String, String> = HashMap() val forwardingCurve25519KeyChain: MutableList<String> = ArrayList() @@ -195,32 +210,25 @@ internal class MXMegolmDecryption( Timber.tag(loggerTag.value).e("onRoomKeyEvent() : Key event is missing fields") return } - if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { + if (event.getDecryptedType() == EventType.FORWARDED_ROOM_KEY) { if (!cryptoStore.isKeyGossipingEnabled()) { Timber.tag(loggerTag.value) .i("onRoomKeyEvent(), ignore forward adding as per crypto config : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") return } Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") - val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>() + val forwardedRoomKeyContent = event.getDecryptedContent()?.toModel<ForwardedRoomKeyContent>() ?: return forwardedRoomKeyContent.forwardingCurve25519KeyChain?.let { forwardingCurve25519KeyChain.addAll(it) } - if (senderKey == null) { - Timber.tag(loggerTag.value).e("onRoomKeyEvent() : event is missing sender_key field") - return - } - - forwardingCurve25519KeyChain.add(senderKey) + forwardingCurve25519KeyChain.add(eventSenderKey) exportFormat = true - senderKey = forwardedRoomKeyContent.senderKey - if (null == senderKey) { + sessionInitiatorSenderKey = forwardedRoomKeyContent.senderKey ?: return Unit.also { Timber.tag(loggerTag.value).e("onRoomKeyEvent() : forwarded_room_key event is missing sender_key field") - return } if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) { @@ -229,13 +237,51 @@ internal class MXMegolmDecryption( } keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key - } else { - Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") - if (null == senderKey) { - Timber.tag(loggerTag.value).e("## onRoomKeyEvent() : key event has no sender key (not encrypted?)") + + // checking if was requested once. + // should we check if the request is sort of active? + val wasNotRequested = cryptoStore.getOutgoingRoomKeyRequest( + roomId = forwardedRoomKeyContent.roomId.orEmpty(), + sessionId = forwardedRoomKeyContent.sessionId.orEmpty(), + algorithm = forwardedRoomKeyContent.algorithm.orEmpty(), + senderKey = forwardedRoomKeyContent.senderKey.orEmpty(), + ).isEmpty() + + trusted = false + + if (!forceAccept && wasNotRequested) { +// val senderId = cryptoStore.deviceWithIdentityKey(event.getSenderKey().orEmpty())?.userId.orEmpty() + unrequestedForwardManager.onUnRequestedKeyForward(roomKeyContent.roomId, event, clock.epochMillis()) + // Ignore unsolicited + Timber.tag(loggerTag.value).w("Ignoring forwarded_room_key_event for ${roomKeyContent.sessionId} that was not requested") + return + } + + // Check who sent the request, as we requested we have the device keys (no need to download) + val sessionThatIsSharing = cryptoStore.deviceWithIdentityKey(eventSenderKey) + if (sessionThatIsSharing == null) { + Timber.tag(loggerTag.value).w("Ignoring forwarded_room_key from unknown device with identity $eventSenderKey") return } + val isOwnDevice = myUserId == sessionThatIsSharing.userId + val isDeviceVerified = sessionThatIsSharing.isVerified + val isFromSessionInitiator = sessionThatIsSharing.identityKey() == sessionInitiatorSenderKey + + val isLegitForward = (isOwnDevice && isDeviceVerified) || + (!cryptoConfig.limitRoomKeyRequestsToMyDevices && isFromSessionInitiator) + val shouldAcceptForward = forceAccept || isLegitForward + + if (!shouldAcceptForward) { + Timber.tag(loggerTag.value) + .w("Ignoring forwarded_room_key device:$eventSenderKey, ownVerified:{$isOwnDevice&&$isDeviceVerified}, fromInitiator:$isFromSessionInitiator") + return + } + } else { + // It's a m.room_key so safe + trusted = true + sessionInitiatorSenderKey = eventSenderKey + Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") // inherit the claimed ed25519 key from the setup message keysClaimed = event.getKeysClaimed().toMutableMap() } @@ -245,12 +291,15 @@ internal class MXMegolmDecryption( sessionId = roomKeyContent.sessionId, sessionKey = roomKeyContent.sessionKey, roomId = roomKeyContent.roomId, - senderKey = senderKey, + senderKey = sessionInitiatorSenderKey, forwardingCurve25519KeyChain = forwardingCurve25519KeyChain, keysClaimed = keysClaimed, exportFormat = exportFormat, - sharedHistory = roomKeyContent.getSharedKey() - ) + sharedHistory = roomKeyContent.getSharedKey(), + trusted = trusted + ).also { + Timber.tag(loggerTag.value).v(".. onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId} result: $it") + } when (addSessionResult) { is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex @@ -258,35 +307,28 @@ internal class MXMegolmDecryption( else -> null }?.let { index -> if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { - val fromDevice = (event.content?.get("sender_key") as? String)?.let { senderDeviceIdentityKey -> - cryptoStore.getUserDeviceList(event.senderId ?: "") - ?.firstOrNull { - it.identityKey() == senderDeviceIdentityKey - } - }?.deviceId - outgoingKeyRequestManager.onRoomKeyForwarded( sessionId = roomKeyContent.sessionId, algorithm = roomKeyContent.algorithm ?: "", roomId = roomKeyContent.roomId, - senderKey = senderKey, + senderKey = sessionInitiatorSenderKey, fromIndex = index, - fromDevice = fromDevice, + fromDevice = fromDevice?.deviceId, event = event ) cryptoStore.saveIncomingForwardKeyAuditTrail( roomId = roomKeyContent.roomId, sessionId = roomKeyContent.sessionId, - senderKey = senderKey, + senderKey = sessionInitiatorSenderKey, algorithm = roomKeyContent.algorithm ?: "", - userId = event.senderId ?: "", - deviceId = fromDevice ?: "", + userId = event.senderId.orEmpty(), + deviceId = fromDevice?.deviceId.orEmpty(), chainIndex = index.toLong() ) // The index is used to decide if we cancel sent request or if we wait for a better key - outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, senderKey, index) + outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, sessionInitiatorSenderKey, index) } } @@ -295,7 +337,7 @@ internal class MXMegolmDecryption( .d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}") defaultKeysBackupService.maybeBackupKeys() - onNewSession(roomKeyContent.roomId, senderKey, roomKeyContent.sessionId) + onNewSession(roomKeyContent.roomId, sessionInitiatorSenderKey, roomKeyContent.sessionId) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt index 38edbb74..99f8bc69 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt @@ -17,28 +17,36 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import dagger.Lazy -import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.StreamEventsManager +import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject internal class MXMegolmDecryptionFactory @Inject constructor( private val olmDevice: MXOlmDevice, + @UserId private val myUserId: String, private val outgoingKeyRequestManager: OutgoingKeyRequestManager, private val cryptoStore: IMXCryptoStore, - private val matrixConfiguration: MatrixConfiguration, - private val eventsManager: Lazy<StreamEventsManager> + private val eventsManager: Lazy<StreamEventsManager>, + private val unrequestedForwardManager: UnRequestedForwardManager, + private val mxCryptoConfig: MXCryptoConfig, + private val clock: Clock, ) { fun create(): MXMegolmDecryption { return MXMegolmDecryption( - olmDevice, - outgoingKeyRequestManager, - cryptoStore, - matrixConfiguration, - eventsManager + olmDevice = olmDevice, + myUserId = myUserId, + outgoingKeyRequestManager = outgoingKeyRequestManager, + cryptoStore = cryptoStore, + liveEventManager = eventsManager, + unrequestedForwardManager = unrequestedForwardManager, + cryptoConfig = mxCryptoConfig, + clock = clock, ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 771b5f9a..fca6fab6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -162,7 +162,8 @@ internal class MXMegolmEncryption( forwardingCurve25519KeyChain = emptyList(), keysClaimed = keysClaimedMap, exportFormat = false, - sharedHistory = sharedHistory + sharedHistory = sharedHistory, + trusted = true ) defaultKeysBackupService.maybeBackupKeys() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt new file mode 100644 index 00000000..42629b61 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2022 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.algorithms.megolm + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer +import timber.log.Timber +import java.util.concurrent.Executors +import javax.inject.Inject +import kotlin.math.abs + +private val INVITE_VALIDITY_TIME_WINDOW_MILLIS = 10 * 60_000 + +@SessionScope +internal class UnRequestedForwardManager @Inject constructor( + private val deviceListManager: DeviceListManager, +) { + + private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + private val scope = CoroutineScope(SupervisorJob() + dispatcher) + private val sequencer = SemaphoreCoroutineSequencer() + + // For now only in memory storage. Maybe we should persist? in case of gappy sync and long catchups? + private val forwardedKeysPerRoom = mutableMapOf<String, MutableMap<String, MutableList<ForwardInfo>>>() + + data class InviteInfo( + val roomId: String, + val fromMxId: String, + val timestamp: Long + ) + + data class ForwardInfo( + val event: Event, + val timestamp: Long + ) + + // roomId, local timestamp of invite + private val recentInvites = mutableListOf<InviteInfo>() + + fun close() { + try { + scope.cancel("User Terminate") + } catch (failure: Throwable) { + Timber.w(failure, "Failed to shutDown UnrequestedForwardManager") + } + } + + fun onInviteReceived(roomId: String, fromUserId: String, localTimeStamp: Long) { + Timber.w("Invite received in room:$roomId from:$fromUserId at $localTimeStamp") + scope.launch { + sequencer.post { + if (!recentInvites.any { it.roomId == roomId && it.fromMxId == fromUserId }) { + recentInvites.add( + InviteInfo( + roomId, + fromUserId, + localTimeStamp + ) + ) + } + } + } + } + + fun onUnRequestedKeyForward(roomId: String, event: Event, localTimeStamp: Long) { + Timber.w("Received unrequested forward in room:$roomId from:${event.senderId} at $localTimeStamp") + scope.launch { + sequencer.post { + val claimSenderId = event.senderId.orEmpty() + val senderKey = event.getSenderKey() + // we might want to download keys, as this user might not be known yet, cache is ok + val ownerMxId = + tryOrNull { + deviceListManager.downloadKeys(listOf(claimSenderId), false) + .map[claimSenderId] + ?.values + ?.firstOrNull { it.identityKey() == senderKey } + ?.userId + } + // Not sure what to do if the device has been deleted? I can't proove the mxid + if (ownerMxId == null || claimSenderId != ownerMxId) { + Timber.w("Mismatch senderId between event and olm owner") + return@post + } + + forwardedKeysPerRoom + .getOrPut(roomId) { mutableMapOf() } + .getOrPut(ownerMxId) { mutableListOf() } + .add(ForwardInfo(event, localTimeStamp)) + } + } + } + + fun postSyncProcessParkedKeysIfNeeded(currentTimestamp: Long, handleForwards: suspend (List<Event>) -> Unit) { + scope.launch { + sequencer.post { + // Prune outdated invites + recentInvites.removeAll { currentTimestamp - it.timestamp > INVITE_VALIDITY_TIME_WINDOW_MILLIS } + val cleanUpEvents = mutableListOf<Pair<String, String>>() + forwardedKeysPerRoom.forEach { (roomId, senderIdToForwardMap) -> + senderIdToForwardMap.forEach { (senderId, eventList) -> + // is there a matching invite in a valid timewindow? + val matchingInvite = recentInvites.firstOrNull { it.fromMxId == senderId && it.roomId == roomId } + if (matchingInvite != null) { + Timber.v("match for room:$roomId from sender:$senderId -> count =${eventList.size}") + + eventList.filter { + abs(matchingInvite.timestamp - it.timestamp) <= INVITE_VALIDITY_TIME_WINDOW_MILLIS + }.map { + it.event + }.takeIf { it.isNotEmpty() }?.let { + Timber.w("Re-processing forwarded_room_key_event that was not requested after invite") + scope.launch { + handleForwards.invoke(it) + } + } + cleanUpEvents.add(roomId to senderId) + } + } + } + + cleanUpEvents.forEach { roomIdToSenderPair -> + forwardedKeysPerRoom[roomIdToSenderPair.first]?.get(roomIdToSenderPair.second)?.clear() + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt index 8691c087..e8700b78 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -652,14 +652,7 @@ internal class DefaultKeysBackupService @Inject constructor( } val recoveryKey = computeRecoveryKey(secret.fromBase64()) if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) { - awaitCallback<Unit> { - trustKeysBackupVersion(keysBackupVersion, true, it) - } // we don't want to start immediately downloading all as it can take very long - -// val importResult = awaitCallback<ImportRoomKeysResult> { -// restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it) -// } withContext(coroutineDispatchers.crypto) { cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt index 2ce36aa2..15e8ba83 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt @@ -38,9 +38,6 @@ data class InboundGroupSessionData( @Json(name = "forwarding_curve25519_key_chain") var forwardingCurve25519KeyChain: List<String>? = emptyList(), - /** Not yet used, will be in backup v2 - val untrusted?: Boolean = false */ - /** * Flag that indicates whether or not the current inboundSession will be shared to * invited users to decrypt past messages. @@ -48,4 +45,10 @@ data class InboundGroupSessionData( @Json(name = "shared_history") val sharedHistory: Boolean = false, + /** + * Flag indicating that this key is trusted. + */ + @Json(name = "trusted") + val trusted: Boolean? = null, + ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt index 2772b348..2c6a0a96 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt @@ -86,6 +86,7 @@ data class MXInboundMegolmSessionWrapper( keysClaimed = megolmSessionData.senderClaimedKeys, forwardingCurve25519KeyChain = megolmSessionData.forwardingCurve25519KeyChain, sharedHistory = megolmSessionData.sharedHistory, + trusted = false ) return MXInboundMegolmSessionWrapper( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt index c36d572d..426d50a5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017 +import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo018 import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject @@ -48,7 +49,7 @@ internal class RealmCryptoStoreMigration @Inject constructor( private val clock: Clock, ) : MatrixRealmMigration( dbName = "Crypto", - schemaVersion = 17L, + schemaVersion = 18L, ) { /** * Forces all RealmCryptoStoreMigration instances to be equal. @@ -75,5 +76,6 @@ internal class RealmCryptoStoreMigration @Inject constructor( if (oldVersion < 15) MigrateCryptoTo015(realm).perform() if (oldVersion < 16) MigrateCryptoTo016(realm).perform() if (oldVersion < 17) MigrateCryptoTo017(realm).perform() + if (oldVersion < 18) MigrateCryptoTo018(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo018.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo018.kt new file mode 100644 index 00000000..3bedf58c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo018.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2022 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.store.db.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.util.database.RealmMigrator +import timber.log.Timber + +/** + * This migration is adding support for trusted flags on megolm sessions. + * We can't really assert the trust of existing keys, so for the sake of simplicity we are going to + * mark existing keys as safe. + * This migration can take long depending on the account + */ +internal class MigrateCryptoTo018(realm: DynamicRealm) : RealmMigrator(realm, 18) { + + private val moshiAdapter = MoshiProvider.providesMoshi().adapter(InboundGroupSessionData::class.java) + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("OlmInboundGroupSessionEntity") + ?.transform { dynamicObject -> + try { + dynamicObject.getString(OlmInboundGroupSessionEntityFields.INBOUND_GROUP_SESSION_DATA_JSON)?.let { oldData -> + moshiAdapter.fromJson(oldData)?.let { dataToMigrate -> + dataToMigrate.copy(trusted = true).let { + dynamicObject.setString(OlmInboundGroupSessionEntityFields.INBOUND_GROUP_SESSION_DATA_JSON, moshiAdapter.toJson(it)) + } + } + } + } catch (failure: Throwable) { + Timber.e(failure, "Failed to migrate megolm session") + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt index a4b4cd07..f93da745 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt @@ -82,7 +82,8 @@ internal class DefaultEncryptEventTask @Inject constructor( ).toContent(), forwardingCurve25519KeyChain = emptyList(), senderCurve25519Key = result.eventContent["sender_key"] as? String, - claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint() + claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint(), + isSafe = true ) } else { null 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 0b118638..2693ca47 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 @@ -53,6 +53,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo035 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo036 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo037 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -61,7 +62,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 36L, + schemaVersion = 37L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -107,5 +108,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 34) MigrateSessionTo034(realm).perform() if (oldVersion < 35) MigrateSessionTo035(realm).perform() if (oldVersion < 36) MigrateSessionTo036(realm).perform() + if (oldVersion < 37) MigrateSessionTo037(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt index 0a6d4bf8..193710f9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt @@ -228,7 +228,8 @@ private fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEnt payload = result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe ) // Save decryption result, to not decrypt every time we enter the thread list eventEntity.setDecryptionResult(result) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LocalRoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LocalRoomSummaryMapper.kt new file mode 100644 index 00000000..09cb5985 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LocalRoomSummaryMapper.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2022 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.mapper + +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary +import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity +import javax.inject.Inject + +internal class LocalRoomSummaryMapper @Inject constructor( + private val roomSummaryMapper: RoomSummaryMapper, +) { + + fun map(localRoomSummaryEntity: LocalRoomSummaryEntity): LocalRoomSummary { + return LocalRoomSummary( + roomId = localRoomSummaryEntity.roomId, + roomSummary = localRoomSummaryEntity.roomSummaryEntity?.let { roomSummaryMapper.map(it) }, + createRoomParams = localRoomSummaryEntity.createRoomParams, + replacementRoomId = localRoomSummaryEntity.replacementRoomId, + creationState = localRoomSummaryEntity.creationState + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo037.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo037.kt new file mode 100644 index 00000000..cdb0b6c6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo037.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 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.api.session.room.model.LocalRoomCreationState +import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo037(realm: DynamicRealm) : RealmMigrator(realm, 37) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("LocalRoomSummaryEntity") + ?.addField(LocalRoomSummaryEntityFields.REPLACEMENT_ROOM_ID, String::class.java) + ?.addField(LocalRoomSummaryEntityFields.STATE_STR, String::class.java) + ?.transform { obj -> + obj.set(LocalRoomSummaryEntityFields.STATE_STR, LocalRoomCreationState.NOT_CREATED.name) + } + } +} 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 8b5a211f..ee5c3d90 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 @@ -87,7 +87,8 @@ internal open class EventEntity( payload = result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe ) val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) decryptionResultJson = adapter.toJson(decryptionResult) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/LocalRoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/LocalRoomSummaryEntity.kt index fd8331e9..a978e371 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/LocalRoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/LocalRoomSummaryEntity.kt @@ -18,15 +18,24 @@ package org.matrix.android.sdk.internal.database.model import io.realm.RealmObject import io.realm.annotations.PrimaryKey +import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.toJSONString internal open class LocalRoomSummaryEntity( @PrimaryKey var roomId: String = "", var roomSummaryEntity: RoomSummaryEntity? = null, - private var createRoomParamsStr: String? = null + var replacementRoomId: String? = null, ) : RealmObject() { + private var stateStr: String = LocalRoomCreationState.NOT_CREATED.name + var creationState: LocalRoomCreationState + get() = LocalRoomCreationState.valueOf(stateStr) + set(value) { + stateStr = value.name + } + + private var createRoomParamsStr: String? = null var createRoomParams: CreateRoomParams? get() { return CreateRoomParams.fromJson(createRoomParamsStr) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LocalRoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LocalRoomSummaryEntityQueries.kt index 527350be..44730eb7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LocalRoomSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LocalRoomSummaryEntityQueries.kt @@ -22,10 +22,6 @@ import io.realm.kotlin.where import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields -internal fun LocalRoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<LocalRoomSummaryEntity> { - val query = realm.where<LocalRoomSummaryEntity>() - if (roomId != null) { - query.equalTo(LocalRoomSummaryEntityFields.ROOM_ID, roomId) - } - return query +internal fun LocalRoomSummaryEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<LocalRoomSummaryEntity> { + return realm.where<LocalRoomSummaryEntity>().equalTo(LocalRoomSummaryEntityFields.ROOM_ID, roomId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt index b180c06e..170814d3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt @@ -33,6 +33,11 @@ internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: Strin .equalTo(ReadReceiptEntityFields.USER_ID, userId) } +internal fun ReadReceiptEntity.Companion.whereRoomId(realm: Realm, roomId: String): RealmQuery<ReadReceiptEntity> { + return realm.where<ReadReceiptEntity>() + .equalTo(ReadReceiptEntityFields.ROOM_ID, roomId) +} + internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity { return ReadReceiptEntity().apply { this.primaryKey = "${roomId}_$userId" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt index b2ab9879..a93ff42c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt @@ -38,6 +38,7 @@ internal class QueryStringValueProcessor @Inject constructor( is ContentQueryStringValue -> when (queryStringValue) { is QueryStringValue.Equals -> equalTo(field, queryStringValue.toRealmValue(), queryStringValue.case.toRealmCase()) is QueryStringValue.Contains -> contains(field, queryStringValue.toRealmValue(), queryStringValue.case.toRealmCase()) + is QueryStringValue.NotContains -> not().process(field, QueryStringValue.Contains(queryStringValue.string, queryStringValue.case)) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index abea2d34..262c111b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.members.MembershipService +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.relation.RelationService @@ -82,6 +83,14 @@ internal class DefaultRoom( return roomSummaryDataSource.getRoomSummary(roomId) } + override fun getLocalRoomSummaryLive(): LiveData<Optional<LocalRoomSummary>> { + return roomSummaryDataSource.getLocalRoomSummaryLive(roomId) + } + + override fun localRoomSummary(): LocalRoomSummary? { + return roomSummaryDataSource.getLocalRoomSummary(roomId) + } + override fun asSpace(): Space? { if (roomSummary()?.roomType != RoomType.SPACE) return null return DefaultSpace(this, roomSummaryDataSource, viaParameterFinder) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt index 989bcaee..6d72b8ef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -29,10 +29,12 @@ import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult import org.matrix.android.sdk.api.session.room.alias.RoomAliasDescription import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho import org.matrix.android.sdk.api.session.room.peeking.PeekResult import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount @@ -106,6 +108,10 @@ internal class DefaultRoomService @Inject constructor( return roomSummaryDataSource.getRoomSummaryLive(roomId) } + override fun getLocalRoomSummaryLive(roomId: String): LiveData<Optional<LocalRoomSummary>> { + return roomSummaryDataSource.getLocalRoomSummaryLive(roomId) + } + override fun getRoomSummaries( queryParams: RoomSummaryQueryParams, sortOrder: RoomSortOrder @@ -173,7 +179,10 @@ internal class DefaultRoomService @Inject constructor( } override suspend fun onRoomDisplayed(roomId: String) { - updateBreadcrumbsTask.execute(UpdateBreadcrumbsTask.Params(roomId)) + // Do not add local rooms to the recent rooms list as they should not be known by the server + if (!RoomLocalEcho.isLocalEchoId(roomId)) { + updateBreadcrumbsTask.execute(UpdateBreadcrumbsTask.Params(roomId)) + } } override suspend fun joinRoom(roomIdOrAlias: String, reason: String?, viaServers: List<String>) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomFromLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomFromLocalRoomTask.kt index 02538a5c..2245eb85 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomFromLocalRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomFromLocalRoomTask.kt @@ -17,38 +17,23 @@ package org.matrix.android.sdk.internal.session.room.create import com.zhuinden.monarchy.Monarchy -import io.realm.kotlin.where import kotlinx.coroutines.TimeoutCancellationException -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure +import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState +import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams -import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent -import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.database.awaitNotEmptyResult -import org.matrix.android.sdk.internal.database.mapper.toEntity -import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity 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.EventInsertType import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity -import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields -import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore -import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereRoomId import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.task.Task -import org.matrix.android.sdk.internal.util.awaitTransaction -import org.matrix.android.sdk.internal.util.time.Clock -import java.util.UUID import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -56,94 +41,100 @@ import javax.inject.Inject * Create a room on the server from a local room. * The configuration of the local room will be use to configure the new room. * The potential local room members will also be invited to this new room. - * - * A local tombstone event will be created to indicate that the local room has been replacing by the new one. */ internal interface CreateRoomFromLocalRoomTask : Task<CreateRoomFromLocalRoomTask.Params, String> { data class Params(val localRoomId: String) } internal class DefaultCreateRoomFromLocalRoomTask @Inject constructor( - @UserId private val userId: String, @SessionDatabase private val monarchy: Monarchy, private val createRoomTask: CreateRoomTask, - private val stateEventDataSource: StateEventDataSource, - private val clock: Clock, + private val roomSummaryDataSource: RoomSummaryDataSource, ) : CreateRoomFromLocalRoomTask { private val realmConfiguration get() = monarchy.realmConfiguration override suspend fun execute(params: CreateRoomFromLocalRoomTask.Params): String { - val replacementRoomId = stateEventDataSource.getStateEvent(params.localRoomId, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty) - ?.content.toModel<RoomTombstoneContent>() - ?.replacementRoomId + val localRoomSummary = roomSummaryDataSource.getLocalRoomSummary(params.localRoomId) + ?: error("## CreateRoomFromLocalRoomTask - Cannot retrieve LocalRoomSummary with roomId ${params.localRoomId}") - if (replacementRoomId != null) { - return replacementRoomId + // If a room has already been created for the given local room, return the existing roomId + if (localRoomSummary.replacementRoomId != null) { + return localRoomSummary.replacementRoomId } - var createRoomParams: CreateRoomParams? = null - var isEncrypted = false - monarchy.doWithRealm { realm -> - realm.where<LocalRoomSummaryEntity>() - .equalTo(LocalRoomSummaryEntityFields.ROOM_ID, params.localRoomId) - .findFirst() - ?.let { - createRoomParams = it.createRoomParams - isEncrypted = it.roomSummaryEntity?.isEncrypted.orFalse() - } + if (localRoomSummary.createRoomParams != null && localRoomSummary.roomSummary != null) { + return createRoom(params.localRoomId, localRoomSummary.roomSummary, localRoomSummary.createRoomParams) + } else { + error("## CreateRoomFromLocalRoomTask - Invalid LocalRoomSummary: $localRoomSummary") } - val roomId = createRoomTask.execute(createRoomParams!!) + } + /** + * Create a room on the server for the given local room. + * + * @param localRoomId the local room identifier. + * @param localRoomSummary the RoomSummary of the local room. + * @param createRoomParams the CreateRoomParams object which was used to configure the local room. + * + * @return the identifier of the created room. + */ + private suspend fun createRoom(localRoomId: String, localRoomSummary: RoomSummary, createRoomParams: CreateRoomParams): String { + updateCreationState(localRoomId, LocalRoomCreationState.CREATING) + val replacementRoomId = runCatching { + createRoomTask.execute(createRoomParams) + }.fold( + { it }, + { + updateCreationState(localRoomId, LocalRoomCreationState.FAILURE) + throw it + } + ) + updateReplacementRoomId(localRoomId, replacementRoomId) + waitForRoomEvents(replacementRoomId, localRoomSummary) + updateCreationState(localRoomId, LocalRoomCreationState.CREATED) + return replacementRoomId + } + + /** + * Wait for all the room events before triggering the created state. + * + * @param replacementRoomId the identifier of the created room + * @param localRoomSummary the RoomSummary of the local room. + */ + private suspend fun waitForRoomEvents(replacementRoomId: String, localRoomSummary: RoomSummary) { try { - // Wait for all the room events before triggering the replacement room awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> realm.where(RoomSummaryEntity::class.java) - .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId) - .equalTo(RoomSummaryEntityFields.INVITED_MEMBERS_COUNT, createRoomParams?.invitedUserIds?.size ?: 0) + .equalTo(RoomSummaryEntityFields.ROOM_ID, replacementRoomId) + .equalTo(RoomSummaryEntityFields.INVITED_MEMBERS_COUNT, localRoomSummary.invitedMembersCount) } awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> - EventEntity.whereRoomId(realm, roomId) + EventEntity.whereRoomId(realm, replacementRoomId) .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_HISTORY_VISIBILITY) } - if (isEncrypted) { + if (localRoomSummary.isEncrypted) { awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> - EventEntity.whereRoomId(realm, roomId) + EventEntity.whereRoomId(realm, replacementRoomId) .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_ENCRYPTION) } } } catch (exception: TimeoutCancellationException) { - throw CreateRoomFailure.CreatedWithTimeout(roomId) + updateCreationState(localRoomSummary.roomId, LocalRoomCreationState.FAILURE) + throw CreateRoomFailure.CreatedWithTimeout(replacementRoomId) } + } - createTombstoneEvent(params, roomId) - return roomId + private fun updateCreationState(roomId: String, creationState: LocalRoomCreationState) { + monarchy.runTransactionSync { realm -> + LocalRoomSummaryEntity.where(realm, roomId).findFirst()?.creationState = creationState + } } - /** - * Create a Tombstone event to indicate that the local room has been replaced by a new one. - */ - private suspend fun createTombstoneEvent(params: CreateRoomFromLocalRoomTask.Params, roomId: String) { - val now = clock.epochMillis() - val event = Event( - type = EventType.STATE_ROOM_TOMBSTONE, - senderId = userId, - originServerTs = now, - stateKey = "", - eventId = UUID.randomUUID().toString(), - content = RoomTombstoneContent( - replacementRoomId = roomId - ).toContent() - ) - monarchy.awaitTransaction { realm -> - val eventEntity = event.toEntity(params.localRoomId, SendState.SYNCED, now).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC) - if (event.stateKey != null && event.type != null && event.eventId != null) { - CurrentStateEventEntity.getOrCreate(realm, params.localRoomId, event.stateKey, event.type).apply { - eventId = event.eventId - root = eventEntity - } - } + private fun updateReplacementRoomId(localRoomId: String, replacementRoomId: String) { + monarchy.runTransactionSync { realm -> + LocalRoomSummaryEntity.where(realm, localRoomId).findFirst()?.replacementRoomId = replacementRoomId } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt index 49951d2d..a60c7e6a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt @@ -22,12 +22,15 @@ import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.deleteOnCascade import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.database.query.whereInRoom import org.matrix.android.sdk.internal.database.query.whereRoomId import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.delete.DeleteLocalRoomTask.Params @@ -50,6 +53,12 @@ internal class DefaultDeleteLocalRoomTask @Inject constructor( if (RoomLocalEcho.isLocalEchoId(roomId)) { monarchy.awaitTransaction { realm -> Timber.i("## DeleteLocalRoomTask - delete local room id $roomId") + ReadReceiptsSummaryEntity.whereInRoom(realm, roomId = roomId).findAll() + ?.also { Timber.i("## DeleteLocalRoomTask - ReadReceiptsSummaryEntity - delete ${it.size} entries") } + ?.deleteAllFromRealm() + ReadReceiptEntity.whereRoomId(realm, roomId = roomId).findAll() + ?.also { Timber.i("## DeleteLocalRoomTask - ReadReceiptEntity - delete ${it.size} entries") } + ?.deleteAllFromRealm() RoomMemberSummaryEntity.where(realm, roomId = roomId).findAll() ?.also { Timber.i("## DeleteLocalRoomTask - RoomMemberSummaryEntity - delete ${it.size} entries") } ?.deleteAllFromRealm() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt index bac810f4..edd74c2c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt @@ -225,7 +225,8 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( payload = result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe ) } catch (e: MXCryptoError) { if (e is MXCryptoError.Base) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt index 82fc94df..5c4ed801 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.session.room.ResultBoundaries import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomType @@ -43,7 +44,9 @@ import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotification import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.LocalRoomSummaryMapper import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper +import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.query.findByAlias @@ -57,6 +60,7 @@ import javax.inject.Inject internal class RoomSummaryDataSource @Inject constructor( @SessionDatabase private val monarchy: Monarchy, private val roomSummaryMapper: RoomSummaryMapper, + private val localRoomSummaryMapper: LocalRoomSummaryMapper, private val queryStringValueProcessor: QueryStringValueProcessor, ) { @@ -95,6 +99,25 @@ internal class RoomSummaryDataSource @Inject constructor( ) } + fun getLocalRoomSummary(roomId: String): LocalRoomSummary? { + return monarchy + .fetchCopyMap({ + LocalRoomSummaryEntity.where(it, roomId).findFirst() + }, { entity, _ -> + localRoomSummaryMapper.map(entity) + }) + } + + fun getLocalRoomSummaryLive(roomId: String): LiveData<Optional<LocalRoomSummary>> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> LocalRoomSummaryEntity.where(realm, roomId) }, + { localRoomSummaryMapper.map(it) } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } + fun getRoomSummariesLive( queryParams: RoomSummaryQueryParams, sortOrder: RoomSortOrder = RoomSortOrder.NONE @@ -272,6 +295,7 @@ internal class RoomSummaryDataSource @Inject constructor( val query = with(queryStringValueProcessor) { RoomSummaryEntity.where(realm) .process(RoomSummaryEntityFields.ROOM_ID, QueryStringValue.IsNotEmpty) + .process(RoomSummaryEntityFields.ROOM_ID, queryParams.roomId) .process(queryParams.displayName.toDisplayNameField(), queryParams.displayName) .process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias) .process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt index 7c662444..e0751865 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt @@ -56,7 +56,8 @@ internal class DefaultGetEventTask @Inject constructor( payload = result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt index d2f1b320..cd13b030 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -21,7 +21,6 @@ import androidx.lifecycle.LiveData import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel @@ -45,6 +44,7 @@ import org.matrix.android.sdk.api.session.space.SpaceHierarchyData import org.matrix.android.sdk.api.session.space.SpaceService import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams import org.matrix.android.sdk.api.session.space.model.SpaceChildContent +import org.matrix.android.sdk.api.session.space.model.SpaceChildSummaryEvent import org.matrix.android.sdk.api.session.space.model.SpaceParentContent import org.matrix.android.sdk.api.session.space.peeking.SpacePeekResult import org.matrix.android.sdk.internal.di.UserId @@ -128,7 +128,7 @@ internal class DefaultSpaceService @Inject constructor( suggestedOnly: Boolean?, limit: Int?, from: String?, - knownStateList: List<Event>? + knownStateList: List<SpaceChildSummaryEvent>? ): SpaceHierarchyData { val spacesResponse = getSpacesResponse(spaceId, suggestedOnly, limit, from) val spaceRootResponse = spacesResponse.getRoot(spaceId) @@ -180,7 +180,7 @@ internal class DefaultSpaceService @Inject constructor( private fun List<SpaceChildSummaryResponse>?.mapSpaceChildren( spaceId: String, spaceRootResponse: SpaceChildSummaryResponse?, - knownStateList: List<Event>?, + knownStateList: List<SpaceChildSummaryEvent>?, ) = this?.filterIdIsNot(spaceId) ?.toSpaceChildInfoList(spaceId, spaceRootResponse, knownStateList) .orEmpty() @@ -190,7 +190,7 @@ internal class DefaultSpaceService @Inject constructor( private fun List<SpaceChildSummaryResponse>.toSpaceChildInfoList( spaceId: String, rootRoomResponse: SpaceChildSummaryResponse?, - knownStateList: List<Event>?, + knownStateList: List<SpaceChildSummaryEvent>?, ) = flatMap { spaceChildSummary -> (rootRoomResponse?.childrenState ?: knownStateList) ?.filter { it.isChildOf(spaceChildSummary) } @@ -198,10 +198,14 @@ internal class DefaultSpaceService @Inject constructor( .orEmpty() } - private fun Event.isChildOf(space: SpaceChildSummaryResponse) = stateKey == space.roomId && type == EventType.STATE_SPACE_CHILD + private fun SpaceChildSummaryEvent.isChildOf(space: SpaceChildSummaryResponse): Boolean { + return stateKey == space.roomId && type == EventType.STATE_SPACE_CHILD + } - private fun Event.toSpaceChildInfo(spaceId: String, summary: SpaceChildSummaryResponse) = content.toModel<SpaceChildContent>()?.let { content -> - createSpaceChildInfo(spaceId, summary, content) + private fun SpaceChildSummaryEvent.toSpaceChildInfo(spaceId: String, summary: SpaceChildSummaryResponse): SpaceChildInfo? { + return content.toModel<SpaceChildContent>()?.let { content -> + createSpaceChildInfo(spaceId, summary, content) + } } private fun createSpaceChildInfo( @@ -255,7 +259,7 @@ internal class DefaultSpaceService @Inject constructor( stateKey = QueryStringValue.IsEmpty ) val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>() - ?: throw UnsupportedOperationException("Cannot add canonical child, missing powerlevel") + ?: throw UnsupportedOperationException("Cannot add canonical child, missing power level") val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) if (!powerLevelsHelper.isUserAllowedToSend(userId, true, EventType.STATE_SPACE_CHILD)) { throw UnsupportedOperationException("Cannot add canonical child, not enough power level") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt index e3f8977a..0419c5ac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt @@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.session.space import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.space.model.SpaceChildSummaryEvent /** * The fields are the same as those returned by /publicRooms (see spec), with the addition of: @@ -36,10 +36,11 @@ internal data class SpaceChildSummaryResponse( */ @Json(name = "room_type") val roomType: String? = null, - /** The m.space.child events of the room. For each event, only the following fields are included: - * type, state_key, content, room_id, sender, with the addition of origin_server_ts. + /** + * The m.space.child events of the room. For each event, only the following fields are included: + * type, state_key, content, sender, and of origin_server_ts. */ - @Json(name = "children_state") val childrenState: List<Event>? = null, + @Json(name = "children_state") val childrenState: List<SpaceChildSummaryEvent>? = null, /** * Aliases of the room. May be empty. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt index b6142b3a..b2fe12eb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.sync.handler +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult @@ -42,17 +43,41 @@ internal class CryptoSyncHandler @Inject constructor( suspend fun handleToDevice(toDevice: ToDeviceSyncResponse, progressReporter: ProgressReporter? = null) { val total = toDevice.events?.size ?: 0 - toDevice.events?.forEachIndexed { index, event -> - progressReporter?.reportProgress(index * 100F / total) - // Decrypt event if necessary - Timber.tag(loggerTag.value).i("To device event from ${event.senderId} of type:${event.type}") - decryptToDeviceEvent(event, null) - if (event.getClearType() == EventType.MESSAGE && - event.getClearContent()?.toModel<MessageContent>()?.msgType == "m.bad.encrypted") { - Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") - } else { - verificationService.onToDeviceEvent(event) - cryptoService.onToDeviceEvent(event) + toDevice.events + ?.filter { isSupportedToDevice(it) } + ?.forEachIndexed { index, event -> + progressReporter?.reportProgress(index * 100F / total) + // Decrypt event if necessary + Timber.tag(loggerTag.value).i("To device event from ${event.senderId} of type:${event.type}") + decryptToDeviceEvent(event, null) + if (event.getClearType() == EventType.MESSAGE && + event.getClearContent()?.toModel<MessageContent>()?.msgType == "m.bad.encrypted") { + Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") + } else { + verificationService.onToDeviceEvent(event) + cryptoService.onToDeviceEvent(event) + } + } + } + + private val unsupportedPlainToDeviceEventTypes = listOf( + EventType.ROOM_KEY, + EventType.FORWARDED_ROOM_KEY, + EventType.SEND_SECRET + ) + + private fun isSupportedToDevice(event: Event): Boolean { + val algorithm = event.content?.get("algorithm") as? String + val type = event.type.orEmpty() + return if (event.isEncrypted()) { + algorithm == MXCRYPTO_ALGORITHM_OLM + } else { + // some clear events are not allowed + type !in unsupportedPlainToDeviceEventTypes + }.also { + if (!it) { + Timber.tag(loggerTag.value) + .w("Ignoring unsupported to device event ${event.type} alg:${algorithm}") } } } @@ -91,7 +116,8 @@ internal class CryptoSyncHandler @Inject constructor( payload = result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe ) return true } else { 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 bc91ca20..a2f2251b 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 @@ -40,6 +40,7 @@ import org.matrix.android.sdk.api.session.sync.model.RoomSync import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import org.matrix.android.sdk.internal.crypto.DefaultCryptoService +import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager import org.matrix.android.sdk.internal.database.helper.addIfNecessary import org.matrix.android.sdk.internal.database.helper.addTimelineEvent import org.matrix.android.sdk.internal.database.helper.createOrUpdate @@ -99,6 +100,7 @@ internal class RoomSyncHandler @Inject constructor( private val timelineInput: TimelineInput, private val liveEventService: Lazy<StreamEventsManager>, private val clock: Clock, + private val unRequestedForwardManager: UnRequestedForwardManager, ) { sealed class HandlingStrategy { @@ -322,6 +324,7 @@ internal class RoomSyncHandler @Inject constructor( } roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.INVITE) roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = true, inviterId = inviterEvent?.senderId, aggregator = aggregator) + unRequestedForwardManager.onInviteReceived(roomId, inviterEvent?.senderId.orEmpty(), clock.epochMillis()) return roomEntity } @@ -551,7 +554,8 @@ internal class RoomSyncHandler @Inject constructor( payload = result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe ) } catch (e: MXCryptoError) { if (e is MXCryptoError.Base) { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt new file mode 100644 index 00000000..95009376 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt @@ -0,0 +1,248 @@ +/* + * Copyright 2022 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 io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.fail +import org.amshove.kluent.shouldBe +import org.junit.Test +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent +import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult +import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager + +class UnRequestedKeysManagerTest { + + private val aliceMxId = "alice@example.com" + private val bobMxId = "bob@example.com" + private val bobDeviceId = "MKRJDSLYGA" + + private val device1Id = "MGDAADVDMG" + + private val aliceFirstDevice = CryptoDeviceInfo( + deviceId = device1Id, + userId = aliceMxId, + algorithms = MXCryptoAlgorithms.supportedAlgorithms(), + keys = mapOf( + "curve25519:$device1Id" to "yDa6cWOZ/WGBqm/JMUfTUCdEbAIzKHhuIcdDbnPEhDU", + "ed25519:$device1Id" to "XTge+TDwfm+WW10IGnaqEyLTSukPPzg3R1J1YvO1SBI", + ), + signatures = mapOf( + aliceMxId to mapOf( + "ed25519:$device1Id" to "bPOAqM40+QSMgeEzUbYbPSZZccDDMUG00lCNdSXCoaS1gKKBGkSEaHO1OcibISIabjLYzmhp9mgtivz32fbABQ", + "ed25519:Ru4ni66dbQ6FZgUoHyyBtmjKecOHMvMSsSBZ2SABtt0" to "owzUsQ4Pvn35uEIc5FdVnXVRPzsVYBV8uJRUSqr4y8r5tp0DvrMArtJukKETgYEAivcZMT1lwNihHIN9xh06DA" + ) + ), + unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Web"), + trustLevel = DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true) + ) + + private val aBobDevice = CryptoDeviceInfo( + deviceId = bobDeviceId, + userId = bobMxId, + algorithms = MXCryptoAlgorithms.supportedAlgorithms(), + keys = mapOf( + "curve25519:$bobDeviceId" to "tWwg63Yfn//61Ylhir6z4QGejvo193E6MVHmURtYVn0", + "ed25519:$bobDeviceId" to "pS5NJ1LiVksQFX+p58NlphqMxE705laRVtUtZpYIAfs", + ), + signatures = mapOf( + bobMxId to mapOf( + "ed25519:$bobDeviceId" to "zAJqsmOSzkx8EWXcrynCsWtbgWZifN7A6DLyEBs+ZPPLnNuPN5Jwzc1Rg+oZWZaRPvOPcSL0cgcxRegSBU0NBA", + ) + ), + unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Ios") + ) + + @Test + fun `test process key request if invite received`() { + val fakeDeviceListManager = mockk<DeviceListManager> { + coEvery { downloadKeys(any(), any()) } returns MXUsersDevicesMap<CryptoDeviceInfo>().apply { + setObject(bobMxId, bobDeviceId, aBobDevice) + } + } + val unrequestedForwardManager = UnRequestedForwardManager(fakeDeviceListManager) + + val roomId = "someRoomId" + + unrequestedForwardManager.onUnRequestedKeyForward( + roomId, + createFakeSuccessfullyDecryptedForwardToDevice( + aBobDevice, + aliceFirstDevice, + aBobDevice, + megolmSessionId = "megolmId1" + ), + 1_000 + ) + + unrequestedForwardManager.onUnRequestedKeyForward( + roomId, + createFakeSuccessfullyDecryptedForwardToDevice( + aBobDevice, + aliceFirstDevice, + aBobDevice, + megolmSessionId = "megolmId2" + ), + 1_000 + ) + // for now no reason to accept + runBlocking { + unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(1000) { + fail("There should be no key to process") + } + } + + // ACT + // suppose an invite is received but from another user + val inviteTime = 1_000L + unrequestedForwardManager.onInviteReceived(roomId, "@jhon:example.com", inviteTime) + + // we shouldn't process the requests! +// runBlocking { + unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(inviteTime) { + fail("There should be no key to process") + } +// } + + // ACT + // suppose an invite is received from correct user + + unrequestedForwardManager.onInviteReceived(roomId, aBobDevice.userId, inviteTime) + runBlocking { + unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(inviteTime) { + it.size shouldBe 2 + } + } + } + + @Test + fun `test invite before keys`() { + val fakeDeviceListManager = mockk<DeviceListManager> { + coEvery { downloadKeys(any(), any()) } returns MXUsersDevicesMap<CryptoDeviceInfo>().apply { + setObject(bobMxId, bobDeviceId, aBobDevice) + } + } + val unrequestedForwardManager = UnRequestedForwardManager(fakeDeviceListManager) + + val roomId = "someRoomId" + + unrequestedForwardManager.onInviteReceived(roomId, aBobDevice.userId, 1_000) + + unrequestedForwardManager.onUnRequestedKeyForward( + roomId, + createFakeSuccessfullyDecryptedForwardToDevice( + aBobDevice, + aliceFirstDevice, + aBobDevice, + megolmSessionId = "megolmId1" + ), + 1_000 + ) + + runBlocking { + unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(1000) { + it.size shouldBe 1 + } + } + } + + @Test + fun `test validity window`() { + val fakeDeviceListManager = mockk<DeviceListManager> { + coEvery { downloadKeys(any(), any()) } returns MXUsersDevicesMap<CryptoDeviceInfo>().apply { + setObject(bobMxId, bobDeviceId, aBobDevice) + } + } + val unrequestedForwardManager = UnRequestedForwardManager(fakeDeviceListManager) + + val roomId = "someRoomId" + + val timeOfKeyReception = 1_000L + + unrequestedForwardManager.onUnRequestedKeyForward( + roomId, + createFakeSuccessfullyDecryptedForwardToDevice( + aBobDevice, + aliceFirstDevice, + aBobDevice, + megolmSessionId = "megolmId1" + ), + timeOfKeyReception + ) + + val currentTimeWindow = 10 * 60_000 + + // simulate very late invite + val inviteTime = timeOfKeyReception + currentTimeWindow + 1_000 + unrequestedForwardManager.onInviteReceived(roomId, aBobDevice.userId, inviteTime) + + runBlocking { + unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(inviteTime) { + fail("There should be no key to process") + } + } + } + + private fun createFakeSuccessfullyDecryptedForwardToDevice( + sentBy: CryptoDeviceInfo, + dest: CryptoDeviceInfo, + sessionInitiator: CryptoDeviceInfo, + algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM, + roomId: String = "!zzgDlIhbWOevcdFBXr:example.com", + megolmSessionId: String = "Z/FSE8wDYheouGjGP9pezC4S1i39RtAXM3q9VXrBVZw" + ): Event { + return Event( + type = EventType.ENCRYPTED, + eventId = "!fake", + senderId = sentBy.userId, + content = OlmEventContent( + ciphertext = mapOf( + dest.identityKey()!! to mapOf( + "type" to 0, + "body" to "AwogcziNF/tv60X0elsBmnKPN3+LTXr4K3vXw+1ZJ6jpTxESIJCmMMDvOA+" + ) + ), + senderKey = sentBy.identityKey() + ).toContent(), + + ).apply { + mxDecryptionResult = OlmDecryptionResult( + payload = mapOf( + "type" to EventType.FORWARDED_ROOM_KEY, + "content" to ForwardedRoomKeyContent( + algorithm = algorithm, + roomId = roomId, + senderKey = sessionInitiator.identityKey(), + sessionId = megolmSessionId, + sessionKey = "AQAAAAAc4dK+lXxXyaFbckSxwjIEoIGDLKYovONJ7viWpwevhfvoBh+Q..." + ).toContent() + ), + senderKey = sentBy.identityKey() + ) + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateRoomFromLocalRoomTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateRoomFromLocalRoomTaskTest.kt index d3732363..9e342804 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateRoomFromLocalRoomTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateRoomFromLocalRoomTaskTest.kt @@ -22,21 +22,22 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.spyk import io.mockk.unmockkAll +import io.mockk.verify +import io.mockk.verifyOrder import io.realm.kotlin.where import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeNull import org.junit.After import org.junit.Before import org.junit.Test -import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams -import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent import org.matrix.android.sdk.internal.database.awaitNotEmptyResult import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.EventEntity @@ -44,29 +45,24 @@ import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore import org.matrix.android.sdk.internal.database.query.getOrCreate -import org.matrix.android.sdk.internal.util.time.DefaultClock import org.matrix.android.sdk.test.fakes.FakeMonarchy -import org.matrix.android.sdk.test.fakes.FakeStateEventDataSource +import org.matrix.android.sdk.test.fakes.FakeRoomSummaryDataSource private const val A_LOCAL_ROOM_ID = "local.a-local-room-id" private const val AN_EXISTING_ROOM_ID = "an-existing-room-id" private const val A_ROOM_ID = "a-room-id" -private const val MY_USER_ID = "my-user-id" @ExperimentalCoroutinesApi internal class DefaultCreateRoomFromLocalRoomTaskTest { private val fakeMonarchy = FakeMonarchy() - private val clock = DefaultClock() private val createRoomTask = mockk<CreateRoomTask>() - private val fakeStateEventDataSource = FakeStateEventDataSource() + private val fakeRoomSummaryDataSource = FakeRoomSummaryDataSource() private val defaultCreateRoomFromLocalRoomTask = DefaultCreateRoomFromLocalRoomTask( - userId = MY_USER_ID, monarchy = fakeMonarchy.instance, createRoomTask = createRoomTask, - stateEventDataSource = fakeStateEventDataSource.instance, - clock = clock + roomSummaryDataSource = fakeRoomSummaryDataSource.instance, ) @Before @@ -91,13 +87,12 @@ internal class DefaultCreateRoomFromLocalRoomTaskTest { @Test fun `given a local room id when execute then the existing room id is kept`() = runTest { // Given - givenATombstoneEvent( - Event( - roomId = A_LOCAL_ROOM_ID, - type = EventType.STATE_ROOM_TOMBSTONE, - stateKey = "", - content = RoomTombstoneContent(replacementRoomId = AN_EXISTING_ROOM_ID).toContent() - ) + val aCreateRoomParams = mockk<CreateRoomParams>(relaxed = true) + givenALocalRoomSummary(aCreateRoomParams = aCreateRoomParams, aCreationState = LocalRoomCreationState.CREATED, aReplacementRoomId = AN_EXISTING_ROOM_ID) + val aLocalRoomSummaryEntity = givenALocalRoomSummaryEntity( + aCreateRoomParams = aCreateRoomParams, + aCreationState = LocalRoomCreationState.CREATED, + aReplacementRoomId = AN_EXISTING_ROOM_ID ) // When @@ -105,20 +100,18 @@ internal class DefaultCreateRoomFromLocalRoomTaskTest { val result = defaultCreateRoomFromLocalRoomTask.execute(params) // Then - verifyTombstoneEvent(AN_EXISTING_ROOM_ID) + fakeRoomSummaryDataSource.verifyGetLocalRoomSummary(A_LOCAL_ROOM_ID) result shouldBeEqualTo AN_EXISTING_ROOM_ID + aLocalRoomSummaryEntity.replacementRoomId shouldBeEqualTo AN_EXISTING_ROOM_ID + aLocalRoomSummaryEntity.creationState shouldBeEqualTo LocalRoomCreationState.CREATED } @Test fun `given a local room id when execute then it is correctly executed`() = runTest { // Given - val aCreateRoomParams = mockk<CreateRoomParams>() - val aLocalRoomSummaryEntity = mockk<LocalRoomSummaryEntity> { - every { roomSummaryEntity } returns mockk(relaxed = true) - every { createRoomParams } returns aCreateRoomParams - } - givenATombstoneEvent(null) - givenALocalRoomSummaryEntity(aLocalRoomSummaryEntity) + val aCreateRoomParams = mockk<CreateRoomParams>(relaxed = true) + givenALocalRoomSummary(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null) + val aLocalRoomSummaryEntity = givenALocalRoomSummaryEntity(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null) coEvery { createRoomTask.execute(any()) } returns A_ROOM_ID @@ -127,32 +120,84 @@ internal class DefaultCreateRoomFromLocalRoomTaskTest { val result = defaultCreateRoomFromLocalRoomTask.execute(params) // Then - verifyTombstoneEvent(null) + fakeRoomSummaryDataSource.verifyGetLocalRoomSummary(A_LOCAL_ROOM_ID) // CreateRoomTask has been called with the initial CreateRoomParams coVerify { createRoomTask.execute(aCreateRoomParams) } // The resulting roomId matches the roomId returned by the createRoomTask result shouldBeEqualTo A_ROOM_ID - // A tombstone state event has been created - coVerify { CurrentStateEventEntity.getOrCreate(realm = any(), roomId = A_LOCAL_ROOM_ID, stateKey = any(), type = EventType.STATE_ROOM_TOMBSTONE) } + // The room creation state has correctly been updated + verifyOrder { + aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.CREATING + aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.CREATED + } + // The local room summary has been updated with the created room id + verify { aLocalRoomSummaryEntity.replacementRoomId = A_ROOM_ID } + aLocalRoomSummaryEntity.replacementRoomId shouldBeEqualTo A_ROOM_ID + aLocalRoomSummaryEntity.creationState shouldBeEqualTo LocalRoomCreationState.CREATED + } + + @Test + fun `given a local room id when execute with an exception then the creation state is correctly updated`() = runTest { + // Given + val aCreateRoomParams = mockk<CreateRoomParams>(relaxed = true) + givenALocalRoomSummary(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null) + val aLocalRoomSummaryEntity = givenALocalRoomSummaryEntity(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null) + + coEvery { createRoomTask.execute(any()) }.throws(mockk()) + + // When + val params = CreateRoomFromLocalRoomTask.Params(A_LOCAL_ROOM_ID) + tryOrNull { defaultCreateRoomFromLocalRoomTask.execute(params) } + + // Then + fakeRoomSummaryDataSource.verifyGetLocalRoomSummary(A_LOCAL_ROOM_ID) + // CreateRoomTask has been called with the initial CreateRoomParams + coVerify { createRoomTask.execute(aCreateRoomParams) } + // The room creation state has correctly been updated + verifyOrder { + aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.CREATING + aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.FAILURE + } + // The local room summary has been updated with the created room id + aLocalRoomSummaryEntity.replacementRoomId.shouldBeNull() + aLocalRoomSummaryEntity.creationState shouldBeEqualTo LocalRoomCreationState.FAILURE } - private fun givenATombstoneEvent(event: Event?) { - fakeStateEventDataSource.givenGetStateEventReturns(event) + private fun givenALocalRoomSummary( + aCreateRoomParams: CreateRoomParams, + aCreationState: LocalRoomCreationState = LocalRoomCreationState.NOT_CREATED, + aReplacementRoomId: String? = null + ): LocalRoomSummary { + val aLocalRoomSummary = LocalRoomSummary( + roomId = A_LOCAL_ROOM_ID, + roomSummary = mockk(relaxed = true), + createRoomParams = aCreateRoomParams, + creationState = aCreationState, + replacementRoomId = aReplacementRoomId, + ) + fakeRoomSummaryDataSource.givenGetLocalRoomSummaryReturns(A_LOCAL_ROOM_ID, aLocalRoomSummary) + return aLocalRoomSummary } - private fun givenALocalRoomSummaryEntity(localRoomSummaryEntity: LocalRoomSummaryEntity) { + private fun givenALocalRoomSummaryEntity( + aCreateRoomParams: CreateRoomParams, + aCreationState: LocalRoomCreationState = LocalRoomCreationState.NOT_CREATED, + aReplacementRoomId: String? = null + ): LocalRoomSummaryEntity { + val aLocalRoomSummaryEntity = spyk(LocalRoomSummaryEntity( + roomId = A_LOCAL_ROOM_ID, + roomSummaryEntity = mockk(relaxed = true), + replacementRoomId = aReplacementRoomId, + ).apply { + createRoomParams = aCreateRoomParams + creationState = aCreationState + }) every { fakeMonarchy.fakeRealm.instance .where<LocalRoomSummaryEntity>() .equalTo(LocalRoomSummaryEntityFields.ROOM_ID, A_LOCAL_ROOM_ID) .findFirst() - } returns localRoomSummaryEntity - } - - private fun verifyTombstoneEvent(expectedRoomId: String?) { - fakeStateEventDataSource.verifyGetStateEvent(A_LOCAL_ROOM_ID, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty) - fakeStateEventDataSource.instance.getStateEvent(A_LOCAL_ROOM_ID, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty) - ?.content.toModel<RoomTombstoneContent>() - ?.replacementRoomId shouldBeEqualTo expectedRoomId + } returns aLocalRoomSummaryEntity + return aLocalRoomSummaryEntity } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt index 2d501f12..93999458 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt @@ -47,6 +47,11 @@ internal class FakeMonarchy { } coAnswers { firstArg<Monarchy.RealmBlock>().doWithRealm(fakeRealm.instance) } + coEvery { + instance.runTransactionSync(any()) + } coAnswers { + firstArg<Realm.Transaction>().execute(fakeRealm.instance) + } every { instance.realmConfiguration } returns mockk() } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomSummaryDataSource.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomSummaryDataSource.kt new file mode 100644 index 00000000..c7b70a3a --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomSummaryDataSource.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 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.test.fakes + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource + +internal class FakeRoomSummaryDataSource { + + val instance: RoomSummaryDataSource = mockk() + + fun givenGetLocalRoomSummaryReturns(roomId: String?, localRoomSummary: LocalRoomSummary?) { + every { instance.getLocalRoomSummary(roomId = roomId ?: any()) } returns localRoomSummary + } + + fun verifyGetLocalRoomSummary(roomId: String) { + verify { instance.getLocalRoomSummary(roomId) } + } +} -- GitLab