diff --git a/metadata/im.vector.app.yml b/metadata/im.vector.app.yml index 65afa82654..b5401696a0 100644 --- a/metadata/im.vector.app.yml +++ b/metadata/im.vector.app.yml @@ -981,6 +981,71 @@ Builds: - cp -r node_modules/jsc-android/dist/org/webkit/android-jsc "$HOME/.m2/repository/org/webkit" ndk: r21d + - versionName: 1.4.36-1 + versionCode: 40104370 + commit: v1.4.36 + subdir: vector-app + sudo: + - cd /opt/android-sdk/ndk + - rm -fr $(ls | sed 's/21.3.6528147//') + - curl -Lo node.tar.xz https://nodejs.org/dist/v16.17.1/node-v16.17.1-linux-x64.tar.xz + - echo "06ba2eb34aa385967f5f58c87a44753f83212f6cccea892b33f80a2e7fda8384 node.tar.xz" + | sha256sum -c - + - tar xJf node.tar.xz --strip-components=1 -C /usr/local/ + - apt-get update || apt-get update + - apt-get install -y -t stretch-backports jq openjdk-11-jdk-headless openjdk-11-jre-headless + libopus0 + - update-alternatives --auto java + - sysctl fs.inotify.max_user_watches=524288 || true + patch: + - element_android_sec_1.4.36-1.patch + - build_gradle_remove_jitsi_repo_40104160.patch + gradle: + - fdroid + srclibs: + - jitsi-meet@android-sdk-5.0.2 + - maplibre-gl-native@android-v9.5.2 + prebuild: + - sed -i -e 's/return generateVersionCodeFromVersionName()/return generateVersionCodeFromVersionName() + + 1/' build.gradle + - sed -i -e 's/versionName "${versionMajor}.${versionMinor}.${versionPatch}${getFdroidVersionSuffix()}"/versionName + "${versionMajor}.${versionMinor}.${versionPatch}${getFdroidVersionSuffix()}-1"/' + build.gradle + - sed -i -e '/applicationIdSuffix/d' -e '/^repositories {/a mavenLocal()' build.gradle + - sed -i -e 's/org.maplibre.gl/org.fdroid/' build.gradle $$maplibre-gl-native$$/platform/android/gradle/artifact-settings.gradle + - pushd $$maplibre-gl-native$$/platform/android/MapboxGLAndroidSDK + - sed -i -e '/signing {/,/}/d' -e '/signing/d' ../gradle/gradle-publish.gradle + - sed -i -e '/gmsLocation/d' build.gradle + - cd src/main/java/com/mapbox/mapboxsdk/location/engine + - sed -i -e '/isOnClasspath(GOOGLE_API_AVAILABILITY)/,/}/d' -e '/import.*gms/d' + -e 's|return isGoogle|return //isGoogle|' LocationEngineProvider.java + - rm GoogleLocationEngineImpl.java + - sed -i -e '/private.*extractGooglePlayResult/,/^ }/d' -e '/extractGooglePlayResult/d' + -e '/import.*gms/d' LocationEngineResult.java + - popd + - sed -i -e '/com.google.gms.google-services/d' build.gradle ../build.gradle + - sed -i -e 's/enable true/enable false/g' -e '/firebase.appdistribution/d' + -e '/AppDistribution {/,/}/d' build.gradle + - sed -i -e '/repositories {/a\ mavenLocal()' -e '/firebase/d' ../build.gradle + - sed -i -e '/gplayImplementation(.*) {/,/}/d; /gplayImplementation/d' build.gradle + build: + - cd $$jitsi-meet$$ + - export LIBRE_BUILD=true + - npm install --save false webpack-bundle-analyzer + - npm ci + - mkdir -p "$HOME/.m2" + - bash android/scripts/release-sdk.sh "$HOME/.m2/repository" + - mkdir -p "$HOME/.m2/repository/org/webkit" + - cp -r node_modules/jsc-android/dist/org/webkit/android-jsc "$HOME/.m2/repository/org/webkit" + - pushd $$maplibre-gl-native$$/platform/android + - git submodule update --init --recursive + - BUILDTYPE=Release make apackage + - gradle -Pmapbox.abis=all -PVERSION_NAME=9.5.2 :MapboxGLAndroidSDK:publishToMavenLocal + - popd + ndk: r21d + gradleprops: + - allWarningsAsErrors=false + MaintainerNotes: Do not update unless requested by the Element team! The Element team will ping us whenever they feel a new release is ready. See https://github.com/vector-im/element-android/issues/3728 as an example of their release process. AntiFeature NonFreeNet is added because @@ -989,5 +1054,5 @@ MaintainerNotes: Do not update unless requested by the Element team! The Element AutoUpdateMode: None UpdateCheckMode: None -CurrentVersion: 1.4.36 -CurrentVersionCode: 40104360 +CurrentVersion: 1.4.36-1 +CurrentVersionCode: 40104370 diff --git a/metadata/im.vector.app/element_android_sec_1.4.36-1.patch b/metadata/im.vector.app/element_android_sec_1.4.36-1.patch new file mode 100644 index 0000000000..0d1e3231ce --- /dev/null +++ b/metadata/im.vector.app/element_android_sec_1.4.36-1.patch @@ -0,0 +1,2240 @@ +From f926cfbae9f0d42120f4ce40d09245a075324e2d Mon Sep 17 00:00:00 2001 +Date: Wed, 28 Sep 2022 16:57:24 +0000 +Subject: [PATCH 1/1] element_android_sec_1.4.37.patch + +--- + .../src/main/res/values/strings.xml | 1 + + .../ui-styles/src/main/res/values/colors.xml | 1 + + .../android/sdk/common/CommonTestHelper.kt | 12 +- + .../android/sdk/common/CryptoTestHelper.kt | 3 +- + .../sdk/internal/crypto/E2eeSanityTests.kt | 73 +++--- + .../crypto/E2eeShareKeysHistoryTest.kt | 14 +- + .../sdk/internal/crypto/UnwedgingTest.kt | 6 +- + .../crypto/gossiping/KeyShareTests.kt | 82 ++++-- + .../crypto/gossiping/WithHeldTests.kt | 11 +- + .../android/sdk/api/crypto/MXCryptoConfig.kt | 5 +- + .../crypto/model/MXEventDecryptionResult.kt | 4 +- + .../crypto/model/OlmDecryptionResult.kt | 7 +- + .../sdk/api/session/events/model/Event.kt | 18 +- + .../internal/crypto/DefaultCryptoService.kt | 30 ++- + .../crypto/InboundGroupSessionStore.kt | 15 ++ + .../sdk/internal/crypto/MXOlmDevice.kt | 60 ++++- + .../sdk/internal/crypto/SecretShareManager.kt | 13 +- + .../crypto/algorithms/IMXDecrypting.kt | 2 +- + .../algorithms/megolm/MXMegolmDecryption.kt | 122 ++++++--- + .../megolm/MXMegolmDecryptionFactory.kt | 24 +- + .../algorithms/megolm/MXMegolmEncryption.kt | 3 +- + .../megolm/UnRequestedForwardManager.kt | 150 +++++++++++ + .../keysbackup/DefaultKeysBackupService.kt | 7 - + .../crypto/model/InboundGroupSessionData.kt | 9 +- + .../model/MXInboundMegolmSessionWrapper.kt | 1 + + .../store/db/RealmCryptoStoreMigration.kt | 4 +- + .../store/db/migration/MigrateCryptoTo018.kt | 52 ++++ + .../internal/crypto/tasks/EncryptEventTask.kt | 3 +- + .../database/helper/ThreadSummaryHelper.kt | 3 +- + .../internal/database/model/EventEntity.kt | 3 +- + .../threads/FetchThreadTimelineTask.kt | 3 +- + .../session/room/timeline/GetEventTask.kt | 3 +- + .../session/sync/handler/CryptoSyncHandler.kt | 30 ++- + .../sync/handler/room/RoomSyncHandler.kt | 6 +- + .../crypto/UnRequestedKeysManagerTest.kt | 248 ++++++++++++++++++ + .../app/core/ui/views/ShieldImageView.kt | 34 +++ + .../action/MessageActionsEpoxyController.kt | 8 + + .../edithistory/ViewEditHistoryViewModel.kt | 3 +- + .../helper/MessageInformationDataFactory.kt | 66 +++-- + .../timeline/item/AbsBaseMessageItem.kt | 13 +- + .../timeline/item/MessageInformationData.kt | 4 +- + .../notifications/NotifiableEventResolver.kt | 3 +- + .../src/main/res/drawable/ic_shield_gray.xml | 11 + + 43 files changed, 970 insertions(+), 200 deletions(-) + create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt + create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo018.kt + create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt + create mode 100644 vector/src/main/res/drawable/ic_shield_gray.xml + +diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml +index 6a87ce82f4..0b7d4b31cc 100644 +--- a/library/ui-strings/src/main/res/values/strings.xml ++++ b/library/ui-strings/src/main/res/values/strings.xml +@@ -2613,6 +2613,7 @@ + + Unencrypted + Encrypted by an unverified device ++ The authenticity of this encrypted message can\'t be guaranteed on this device. + Review where you’re logged in + Verify all your sessions to ensure your account & messages are safe + +diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml +index 01af740d43..d64056ab9c 100644 +--- a/library/ui-styles/src/main/res/values/colors.xml ++++ b/library/ui-styles/src/main/res/values/colors.xml +@@ -142,6 +142,7 @@ + + #0DBD8B + #17191C ++ #91A1C0 + #FF4B55 + #0FFF4B55 + +diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +index a78953caac..43f42a3ed4 100644 +--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt ++++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +@@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.MatrixCallback + import org.matrix.android.sdk.api.MatrixConfiguration + import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig + import org.matrix.android.sdk.api.auth.registration.RegistrationResult ++import org.matrix.android.sdk.api.crypto.MXCryptoConfig + import org.matrix.android.sdk.api.session.Session + import org.matrix.android.sdk.api.session.events.model.EventType + import org.matrix.android.sdk.api.session.events.model.toModel +@@ -61,7 +62,7 @@ import java.util.concurrent.TimeUnit + * This class exposes methods to be used in common cases + * Registration, login, Sync, Sending messages... + */ +-class CommonTestHelper internal constructor(context: Context) { ++class CommonTestHelper internal constructor(context: Context, val cryptoConfig: MXCryptoConfig? = null) { + + companion object { + internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) { +@@ -75,8 +76,10 @@ class CommonTestHelper internal constructor(context: Context) { + } + } + +- internal fun runCryptoTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CryptoTestHelper, CommonTestHelper) -> Unit) { +- val testHelper = CommonTestHelper(context) ++ internal fun runCryptoTest(context: Context, autoSignoutOnClose: Boolean = true, ++ cryptoConfig: MXCryptoConfig? = null, ++ block: (CryptoTestHelper, CommonTestHelper) -> Unit) { ++ val testHelper = CommonTestHelper(context, cryptoConfig) + val cryptoTestHelper = CryptoTestHelper(testHelper) + return try { + block(cryptoTestHelper, testHelper) +@@ -103,7 +106,8 @@ class CommonTestHelper internal constructor(context: Context) { + context, + MatrixConfiguration( + applicationFlavor = "TestFlavor", +- roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider() ++ roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider(), ++ cryptoConfig = cryptoConfig ?: MXCryptoConfig() + ) + ) + } +diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +index f36bfb6210..210ce90692 100644 +--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt ++++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +@@ -529,7 +529,8 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, +- forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain ++ forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, ++ isSafe = result.isSafe + ) + } + } catch (error: MXCryptoError) { +diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt +index f883295495..410fb4f5d4 100644 +--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt ++++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt +@@ -29,9 +29,9 @@ import org.junit.runner.RunWith + import org.junit.runners.JUnit4 + import org.junit.runners.MethodSorters + import org.matrix.android.sdk.InstrumentedTest ++import org.matrix.android.sdk.api.crypto.MXCryptoConfig + import org.matrix.android.sdk.api.session.Session + import org.matrix.android.sdk.api.session.crypto.MXCryptoError +-import org.matrix.android.sdk.api.session.crypto.RequestResult + import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion + import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult + import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo +@@ -45,7 +45,6 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic + import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction + import org.matrix.android.sdk.api.session.events.model.EventType + import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent +-import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode + import org.matrix.android.sdk.api.session.events.model.toModel + import org.matrix.android.sdk.api.session.getRoom + import org.matrix.android.sdk.api.session.room.Room +@@ -134,7 +133,8 @@ class E2eeSanityTests : InstrumentedTest { + val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!) + timeLineEvent != null && + timeLineEvent.isEncrypted() && +- timeLineEvent.root.getClearType() == EventType.MESSAGE ++ timeLineEvent.root.getClearType() == EventType.MESSAGE && ++ timeLineEvent.root.mxDecryptionResult?.isSafe == true + } + } + } +@@ -331,6 +331,15 @@ class E2eeSanityTests : InstrumentedTest { + + // ensure bob can now decrypt + cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) ++ ++ // Check key trust ++ sentEventIds.forEach { sentEventId -> ++ val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)!! ++ val result = testHelper.runBlockingTest { ++ newBobSession.cryptoService().decryptEvent(timelineEvent.root, "") ++ } ++ assertEquals("Keys from history should be deniable", false, result.isSafe) ++ } + } + + /** +@@ -379,10 +388,6 @@ class E2eeSanityTests : InstrumentedTest { + Log.v("#E2E TEST", "check that new bob can't currently decrypt") + + cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) +-// newBobSession.cryptoService().getOutgoingRoomKeyRequests() +-// .firstOrNull { +-// it.sessionId == +-// } + + // Try to request + sentEventIds.forEach { sentEventId -> +@@ -390,33 +395,30 @@ class E2eeSanityTests : InstrumentedTest { + newBobSession.cryptoService().requestRoomKeyForEvent(event) + } + +- // wait a bit +- // we need to wait a couple of syncs to let sharing occurs +-// testHelper.waitFewSyncs(newBobSession, 6) +- + // Ensure that new bob still can't decrypt (keys must have been withheld) +- sentEventIds.forEach { sentEventId -> +- val megolmSessionId = newBobSession.getRoom(e2eRoomID)!! +- .getTimelineEvent(sentEventId)!! +- .root.content.toModel()!!.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()!!.sessionId ++// testHelper.waitWithLatch { latch -> ++// testHelper.retryPeriodicallyWithLatch(latch) { ++// val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests() ++// .first { ++// it.sessionId == megolmSessionId && ++// it.roomId == e2eRoomID ++// } ++// .results.also { ++// Log.w("##TEST", "result list is $it") ++// } ++// .firstOrNull { it.userId == aliceSession.myUserId } ++// ?.result ++// aliceReply != null && ++// aliceReply is RequestResult.Failure && ++// WithHeldCode.UNAUTHORISED == aliceReply.code ++// } ++// } ++// } + + cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) + +@@ -438,7 +440,10 @@ class E2eeSanityTests : InstrumentedTest { + * Test that if a better key is forwarded (lower index, it is then used) + */ + @Test +- fun testForwardBetterKey() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> ++ fun testForwardBetterKey() = runCryptoTest( ++ context(), ++ cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) ++ ) { cryptoTestHelper, testHelper -> + + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = cryptoTestData.firstSession +diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt +index 32a95008b1..4b44aab18b 100644 +--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt ++++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt +@@ -77,6 +77,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { + */ + private fun testShareHistoryWithRoomVisibility(roomHistoryVisibility: RoomHistoryVisibility? = null) = + runCryptoTest(context()) { cryptoTestHelper, testHelper -> ++ val aliceMessageText = "Hello Bob, I am Alice!" + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, roomHistoryVisibility) + + val e2eRoomID = cryptoTestData.roomId +@@ -96,7 +97,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { + assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2) + Log.v("#E2E TEST", "Alice and Bob are in roomId: $e2eRoomID") + +- val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, "Hello Bob, I am Alice!", testHelper) ++ val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, aliceMessageText, testHelper) + Assert.assertTrue("Message should be sent", aliceMessageId != null) + Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID") + +@@ -106,7 +107,8 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { + val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) + (timelineEvent != null && + timelineEvent.isEncrypted() && +- timelineEvent.root.getClearType() == EventType.MESSAGE).also { ++ timelineEvent.root.getClearType() == EventType.MESSAGE && ++ timelineEvent.root.mxDecryptionResult?.isSafe == true).also { + if (it) { + Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") + } +@@ -142,7 +144,8 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { + val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) + (timelineEvent != null && + timelineEvent.isEncrypted() && +- timelineEvent.root.getClearType() == EventType.MESSAGE ++ timelineEvent.root.getClearType() == EventType.MESSAGE && ++ timelineEvent.root.mxDecryptionResult?.isSafe == false + ).also { + if (it) { + Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") +@@ -377,7 +380,10 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { + } + + private fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? { +- return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.eventId ++ return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.let { ++ Log.v("#E2E TEST", "Message sent with session ${it.root.content?.get("session_id")}") ++ return it.eventId ++ } + } + + private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List, e2eRoomID: String, testHelper: CommonTestHelper) { +diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt +index 5fe7376184..130c8d13f9 100644 +--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt ++++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt +@@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth + import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor + import org.matrix.android.sdk.api.auth.UserPasswordAuth + import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse ++import org.matrix.android.sdk.api.crypto.MXCryptoConfig + import org.matrix.android.sdk.api.extensions.tryOrNull + import org.matrix.android.sdk.api.session.crypto.MXCryptoError + import org.matrix.android.sdk.api.session.events.model.EventType +@@ -82,7 +83,10 @@ class UnwedgingTest : InstrumentedTest { + * -> This is automatically fixed after SDKs restarted the olm session + */ + @Test +- fun testUnwedging() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> ++ fun testUnwedging() = runCryptoTest( ++ context(), ++ cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) ++ ) { cryptoTestHelper, testHelper -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession +diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt +index 7bb53e139c..df0b10ea6d 100644 +--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt ++++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt +@@ -22,15 +22,16 @@ import androidx.test.filters.LargeTest + import junit.framework.TestCase.assertNotNull + import junit.framework.TestCase.assertTrue + import org.amshove.kluent.internal.assertEquals ++import org.amshove.kluent.shouldBeEqualTo + import org.junit.Assert + import org.junit.Assert.assertNull + import org.junit.FixMethodOrder +-import org.junit.Ignore +-import org.junit.Rule + import org.junit.Test + import org.junit.runner.RunWith + import org.junit.runners.MethodSorters + import org.matrix.android.sdk.InstrumentedTest ++import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM ++import org.matrix.android.sdk.api.crypto.MXCryptoConfig + import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState + import org.matrix.android.sdk.api.session.crypto.RequestResult + import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel +@@ -43,7 +44,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility + import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams + import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent + import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest +-import org.matrix.android.sdk.common.RetryTestRule + import org.matrix.android.sdk.common.SessionTestParams + import org.matrix.android.sdk.common.TestConstants + import org.matrix.android.sdk.mustFail +@@ -51,16 +51,15 @@ import org.matrix.android.sdk.mustFail + @RunWith(AndroidJUnit4::class) + @FixMethodOrder(MethodSorters.JVM) + @LargeTest +-@Ignore + class KeyShareTests : InstrumentedTest { + +- @get:Rule val rule = RetryTestRule(3) ++ // @get:Rule val rule = RetryTestRule(3) + + @Test + fun test_DoNotSelfShareIfNotTrusted() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + + val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) +- Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}") ++ Log.v("#TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}") + + // Create an encrypted room and add a message + val roomId = commonTestHelper.runBlockingTest { +@@ -86,7 +85,7 @@ class KeyShareTests : InstrumentedTest { + aliceSession2.cryptoService().enableKeyGossiping(false) + commonTestHelper.syncSession(aliceSession2) + +- Log.v("TEST", "=======> AliceSession 2 is ${aliceSession2.sessionParams.deviceId}") ++ Log.v("#TEST", "=======> AliceSession 2 is ${aliceSession2.sessionParams.deviceId}") + + val roomSecondSessionPOV = aliceSession2.getRoom(roomId) + +@@ -121,7 +120,7 @@ class KeyShareTests : InstrumentedTest { + } + } + } +- Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId") ++ Log.v("#TEST", "=======> Outgoing requet Id is $outGoingRequestId") + + val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequests() + +@@ -134,14 +133,17 @@ class KeyShareTests : InstrumentedTest { + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + // DEBUG LOGS +-// aliceSession.cryptoService().getIncomingRoomKeyRequests().let { +-// Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)") +-// Log.v("TEST", "=========================") +-// it.forEach { keyRequest -> +-// Log.v("TEST", "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") +-// } +-// Log.v("TEST", "=========================") +-// } ++ aliceSession.cryptoService().getIncomingRoomKeyRequests().let { ++ Log.v("#TEST", "Incoming request Session 1 (looking for $outGoingRequestId)") ++ Log.v("#TEST", "=========================") ++ it.forEach { keyRequest -> ++ Log.v( ++ "#TEST", ++ "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}" ++ ) ++ } ++ Log.v("#TEST", "=========================") ++ } + + val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } + incoming != null +@@ -152,10 +154,10 @@ class KeyShareTests : InstrumentedTest { + commonTestHelper.retryPeriodicallyWithLatch(latch) { + // DEBUG LOGS + aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest -> +- Log.v("TEST", "=========================") +- Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") +- Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") +- Log.v("TEST", "=========================") ++ Log.v("#TEST", "=========================") ++ Log.v("#TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") ++ Log.v("#TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") ++ Log.v("#TEST", "=========================") + } + + val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } +@@ -172,11 +174,24 @@ class KeyShareTests : InstrumentedTest { + } + + // Mark the device as trusted ++ ++ Log.v("#TEST", "=======> Alice device 1 is ${aliceSession.sessionParams.deviceId}|${aliceSession.cryptoService().getMyDevice().identityKey()}") ++ val aliceSecondSession = aliceSession2.cryptoService().getMyDevice() ++ Log.v("#TEST", "=======> Alice device 2 is ${aliceSession2.sessionParams.deviceId}|${aliceSecondSession.identityKey()}") ++ + aliceSession.cryptoService().setDeviceVerification( + DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId, + aliceSession2.sessionParams.deviceId ?: "" + ) + ++ // We only accept forwards from trusted session, so we need to trust on other side to ++ aliceSession2.cryptoService().setDeviceVerification( ++ DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId, ++ aliceSession.sessionParams.deviceId ?: "" ++ ) ++ ++ aliceSession.cryptoService().deviceWithIdentityKey(aliceSecondSession.identityKey()!!, MXCRYPTO_ALGORITHM_OLM)!!.isVerified shouldBeEqualTo true ++ + // Re request + aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root) + +@@ -193,7 +208,10 @@ class KeyShareTests : InstrumentedTest { + * if the key was originally shared with him + */ + @Test +- fun test_reShareIfWasIntendedToBeShared() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> ++ fun test_reShareIfWasIntendedToBeShared() = runCryptoTest( ++ context(), ++ cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) ++ ) { cryptoTestHelper, commonTestHelper -> + + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = testData.firstSession +@@ -224,7 +242,10 @@ class KeyShareTests : InstrumentedTest { + * if the key was originally shared with him + */ + @Test +- fun test_reShareToUnverifiedIfWasIntendedToBeShared() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> ++ fun test_reShareToUnverifiedIfWasIntendedToBeShared() = runCryptoTest( ++ context(), ++ cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) ++ ) { cryptoTestHelper, commonTestHelper -> + + val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true) + val aliceSession = testData.firstSession +@@ -242,7 +263,6 @@ class KeyShareTests : InstrumentedTest { + } + val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first() + val sentEventMegolmSession = sentEvent.root.content.toModel()!!.sessionId!! +- + // Let's try to request any how. + // As it was share previously alice should accept to reshare + aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root) +@@ -261,7 +281,10 @@ class KeyShareTests : InstrumentedTest { + * Tests that keys reshared with own verified session are done from the earliest known index + */ + @Test +- fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> ++ fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() = runCryptoTest( ++ context(), ++ cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) ++ ) { cryptoTestHelper, commonTestHelper -> + + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = testData.firstSession +@@ -333,6 +356,9 @@ class KeyShareTests : InstrumentedTest { + aliceSession.cryptoService() + .verificationService() + .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!) ++ aliceNewSession.cryptoService() ++ .verificationService() ++ .markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!) + + // Let's now try to request + aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root) +@@ -381,7 +407,10 @@ class KeyShareTests : InstrumentedTest { + * Tests that we don't cancel a request to early on first forward if the index is not good enough + */ + @Test +- fun test_dontCancelToEarly() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> ++ fun test_dontCancelToEarly() = runCryptoTest( ++ context(), ++ cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) ++ ) { cryptoTestHelper, commonTestHelper -> + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = testData.firstSession + val bobSession = testData.secondSession!! +@@ -421,6 +450,9 @@ class KeyShareTests : InstrumentedTest { + aliceSession.cryptoService() + .verificationService() + .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!) ++ aliceNewSession.cryptoService() ++ .verificationService() ++ .markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!) + + // /!\ Stop initial alice session syncing so that it can't reply + aliceSession.cryptoService().enableKeyGossiping(false) +diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt +index 0aac4297e4..910a349b40 100644 +--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt ++++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt +@@ -27,6 +27,7 @@ import org.junit.runner.RunWith + import org.junit.runners.MethodSorters + import org.matrix.android.sdk.InstrumentedTest + import org.matrix.android.sdk.api.NoOpMatrixCallback ++import org.matrix.android.sdk.api.crypto.MXCryptoConfig + import org.matrix.android.sdk.api.extensions.tryOrNull + import org.matrix.android.sdk.api.session.crypto.MXCryptoError + import org.matrix.android.sdk.api.session.crypto.RequestResult +@@ -153,7 +154,10 @@ class WithHeldTests : InstrumentedTest { + } + + @Test +- fun test_WithHeldNoOlm() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> ++ fun test_WithHeldNoOlm() = runCryptoTest( ++ context(), ++ cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) ++ ) { cryptoTestHelper, testHelper -> + + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + val aliceSession = testData.firstSession +@@ -233,7 +237,10 @@ class WithHeldTests : InstrumentedTest { + } + + @Test +- fun test_WithHeldKeyRequest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> ++ fun test_WithHeldKeyRequest() = runCryptoTest( ++ context(), ++ cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) ++ ) { cryptoTestHelper, testHelper -> + + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + val aliceSession = testData.firstSession +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt +index 015cb6a1a2..38f522586f 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt +@@ -35,8 +35,9 @@ data class MXCryptoConfig constructor( + + /** + * Currently megolm keys are requested to the sender device and to all of our devices. +- * You can limit request only to your sessions by turning this setting to `true` ++ * You can limit request only to your sessions by turning this setting to `true`. ++ * Forwarded keys coming from other users will also be ignored if set to true. + */ +- val limitRoomKeyRequestsToMyDevices: Boolean = false, ++ val limitRoomKeyRequestsToMyDevices: Boolean = true, + + ) +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt +index 0a0ccc2db3..66d7558fe2 100755 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt +@@ -43,5 +43,7 @@ data class MXEventDecryptionResult( + * List of curve25519 keys involved in telling us about the senderCurve25519Key and + * claimedEd25519Key. See MXEvent.forwardingCurve25519KeyChain. + */ +- val forwardingCurve25519KeyChain: List = emptyList() ++ val forwardingCurve25519KeyChain: List = emptyList(), ++ ++ val isSafe: Boolean = false + ) +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt +index a26f6606ed..6d57318f87 100755 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt +@@ -44,5 +44,10 @@ data class OlmDecryptionResult( + /** + * Devices which forwarded this session to us (normally empty). + */ +- @Json(name = "forwardingCurve25519KeyChain") val forwardingCurve25519KeyChain: List? = null ++ @Json(name = "forwardingCurve25519KeyChain") val forwardingCurve25519KeyChain: List? = null, ++ ++ /** ++ * True if the key used to decrypt is considered safe (trusted). ++ */ ++ @Json(name = "key_safety") val isSafe: Boolean? = null, + ) +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +index 59dc6c434d..f5d2c0d9a0 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +@@ -174,15 +174,29 @@ data class Event( + * @return the event type + */ + fun getClearType(): String { +- return mxDecryptionResult?.payload?.get("type")?.toString() ?: type ?: EventType.MISSING_TYPE ++ return getDecryptedType() ?: type ?: EventType.MISSING_TYPE ++ } ++ ++ /** ++ * @return The decrypted type, or null. Won't fallback to the wired type ++ */ ++ fun getDecryptedType(): String? { ++ return mxDecryptionResult?.payload?.get("type")?.toString() + } + + /** + * @return the event content + */ + fun getClearContent(): Content? { ++ return getDecryptedContent() ?: content ++ } ++ ++ /** ++ * @return the decrypted event content or null, Won't fallback to the wired content ++ */ ++ fun getDecryptedContent(): Content? { + @Suppress("UNCHECKED_CAST") +- return mxDecryptionResult?.payload?.get("content") as? Content ?: content ++ return mxDecryptionResult?.payload?.get("content") as? Content + } + + fun toContentStringWithIndent(): String { +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +index 8dd7c309c6..322f297ac3 100755 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +@@ -79,6 +79,7 @@ import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationActio + import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting + import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption + import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory ++import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager + import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory + import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService + import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService +@@ -183,7 +184,8 @@ internal class DefaultCryptoService @Inject constructor( + private val cryptoCoroutineScope: CoroutineScope, + private val eventDecryptor: EventDecryptor, + private val verificationMessageProcessor: VerificationMessageProcessor, +- private val liveEventManager: Lazy ++ private val liveEventManager: Lazy, ++ 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() ?: 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() ?: return ++ Timber.tag(loggerTag.value).i("onRoomKeyEvent(forceAccept:$acceptUnrequested) from: ${event.senderId} type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>") + if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { + Timber.tag(loggerTag.value).e("onRoomKeyEvent() : missing fields") + return +@@ -857,7 +868,7 @@ internal class DefaultCryptoService @Inject constructor( + Timber.tag(loggerTag.value).e("GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") + return + } +- alg.onRoomKeyEvent(event, keysBackupService) ++ alg.onRoomKeyEvent(event, keysBackupService, acceptUnrequested) + } + + private fun onKeyWithHeldReceived(event: Event) { +@@ -950,6 +961,15 @@ internal class DefaultCryptoService @Inject constructor( + * @param event the membership event causing the change + */ + private fun onRoomMembershipEvent(roomId: String, event: Event) { ++ // because the encryption event can be after the join/invite in the same batch ++ event.stateKey?.let { _ -> ++ val roomMember: RoomMemberContent? = event.content.toModel() ++ val membership = roomMember?.membership ++ if (membership == Membership.INVITE) { ++ unrequestedForwardManager.onInviteReceived(roomId, event.senderId.orEmpty(), clock.epochMillis()) ++ } ++ } ++ + roomEncryptorsStore.get(roomId) ?: /* No encrypting in this room */ return + + event.stateKey?.let { userId -> +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt +index 39dfb72149..6d197a09ed 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt +@@ -91,6 +91,21 @@ internal class InboundGroupSessionStore @Inject constructor( + internalStoreGroupSession(new, sessionId, senderKey) + } + ++ @Synchronized ++ fun updateToSafe(old: InboundGroupSessionHolder, sessionId: String, senderKey: String) { ++ Timber.tag(loggerTag.value).v("## updateToSafe for session ${old.wrapper.roomId}-${old.wrapper.senderKey}") ++ ++ store.storeInboundGroupSessions( ++ listOf( ++ old.wrapper.copy( ++ sessionData = old.wrapper.sessionData.copy(trusted = true) ++ ) ++ ) ++ ) ++ // will release it :/ ++ sessionCache.remove(CacheKey(sessionId, senderKey)) ++ } ++ + @Synchronized + fun storeInBoundGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) { + internalStoreGroupSession(holder, sessionId, senderKey) +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +index 96ccba51dc..b6a5136b8f 100755 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto + import androidx.annotation.VisibleForTesting + import kotlinx.coroutines.sync.Mutex + import kotlinx.coroutines.sync.withLock ++import org.matrix.android.sdk.api.extensions.orFalse + import org.matrix.android.sdk.api.extensions.tryOrNull + import org.matrix.android.sdk.api.logger.LoggerTag + import org.matrix.android.sdk.api.session.crypto.MXCryptoError +@@ -612,7 +613,8 @@ internal class MXOlmDevice @Inject constructor( + forwardingCurve25519KeyChain: List, + keysClaimed: Map, + exportFormat: Boolean, +- sharedHistory: Boolean ++ sharedHistory: Boolean, ++ trusted: Boolean + ): AddSessionResult { + val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") { + if (exportFormat) { +@@ -620,6 +622,8 @@ internal class MXOlmDevice @Inject constructor( + } else { + OlmInboundGroupSession(sessionKey) + } ++ } ?: return AddSessionResult.NotImported.also { ++ Timber.tag(loggerTag.value).d("## addInboundGroupSession() : failed to import key candidate $senderKey/$sessionId") + } + + val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } +@@ -631,31 +635,49 @@ internal class MXOlmDevice @Inject constructor( + val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex } ?: return AddSessionResult.NotImported.also { + // This is quite unexpected, could throw if native was released? + Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session") +- candidateSession?.releaseSession() ++ candidateSession.releaseSession() + // Probably should discard it? + } +- val newKnownFirstIndex = tryOrNull("Failed to get candidate first known index") { candidateSession?.firstKnownIndex } ++ 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 (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { ++ 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() ++ 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 ") +- return AddSessionResult.NotImported +- } +- + try { + if (candidateSession.sessionIdentifier() != sessionId) { + Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") +@@ -674,6 +696,7 @@ internal class MXOlmDevice @Inject constructor( + keysClaimed = keysClaimed, + forwardingCurve25519KeyChain = forwardingCurve25519KeyChain, + sharedHistory = sharedHistory, ++ trusted = trusted + ) + + val wrapper = MXInboundMegolmSessionWrapper( +@@ -689,6 +712,16 @@ internal class MXOlmDevice @Inject constructor( + return AddSessionResult.Imported(candidateSession.firstKnownIndex.toInt()) + } + ++ fun OlmInboundGroupSession.connects(other: OlmInboundGroupSession): Boolean { ++ return try { ++ val lowestCommonIndex = this.firstKnownIndex.coerceAtLeast(other.firstKnownIndex) ++ this.export(lowestCommonIndex) == other.export(lowestCommonIndex) ++ } catch (failure: Throwable) { ++ // native error? key disposed? ++ false ++ } ++ } ++ + /** + * Import an inbound group sessions to the session store. + * +@@ -821,7 +854,8 @@ internal class MXOlmDevice @Inject constructor( + payload, + wrapper.sessionData.keysClaimed, + senderKey, +- wrapper.sessionData.forwardingCurve25519KeyChain ++ wrapper.sessionData.forwardingCurve25519KeyChain, ++ isSafe = sessionHolder.wrapper.sessionData.trusted.orFalse() + ) + } + +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt +index a79e1a8901..5691f24d17 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt +@@ -267,13 +267,24 @@ internal class SecretShareManager @Inject constructor( + Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event") + return + } ++ // no need to download keys, after a verification we already forced download ++ val sendingDevice = toDevice.getSenderKey()?.let { cryptoStore.deviceWithIdentityKey(it) } ++ if (sendingDevice == null) { ++ Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from unknown device ${toDevice.getSenderKey()}") ++ return ++ } + + // Was that sent by us? +- if (toDevice.senderId != credentials.userId) { ++ if (sendingDevice.userId != credentials.userId) { + Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}") + return + } + ++ if (!sendingDevice.isVerified) { ++ Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from untrusted device ${toDevice.getSenderKey()}") ++ return ++ } ++ + val secretContent = toDevice.getClearContent().toModel() ?: return + + val existingRequest = verifMutex.withLock { +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt +index 6847a46369..e2ddd5d19f 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt +@@ -42,5 +42,5 @@ internal interface IMXDecrypting { + * @param event the key event. + * @param defaultKeysBackupService the keys backup service + */ +- fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {} ++ fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean = false) {} + } +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +index 410b74e19f..5354cbff3b 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +@@ -17,7 +17,8 @@ + package org.matrix.android.sdk.internal.crypto.algorithms.megolm + + import dagger.Lazy +-import org.matrix.android.sdk.api.MatrixConfiguration ++import org.matrix.android.sdk.api.crypto.MXCryptoConfig ++import org.matrix.android.sdk.api.extensions.orFalse + import org.matrix.android.sdk.api.logger.LoggerTag + import org.matrix.android.sdk.api.session.crypto.MXCryptoError + import org.matrix.android.sdk.api.session.crypto.NewSessionListener +@@ -34,16 +35,20 @@ import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting + import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService + import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore + import org.matrix.android.sdk.internal.session.StreamEventsManager ++import org.matrix.android.sdk.internal.util.time.Clock + import timber.log.Timber + + private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO) + + internal class MXMegolmDecryption( + private val olmDevice: MXOlmDevice, ++ private val myUserId: String, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, + private val cryptoStore: IMXCryptoStore, +- private val matrixConfiguration: MatrixConfiguration, +- private val liveEventManager: Lazy ++ private val liveEventManager: Lazy, ++ private val unrequestedForwardManager: UnRequestedForwardManager, ++ private val cryptoConfig: MXCryptoConfig, ++ private val clock: Clock, + ) : IMXDecrypting { + + var newSessionListener: NewSessionListener? = null +@@ -94,7 +99,8 @@ internal class MXMegolmDecryption( + senderCurve25519Key = olmDecryptionResult.senderKey, + claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"), + forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain +- .orEmpty() ++ .orEmpty(), ++ isSafe = olmDecryptionResult.isSafe.orFalse() + ).also { + liveEventManager.get().dispatchLiveEventDecrypted(event, it) + } +@@ -182,12 +188,21 @@ internal class MXMegolmDecryption( + * @param event the key event. + * @param defaultKeysBackupService the keys backup service + */ +- override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) { +- Timber.tag(loggerTag.value).v("onRoomKeyEvent()") ++ override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean) { ++ Timber.tag(loggerTag.value).v("onRoomKeyEvent(${event.getSenderKey()})") + var exportFormat = false +- val roomKeyContent = event.getClearContent().toModel() ?: return ++ val roomKeyContent = event.getDecryptedContent()?.toModel() ?: 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 = HashMap() + val forwardingCurve25519KeyChain: MutableList = ArrayList() + +@@ -195,32 +210,25 @@ internal class MXMegolmDecryption( + Timber.tag(loggerTag.value).e("onRoomKeyEvent() : Key event is missing fields") + return + } +- if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { ++ if (event.getDecryptedType() == EventType.FORWARDED_ROOM_KEY) { + if (!cryptoStore.isKeyGossipingEnabled()) { + Timber.tag(loggerTag.value) + .i("onRoomKeyEvent(), ignore forward adding as per crypto config : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") + return + } + Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") +- val forwardedRoomKeyContent = event.getClearContent().toModel() ++ val forwardedRoomKeyContent = event.getDecryptedContent()?.toModel() + ?: return + + forwardedRoomKeyContent.forwardingCurve25519KeyChain?.let { + forwardingCurve25519KeyChain.addAll(it) + } + +- if (senderKey == null) { +- Timber.tag(loggerTag.value).e("onRoomKeyEvent() : event is missing sender_key field") +- return +- } +- +- forwardingCurve25519KeyChain.add(senderKey) ++ forwardingCurve25519KeyChain.add(eventSenderKey) + + exportFormat = true +- senderKey = forwardedRoomKeyContent.senderKey +- if (null == senderKey) { ++ sessionInitiatorSenderKey = forwardedRoomKeyContent.senderKey ?: return Unit.also { + Timber.tag(loggerTag.value).e("onRoomKeyEvent() : forwarded_room_key event is missing sender_key field") +- return + } + + if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) { +@@ -229,13 +237,51 @@ internal class MXMegolmDecryption( + } + + keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key +- } else { +- Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") +- if (null == senderKey) { +- Timber.tag(loggerTag.value).e("## onRoomKeyEvent() : key event has no sender key (not encrypted?)") ++ ++ // checking if was requested once. ++ // should we check if the request is sort of active? ++ val wasNotRequested = cryptoStore.getOutgoingRoomKeyRequest( ++ roomId = forwardedRoomKeyContent.roomId.orEmpty(), ++ sessionId = forwardedRoomKeyContent.sessionId.orEmpty(), ++ algorithm = forwardedRoomKeyContent.algorithm.orEmpty(), ++ senderKey = forwardedRoomKeyContent.senderKey.orEmpty(), ++ ).isEmpty() ++ ++ trusted = false ++ ++ if (!forceAccept && wasNotRequested) { ++// val senderId = cryptoStore.deviceWithIdentityKey(event.getSenderKey().orEmpty())?.userId.orEmpty() ++ unrequestedForwardManager.onUnRequestedKeyForward(roomKeyContent.roomId, event, clock.epochMillis()) ++ // Ignore unsolicited ++ Timber.tag(loggerTag.value).w("Ignoring forwarded_room_key_event for ${roomKeyContent.sessionId} that was not requested") ++ return ++ } ++ ++ // Check who sent the request, as we requested we have the device keys (no need to download) ++ val sessionThatIsSharing = cryptoStore.deviceWithIdentityKey(eventSenderKey) ++ if (sessionThatIsSharing == null) { ++ Timber.tag(loggerTag.value).w("Ignoring forwarded_room_key from unknown device with identity $eventSenderKey") + return + } ++ val isOwnDevice = myUserId == sessionThatIsSharing.userId ++ val isDeviceVerified = sessionThatIsSharing.isVerified ++ val isFromSessionInitiator = sessionThatIsSharing.identityKey() == sessionInitiatorSenderKey ++ ++ val isLegitForward = (isOwnDevice && isDeviceVerified) || ++ (!cryptoConfig.limitRoomKeyRequestsToMyDevices && isFromSessionInitiator) + ++ val shouldAcceptForward = forceAccept || isLegitForward ++ ++ if (!shouldAcceptForward) { ++ Timber.tag(loggerTag.value) ++ .w("Ignoring forwarded_room_key device:$eventSenderKey, ownVerified:{$isOwnDevice&&$isDeviceVerified}, fromInitiator:$isFromSessionInitiator") ++ return ++ } ++ } else { ++ // It's a m.room_key so safe ++ trusted = true ++ sessionInitiatorSenderKey = eventSenderKey ++ Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") + // inherit the claimed ed25519 key from the setup message + keysClaimed = event.getKeysClaimed().toMutableMap() + } +@@ -245,12 +291,15 @@ internal class MXMegolmDecryption( + sessionId = roomKeyContent.sessionId, + sessionKey = roomKeyContent.sessionKey, + roomId = roomKeyContent.roomId, +- senderKey = senderKey, ++ senderKey = sessionInitiatorSenderKey, + forwardingCurve25519KeyChain = forwardingCurve25519KeyChain, + keysClaimed = keysClaimed, + exportFormat = exportFormat, +- sharedHistory = roomKeyContent.getSharedKey() +- ) ++ sharedHistory = roomKeyContent.getSharedKey(), ++ trusted = trusted ++ ).also { ++ Timber.tag(loggerTag.value).v(".. onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId} result: $it") ++ } + + when (addSessionResult) { + is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex +@@ -258,35 +307,28 @@ internal class MXMegolmDecryption( + else -> null + }?.let { index -> + if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { +- val fromDevice = (event.content?.get("sender_key") as? String)?.let { senderDeviceIdentityKey -> +- cryptoStore.getUserDeviceList(event.senderId ?: "") +- ?.firstOrNull { +- it.identityKey() == senderDeviceIdentityKey +- } +- }?.deviceId +- + outgoingKeyRequestManager.onRoomKeyForwarded( + sessionId = roomKeyContent.sessionId, + algorithm = roomKeyContent.algorithm ?: "", + roomId = roomKeyContent.roomId, +- senderKey = senderKey, ++ senderKey = sessionInitiatorSenderKey, + fromIndex = index, +- fromDevice = fromDevice, ++ fromDevice = fromDevice?.deviceId, + event = event + ) + + cryptoStore.saveIncomingForwardKeyAuditTrail( + roomId = roomKeyContent.roomId, + sessionId = roomKeyContent.sessionId, +- senderKey = senderKey, ++ senderKey = sessionInitiatorSenderKey, + algorithm = roomKeyContent.algorithm ?: "", +- userId = event.senderId ?: "", +- deviceId = fromDevice ?: "", ++ userId = event.senderId.orEmpty(), ++ deviceId = fromDevice?.deviceId.orEmpty(), + chainIndex = index.toLong() + ) + + // The index is used to decide if we cancel sent request or if we wait for a better key +- outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, senderKey, index) ++ outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, sessionInitiatorSenderKey, index) + } + } + +@@ -295,7 +337,7 @@ internal class MXMegolmDecryption( + .d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}") + defaultKeysBackupService.maybeBackupKeys() + +- onNewSession(roomKeyContent.roomId, senderKey, roomKeyContent.sessionId) ++ onNewSession(roomKeyContent.roomId, sessionInitiatorSenderKey, roomKeyContent.sessionId) + } + } + +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +index 38edbb7430..99f8bc69e0 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +@@ -17,28 +17,36 @@ + package org.matrix.android.sdk.internal.crypto.algorithms.megolm + + import dagger.Lazy +-import org.matrix.android.sdk.api.MatrixConfiguration ++import org.matrix.android.sdk.api.crypto.MXCryptoConfig + import org.matrix.android.sdk.internal.crypto.MXOlmDevice + import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager + import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore ++import org.matrix.android.sdk.internal.di.UserId + import org.matrix.android.sdk.internal.session.StreamEventsManager ++import org.matrix.android.sdk.internal.util.time.Clock + import javax.inject.Inject + + internal class MXMegolmDecryptionFactory @Inject constructor( + private val olmDevice: MXOlmDevice, ++ @UserId private val myUserId: String, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, + private val cryptoStore: IMXCryptoStore, +- private val matrixConfiguration: MatrixConfiguration, +- private val eventsManager: Lazy ++ private val eventsManager: Lazy, ++ private val unrequestedForwardManager: UnRequestedForwardManager, ++ private val mxCryptoConfig: MXCryptoConfig, ++ private val clock: Clock, + ) { + + fun create(): MXMegolmDecryption { + return MXMegolmDecryption( +- olmDevice, +- outgoingKeyRequestManager, +- cryptoStore, +- matrixConfiguration, +- eventsManager ++ olmDevice = olmDevice, ++ myUserId = myUserId, ++ outgoingKeyRequestManager = outgoingKeyRequestManager, ++ cryptoStore = cryptoStore, ++ liveEventManager = eventsManager, ++ unrequestedForwardManager = unrequestedForwardManager, ++ cryptoConfig = mxCryptoConfig, ++ clock = clock, + ) + } + } +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +index 771b5f9a62..fca6fab66c 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +@@ -162,7 +162,8 @@ internal class MXMegolmEncryption( + forwardingCurve25519KeyChain = emptyList(), + keysClaimed = keysClaimedMap, + exportFormat = false, +- sharedHistory = sharedHistory ++ sharedHistory = sharedHistory, ++ trusted = true + ) + + defaultKeysBackupService.maybeBackupKeys() +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt +new file mode 100644 +index 0000000000..42629b617e +--- /dev/null ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt +@@ -0,0 +1,150 @@ ++/* ++ * Copyright 2022 The Matrix.org Foundation C.I.C. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); ++ * you may not use this file except in compliance with the License. ++ * You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software ++ * distributed under the License is distributed on an "AS IS" BASIS, ++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ * See the License for the specific language governing permissions and ++ * limitations under the License. ++ */ ++ ++package org.matrix.android.sdk.internal.crypto.algorithms.megolm ++ ++import kotlinx.coroutines.CoroutineScope ++import kotlinx.coroutines.SupervisorJob ++import kotlinx.coroutines.asCoroutineDispatcher ++import kotlinx.coroutines.cancel ++import kotlinx.coroutines.launch ++import org.matrix.android.sdk.api.extensions.tryOrNull ++import org.matrix.android.sdk.api.session.events.model.Event ++import org.matrix.android.sdk.internal.crypto.DeviceListManager ++import org.matrix.android.sdk.internal.session.SessionScope ++import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer ++import timber.log.Timber ++import java.util.concurrent.Executors ++import javax.inject.Inject ++import kotlin.math.abs ++ ++private val INVITE_VALIDITY_TIME_WINDOW_MILLIS = 10 * 60_000 ++ ++@SessionScope ++internal class UnRequestedForwardManager @Inject constructor( ++ private val deviceListManager: DeviceListManager, ++) { ++ ++ private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() ++ private val scope = CoroutineScope(SupervisorJob() + dispatcher) ++ private val sequencer = SemaphoreCoroutineSequencer() ++ ++ // For now only in memory storage. Maybe we should persist? in case of gappy sync and long catchups? ++ private val forwardedKeysPerRoom = mutableMapOf>>() ++ ++ 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() ++ ++ 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) -> Unit) { ++ scope.launch { ++ sequencer.post { ++ // Prune outdated invites ++ recentInvites.removeAll { currentTimestamp - it.timestamp > INVITE_VALIDITY_TIME_WINDOW_MILLIS } ++ val cleanUpEvents = mutableListOf>() ++ forwardedKeysPerRoom.forEach { (roomId, senderIdToForwardMap) -> ++ senderIdToForwardMap.forEach { (senderId, eventList) -> ++ // is there a matching invite in a valid timewindow? ++ val matchingInvite = recentInvites.firstOrNull { it.fromMxId == senderId && it.roomId == roomId } ++ if (matchingInvite != null) { ++ Timber.v("match for room:$roomId from sender:$senderId -> count =${eventList.size}") ++ ++ eventList.filter { ++ abs(matchingInvite.timestamp - it.timestamp) <= INVITE_VALIDITY_TIME_WINDOW_MILLIS ++ }.map { ++ it.event ++ }.takeIf { it.isNotEmpty() }?.let { ++ Timber.w("Re-processing forwarded_room_key_event that was not requested after invite") ++ scope.launch { ++ handleForwards.invoke(it) ++ } ++ } ++ cleanUpEvents.add(roomId to senderId) ++ } ++ } ++ } ++ ++ cleanUpEvents.forEach { roomIdToSenderPair -> ++ forwardedKeysPerRoom[roomIdToSenderPair.first]?.get(roomIdToSenderPair.second)?.clear() ++ } ++ } ++ } ++ } ++} +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt +index 8691c08779..e8700b7809 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt +@@ -652,14 +652,7 @@ internal class DefaultKeysBackupService @Inject constructor( + } + val recoveryKey = computeRecoveryKey(secret.fromBase64()) + if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) { +- awaitCallback { +- trustKeysBackupVersion(keysBackupVersion, true, it) +- } + // we don't want to start immediately downloading all as it can take very long +- +-// val importResult = awaitCallback { +-// restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it) +-// } + withContext(coroutineDispatchers.crypto) { + cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version) + } +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt +index 2ce36aa209..15e8ba835b 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt +@@ -38,9 +38,6 @@ data class InboundGroupSessionData( + @Json(name = "forwarding_curve25519_key_chain") + var forwardingCurve25519KeyChain: List? = emptyList(), + +- /** Not yet used, will be in backup v2 +- val untrusted?: Boolean = false */ +- + /** + * Flag that indicates whether or not the current inboundSession will be shared to + * invited users to decrypt past messages. +@@ -48,4 +45,10 @@ data class InboundGroupSessionData( + @Json(name = "shared_history") + val sharedHistory: Boolean = false, + ++ /** ++ * Flag indicating that this key is trusted. ++ */ ++ @Json(name = "trusted") ++ val trusted: Boolean? = null, ++ + ) +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt +index 2772b34835..2c6a0a967a 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt +@@ -86,6 +86,7 @@ data class MXInboundMegolmSessionWrapper( + keysClaimed = megolmSessionData.senderClaimedKeys, + forwardingCurve25519KeyChain = megolmSessionData.forwardingCurve25519KeyChain, + sharedHistory = megolmSessionData.sharedHistory, ++ trusted = false + ) + + return MXInboundMegolmSessionWrapper( +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt +index c36d572da6..426d50a54f 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt +@@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo + import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015 + import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016 + import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017 ++import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo018 + import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration + import org.matrix.android.sdk.internal.util.time.Clock + import javax.inject.Inject +@@ -48,7 +49,7 @@ internal class RealmCryptoStoreMigration @Inject constructor( + private val clock: Clock, + ) : MatrixRealmMigration( + dbName = "Crypto", +- schemaVersion = 17L, ++ schemaVersion = 18L, + ) { + /** + * Forces all RealmCryptoStoreMigration instances to be equal. +@@ -75,5 +76,6 @@ internal class RealmCryptoStoreMigration @Inject constructor( + if (oldVersion < 15) MigrateCryptoTo015(realm).perform() + if (oldVersion < 16) MigrateCryptoTo016(realm).perform() + if (oldVersion < 17) MigrateCryptoTo017(realm).perform() ++ if (oldVersion < 18) MigrateCryptoTo018(realm).perform() + } + } +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo018.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo018.kt +new file mode 100644 +index 0000000000..3bedf58ca2 +--- /dev/null ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo018.kt +@@ -0,0 +1,52 @@ ++/* ++ * Copyright (c) 2022 The Matrix.org Foundation C.I.C. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); ++ * you may not use this file except in compliance with the License. ++ * You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software ++ * distributed under the License is distributed on an "AS IS" BASIS, ++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ * See the License for the specific language governing permissions and ++ * limitations under the License. ++ */ ++ ++package org.matrix.android.sdk.internal.crypto.store.db.migration ++ ++import io.realm.DynamicRealm ++import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData ++import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields ++import org.matrix.android.sdk.internal.di.MoshiProvider ++import org.matrix.android.sdk.internal.util.database.RealmMigrator ++import timber.log.Timber ++ ++/** ++ * This migration is adding support for trusted flags on megolm sessions. ++ * We can't really assert the trust of existing keys, so for the sake of simplicity we are going to ++ * mark existing keys as safe. ++ * This migration can take long depending on the account ++ */ ++internal class MigrateCryptoTo018(realm: DynamicRealm) : RealmMigrator(realm, 18) { ++ ++ private val moshiAdapter = MoshiProvider.providesMoshi().adapter(InboundGroupSessionData::class.java) ++ ++ override fun doMigrate(realm: DynamicRealm) { ++ realm.schema.get("OlmInboundGroupSessionEntity") ++ ?.transform { dynamicObject -> ++ try { ++ dynamicObject.getString(OlmInboundGroupSessionEntityFields.INBOUND_GROUP_SESSION_DATA_JSON)?.let { oldData -> ++ moshiAdapter.fromJson(oldData)?.let { dataToMigrate -> ++ dataToMigrate.copy(trusted = true).let { ++ dynamicObject.setString(OlmInboundGroupSessionEntityFields.INBOUND_GROUP_SESSION_DATA_JSON, moshiAdapter.toJson(it)) ++ } ++ } ++ } ++ } catch (failure: Throwable) { ++ Timber.e(failure, "Failed to migrate megolm session") ++ } ++ } ++ } ++} +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt +index a4b4cd0761..f93da74507 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt +@@ -82,7 +82,8 @@ internal class DefaultEncryptEventTask @Inject constructor( + ).toContent(), + forwardingCurve25519KeyChain = emptyList(), + senderCurve25519Key = result.eventContent["sender_key"] as? String, +- claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint() ++ claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint(), ++ isSafe = true + ) + } else { + null +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt +index 0a6d4bf833..193710f962 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt +@@ -228,7 +228,8 @@ private fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEnt + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, +- forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain ++ forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, ++ isSafe = result.isSafe + ) + // Save decryption result, to not decrypt every time we enter the thread list + eventEntity.setDecryptionResult(result) +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt +index 8b5a211fba..ee5c3d90c1 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt +@@ -87,7 +87,8 @@ internal open class EventEntity( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, +- forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain ++ forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, ++ isSafe = result.isSafe + ) + val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) + decryptionResultJson = adapter.toJson(decryptionResult) +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt +index bac810f424..edd74c2ce0 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt +@@ -225,7 +225,8 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, +- forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain ++ forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, ++ isSafe = result.isSafe + ) + } catch (e: MXCryptoError) { + if (e is MXCryptoError.Base) { +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt +index 7c662444e4..e0751865ad 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt +@@ -56,7 +56,8 @@ internal class DefaultGetEventTask @Inject constructor( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, +- forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain ++ forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, ++ isSafe = result.isSafe + ) + } + } +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt +index b6142b3a7a..7bda5f0a2f 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt +@@ -16,6 +16,7 @@ + + package org.matrix.android.sdk.internal.session.sync.handler + ++import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM + import org.matrix.android.sdk.api.logger.LoggerTag + import org.matrix.android.sdk.api.session.crypto.MXCryptoError + import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult +@@ -42,7 +43,9 @@ internal class CryptoSyncHandler @Inject constructor( + + suspend fun handleToDevice(toDevice: ToDeviceSyncResponse, progressReporter: ProgressReporter? = null) { + val total = toDevice.events?.size ?: 0 +- toDevice.events?.forEachIndexed { index, 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}") +@@ -57,6 +60,28 @@ internal class CryptoSyncHandler @Inject constructor( + } + } + ++ 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}") ++ } ++ } ++ } ++ + fun onSyncCompleted(syncResponse: SyncResponse) { + cryptoService.onSyncCompleted(syncResponse) + } +@@ -91,7 +116,8 @@ internal class CryptoSyncHandler @Inject constructor( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, +- forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain ++ forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, ++ isSafe = result.isSafe + ) + return true + } else { +diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +index bc91ca205d..a2f2251b70 100644 +--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt ++++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +@@ -40,6 +40,7 @@ import org.matrix.android.sdk.api.session.sync.model.RoomSync + import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse + import org.matrix.android.sdk.api.settings.LightweightSettingsStorage + import org.matrix.android.sdk.internal.crypto.DefaultCryptoService ++import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager + import org.matrix.android.sdk.internal.database.helper.addIfNecessary + import org.matrix.android.sdk.internal.database.helper.addTimelineEvent + import org.matrix.android.sdk.internal.database.helper.createOrUpdate +@@ -99,6 +100,7 @@ internal class RoomSyncHandler @Inject constructor( + private val timelineInput: TimelineInput, + private val liveEventService: Lazy, + private val clock: Clock, ++ private val unRequestedForwardManager: UnRequestedForwardManager, + ) { + + sealed class HandlingStrategy { +@@ -322,6 +324,7 @@ internal class RoomSyncHandler @Inject constructor( + } + roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.INVITE) + roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = true, inviterId = inviterEvent?.senderId, aggregator = aggregator) ++ unRequestedForwardManager.onInviteReceived(roomId, inviterEvent?.senderId.orEmpty(), clock.epochMillis()) + return roomEntity + } + +@@ -551,7 +554,8 @@ internal class RoomSyncHandler @Inject constructor( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, +- forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain ++ forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, ++ isSafe = result.isSafe + ) + } catch (e: MXCryptoError) { + if (e is MXCryptoError.Base) { +diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt +new file mode 100644 +index 0000000000..950093760a +--- /dev/null ++++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt +@@ -0,0 +1,248 @@ ++/* ++ * Copyright 2022 The Matrix.org Foundation C.I.C. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); ++ * you may not use this file except in compliance with the License. ++ * You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software ++ * distributed under the License is distributed on an "AS IS" BASIS, ++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ * See the License for the specific language governing permissions and ++ * limitations under the License. ++ */ ++ ++package org.matrix.android.sdk.internal.crypto ++ ++import io.mockk.coEvery ++import io.mockk.mockk ++import kotlinx.coroutines.runBlocking ++import org.amshove.kluent.fail ++import org.amshove.kluent.shouldBe ++import org.junit.Test ++import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM ++import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel ++import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo ++import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent ++import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap ++import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult ++import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo ++import org.matrix.android.sdk.api.session.events.model.Event ++import org.matrix.android.sdk.api.session.events.model.EventType ++import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent ++import org.matrix.android.sdk.api.session.events.model.toContent ++import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager ++ ++class UnRequestedKeysManagerTest { ++ ++ private val aliceMxId = "alice@example.com" ++ private val bobMxId = "bob@example.com" ++ private val bobDeviceId = "MKRJDSLYGA" ++ ++ private val device1Id = "MGDAADVDMG" ++ ++ private val aliceFirstDevice = CryptoDeviceInfo( ++ deviceId = device1Id, ++ userId = aliceMxId, ++ algorithms = MXCryptoAlgorithms.supportedAlgorithms(), ++ keys = mapOf( ++ "curve25519:$device1Id" to "yDa6cWOZ/WGBqm/JMUfTUCdEbAIzKHhuIcdDbnPEhDU", ++ "ed25519:$device1Id" to "XTge+TDwfm+WW10IGnaqEyLTSukPPzg3R1J1YvO1SBI", ++ ), ++ signatures = mapOf( ++ aliceMxId to mapOf( ++ "ed25519:$device1Id" to "bPOAqM40+QSMgeEzUbYbPSZZccDDMUG00lCNdSXCoaS1gKKBGkSEaHO1OcibISIabjLYzmhp9mgtivz32fbABQ", ++ "ed25519:Ru4ni66dbQ6FZgUoHyyBtmjKecOHMvMSsSBZ2SABtt0" to "owzUsQ4Pvn35uEIc5FdVnXVRPzsVYBV8uJRUSqr4y8r5tp0DvrMArtJukKETgYEAivcZMT1lwNihHIN9xh06DA" ++ ) ++ ), ++ unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Web"), ++ trustLevel = DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true) ++ ) ++ ++ private val aBobDevice = CryptoDeviceInfo( ++ deviceId = bobDeviceId, ++ userId = bobMxId, ++ algorithms = MXCryptoAlgorithms.supportedAlgorithms(), ++ keys = mapOf( ++ "curve25519:$bobDeviceId" to "tWwg63Yfn//61Ylhir6z4QGejvo193E6MVHmURtYVn0", ++ "ed25519:$bobDeviceId" to "pS5NJ1LiVksQFX+p58NlphqMxE705laRVtUtZpYIAfs", ++ ), ++ signatures = mapOf( ++ bobMxId to mapOf( ++ "ed25519:$bobDeviceId" to "zAJqsmOSzkx8EWXcrynCsWtbgWZifN7A6DLyEBs+ZPPLnNuPN5Jwzc1Rg+oZWZaRPvOPcSL0cgcxRegSBU0NBA", ++ ) ++ ), ++ unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Ios") ++ ) ++ ++ @Test ++ fun `test process key request if invite received`() { ++ val fakeDeviceListManager = mockk { ++ coEvery { downloadKeys(any(), any()) } returns MXUsersDevicesMap().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 { ++ coEvery { downloadKeys(any(), any()) } returns MXUsersDevicesMap().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 { ++ coEvery { downloadKeys(any(), any()) } returns MXUsersDevicesMap().apply { ++ setObject(bobMxId, bobDeviceId, aBobDevice) ++ } ++ } ++ val unrequestedForwardManager = UnRequestedForwardManager(fakeDeviceListManager) ++ ++ val roomId = "someRoomId" ++ ++ val timeOfKeyReception = 1_000L ++ ++ unrequestedForwardManager.onUnRequestedKeyForward( ++ roomId, ++ createFakeSuccessfullyDecryptedForwardToDevice( ++ aBobDevice, ++ aliceFirstDevice, ++ aBobDevice, ++ megolmSessionId = "megolmId1" ++ ), ++ timeOfKeyReception ++ ) ++ ++ val currentTimeWindow = 10 * 60_000 ++ ++ // simulate very late invite ++ val inviteTime = timeOfKeyReception + currentTimeWindow + 1_000 ++ unrequestedForwardManager.onInviteReceived(roomId, aBobDevice.userId, inviteTime) ++ ++ runBlocking { ++ unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(inviteTime) { ++ fail("There should be no key to process") ++ } ++ } ++ } ++ ++ private fun createFakeSuccessfullyDecryptedForwardToDevice( ++ sentBy: CryptoDeviceInfo, ++ dest: CryptoDeviceInfo, ++ sessionInitiator: CryptoDeviceInfo, ++ algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM, ++ roomId: String = "!zzgDlIhbWOevcdFBXr:example.com", ++ megolmSessionId: String = "Z/FSE8wDYheouGjGP9pezC4S1i39RtAXM3q9VXrBVZw" ++ ): Event { ++ return Event( ++ type = EventType.ENCRYPTED, ++ eventId = "!fake", ++ senderId = sentBy.userId, ++ content = OlmEventContent( ++ ciphertext = mapOf( ++ dest.identityKey()!! to mapOf( ++ "type" to 0, ++ "body" to "AwogcziNF/tv60X0elsBmnKPN3+LTXr4K3vXw+1ZJ6jpTxESIJCmMMDvOA+" ++ ) ++ ), ++ senderKey = sentBy.identityKey() ++ ).toContent(), ++ ++ ).apply { ++ mxDecryptionResult = OlmDecryptionResult( ++ payload = mapOf( ++ "type" to EventType.FORWARDED_ROOM_KEY, ++ "content" to ForwardedRoomKeyContent( ++ algorithm = algorithm, ++ roomId = roomId, ++ senderKey = sessionInitiator.identityKey(), ++ sessionId = megolmSessionId, ++ sessionKey = "AQAAAAAc4dK+lXxXyaFbckSxwjIEoIGDLKYovONJ7viWpwevhfvoBh+Q..." ++ ).toContent() ++ ), ++ senderKey = sentBy.identityKey() ++ ) ++ } ++ } ++} +diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt +index 4d947f134b..4642fb8525 100644 +--- a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt ++++ b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt +@@ -22,6 +22,7 @@ import androidx.annotation.DrawableRes + import androidx.appcompat.widget.AppCompatImageView + import androidx.core.view.isVisible + import im.vector.app.R ++import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration + import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel + + class ShieldImageView @JvmOverloads constructor( +@@ -68,6 +69,39 @@ class ShieldImageView @JvmOverloads constructor( + null -> Unit + } + } ++ ++ fun renderE2EDecoration(decoration: E2EDecoration?) { ++ isVisible = true ++ when (decoration) { ++ E2EDecoration.WARN_IN_CLEAR -> { ++ contentDescription = context.getString(R.string.unencrypted) ++ setImageResource(R.drawable.ic_shield_warning) ++ } ++ E2EDecoration.WARN_SENT_BY_UNVERIFIED -> { ++ contentDescription = context.getString(R.string.encrypted_unverified) ++ setImageResource(R.drawable.ic_shield_warning) ++ } ++ E2EDecoration.WARN_SENT_BY_UNKNOWN -> { ++ contentDescription = context.getString(R.string.encrypted_unverified) ++ setImageResource(R.drawable.ic_shield_warning) ++ } ++ E2EDecoration.WARN_SENT_BY_DELETED_SESSION -> { ++ contentDescription = context.getString(R.string.encrypted_unverified) ++ setImageResource(R.drawable.ic_shield_warning) ++ } ++ E2EDecoration.WARN_UNSAFE_KEY -> { ++ contentDescription = context.getString(R.string.key_authenticity_not_guaranteed) ++ setImageResource( ++ R.drawable.ic_shield_gray ++ ) ++ } ++ E2EDecoration.NONE, ++ null -> { ++ contentDescription = null ++ isVisible = false ++ } ++ } ++ } + } + + @DrawableRes +diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +index d918703f95..5daf82fae6 100644 +--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt ++++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +@@ -143,6 +143,14 @@ class MessageActionsEpoxyController @Inject constructor( + drawableStart(R.drawable.ic_shield_warning_small) + } + } ++ E2EDecoration.WARN_UNSAFE_KEY -> { ++ bottomSheetSendStateItem { ++ id("e2e_unsafe") ++ showProgress(false) ++ text(host.stringProvider.getString(R.string.key_authenticity_not_guaranteed)) ++ drawableStart(R.drawable.ic_shield_gray) ++ } ++ } + else -> { + // nothing + } +diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt +index c8a3bb8967..ca93c1389e 100644 +--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt ++++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt +@@ -83,7 +83,8 @@ class ViewEditHistoryViewModel @AssistedInject 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) { + Timber.w("Failed to decrypt event in history") +diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +index b711bf37bd..85a4c9da8a 100644 +--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt ++++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +@@ -31,7 +31,6 @@ import org.matrix.android.sdk.api.extensions.orFalse + import org.matrix.android.sdk.api.session.Session + import org.matrix.android.sdk.api.session.crypto.verification.VerificationState + 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.getMsgType + import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage + import org.matrix.android.sdk.api.session.events.model.isSticker +@@ -146,29 +145,40 @@ class MessageInformationDataFactory @Inject constructor( + } + + private fun getE2EDecoration(roomSummary: RoomSummary?, event: TimelineEvent): E2EDecoration { +- return if ( +- event.root.sendState == SendState.SYNCED && +- roomSummary?.isEncrypted.orFalse() && +- // is user verified +- session.cryptoService().crossSigningService().getUserCrossSigningKeys(event.root.senderId ?: "")?.isTrusted() == true) { +- val ts = roomSummary?.encryptionEventTs ?: 0 +- val eventTs = event.root.originServerTs ?: 0 +- if (event.isEncrypted()) { ++ if (roomSummary?.isEncrypted != true) { ++ // No decoration for clear room ++ // Questionable? what if the event is E2E? ++ return E2EDecoration.NONE ++ } ++ if (event.root.sendState != SendState.SYNCED) { ++ // we don't display e2e decoration if event not synced back ++ return E2EDecoration.NONE ++ } ++ val userCrossSigningInfo = session.cryptoService() ++ .crossSigningService() ++ .getUserCrossSigningKeys(event.root.senderId.orEmpty()) ++ ++ if (userCrossSigningInfo?.isTrusted() == true) { ++ return if (event.isEncrypted()) { + // Do not decorate failed to decrypt, or redaction (we lost sender device info) + if (event.root.getClearType() == EventType.ENCRYPTED || event.root.isRedacted()) { + E2EDecoration.NONE + } else { +- val sendingDevice = event.root.content +- .toModel() +- ?.deviceId +- ?.let { deviceId -> +- session.cryptoService().getCryptoDeviceInfo(event.root.senderId ?: "", deviceId) ++ val sendingDevice = event.root.getSenderKey() ++ ?.let { it -> ++ session.cryptoService().deviceWithIdentityKey( ++ it, ++ event.root.content?.get("algorithm") as? String ?: "" ++ ) + } ++ if (event.root.mxDecryptionResult?.isSafe == false) { ++ E2EDecoration.WARN_UNSAFE_KEY ++ } else { + when { + sendingDevice == null -> { + // For now do not decorate this with warning + // maybe it's a deleted session +- E2EDecoration.NONE ++ E2EDecoration.WARN_SENT_BY_DELETED_SESSION + } + sendingDevice.trustLevel == null -> { + E2EDecoration.WARN_SENT_BY_UNKNOWN +@@ -181,19 +191,35 @@ class MessageInformationDataFactory @Inject constructor( + } + } + } ++ } + } else { +- if (event.root.isStateEvent()) { +- // Do not warn for state event, they are always in clear ++ e2EDecorationForClearEventInE2ERoom(event, roomSummary) ++ } ++ } else { ++ return if (!event.isEncrypted()) { ++ e2EDecorationForClearEventInE2ERoom(event, roomSummary) ++ } else if (event.root.mxDecryptionResult != null) { ++ if (event.root.mxDecryptionResult?.isSafe == true) { + E2EDecoration.NONE + } else { +- // If event is in clear after the room enabled encryption we should warn +- if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE +- } ++ E2EDecoration.WARN_UNSAFE_KEY + } + } else { + E2EDecoration.NONE + } + } ++ } ++ ++ private fun e2EDecorationForClearEventInE2ERoom(event: TimelineEvent, roomSummary: RoomSummary) = ++ if (event.root.isStateEvent()) { ++ // Do not warn for state event, they are always in clear ++ E2EDecoration.NONE ++ } else { ++ val ts = roomSummary.encryptionEventTs ?: 0 ++ val eventTs = event.root.originServerTs ?: 0 ++ // If event is in clear after the room enabled encryption we should warn ++ if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE ++ } + + /** + * Tiles type message never show the sender information (like verification request), so we should repeat it for next message +diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt +index 5e23f4db16..ab383f04ff 100644 +--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt ++++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt +@@ -40,7 +40,6 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController + import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer + import im.vector.app.features.reactions.widget.ReactionButton + import im.vector.app.features.themes.ThemeUtils +-import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel + import org.matrix.android.sdk.api.session.room.send.SendState + + private const val MAX_REACTIONS_TO_SHOW = 8 +@@ -80,17 +79,7 @@ abstract class AbsBaseMessageItem(@LayoutRes layo + override fun bind(holder: H) { + super.bind(holder) + renderReactions(holder, baseAttributes.informationData.reactionsSummary) +- when (baseAttributes.informationData.e2eDecoration) { +- E2EDecoration.NONE -> { +- holder.e2EDecorationView.render(null) +- } +- E2EDecoration.WARN_IN_CLEAR, +- E2EDecoration.WARN_SENT_BY_UNVERIFIED, +- E2EDecoration.WARN_SENT_BY_UNKNOWN -> { +- holder.e2EDecorationView.render(RoomEncryptionTrustLevel.Warning) +- } +- } +- ++ holder.e2EDecorationView.renderE2EDecoration(baseAttributes.informationData.e2eDecoration) + holder.view.onClick(baseAttributes.itemClickListener) + holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener) + (holder.view as? TimelineMessageLayoutRenderer)?.renderMessageLayout(baseAttributes.informationData.messageLayout) +diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt +index 9b24720c88..757246d4e4 100644 +--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt ++++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt +@@ -106,7 +106,9 @@ enum class E2EDecoration { + NONE, + WARN_IN_CLEAR, + WARN_SENT_BY_UNVERIFIED, +- WARN_SENT_BY_UNKNOWN ++ WARN_SENT_BY_UNKNOWN, ++ WARN_SENT_BY_DELETED_SESSION, ++ WARN_UNSAFE_KEY + } + + enum class SendStateDecoration { +diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +index ae5a8aec7d..90138fd495 100644 +--- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt ++++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +@@ -213,7 +213,8 @@ class NotifiableEventResolver @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 + ) + } catch (ignore: MXCryptoError) { + } +diff --git a/vector/src/main/res/drawable/ic_shield_gray.xml b/vector/src/main/res/drawable/ic_shield_gray.xml +new file mode 100644 +index 0000000000..a4c52d74ba +--- /dev/null ++++ b/vector/src/main/res/drawable/ic_shield_gray.xml +@@ -0,0 +1,11 @@ ++ ++ ++ +-- +2.30.2 +