Skip to content

Per room block unverified devices #6726

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Oct 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/6725.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add option to only send to verified devices per room (web parity)
3 changes: 3 additions & 0 deletions library/ui-strings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1234,6 +1234,9 @@
<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_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 won’t 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">
<item quantity="one">%1$d/%2$d key imported with success.</item>
<item quantity="other">%1$d/%2$d keys imported with success.</item>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ interface CryptoService {

fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean

fun getLiveBlockUnverifiedDevices(roomId: String): LiveData<Boolean>

fun setWarnOnUnknownDevices(warn: Boolean)

fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String)
Expand All @@ -77,6 +79,8 @@ interface CryptoService {

fun setGlobalBlacklistUnverifiedDevices(block: Boolean)

fun getLiveGlobalCryptoConfig(): LiveData<GlobalCryptoConfig>

/**
* Enable or disable key gossiping.
* Default is true.
Expand All @@ -100,7 +104,7 @@ interface CryptoService {
*/
fun isShareKeysOnInviteEnabled(): Boolean

fun setRoomUnBlacklistUnverifiedDevices(roomId: String)
fun setRoomUnBlockUnverifiedDevices(roomId: String)

fun getDeviceTrackingStatus(userId: String): Int

Expand All @@ -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?

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1163,6 +1164,10 @@ internal class DefaultCryptoService @Inject constructor(
return cryptoStore.getGlobalBlacklistUnverifiedDevices()
}

override fun getLiveGlobalCryptoConfig(): LiveData<GlobalCryptoConfig> {
return cryptoStore.getLiveGlobalCryptoConfig()
}

/**
* Tells whether the client should encrypt messages only for the verified devices
* in this room.
Expand All @@ -1171,48 +1176,37 @@ 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<Boolean> {
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)
}

/**
* Remove this room to the ones which don't encrypt messages to unverified devices.
*
* @param roomId the room id
*/
override fun setRoomUnBlacklistUnverifiedDevices(roomId: String) {
setRoomBlacklistUnverifiedDevices(roomId, false)
override fun setRoomUnBlockUnverifiedDevices(roomId: String) {
setRoomBlockUnverifiedDevices(roomId, false)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CryptoDeviceInfo>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -120,11 +121,26 @@ internal interface IMXCryptoStore {
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.
Expand Down Expand Up @@ -516,6 +532,9 @@ internal interface IMXCryptoStore {
fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>>

fun getGlobalCryptoConfig(): GlobalCryptoConfig
fun getLiveGlobalCryptoConfig(): LiveData<GlobalCryptoConfig>

fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo?

Expand Down
Loading