VYPR
High severityNVD Advisory· Published Sep 28, 2022· Updated Apr 23, 2025

matrix-android-sdk2 vulnerable to Olm/Megolm protocol confusion

CVE-2022-39248

Description

matrix-android-sdk2 is the Matrix SDK for Android. Prior to version 1.5.1, an attacker cooperating with a malicious homeserver can construct messages that legitimately appear to have come from another person, without any indication such as a grey shield. Additionally, a sophisticated attacker cooperating with a malicious homeserver could employ this vulnerability to perform a targeted attack in order to send fake to-device messages appearing to originate from another user. This can allow, for example, to inject the key backup secret during a self-verification, to make a targeted device start using a malicious key backup spoofed by the homeserver. matrix-android-sdk2 would then additionally sign such a key backup with its device key, spilling trust over to other devices trusting the matrix-android-sdk2 device. These attacks are possible due to a protocol confusion vulnerability that accepts to-device messages encrypted with Megolm instead of Olm. matrix-android-sdk2 version 1.5.1 has been modified to only accept Olm-encrypted to-device messages and to stop signing backups on a successful decryption. Out of caution, several other checks have been audited or added. This attack requires coordination between a malicious home server and an attacker, so those who trust their home servers do not need a workaround.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.matrix.android:matrix-android-sdk2Maven
< 1.5.11.5.1

Affected products

1

Patches

1
77df720a238d

Merge branch 'release/1.5.1' into develop

