diff --git a/changelog.d/6702.bugfix b/changelog.d/6702.bugfix new file mode 100644 index 0000000000..a1d646cf71 --- /dev/null +++ b/changelog.d/6702.bugfix @@ -0,0 +1 @@ +Add Warning shield when a user previously verified rotated their cross signing keys diff --git a/dependencies.gradle b/dependencies.gradle index f4165ad692..f5d64a78d1 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -167,7 +167,8 @@ ext.libs = [ tests : [ 'kluent' : "org.amshove.kluent:kluent-android:1.68", 'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1", - 'junit' : "junit:junit:4.13.2" + 'junit' : "junit:junit:4.13.2", + 'robolectric' : "org.robolectric:robolectric:4.8", ] ] 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..c4fb896934 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 @@ -42,13 +41,13 @@ import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants +import timber.log.Timber import kotlin.coroutines.Continuation import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @LargeTest -@Ignore class XSigningTest : InstrumentedTest { @Test @@ -214,4 +213,104 @@ 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.waitForCallback { + aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(aliceAuthParams) + } + }, it) + } + testHelper.waitForCallback { + 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.retryPeriodically { + aliceSession.cryptoService().crossSigningService().isUserTrusted(bobSession.myUserId) + } + + testHelper.retryPeriodically { + aliceSession.cryptoService().crossSigningService().checkUserTrust(bobSession.myUserId).isVerified() + } + + aliceSession.cryptoService() + // Ensure also that bob device is trusted + testHelper.retryPeriodically { + val deviceInfo = aliceSession.cryptoService().getUserDevices(bobSession.myUserId).firstOrNull() + Timber.v("#TEST device:${deviceInfo?.shortDebugString()} trust ${deviceInfo?.trustLevel}") + deviceInfo?.trustLevel?.crossSigningVerified == true + } + + val currentBobMSK = aliceSession.cryptoService().crossSigningService() + .getUserCrossSigningKeys(bobSession.myUserId)!! + .masterKey()!!.unpaddedBase64PublicKey!! + + testHelper.waitForCallback { + bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(bobAuthParams) + } + }, it) + } + + testHelper.retryPeriodically { + 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.retryPeriodically { + 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.retryPeriodically { + 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.retryPeriodically { + !aliceSession.cryptoService().crossSigningService().isUserTrusted(bobSession.myUserId) + } + + // Ensure also that bob device are not trusted + testHelper.retryPeriodically { + aliceSession.cryptoService().getUserDevices(bobSession.myUserId).first().trustLevel?.crossSigningVerified != true + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt index 9604decd62..30a2cfd719 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt @@ -18,7 +18,8 @@ package org.matrix.android.sdk.api.session.crypto.crosssigning data class MXCrossSigningInfo( val userId: String, - val crossSigningKeys: List + val crossSigningKeys: List, + val wasTrustedOnce: Boolean ) { fun isTrusted(): Boolean = masterKey()?.trustLevel?.isVerified() == true && diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/UserVerificationLevel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/UserVerificationLevel.kt new file mode 100644 index 0000000000..e3c7057b6b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/UserVerificationLevel.kt @@ -0,0 +1,28 @@ +/* + * 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.api.session.crypto.model + +enum class UserVerificationLevel { + + VERIFIED_ALL_DEVICES_TRUSTED, + + VERIFIED_WITH_DEVICES_UNTRUSTED, + + UNVERIFIED_BUT_WAS_PREVIOUSLY, + + WAS_NEVER_VERIFIED, +} 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 d405bdce27..f4796155c6 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 @@ -167,9 +167,13 @@ internal class DefaultCrossSigningService @Inject constructor( } override fun onSuccess(data: InitializeCrossSigningTask.Result) { - val crossSigningInfo = MXCrossSigningInfo(userId, listOf(data.masterKeyInfo, data.userKeyInfo, data.selfSignedKeyInfo)) + val crossSigningInfo = MXCrossSigningInfo( + 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()) } @@ -266,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 @@ -330,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 } @@ -351,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 { @@ -363,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 @@ -371,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) @@ -391,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()) { @@ -417,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 { @@ -429,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 @@ -453,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) { @@ -475,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) } @@ -498,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) } @@ -555,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 @@ -586,16 +588,22 @@ 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 this.callback = callback }.executeBy(taskExecutor) + + // Local echo for device cross trust, to avoid having to wait for a notification of key change + cryptoStore.getUserDeviceList(otherUserId)?.forEach { device -> + val updatedTrust = checkDeviceTrust(device.userId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) + Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") + cryptoStore.setDeviceTrust(device.userId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) + } } } @@ -604,20 +612,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 @@ -639,7 +647,7 @@ internal class DefaultCrossSigningService @Inject constructor( } val toUpload = device.copy( signatures = mapOf( - userId + myUserId to mapOf( "ed25519:$ssPubKey" to newSignature @@ -661,8 +669,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)) @@ -717,7 +725,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)) @@ -805,7 +813,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 6d845ec59e..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) } @@ -259,21 +260,27 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses cryptoRealm.where(CrossSigningInfoEntity::class.java) .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) .findFirst() - ?.crossSigningKeys - ?.forEach { info -> - // optimization to avoid trigger updates when there is no change.. - if (info.trustLevelEntity?.isVerified() != verified) { - Timber.d("## CrossSigning - Trust change for $userId : $verified") - val level = info.trustLevelEntity - if (level == null) { - info.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also { - it.locallyVerified = verified - it.crossSignedVerified = verified + ?.let { userKeyInfo -> + userKeyInfo + .crossSigningKeys + .forEach { key -> + // optimization to avoid trigger updates when there is no change.. + if (key.trustLevelEntity?.isVerified() != verified) { + Timber.d("## CrossSigning - Trust change for $userId : $verified") + val level = key.trustLevelEntity + if (level == null) { + key.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also { + it.locallyVerified = verified + it.crossSignedVerified = verified + } + } else { + level.locallyVerified = verified + level.crossSignedVerified = verified + } + } } - } else { - level.locallyVerified = verified - level.crossSignedVerified = verified - } + if (verified) { + userKeyInfo.wasUserVerifiedOnce = true } } } @@ -299,8 +306,18 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses getCrossSigningInfo(cryptoRealm, userId)?.isTrusted() == true } + val resetTrust = listToCheck + .filter { userId -> + val crossSigningInfo = getCrossSigningInfo(cryptoRealm, userId) + crossSigningInfo?.isTrusted() != true && crossSigningInfo?.wasTrustedOnce == true + } + return if (allTrustedUserIds.isEmpty()) { - RoomEncryptionTrustLevel.Default + if (resetTrust.isEmpty()) { + RoomEncryptionTrustLevel.Default + } else { + RoomEncryptionTrustLevel.Warning + } } else { // If one of the verified user as an untrusted device -> warning // If all devices of all verified users are trusted -> green @@ -327,11 +344,15 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses if (hasWarning) { RoomEncryptionTrustLevel.Warning } else { - if (listToCheck.size == allTrustedUserIds.size) { - // all users are trusted and all devices are verified - RoomEncryptionTrustLevel.Trusted + if (resetTrust.isEmpty()) { + if (listToCheck.size == allTrustedUserIds.size) { + // all users are trusted and all devices are verified + RoomEncryptionTrustLevel.Trusted + } else { + RoomEncryptionTrustLevel.Default + } } else { - RoomEncryptionTrustLevel.Default + RoomEncryptionTrustLevel.Warning } } } @@ -344,7 +365,8 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses userId = userId, crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull { crossSigningKeysMapper.map(userId, it) - } + }, + wasTrustedOnce = xsignInfo.wasUserVerifiedOnce ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index 3b8fa4cacd..6a2ef3bde1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -1611,7 +1611,8 @@ internal class RealmCryptoStore @Inject constructor( userId = userId, crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull { crossSigningKeysMapper.map(userId, it) - } + }, + wasTrustedOnce = xsignInfo.wasUserVerifiedOnce ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt index 426d50a54f..de2b74308d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo018 +import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo019 import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject @@ -49,7 +50,7 @@ internal class RealmCryptoStoreMigration @Inject constructor( private val clock: Clock, ) : MatrixRealmMigration( dbName = "Crypto", - schemaVersion = 18L, + schemaVersion = 19L, ) { /** * Forces all RealmCryptoStoreMigration instances to be equal. @@ -77,5 +78,6 @@ internal class RealmCryptoStoreMigration @Inject constructor( if (oldVersion < 16) MigrateCryptoTo016(realm).perform() if (oldVersion < 17) MigrateCryptoTo017(realm).perform() if (oldVersion < 18) MigrateCryptoTo018(realm).perform() + if (oldVersion < 19) MigrateCryptoTo019(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo019.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo019.kt new file mode 100644 index 0000000000..9d2eb60a60 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo019.kt @@ -0,0 +1,59 @@ +/* + * 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.internal.crypto.store.db.migration + +import io.realm.DynamicRealm +import io.realm.DynamicRealmObject +import org.matrix.android.sdk.api.session.crypto.crosssigning.KeyUsage +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +/** + * This migration is adding support for trusted flags on megolm sessions. + * We can't really assert the trust of existing keys, so for the sake of simplicity we are going to + * mark existing keys as safe. + * This migration can take long depending on the account + */ +internal class MigrateCryptoTo019(realm: DynamicRealm) : RealmMigrator(realm, 18) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("CrossSigningInfoEntity") + ?.addField(CrossSigningInfoEntityFields.WAS_USER_VERIFIED_ONCE, Boolean::class.java) + ?.transform { dynamicObject -> + + val knowKeys = dynamicObject.getList(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`) + val msk = knowKeys.firstOrNull { + it.getList(KeyInfoEntityFields.USAGES.`$`, String::class.java).orEmpty().contains(KeyUsage.MASTER.value) + } + val ssk = knowKeys.firstOrNull { + it.getList(KeyInfoEntityFields.USAGES.`$`, String::class.java).orEmpty().contains(KeyUsage.SELF_SIGNING.value) + } + val isTrusted = isDynamicKeyInfoTrusted(msk?.get(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`)) && + isDynamicKeyInfoTrusted(ssk?.get(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`)) + + dynamicObject.setBoolean(CrossSigningInfoEntityFields.WAS_USER_VERIFIED_ONCE, isTrusted) + } + } + + private fun isDynamicKeyInfoTrusted(keyInfo: DynamicRealmObject?): Boolean { + if (keyInfo == null) return false + return !keyInfo.isNull(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED) && keyInfo.getBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED) && + !keyInfo.isNull(TrustLevelEntityFields.LOCALLY_VERIFIED) && keyInfo.getBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt index 5aba9bb9ba..033b7662c5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.internal.extensions.clearWith internal open class CrossSigningInfoEntity( @PrimaryKey var userId: String? = null, + var wasUserVerifiedOnce: Boolean = false, var crossSigningKeys: RealmList = RealmList() ) : RealmObject() { diff --git a/vector/build.gradle b/vector/build.gradle index ff0d907212..7bc97b1a57 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -289,6 +289,8 @@ dependencies { testImplementation libs.tests.junit testImplementation libs.tests.kluent testImplementation libs.mockk.mockk + testImplementation libs.androidx.coreTesting + testImplementation libs.tests.robolectric // Plant Timber tree for test testImplementation libs.tests.timberJunitRule testImplementation libs.airbnb.mavericksTesting diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/BaseProfileMatrixItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/BaseProfileMatrixItem.kt index 9c5ad49339..ef22aba624 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/profiles/BaseProfileMatrixItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/BaseProfileMatrixItem.kt @@ -26,7 +26,7 @@ import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer -import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel import org.matrix.android.sdk.api.util.MatrixItem abstract class BaseProfileMatrixItem(@LayoutRes layoutId: Int) : VectorEpoxyModel(layoutId) { @@ -35,7 +35,7 @@ abstract class BaseProfileMatrixItem(@LayoutRes la @EpoxyAttribute var editable: Boolean = true @EpoxyAttribute - var userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null + var userVerificationLevel: UserVerificationLevel? = null @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var clickListener: ClickListener? = null @@ -53,6 +53,6 @@ abstract class BaseProfileMatrixItem(@LayoutRes la holder.subtitleView.setTextOrHide(matrixId) holder.editableView.isVisible = editable avatarRenderer.render(matrixItem, holder.avatarImageView) - holder.avatarDecorationImageView.render(userEncryptionTrustLevel) + holder.avatarDecorationImageView.renderUser(userVerificationLevel) } } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt index 4642fb8525..1990859668 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt @@ -24,6 +24,7 @@ import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel class ShieldImageView @JvmOverloads constructor( context: Context, @@ -102,6 +103,35 @@ class ShieldImageView @JvmOverloads constructor( } } } + + fun renderUser(userVerificationLevel: UserVerificationLevel?, borderLess: Boolean = false) { + isVisible = userVerificationLevel != null + when (userVerificationLevel) { + UserVerificationLevel.VERIFIED_ALL_DEVICES_TRUSTED -> { + contentDescription = context.getString(R.string.a11y_trust_level_trusted) + setImageResource( + if (borderLess) R.drawable.ic_shield_trusted_no_border + else R.drawable.ic_shield_trusted + ) + } + UserVerificationLevel.UNVERIFIED_BUT_WAS_PREVIOUSLY, + UserVerificationLevel.VERIFIED_WITH_DEVICES_UNTRUSTED -> { + contentDescription = context.getString(R.string.a11y_trust_level_warning) + setImageResource( + if (borderLess) R.drawable.ic_shield_warning_no_border + else R.drawable.ic_shield_warning + ) + } + UserVerificationLevel.WAS_NEVER_VERIFIED -> { + contentDescription = context.getString(R.string.a11y_trust_level_default) + setImageResource( + if (borderLess) R.drawable.ic_shield_black_no_border + else R.drawable.ic_shield_black + ) + } + null -> Unit + } + } } @DrawableRes diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt index 2894cd4621..65d28a5ceb 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt @@ -59,7 +59,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorPr import im.vector.app.features.roommemberprofile.devices.DeviceListBottomSheet import im.vector.app.features.roommemberprofile.powerlevel.EditPowerLevelDialogs import kotlinx.parcelize.Parcelize -import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject @@ -235,23 +235,27 @@ class RoomMemberProfileFragment : if (state.userMXCrossSigningInfo.isTrusted()) { // User is trusted if (state.allDevicesAreCrossSignedTrusted) { - RoomEncryptionTrustLevel.Trusted + UserVerificationLevel.VERIFIED_ALL_DEVICES_TRUSTED } else { - RoomEncryptionTrustLevel.Warning + UserVerificationLevel.VERIFIED_WITH_DEVICES_UNTRUSTED } } else { - RoomEncryptionTrustLevel.Default + if (state.userMXCrossSigningInfo.wasTrustedOnce) { + UserVerificationLevel.UNVERIFIED_BUT_WAS_PREVIOUSLY + } else { + UserVerificationLevel.WAS_NEVER_VERIFIED + } } } else { // Legacy if (state.allDevicesAreTrusted) { - RoomEncryptionTrustLevel.Trusted + UserVerificationLevel.VERIFIED_ALL_DEVICES_TRUSTED } else { - RoomEncryptionTrustLevel.Warning + UserVerificationLevel.VERIFIED_WITH_DEVICES_UNTRUSTED } } - headerViews.memberProfileDecorationImageView.render(trustLevel) - views.matrixProfileDecorationToolbarAvatarImageView.render(trustLevel) + headerViews.memberProfileDecorationImageView.renderUser(trustLevel) + views.matrixProfileDecorationToolbarAvatarImageView.renderUser(trustLevel) } else { headerViews.memberProfileDecorationImageView.isVisible = false } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt index 8f310a6a89..9adfeb2a0e 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt @@ -129,7 +129,7 @@ class RoomMemberListController @Inject constructor( id(roomMember.userId) matrixItem(roomMember.toMatrixItem()) avatarRenderer(host.avatarRenderer) - userEncryptionTrustLevel(data.trustLevelMap.invoke()?.get(roomMember.userId)) + userVerificationLevel(data.trustLevelMap.invoke()?.get(roomMember.userId)) clickListener { host.callback?.onRoomMemberClicked(roomMember) } 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 915ce51d91..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,7 +37,8 @@ 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.RoomEncryptionTrustLevel +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 import org.matrix.android.sdk.api.session.getRoom @@ -116,14 +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() - } - if (session.cryptoService().crossSigningService().getUserCrossSigningKeys(it.key)?.isTrusted().orFalse()) { - if (allDeviceTrusted) RoomEncryptionTrustLevel.Trusted else RoomEncryptionTrustLevel.Warning - } else { - RoomEncryptionTrustLevel.Default - } + getUserTrustLevel(it.key, it.value) } } } @@ -133,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/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt index 3cea47e60d..7861970c28 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt @@ -23,7 +23,7 @@ import com.airbnb.mvrx.Uninitialized import im.vector.app.R import im.vector.app.core.platform.GenericIdArgs import im.vector.app.features.roomprofile.RoomProfileArgs -import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -36,7 +36,7 @@ data class RoomMemberListViewState( val ignoredUserIds: List = emptyList(), val filter: String = "", val threePidInvites: Async> = Uninitialized, - val trustLevelMap: Async> = Uninitialized, + val trustLevelMap: Async> = Uninitialized, val actionsPermissions: ActionPermissions = ActionPermissions() ) : MavericksState { diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt index 5e6efcc816..3b74b4b38b 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt @@ -77,7 +77,7 @@ class SpacePeopleListController @Inject constructor( id(roomMember.userId) matrixItem(roomMember.toMatrixItem()) avatarRenderer(host.avatarRenderer) - userEncryptionTrustLevel(data.trustLevelMap.invoke()?.get(roomMember.userId)) + userVerificationLevel(data.trustLevelMap.invoke()?.get(roomMember.userId)) .apply { val pl = host.toPowerLevelLabel(memberEntry.first) if (memberEntry.first == RoomMemberListCategories.INVITE) { 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..5cad1fbc39 --- /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() + .assertLatestState { + val trustMap = it.trustLevelMap.invoke() ?: return@assertLatestState 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, + ) + } +} diff --git a/vector/src/test/java/im/vector/app/features/RoomMemberListControllerTest.kt b/vector/src/test/java/im/vector/app/features/RoomMemberListControllerTest.kt new file mode 100644 index 0000000000..d32c3b5532 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/RoomMemberListControllerTest.kt @@ -0,0 +1,132 @@ +/* + * 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 com.airbnb.mvrx.Success +import com.airbnb.mvrx.test.MvRxTestRule +import im.vector.app.core.epoxy.profiles.ProfileMatrixItemWithPowerLevelWithPresence +import im.vector.app.features.roomprofile.members.RoomMemberListCategories +import im.vector.app.features.roomprofile.members.RoomMemberListController +import im.vector.app.features.roomprofile.members.RoomMemberListViewState +import io.mockk.every +import io.mockk.mockk +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel +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.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class RoomMemberListControllerTest { + + @get:Rule + val mvrxTestRule = MvRxTestRule() + + @get:Rule + val instantExecutorRule = InstantTaskExecutorRule() + + @Test + fun testControllerUserVerificationLevel() { + val roomListController = RoomMemberListController( + avatarRenderer = mockk { + }, + stringProvider = mockk { + every { getString(any()) } answers { + this.args[0].toString() + } + }, + colorProvider = mockk { + every { getColorFromAttribute(any()) } returns 0x0 + }, + roomMemberSummaryFilter = mockk(relaxed = true) { + every { test(any()) } returns true + } + ) + + val fakeRoomSummary = RoomSummary( + roomId = "!roomId", + displayName = "Fake Room", + topic = "A topic", + isEncrypted = true, + encryptionEventTs = 0, + typingUsers = emptyList(), + ) + + val state = RoomMemberListViewState( + roomId = "!roomId", + roomSummary = Success(fakeRoomSummary), + areAllMembersLoaded = true, + roomMemberSummaries = Success( + listOf( + RoomMemberListCategories.USER to listOf( + RoomMemberSummary( + membership = Membership.JOIN, + userId = "@alice:example.com" + ), + RoomMemberSummary( + membership = Membership.JOIN, + userId = "@bob:example.com" + ), + RoomMemberSummary( + membership = Membership.JOIN, + userId = "@carl:example.com" + ), + RoomMemberSummary( + membership = Membership.JOIN, + userId = "@massy:example.com" + ) + ) + ) + ), + trustLevelMap = Success( + mapOf( + "@alice:example.com" to UserVerificationLevel.UNVERIFIED_BUT_WAS_PREVIOUSLY, + "@bob:example.com" to UserVerificationLevel.VERIFIED_ALL_DEVICES_TRUSTED, + "@carl:example.com" to UserVerificationLevel.WAS_NEVER_VERIFIED, + "@massy:example.com" to UserVerificationLevel.VERIFIED_WITH_DEVICES_UNTRUSTED, + ) + ) + ) + + roomListController.setData(state) + + val models = roomListController.adapter.copyOfModels + + val profileItems = models.filterIsInstance() + + profileItems.firstOrNull { + it.matrixItem.id == "@alice:example.com" + }!!.userVerificationLevel shouldBeEqualTo UserVerificationLevel.UNVERIFIED_BUT_WAS_PREVIOUSLY + + profileItems.firstOrNull { + it.matrixItem.id == "@bob:example.com" + }!!.userVerificationLevel shouldBeEqualTo UserVerificationLevel.VERIFIED_ALL_DEVICES_TRUSTED + + profileItems.firstOrNull { + it.matrixItem.id == "@carl:example.com" + }!!.userVerificationLevel shouldBeEqualTo UserVerificationLevel.WAS_NEVER_VERIFIED + + profileItems.firstOrNull { + it.matrixItem.id == "@massy:example.com" + }!!.userVerificationLevel shouldBeEqualTo UserVerificationLevel.VERIFIED_WITH_DEVICES_UNTRUSTED + } +}