Merge pull request #6726 from vector-im/feature/bca/block_unverified

Per room block unverified devices
This commit is contained in:
Valere 2022-10-05 14:29:59 +02:00 committed by GitHub
commit ec7c8c8b31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 386 additions and 48 deletions

1
changelog.d/6725.bugfix Normal file
View File

@ -0,0 +1 @@
Add option to only send to verified devices per room (web parity)

View File

@ -1234,6 +1234,9 @@
<string name="encryption_import_import">Import</string> <string name="encryption_import_import">Import</string>
<string name="encryption_never_send_to_unverified_devices_title">Encrypt to verified sessions only</string> <string name="encryption_never_send_to_unverified_devices_title">Encrypt to verified sessions only</string>
<string name="encryption_never_send_to_unverified_devices_summary">Never send encrypted messages to unverified sessions from this session.</string> <string name="encryption_never_send_to_unverified_devices_summary">Never send encrypted messages to unverified sessions from this session.</string>
<string name="encryption_never_send_to_unverified_devices_in_room">Never send encrypted messages to unverified sessions in this room.</string>
<string name="some_devices_will_not_be_able_to_decrypt">⚠ There are unverified devices in this room, they wont be able to decrypt messages you send.</string>
<string name="room_settings_global_block_unverified_info_text">🔒 You have enabled encrypt to verified sessions only for all rooms in Security Settings.</string>
<plurals name="encryption_import_room_keys_success"> <plurals name="encryption_import_room_keys_success">
<item quantity="one">%1$d/%2$d key imported with success.</item> <item quantity="one">%1$d/%2$d key imported with success.</item>
<item quantity="other">%1$d/%2$d keys imported with success.</item> <item quantity="other">%1$d/%2$d keys imported with success.</item>

View File

@ -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
)
}
}

View File

@ -61,6 +61,8 @@ interface CryptoService {
fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean
fun getLiveBlockUnverifiedDevices(roomId: String): LiveData<Boolean>
fun setWarnOnUnknownDevices(warn: Boolean) fun setWarnOnUnknownDevices(warn: Boolean)
fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String)
@ -77,6 +79,8 @@ interface CryptoService {
fun setGlobalBlacklistUnverifiedDevices(block: Boolean) fun setGlobalBlacklistUnverifiedDevices(block: Boolean)
fun getLiveGlobalCryptoConfig(): LiveData<GlobalCryptoConfig>
/** /**
* Enable or disable key gossiping. * Enable or disable key gossiping.
* Default is true. * Default is true.
@ -100,7 +104,7 @@ interface CryptoService {
*/ */
fun isShareKeysOnInviteEnabled(): Boolean fun isShareKeysOnInviteEnabled(): Boolean
fun setRoomUnBlacklistUnverifiedDevices(roomId: String) fun setRoomUnBlockUnverifiedDevices(roomId: String)
fun getDeviceTrackingStatus(userId: String): Int fun getDeviceTrackingStatus(userId: String): Int
@ -112,7 +116,7 @@ interface CryptoService {
suspend fun exportRoomKeys(password: String): ByteArray suspend fun exportRoomKeys(password: String): ByteArray
fun setRoomBlacklistUnverifiedDevices(roomId: String) fun setRoomBlockUnverifiedDevices(roomId: String, block: Boolean)
fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo?

View File

@ -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,
)

View File

@ -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.listeners.ProgressListener
import org.matrix.android.sdk.api.logger.LoggerTag 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.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.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.NewSessionListener 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.OutgoingKeyRequest
@ -1163,6 +1164,10 @@ internal class DefaultCryptoService @Inject constructor(
return cryptoStore.getGlobalBlacklistUnverifiedDevices() return cryptoStore.getGlobalBlacklistUnverifiedDevices()
} }
override fun getLiveGlobalCryptoConfig(): LiveData<GlobalCryptoConfig> {
return cryptoStore.getLiveGlobalCryptoConfig()
}
/** /**
* Tells whether the client should encrypt messages only for the verified devices * Tells whether the client should encrypt messages only for the verified devices
* in this room. * in this room.
@ -1171,39 +1176,28 @@ internal class DefaultCryptoService @Inject constructor(
* @param roomId the room id * @param roomId the room id
* @return true if the client should encrypt messages only for the verified devices. * @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 { override fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean {
return roomId?.let { cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(it) } return roomId?.let { cryptoStore.getBlockUnverifiedDevices(roomId) }
?: false ?: 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 * @return Live status
* @param add true to add the room id to the list, false to remove it.
*/ */
private fun setRoomBlacklistUnverifiedDevices(roomId: String, add: Boolean) { override fun getLiveBlockUnverifiedDevices(roomId: String): LiveData<Boolean> {
val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList() return cryptoStore.getLiveBlockUnverifiedDevices(roomId)
if (add) {
if (roomId !in roomIds) {
roomIds.add(roomId)
}
} else {
roomIds.remove(roomId)
}
cryptoStore.setRoomsListBlacklistUnverifiedDevices(roomIds)
} }
/** /**
* Add this room to the ones which don't encrypt messages to unverified devices. * Add this room to the ones which don't encrypt messages to unverified devices.
* *
* @param roomId the room id * @param roomId the room id
* @param block if true will block sending keys to unverified devices
*/ */
override fun setRoomBlacklistUnverifiedDevices(roomId: String) { override fun setRoomBlockUnverifiedDevices(roomId: String, block: Boolean) {
setRoomBlacklistUnverifiedDevices(roomId, true) cryptoStore.blockUnverifiedDevicesInRoom(roomId, block)
} }
/** /**
@ -1211,8 +1205,8 @@ internal class DefaultCryptoService @Inject constructor(
* *
* @param roomId the room id * @param roomId the room id
*/ */
override fun setRoomUnBlacklistUnverifiedDevices(roomId: String) { override fun setRoomUnBlockUnverifiedDevices(roomId: String) {
setRoomBlacklistUnverifiedDevices(roomId, false) setRoomBlockUnverifiedDevices(roomId, false)
} }
/** /**

View File

@ -424,7 +424,7 @@ internal class MXMegolmEncryption(
// an m.new_device. // an m.new_device.
val keys = deviceListManager.downloadKeys(userIds, false) val keys = deviceListManager.downloadKeys(userIds, false)
val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() || val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() ||
cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId) cryptoStore.getBlockUnverifiedDevices(roomId)
val devicesInRoom = DeviceInRoomInfo() val devicesInRoom = DeviceInRoomInfo()
val unknownDevices = MXUsersDevicesMap<CryptoDeviceInfo>() val unknownDevices = MXUsersDevicesMap<CryptoDeviceInfo>()

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.store
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.paging.PagedList 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.NewSessionListener
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
@ -120,11 +121,26 @@ internal interface IMXCryptoStore {
fun getRoomsListBlacklistUnverifiedDevices(): List<String> fun getRoomsListBlacklistUnverifiedDevices(): List<String>
/** /**
* 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<String>) fun getLiveBlockUnverifiedDevices(roomId: String): LiveData<Boolean>
/**
* 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. * Get the current keys backup version.
@ -516,6 +532,9 @@ internal interface IMXCryptoStore {
fun getCrossSigningPrivateKeys(): PrivateKeysInfo? fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>> fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>>
fun getGlobalCryptoConfig(): GlobalCryptoConfig
fun getLiveGlobalCryptoConfig(): LiveData<GlobalCryptoConfig>
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo?

View File

@ -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.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.logger.LoggerTag 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.NewSessionListener
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState 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<CryptoMetadataEntity>().findFirst()
?.let {
GlobalCryptoConfig(
globalBlockUnverifiedDevices = it.globalBlacklistUnverifiedDevices,
globalEnableKeyGossiping = it.globalEnableKeyGossiping,
enableKeyForwardingOnInvite = it.enableKeyForwardingOnInvite
)
} ?: GlobalCryptoConfig(false, false, false)
}
}
override fun getLiveGlobalCryptoConfig(): LiveData<GlobalCryptoConfig> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm: Realm ->
realm
.where<CryptoMetadataEntity>()
},
{
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?) { override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) {
Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null}, ${usk != null}, ${ssk != null}") Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null}, ${usk != null}, ${ssk != null}")
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction(realmConfiguration) { realm ->
@ -1053,25 +1086,6 @@ internal class RealmCryptoStore @Inject constructor(
} ?: false } ?: false
} }
override fun setRoomsListBlacklistUnverifiedDevices(roomIds: List<String>) {
doRealmTransaction(realmConfiguration) {
// Reset all
it.where<CryptoRoomEntity>()
.findAll()
.forEach { room ->
room.blacklistUnverifiedDevices = false
}
// Enable those in the list
it.where<CryptoRoomEntity>()
.`in`(CryptoRoomEntityFields.ROOM_ID, roomIds.toTypedArray())
.findAll()
.forEach { room ->
room.blacklistUnverifiedDevices = true
}
}
}
override fun getRoomsListBlacklistUnverifiedDevices(): List<String> { override fun getRoomsListBlacklistUnverifiedDevices(): List<String> {
return doWithRealm(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<CryptoRoomEntity>() it.where<CryptoRoomEntity>()
@ -1083,6 +1097,37 @@ internal class RealmCryptoStore @Inject constructor(
} }
} }
override fun getLiveBlockUnverifiedDevices(roomId: String): LiveData<Boolean> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm: Realm ->
realm.where<CryptoRoomEntity>()
.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<CryptoRoomEntity>()
.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<String, Int> { override fun getDeviceTrackingStatuses(): Map<String, Int> {
return doWithRealm(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<UserEntity>() it.where<UserEntity>()

View File

@ -27,4 +27,5 @@ sealed class RoomProfileAction : VectorViewModelAction {
object ShareRoomProfile : RoomProfileAction() object ShareRoomProfile : RoomProfileAction()
object CreateShortcut : RoomProfileAction() object CreateShortcut : RoomProfileAction()
object RestoreEncryptionState : RoomProfileAction() object RestoreEncryptionState : RoomProfileAction()
data class SetEncryptToVerifiedDeviceOnly(val enabled: Boolean) : RoomProfileAction()
} }

View File

@ -27,6 +27,7 @@ import im.vector.app.core.resources.DrawableProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.core.ui.list.genericPositiveButtonItem 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.ShortcutCreator
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
@ -66,6 +67,8 @@ class RoomProfileController @Inject constructor(
fun onUrlInTopicLongClicked(url: String) fun onUrlInTopicLongClicked(url: String)
fun doMigrateToVersion(newVersion: String) fun doMigrateToVersion(newVersion: String)
fun restoreEncryptionState() fun restoreEncryptionState()
fun setEncryptedToVerifiedDevicesOnly(enabled: Boolean)
fun openGlobalBlockSettings()
} }
override fun buildModels(data: RoomProfileViewState?) { override fun buildModels(data: RoomProfileViewState?) {
@ -175,6 +178,53 @@ class RoomProfileController @Inject constructor(
} }
buildEncryptionAction(data.actionPermissions, roomSummary) 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 // More
buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))
buildProfileAction( buildProfileAction(

View File

@ -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.detail.upgrade.MigrateRoomBottomSheet
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction 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.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.app.features.navigation.SettingsActivityPayload
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize 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 -> private fun onAvatarClicked(view: View) = withState(roomProfileViewModel) { state ->
state.roomSummary()?.toMatrixItem()?.let { matrixItem -> state.roomSummary()?.toMatrixItem()?.let { matrixItem ->
navigator.openBigImageViewer(requireActivity(), view, matrixItem) navigator.openBigImageViewer(requireActivity(), view, matrixItem)

View File

@ -17,6 +17,7 @@
package im.vector.app.features.roomprofile package im.vector.app.features.roomprofile
import androidx.lifecycle.asFlow
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory 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.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import kotlinx.coroutines.Dispatchers 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.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
@ -76,6 +81,45 @@ class RoomProfileViewModel @AssistedInject constructor(
observeBannedRoomMembers(flowRoom) observeBannedRoomMembers(flowRoom)
observePermissions() observePermissions()
observePowerLevels() 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() { private fun observePowerLevels() {
@ -141,6 +185,7 @@ class RoomProfileViewModel @AssistedInject constructor(
is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile() is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile()
RoomProfileAction.CreateShortcut -> handleCreateShortcut() RoomProfileAction.CreateShortcut -> handleCreateShortcut()
RoomProfileAction.RestoreEncryptionState -> restoreEncryptionState() 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() { private fun restoreEncryptionState() {
_viewEvents.post(RoomProfileViewEvents.Loading()) _viewEvents.post(RoomProfileViewEvents.Loading())
session.coroutineScope.launch { session.coroutineScope.launch {

View File

@ -20,6 +20,7 @@ package im.vector.app.features.roomprofile
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized 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.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
@ -35,7 +36,10 @@ data class RoomProfileViewState(
val recommendedRoomVersion: String? = null, val recommendedRoomVersion: String? = null,
val canUpgradeRoom: Boolean = false, val canUpgradeRoom: Boolean = false,
val isTombstoned: Boolean = false, val isTombstoned: Boolean = false,
val canUpdateRoomState: Boolean = false val canUpdateRoomState: Boolean = false,
val encryptToVerifiedDeviceOnly: Async<Boolean> = Uninitialized,
val globalCryptoConfig: Async<GlobalCryptoConfig> = Uninitialized,
val unverifiedDevicesInTheRoom: Async<Boolean> = Uninitialized,
) : MavericksState { ) : MavericksState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId) constructor(args: RoomProfileArgs) : this(roomId = args.roomId)

View File

@ -45,7 +45,7 @@ data class RoomSettingsViewState(
val showSaveAction: Boolean = false, val showSaveAction: Boolean = false,
val actionPermissions: ActionPermissions = ActionPermissions(), val actionPermissions: ActionPermissions = ActionPermissions(),
val supportsRestricted: Boolean = false, val supportsRestricted: Boolean = false,
val canUpgradeToRestricted: Boolean = false val canUpgradeToRestricted: Boolean = false,
) : MavericksState { ) : MavericksState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId) constructor(args: RoomProfileArgs) : this(roomId = args.roomId)

View File

@ -7,6 +7,8 @@
android:background="?android:colorBackground" android:background="?android:colorBackground"
android:foreground="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground"
android:minHeight="@dimen/item_form_min_height" android:minHeight="@dimen/item_form_min_height"
android:paddingBottom="8dp"
android:paddingTop="8dp"
tools:viewBindingIgnore="true"> tools:viewBindingIgnore="true">
<TextView <TextView