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">