diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTest.kt index 39dfaae384..4c80808073 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTest.kt @@ -206,14 +206,14 @@ class VerificationTest : InstrumentedTest { aliceReadyPendingVerificationRequest!!.let { pr -> pr.isSasSupported shouldBe expectedResultForAlice.sasIsSupported - pr.otherCanShowQrCode shouldBe expectedResultForAlice.otherCanShowQrCode - pr.otherCanScanQrCode shouldBe expectedResultForAlice.otherCanScanQrCode + pr.weShouldShowScanOption shouldBe expectedResultForAlice.otherCanShowQrCode + pr.weShouldDisplayQRCode shouldBe expectedResultForAlice.otherCanScanQrCode } bobReadyPendingVerificationRequest!!.let { pr -> pr.isSasSupported shouldBe expectedResultForBob.sasIsSupported - pr.otherCanShowQrCode shouldBe expectedResultForBob.otherCanShowQrCode - pr.otherCanScanQrCode shouldBe expectedResultForBob.otherCanScanQrCode + pr.weShouldShowScanOption shouldBe expectedResultForBob.otherCanShowQrCode + pr.weShouldDisplayQRCode shouldBe expectedResultForBob.otherCanScanQrCode } cryptoTestData.cleanUp(testHelper) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt index daeb725d61..ffb16ab080 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt @@ -246,14 +246,14 @@ class VerificationTest : InstrumentedTest { aliceReadyPendingVerificationRequest!!.let { pr -> pr.isSasSupported shouldBe expectedResultForAlice.sasIsSupported - pr.otherCanShowQrCode shouldBe expectedResultForAlice.otherCanShowQrCode - pr.otherCanScanQrCode shouldBe expectedResultForAlice.otherCanScanQrCode + pr.weShouldShowScanOption shouldBe expectedResultForAlice.otherCanShowQrCode + pr.weShouldDisplayQRCode shouldBe expectedResultForAlice.otherCanScanQrCode } bobReadyPendingVerificationRequest!!.let { pr -> pr.isSasSupported shouldBe expectedResultForBob.sasIsSupported - pr.otherCanShowQrCode shouldBe expectedResultForBob.otherCanShowQrCode - pr.otherCanScanQrCode shouldBe expectedResultForBob.otherCanScanQrCode + pr.weShouldShowScanOption shouldBe expectedResultForBob.otherCanShowQrCode + pr.weShouldDisplayQRCode shouldBe expectedResultForBob.otherCanScanQrCode } } diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinVerificationRequest.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinVerificationRequest.kt index 9ee08aaec8..3c3ae8c742 100644 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinVerificationRequest.kt +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinVerificationRequest.kt @@ -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.session.crypto.verification.CancelCode 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.ValidVerificationInfoReady import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest @@ -35,7 +34,7 @@ internal class KotlinVerificationRequest( val otherUserId: String, var state: EVerificationState, val ageLocalTs: Long -) : IVerificationRequest { +) { var roomId: String? = null var qrCodeData: QrCodeData? = null @@ -44,21 +43,21 @@ internal class KotlinVerificationRequest( var readyInfo: ValidVerificationInfoReady? = 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 - - override fun otherUserId() = otherUserId - - override fun roomId() = roomId - - override fun targetDevices() = targetDevices - - override fun state() = state - - override fun ageLocalTs() = ageLocalTs - - override fun otherDeviceId(): String? { + fun otherDeviceId(): String? { return if (incoming) { requestInfo?.fromDevice } 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. */ - override fun isSasSupported(): Boolean { + private fun isSasSupported(): Boolean { return requestInfo?.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. */ - override fun otherCanShowQrCode(): Boolean { + private fun otherCanShowQrCode(): Boolean { return if (incoming) { requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).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. */ - override fun otherCanScanQrCode(): Boolean { + private fun otherCanScanQrCode(): Boolean { return if (incoming) { requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).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 { return toPendingVerificationRequest().toString() @@ -122,9 +121,11 @@ internal class KotlinVerificationRequest( targetDevices = targetDevices, qrCodeText = qrCodeText(), isSasSupported = isSasSupported(), - otherCanShowQrCode = otherCanShowQrCode(), - otherCanScanQrCode = otherCanScanQrCode(), + weShouldShowScanOption = otherCanShowQrCode(), + weShouldDisplayQRCode = otherCanScanQrCode(), otherDeviceId = otherDeviceId() ) } + + fun isFinished() = state == EVerificationState.Cancelled || state == EVerificationState.Done } diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt index 2e7cd0b415..8ac7944a95 100644 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt @@ -130,8 +130,8 @@ internal class VerificationActor @AssistedInject constructor( */ private val pendingRequests = HashMap>() - // Replaces the typical list of listeners pattern. Looks to me as the sane setup, not sure if more than 1 is needed as extraBufferCapacity - // We don't want to use emit as it would block if no listener is subscribed + // Replaces the typical list of listeners pattern. + // 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. val eventFlow = MutableSharedFlow(extraBufferCapacity = 4, onBufferOverflow = BufferOverflow.DROP_OLDEST) @@ -214,7 +214,7 @@ internal class VerificationActor @AssistedInject constructor( .v("[${myUserId.take(8)}]: $msg") when (msg) { is VerificationIntent.ActionRequestVerification -> { - handleRequestAdd(msg) + handleActionRequestVerification(msg) } is VerificationIntent.OnReadyReceived -> { handleReadyReceived(msg) @@ -1046,7 +1046,7 @@ internal class VerificationActor @AssistedInject constructor( existing.state = SasTransactionState.Done(true) dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) // we can forget about it - txMap[matchingRequest.otherUserId()]?.remove(matchingRequest.requestId) + txMap[matchingRequest.otherUserId]?.remove(matchingRequest.requestId) // XXX whatabout waiting for done? matchingRequest.state = EVerificationState.Done dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) @@ -1137,7 +1137,7 @@ internal class VerificationActor @AssistedInject constructor( existing.state = QRCodeVerificationState.Done dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) // we can forget about it - txMap[matchingRequest.otherUserId()]?.remove(matchingRequest.requestId) + txMap[matchingRequest.otherUserId]?.remove(matchingRequest.requestId) matchingRequest.state = EVerificationState.WaitingForDone dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) @@ -1309,6 +1309,11 @@ internal class VerificationActor @AssistedInject constructor( if (commonMethods.isEmpty()) { Timber.tag(loggerTag.value).v("Request ${msg.transactionId} no common methods") cancelRequest(existing, CancelCode.UnknownMethod) + // Upon receipt of Alice’s m.key.verification.request message, if Bob’s 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, Bob’s device should tell Bob that no supported method was found, and allow him to manually reject the request. msg.deferred.complete(null) return } @@ -1367,7 +1372,7 @@ internal class VerificationActor @AssistedInject constructor( private fun getMethodAgreement( otherUserMethods: List?, - methods: List, + myMethods: List, ): List { if (otherUserMethods.isNullOrEmpty()) { return emptyList() @@ -1375,18 +1380,18 @@ internal class VerificationActor @AssistedInject constructor( val result = mutableSetOf() - 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 result.add(VERIFICATION_METHOD_SAS) } - if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods || VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods) { - if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods && VerificationMethod.QR_CODE_SHOW in methods) { + if (VERIFICATION_METHOD_RECIPROCATE in otherUserMethods) { + if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods && VerificationMethod.QR_CODE_SHOW in myMethods) { // Other can Scan and I can show QR code result.add(VERIFICATION_METHOD_QR_CODE_SHOW) 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 result.add(VERIFICATION_METHOD_QR_CODE_SCAN) 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) } - private suspend fun handleRequestAdd(msg: VerificationIntent.ActionRequestVerification) { + private fun List.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() } // there can only be one active request per user, so cancel existing ones requestsForUser.toList().forEach { existingRequest -> @@ -1410,8 +1419,6 @@ internal class VerificationActor @AssistedInject constructor( } } - val validLocalId = LocalEcho.createLocalEchoId() - val methodValues = if (cryptoStore.getMyCrossSigningInfo()?.isTrusted().orFalse()) { // Add reciprocate method if application declares it can scan or show QR codes // Not sure if it ok to do that (?) @@ -1521,6 +1528,10 @@ internal class VerificationActor @AssistedInject constructor( 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.state = EVerificationState.Ready dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/IVerificationRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/EVerificationState.kt similarity index 55% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/IVerificationRequest.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/EVerificationState.kt index 3c68909952..86a0ebf977 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/IVerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/EVerificationState.kt @@ -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"); * you may not use this file except in compliance with the License. @@ -32,31 +32,3 @@ enum class EVerificationState { Cancelled, 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? - - 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? -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt index 632e90f5f5..5d30c847c9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt @@ -38,11 +38,10 @@ data class PendingVerificationRequest( // if available store here the qr code to show val qrCodeText: String? = null, val isSasSupported: Boolean = false, - val otherCanShowQrCode: Boolean = false, - val otherCanScanQrCode: Boolean = false, + val weShouldShowScanOption: Boolean = false, + val weShouldDisplayQRCode: Boolean = false, -) { + ) { // val isReady: Boolean = readyInfo != null // -// val isFinished: Boolean = isSuccessful || cancelConclusion != null } diff --git a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt new file mode 100644 index 0000000000..d95a2733a1 --- /dev/null +++ b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt @@ -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? = null + var aliceChannel: SendChannel? = 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> { + every { + get() + } returns mockk(relaxed = true) + } + ) + aliceChannel = aliceActor.channel + + val bobActor = fakeActor( + actorBScope, + FakeCryptoStoreForVerification.aliceMxId, + FakeCryptoStoreForVerification(StoreMode.Alice).instance, + bobTransportLayer, + mockk> { + every { + get() + } returns mockk(relaxed = true) + } + ) + bobChannel = bobActor.channel + + return TestData( + aliceActor, + bobActor + ) + } + + private fun mockTransportTo(fromUser: String, otherChannel: (() -> SendChannel?)): VerificationTransportLayer { + return mockk { + coEvery { sendToOther(any(), any(), any()) } answers { + val request = firstArg() + val type = secondArg() + val info = thirdArg>() + + 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() + val roomId = thirdArg() + val content = arg(3) + + val fakeEventId = UUID.randomUUID().toString() + transportScope.launch(Dispatchers.IO) { + when (type) { + EventType.MESSAGE -> { + val requestContent = content.toModel()?.copy( + transactionId = fakeEventId + )?.asValidObject() + otherChannel()?.send( + VerificationIntent.OnVerificationRequestReceived( + requestContent!!, + senderId = FakeCryptoStoreForVerification.aliceMxId, + roomId = roomId, + timeStamp = 0 + ) + ) + } + EventType.KEY_VERIFICATION_READY -> { + val readyContent = content.toModel() + ?.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, + ): VerificationActor { + return VerificationActor( + scope, +// channel = channel, + clock = mockk { + every { epochMillis() } returns System.currentTimeMillis() + }, + myUserId = userId, + cryptoStore = cryptoStore, + secretShareManager = mockk {}, + transportLayer = transportLayer, + crossSigningService = crossSigningService, + setDeviceVerificationAction = SetDeviceVerificationAction( + cryptoStore = cryptoStore, + userId = userId, + defaultKeysBackupService = mockk { + coEvery { checkAndStartKeysBackup() } coAnswers { } + } + ) + ) + } +} diff --git a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorTest.kt b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorTest.kt index f60741a3c7..904f8a6219 100644 --- a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorTest.kt +++ b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorTest.kt @@ -17,9 +17,7 @@ package org.matrix.android.sdk.internal.crypto.verification.org.matrix.android.sdk.internal.crypto.verification import android.util.Base64 -import io.mockk.coEvery import io.mockk.every -import io.mockk.mockk import io.mockk.mockkStatic import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope @@ -27,44 +25,32 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import kotlinx.coroutines.channels.SendChannel -import kotlinx.coroutines.isActive +import kotlinx.coroutines.delay import kotlinx.coroutines.launch 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.shouldNotBe import org.junit.Before 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.IVerificationRequest 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.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.StoreMode 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.VerificationTransportLayer -import org.matrix.android.sdk.internal.util.time.Clock -import java.util.UUID @OptIn(ExperimentalCoroutinesApi::class) class VerificationActorTest { val transportScope = CoroutineScope(SupervisorJob()) - val actorAScope = CoroutineScope(SupervisorJob()) - val actorBScope = CoroutineScope(SupervisorJob()) +// val actorAScope = CoroutineScope(SupervisorJob()) +// val actorBScope = CoroutineScope(SupervisorJob()) @Before fun setUp() { @@ -86,38 +72,10 @@ class VerificationActorTest { } @Test - fun `Request and accept`() = runTest { - var bobChannel: SendChannel? = null - var aliceChannel: SendChannel? = null - - val aliceTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.aliceMxId) { bobChannel } - val bobTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.bobMxId) { aliceChannel } - - val aliceActor = fakeActor( - actorAScope, - FakeCryptoStoreForVerification.aliceMxId, - FakeCryptoStoreForVerification(StoreMode.Alice).instance, - aliceTransportLayer, - mockk> { - every { - get() - } returns mockk(relaxed = true) - } - ) - aliceChannel = aliceActor.channel - - val bobActor = fakeActor( - actorBScope, - FakeCryptoStoreForVerification.aliceMxId, - FakeCryptoStoreForVerification(StoreMode.Alice).instance, - bobTransportLayer, - mockk> { - every { - get() - } returns mockk(relaxed = true) - } - ) - bobChannel = bobActor.channel + fun `If ready both side should support sas and Qr show and scan`() = runTest { + val testData = VerificationActorHelper().setUpActors() + val aliceActor = testData.aliceActor + val bobActor = testData.bobActor val completableDeferred = CompletableDeferred() @@ -130,23 +88,14 @@ class VerificationActorTest { } } - awaitDeferrable { - aliceActor.send( - VerificationIntent.ActionRequestVerification( - otherUserId = FakeCryptoStoreForVerification.bobMxId, - roomId = "aRoom", - methods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SHOW, VerificationMethod.QR_CODE_SCAN), - deferred = it - ) - ) - } + aliceActor.requestVerification(listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SHOW, VerificationMethod.QR_CODE_SCAN)) val bobIncomingRequest = completableDeferred.await() bobIncomingRequest.state shouldBeEqualTo EVerificationState.Requested val aliceReadied = CompletableDeferred() - val theJob = transportScope.launch { + transportScope.launch { aliceActor.eventFlow.collect { if (it is VerificationEvent.RequestUpdated && it.request.state == EVerificationState.Ready) { aliceReadied.complete(it.request) @@ -156,7 +105,7 @@ class VerificationActorTest { } // test ready - awaitDeferrable { + val bobReadied = awaitDeferrable { bobActor.send( VerificationIntent.ActionReadyRequest( bobIncomingRequest.transactionId, @@ -168,11 +117,143 @@ class VerificationActorTest { val readiedAliceSide = aliceReadied.await() - println("transporte scope active? ${transportScope.isActive}") - println("the job? ${theJob.isActive}") - 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 { + aliceActor.send(VerificationIntent.GetExistingRequest(outgoingRequest.transactionId, outgoingRequest.otherUserId, it)) + }?.state == EVerificationState.Ready + } + + val aliceReady = awaitDeferrable { + 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 { + aliceActor.send(VerificationIntent.GetExistingRequest(outgoingRequest.transactionId, outgoingRequest.otherUserId, it)) + }?.state == EVerificationState.Ready + } + + val aliceReady = awaitDeferrable { + 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): PendingVerificationRequest { + return awaitDeferrable { + send( + VerificationIntent.ActionRequestVerification( + otherUserId = FakeCryptoStoreForVerification.bobMxId, + roomId = "aRoom", + methods = methods, + deferred = it + ) + ) + } + } + + private suspend fun waitForBobToSeeIncomingRequest(bobActor: VerificationActor, aliceOutgoing: PendingVerificationRequest) { + retryUntil { + awaitDeferrable { + 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 awaitDeferrable(block: suspend ((CompletableDeferred) -> Unit)): T { @@ -181,113 +262,85 @@ class VerificationActorTest { return deferred.await() } - private fun mockTransportTo(fromUser: String, otherChannel: (() -> SendChannel?)): VerificationTransportLayer { - return mockk { - coEvery { sendToOther(any(), any(), any()) } answers { - val request = firstArg() - val type = secondArg() - val info = thirdArg>() - - 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() - val roomId = thirdArg() - val content = arg(3) - - val fakeEventId = UUID.randomUUID().toString() - transportScope.launch(Dispatchers.IO) { - when (type) { - EventType.MESSAGE -> { - val requestContent = content.toModel()?.copy( - transactionId = fakeEventId - )?.asValidObject() - otherChannel()?.send( - VerificationIntent.OnVerificationRequestReceived( - requestContent!!, - senderId = FakeCryptoStoreForVerification.aliceMxId, - roomId = roomId, - timeStamp = 0 - ) - ) - } - EventType.KEY_VERIFICATION_READY -> { - val readyContent = content.toModel() - ?.asValidObject() - otherChannel()?.send( - VerificationIntent.OnReadyReceived( - transactionId = readyContent!!.transactionId, - fromUser = fromUser, - viaRoom = roomId, - readyInfo = readyContent, - ) - ) - } - } - } - fakeEventId - } - } + private suspend fun VerificationActor.readyVerification(transactionId: String, methods: List): PendingVerificationRequest { + return awaitDeferrable { + send( + VerificationIntent.ActionReadyRequest( + transactionId, + methods = methods, + it + ) + ) + }!! } - @Test - fun `Every testing`() { - val mockStore = mockk() - every { mockStore.getDeviceId() } returns "A" - println("every ${mockStore.getDeviceId()}") - every { mockStore.getDeviceId() } returns "B" - println("every ${mockStore.getDeviceId()}") +// @Test +// fun `Every testing`() { +// val mockStore = mockk() +// every { mockStore.getDeviceId() } returns "A" +// println("every ${mockStore.getDeviceId()}") +// every { mockStore.getDeviceId() } returns "B" +// 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" - 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")}") - } - - private fun fakeActor( - scope: CoroutineScope, - userId: String, - cryptoStore: IMXCryptoStore, - transportLayer: VerificationTransportLayer, - crossSigningService: dagger.Lazy, - ): VerificationActor { - return VerificationActor( - scope, -// channel = channel, - clock = mockk { - every { epochMillis() } returns System.currentTimeMillis() - }, - myUserId = userId, - cryptoStore = cryptoStore, - secretShareManager = mockk {}, - transportLayer = transportLayer, - crossSigningService = crossSigningService, - setDeviceVerificationAction = SetDeviceVerificationAction( - cryptoStore = cryptoStore, - userId = userId, - defaultKeysBackupService = mockk { - coEvery { checkAndStartKeysBackup() } coAnswers { } - } - ) - ) - } +// @Test +// fun `Basic channel test`() { +// // val sharedFlow = MutableSharedFlow(replay = 0, extraBufferCapacity = 2, BufferOverflow.DROP_OLDEST) +// val sharedFlow = MutableSharedFlow(replay = 0)//, extraBufferCapacity = 0, BufferOverflow.DROP_OLDEST) +// +// val scope = CoroutineScope(SupervisorJob()) +// val deferred = CompletableDeferred() +// val listener = scope.launch { +// sharedFlow.onEach { +// println("L1 : Just collected $it") +// delay(1000) +// println("L1 : Just processed $it") +// if (it == 2) { +// deferred.complete(Unit) +// } +// }.launchIn(scope) +// } +// +// // scope.launch { +// // delay(700) +// println("Pre Emit 1") +// sharedFlow.tryEmit(1) +// println("Emited 1") +// sharedFlow.tryEmit(2) +// println("Emited 2") +// // } +// +// // runBlocking { +// // deferred.await() +// // } +// +// sharedFlow.onEach { +// println("L2: Just collected $it") +// 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}") +// } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/user/UserVerificationController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/user/UserVerificationController.kt index 8561fc2da2..597ff58faa 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/user/UserVerificationController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/user/UserVerificationController.kt @@ -187,7 +187,7 @@ class UserVerificationController @Inject constructor( notice(scanCodeInstructions.toEpoxyCharSequence()) } - if (request.otherCanScanQrCode && !request.qrCodeText.isNullOrEmpty()) { + if (request.weShouldDisplayQRCode && !request.qrCodeText.isNullOrEmpty()) { bottomSheetVerificationQrCodeItem { id("qr") data(request.qrCodeText!!) @@ -198,7 +198,7 @@ class UserVerificationController @Inject constructor( } } - if (request.otherCanShowQrCode) { + if (request.weShouldShowScanOption) { bottomSheetVerificationActionItem { id("openCamera") title(scanOtherCodeTitle)