Fix QR code not always displayed

This commit is contained in:
Valere 2022-11-19 22:49:20 +01:00
parent 0c1e439313
commit bed2c221e3
9 changed files with 473 additions and 256 deletions

View File

@ -206,14 +206,14 @@ class VerificationTest : InstrumentedTest {
aliceReadyPendingVerificationRequest!!.let { pr -> aliceReadyPendingVerificationRequest!!.let { pr ->
pr.isSasSupported shouldBe expectedResultForAlice.sasIsSupported pr.isSasSupported shouldBe expectedResultForAlice.sasIsSupported
pr.otherCanShowQrCode shouldBe expectedResultForAlice.otherCanShowQrCode pr.weShouldShowScanOption shouldBe expectedResultForAlice.otherCanShowQrCode
pr.otherCanScanQrCode shouldBe expectedResultForAlice.otherCanScanQrCode pr.weShouldDisplayQRCode shouldBe expectedResultForAlice.otherCanScanQrCode
} }
bobReadyPendingVerificationRequest!!.let { pr -> bobReadyPendingVerificationRequest!!.let { pr ->
pr.isSasSupported shouldBe expectedResultForBob.sasIsSupported pr.isSasSupported shouldBe expectedResultForBob.sasIsSupported
pr.otherCanShowQrCode shouldBe expectedResultForBob.otherCanShowQrCode pr.weShouldShowScanOption shouldBe expectedResultForBob.otherCanShowQrCode
pr.otherCanScanQrCode shouldBe expectedResultForBob.otherCanScanQrCode pr.weShouldDisplayQRCode shouldBe expectedResultForBob.otherCanScanQrCode
} }
cryptoTestData.cleanUp(testHelper) cryptoTestData.cleanUp(testHelper)

View File

@ -246,14 +246,14 @@ class VerificationTest : InstrumentedTest {
aliceReadyPendingVerificationRequest!!.let { pr -> aliceReadyPendingVerificationRequest!!.let { pr ->
pr.isSasSupported shouldBe expectedResultForAlice.sasIsSupported pr.isSasSupported shouldBe expectedResultForAlice.sasIsSupported
pr.otherCanShowQrCode shouldBe expectedResultForAlice.otherCanShowQrCode pr.weShouldShowScanOption shouldBe expectedResultForAlice.otherCanShowQrCode
pr.otherCanScanQrCode shouldBe expectedResultForAlice.otherCanScanQrCode pr.weShouldDisplayQRCode shouldBe expectedResultForAlice.otherCanScanQrCode
} }
bobReadyPendingVerificationRequest!!.let { pr -> bobReadyPendingVerificationRequest!!.let { pr ->
pr.isSasSupported shouldBe expectedResultForBob.sasIsSupported pr.isSasSupported shouldBe expectedResultForBob.sasIsSupported
pr.otherCanShowQrCode shouldBe expectedResultForBob.otherCanShowQrCode pr.weShouldShowScanOption shouldBe expectedResultForBob.otherCanShowQrCode
pr.otherCanScanQrCode shouldBe expectedResultForBob.otherCanScanQrCode pr.weShouldDisplayQRCode shouldBe expectedResultForBob.otherCanScanQrCode
} }
} }

View File

