diff --git a/CHANGES.md b/CHANGES.md index 4cecceda29a3507359b5329c7d9d6f349b08e7a7..45e0895624b4b3afc2e50f1472937ce703ea7d70 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,16 @@ Please also refer to the Changelog of Element Android: https://github.com/vector-im/element-android/blob/main/CHANGES.md +Changes in Matrix-SDK v1.5.1 (2022-09-28) +======================================= + +Imported from Element 1.5.1. (https://github.com/vector-im/element-android/releases/tag/v1.5.1) + +Security âš ï¸ +---------- + +This update provides important security fixes, update now. +Ref: CVE-2022-39246 CVE-2022-39248 + Changes in Matrix-SDK v1.4.36 (2022-09-13) ======================================= diff --git a/dependencies.gradle b/dependencies.gradle index 3759763fb76e2d11894c0054ffec42ce5262beb7..9641a63f263b3ac5cb21a96cd3cef9625299beb3 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 fcd127905e553f87ec160ac3e18fbc2a053580cc..f905612a22130da3bd07984def35d99cb48f31b6 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -70,8 +70,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/gradle.properties b/gradle.properties index 14a2c0b06ec9c08941e4f74e78201c0b3610265f..ca3a954156ed20f296576893d625b05e656f687f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,7 +26,7 @@ vector.httpLogLevel=NONE # Ref: https://github.com/vanniktech/gradle-maven-publish-plugin GROUP=org.matrix.android POM_ARTIFACT_ID=matrix-android-sdk2 -VERSION_NAME=1.4.36 +VERSION_NAME=1.5.1 POM_PACKAGING=aar 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 a78953caacc87a7cfa27534f64b9d104876ff597..43f42a3ed460c08d009a1d5d67ea846aee584bf8 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 f36bfb6210e164e8e4c82316bbd4b70601ff3776..210ce906928ac8c161777018ed0a20da835ccc51 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 f8832954950e7ff5570023e56ae0bced2ca8c4fd..410fb4f5d4778be9e415eeca3b933eec41605805 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 32a95008b1ac5017ff1790dc6442f09041795e66..4b44aab18b8b0030a48e279d7b3539282b80eaa5 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 5fe737618411831e27d93b4baa6c465b4b11a8d6..130c8d13f9909a242ee33e12118f8b0ee437125f 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 7bb53e139c3bd5d63206c844f0fa57aeb51755a2..df0b10ea6d9143e49b856c5d268fe19413ab6715 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 0aac4297e4265114ac8b405218e7fcf424b444f1..910a349b403ac108b7f7809dfde68600a6bb690c 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 015cb6a1a28bfc409aa7ee625aefd71447d70de0..38f522586f069c4596a82bb92accbfd8e82f26a2 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 d3f6ec2287f8e375f43788c2e1cab9bf45489de9..1d6e79c8f63041803aec822fbc941e162d041839 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 a15e73eb88f007dd917797478d1c6c9896641f6c..96dac2761869116842d9b90ef95b2e4414f85b9d 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 0a0ccc2db3e26be08aa2496f591f503090c3b48d..66d7558fe205c53bef67bef27675aae65538ad5e 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 a26f6606edd1a8f7fea8108520ad9a277619b516..6d57318f87a51eb628753ebf4d4aa0dfddc5d8c2 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 59dc6c434d00bfd083f2ac905da2051fd4ebf359..f5d2c0d9a0ef6592e932fd34c5f6ba2dd321d76b 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 5d2769ac3cbd774c5c78051101ee9b48316673b0..8031fcaeea893dae8cb5e4dbdbb2e3895ebf6d49 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 ad8106c9c10dd8a179d1e9d2aa86f02def0417a0..65383f1007b1874da82cc0c99e2d4eff3077834f 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 60963ef25a4da2e1a530cbb16bd24c34699c3456..d651f06e2351b0206be9b5d362b1ac122236deb9 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 0000000000000000000000000000000000000000..4fc99225c496944ceab1e508d5c3e47da01bd2f8 --- /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 0000000000000000000000000000000000000000..eced1dd5815c363498611767f8b6afda6ca15067 --- /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 7d3109fb6e60018daac58a830c8898ca201d1819..2388bee0ee7a3b45f1f9173efc6cd552fb7cccf3 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 7ef0d63924ddba419eeb183a28871108c5f2fc28..ec0e642ad348f5bbfadc243671004d042508ddb6 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 ecc3eb5224890c1345e16b73cd2d7b529b9a28ac..d03f4c42cfb4e89cf21e404ebcdcb54e9d693b0c 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 c7a64050146be35ae42b3cb5c663c4a625948337..5d2a9412d1109d2be2dfee45ba32b99fb1a39c4d 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 0000000000000000000000000000000000000000..13aa0336e581fd28978f0c2b69c6d83ce9de8e46 --- /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 0c5465e12a8ae51a8a018c89121ab8e4dbf68232..7075023798e6089c59f986ef8b4018d86b8af810 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 8dd7c309c6b432b37daf1838638a9f2f3a94fb40..322f297ac30ae61625f2df17ededcf13e89ac431 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 39dfb72149047bd09ca80ca7182d3512f6064612..6d197a09edd461d5c72c4972235ad42a2c84ae6c 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 96ccba51dc10094967157ff4267884a9d35975dc..48b46523048c2d24a00c6c8f67f8767108c4534b 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 a79e1a89012a7e3d4b02aa18825111cfb6047f27..5691f24d17de077990a14bb9d4e8cb88f7edf964 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 6847a463690c369774fd09d6efb7bf0c52bdabc4..e2ddd5d19f9598e6d9b31c1f1ec32c9144e41473 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 410b74e19f1d6b155875241bff96d50e79e584c7..5354cbff3bc52753981d3ed25d4fe22ba2987bc2 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 38edbb7430a2bd932dd86c855a237cc4482ad6d1..99f8bc69e05edafd4a5b3338e709eb6c367f7352 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 771b5f9a626e6eaf17322a2cb87638f9ac34f850..fca6fab66c27bbdc0d402dabf5561b4e62d3514c 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 0000000000000000000000000000000000000000..42629b617e6dd8e1aeb59f239e1e7a03d374c4a4 --- /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 23ddf8315fc4fad4334e78cde57df96213d957da..21d57f5be43f9e0e423c65093f229b0779552013 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 @@ -703,14 +703,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 2ce36aa209ba7f90d922652b431f7890187f429d..15e8ba835b5cabdfd93a239585cee17b6eb2087f 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 2772b3483594e1e58205386a6e6526ac27d34bbd..2c6a0a967af20de122a85fb97e6d08fecf9dae47 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 c36d572da63024fe55622e1a283ca9c029d76b0b..426d50a54fc997c1398e1d25874974b23d2c8d3d 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 0000000000000000000000000000000000000000..3bedf58ca2796c416e8388c9e1da459077cb4ad4 --- /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 a4b4cd0761bca993a558e8c857644f64e4be6a8e..f93da745073ded51382476c3c1eaa04b89c41732 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 0b1186386439e19dc6c72eb7db82014ab4a90ad5..2693ca474c21d8cf434b9c94f9ee876916a81a2b 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 0a6d4bf833285e5312630fdb373faa39e448b6a0..193710f9621fc0b6e562561054f4f8be5d6d2550 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 0000000000000000000000000000000000000000..09cb5985f3ccb1d0c9cd183683ea64d7c34eb06c --- /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 0000000000000000000000000000000000000000..cdb0b6c6827014b075bcc608d48af8e0f99fc9ca --- /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 8b5a211fbaba4d5a477baff5aec881a48f047765..ee5c3d90c1282929e52867f697012bceb99dd076 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 fd8331e986505293dbcc1ab5c8e63d84e1731914..a978e3719d351a9827d4f09d32449f0b0bae8d99 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 527350bedc261cdc4b352c5c0f97c8803a24584b..44730eb75d74ac77b71c07767b51c313e0284a3f 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 b180c06e2c2283739e9345e29be6566ecf6e43bf..170814d3f280ede63662e52710a8d9371bec7a70 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 b2ab9879df934ed0047d684b9241d925d23a8331..a93ff42c9e290b1f54d840b5b7a6bc8bb5276248 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 abea2d34cd210834c680d81dc819b5346ecaf17a..262c111b736a48b446dfbb9a95d55005e2abe1ad 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 989bcaee4474ad2b8bf33f944ed95d33e0c4f2f8..6d72b8ef2062aeaca73787a3eedebe28019616cf 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 02538a5cc33153e8f1f3940811e03329d370a3c2..2245eb851380e26e9cd3c79a5e61d2a1d8869567 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 49951d2da05ec11e6a89ebc3ed93dcfa026d3131..a60c7e6a27c3ce1bc335e8b8da84e47648bc7a7a 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 bac810f424704bb3604d25d07dd044410c4198bc..edd74c2ce04a2013c0ec4b2685dc378cabdb7f79 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 82fc94df7c8fcdb80714d5e684a70e47b54ebd63..5c4ed8012bf754e41ca4a3cb93499e8a1b3b6c5b 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 7c662444e4bb10cbf28db56acfda64a8f524518b..e0751865ad5f60625ba3c687b2daaefb20c25158 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 d2f1b3202b049da22a5d84c6d8516c2418b26c52..cd13b03017f60feee229715fb5043b2319664ed3 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 e3f8977ac59bd5dd6ac6547310ee13215fee7b44..0419c5acf168d2b03b768e3a61965faf0ead0119 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 b6142b3a7aef7192deb59982d3f1f67fa90d5e88..b2fe12ebc3fd8dceecd198f497dd49f4c9e093ee 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 bc91ca205d8aa18c541fd89ab2055246068b4f11..a2f2251b70e032c6f768cced51d275335a5932d1 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 0000000000000000000000000000000000000000..950093760a11b874950f06357d320a590a454653 --- /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 d3732363b5a77a9dd808706710e32dc3997c0619..9e3428043786ef390f3f0bce31167c277a9abb89 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 2d501f12af2595e97643e1ca3e0ac2c29ba31e0c..93999458c68caa0d72d09fb591324cc67792de8c 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 0000000000000000000000000000000000000000..c7b70a3ad5c8450a4ac064db49cbfa4e219a7ff2 --- /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) } + } +}