67 files changed · +1388 328
  • CHANGES.md+11 0 modified
    @@ -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)
     =======================================
     
    
  • dependencies.gradle+3 3 modified
    @@ -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",
    
  • dependencies_groups.gradle+0 2 modified
    @@ -69,8 +69,6 @@ ext.groups = [
                             'com.gabrielittner.threetenbp',
                             'com.getkeepsafe.relinker',
                             'com.github.bumptech.glide',
    -                        'com.github.filippudak',
    -                        'com.github.filippudak.progresspieview',
                             'com.github.javaparser',
                             'com.github.piasy',
                             'com.github.shyiko.klob',
    
  • gradle.properties+1 1 modified
    @@ -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
     
    
  • matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt+8 4 modified
    @@ -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()
                         )
                 )
             }
    
  • matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt+2 1 modified
    @@ -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) {
    
  • matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt+39 34 modified
    @@ -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,44 +388,37 @@ 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 ->
                 val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
                 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
    
  • matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt+10 4 modified
    @@ -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) {
    
  • matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt+57 25 modified
    @@ -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,24 +44,22 @@ 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
     
     @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)
    
  • matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt+9 2 modified
    @@ -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
    
  • matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt+5 1 modified
    @@ -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
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt+3 2 modified
    @@ -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,
     
             )
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt+5 0 modified
    @@ -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].
          */
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt+3 1 modified
    @@ -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
     )
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt+6 1 modified
    @@ -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,
     )
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt+16 2 modified
    @@ -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 {
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt+1 1 modified
    @@ -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.
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomCreationState.kt+24 0 added
    @@ -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
    +}
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/LocalRoomSummary.kt+46 0 added
    @@ -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,
    +)
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt+0 1 modified
    @@ -34,5 +34,4 @@ data class SpaceChildInfo(
             val canonicalAlias: String?,
             val aliases: List<String>?,
             val worldReadable: Boolean
    -
     )
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt+12 0 modified
    @@ -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.
          */
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt+7 0 modified
    @@ -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]
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt+8 0 modified
    @@ -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,
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionExtensions.kt+8 0 modified
    @@ -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)
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildSummaryEvent.kt+30 0 added
    @@ -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,
    +)
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceHierarchyData.kt+2 2 modified
    @@ -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
     )
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt+3 3 modified
    @@ -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,
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt+1 1 modified
    @@ -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?
     
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt+1 1 modified
    @@ -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) {}
     }
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt+16 8 modified
    @@ -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,
             )
         }
     }
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt+82 40 modified
    @@ -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,45 +188,47 @@ 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()
     
             if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.sessionId.isNullOrEmpty() || roomKeyContent.sessionKey.isNullOrEmpty()) {
                 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,48 +291,44 @@ 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
                 is MXOlmDevice.AddSessionResult.NotImportedHigherIndex -> addSessionResult.newIndex
                 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)
             }
         }
     
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt+2 1 modified
    @@ -162,7 +162,8 @@ internal class MXMegolmEncryption(
                     forwardingCurve25519KeyChain = emptyList(),
                     keysClaimed = keysClaimedMap,
                     exportFormat = false,
    -                sharedHistory = sharedHistory
    +                sharedHistory = sharedHistory,
    +                trusted = true
             )
     
             defaultKeysBackupService.maybeBackupKeys()
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt+150 0 added
    @@ -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()
    +                }
    +            }
    +        }
    +    }
    +}
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt+25 5 modified
    @@ -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 ->
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt+15 0 modified
    @@ -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)
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt+0 7 modified
    @@ -652,14 +652,7 @@ internal class DefaultKeysBackupService @Inject constructor(
                             }
                     val recoveryKey = computeRecoveryKey(secret.fromBase64())
                     if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) {
    -                    awaitCallback<Unit> {
    -                        trustKeysBackupVersion(keysBackupVersion, true, it)
    -                    }
                         // we don't want to start immediately downloading all as it can take very long
    -
    -//                    val importResult = awaitCallback<ImportRoomKeysResult> {
    -//                        restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it)
    -//                    }
                         withContext(coroutineDispatchers.crypto) {
                             cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
                         }
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt+6 3 modified
    @@ -38,14 +38,17 @@ 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.
              */
             @Json(name = "shared_history")
             val sharedHistory: Boolean = false,
     
    +        /**
    +         * Flag indicating that this key is trusted.
    +         */
    +        @Json(name = "trusted")
    +        val trusted: Boolean? = null,
    +
             )
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt+1 0 modified
    @@ -86,6 +86,7 @@ data class MXInboundMegolmSessionWrapper(
                         keysClaimed = megolmSessionData.senderClaimedKeys,
                         forwardingCurve25519KeyChain = megolmSessionData.forwardingCurve25519KeyChain,
                         sharedHistory = megolmSessionData.sharedHistory,
    +                    trusted = false
                 )
     
                 return MXInboundMegolmSessionWrapper(
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt+50 16 modified
    @@ -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,14 +613,17 @@ 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) {
                     OlmInboundGroupSession.importSession(sessionKey)
                 } 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()
             )
         }
     
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt+12 1 modified
    @@ -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 {
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo018.kt+52 0 added
    @@ -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")
    +                    }
    +                }
    +    }
    +}
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt+3 1 modified
    @@ -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()
         }
     }
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt+2 1 modified
    @@ -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
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt+2 1 modified
    @@ -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)
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LocalRoomSummaryMapper.kt+36 0 added
    @@ -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
    +        )
    +    }
    +}
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo037.kt+34 0 added
    @@ -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)
    +                }
    +    }
    +}
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt+2 1 modified
    @@ -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)
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/LocalRoomSummaryEntity.kt+10 1 modified
    @@ -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)
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LocalRoomSummaryEntityQueries.kt+2 6 modified
    @@ -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)
     }
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt+5 0 modified
    @@ -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"
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt+3 1 modified
    @@ -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()
         }
     }
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt+1 0 modified
    @@ -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))
                 }
             }
         }
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomFromLocalRoomTask.kt+63 72 modified
    @@ -17,133 +17,124 @@
     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
     
     /**
      * 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
             }
         }
     }
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt+9 0 modified
    @@ -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)
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt+10 1 modified
    @@ -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>) {
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt+9 0 modified
    @@ -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()
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt+2 1 modified
    @@ -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) {
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt+24 0 modified
    @@ -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)
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt+2 1 modified
    @@ -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
                             )
                         }
             }
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt+12 8 modified
    @@ -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,18 +190,22 @@ 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) }
                     ?.mapNotNull { childStateEvent -> childStateEvent.toSpaceChildInfo(spaceId, spaceChildSummary) }
                     .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")
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt+5 4 modified
    @@ -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.
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt+38 12 modified
    @@ -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 {
    
  • matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt+5 1 modified
    @@ -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) {
    
  • matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt+248 0 added
    @@ -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()
    +            )
    +        }
    +    }
    +}
    
  • matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateRoomFromLocalRoomTaskTest.kt+88 43 modified
    @@ -22,51 +22,47 @@ 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
     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,34 +87,31 @@ 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
             val params = CreateRoomFromLocalRoomTask.Params(A_LOCAL_ROOM_ID)
             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
         }
     }
    
  • matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt+5 0 modified
    @@ -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()
         }
     
    
  • matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomSummaryDataSource.kt+36 0 added
    @@ -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) }
    +    }
    +}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.