@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.crypto.verification
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
import org.matrix.android.sdk.api.session.crypto.verification.IVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest
@ -35,7 +34,7 @@ internal class KotlinVerificationRequest(
val otherUserId: String, val otherUserId: String,
var state: EVerificationState, var state: EVerificationState,
val ageLocalTs: Long val ageLocalTs: Long
) : IVerificationRequest { ) {
var roomId: String? = null var roomId: String? = null
var qrCodeData: QrCodeData? = null var qrCodeData: QrCodeData? = null
@ -44,21 +43,21 @@ internal class KotlinVerificationRequest(
var readyInfo: ValidVerificationInfoReady? = null var readyInfo: ValidVerificationInfoReady? = null
var cancelCode: CancelCode? = null var cancelCode: CancelCode? = null
override fun requestId() = requestId // fun requestId() = requestId
//
// fun incoming() = incoming
//
// fun otherUserId() = otherUserId
//
// fun roomId() = roomId
//
// fun targetDevices() = targetDevices
//
// fun state() = state
//
// fun ageLocalTs() = ageLocalTs
override fun incoming() = incoming fun otherDeviceId(): String? {
override fun otherUserId() = otherUserId
override fun roomId() = roomId
override fun targetDevices() = targetDevices
override fun state() = state
override fun ageLocalTs() = ageLocalTs
override fun otherDeviceId(): String? {
return if (incoming) { return if (incoming) {
requestInfo?.fromDevice requestInfo?.fromDevice
} else { } else {
@ -66,12 +65,12 @@ internal class KotlinVerificationRequest(
} }
} }
override fun cancelCode(): CancelCode? = cancelCode fun cancelCode(): CancelCode? = cancelCode
/** /**
* SAS is supported if I support it and the other party support it. * SAS is supported if I support it and the other party support it.
*/ */
override fun isSasSupported(): Boolean { private fun isSasSupported(): Boolean {
return requestInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() && return requestInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() &&
readyInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() readyInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse()
} }
@ -79,7 +78,7 @@ internal class KotlinVerificationRequest(
/** /**
* Other can show QR code if I can scan QR code and other can show QR code. * Other can show QR code if I can scan QR code and other can show QR code.
*/ */
override fun otherCanShowQrCode(): Boolean { private fun otherCanShowQrCode(): Boolean {
return if (incoming) { return if (incoming) {
requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() && requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() &&
readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse()
@ -92,7 +91,7 @@ internal class KotlinVerificationRequest(
/** /**
* Other can scan QR code if I can show QR code and other can scan QR code. * Other can scan QR code if I can show QR code and other can scan QR code.
*/ */
override fun otherCanScanQrCode(): Boolean { private fun otherCanScanQrCode(): Boolean {
return if (incoming) { return if (incoming) {
requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() && requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() &&
readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse()
@ -102,7 +101,7 @@ internal class KotlinVerificationRequest(
} }
} }
override fun qrCodeText() = qrCodeData?.toEncodedString() fun qrCodeText() = qrCodeData?.toEncodedString()
override fun toString(): String { override fun toString(): String {
return toPendingVerificationRequest().toString() return toPendingVerificationRequest().toString()
@ -122,9 +121,11 @@ internal class KotlinVerificationRequest(
targetDevices = targetDevices, targetDevices = targetDevices,
qrCodeText = qrCodeText(), qrCodeText = qrCodeText(),
isSasSupported = isSasSupported(), isSasSupported = isSasSupported(),
otherCanShowQrCode = otherCanShowQrCode(), weShouldShowScanOption = otherCanShowQrCode(),
otherCanScanQrCode = otherCanScanQrCode(), weShouldDisplayQRCode = otherCanScanQrCode(),
otherDeviceId = otherDeviceId() otherDeviceId = otherDeviceId()
) )
} }
fun isFinished() = state == EVerificationState.Cancelled || state == EVerificationState.Done
} }

View File

@ -130,8 +130,8 @@ internal class VerificationActor @AssistedInject constructor(
*/ */
private val pendingRequests = HashMap<String, MutableList<KotlinVerificationRequest>>() private val pendingRequests = HashMap<String, MutableList<KotlinVerificationRequest>>()
// Replaces the typical list of listeners pattern. Looks to me as the sane setup, not sure if more than 1 is needed as extraBufferCapacity // Replaces the typical list of listeners pattern.
// We don't want to use emit as it would block if no listener is subscribed // Looks to me as the sane setup, not sure if more than 1 is needed as extraBufferCapacity
// So we should use try emit using extraBufferCapacity, we use drop_oldest instead of suspend. // So we should use try emit using extraBufferCapacity, we use drop_oldest instead of suspend.
val eventFlow = MutableSharedFlow<VerificationEvent>(extraBufferCapacity = 4, onBufferOverflow = BufferOverflow.DROP_OLDEST) val eventFlow = MutableSharedFlow<VerificationEvent>(extraBufferCapacity = 4, onBufferOverflow = BufferOverflow.DROP_OLDEST)
@ -214,7 +214,7 @@ internal class VerificationActor @AssistedInject constructor(
.v("[${myUserId.take(8)}]: $msg") .v("[${myUserId.take(8)}]: $msg")
when (msg) { when (msg) {
is VerificationIntent.ActionRequestVerification -> { is VerificationIntent.ActionRequestVerification -> {
handleRequestAdd(msg) handleActionRequestVerification(msg)
} }
is VerificationIntent.OnReadyReceived -> { is VerificationIntent.OnReadyReceived -> {
handleReadyReceived(msg) handleReadyReceived(msg)
@ -1046,7 +1046,7 @@ internal class VerificationActor @AssistedInject constructor(
existing.state = SasTransactionState.Done(true) existing.state = SasTransactionState.Done(true)
dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) dispatchUpdate(VerificationEvent.TransactionUpdated(existing))
// we can forget about it // we can forget about it
txMap[matchingRequest.otherUserId()]?.remove(matchingRequest.requestId) txMap[matchingRequest.otherUserId]?.remove(matchingRequest.requestId)
// XXX whatabout waiting for done? // XXX whatabout waiting for done?
matchingRequest.state = EVerificationState.Done matchingRequest.state = EVerificationState.Done
dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest()))
@ -1137,7 +1137,7 @@ internal class VerificationActor @AssistedInject constructor(
existing.state = QRCodeVerificationState.Done existing.state = QRCodeVerificationState.Done
dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) dispatchUpdate(VerificationEvent.TransactionUpdated(existing))
// we can forget about it // we can forget about it
txMap[matchingRequest.otherUserId()]?.remove(matchingRequest.requestId) txMap[matchingRequest.otherUserId]?.remove(matchingRequest.requestId)
matchingRequest.state = EVerificationState.WaitingForDone matchingRequest.state = EVerificationState.WaitingForDone
dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest()))
@ -1309,6 +1309,11 @@ internal class VerificationActor @AssistedInject constructor(
if (commonMethods.isEmpty()) { if (commonMethods.isEmpty()) {
Timber.tag(loggerTag.value).v("Request ${msg.transactionId} no common methods") Timber.tag(loggerTag.value).v("Request ${msg.transactionId} no common methods")
cancelRequest(existing, CancelCode.UnknownMethod) cancelRequest(existing, CancelCode.UnknownMethod)
// Upon receipt of Alices m.key.verification.request message, if Bobs device does not understand any of the methods,
// it should not cancel the request as one of his other devices may support the request.
// XXX How to o that??
// Instead, Bobs device should tell Bob that no supported method was found, and allow him to manually reject the request.
msg.deferred.complete(null) msg.deferred.complete(null)
return return
} }
@ -1367,7 +1372,7 @@ internal class VerificationActor @AssistedInject constructor(
private fun getMethodAgreement( private fun getMethodAgreement(
otherUserMethods: List<String>?, otherUserMethods: List<String>?,
methods: List<VerificationMethod>, myMethods: List<VerificationMethod>,
): List<String> { ): List<String> {
if (otherUserMethods.isNullOrEmpty()) { if (otherUserMethods.isNullOrEmpty()) {
return emptyList() return emptyList()
@ -1375,18 +1380,18 @@ internal class VerificationActor @AssistedInject constructor(
val result = mutableSetOf<String>() val result = mutableSetOf<String>()
if (VERIFICATION_METHOD_SAS in otherUserMethods && VerificationMethod.SAS in methods) { if (VERIFICATION_METHOD_SAS in otherUserMethods && VerificationMethod.SAS in myMethods) {
// Other can do SAS and so do I // Other can do SAS and so do I
result.add(VERIFICATION_METHOD_SAS) result.add(VERIFICATION_METHOD_SAS)
} }
if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods || VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods) { if (VERIFICATION_METHOD_RECIPROCATE in otherUserMethods) {
if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods && VerificationMethod.QR_CODE_SHOW in methods) { if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods && VerificationMethod.QR_CODE_SHOW in myMethods) {
// Other can Scan and I can show QR code // Other can Scan and I can show QR code
result.add(VERIFICATION_METHOD_QR_CODE_SHOW) result.add(VERIFICATION_METHOD_QR_CODE_SHOW)
result.add(VERIFICATION_METHOD_RECIPROCATE) result.add(VERIFICATION_METHOD_RECIPROCATE)
} }
if (VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods && VerificationMethod.QR_CODE_SCAN in methods) { if (VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods && VerificationMethod.QR_CODE_SCAN in myMethods) {
// Other can show and I can scan QR code // Other can show and I can scan QR code
result.add(VERIFICATION_METHOD_QR_CODE_SCAN) result.add(VERIFICATION_METHOD_QR_CODE_SCAN)
result.add(VERIFICATION_METHOD_RECIPROCATE) result.add(VERIFICATION_METHOD_RECIPROCATE)
@ -1400,7 +1405,11 @@ internal class VerificationActor @AssistedInject constructor(
return contains(VERIFICATION_METHOD_QR_CODE_SCAN) && contains(VERIFICATION_METHOD_RECIPROCATE) return contains(VERIFICATION_METHOD_QR_CODE_SCAN) && contains(VERIFICATION_METHOD_RECIPROCATE)
} }
private suspend fun handleRequestAdd(msg: VerificationIntent.ActionRequestVerification) { private fun List<String>.canShowCode(): Boolean {
return contains(VERIFICATION_METHOD_QR_CODE_SHOW) && contains(VERIFICATION_METHOD_RECIPROCATE)
}
private suspend fun handleActionRequestVerification(msg: VerificationIntent.ActionRequestVerification) {
val requestsForUser = pendingRequests.getOrPut(msg.otherUserId) { mutableListOf() } val requestsForUser = pendingRequests.getOrPut(msg.otherUserId) { mutableListOf() }
// there can only be one active request per user, so cancel existing ones // there can only be one active request per user, so cancel existing ones
requestsForUser.toList().forEach { existingRequest -> requestsForUser.toList().forEach { existingRequest ->
@ -1410,8 +1419,6 @@ internal class VerificationActor @AssistedInject constructor(
} }
} }
val validLocalId = LocalEcho.createLocalEchoId()
val methodValues = if (cryptoStore.getMyCrossSigningInfo()?.isTrusted().orFalse()) { val methodValues = if (cryptoStore.getMyCrossSigningInfo()?.isTrusted().orFalse()) {
// Add reciprocate method if application declares it can scan or show QR codes // Add reciprocate method if application declares it can scan or show QR codes
// Not sure if it ok to do that (?) // Not sure if it ok to do that (?)
@ -1521,6 +1528,10 @@ internal class VerificationActor @AssistedInject constructor(
return return
} }
if (matchingRequest.requestInfo?.methods?.canShowCode().orFalse() &&
msg.readyInfo.methods.canScanCode()) {
matchingRequest.qrCodeData = createQrCodeData(matchingRequest.requestId, msg.fromUser, msg.readyInfo.fromDevice)
}
matchingRequest.readyInfo = msg.readyInfo matchingRequest.readyInfo = msg.readyInfo
matchingRequest.state = EVerificationState.Ready matchingRequest.state = EVerificationState.Ready
dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest()))

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2020 The Matrix.org Foundation C.I.C. * Copyright 2022 The Matrix.org Foundation C.I.C.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -32,31 +32,3 @@ enum class EVerificationState {
Cancelled, Cancelled,
HandledByOtherSession HandledByOtherSession
} }
// TODO remove that
interface IVerificationRequest {
fun requestId(): String
fun incoming(): Boolean
fun otherUserId(): String
fun roomId(): String?
// target devices in case of to_device self verification
fun targetDevices(): List<String>?
fun state(): EVerificationState
fun ageLocalTs(): Long
fun isSasSupported(): Boolean
fun otherCanShowQrCode(): Boolean
fun otherCanScanQrCode(): Boolean
fun otherDeviceId(): String?
fun qrCodeText(): String?
fun isFinished(): Boolean = state() == EVerificationState.Cancelled || state() == EVerificationState.Done
fun cancelCode(): CancelCode?
}

View File

@ -38,11 +38,10 @@ data class PendingVerificationRequest(
// if available store here the qr code to show // if available store here the qr code to show
val qrCodeText: String? = null, val qrCodeText: String? = null,
val isSasSupported: Boolean = false, val isSasSupported: Boolean = false,
val otherCanShowQrCode: Boolean = false, val weShouldShowScanOption: Boolean = false,
val otherCanScanQrCode: Boolean = false, val weShouldDisplayQRCode: Boolean = false,
) { ) {
// val isReady: Boolean = readyInfo != null // val isReady: Boolean = readyInfo != null
// //
// val isFinished: Boolean = isSuccessful || cancelConclusion != null
} }

View File

@ -0,0 +1,181 @@
/*
* 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 org.matrix.android.sdk.internal.crypto.verification
import dagger.Lazy
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady
import org.matrix.android.sdk.api.session.events.model.Content
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.room.model.message.MessageVerificationReadyContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
import org.matrix.android.sdk.internal.crypto.SecretShareManager
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.util.time.Clock
import java.util.UUID
internal class VerificationActorHelper {
data class TestData(
val aliceActor: VerificationActor,
val bobActor: VerificationActor,
)
val actorAScope = CoroutineScope(SupervisorJob())
val actorBScope = CoroutineScope(SupervisorJob())
val transportScope = CoroutineScope(SupervisorJob())
var bobChannel: SendChannel<VerificationIntent>? = null
var aliceChannel: SendChannel<VerificationIntent>? = null
fun setUpActors(): TestData {
val aliceTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.aliceMxId) { bobChannel }
val bobTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.bobMxId) { aliceChannel }
val aliceActor = fakeActor(
actorAScope,
FakeCryptoStoreForVerification.aliceMxId,
FakeCryptoStoreForVerification(StoreMode.Alice).instance,
aliceTransportLayer,
mockk<Lazy<CrossSigningService>> {
every {
get()
} returns mockk<CrossSigningService>(relaxed = true)
}
)
aliceChannel = aliceActor.channel
val bobActor = fakeActor(
actorBScope,
FakeCryptoStoreForVerification.aliceMxId,
FakeCryptoStoreForVerification(StoreMode.Alice).instance,
bobTransportLayer,
mockk<Lazy<CrossSigningService>> {
every {
get()
} returns mockk<CrossSigningService>(relaxed = true)
}
)
bobChannel = bobActor.channel
return TestData(
aliceActor,
bobActor
)
}
private fun mockTransportTo(fromUser: String, otherChannel: (() -> SendChannel<VerificationIntent>?)): VerificationTransportLayer {
return mockk<VerificationTransportLayer> {
coEvery { sendToOther(any(), any(), any()) } answers {
val request = firstArg<KotlinVerificationRequest>()
val type = secondArg<String>()
val info = thirdArg<VerificationInfo<*>>()
transportScope.launch(Dispatchers.IO) {
when (type) {
EventType.KEY_VERIFICATION_READY -> {
val readyContent = info.asValidObject()
otherChannel()?.send(
VerificationIntent.OnReadyReceived(
transactionId = request.requestId,
fromUser = fromUser,
viaRoom = request.roomId,
readyInfo = readyContent as ValidVerificationInfoReady,
)
)
}
}
}
}
coEvery { sendInRoom(any(), any(), any(), any()) } answers {
val type = secondArg<String>()
val roomId = thirdArg<String>()
val content = arg<Content>(3)
val fakeEventId = UUID.randomUUID().toString()
transportScope.launch(Dispatchers.IO) {
when (type) {
EventType.MESSAGE -> {
val requestContent = content.toModel<MessageVerificationRequestContent>()?.copy(
transactionId = fakeEventId
)?.asValidObject()
otherChannel()?.send(
VerificationIntent.OnVerificationRequestReceived(
requestContent!!,
senderId = FakeCryptoStoreForVerification.aliceMxId,
roomId = roomId,
timeStamp = 0
)
)
}
EventType.KEY_VERIFICATION_READY -> {
val readyContent = content.toModel<MessageVerificationReadyContent>()
?.asValidObject()
otherChannel()?.send(
VerificationIntent.OnReadyReceived(
transactionId = readyContent!!.transactionId,
fromUser = fromUser,
viaRoom = roomId,
readyInfo = readyContent,
)
)
}
}
}
fakeEventId
}
}
}
private fun fakeActor(
scope: CoroutineScope,
userId: String,
cryptoStore: IMXCryptoStore,
transportLayer: VerificationTransportLayer,
crossSigningService: dagger.Lazy<CrossSigningService>,
): VerificationActor {
return VerificationActor(
scope,
// channel = channel,
clock = mockk<Clock> {
every { epochMillis() } returns System.currentTimeMillis()
},
myUserId = userId,
cryptoStore = cryptoStore,
secretShareManager = mockk<SecretShareManager> {},
transportLayer = transportLayer,
crossSigningService = crossSigningService,
setDeviceVerificationAction = SetDeviceVerificationAction(
cryptoStore = cryptoStore,
userId = userId,
defaultKeysBackupService = mockk {
coEvery { checkAndStartKeysBackup() } coAnswers { }
}
)
)
}
}

View File

@ -17,9 +17,7 @@
package org.matrix.android.sdk.internal.crypto.verification.org.matrix.android.sdk.internal.crypto.verification package org.matrix.android.sdk.internal.crypto.verification.org.matrix.android.sdk.internal.crypto.verification
import android.util.Base64 import android.util.Base64
import io.mockk.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -27,44 +25,32 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import org.amshove.kluent.fail
import org.amshove.kluent.internal.assertEquals
import org.amshove.kluent.internal.assertNotEquals
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldNotBe
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
import org.matrix.android.sdk.api.session.crypto.verification.IVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady
import org.matrix.android.sdk.api.session.crypto.verification.VerificationEvent import org.matrix.android.sdk.api.session.crypto.verification.VerificationEvent
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.events.model.Content
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.room.model.message.MessageVerificationReadyContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
import org.matrix.android.sdk.internal.crypto.SecretShareManager
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.verification.FakeCryptoStoreForVerification import org.matrix.android.sdk.internal.crypto.verification.FakeCryptoStoreForVerification
import org.matrix.android.sdk.internal.crypto.verification.StoreMode
import org.matrix.android.sdk.internal.crypto.verification.VerificationActor import org.matrix.android.sdk.internal.crypto.verification.VerificationActor
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfo import org.matrix.android.sdk.internal.crypto.verification.VerificationActorHelper
import org.matrix.android.sdk.internal.crypto.verification.VerificationIntent import org.matrix.android.sdk.internal.crypto.verification.VerificationIntent
import org.matrix.android.sdk.internal.crypto.verification.VerificationTransportLayer
import org.matrix.android.sdk.internal.util.time.Clock
import java.util.UUID
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class VerificationActorTest { class VerificationActorTest {
val transportScope = CoroutineScope(SupervisorJob()) val transportScope = CoroutineScope(SupervisorJob())
val actorAScope = CoroutineScope(SupervisorJob()) // val actorAScope = CoroutineScope(SupervisorJob())
val actorBScope = CoroutineScope(SupervisorJob()) // val actorBScope = CoroutineScope(SupervisorJob())
@Before @Before
fun setUp() { fun setUp() {
@ -86,38 +72,10 @@ class VerificationActorTest {
} }
@Test @Test
fun `Request and accept`() = runTest { fun `If ready both side should support sas and Qr show and scan`() = runTest {
var bobChannel: SendChannel<VerificationIntent>? = null val testData = VerificationActorHelper().setUpActors()
var aliceChannel: SendChannel<VerificationIntent>? = null val aliceActor = testData.aliceActor
val bobActor = testData.bobActor
val aliceTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.aliceMxId) { bobChannel }
val bobTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.bobMxId) { aliceChannel }
val aliceActor = fakeActor(
actorAScope,
FakeCryptoStoreForVerification.aliceMxId,
FakeCryptoStoreForVerification(StoreMode.Alice).instance,
aliceTransportLayer,
mockk<dagger.Lazy<CrossSigningService>> {
every {
get()
} returns mockk<CrossSigningService>(relaxed = true)
}
)
aliceChannel = aliceActor.channel
val bobActor = fakeActor(
actorBScope,
FakeCryptoStoreForVerification.aliceMxId,
FakeCryptoStoreForVerification(StoreMode.Alice).instance,
bobTransportLayer,
mockk<dagger.Lazy<CrossSigningService>> {
every {
get()
} returns mockk<CrossSigningService>(relaxed = true)
}
)
bobChannel = bobActor.channel
val completableDeferred = CompletableDeferred<PendingVerificationRequest>() val completableDeferred = CompletableDeferred<PendingVerificationRequest>()
@ -130,23 +88,14 @@ class VerificationActorTest {
} }
} }
awaitDeferrable<PendingVerificationRequest> { aliceActor.requestVerification(listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SHOW, VerificationMethod.QR_CODE_SCAN))
aliceActor.send(
VerificationIntent.ActionRequestVerification(
otherUserId = FakeCryptoStoreForVerification.bobMxId,
roomId = "aRoom",
methods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SHOW, VerificationMethod.QR_CODE_SCAN),
deferred = it
)
)
}
val bobIncomingRequest = completableDeferred.await() val bobIncomingRequest = completableDeferred.await()
bobIncomingRequest.state shouldBeEqualTo EVerificationState.Requested bobIncomingRequest.state shouldBeEqualTo EVerificationState.Requested
val aliceReadied = CompletableDeferred<PendingVerificationRequest>() val aliceReadied = CompletableDeferred<PendingVerificationRequest>()
val theJob = transportScope.launch { transportScope.launch {
aliceActor.eventFlow.collect { aliceActor.eventFlow.collect {
if (it is VerificationEvent.RequestUpdated && it.request.state == EVerificationState.Ready) { if (it is VerificationEvent.RequestUpdated && it.request.state == EVerificationState.Ready) {
aliceReadied.complete(it.request) aliceReadied.complete(it.request)
@ -156,7 +105,7 @@ class VerificationActorTest {
} }
// test ready // test ready
awaitDeferrable<PendingVerificationRequest?> { val bobReadied = awaitDeferrable<PendingVerificationRequest?> {
bobActor.send( bobActor.send(
VerificationIntent.ActionReadyRequest( VerificationIntent.ActionReadyRequest(
bobIncomingRequest.transactionId, bobIncomingRequest.transactionId,
@ -168,11 +117,143 @@ class VerificationActorTest {
val readiedAliceSide = aliceReadied.await() val readiedAliceSide = aliceReadied.await()
println("transporte scope active? ${transportScope.isActive}")
println("the job? ${theJob.isActive}")
readiedAliceSide.isSasSupported shouldBeEqualTo true readiedAliceSide.isSasSupported shouldBeEqualTo true
readiedAliceSide.otherCanScanQrCode shouldBeEqualTo true readiedAliceSide.weShouldDisplayQRCode shouldBeEqualTo true
bobReadied shouldNotBe null
bobReadied!!.isSasSupported shouldBeEqualTo true
bobReadied.weShouldDisplayQRCode shouldBeEqualTo true
bobReadied.qrCodeText shouldNotBe null
readiedAliceSide.qrCodeText shouldNotBe null
}
@Test
fun `Test alice can show but not scan QR`() = runTest {
val testData = VerificationActorHelper().setUpActors()
val aliceActor = testData.aliceActor
val bobActor = testData.bobActor
println("Alice sends a request")
val outgoingRequest = aliceActor.requestVerification(
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SHOW)
)
// wait for bob to get it
println("Wait for bob to get it")
waitForBobToSeeIncomingRequest(bobActor, outgoingRequest)
println("let bob ready it")
val bobReady = bobActor.readyVerification(
outgoingRequest.transactionId,
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW)
)
println("Wait for alice to get the ready")
retryUntil {
awaitDeferrable<PendingVerificationRequest?> {
aliceActor.send(VerificationIntent.GetExistingRequest(outgoingRequest.transactionId, outgoingRequest.otherUserId, it))
}?.state == EVerificationState.Ready
}
val aliceReady = awaitDeferrable<PendingVerificationRequest?> {
aliceActor.send(VerificationIntent.GetExistingRequest(outgoingRequest.transactionId, outgoingRequest.otherUserId, it))
}!!
aliceReady.isSasSupported shouldBeEqualTo bobReady.isSasSupported
// alice can't scan so there should not be option to do so
assertEquals("Alice should not show scan option", false, aliceReady.weShouldShowScanOption)
assertEquals("Alice should show QR as bob can scan", true, aliceReady.weShouldDisplayQRCode)
assertEquals("Bob should be able to scan", true, bobReady.weShouldShowScanOption)
assertEquals("Bob should not show QR as alice can scan", false, bobReady.weShouldDisplayQRCode)
}
@Test
fun `Test bob can show but not scan QR`() = runTest {
val testData = VerificationActorHelper().setUpActors()
val aliceActor = testData.aliceActor
val bobActor = testData.bobActor
println("Alice sends a request")
val outgoingRequest = aliceActor.requestVerification(
listOf(VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW)
)
// wait for bob to get it
println("Wait for bob to get it")
waitForBobToSeeIncomingRequest(bobActor, outgoingRequest)
println("let bob ready it")
val bobReady = bobActor.readyVerification(
outgoingRequest.transactionId,
listOf(VerificationMethod.QR_CODE_SHOW)
)
println("Wait for alice to get the ready")
retryUntil {
awaitDeferrable<PendingVerificationRequest?> {
aliceActor.send(VerificationIntent.GetExistingRequest(outgoingRequest.transactionId, outgoingRequest.otherUserId, it))
}?.state == EVerificationState.Ready
}
val aliceReady = awaitDeferrable<PendingVerificationRequest?> {
aliceActor.send(VerificationIntent.GetExistingRequest(outgoingRequest.transactionId, outgoingRequest.otherUserId, it))
}!!
assertEquals("Alice sas is not supported", false, aliceReady.isSasSupported)
aliceReady.isSasSupported shouldBeEqualTo bobReady.isSasSupported
// alice can't scan so there should not be option to do so
assertEquals("Alice should show scan option", true, aliceReady.weShouldShowScanOption)
assertEquals("Alice QR data should be null", null, aliceReady.qrCodeText)
assertEquals("Alice should not show QR as bob can scan", false, aliceReady.weShouldDisplayQRCode)
assertEquals("Bob should not should not show cam option as it can't scan", false, bobReady.weShouldShowScanOption)
assertNotEquals("Bob QR data should be there", null, bobReady.qrCodeText)
assertEquals("Bob should show QR as alice can scan", true, bobReady.weShouldDisplayQRCode)
}
private suspend fun VerificationActor.requestVerification(methods: List<VerificationMethod>): PendingVerificationRequest {
return awaitDeferrable<PendingVerificationRequest> {
send(
VerificationIntent.ActionRequestVerification(
otherUserId = FakeCryptoStoreForVerification.bobMxId,
roomId = "aRoom",
methods = methods,
deferred = it
)
)
}
}
private suspend fun waitForBobToSeeIncomingRequest(bobActor: VerificationActor, aliceOutgoing: PendingVerificationRequest) {
retryUntil {
awaitDeferrable<PendingVerificationRequest?> {
bobActor.send(
VerificationIntent.GetExistingRequest(
aliceOutgoing.transactionId,
FakeCryptoStoreForVerification.aliceMxId, it
)
)
}?.state == EVerificationState.Requested
}
}
private val backoff = listOf(500L, 1_000L, 1_000L, 3_000L, 3_000L, 5_000L)
private suspend fun retryUntil(condition: suspend (() -> Boolean)) {
var tryCount = 0
while (!condition()) {
if (tryCount >= backoff.size) {
fail("Retry Until Fialed")
}
withContext(Dispatchers.IO) {
delay(backoff[tryCount])
}
tryCount++
}
} }
private suspend fun <T> awaitDeferrable(block: suspend ((CompletableDeferred<T>) -> Unit)): T { private suspend fun <T> awaitDeferrable(block: suspend ((CompletableDeferred<T>) -> Unit)): T {
@ -181,113 +262,85 @@ class VerificationActorTest {
return deferred.await() return deferred.await()
} }
private fun mockTransportTo(fromUser: String, otherChannel: (() -> SendChannel<VerificationIntent>?)): VerificationTransportLayer { private suspend fun VerificationActor.readyVerification(transactionId: String, methods: List<VerificationMethod>): PendingVerificationRequest {
return mockk<VerificationTransportLayer> { return awaitDeferrable<PendingVerificationRequest?> {
coEvery { sendToOther(any(), any(), any()) } answers { send(
val request = firstArg<IVerificationRequest>() VerificationIntent.ActionReadyRequest(
val type = secondArg<String>() transactionId,
val info = thirdArg<VerificationInfo<*>>() methods = methods,
it
transportScope.launch(Dispatchers.IO) {
when (type) {
EventType.KEY_VERIFICATION_READY -> {
val readyContent = info.asValidObject()
otherChannel()?.send(
VerificationIntent.OnReadyReceived(
transactionId = request.requestId(),
fromUser = fromUser,
viaRoom = request.roomId(),
readyInfo = readyContent as ValidVerificationInfoReady,
) )
) )
} }!!
}
}
}
coEvery { sendInRoom(any(), any(), any(), any()) } answers {
val type = secondArg<String>()
val roomId = thirdArg<String>()
val content = arg<Content>(3)
val fakeEventId = UUID.randomUUID().toString()
transportScope.launch(Dispatchers.IO) {
when (type) {
EventType.MESSAGE -> {
val requestContent = content.toModel<MessageVerificationRequestContent>()?.copy(
transactionId = fakeEventId
)?.asValidObject()
otherChannel()?.send(
VerificationIntent.OnVerificationRequestReceived(
requestContent!!,
senderId = FakeCryptoStoreForVerification.aliceMxId,
roomId = roomId,
timeStamp = 0
)
)
}
EventType.KEY_VERIFICATION_READY -> {
val readyContent = content.toModel<MessageVerificationReadyContent>()
?.asValidObject()
otherChannel()?.send(
VerificationIntent.OnReadyReceived(
transactionId = readyContent!!.transactionId,
fromUser = fromUser,
viaRoom = roomId,
readyInfo = readyContent,
)
)
}
}
}
fakeEventId
}
}
} }
@Test // @Test
fun `Every testing`() { // fun `Every testing`() {
val mockStore = mockk<IMXCryptoStore>() // val mockStore = mockk<IMXCryptoStore>()
every { mockStore.getDeviceId() } returns "A" // every { mockStore.getDeviceId() } returns "A"
println("every ${mockStore.getDeviceId()}") // println("every ${mockStore.getDeviceId()}")
every { mockStore.getDeviceId() } returns "B" // every { mockStore.getDeviceId() } returns "B"
println("every ${mockStore.getDeviceId()}") // println("every ${mockStore.getDeviceId()}")
//
// every { mockStore.getDeviceId() } returns "A"
// every { mockStore.getDeviceId() } returns "B"
// println("every ${mockStore.getDeviceId()}")
//
// every { mockStore.getCrossSigningInfo(any()) } returns null
// every { mockStore.getCrossSigningInfo("alice") } returns MXCrossSigningInfo("alice", emptyList(), false)
//
// println("XS ${mockStore.getCrossSigningInfo("alice")}")
// println("XS ${mockStore.getCrossSigningInfo("bob")}")
// }
every { mockStore.getDeviceId() } returns "A" // @Test
every { mockStore.getDeviceId() } returns "B" // fun `Basic channel test`() {
println("every ${mockStore.getDeviceId()}") // // val sharedFlow = MutableSharedFlow<Int>(replay = 0, extraBufferCapacity = 2, BufferOverflow.DROP_OLDEST)
// val sharedFlow = MutableSharedFlow<Int>(replay = 0)//, extraBufferCapacity = 0, BufferOverflow.DROP_OLDEST)
every { mockStore.getCrossSigningInfo(any()) } returns null //
every { mockStore.getCrossSigningInfo("alice") } returns MXCrossSigningInfo("alice", emptyList(), false) // val scope = CoroutineScope(SupervisorJob())
// val deferred = CompletableDeferred<Unit>()
println("XS ${mockStore.getCrossSigningInfo("alice")}") // val listener = scope.launch {
println("XS ${mockStore.getCrossSigningInfo("bob")}") // sharedFlow.onEach {
} // println("L1 : Just collected $it")
// delay(1000)
private fun fakeActor( // println("L1 : Just processed $it")
scope: CoroutineScope, // if (it == 2) {
userId: String, // deferred.complete(Unit)
cryptoStore: IMXCryptoStore, // }
transportLayer: VerificationTransportLayer, // }.launchIn(scope)
crossSigningService: dagger.Lazy<CrossSigningService>, // }
): VerificationActor { //
return VerificationActor( // // scope.launch {
scope, // // delay(700)
// channel = channel, // println("Pre Emit 1")
clock = mockk<Clock> { // sharedFlow.tryEmit(1)
every { epochMillis() } returns System.currentTimeMillis() // println("Emited 1")
}, // sharedFlow.tryEmit(2)
myUserId = userId, // println("Emited 2")
cryptoStore = cryptoStore, // // }
secretShareManager = mockk<SecretShareManager> {}, //
transportLayer = transportLayer, // // runBlocking {
crossSigningService = crossSigningService, // // deferred.await()
setDeviceVerificationAction = SetDeviceVerificationAction( // // }
cryptoStore = cryptoStore, //
userId = userId, // sharedFlow.onEach {
defaultKeysBackupService = mockk { // println("L2: Just collected $it")
coEvery { checkAndStartKeysBackup() } coAnswers { } // delay(1000)
} // println("L2: Just processed $it")
) // }.launchIn(scope)
) //
} //
// runBlocking {
// deferred.await()
// }
//
// val now = System.currentTimeMillis()
// println("Just give some time for execution")
// val job = scope.launch { delay(10_000) }
// runBlocking {
// job.join()
// }
// println("enough ${System.currentTimeMillis() - now}")
// }
} }

View File

@ -187,7 +187,7 @@ class UserVerificationController @Inject constructor(
notice(scanCodeInstructions.toEpoxyCharSequence()) notice(scanCodeInstructions.toEpoxyCharSequence())
} }
if (request.otherCanScanQrCode && !request.qrCodeText.isNullOrEmpty()) { if (request.weShouldDisplayQRCode && !request.qrCodeText.isNullOrEmpty()) {
bottomSheetVerificationQrCodeItem { bottomSheetVerificationQrCodeItem {
id("qr") id("qr")
data(request.qrCodeText!!) data(request.qrCodeText!!)
@ -198,7 +198,7 @@ class UserVerificationController @Inject constructor(
} }
} }
if (request.otherCanShowQrCode) { if (request.weShouldShowScanOption) {
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("openCamera") id("openCamera")
title(scanOtherCodeTitle) title(scanOtherCodeTitle)