diff --git a/changelog.d/6725.bugfix b/changelog.d/6725.bugfix
new file mode 100644
index 0000000000..f05ddbc69d
--- /dev/null
+++ b/changelog.d/6725.bugfix
@@ -0,0 +1 @@
+Add option to only send to verified devices per room (web parity)
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 98f9fea8f2..bb28fe1151 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -1234,6 +1234,9 @@
Import
Encrypt to verified sessions only
Never send encrypted messages to unverified sessions from this session.
+ Never send encrypted messages to unverified sessions in this room.
+ ⚠ There are unverified devices in this room, they won’t be able to decrypt messages you send.
+ 🔒 You have enabled encrypt to verified sessions only for all rooms in Security Settings.
- %1$d/%2$d key imported with success.
- %1$d/%2$d keys imported with success.
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeConfigTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeConfigTest.kt
new file mode 100644
index 0000000000..8b12092b79
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeConfigTest.kt
@@ -0,0 +1,132 @@
+/*
+ * 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 androidx.test.filters.LargeTest
+import org.amshove.kluent.shouldBe
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.MethodSorters
+import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+@LargeTest
+class E2eeConfigTest : InstrumentedTest {
+
+ @Test
+ fun testBlacklistUnverifiedDefault() = runCryptoTest(context()) { cryptoTestHelper, _ ->
+ val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+
+ cryptoTestData.firstSession.cryptoService().getGlobalBlacklistUnverifiedDevices() shouldBe false
+ cryptoTestData.firstSession.cryptoService().isRoomBlacklistUnverifiedDevices(cryptoTestData.roomId) shouldBe false
+ cryptoTestData.secondSession!!.cryptoService().getGlobalBlacklistUnverifiedDevices() shouldBe false
+ cryptoTestData.secondSession!!.cryptoService().isRoomBlacklistUnverifiedDevices(cryptoTestData.roomId) shouldBe false
+ }
+
+ @Test
+ fun testCantDecryptIfGlobalUnverified() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
+ val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+
+ cryptoTestData.firstSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true)
+
+ val roomAlicePOV = cryptoTestData.firstSession.roomService().getRoom(cryptoTestData.roomId)!!
+
+ val sentMessage = testHelper.sendTextMessage(roomAlicePOV, "you are blocked", 1).first()
+
+ val roomBobPOV = cryptoTestData.secondSession!!.roomService().getRoom(cryptoTestData.roomId)!!
+ // ensure other received
+ testHelper.retryPeriodically {
+ roomBobPOV.timelineService().getTimelineEvent(sentMessage.eventId) != null
+ }
+
+ cryptoTestHelper.ensureCannotDecrypt(listOf(sentMessage.eventId), cryptoTestData.secondSession!!, cryptoTestData.roomId)
+ }
+
+ @Test
+ fun testCanDecryptIfGlobalUnverifiedAndUserTrusted() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
+ val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+
+ cryptoTestHelper.initializeCrossSigning(cryptoTestData.firstSession)
+ cryptoTestHelper.initializeCrossSigning(cryptoTestData.secondSession!!)
+
+ cryptoTestHelper.verifySASCrossSign(cryptoTestData.firstSession, cryptoTestData.secondSession!!, cryptoTestData.roomId)
+
+ cryptoTestData.firstSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true)
+
+ val roomAlicePOV = cryptoTestData.firstSession.roomService().getRoom(cryptoTestData.roomId)!!
+
+ val sentMessage = testHelper.sendTextMessage(roomAlicePOV, "you can read", 1).first()
+
+ val roomBobPOV = cryptoTestData.secondSession!!.roomService().getRoom(cryptoTestData.roomId)!!
+ // ensure other received
+ testHelper.retryPeriodically {
+ roomBobPOV.timelineService().getTimelineEvent(sentMessage.eventId) != null
+ }
+
+ cryptoTestHelper.ensureCanDecrypt(
+ listOf(sentMessage.eventId),
+ cryptoTestData.secondSession!!,
+ cryptoTestData.roomId,
+ listOf(sentMessage.getLastMessageContent()!!.body)
+ )
+ }
+
+ @Test
+ fun testCantDecryptIfPerRoomUnverified() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
+ val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+
+ val roomAlicePOV = cryptoTestData.firstSession.roomService().getRoom(cryptoTestData.roomId)!!
+
+ val beforeMessage = testHelper.sendTextMessage(roomAlicePOV, "you can read", 1).first()
+
+ val roomBobPOV = cryptoTestData.secondSession!!.roomService().getRoom(cryptoTestData.roomId)!!
+ // ensure other received
+ testHelper.retryPeriodically {
+ roomBobPOV.timelineService().getTimelineEvent(beforeMessage.eventId) != null
+ }
+
+ cryptoTestHelper.ensureCanDecrypt(
+ listOf(beforeMessage.eventId),
+ cryptoTestData.secondSession!!,
+ cryptoTestData.roomId,
+ listOf(beforeMessage.getLastMessageContent()!!.body)
+ )
+
+ cryptoTestData.firstSession.cryptoService().setRoomBlockUnverifiedDevices(cryptoTestData.roomId, true)
+
+ val afterMessage = testHelper.sendTextMessage(roomAlicePOV, "you are blocked", 1).first()
+
+ // ensure received
+ testHelper.retryPeriodically {
+ cryptoTestData.secondSession?.getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(afterMessage.eventId)?.root != null
+ }
+
+ cryptoTestHelper.ensureCannotDecrypt(
+ listOf(afterMessage.eventId),
+ cryptoTestData.secondSession!!,
+ cryptoTestData.roomId,
+ MXCryptoError.ErrorType.KEYS_WITHHELD
+ )
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
index e0e662c789..d2aa8020e8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
@@ -61,6 +61,8 @@ interface CryptoService {
fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean
+ fun getLiveBlockUnverifiedDevices(roomId: String): LiveData
+
fun setWarnOnUnknownDevices(warn: Boolean)
fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String)
@@ -77,6 +79,8 @@ interface CryptoService {
fun setGlobalBlacklistUnverifiedDevices(block: Boolean)
+ fun getLiveGlobalCryptoConfig(): LiveData
+
/**
* Enable or disable key gossiping.
* Default is true.
@@ -100,7 +104,7 @@ interface CryptoService {
*/
fun isShareKeysOnInviteEnabled(): Boolean
- fun setRoomUnBlacklistUnverifiedDevices(roomId: String)
+ fun setRoomUnBlockUnverifiedDevices(roomId: String)
fun getDeviceTrackingStatus(userId: String): Int
@@ -112,7 +116,7 @@ interface CryptoService {
suspend fun exportRoomKeys(password: String): ByteArray
- fun setRoomBlacklistUnverifiedDevices(roomId: String)
+ fun setRoomBlockUnverifiedDevices(roomId: String, block: Boolean)
fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo?
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/GlobalCryptoConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/GlobalCryptoConfig.kt
new file mode 100644
index 0000000000..6405652a68
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/GlobalCryptoConfig.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.crypto
+
+data class GlobalCryptoConfig(
+ val globalBlockUnverifiedDevices: Boolean,
+ val globalEnableKeyGossiping: Boolean,
+ val enableKeyForwardingOnInvite: Boolean,
+)
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 901700cac6..9c3e0ba1c5 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
@@ -40,6 +40,7 @@ import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.CryptoService
+import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
@@ -1163,6 +1164,10 @@ internal class DefaultCryptoService @Inject constructor(
return cryptoStore.getGlobalBlacklistUnverifiedDevices()
}
+ override fun getLiveGlobalCryptoConfig(): LiveData {
+ return cryptoStore.getLiveGlobalCryptoConfig()
+ }
+
/**
* Tells whether the client should encrypt messages only for the verified devices
* in this room.
@@ -1171,39 +1176,28 @@ internal class DefaultCryptoService @Inject constructor(
* @param roomId the room id
* @return true if the client should encrypt messages only for the verified devices.
*/
-// TODO add this info in CryptoRoomEntity?
override fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean {
- return roomId?.let { cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(it) }
+ return roomId?.let { cryptoStore.getBlockUnverifiedDevices(roomId) }
?: false
}
/**
- * Manages the room black-listing for unverified devices.
+ * A live status regarding sharing keys for unverified devices in this room.
*
- * @param roomId the room id
- * @param add true to add the room id to the list, false to remove it.
+ * @return Live status
*/
- private fun setRoomBlacklistUnverifiedDevices(roomId: String, add: Boolean) {
- val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList()
-
- if (add) {
- if (roomId !in roomIds) {
- roomIds.add(roomId)
- }
- } else {
- roomIds.remove(roomId)
- }
-
- cryptoStore.setRoomsListBlacklistUnverifiedDevices(roomIds)
+ override fun getLiveBlockUnverifiedDevices(roomId: String): LiveData {
+ return cryptoStore.getLiveBlockUnverifiedDevices(roomId)
}
/**
* Add this room to the ones which don't encrypt messages to unverified devices.
*
* @param roomId the room id
+ * @param block if true will block sending keys to unverified devices
*/
- override fun setRoomBlacklistUnverifiedDevices(roomId: String) {
- setRoomBlacklistUnverifiedDevices(roomId, true)
+ override fun setRoomBlockUnverifiedDevices(roomId: String, block: Boolean) {
+ cryptoStore.blockUnverifiedDevicesInRoom(roomId, block)
}
/**
@@ -1211,8 +1205,8 @@ internal class DefaultCryptoService @Inject constructor(
*
* @param roomId the room id
*/
- override fun setRoomUnBlacklistUnverifiedDevices(roomId: String) {
- setRoomBlacklistUnverifiedDevices(roomId, false)
+ override fun setRoomUnBlockUnverifiedDevices(roomId: String) {
+ setRoomBlockUnverifiedDevices(roomId, false)
}
/**
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 fca6fab66c..7b6051932a 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
@@ -424,7 +424,7 @@ internal class MXMegolmEncryption(
// an m.new_device.
val keys = deviceListManager.downloadKeys(userIds, false)
val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() ||
- cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId)
+ cryptoStore.getBlockUnverifiedDevices(roomId)
val devicesInRoom = DeviceInRoomInfo()
val unknownDevices = MXUsersDevicesMap()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
index 56eba25249..21e3342365 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.store
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
+import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
@@ -120,11 +121,26 @@ internal interface IMXCryptoStore {
fun getRoomsListBlacklistUnverifiedDevices(): List
/**
- * Updates the rooms ids list in which the messages are not encrypted for the unverified devices.
+ * A live status regarding sharing keys for unverified devices in this room.
*
- * @param roomIds the room ids list
+ * @return Live status
*/
- fun setRoomsListBlacklistUnverifiedDevices(roomIds: List)
+ fun getLiveBlockUnverifiedDevices(roomId: String): LiveData
+
+ /**
+ * Tell if unverified devices should be blacklisted when sending keys.
+ *
+ * @return true if should not send keys to unverified devices
+ */
+ fun getBlockUnverifiedDevices(roomId: String): Boolean
+
+ /**
+ * Define if encryption keys should be sent to unverified devices in this room.
+ *
+ * @param roomId the roomId
+ * @param block if true will not send keys to unverified devices
+ */
+ fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean)
/**
* Get the current keys backup version.
@@ -516,6 +532,9 @@ internal interface IMXCryptoStore {
fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
fun getLiveCrossSigningPrivateKeys(): LiveData>
+ fun getGlobalCryptoConfig(): GlobalCryptoConfig
+ fun getLiveGlobalCryptoConfig(): LiveData
+
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo?
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
index 6a2ef3bde1..e97cf437c6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
@@ -29,6 +29,7 @@ import io.realm.kotlin.where
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.logger.LoggerTag
+import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
@@ -445,6 +446,38 @@ internal class RealmCryptoStore @Inject constructor(
}
}
+ override fun getGlobalCryptoConfig(): GlobalCryptoConfig {
+ return doWithRealm(realmConfiguration) { realm ->
+ realm.where().findFirst()
+ ?.let {
+ GlobalCryptoConfig(
+ globalBlockUnverifiedDevices = it.globalBlacklistUnverifiedDevices,
+ globalEnableKeyGossiping = it.globalEnableKeyGossiping,
+ enableKeyForwardingOnInvite = it.enableKeyForwardingOnInvite
+ )
+ } ?: GlobalCryptoConfig(false, false, false)
+ }
+ }
+
+ override fun getLiveGlobalCryptoConfig(): LiveData {
+ val liveData = monarchy.findAllMappedWithChanges(
+ { realm: Realm ->
+ realm
+ .where()
+ },
+ {
+ GlobalCryptoConfig(
+ globalBlockUnverifiedDevices = it.globalBlacklistUnverifiedDevices,
+ globalEnableKeyGossiping = it.globalEnableKeyGossiping,
+ enableKeyForwardingOnInvite = it.enableKeyForwardingOnInvite
+ )
+ }
+ )
+ return Transformations.map(liveData) {
+ it.firstOrNull() ?: GlobalCryptoConfig(false, false, false)
+ }
+ }
+
override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) {
Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null}, ${usk != null}, ${ssk != null}")
doRealmTransaction(realmConfiguration) { realm ->
@@ -1053,25 +1086,6 @@ internal class RealmCryptoStore @Inject constructor(
} ?: false
}
- override fun setRoomsListBlacklistUnverifiedDevices(roomIds: List) {
- doRealmTransaction(realmConfiguration) {
- // Reset all
- it.where()
- .findAll()
- .forEach { room ->
- room.blacklistUnverifiedDevices = false
- }
-
- // Enable those in the list
- it.where()
- .`in`(CryptoRoomEntityFields.ROOM_ID, roomIds.toTypedArray())
- .findAll()
- .forEach { room ->
- room.blacklistUnverifiedDevices = true
- }
- }
- }
-
override fun getRoomsListBlacklistUnverifiedDevices(): List {
return doWithRealm(realmConfiguration) {
it.where()
@@ -1083,6 +1097,37 @@ internal class RealmCryptoStore @Inject constructor(
}
}
+ override fun getLiveBlockUnverifiedDevices(roomId: String): LiveData {
+ val liveData = monarchy.findAllMappedWithChanges(
+ { realm: Realm ->
+ realm.where()
+ .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId)
+ },
+ {
+ it.blacklistUnverifiedDevices
+ }
+ )
+ return Transformations.map(liveData) {
+ it.firstOrNull() ?: false
+ }
+ }
+
+ override fun getBlockUnverifiedDevices(roomId: String): Boolean {
+ return doWithRealm(realmConfiguration) { realm ->
+ realm.where()
+ .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId)
+ .findFirst()
+ ?.blacklistUnverifiedDevices ?: false
+ }
+ }
+
+ override fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) {
+ doRealmTransaction(realmConfiguration) { realm ->
+ CryptoRoomEntity.getById(realm, roomId)
+ ?.blacklistUnverifiedDevices = block
+ }
+ }
+
override fun getDeviceTrackingStatuses(): Map {
return doWithRealm(realmConfiguration) {
it.where()
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt
index 22b040b4c0..44bac1c8a0 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt
@@ -27,4 +27,5 @@ sealed class RoomProfileAction : VectorViewModelAction {
object ShareRoomProfile : RoomProfileAction()
object CreateShortcut : RoomProfileAction()
object RestoreEncryptionState : RoomProfileAction()
+ data class SetEncryptToVerifiedDeviceOnly(val enabled: Boolean) : RoomProfileAction()
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt
index 06f56bff89..eb43a345f2 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt
@@ -27,6 +27,7 @@ import im.vector.app.core.resources.DrawableProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.core.ui.list.genericPositiveButtonItem
+import im.vector.app.features.form.formSwitchItem
import im.vector.app.features.home.ShortcutCreator
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
@@ -66,6 +67,8 @@ class RoomProfileController @Inject constructor(
fun onUrlInTopicLongClicked(url: String)
fun doMigrateToVersion(newVersion: String)
fun restoreEncryptionState()
+ fun setEncryptedToVerifiedDevicesOnly(enabled: Boolean)
+ fun openGlobalBlockSettings()
}
override fun buildModels(data: RoomProfileViewState?) {
@@ -175,6 +178,53 @@ class RoomProfileController @Inject constructor(
}
buildEncryptionAction(data.actionPermissions, roomSummary)
+ if (roomSummary.isEncrypted && !encryptionMisconfigured) {
+ data.globalCryptoConfig.invoke()?.let { globalConfig ->
+ if (globalConfig.globalBlockUnverifiedDevices) {
+ genericFooterItem {
+ id("globalConfig")
+ centered(false)
+ text(
+ span {
+ +host.stringProvider.getString(R.string.room_settings_global_block_unverified_info_text)
+ apply {
+ if (data.unverifiedDevicesInTheRoom.invoke() == true) {
+ +"\n"
+ +host.stringProvider.getString(R.string.some_devices_will_not_be_able_to_decrypt)
+ }
+ }
+ }.toEpoxyCharSequence()
+ )
+ itemClickAction {
+ host.callback?.openGlobalBlockSettings()
+ }
+ }
+ } else {
+ // per room setting is available
+ val shouldBlockUnverified = data.encryptToVerifiedDeviceOnly.invoke()
+ formSwitchItem {
+ id("send_to_unverified")
+ enabled(shouldBlockUnverified != null)
+ title(host.stringProvider.getString(R.string.encryption_never_send_to_unverified_devices_in_room))
+
+ switchChecked(shouldBlockUnverified ?: false)
+
+ apply {
+ if (shouldBlockUnverified == true && data.unverifiedDevicesInTheRoom.invoke() == true) {
+ summary(
+ host.stringProvider.getString(R.string.some_devices_will_not_be_able_to_decrypt)
+ )
+ } else {
+ summary(null)
+ }
+ }
+ listener { value ->
+ host.callback?.setEncryptedToVerifiedDevicesOnly(value)
+ }
+ }
+ }
+ }
+ }
// More
buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))
buildProfileAction(
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
index 4135ab3d1c..f4394111ab 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
@@ -53,6 +53,7 @@ import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
+import im.vector.app.features.navigation.SettingsActivityPayload
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize
@@ -346,6 +347,14 @@ class RoomProfileFragment :
)
}
+ override fun setEncryptedToVerifiedDevicesOnly(enabled: Boolean) {
+ roomProfileViewModel.handle(RoomProfileAction.SetEncryptToVerifiedDeviceOnly(enabled))
+ }
+
+ override fun openGlobalBlockSettings() {
+ navigator.openSettings(requireContext(), SettingsActivityPayload.SecurityPrivacy)
+ }
+
private fun onAvatarClicked(view: View) = withState(roomProfileViewModel) { state ->
state.roomSummary()?.toMatrixItem()?.let { matrixItem ->
navigator.openBigImageViewer(requireActivity(), view, matrixItem)
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt
index 30664c5618..215a1e1e9c 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt
@@ -17,6 +17,7 @@
package im.vector.app.features.roomprofile
+import androidx.lifecycle.asFlow
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@@ -32,7 +33,11 @@ import im.vector.app.features.home.ShortcutCreator
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.session.coroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.query.QueryStringValue
@@ -76,6 +81,45 @@ class RoomProfileViewModel @AssistedInject constructor(
observeBannedRoomMembers(flowRoom)
observePermissions()
observePowerLevels()
+ observeCryptoSettings(flowRoom)
+ }
+
+ private fun observeCryptoSettings(flowRoom: FlowRoom) {
+ val perRoomBlockStatus = session.cryptoService().getLiveBlockUnverifiedDevices(initialState.roomId)
+ .asFlow()
+
+ perRoomBlockStatus
+ .execute {
+ copy(encryptToVerifiedDeviceOnly = it)
+ }
+
+ val globalBlockStatus = session.cryptoService().getLiveGlobalCryptoConfig()
+ .asFlow()
+
+ globalBlockStatus
+ .execute {
+ copy(globalCryptoConfig = it)
+ }
+
+ perRoomBlockStatus.combine(globalBlockStatus) { perRoom, global ->
+ perRoom || global.globalBlockUnverifiedDevices
+ }.flatMapLatest {
+ if (it) {
+ flowRoom.liveRoomMembers(roomMemberQueryParams { memberships = Membership.activeMemberships() })
+ .map { it.map { it.userId } }
+ .flatMapLatest {
+ session.cryptoService().getLiveCryptoDeviceInfo(it).asFlow()
+ }
+ } else {
+ flowOf(emptyList())
+ }
+ }.map {
+ it.isNotEmpty()
+ }.execute {
+ copy(
+ unverifiedDevicesInTheRoom = it
+ )
+ }
}
private fun observePowerLevels() {
@@ -141,6 +185,7 @@ class RoomProfileViewModel @AssistedInject constructor(
is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile()
RoomProfileAction.CreateShortcut -> handleCreateShortcut()
RoomProfileAction.RestoreEncryptionState -> restoreEncryptionState()
+ is RoomProfileAction.SetEncryptToVerifiedDeviceOnly -> setEncryptToVerifiedDeviceOnly(action.enabled)
}
}
@@ -212,6 +257,12 @@ class RoomProfileViewModel @AssistedInject constructor(
}
}
+ private fun setEncryptToVerifiedDeviceOnly(enabled: Boolean) {
+ session.coroutineScope.launch {
+ session.cryptoService().setRoomBlockUnverifiedDevices(room.roomId, enabled)
+ }
+ }
+
private fun restoreEncryptionState() {
_viewEvents.post(RoomProfileViewEvents.Loading())
session.coroutineScope.launch {
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt
index 87db15ea3b..5393ceb152 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt
@@ -20,6 +20,7 @@ package im.vector.app.features.roomprofile
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized
+import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
@@ -35,7 +36,10 @@ data class RoomProfileViewState(
val recommendedRoomVersion: String? = null,
val canUpgradeRoom: Boolean = false,
val isTombstoned: Boolean = false,
- val canUpdateRoomState: Boolean = false
+ val canUpdateRoomState: Boolean = false,
+ val encryptToVerifiedDeviceOnly: Async = Uninitialized,
+ val globalCryptoConfig: Async = Uninitialized,
+ val unverifiedDevicesInTheRoom: Async = Uninitialized,
) : MavericksState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt
index 81e98335c0..10465b03ea 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt
@@ -45,7 +45,7 @@ data class RoomSettingsViewState(
val showSaveAction: Boolean = false,
val actionPermissions: ActionPermissions = ActionPermissions(),
val supportsRestricted: Boolean = false,
- val canUpgradeToRestricted: Boolean = false
+ val canUpgradeToRestricted: Boolean = false,
) : MavericksState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
diff --git a/vector/src/main/res/layout/item_form_switch.xml b/vector/src/main/res/layout/item_form_switch.xml
index a637c8f52e..67d286a917 100644
--- a/vector/src/main/res/layout/item_form_switch.xml
+++ b/vector/src/main/res/layout/item_form_switch.xml
@@ -7,6 +7,8 @@
android:background="?android:colorBackground"
android:foreground="?attr/selectableItemBackground"
android:minHeight="@dimen/item_form_min_height"
+ android:paddingBottom="8dp"
+ android:paddingTop="8dp"
tools:viewBindingIgnore="true">