Merge pull request #8507 from vector-im/feature/bca/clean_room_shield_update

Clean room shield update logic
This commit is contained in:
Valere 2023-06-13 12:27:28 +02:00 committed by GitHub
commit ce80d7ff2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 380 additions and 99 deletions

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

@ -0,0 +1 @@
In some conditions the room shield is not refreshed correctly

View File

@ -99,6 +99,23 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
} }
} }
} }
@OptIn(ExperimentalCoroutinesApi::class)
internal fun runLongCryptoTest(context: Context, cryptoConfig: MXCryptoConfig? = null, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CryptoTestHelper, CommonTestHelper) -> Unit) {
val testHelper = CommonTestHelper(context, cryptoConfig)
val cryptoTestHelper = CryptoTestHelper(testHelper)
return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis * 4) {
try {
withContext(Dispatchers.Default) {
block(cryptoTestHelper, testHelper)
}
} finally {
if (autoSignoutOnClose) {
testHelper.cleanUpOpenedSessions()
}
}
}
}
} }
internal val matrix: TestMatrix internal val matrix: TestMatrix

View File

@ -0,0 +1,133 @@
/*
* Copyright 2023 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 android.util.Log
import androidx.lifecycle.Observer
import androidx.test.filters.LargeTest
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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.Session
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
class RoomShieldTest : InstrumentedTest {
@Test
fun testShieldNoVerification() = CommonTestHelper.runCryptoTest(context()) { cryptoTestHelper, _ ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val roomId = testData.roomId
cryptoTestHelper.initializeCrossSigning(testData.firstSession)
cryptoTestHelper.initializeCrossSigning(testData.secondSession!!)
// Test are flaky unless I use liveData observer on main thread
// Just calling getRoomSummary() with retryWithBackOff keeps an outdated version of the value
testData.firstSession.assertRoomShieldIs(roomId, RoomEncryptionTrustLevel.Default)
testData.secondSession!!.assertRoomShieldIs(roomId, RoomEncryptionTrustLevel.Default)
}
@Test
fun testShieldInOneOne() = CommonTestHelper.runLongCryptoTest(context()) { cryptoTestHelper, testHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val roomId = testData.roomId
Log.v("#E2E TEST", "Initialize cross signing...")
cryptoTestHelper.initializeCrossSigning(testData.firstSession)
cryptoTestHelper.initializeCrossSigning(testData.secondSession!!)
Log.v("#E2E TEST", "... Initialized.")
// let alive and bob verify
Log.v("#E2E TEST", "Alice and Bob verify each others...")
cryptoTestHelper.verifySASCrossSign(testData.firstSession, testData.secondSession!!, testData.roomId)
// Add a new session for bob
// This session will be unverified for now
Log.v("#E2E TEST", "Log in a new bob device...")
val bobSecondSession = testHelper.logIntoAccount(testData.secondSession!!.myUserId, SessionTestParams(true))
Log.v("#E2E TEST", "Bob session logged in ${bobSecondSession.myUserId.take(6)}")
Log.v("#E2E TEST", "Assert room shields...")
testData.firstSession.assertRoomShieldIs(roomId, RoomEncryptionTrustLevel.Warning)
// in 1:1 we ignore our own status
testData.secondSession!!.assertRoomShieldIs(roomId, RoomEncryptionTrustLevel.Trusted)
// Adding another user should make bob consider his devices now and see same shield as alice
Log.v("#E2E TEST", "Create Sam account")
val samSession = testHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
// Let alice invite sam
Log.v("#E2E TEST", "Let alice invite sam")
testData.firstSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId)
testHelper.waitForAndAcceptInviteInRoom(samSession, roomId)
Log.v("#E2E TEST", "Assert room shields...")
testData.firstSession.assertRoomShieldIs(roomId, RoomEncryptionTrustLevel.Warning)
testData.secondSession!!.assertRoomShieldIs(roomId, RoomEncryptionTrustLevel.Warning)
// Now let's bob verify his session
Log.v("#E2E TEST", "Bob verifies his new session")
cryptoTestHelper.verifyNewSession(testData.secondSession!!, bobSecondSession)
testData.firstSession.assertRoomShieldIs(roomId, RoomEncryptionTrustLevel.Trusted)
testData.secondSession!!.assertRoomShieldIs(roomId, RoomEncryptionTrustLevel.Trusted)
}
@OptIn(DelicateCoroutinesApi::class)
private suspend fun Session.assertRoomShieldIs(roomId: String, state: RoomEncryptionTrustLevel?) {
val lock = CountDownLatch(1)
val roomLiveData = withContext(Dispatchers.Main) {
roomService().getRoomSummaryLive(roomId)
}
val observer = object : Observer<Optional<RoomSummary>> {
override fun onChanged(value: Optional<RoomSummary>) {
Log.v("#E2E TEST ${this@assertRoomShieldIs.myUserId.take(6)}", "Shield Update ${value.getOrNull()?.roomEncryptionTrustLevel}")
if (value.getOrNull()?.roomEncryptionTrustLevel == state) {
lock.countDown()
roomLiveData.removeObserver(this)
}
}
}
GlobalScope.launch(Dispatchers.Main) { roomLiveData.observeForever(observer) }
lock.await(40_000, TimeUnit.MILLISECONDS)
}
}

View File

@ -49,7 +49,6 @@ import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.logLimit import org.matrix.android.sdk.internal.util.logLimit
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
@ -66,7 +65,6 @@ internal class DefaultCrossSigningService @Inject constructor(
private val deviceListManager: DeviceListManager, private val deviceListManager: DeviceListManager,
private val initializeCrossSigningTask: InitializeCrossSigningTask, private val initializeCrossSigningTask: InitializeCrossSigningTask,
private val uploadSignaturesTask: UploadSignaturesTask, private val uploadSignaturesTask: UploadSignaturesTask,
private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope, private val cryptoCoroutineScope: CoroutineScope,
private val workManagerProvider: WorkManagerProvider, private val workManagerProvider: WorkManagerProvider,
@ -612,9 +610,7 @@ internal class DefaultCrossSigningService @Inject constructor(
withContext(coroutineDispatchers.crypto) { withContext(coroutineDispatchers.crypto) {
// This device should be yours // This device should be yours
val device = cryptoStore.getUserDevice(myUserId, deviceId) val device = cryptoStore.getUserDevice(myUserId, deviceId)
if (device == null) { ?: throw IllegalArgumentException("This device [$deviceId] is not known, or not yours")
throw IllegalArgumentException("This device [$deviceId] is not known, or not yours")
}
val myKeys = getUserCrossSigningKeys(myUserId) val myKeys = getUserCrossSigningKeys(myUserId)
?: throw Throwable("CrossSigning is not setup for this account") ?: throw Throwable("CrossSigning is not setup for this account")

View File

@ -31,7 +31,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerif
import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
@ -40,10 +40,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields
import org.matrix.android.sdk.internal.database.awaitTransaction import org.matrix.android.sdk.internal.database.awaitTransaction
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.CryptoDatabase import org.matrix.android.sdk.internal.di.CryptoDatabase
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
@ -83,35 +80,54 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
@Inject lateinit var myUserId: String @Inject lateinit var myUserId: String
@Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper @Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper
@Inject lateinit var updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository @Inject lateinit var updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
@Inject lateinit var cryptoSessionInfoProvider: CryptoSessionInfoProvider
// @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater // @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater
@Inject lateinit var cryptoStore: IMXCryptoStore // @Inject lateinit var cryptoStore: IMXCryptoStore
override fun injectWith(injector: SessionComponent) { override fun injectWith(injector: SessionComponent) {
injector.inject(this) injector.inject(this)
} }
override suspend fun doSafeWork(params: Params): Result { override suspend fun doSafeWork(params: Params): Result {
val userList = params.filename val sId = myUserId.take(5)
Timber.v("## CrossSigning - UpdateTrustWorker started..")
val workerParams = params.filename
?.let { updateTrustWorkerDataRepository.getParam(it) } ?.let { updateTrustWorkerDataRepository.getParam(it) }
?.userIds ?: return Result.success().also {
?: params.updatedUserIds.orEmpty() Timber.w("## CrossSigning - UpdateTrustWorker failed to get params")
cleanup(params)
}
Timber.v("## CrossSigning [$sId]- UpdateTrustWorker userIds:${workerParams.userIds.logLimit()}, roomIds:${workerParams.roomIds.orEmpty().logLimit()}")
val userList = workerParams.userIds
// List should not be empty, but let's avoid go further in case of empty list // List should not be empty, but let's avoid go further in case of empty list
if (userList.isNotEmpty()) { if (userList.isNotEmpty()) {
// Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user, // Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
// or a new device?) So we check all again :/ // or a new device?) So we check all again :/
Timber.v("## CrossSigning - Updating trust for users: ${userList.logLimit()}") Timber.v("## CrossSigning [$sId]- Updating trust for users: ${userList.logLimit()}")
updateTrust(userList) updateTrust(userList)
} }
val roomsToCheck = workerParams.roomIds ?: cryptoSessionInfoProvider.getRoomsWhereUsersAreParticipating(userList)
Timber.v("## CrossSigning [$sId]- UpdateTrustWorker roomShield to check:${roomsToCheck.logLimit()}")
var myCrossSigningInfo: MXCrossSigningInfo?
Realm.getInstance(cryptoRealmConfiguration).use { realm ->
myCrossSigningInfo = getCrossSigningInfo(realm, myUserId)
}
// So Cross Signing keys trust is updated, device trust is updated
// We can now update room shields? in the session DB?
updateRoomShieldInSummaries(roomsToCheck, myCrossSigningInfo)
cleanup(params) cleanup(params)
return Result.success() return Result.success()
} }
private suspend fun updateTrust(userListParam: List<String>) { private suspend fun updateTrust(userListParam: List<String>) {
val sId = myUserId.take(5)
var userList = userListParam var userList = userListParam
var myCrossSigningInfo: MXCrossSigningInfo? = null var myCrossSigningInfo: MXCrossSigningInfo?
// First we check that the users MSK are trusted by mine // First we check that the users MSK are trusted by mine
// After that we check the trust chain for each devices of each users // After that we check the trust chain for each devices of each users
@ -123,7 +139,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
var myTrustResult: UserTrustResult? = null var myTrustResult: UserTrustResult? = null
if (userList.contains(myUserId)) { if (userList.contains(myUserId)) {
Timber.d("## CrossSigning - Clear all trust as a change on my user was detected") Timber.d("## CrossSigning [$sId]- Clear all trust as a change on my user was detected")
// i am in the list.. but i don't know exactly the delta of change :/ // i am in the list.. but i don't know exactly the delta of change :/
// If it's my cross signing keys we should refresh all trust // If it's my cross signing keys we should refresh all trust
// do it anyway ? // do it anyway ?
@ -153,7 +169,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
myUserId -> myTrustResult myUserId -> myTrustResult
else -> { else -> {
crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, entry.value).also { crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, entry.value).also {
Timber.v("## CrossSigning - user:${entry.key} result:$it") Timber.v("## CrossSigning [$sId]- user:${entry.key} result:$it")
} }
} }
} }
@ -163,12 +179,12 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
// i have all the new trusts, update DB // i have all the new trusts, update DB
trusts.forEach { trusts.forEach {
val verified = it.value?.isVerified() == true val verified = it.value?.isVerified() == true
Timber.v("[$myUserId] ## CrossSigning - Updating user trust: ${it.key} to $verified") Timber.v("[$myUserId] ## CrossSigning [$sId]- Updating user trust: ${it.key} to $verified")
updateCrossSigningKeysTrust(cryptoRealm, it.key, verified) updateCrossSigningKeysTrust(cryptoRealm, it.key, verified)
} }
// Ok so now we have to check device trust for all these users.. // Ok so now we have to check device trust for all these users..
Timber.v("## CrossSigning - Updating devices cross trust users: ${trusts.keys.logLimit()}") Timber.v("## CrossSigning [$sId]- Updating devices cross trust users: ${trusts.keys.logLimit()}")
trusts.keys.forEach { userId -> trusts.keys.forEach { userId ->
val devicesEntities = cryptoRealm.where<UserEntity>() val devicesEntities = cryptoRealm.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, userId) .equalTo(UserEntityFields.USER_ID, userId)
@ -184,9 +200,9 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
// Update trust if needed // Update trust if needed
devicesEntities?.forEach { device -> devicesEntities?.forEach { device ->
val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified() val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified()
Timber.v("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}") Timber.v("## CrossSigning [$sId]- Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) { if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) {
Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified") Timber.d("## CrossSigning [$sId]- Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
// need to save // need to save
val trustEntity = device.trustLevelEntity val trustEntity = device.trustLevelEntity
if (trustEntity == null) { if (trustEntity == null) {
@ -197,50 +213,46 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
} else { } else {
trustEntity.crossSignedVerified = crossSignedVerified trustEntity.crossSignedVerified = crossSignedVerified
} }
} else {
Timber.v("## CrossSigning [$sId]- Trust unchanged for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
} }
} }
} }
} }
// So Cross Signing keys trust is updated, device trust is updated
// We can now update room shields? in the session DB?
updateTrustStep2(userList, myCrossSigningInfo)
} }
private suspend fun updateTrustStep2(userList: List<String>, myCrossSigningInfo: MXCrossSigningInfo?) { private suspend fun updateRoomShieldInSummaries(roomList: List<String>, myCrossSigningInfo: MXCrossSigningInfo?) {
Timber.d("## CrossSigning - Updating shields for impacted rooms...") val sId = myUserId.take(5)
Timber.d("## CrossSigning [$sId]- Updating shields for impacted rooms... ${roomList.logLimit()}")
awaitTransaction(sessionRealmConfiguration) { sessionRealm -> awaitTransaction(sessionRealmConfiguration) { sessionRealm ->
Timber.d("## CrossSigning - Updating shields for impacted rooms - in transaction") Timber.d("## CrossSigning - Updating shields for impacted rooms - in transaction")
Realm.getInstance(cryptoRealmConfiguration).use { cryptoRealm -> Realm.getInstance(cryptoRealmConfiguration).use { cryptoRealm ->
sessionRealm.where(RoomMemberSummaryEntity::class.java) roomList.forEach { roomId ->
.`in`(RoomMemberSummaryEntityFields.USER_ID, userList.toTypedArray()) Timber.v("## CrossSigning [$sId]- Checking room $roomId")
.distinct(RoomMemberSummaryEntityFields.ROOM_ID) RoomSummaryEntity.where(sessionRealm, roomId)
.findAll() // .equalTo(RoomSummaryEntityFields.IS_ENCRYPTED, true)
.map { it.roomId } .findFirst()
.also { Timber.d("## CrossSigning - ... impacted rooms ${it.logLimit()}") } ?.let { roomSummary ->
.forEach { roomId -> Timber.v("## CrossSigning [$sId]- Check shield state for room $roomId")
RoomSummaryEntity.where(sessionRealm, roomId) val allActiveRoomMembers = RoomMemberHelper(sessionRealm, roomId).getActiveRoomMemberIds()
.equalTo(RoomSummaryEntityFields.IS_ENCRYPTED, true) try {
.findFirst() val updatedTrust = computeRoomShield(
?.let { roomSummary -> myCrossSigningInfo,
Timber.v("## CrossSigning - Check shield state for room $roomId") cryptoRealm,
val allActiveRoomMembers = RoomMemberHelper(sessionRealm, roomId).getActiveRoomMemberIds() allActiveRoomMembers,
try { roomSummary
val updatedTrust = computeRoomShield( )
myCrossSigningInfo, if (roomSummary.roomEncryptionTrustLevel != updatedTrust) {
cryptoRealm, Timber.d("## CrossSigning [$sId]- Shield change detected for $roomId -> $updatedTrust")
allActiveRoomMembers, roomSummary.roomEncryptionTrustLevel = updatedTrust
roomSummary } else {
) Timber.v("## CrossSigning [$sId]- Shield unchanged for $roomId -> $updatedTrust")
if (roomSummary.roomEncryptionTrustLevel != updatedTrust) {
Timber.d("## CrossSigning - Shield change detected for $roomId -> $updatedTrust")
roomSummary.roomEncryptionTrustLevel = updatedTrust
}
} catch (failure: Throwable) {
Timber.e(failure)
}
} }
} } catch (failure: Throwable) {
Timber.e(failure)
}
}
}
} }
} }
Timber.d("## CrossSigning - Updating shields for impacted rooms - END") Timber.d("## CrossSigning - Updating shields for impacted rooms - END")

View File

@ -0,0 +1,56 @@
/*
* Copyright 2023 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.session.sync.handler
import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorkerDataRepository
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.util.logLimit
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@SessionScope
internal class ShieldSummaryUpdater @Inject constructor(
@SessionId private val sessionId: String,
private val workManagerProvider: WorkManagerProvider,
private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository,
) {
fun refreshShieldsForRoomIds(roomIds: Set<String>) {
Timber.d("## CrossSigning - checkAffectedRoomShields for roomIds: ${roomIds.logLimit()}")
val workerParams = UpdateTrustWorker.Params(
sessionId = sessionId,
filename = updateTrustWorkerDataRepository.createParam(emptyList(), roomIds = roomIds.toList())
)
val workerData = WorkerParamsFactory.toData(workerParams)
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<UpdateTrustWorker>()
.setInputData(workerData)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.build()
workManagerProvider.workManager
.beginUniqueWork("TRUST_UPDATE_QUEUE", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
.enqueue()
}
}

View File

@ -19,19 +19,19 @@ package org.matrix.android.sdk.internal.crypto
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.internal.database.mapper.EventMapper import org.matrix.android.sdk.internal.database.mapper.EventMapper
import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventEntityFields import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereType import org.matrix.android.sdk.internal.database.query.whereType
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.query.process
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.util.fetchCopied import org.matrix.android.sdk.internal.util.fetchCopied
import org.matrix.android.sdk.internal.util.logLimit
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -89,14 +89,30 @@ internal class CryptoSessionInfoProvider @Inject constructor(
} }
fun getRoomsWhereUsersAreParticipating(userList: List<String>): List<String> { fun getRoomsWhereUsersAreParticipating(userList: List<String>): List<String> {
if (userList.contains(myUserId)) {
// just take all
val roomIds: List<String>? = null
monarchy.doWithRealm { sessionRealm ->
RoomSummaryEntity.where(sessionRealm)
.process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
.findAll()
.map { it.roomId }
}
return roomIds.orEmpty()
}
var roomIds: List<String>? = null var roomIds: List<String>? = null
monarchy.doWithRealm { sessionRealm -> monarchy.doWithRealm { sessionRealm ->
roomIds = sessionRealm.where(RoomMemberSummaryEntity::class.java) roomIds = RoomSummaryEntity.where(sessionRealm)
.`in`(RoomMemberSummaryEntityFields.USER_ID, userList.toTypedArray()) .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
.distinct(RoomMemberSummaryEntityFields.ROOM_ID)
.findAll() .findAll()
.filter { it.otherMemberIds.any { it in userList } }
.map { it.roomId } .map { it.roomId }
.also { Timber.d("## CrossSigning - ... impacted rooms ${it.logLimit()}") } // roomIds = sessionRealm.where(RoomMemberSummaryEntity::class.java)
// .`in`(RoomMemberSummaryEntityFields.USER_ID, userList.toTypedArray())
// .distinct(RoomMemberSummaryEntityFields.ROOM_ID)
// .findAll()
// .map { it.roomId }
// .also { Timber.d("## CrossSigning - ... impacted rooms ${it.logLimit()}") }
} }
return roomIds.orEmpty() return roomIds.orEmpty()
} }

View File

@ -28,7 +28,10 @@ import javax.inject.Inject
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class UpdateTrustWorkerData( internal data class UpdateTrustWorkerData(
@Json(name = "userIds") @Json(name = "userIds")
val userIds: List<String> val userIds: List<String>,
// When we just need to refresh the room shield (no change on user keys, but a membership change)
@Json(name = "roomIds")
val roomIds: List<String>? = null
) )
internal class UpdateTrustWorkerDataRepository @Inject constructor( internal class UpdateTrustWorkerDataRepository @Inject constructor(
@ -38,12 +41,12 @@ internal class UpdateTrustWorkerDataRepository @Inject constructor(
private val jsonAdapter = MoshiProvider.providesMoshi().adapter(UpdateTrustWorkerData::class.java) private val jsonAdapter = MoshiProvider.providesMoshi().adapter(UpdateTrustWorkerData::class.java)
// Return the path of the created file // Return the path of the created file
fun createParam(userIds: List<String>): String { fun createParam(userIds: List<String>, roomIds: List<String>? = null): String {
val filename = "${UUID.randomUUID()}.json" val filename = "${UUID.randomUUID()}.json"
workingDirectory.mkdirs() workingDirectory.mkdirs()
val file = File(workingDirectory, filename) val file = File(workingDirectory, filename)
UpdateTrustWorkerData(userIds = userIds) UpdateTrustWorkerData(userIds = userIds, roomIds = roomIds)
.let { jsonAdapter.toJson(it) } .let { jsonAdapter.toJson(it) }
.let { file.writeText(it) } .let { file.writeText(it) }

View File

@ -168,6 +168,9 @@ internal class RoomSummaryUpdater @Inject constructor(
val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases
.orEmpty() .orEmpty()
roomSummaryEntity.updateAliases(roomAliases) roomSummaryEntity.updateAliases(roomAliases)
val wasEncrypted = roomSummaryEntity.isEncrypted
roomSummaryEntity.isEncrypted = encryptionEvent != null roomSummaryEntity.isEncrypted = encryptionEvent != null
roomSummaryEntity.e2eAlgorithm = ContentMapper.map(encryptionEvent?.content) roomSummaryEntity.e2eAlgorithm = ContentMapper.map(encryptionEvent?.content)
@ -197,17 +200,13 @@ internal class RoomSummaryUpdater @Inject constructor(
// better to use what we know // better to use what we know
roomSummaryEntity.joinedMembersCount = otherRoomMembers.size + 1 roomSummaryEntity.joinedMembersCount = otherRoomMembers.size + 1
} }
if (roomSummaryEntity.isEncrypted && otherRoomMembers.isNotEmpty()) { }
if (aggregator == null) {
// Do it now if (roomSummaryEntity.isEncrypted) {
// mmm maybe we could only refresh shield instead of checking trust also? if (!wasEncrypted || updateMembers || roomSummaryEntity.roomEncryptionTrustLevel == null) {
// XXX why doing this here? we don't show shield anymore and it will be refreshed // trigger a shield update
// by the sdk // if users add devices/keys or signatures the device list manager will trigger a refresh
// crossSigningService.checkTrustAndAffectedRoomShields(otherRoomMembers) aggregator?.roomsWithMembershipChangesForShieldUpdate?.add(roomId)
} else {
// Schedule it
aggregator.userIdsForCheckingTrustAndAffectedRoomShields.addAll(otherRoomMembers)
}
} }
} }
} }
@ -410,7 +409,7 @@ internal class RoomSummaryUpdater @Inject constructor(
val relatedSpaces = lookupMap.keys val relatedSpaces = lookupMap.keys
.filter { it.roomType == RoomType.SPACE } .filter { it.roomType == RoomType.SPACE }
.filter { .filter {
dmRoom.otherMemberIds.toList().intersect(it.otherMemberIds.toList()).isNotEmpty() dmRoom.otherMemberIds.toList().intersect(it.otherMemberIds.toSet()).isNotEmpty()
} }
.map { it.roomId } .map { it.roomId }
.distinct() .distinct()

View File

@ -29,7 +29,8 @@ internal class SyncResponsePostTreatmentAggregator {
val userIdsToFetch = mutableSetOf<String>() val userIdsToFetch = mutableSetOf<String>()
// Set of users to call `crossSigningService.checkTrustAndAffectedRoomShields` once per sync // Set of users to call `crossSigningService.checkTrustAndAffectedRoomShields` once per sync
val userIdsForCheckingTrustAndAffectedRoomShields = mutableSetOf<String>()
val roomsWithMembershipChangesForShieldUpdate = mutableSetOf<String>()
// For the crypto store // For the crypto store
val cryptoStoreAggregator = CryptoStoreAggregator() val cryptoStoreAggregator = CryptoStoreAggregator()

View File

@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.sync.handler
import androidx.work.BackoffPolicy import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorkerDataRepository import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorkerDataRepository
import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.SessionId
@ -39,16 +38,16 @@ internal class SyncResponsePostTreatmentAggregatorHandler @Inject constructor(
private val directChatsHelper: DirectChatsHelper, private val directChatsHelper: DirectChatsHelper,
private val ephemeralTemporaryStore: RoomSyncEphemeralTemporaryStore, private val ephemeralTemporaryStore: RoomSyncEphemeralTemporaryStore,
private val updateUserAccountDataTask: UpdateUserAccountDataTask, private val updateUserAccountDataTask: UpdateUserAccountDataTask,
private val crossSigningService: CrossSigningService,
private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository, private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository,
private val workManagerProvider: WorkManagerProvider, private val workManagerProvider: WorkManagerProvider,
private val roomShieldSummaryUpdater: ShieldSummaryUpdater,
@SessionId private val sessionId: String, @SessionId private val sessionId: String,
) { ) {
suspend fun handle(aggregator: SyncResponsePostTreatmentAggregator) { suspend fun handle(aggregator: SyncResponsePostTreatmentAggregator) {
cleanupEphemeralFiles(aggregator.ephemeralFilesToDelete) cleanupEphemeralFiles(aggregator.ephemeralFilesToDelete)
updateDirectUserIds(aggregator.directChatsToCheck) updateDirectUserIds(aggregator.directChatsToCheck)
fetchAndUpdateUsers(aggregator.userIdsToFetch) fetchAndUpdateUsers(aggregator.userIdsToFetch)
handleUserIdsForCheckingTrustAndAffectedRoomShields(aggregator.userIdsForCheckingTrustAndAffectedRoomShields) handleRefreshRoomShieldsForRooms(aggregator.roomsWithMembershipChangesForShieldUpdate)
} }
private fun cleanupEphemeralFiles(ephemeralFilesToDelete: List<String>) { private fun cleanupEphemeralFiles(ephemeralFilesToDelete: List<String>) {
@ -105,8 +104,8 @@ internal class SyncResponsePostTreatmentAggregatorHandler @Inject constructor(
.enqueue() .enqueue()
} }
private suspend fun handleUserIdsForCheckingTrustAndAffectedRoomShields(userIdsWithDeviceUpdate: Collection<String>) { private fun handleRefreshRoomShieldsForRooms(roomIds: Set<String>) {
if (userIdsWithDeviceUpdate.isEmpty()) return if (roomIds.isEmpty()) return
crossSigningService.checkTrustAndAffectedRoomShields(userIdsWithDeviceUpdate.toList()) roomShieldSummaryUpdater.refreshShieldsForRoomIds(roomIds)
} }
} }

View File

@ -27,10 +27,10 @@ import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.crypto.ComputeShieldForGroupUseCase
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.crypto.OlmMachine import org.matrix.android.sdk.internal.crypto.OlmMachine
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.sync.handler.ShieldSummaryUpdater
import org.matrix.rustcomponents.sdk.crypto.Request import org.matrix.rustcomponents.sdk.crypto.Request
import org.matrix.rustcomponents.sdk.crypto.RequestType import org.matrix.rustcomponents.sdk.crypto.RequestType
import timber.log.Timber import timber.log.Timber
@ -43,7 +43,7 @@ internal class OutgoingRequestsProcessor @Inject constructor(
private val requestSender: RequestSender, private val requestSender: RequestSender,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val computeShieldForGroup: ComputeShieldForGroupUseCase, private val shieldSummaryUpdater: ShieldSummaryUpdater,
private val matrixConfiguration: MatrixConfiguration, private val matrixConfiguration: MatrixConfiguration,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
) { ) {
@ -137,7 +137,7 @@ internal class OutgoingRequestsProcessor @Inject constructor(
return try { return try {
val response = requestSender.queryKeys(request) val response = requestSender.queryKeys(request)
olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_QUERY, response) olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_QUERY, response)
coroutineScope.updateShields(olmMachine, request.users) shieldSummaryUpdater.refreshShieldsForRoomsWithMembers(request.users)
coroutineScope.markMessageVerificationStatesAsDirty(request.users) coroutineScope.markMessageVerificationStatesAsDirty(request.users)
true true
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
@ -146,18 +146,6 @@ internal class OutgoingRequestsProcessor @Inject constructor(
} }
} }
private fun CoroutineScope.updateShields(olmMachine: OlmMachine, userIds: List<String>) = launch(coroutineDispatchers.computation) {
cryptoSessionInfoProvider.getRoomsWhereUsersAreParticipating(userIds).forEach { roomId ->
if (cryptoSessionInfoProvider.isRoomEncrypted(roomId)) {
val userGroup = cryptoSessionInfoProvider.getUserListForShieldComputation(roomId)
val shield = computeShieldForGroup(olmMachine, userGroup)
cryptoSessionInfoProvider.updateShieldForRoom(roomId, shield)
} else {
cryptoSessionInfoProvider.updateShieldForRoom(roomId, null)
}
}
}
private fun CoroutineScope.markMessageVerificationStatesAsDirty(userIds: List<String>) = launch(coroutineDispatchers.computation) { private fun CoroutineScope.markMessageVerificationStatesAsDirty(userIds: List<String>) = launch(coroutineDispatchers.computation) {
cryptoSessionInfoProvider.markMessageVerificationStateAsDirty(userIds) cryptoSessionInfoProvider.markMessageVerificationStateAsDirty(userIds)
} }

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2023 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.session.sync.handler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.crypto.ComputeShieldForGroupUseCase
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.crypto.OlmMachine
import org.matrix.android.sdk.internal.session.SessionScope
import javax.inject.Inject
@SessionScope
internal class ShieldSummaryUpdater @Inject constructor(
private val olmMachine: dagger.Lazy<OlmMachine>,
private val coroutineScope: CoroutineScope,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val computeShieldForGroup: ComputeShieldForGroupUseCase,
) {
fun refreshShieldsForRoomsWithMembers(userIds: List<String>) {
coroutineScope.launch(coroutineDispatchers.computation) {
cryptoSessionInfoProvider.getRoomsWhereUsersAreParticipating(userIds).forEach { roomId ->
if (cryptoSessionInfoProvider.isRoomEncrypted(roomId)) {
val userGroup = cryptoSessionInfoProvider.getUserListForShieldComputation(roomId)
val shield = computeShieldForGroup(olmMachine.get(), userGroup)
cryptoSessionInfoProvider.updateShieldForRoom(roomId, shield)
} else {
cryptoSessionInfoProvider.updateShieldForRoom(roomId, null)
}
}
}
}
fun refreshShieldsForRoomIds(roomIds: Set<String>) {
coroutineScope.launch(coroutineDispatchers.computation) {
roomIds.forEach { roomId ->
val userGroup = cryptoSessionInfoProvider.getUserListForShieldComputation(roomId)
val shield = computeShieldForGroup(olmMachine.get(), userGroup)
cryptoSessionInfoProvider.updateShieldForRoom(roomId, shield)
}
}
}
}