Merge pull request #6703 from vector-im/feature/bca/crosssigning_reset_warning
warn on cross signing reset
This commit is contained in:
commit
005e712396
|
@ -0,0 +1 @@
|
||||||
|
Add Warning shield when a user previously verified rotated their cross signing keys
|
|
@ -167,7 +167,8 @@ ext.libs = [
|
||||||
tests : [
|
tests : [
|
||||||
'kluent' : "org.amshove.kluent:kluent-android:1.68",
|
'kluent' : "org.amshove.kluent:kluent-android:1.68",
|
||||||
'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1",
|
'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",
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -365,7 +365,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
|
||||||
}
|
}
|
||||||
|
|
||||||
testHelper.retryPeriodically {
|
testHelper.retryPeriodically {
|
||||||
alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
|
bob.cryptoService().crossSigningService().isUserTrusted(alice.myUserId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,6 @@ import org.junit.Assert.assertNull
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Assert.fail
|
import org.junit.Assert.fail
|
||||||
import org.junit.FixMethodOrder
|
import org.junit.FixMethodOrder
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.MethodSorters
|
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.CommonTestHelper.Companion.runSessionTest
|
||||||
import org.matrix.android.sdk.common.SessionTestParams
|
import org.matrix.android.sdk.common.SessionTestParams
|
||||||
import org.matrix.android.sdk.common.TestConstants
|
import org.matrix.android.sdk.common.TestConstants
|
||||||
|
import timber.log.Timber
|
||||||
import kotlin.coroutines.Continuation
|
import kotlin.coroutines.Continuation
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@Ignore
|
|
||||||
class XSigningTest : InstrumentedTest {
|
class XSigningTest : InstrumentedTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -214,4 +213,104 @@ class XSigningTest : InstrumentedTest {
|
||||||
val result = aliceSession.cryptoService().crossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId, null)
|
val result = aliceSession.cryptoService().crossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId, null)
|
||||||
assertTrue("Bob second device should be trusted from alice POV", result.isCrossSignedVerified())
|
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<Unit> {
|
||||||
|
aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
promise.resume(aliceAuthParams)
|
||||||
|
}
|
||||||
|
}, it)
|
||||||
|
}
|
||||||
|
testHelper.waitForCallback<Unit> {
|
||||||
|
bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
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<Unit> {
|
||||||
|
bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,8 @@ package org.matrix.android.sdk.api.session.crypto.crosssigning
|
||||||
|
|
||||||
data class MXCrossSigningInfo(
|
data class MXCrossSigningInfo(
|
||||||
val userId: String,
|
val userId: String,
|
||||||
val crossSigningKeys: List<CryptoCrossSigningKey>
|
val crossSigningKeys: List<CryptoCrossSigningKey>,
|
||||||
|
val wasTrustedOnce: Boolean
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun isTrusted(): Boolean = masterKey()?.trustLevel?.isVerified() == true &&
|
fun isTrusted(): Boolean = masterKey()?.trustLevel?.isVerified() == true &&
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
|
@ -60,7 +60,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
internal class DefaultCrossSigningService @Inject constructor(
|
internal class DefaultCrossSigningService @Inject constructor(
|
||||||
@UserId private val userId: String,
|
@UserId private val myUserId: String,
|
||||||
@SessionId private val sessionId: String,
|
@SessionId private val sessionId: String,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val deviceListManager: DeviceListManager,
|
private val deviceListManager: DeviceListManager,
|
||||||
|
@ -127,7 +127,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recover local trust in case private key are there?
|
// Recover local trust in case private key are there?
|
||||||
setUserKeysAsTrusted(userId, checkUserTrust(userId).isVerified())
|
setUserKeysAsTrusted(myUserId, checkUserTrust(myUserId).isVerified())
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
// Mmm this kind of a big issue
|
// Mmm this kind of a big issue
|
||||||
|
@ -167,9 +167,13 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSuccess(data: InitializeCrossSigningTask.Result) {
|
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)
|
cryptoStore.setMyCrossSigningInfo(crossSigningInfo)
|
||||||
setUserKeysAsTrusted(userId, true)
|
setUserKeysAsTrusted(myUserId, true)
|
||||||
cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK)
|
cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK)
|
||||||
crossSigningOlm.masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) }
|
crossSigningOlm.masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) }
|
||||||
crossSigningOlm.userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) }
|
crossSigningOlm.userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) }
|
||||||
|
@ -266,7 +270,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
uskKeyPrivateKey: String?,
|
uskKeyPrivateKey: String?,
|
||||||
sskPrivateKey: String?
|
sskPrivateKey: String?
|
||||||
): UserTrustResult {
|
): UserTrustResult {
|
||||||
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
|
||||||
|
|
||||||
var masterKeyIsTrusted = false
|
var masterKeyIsTrusted = false
|
||||||
var userKeyIsTrusted = false
|
var userKeyIsTrusted = false
|
||||||
|
@ -330,7 +334,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
val checkSelfTrust = checkSelfTrust()
|
val checkSelfTrust = checkSelfTrust()
|
||||||
if (checkSelfTrust.isVerified()) {
|
if (checkSelfTrust.isVerified()) {
|
||||||
cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey, uskKeyPrivateKey, sskPrivateKey)
|
cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey, uskKeyPrivateKey, sskPrivateKey)
|
||||||
setUserKeysAsTrusted(userId, true)
|
setUserKeysAsTrusted(myUserId, true)
|
||||||
}
|
}
|
||||||
return checkSelfTrust
|
return checkSelfTrust
|
||||||
}
|
}
|
||||||
|
@ -351,7 +355,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
* .
|
* .
|
||||||
*/
|
*/
|
||||||
override fun isUserTrusted(otherUserId: String): Boolean {
|
override fun isUserTrusted(otherUserId: String): Boolean {
|
||||||
return cryptoStore.getCrossSigningInfo(userId)?.isTrusted() == true
|
return cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() == true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isCrossSigningVerified(): Boolean {
|
override fun isCrossSigningVerified(): Boolean {
|
||||||
|
@ -363,7 +367,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
*/
|
*/
|
||||||
override fun checkUserTrust(otherUserId: String): UserTrustResult {
|
override fun checkUserTrust(otherUserId: String): UserTrustResult {
|
||||||
Timber.v("## CrossSigning checkUserTrust for $otherUserId")
|
Timber.v("## CrossSigning checkUserTrust for $otherUserId")
|
||||||
if (otherUserId == userId) {
|
if (otherUserId == myUserId) {
|
||||||
return checkSelfTrust()
|
return checkSelfTrust()
|
||||||
}
|
}
|
||||||
// I trust a user if I trust his master key
|
// 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
|
// TODO what if the master key is signed by a device key that i have verified
|
||||||
|
|
||||||
// First let's get my user key
|
// First let's get my user key
|
||||||
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
|
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(myUserId)
|
||||||
|
|
||||||
checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId))
|
return checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId))
|
||||||
|
|
||||||
return UserTrustResult.Success
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult {
|
fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult {
|
||||||
val myUserKey = myCrossSigningInfo?.userKey()
|
val myUserKey = myCrossSigningInfo?.userKey()
|
||||||
?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
|
||||||
|
|
||||||
if (!myCrossSigningInfo.isTrusted()) {
|
if (!myCrossSigningInfo.isTrusted()) {
|
||||||
return UserTrustResult.KeysNotTrusted(myCrossSigningInfo)
|
return UserTrustResult.KeysNotTrusted(myCrossSigningInfo)
|
||||||
|
@ -391,7 +393,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
?: return UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "")
|
?: return UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "")
|
||||||
|
|
||||||
val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures
|
val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures
|
||||||
?.get(userId) // Signatures made by me
|
?.get(myUserId) // Signatures made by me
|
||||||
?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}")
|
?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}")
|
||||||
|
|
||||||
if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) {
|
if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) {
|
||||||
|
@ -417,9 +419,9 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
// Special case when it's me,
|
// Special case when it's me,
|
||||||
// I have to check that MSK -> USK -> SSK
|
// 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)
|
// 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<CryptoDeviceInfo>?): UserTrustResult {
|
fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List<CryptoDeviceInfo>?): UserTrustResult {
|
||||||
|
@ -429,7 +431,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
// val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
|
// val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
|
||||||
|
|
||||||
val myMasterKey = myCrossSigningInfo?.masterKey()
|
val myMasterKey = myCrossSigningInfo?.masterKey()
|
||||||
?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
|
||||||
|
|
||||||
// Is the master key trusted
|
// Is the master key trusted
|
||||||
// 1) check if I know the private key
|
// 1) check if I know the private key
|
||||||
|
@ -453,7 +455,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
olmPkSigning?.releaseSigning()
|
olmPkSigning?.releaseSigning()
|
||||||
} else {
|
} else {
|
||||||
// Maybe it's signed by a locally trusted device?
|
// 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 potentialDeviceId = key.removePrefix("ed25519:")
|
||||||
val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId)
|
val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId)
|
||||||
if (potentialDevice != null && potentialDevice.isVerified) {
|
if (potentialDevice != null && potentialDevice.isVerified) {
|
||||||
|
@ -475,14 +477,14 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
val myUserKey = myCrossSigningInfo.userKey()
|
val myUserKey = myCrossSigningInfo.userKey()
|
||||||
?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
|
||||||
|
|
||||||
val userKeySignaturesMadeByMyMasterKey = myUserKey.signatures
|
val userKeySignaturesMadeByMyMasterKey = myUserKey.signatures
|
||||||
?.get(userId) // Signatures made by me
|
?.get(myUserId) // Signatures made by me
|
||||||
?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}")
|
?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}")
|
||||||
|
|
||||||
if (userKeySignaturesMadeByMyMasterKey.isNullOrBlank()) {
|
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)
|
return UserTrustResult.KeyNotSigned(myUserKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -498,14 +500,14 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
val mySSKey = myCrossSigningInfo.selfSigningKey()
|
val mySSKey = myCrossSigningInfo.selfSigningKey()
|
||||||
?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
|
||||||
|
|
||||||
val ssKeySignaturesMadeByMyMasterKey = mySSKey.signatures
|
val ssKeySignaturesMadeByMyMasterKey = mySSKey.signatures
|
||||||
?.get(userId) // Signatures made by me
|
?.get(myUserId) // Signatures made by me
|
||||||
?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}")
|
?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}")
|
||||||
|
|
||||||
if (ssKeySignaturesMadeByMyMasterKey.isNullOrBlank()) {
|
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)
|
return UserTrustResult.KeyNotSigned(mySSKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -555,14 +557,14 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
|
|
||||||
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
|
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
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
|
// We should have this user keys
|
||||||
val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey()
|
val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey()
|
||||||
if (otherMasterKeys == null) {
|
if (otherMasterKeys == null) {
|
||||||
callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known"))
|
callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known"))
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
val myKeys = getUserCrossSigningKeys(userId)
|
val myKeys = getUserCrossSigningKeys(myUserId)
|
||||||
if (myKeys == null) {
|
if (myKeys == null) {
|
||||||
callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account"))
|
callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account"))
|
||||||
return@launch
|
return@launch
|
||||||
|
@ -586,16 +588,22 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
cryptoStore.setUserKeysAsTrusted(otherUserId, true)
|
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()
|
val uploadQuery = UploadSignatureQueryBuilder()
|
||||||
.withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature))
|
.withSigningKeyInfo(otherMasterKeys.copyForSignature(myUserId, userPubKey, newSignature))
|
||||||
.build()
|
.build()
|
||||||
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
|
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
|
||||||
this.executionThread = TaskThread.CRYPTO
|
this.executionThread = TaskThread.CRYPTO
|
||||||
this.callback = callback
|
this.callback = callback
|
||||||
}.executeBy(taskExecutor)
|
}.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)
|
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
|
||||||
checkSelfTrust()
|
checkSelfTrust()
|
||||||
// re-verify all trusts
|
// re-verify all trusts
|
||||||
onUsersDeviceUpdate(listOf(userId))
|
onUsersDeviceUpdate(listOf(myUserId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun trustDevice(deviceId: String, callback: MatrixCallback<Unit>) {
|
override fun trustDevice(deviceId: String, callback: MatrixCallback<Unit>) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
// This device should be yours
|
// This device should be yours
|
||||||
val device = cryptoStore.getUserDevice(userId, deviceId)
|
val device = cryptoStore.getUserDevice(myUserId, deviceId)
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours"))
|
callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours"))
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val myKeys = getUserCrossSigningKeys(userId)
|
val myKeys = getUserCrossSigningKeys(myUserId)
|
||||||
if (myKeys == null) {
|
if (myKeys == null) {
|
||||||
callback.onFailure(Throwable("CrossSigning is not setup for this account"))
|
callback.onFailure(Throwable("CrossSigning is not setup for this account"))
|
||||||
return@launch
|
return@launch
|
||||||
|
@ -639,7 +647,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
}
|
}
|
||||||
val toUpload = device.copy(
|
val toUpload = device.copy(
|
||||||
signatures = mapOf(
|
signatures = mapOf(
|
||||||
userId
|
myUserId
|
||||||
to
|
to
|
||||||
mapOf(
|
mapOf(
|
||||||
"ed25519:$ssPubKey" to newSignature
|
"ed25519:$ssPubKey" to newSignature
|
||||||
|
@ -661,8 +669,8 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
val otherDevice = cryptoStore.getUserDevice(otherUserId, otherDeviceId)
|
val otherDevice = cryptoStore.getUserDevice(otherUserId, otherDeviceId)
|
||||||
?: return DeviceTrustResult.UnknownDevice(otherDeviceId)
|
?: return DeviceTrustResult.UnknownDevice(otherDeviceId)
|
||||||
|
|
||||||
val myKeys = getUserCrossSigningKeys(userId)
|
val myKeys = getUserCrossSigningKeys(myUserId)
|
||||||
?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId))
|
?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(myUserId))
|
||||||
|
|
||||||
if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
|
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 {
|
fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo): DeviceTrustResult {
|
||||||
val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified()
|
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))
|
if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
|
||||||
|
|
||||||
|
@ -805,7 +813,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
cryptoStore.setUserKeysAsTrusted(otherUserId, trusted)
|
cryptoStore.setUserKeysAsTrusted(otherUserId, trusted)
|
||||||
// If it's me, recheck trust of all users and devices?
|
// If it's me, recheck trust of all users and devices?
|
||||||
val users = ArrayList<String>()
|
val users = ArrayList<String>()
|
||||||
if (otherUserId == userId && currentTrust != trusted) {
|
if (otherUserId == myUserId && currentTrust != trusted) {
|
||||||
// notify key requester
|
// notify key requester
|
||||||
outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted)
|
outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted)
|
||||||
cryptoStore.updateUsersTrust {
|
cryptoStore.updateUsersTrust {
|
||||||
|
|
|
@ -161,6 +161,7 @@ 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")
|
||||||
updateCrossSigningKeysTrust(cryptoRealm, it.key, verified)
|
updateCrossSigningKeysTrust(cryptoRealm, it.key, verified)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,21 +260,27 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
|
||||||
cryptoRealm.where(CrossSigningInfoEntity::class.java)
|
cryptoRealm.where(CrossSigningInfoEntity::class.java)
|
||||||
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
|
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
?.crossSigningKeys
|
?.let { userKeyInfo ->
|
||||||
?.forEach { info ->
|
userKeyInfo
|
||||||
// optimization to avoid trigger updates when there is no change..
|
.crossSigningKeys
|
||||||
if (info.trustLevelEntity?.isVerified() != verified) {
|
.forEach { key ->
|
||||||
Timber.d("## CrossSigning - Trust change for $userId : $verified")
|
// optimization to avoid trigger updates when there is no change..
|
||||||
val level = info.trustLevelEntity
|
if (key.trustLevelEntity?.isVerified() != verified) {
|
||||||
if (level == null) {
|
Timber.d("## CrossSigning - Trust change for $userId : $verified")
|
||||||
info.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also {
|
val level = key.trustLevelEntity
|
||||||
it.locallyVerified = verified
|
if (level == null) {
|
||||||
it.crossSignedVerified = verified
|
key.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also {
|
||||||
|
it.locallyVerified = verified
|
||||||
|
it.crossSignedVerified = verified
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
level.locallyVerified = verified
|
||||||
|
level.crossSignedVerified = verified
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
if (verified) {
|
||||||
level.locallyVerified = verified
|
userKeyInfo.wasUserVerifiedOnce = true
|
||||||
level.crossSignedVerified = verified
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -299,8 +306,18 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
|
||||||
getCrossSigningInfo(cryptoRealm, userId)?.isTrusted() == true
|
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()) {
|
return if (allTrustedUserIds.isEmpty()) {
|
||||||
RoomEncryptionTrustLevel.Default
|
if (resetTrust.isEmpty()) {
|
||||||
|
RoomEncryptionTrustLevel.Default
|
||||||
|
} else {
|
||||||
|
RoomEncryptionTrustLevel.Warning
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// If one of the verified user as an untrusted device -> warning
|
// If one of the verified user as an untrusted device -> warning
|
||||||
// If all devices of all verified users are trusted -> green
|
// If all devices of all verified users are trusted -> green
|
||||||
|
@ -327,11 +344,15 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
|
||||||
if (hasWarning) {
|
if (hasWarning) {
|
||||||
RoomEncryptionTrustLevel.Warning
|
RoomEncryptionTrustLevel.Warning
|
||||||
} else {
|
} else {
|
||||||
if (listToCheck.size == allTrustedUserIds.size) {
|
if (resetTrust.isEmpty()) {
|
||||||
// all users are trusted and all devices are verified
|
if (listToCheck.size == allTrustedUserIds.size) {
|
||||||
RoomEncryptionTrustLevel.Trusted
|
// all users are trusted and all devices are verified
|
||||||
|
RoomEncryptionTrustLevel.Trusted
|
||||||
|
} else {
|
||||||
|
RoomEncryptionTrustLevel.Default
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
RoomEncryptionTrustLevel.Default
|
RoomEncryptionTrustLevel.Warning
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -344,7 +365,8 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
|
||||||
userId = userId,
|
userId = userId,
|
||||||
crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull {
|
crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull {
|
||||||
crossSigningKeysMapper.map(userId, it)
|
crossSigningKeysMapper.map(userId, it)
|
||||||
}
|
},
|
||||||
|
wasTrustedOnce = xsignInfo.wasUserVerifiedOnce
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1611,7 +1611,8 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull {
|
crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull {
|
||||||
crossSigningKeysMapper.map(userId, it)
|
crossSigningKeysMapper.map(userId, it)
|
||||||
}
|
},
|
||||||
|
wasTrustedOnce = xsignInfo.wasUserVerifiedOnce
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.MigrateCryptoTo016
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017
|
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.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.database.MatrixRealmMigration
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
import org.matrix.android.sdk.internal.util.time.Clock
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -49,7 +50,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
) : MatrixRealmMigration(
|
) : MatrixRealmMigration(
|
||||||
dbName = "Crypto",
|
dbName = "Crypto",
|
||||||
schemaVersion = 18L,
|
schemaVersion = 19L,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Forces all RealmCryptoStoreMigration instances to be equal.
|
* Forces all RealmCryptoStoreMigration instances to be equal.
|
||||||
|
@ -77,5 +78,6 @@ internal class RealmCryptoStoreMigration @Inject constructor(
|
||||||
if (oldVersion < 16) MigrateCryptoTo016(realm).perform()
|
if (oldVersion < 16) MigrateCryptoTo016(realm).perform()
|
||||||
if (oldVersion < 17) MigrateCryptoTo017(realm).perform()
|
if (oldVersion < 17) MigrateCryptoTo017(realm).perform()
|
||||||
if (oldVersion < 18) MigrateCryptoTo018(realm).perform()
|
if (oldVersion < 18) MigrateCryptoTo018(realm).perform()
|
||||||
|
if (oldVersion < 19) MigrateCryptoTo019(realm).perform()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<DynamicRealmObject>(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`)) &&
|
||||||
|
isDynamicKeyInfoTrusted(ssk?.get<DynamicRealmObject>(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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ import org.matrix.android.sdk.internal.extensions.clearWith
|
||||||
internal open class CrossSigningInfoEntity(
|
internal open class CrossSigningInfoEntity(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
var userId: String? = null,
|
var userId: String? = null,
|
||||||
|
var wasUserVerifiedOnce: Boolean = false,
|
||||||
var crossSigningKeys: RealmList<KeyInfoEntity> = RealmList()
|
var crossSigningKeys: RealmList<KeyInfoEntity> = RealmList()
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
|
|
|
@ -289,6 +289,8 @@ dependencies {
|
||||||
testImplementation libs.tests.junit
|
testImplementation libs.tests.junit
|
||||||
testImplementation libs.tests.kluent
|
testImplementation libs.tests.kluent
|
||||||
testImplementation libs.mockk.mockk
|
testImplementation libs.mockk.mockk
|
||||||
|
testImplementation libs.androidx.coreTesting
|
||||||
|
testImplementation libs.tests.robolectric
|
||||||
// Plant Timber tree for test
|
// Plant Timber tree for test
|
||||||
testImplementation libs.tests.timberJunitRule
|
testImplementation libs.tests.timberJunitRule
|
||||||
testImplementation libs.airbnb.mavericksTesting
|
testImplementation libs.airbnb.mavericksTesting
|
||||||
|
|
|
@ -26,7 +26,7 @@ import im.vector.app.core.epoxy.onClick
|
||||||
import im.vector.app.core.extensions.setTextOrHide
|
import im.vector.app.core.extensions.setTextOrHide
|
||||||
import im.vector.app.features.displayname.getBestName
|
import im.vector.app.features.displayname.getBestName
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
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
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
|
||||||
abstract class BaseProfileMatrixItem<T : ProfileMatrixItem.Holder>(@LayoutRes layoutId: Int) : VectorEpoxyModel<T>(layoutId) {
|
abstract class BaseProfileMatrixItem<T : ProfileMatrixItem.Holder>(@LayoutRes layoutId: Int) : VectorEpoxyModel<T>(layoutId) {
|
||||||
|
@ -35,7 +35,7 @@ abstract class BaseProfileMatrixItem<T : ProfileMatrixItem.Holder>(@LayoutRes la
|
||||||
@EpoxyAttribute var editable: Boolean = true
|
@EpoxyAttribute var editable: Boolean = true
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
|
var userVerificationLevel: UserVerificationLevel? = null
|
||||||
|
|
||||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||||
var clickListener: ClickListener? = null
|
var clickListener: ClickListener? = null
|
||||||
|
@ -53,6 +53,6 @@ abstract class BaseProfileMatrixItem<T : ProfileMatrixItem.Holder>(@LayoutRes la
|
||||||
holder.subtitleView.setTextOrHide(matrixId)
|
holder.subtitleView.setTextOrHide(matrixId)
|
||||||
holder.editableView.isVisible = editable
|
holder.editableView.isVisible = editable
|
||||||
avatarRenderer.render(matrixItem, holder.avatarImageView)
|
avatarRenderer.render(matrixItem, holder.avatarImageView)
|
||||||
holder.avatarDecorationImageView.render(userEncryptionTrustLevel)
|
holder.avatarDecorationImageView.renderUser(userVerificationLevel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import androidx.core.view.isVisible
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
|
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.RoomEncryptionTrustLevel
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel
|
||||||
|
|
||||||
class ShieldImageView @JvmOverloads constructor(
|
class ShieldImageView @JvmOverloads constructor(
|
||||||
context: Context,
|
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
|
@DrawableRes
|
||||||
|
|
|
@ -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.devices.DeviceListBottomSheet
|
||||||
import im.vector.app.features.roommemberprofile.powerlevel.EditPowerLevelDialogs
|
import im.vector.app.features.roommemberprofile.powerlevel.EditPowerLevelDialogs
|
||||||
import kotlinx.parcelize.Parcelize
|
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.session.room.powerlevels.Role
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -235,23 +235,27 @@ class RoomMemberProfileFragment :
|
||||||
if (state.userMXCrossSigningInfo.isTrusted()) {
|
if (state.userMXCrossSigningInfo.isTrusted()) {
|
||||||
// User is trusted
|
// User is trusted
|
||||||
if (state.allDevicesAreCrossSignedTrusted) {
|
if (state.allDevicesAreCrossSignedTrusted) {
|
||||||
RoomEncryptionTrustLevel.Trusted
|
UserVerificationLevel.VERIFIED_ALL_DEVICES_TRUSTED
|
||||||
} else {
|
} else {
|
||||||
RoomEncryptionTrustLevel.Warning
|
UserVerificationLevel.VERIFIED_WITH_DEVICES_UNTRUSTED
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
RoomEncryptionTrustLevel.Default
|
if (state.userMXCrossSigningInfo.wasTrustedOnce) {
|
||||||
|
UserVerificationLevel.UNVERIFIED_BUT_WAS_PREVIOUSLY
|
||||||
|
} else {
|
||||||
|
UserVerificationLevel.WAS_NEVER_VERIFIED
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Legacy
|
// Legacy
|
||||||
if (state.allDevicesAreTrusted) {
|
if (state.allDevicesAreTrusted) {
|
||||||
RoomEncryptionTrustLevel.Trusted
|
UserVerificationLevel.VERIFIED_ALL_DEVICES_TRUSTED
|
||||||
} else {
|
} else {
|
||||||
RoomEncryptionTrustLevel.Warning
|
UserVerificationLevel.VERIFIED_WITH_DEVICES_UNTRUSTED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
headerViews.memberProfileDecorationImageView.render(trustLevel)
|
headerViews.memberProfileDecorationImageView.renderUser(trustLevel)
|
||||||
views.matrixProfileDecorationToolbarAvatarImageView.render(trustLevel)
|
views.matrixProfileDecorationToolbarAvatarImageView.renderUser(trustLevel)
|
||||||
} else {
|
} else {
|
||||||
headerViews.memberProfileDecorationImageView.isVisible = false
|
headerViews.memberProfileDecorationImageView.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,7 +129,7 @@ class RoomMemberListController @Inject constructor(
|
||||||
id(roomMember.userId)
|
id(roomMember.userId)
|
||||||
matrixItem(roomMember.toMatrixItem())
|
matrixItem(roomMember.toMatrixItem())
|
||||||
avatarRenderer(host.avatarRenderer)
|
avatarRenderer(host.avatarRenderer)
|
||||||
userEncryptionTrustLevel(data.trustLevelMap.invoke()?.get(roomMember.userId))
|
userVerificationLevel(data.trustLevelMap.invoke()?.get(roomMember.userId))
|
||||||
clickListener {
|
clickListener {
|
||||||
host.callback?.onRoomMemberClicked(roomMember)
|
host.callback?.onRoomMemberClicked(roomMember)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,8 @@ import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||||
import org.matrix.android.sdk.api.session.Session
|
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.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.getRoom
|
import org.matrix.android.sdk.api.session.getRoom
|
||||||
|
@ -116,14 +117,7 @@ class RoomMemberListViewModel @AssistedInject constructor(
|
||||||
.map { deviceList ->
|
.map { deviceList ->
|
||||||
// If any key change, emit the userIds list
|
// If any key change, emit the userIds list
|
||||||
deviceList.groupBy { it.userId }.mapValues {
|
deviceList.groupBy { it.userId }.mapValues {
|
||||||
val allDeviceTrusted = it.value.fold(it.value.isNotEmpty()) { prev, next ->
|
getUserTrustLevel(it.key, it.value)
|
||||||
prev && next.trustLevel?.isCrossSigningVerified().orFalse()
|
|
||||||
}
|
|
||||||
if (session.cryptoService().crossSigningService().getUserCrossSigningKeys(it.key)?.isTrusted().orFalse()) {
|
|
||||||
if (allDeviceTrusted) RoomEncryptionTrustLevel.Trusted else RoomEncryptionTrustLevel.Warning
|
|
||||||
} else {
|
|
||||||
RoomEncryptionTrustLevel.Default
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,6 +127,29 @@ class RoomMemberListViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getUserTrustLevel(userId: String, devices: List<CryptoDeviceInfo>): 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() {
|
private fun observePowerLevel() {
|
||||||
PowerLevelsFlowFactory(room).createFlow()
|
PowerLevelsFlowFactory(room).createFlow()
|
||||||
.onEach {
|
.onEach {
|
||||||
|
|
|
@ -23,7 +23,7 @@ import com.airbnb.mvrx.Uninitialized
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.platform.GenericIdArgs
|
import im.vector.app.core.platform.GenericIdArgs
|
||||||
import im.vector.app.features.roomprofile.RoomProfileArgs
|
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.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
|
@ -36,7 +36,7 @@ data class RoomMemberListViewState(
|
||||||
val ignoredUserIds: List<String> = emptyList(),
|
val ignoredUserIds: List<String> = emptyList(),
|
||||||
val filter: String = "",
|
val filter: String = "",
|
||||||
val threePidInvites: Async<List<Event>> = Uninitialized,
|
val threePidInvites: Async<List<Event>> = Uninitialized,
|
||||||
val trustLevelMap: Async<Map<String, RoomEncryptionTrustLevel?>> = Uninitialized,
|
val trustLevelMap: Async<Map<String, UserVerificationLevel>> = Uninitialized,
|
||||||
val actionsPermissions: ActionPermissions = ActionPermissions()
|
val actionsPermissions: ActionPermissions = ActionPermissions()
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ class SpacePeopleListController @Inject constructor(
|
||||||
id(roomMember.userId)
|
id(roomMember.userId)
|
||||||
matrixItem(roomMember.toMatrixItem())
|
matrixItem(roomMember.toMatrixItem())
|
||||||
avatarRenderer(host.avatarRenderer)
|
avatarRenderer(host.avatarRenderer)
|
||||||
userEncryptionTrustLevel(data.trustLevelMap.invoke()?.get(roomMember.userId))
|
userVerificationLevel(data.trustLevelMap.invoke()?.get(roomMember.userId))
|
||||||
.apply {
|
.apply {
|
||||||
val pl = host.toPowerLevelLabel(memberEntry.first)
|
val pl = host.toPowerLevelLabel(memberEntry.first)
|
||||||
if (memberEntry.first == RoomMemberListCategories.INVITE) {
|
if (memberEntry.first == RoomMemberListCategories.INVITE) {
|
||||||
|
|
|
@ -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>(
|
||||||
|
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<RoomSummary>>(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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ProfileMatrixItemWithPowerLevelWithPresence>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue