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 5aeac44a55..cc339f91d9 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 enable 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/E2eeTestConfig.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeTestConfig.kt
new file mode 100644
index 0000000000..7a03276d34
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeTestConfig.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.room.timeline.getLastMessageContent
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+@LargeTest
+class E2eeTestConfig : 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.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ 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.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ 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.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ roomBobPOV.timelineService().getTimelineEvent(beforeMessage.eventId) != null
+ }
+ }
+
+ cryptoTestHelper.ensureCanDecrypt(
+ listOf(beforeMessage.eventId),
+ cryptoTestData.secondSession!!,
+ cryptoTestData.roomId,
+ listOf(beforeMessage.getLastMessageContent()!!.body)
+ )
+
+ cryptoTestData.firstSession.cryptoService().setRoomBlacklistUnverifiedDevices(cryptoTestData.roomId, true)
+
+ val afterMessage = testHelper.sendTextMessage(roomAlicePOV, "you are blocked", 1).first()
+
+ 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..5e0c087c08 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
@@ -42,6 +42,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.model.SessionInfo
+import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig
interface CryptoService {
@@ -61,6 +62,8 @@ interface CryptoService {
fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean
+ fun getLiveBlacklistUnverifiedDevices(roomId: String): LiveData
+
fun setWarnOnUnknownDevices(warn: Boolean)
fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String)
@@ -77,6 +80,8 @@ interface CryptoService {
fun setGlobalBlacklistUnverifiedDevices(block: Boolean)
+ fun getLiveGlobalCryptoConfig(): LiveData
+
/**
* Enable or disable key gossiping.
* Default is true.
@@ -112,7 +117,7 @@ interface CryptoService {
suspend fun exportRoomKeys(password: String): ByteArray
- fun setRoomBlacklistUnverifiedDevices(roomId: String)
+ fun setRoomBlacklistUnverifiedDevices(roomId: String, enable: 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..a6afe6087b
--- /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 New Vector Ltd
+ *
+ * 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 globalBlacklistUnverifiedDevices: 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..f37be4ff62 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
@@ -87,6 +87,7 @@ import org.matrix.android.sdk.internal.crypto.model.MXKey.Companion.KEY_SIGNED_C
import org.matrix.android.sdk.internal.crypto.model.SessionInfo
import org.matrix.android.sdk.internal.crypto.model.toRest
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
+import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask
@@ -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,30 +1176,18 @@ 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.getBlacklistUnverifiedDevices(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 getLiveBlacklistUnverifiedDevices(roomId: String): LiveData {
+ return cryptoStore.getLiveBlacklistUnverifiedDevices(roomId)
}
/**
@@ -1202,8 +1195,8 @@ internal class DefaultCryptoService @Inject constructor(
*
* @param roomId the room id
*/
- override fun setRoomBlacklistUnverifiedDevices(roomId: String) {
- setRoomBlacklistUnverifiedDevices(roomId, true)
+ override fun setRoomBlacklistUnverifiedDevices(roomId: String, enable: Boolean) {
+ cryptoStore.blackListUnverifiedDevicesInRoom(roomId, enable)
}
/**
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..e19e513b63 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.getBlacklistUnverifiedDevices(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..193b53ec4e 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 getLiveBlacklistUnverifiedDevices(roomId: String): LiveData
+
+ /**
+ * Tell if unverified devices should be blacklisted when sending keys.
+ *
+ * @return true if should not send keys to unverified devices
+ */
+ fun getBlacklistUnverifiedDevices(roomId: String): Boolean
+
+ /**
+ * Define if encryption keys should be sent to unverified devices in this room.
+ *
+ * @param roomId the roomId
+ * @param blacklist if true will not send keys to unverified devices
+ */
+ fun blackListUnverifiedDevicesInRoom(roomId: String, blacklist: 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..801d012385 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
@@ -53,6 +53,7 @@ import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
+import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper
@@ -445,6 +446,38 @@ internal class RealmCryptoStore @Inject constructor(
}
}
+ override fun getGlobalCryptoConfig(): GlobalCryptoConfig {
+ return doWithRealm(realmConfiguration) { realm ->
+ realm.where().findFirst()
+ ?.let {
+ GlobalCryptoConfig(
+ globalBlacklistUnverifiedDevices = 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(
+ globalBlacklistUnverifiedDevices = 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 getLiveBlacklistUnverifiedDevices(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 getBlacklistUnverifiedDevices(roomId: String): Boolean {
+ return doWithRealm(realmConfiguration) { realm ->
+ realm.where()
+ .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId)
+ .findFirst()
+ ?.blacklistUnverifiedDevices ?: false
+ }
+ }
+
+ override fun blackListUnverifiedDevicesInRoom(roomId: String, blacklist: Boolean) {
+ doRealmTransaction(realmConfiguration) { realm ->
+ CryptoRoomEntity.getById(realm, roomId)
+ ?.blacklistUnverifiedDevices = blacklist
+ }
+ }
+
override fun getDeviceTrackingStatuses(): Map {
return doWithRealm(realmConfiguration) {
it.where()
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..c457a01750 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
@@ -35,7 +35,7 @@ data class RoomProfileViewState(
val recommendedRoomVersion: String? = null,
val canUpgradeRoom: Boolean = false,
val isTombstoned: Boolean = false,
- val canUpdateRoomState: Boolean = false
+ val canUpdateRoomState: Boolean = false,
) : MavericksState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt
index eb601605e0..bb2dc08e76 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt
@@ -28,6 +28,7 @@ sealed class RoomSettingsAction : VectorViewModelAction {
data class SetRoomHistoryVisibility(val visibility: RoomHistoryVisibility) : RoomSettingsAction()
data class SetRoomJoinRule(val roomJoinRule: RoomJoinRules) : RoomSettingsAction()
data class SetRoomGuestAccess(val guestAccess: GuestAccess) : RoomSettingsAction()
+ data class SetEncryptToVerifiedDeviceOnly(val enable: Boolean) : RoomSettingsAction()
object Save : RoomSettingsAction()
object Cancel : RoomSettingsAction()
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt
index f8efe73ebf..936ebbd861 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt
@@ -22,6 +22,7 @@ import im.vector.app.core.epoxy.dividerItem
import im.vector.app.core.epoxy.profiles.buildProfileAction
import im.vector.app.core.epoxy.profiles.buildProfileSection
import im.vector.app.core.resources.StringProvider
+import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.core.ui.list.verticalMarginItem
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.form.formEditTextItem
@@ -30,6 +31,8 @@ import im.vector.app.features.form.formSwitchItem
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter
import im.vector.app.features.settings.VectorPreferences
+import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
+import me.gujun.android.span.span
import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.util.toMatrixItem
@@ -52,6 +55,7 @@ class RoomSettingsController @Inject constructor(
fun onHistoryVisibilityClicked()
fun onJoinRuleClicked()
fun onToggleGuestAccess()
+ fun setEncryptedToVerifiedDevicesOnly(enabled: Boolean)
}
var callback: Callback? = null
@@ -145,5 +149,53 @@ class RoomSettingsController @Inject constructor(
id("guestAccessDivider")
}
}
+
+ // Security
+ buildProfileSection(stringProvider.getString(R.string.room_profile_section_security))
+
+ data.globalCryptoConfig.invoke()?.let { globalConfig ->
+ if (globalConfig.globalBlacklistUnverifiedDevices) {
+ genericFooterItem {
+ id("globalConfig")
+ centered(false)
+ text(
+ span {
+ +host.stringProvider.getString(R.string.room_settings_global_blacklist_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 {
+ }
+ }
+ } 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)
+ }
+ }
+ }
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt
index ba50890db3..b7d8f13343 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt
@@ -199,6 +199,10 @@ class RoomSettingsFragment :
viewModel.handle(RoomSettingsAction.SetRoomGuestAccess(toggled))
}
+ override fun setEncryptedToVerifiedDevicesOnly(enabled: Boolean) {
+ viewModel.handle(RoomSettingsAction.SetEncryptToVerifiedDeviceOnly(enabled))
+ }
+
override fun onImageReady(uri: Uri?) {
uri ?: return
viewModel.handle(
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt
index 501ff7553a..cb5809d96a 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt
@@ -17,6 +17,7 @@
package im.vector.app.features.roomprofile.settings
import androidx.core.net.toFile
+import androidx.lifecycle.asFlow
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@@ -25,8 +26,12 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
+import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@@ -37,6 +42,8 @@ import org.matrix.android.sdk.api.session.events.model.EventType
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.homeserver.HomeServerCapabilities
+import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
+import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent
import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
@@ -83,6 +90,39 @@ class RoomSettingsViewModel @AssistedInject constructor(
canUpgradeToRestricted = couldUpgradeToRestricted
)
}
+
+ session.cryptoService().getLiveBlacklistUnverifiedDevices(initialState.roomId)
+ .asFlow()
+ .execute {
+ copy(encryptToVerifiedDeviceOnly = it)
+ }
+
+ session.cryptoService().getLiveGlobalCryptoConfig()
+ .asFlow()
+ .execute {
+ copy(globalCryptoConfig = it)
+ }
+
+ val flowRoom = room.flow()
+ session.cryptoService().getLiveBlacklistUnverifiedDevices(initialState.roomId)
+ .asFlow()
+ .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 observeState() {
@@ -212,6 +252,7 @@ class RoomSettingsViewModel @AssistedInject constructor(
is RoomSettingsAction.SetRoomGuestAccess -> handleSetGuestAccess(action)
is RoomSettingsAction.Save -> saveSettings()
is RoomSettingsAction.Cancel -> cancel()
+ is RoomSettingsAction.SetEncryptToVerifiedDeviceOnly -> setEncryptToVerifiedDeviceOnly(action.enable)
}
}
@@ -233,6 +274,12 @@ class RoomSettingsViewModel @AssistedInject constructor(
}
}
+ private fun setEncryptToVerifiedDeviceOnly(enabled: Boolean) {
+ session.coroutineScope.launch {
+ session.cryptoService().setRoomBlacklistUnverifiedDevices(room.roomId, enabled)
+ }
+ }
+
private fun handleSetAvatarAction(action: RoomSettingsAction.SetAvatarAction) {
setState {
deletePendingAvatar(this)
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..e3c6cd9b03 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
@@ -27,6 +27,7 @@ import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig
data class RoomSettingsViewState(
val roomId: String,
@@ -45,7 +46,10 @@ data class RoomSettingsViewState(
val showSaveAction: Boolean = false,
val actionPermissions: ActionPermissions = ActionPermissions(),
val supportsRestricted: Boolean = false,
- val canUpgradeToRestricted: Boolean = false
+ val canUpgradeToRestricted: 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/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">