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