From ac0e5e9decbfa8ca889fef935dbb0cc1ac450f3b Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 2 Aug 2022 16:27:29 +0200 Subject: [PATCH] fix broken test, userId confustion cleaning cleaning add tests cleaning --- .../android/sdk/common/CryptoTestHelper.kt | 2 +- .../crypto/crosssigning/XSigningTest.kt | 100 ++++++- .../DefaultCrossSigningService.kt | 70 +++-- .../crypto/crosssigning/UpdateTrustWorker.kt | 1 + vector/build.gradle | 1 + .../members/RoomMemberListViewModel.kt | 45 +-- .../app/features/MemberListViewModelTest.kt | 282 ++++++++++++++++++ 7 files changed, 442 insertions(+), 59 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/features/MemberListViewModelTest.kt diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index 7b5ee97ae4..cbaa3153df 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -365,7 +365,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { } testHelper.retryPeriodically { - alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) + bob.cryptoService().crossSigningService().isUserTrusted(alice.myUserId) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt index 2bb04a1faa..bab1d0f1bf 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt @@ -25,7 +25,6 @@ import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.FixMethodOrder -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -34,6 +33,7 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.api.session.crypto.crosssigning.UserTrustResult import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerified import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo @@ -48,7 +48,6 @@ import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @LargeTest -@Ignore class XSigningTest : InstrumentedTest { @Test @@ -214,4 +213,101 @@ class XSigningTest : InstrumentedTest { val result = aliceSession.cryptoService().crossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId, null) assertTrue("Bob second device should be trusted from alice POV", result.isCrossSignedVerified()) } + + @Test + fun testWarnOnCrossSigningReset() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceAuthParams = UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD + ) + val bobAuthParams = UserPasswordAuth( + user = bobSession!!.myUserId, + password = TestConstants.PASSWORD + ) + + testHelper.doSync { + aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(aliceAuthParams) + } + }, it) + } + testHelper.doSync { + bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(bobAuthParams) + } + }, it) + } + + cryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, cryptoTestData.roomId) + + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + aliceSession.cryptoService().crossSigningService().isUserTrusted(bobSession.myUserId) + } + } + + aliceSession.cryptoService().crossSigningService().checkUserTrust(bobSession.myUserId).let { + assertTrue(it is UserTrustResult.Success) + } + + val currentBobMSK = aliceSession.cryptoService().crossSigningService() + .getUserCrossSigningKeys(bobSession.myUserId)!! + .masterKey()!!.unpaddedBase64PublicKey!! + + testHelper.doSync { + bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(bobAuthParams) + } + }, it) + } + + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + val newBobMsk = aliceSession.cryptoService().crossSigningService() + .getUserCrossSigningKeys(bobSession.myUserId) + ?.masterKey()?.unpaddedBase64PublicKey + newBobMsk != null && newBobMsk != currentBobMSK + } + } + + // trick to force event to sync + bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userIsTyping() + + // assert that bob is not trusted anymore from alice s + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + val trust = aliceSession.cryptoService().crossSigningService().checkUserTrust(bobSession.myUserId) + !trust.isVerified() + } + } + + // trick to force event to sync + bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userStopsTyping() + bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userIsTyping() + + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + val info = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobSession.myUserId) + info?.wasTrustedOnce == true + } + } + + // trick to force event to sync + bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userStopsTyping() + bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userIsTyping() + + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + !aliceSession.cryptoService().crossSigningService().isUserTrusted(bobSession.myUserId) + } + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt index e512ea503b..421dd8c244 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -60,7 +60,7 @@ import javax.inject.Inject @SessionScope internal class DefaultCrossSigningService @Inject constructor( - @UserId private val userId: String, + @UserId private val myUserId: String, @SessionId private val sessionId: String, private val cryptoStore: IMXCryptoStore, private val deviceListManager: DeviceListManager, @@ -127,7 +127,7 @@ internal class DefaultCrossSigningService @Inject constructor( } // Recover local trust in case private key are there? - setUserKeysAsTrusted(userId, checkUserTrust(userId).isVerified()) + setUserKeysAsTrusted(myUserId, checkUserTrust(myUserId).isVerified()) } } catch (e: Throwable) { // Mmm this kind of a big issue @@ -168,12 +168,12 @@ internal class DefaultCrossSigningService @Inject constructor( override fun onSuccess(data: InitializeCrossSigningTask.Result) { val crossSigningInfo = MXCrossSigningInfo( - userId, + myUserId, listOf(data.masterKeyInfo, data.userKeyInfo, data.selfSignedKeyInfo), true ) cryptoStore.setMyCrossSigningInfo(crossSigningInfo) - setUserKeysAsTrusted(userId, true) + setUserKeysAsTrusted(myUserId, true) cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK) crossSigningOlm.masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) } crossSigningOlm.userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) } @@ -270,7 +270,7 @@ internal class DefaultCrossSigningService @Inject constructor( uskKeyPrivateKey: String?, sskPrivateKey: String? ): UserTrustResult { - val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(userId) + val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) var masterKeyIsTrusted = false var userKeyIsTrusted = false @@ -334,7 +334,7 @@ internal class DefaultCrossSigningService @Inject constructor( val checkSelfTrust = checkSelfTrust() if (checkSelfTrust.isVerified()) { cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey, uskKeyPrivateKey, sskPrivateKey) - setUserKeysAsTrusted(userId, true) + setUserKeysAsTrusted(myUserId, true) } return checkSelfTrust } @@ -355,7 +355,7 @@ internal class DefaultCrossSigningService @Inject constructor( * . */ override fun isUserTrusted(otherUserId: String): Boolean { - return cryptoStore.getCrossSigningInfo(userId)?.isTrusted() == true + return cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() == true } override fun isCrossSigningVerified(): Boolean { @@ -367,7 +367,7 @@ internal class DefaultCrossSigningService @Inject constructor( */ override fun checkUserTrust(otherUserId: String): UserTrustResult { Timber.v("## CrossSigning checkUserTrust for $otherUserId") - if (otherUserId == userId) { + if (otherUserId == myUserId) { return checkSelfTrust() } // I trust a user if I trust his master key @@ -375,16 +375,14 @@ internal class DefaultCrossSigningService @Inject constructor( // TODO what if the master key is signed by a device key that i have verified // First let's get my user key - val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) + val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(myUserId) - checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId)) - - return UserTrustResult.Success + return checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId)) } fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult { val myUserKey = myCrossSigningInfo?.userKey() - ?: return UserTrustResult.CrossSigningNotConfigured(userId) + ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) if (!myCrossSigningInfo.isTrusted()) { return UserTrustResult.KeysNotTrusted(myCrossSigningInfo) @@ -395,7 +393,7 @@ internal class DefaultCrossSigningService @Inject constructor( ?: return UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "") val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures - ?.get(userId) // Signatures made by me + ?.get(myUserId) // Signatures made by me ?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}") if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) { @@ -421,9 +419,9 @@ internal class DefaultCrossSigningService @Inject constructor( // Special case when it's me, // I have to check that MSK -> USK -> SSK // and that MSK is trusted (i know the private key, or is signed by a trusted device) - val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) + val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(myUserId) - return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(userId)) + return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(myUserId)) } fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List?): UserTrustResult { @@ -433,7 +431,7 @@ internal class DefaultCrossSigningService @Inject constructor( // val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) val myMasterKey = myCrossSigningInfo?.masterKey() - ?: return UserTrustResult.CrossSigningNotConfigured(userId) + ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) // Is the master key trusted // 1) check if I know the private key @@ -457,7 +455,7 @@ internal class DefaultCrossSigningService @Inject constructor( olmPkSigning?.releaseSigning() } else { // Maybe it's signed by a locally trusted device? - myMasterKey.signatures?.get(userId)?.forEach { (key, value) -> + myMasterKey.signatures?.get(myUserId)?.forEach { (key, value) -> val potentialDeviceId = key.removePrefix("ed25519:") val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId) if (potentialDevice != null && potentialDevice.isVerified) { @@ -479,14 +477,14 @@ internal class DefaultCrossSigningService @Inject constructor( } val myUserKey = myCrossSigningInfo.userKey() - ?: return UserTrustResult.CrossSigningNotConfigured(userId) + ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) val userKeySignaturesMadeByMyMasterKey = myUserKey.signatures - ?.get(userId) // Signatures made by me + ?.get(myUserId) // Signatures made by me ?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}") if (userKeySignaturesMadeByMyMasterKey.isNullOrBlank()) { - Timber.d("## CrossSigning checkUserTrust false for $userId, USK not signed by MSK") + Timber.d("## CrossSigning checkUserTrust false for $myUserId, USK not signed by MSK") return UserTrustResult.KeyNotSigned(myUserKey) } @@ -502,14 +500,14 @@ internal class DefaultCrossSigningService @Inject constructor( } val mySSKey = myCrossSigningInfo.selfSigningKey() - ?: return UserTrustResult.CrossSigningNotConfigured(userId) + ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) val ssKeySignaturesMadeByMyMasterKey = mySSKey.signatures - ?.get(userId) // Signatures made by me + ?.get(myUserId) // Signatures made by me ?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}") if (ssKeySignaturesMadeByMyMasterKey.isNullOrBlank()) { - Timber.d("## CrossSigning checkUserTrust false for $userId, SSK not signed by MSK") + Timber.d("## CrossSigning checkUserTrust false for $myUserId, SSK not signed by MSK") return UserTrustResult.KeyNotSigned(mySSKey) } @@ -559,14 +557,14 @@ internal class DefaultCrossSigningService @Inject constructor( override fun trustUser(otherUserId: String, callback: MatrixCallback) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - Timber.d("## CrossSigning - Mark user $userId as trusted ") + Timber.d("## CrossSigning - Mark user $otherUserId as trusted ") // We should have this user keys val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey() if (otherMasterKeys == null) { callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known")) return@launch } - val myKeys = getUserCrossSigningKeys(userId) + val myKeys = getUserCrossSigningKeys(myUserId) if (myKeys == null) { callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account")) return@launch @@ -592,9 +590,9 @@ internal class DefaultCrossSigningService @Inject constructor( cryptoStore.setUserKeysAsTrusted(otherUserId, true) // TODO update local copy with new signature directly here? kind of local echo of trust? - Timber.d("## CrossSigning - Upload signature of $userId MSK signed by USK") + Timber.d("## CrossSigning - Upload signature of $otherUserId MSK signed by USK") val uploadQuery = UploadSignatureQueryBuilder() - .withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature)) + .withSigningKeyInfo(otherMasterKeys.copyForSignature(myUserId, userPubKey, newSignature)) .build() uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { this.executionThread = TaskThread.CRYPTO @@ -608,20 +606,20 @@ internal class DefaultCrossSigningService @Inject constructor( cryptoStore.markMyMasterKeyAsLocallyTrusted(true) checkSelfTrust() // re-verify all trusts - onUsersDeviceUpdate(listOf(userId)) + onUsersDeviceUpdate(listOf(myUserId)) } } override fun trustDevice(deviceId: String, callback: MatrixCallback) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { // This device should be yours - val device = cryptoStore.getUserDevice(userId, deviceId) + val device = cryptoStore.getUserDevice(myUserId, deviceId) if (device == null) { callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours")) return@launch } - val myKeys = getUserCrossSigningKeys(userId) + val myKeys = getUserCrossSigningKeys(myUserId) if (myKeys == null) { callback.onFailure(Throwable("CrossSigning is not setup for this account")) return@launch @@ -643,7 +641,7 @@ internal class DefaultCrossSigningService @Inject constructor( } val toUpload = device.copy( signatures = mapOf( - userId + myUserId to mapOf( "ed25519:$ssPubKey" to newSignature @@ -665,8 +663,8 @@ internal class DefaultCrossSigningService @Inject constructor( val otherDevice = cryptoStore.getUserDevice(otherUserId, otherDeviceId) ?: return DeviceTrustResult.UnknownDevice(otherDeviceId) - val myKeys = getUserCrossSigningKeys(userId) - ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId)) + val myKeys = getUserCrossSigningKeys(myUserId) + ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(myUserId)) if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys)) @@ -721,7 +719,7 @@ internal class DefaultCrossSigningService @Inject constructor( fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo): DeviceTrustResult { val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified() - myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId)) + myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(myUserId)) if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys)) @@ -809,7 +807,7 @@ internal class DefaultCrossSigningService @Inject constructor( cryptoStore.setUserKeysAsTrusted(otherUserId, trusted) // If it's me, recheck trust of all users and devices? val users = ArrayList() - if (otherUserId == userId && currentTrust != trusted) { + if (otherUserId == myUserId && currentTrust != trusted) { // notify key requester outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted) cryptoStore.updateUsersTrust { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt index bde68a726d..fffc6707d7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt @@ -161,6 +161,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses // i have all the new trusts, update DB trusts.forEach { val verified = it.value?.isVerified() == true + Timber.v("[$myUserId] ## CrossSigning - Updating user trust: ${it.key} to $verified") updateCrossSigningKeysTrust(cryptoRealm, it.key, verified) } diff --git a/vector/build.gradle b/vector/build.gradle index ff0d907212..763d968642 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -289,6 +289,7 @@ dependencies { testImplementation libs.tests.junit testImplementation libs.tests.kluent testImplementation libs.mockk.mockk + testImplementation libs.androidx.coreTesting // Plant Timber tree for test testImplementation libs.tests.timberJunitRule testImplementation libs.airbnb.mavericksTesting diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt index 31b6430361..9ddcde7e4a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt @@ -37,6 +37,7 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel @@ -116,26 +117,7 @@ class RoomMemberListViewModel @AssistedInject constructor( .map { deviceList -> // If any key change, emit the userIds list deviceList.groupBy { it.userId }.mapValues { - val allDeviceTrusted = it.value.fold(it.value.isNotEmpty()) { prev, next -> - prev && next.trustLevel?.isCrossSigningVerified().orFalse() - } - val mxCrossSigningInfo = session.cryptoService().crossSigningService().getUserCrossSigningKeys(it.key) - when { - mxCrossSigningInfo == null -> { - UserVerificationLevel.WAS_NEVER_VERIFIED - } - mxCrossSigningInfo.isTrusted() -> { - if (allDeviceTrusted) UserVerificationLevel.VERIFIED_ALL_DEVICES_TRUSTED - else UserVerificationLevel.VERIFIED_WITH_DEVICES_UNTRUSTED - } - else -> { - if (mxCrossSigningInfo.wasTrustedOnce) { - UserVerificationLevel.UNVERIFIED_BUT_WAS_PREVIOUSLY - } else { - UserVerificationLevel.WAS_NEVER_VERIFIED - } - } - } + getUserTrustLevel(it.key, it.value) } } } @@ -145,6 +127,29 @@ class RoomMemberListViewModel @AssistedInject constructor( } } + private fun getUserTrustLevel(userId: String, devices: List): UserVerificationLevel { + val allDeviceTrusted = devices.fold(devices.isNotEmpty()) { prev, next -> + prev && next.trustLevel?.isCrossSigningVerified().orFalse() + } + val mxCrossSigningInfo = session.cryptoService().crossSigningService().getUserCrossSigningKeys(userId) + return when { + mxCrossSigningInfo == null -> { + UserVerificationLevel.WAS_NEVER_VERIFIED + } + mxCrossSigningInfo.isTrusted() -> { + if (allDeviceTrusted) UserVerificationLevel.VERIFIED_ALL_DEVICES_TRUSTED + else UserVerificationLevel.VERIFIED_WITH_DEVICES_UNTRUSTED + } + else -> { + if (mxCrossSigningInfo.wasTrustedOnce) { + UserVerificationLevel.UNVERIFIED_BUT_WAS_PREVIOUSLY + } else { + UserVerificationLevel.WAS_NEVER_VERIFIED + } + } + } + } + private fun observePowerLevel() { PowerLevelsFlowFactory(room).createFlow() .onEach { diff --git a/vector/src/test/java/im/vector/app/features/MemberListViewModelTest.kt b/vector/src/test/java/im/vector/app/features/MemberListViewModelTest.kt new file mode 100644 index 0000000000..3b4966a21e --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/MemberListViewModelTest.kt @@ -0,0 +1,282 @@ +/* + * 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 im.vector.app.features + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.MutableLiveData +import com.airbnb.mvrx.test.MvRxTestRule +import im.vector.app.features.roomprofile.RoomProfileArgs +import im.vector.app.features.roomprofile.members.RoomMemberListViewModel +import im.vector.app.features.roomprofile.members.RoomMemberListViewState +import im.vector.app.features.roomprofile.members.RoomMemberSummaryComparator +import im.vector.app.test.test +import im.vector.app.test.testCoroutineDispatchers +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import org.junit.Rule +import org.junit.Test +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey +import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.RoomService +import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService +import org.matrix.android.sdk.api.session.room.members.MembershipService +import org.matrix.android.sdk.api.session.room.model.Membership +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.state.StateService +import org.matrix.android.sdk.api.session.user.UserService +import org.matrix.android.sdk.api.util.Optional + +class MemberListViewModelTest { + + @get:Rule + val mvrxTestRule = MvRxTestRule() + + @get:Rule + val instantExecutorRule = InstantTaskExecutorRule() + + private val fakeRoomId = "!roomId" + private val args = RoomProfileArgs(fakeRoomId) + + private val aliceMxid = "@alice:example.com" + private val bobMxid = "@bob:example.com" + private val marcMxid = "@marc:example.com" + + private val aliceDevice1 = CryptoDeviceInfo( + deviceId = "ALICE_1", + userId = aliceMxid, + trustLevel = DeviceTrustLevel(true, true) + ) + + private val aliceDevice2 = CryptoDeviceInfo( + deviceId = "ALICE_2", + userId = aliceMxid, + trustLevel = DeviceTrustLevel(false, false) + ) + + private val bobDevice1 = CryptoDeviceInfo( + deviceId = "BOB_1", + userId = bobMxid, + trustLevel = DeviceTrustLevel(true, true) + ) + + private val bobDevice2 = CryptoDeviceInfo( + deviceId = "BOB_2", + userId = bobMxid, + trustLevel = DeviceTrustLevel(true, true) + ) + + private val markDevice = CryptoDeviceInfo( + deviceId = "MARK_1", + userId = marcMxid, + trustLevel = DeviceTrustLevel(false, true) + ) + + private val fakeMembershipservice: MembershipService = mockk { + + val memberList = mutableListOf( + RoomMemberSummary(Membership.JOIN, aliceMxid, displayName = "Alice"), + RoomMemberSummary(Membership.JOIN, bobMxid, displayName = "Bob"), + RoomMemberSummary(Membership.JOIN, marcMxid, displayName = "marc") + ) + + every { getRoomMembers(any()) } returns memberList + + every { getRoomMembersLive(any()) } returns MutableLiveData(memberList) + + every { areAllMembersLoadedLive() } returns MutableLiveData(true) + + coEvery { areAllMembersLoaded() } returns true + } + + private val fakeRoomCryptoService: RoomCryptoService = mockk { + every { isEncrypted() } returns true + } + private val fakeRoom: Room = mockk { + + val fakeStateService: StateService = mockk { + every { getStateEventLive(any(), any()) } returns MutableLiveData() + every { getStateEventsLive(any(), any()) } returns MutableLiveData() + every { getStateEvent(any(), any()) } returns null + } + + every { stateService() } returns fakeStateService + + every { coroutineDispatchers } returns testCoroutineDispatchers + + every { getRoomSummaryLive() } returns MutableLiveData>(Optional(fakeRoomSummary)) + + every { membershipService() } returns fakeMembershipservice + + every { roomCryptoService() } returns fakeRoomCryptoService + + every { roomSummary() } returns fakeRoomSummary + } + + private val fakeUserService: UserService = mockk { + every { getIgnoredUsersLive() } returns MutableLiveData() + } + + val fakeSession: Session = mockk { + + val fakeCrossSigningService: CrossSigningService = mockk { + every { isUserTrusted(aliceMxid) } returns true + every { isUserTrusted(bobMxid) } returns true + every { isUserTrusted(marcMxid) } returns false + + every { getUserCrossSigningKeys(aliceMxid) } returns MXCrossSigningInfo( + aliceMxid, + crossSigningKeys = listOf( + CryptoCrossSigningKey( + aliceMxid, + usages = listOf("master"), + keys = emptyMap(), + trustLevel = DeviceTrustLevel(true, true), + signatures = emptyMap() + ), + CryptoCrossSigningKey( + aliceMxid, + usages = listOf("self_signing"), + keys = emptyMap(), + trustLevel = DeviceTrustLevel(true, true), + signatures = emptyMap() + ), + CryptoCrossSigningKey( + aliceMxid, + usages = listOf("user_signing"), + keys = emptyMap(), + trustLevel = DeviceTrustLevel(true, true), + signatures = emptyMap() + ) + ), + true + ) + every { getUserCrossSigningKeys(bobMxid) } returns MXCrossSigningInfo( + aliceMxid, + crossSigningKeys = listOf( + CryptoCrossSigningKey( + bobMxid, + usages = listOf("master"), + keys = emptyMap(), + trustLevel = DeviceTrustLevel(true, true), + signatures = emptyMap() + ), + CryptoCrossSigningKey( + bobMxid, + usages = listOf("self_signing"), + keys = emptyMap(), + trustLevel = DeviceTrustLevel(true, true), + signatures = emptyMap() + ), + CryptoCrossSigningKey( + bobMxid, + usages = listOf("user_signing"), + keys = emptyMap(), + trustLevel = DeviceTrustLevel(true, true), + signatures = emptyMap() + ) + ), + true + ) + every { getUserCrossSigningKeys(marcMxid) } returns MXCrossSigningInfo( + aliceMxid, + crossSigningKeys = listOf( + CryptoCrossSigningKey( + marcMxid, + usages = listOf("master"), + keys = emptyMap(), + trustLevel = DeviceTrustLevel(false, false), + signatures = emptyMap() + ), + CryptoCrossSigningKey( + marcMxid, + usages = listOf("self_signing"), + keys = emptyMap(), + trustLevel = DeviceTrustLevel(false, false), + signatures = emptyMap() + ), + CryptoCrossSigningKey( + marcMxid, + usages = listOf("user_signing"), + keys = emptyMap(), + trustLevel = DeviceTrustLevel(false, false), + signatures = emptyMap() + ) + ), + true + ) + } + + val fakeCryptoService: CryptoService = mockk { + every { crossSigningService() } returns fakeCrossSigningService + + every { + getLiveCryptoDeviceInfo(listOf(aliceMxid, bobMxid, marcMxid)) + } returns MutableLiveData( + listOf( + aliceDevice1, aliceDevice2, bobDevice1, bobDevice2, markDevice + ) + ) + } + + val fakeRoomService: RoomService = mockk { + every { getRoom(any()) } returns fakeRoom + } + every { roomService() } returns fakeRoomService + every { userService() } returns fakeUserService + every { cryptoService() } returns fakeCryptoService + } + + private val fakeRoomSummary = RoomSummary( + roomId = fakeRoomId, + displayName = "Fake Room", + topic = "A topic", + isEncrypted = true, + encryptionEventTs = 0, + typingUsers = emptyList(), + ) + + @Test + fun testBasicUserVerificationLevels() { + val viewModel = createViewModel() + viewModel + .test() + .assertPredicateLatestState { + val trustMap = it.trustLevelMap.invoke() ?: return@assertPredicateLatestState false + trustMap[aliceMxid] == UserVerificationLevel.VERIFIED_WITH_DEVICES_UNTRUSTED && + trustMap[bobMxid] == UserVerificationLevel.VERIFIED_ALL_DEVICES_TRUSTED && + trustMap[marcMxid] == UserVerificationLevel.UNVERIFIED_BUT_WAS_PREVIOUSLY + } + .finish() + } + + private fun createViewModel(): RoomMemberListViewModel { + return RoomMemberListViewModel( + RoomMemberListViewState(args), + RoomMemberSummaryComparator(), + fakeSession, + ) + } +}