Incoming session verification request

Add more log to the state machines
Ensure the block cannot be cancelled, else if the Rust SDK emit a new state during the API execution, the state machine may cancel the api call.
Let VerificationFlowState values match the SDK api for code clarity.
Rename sub interface for clarity.
Migrate tests to the new FakeVerificationService.
This commit is contained in:
Benoit Marty 2024-10-29 09:14:40 +01:00 committed by Benoit Marty
parent c56e9604bf
commit d1b3ecab36
40 changed files with 2203 additions and 461 deletions

View file

@ -11,8 +11,9 @@ import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormat
const val A_FORMATTED_DATE = "formatted_date"
class FakeLastMessageTimestampFormatter : LastMessageTimestampFormatter {
private var format = ""
class FakeLastMessageTimestampFormatter(
var format: String = "",
) : LastMessageTimestampFormatter {
fun givenFormat(format: String) {
this.format = format
}

View file

@ -0,0 +1,15 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.core
import java.io.Serializable
@JvmInline
value class FlowId(val value: String) : Serializable {
override fun toString(): String = value
}

View file

@ -0,0 +1,23 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.verification
import android.os.Parcelable
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.FlowId
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.parcelize.Parcelize
@Parcelize
data class SessionVerificationRequestDetails(
val senderId: UserId,
val flowId: FlowId,
val deviceId: DeviceId,
val displayName: String?,
val firstSeenTimestamp: Long,
) : Parcelable

View file

@ -56,7 +56,27 @@ interface SessionVerificationService {
/**
* Returns the verification service state to the initial step.
*/
suspend fun reset()
suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean)
/**
* Register a listener to be notified of incoming session verification requests.
*/
fun setListener(listener: SessionVerificationServiceListener?)
/**
* Set this particular request as the currently active one and register for
* events pertaining it.
*/
suspend fun acknowledgeVerificationRequest(details: SessionVerificationRequestDetails)
/**
* Accept the previously acknowledged verification request.
*/
suspend fun acceptVerificationRequest()
}
interface SessionVerificationServiceListener {
fun onIncomingSessionRequest(sessionVerificationRequestDetails: SessionVerificationRequestDetails)
}
/** Verification status of the current session. */
@ -82,20 +102,20 @@ sealed interface VerificationFlowState {
data object Initial : VerificationFlowState
/** Session verification request was accepted by another device. */
data object AcceptedVerificationRequest : VerificationFlowState
data object DidAcceptVerificationRequest : VerificationFlowState
/** Short Authentication String (SAS) verification started between the 2 devices. */
data object StartedSasVerification : VerificationFlowState
data object DidStartSasVerification : VerificationFlowState
/** Verification data for the SAS verification received. */
data class ReceivedVerificationData(val data: SessionVerificationData) : VerificationFlowState
data class DidReceiveVerificationData(val data: SessionVerificationData) : VerificationFlowState
/** Verification completed successfully. */
data object Finished : VerificationFlowState
data object DidFinish : VerificationFlowState
/** Verification was cancelled by either device. */
data object Canceled : VerificationFlowState
data object DidCancel : VerificationFlowState
/** Verification failed with an error. */
data object Failed : VerificationFlowState
data object DidFail : VerificationFlowState
}

View file

@ -9,12 +9,15 @@ package io.element.android.libraries.matrix.impl.verification
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@ -28,6 +31,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.Encryption
@ -35,13 +39,13 @@ import org.matrix.rustcomponents.sdk.RecoveryState
import org.matrix.rustcomponents.sdk.RecoveryStateListener
import org.matrix.rustcomponents.sdk.SessionVerificationController
import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate
import org.matrix.rustcomponents.sdk.SessionVerificationRequestDetails
import org.matrix.rustcomponents.sdk.VerificationState
import org.matrix.rustcomponents.sdk.VerificationStateListener
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
import org.matrix.rustcomponents.sdk.SessionVerificationData as RustSessionVerificationData
import org.matrix.rustcomponents.sdk.SessionVerificationRequestDetails as RustSessionVerificationRequestDetails
class RustSessionVerificationService(
private val client: Client,
@ -101,6 +105,16 @@ class RustSessionVerificationService(
.launchIn(sessionCoroutineScope)
}
override fun didReceiveVerificationRequest(details: RustSessionVerificationRequestDetails) {
listener?.onIncomingSessionRequest(details.map())
}
private var listener: SessionVerificationServiceListener? = null
override fun setListener(listener: SessionVerificationServiceListener?) {
this.listener = listener
}
override suspend fun requestVerification() = tryOrFail {
initVerificationControllerIfNeeded()
verificationController.requestVerification()
@ -120,9 +134,24 @@ class RustSessionVerificationService(
verificationController.startSasVerification()
}
override suspend fun acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) = tryOrFail {
verificationController.acknowledgeVerificationRequest(
senderId = details.senderId.value,
flowId = details.flowId.value,
)
}
override suspend fun acceptVerificationRequest() = tryOrFail {
verificationController.acceptVerificationRequest()
}
private suspend fun tryOrFail(block: suspend () -> Unit) {
runCatching {
block()
// Ensure the block cannot be cancelled, else if the Rust SDK emit a new state during the API execution,
// the state machine may cancel the api call.
withContext(NonCancellable) {
block()
}
}.onFailure {
Timber.e(it, "Failed to verify session")
didFail()
@ -133,16 +162,16 @@ class RustSessionVerificationService(
// When verification attempt is accepted by the other device
override fun didAcceptVerificationRequest() {
_verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest
_verificationFlowState.value = VerificationFlowState.DidAcceptVerificationRequest
}
override fun didCancel() {
_verificationFlowState.value = VerificationFlowState.Canceled
_verificationFlowState.value = VerificationFlowState.DidCancel
}
override fun didFail() {
Timber.e("Session verification failed with an unknown error")
_verificationFlowState.value = VerificationFlowState.Failed
_verificationFlowState.value = VerificationFlowState.DidFail
}
override fun didFinish() {
@ -158,7 +187,7 @@ class RustSessionVerificationService(
}
.onSuccess {
// Order here is important, first set the flow state as finished, then update the verification status
_verificationFlowState.value = VerificationFlowState.Finished
_verificationFlowState.value = VerificationFlowState.DidFinish
updateVerificationStatus()
}
.onFailure {
@ -169,22 +198,18 @@ class RustSessionVerificationService(
}
override fun didReceiveVerificationData(data: RustSessionVerificationData) {
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(data.map())
_verificationFlowState.value = VerificationFlowState.DidReceiveVerificationData(data.map())
}
// When the actual SAS verification starts
override fun didStartSasVerification() {
_verificationFlowState.value = VerificationFlowState.StartedSasVerification
}
override fun didReceiveVerificationRequest(details: SessionVerificationRequestDetails) {
// TODO
_verificationFlowState.value = VerificationFlowState.DidStartSasVerification
}
// end-region
override suspend fun reset() {
if (isReady.value) {
override suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean) {
if (isReady.value && cancelAnyPendingVerificationAttempt) {
// Cancel any pending verification attempt
tryOrNull { verificationController.cancelVerification() }
}
@ -213,7 +238,7 @@ class RustSessionVerificationService(
}
private suspend fun updateVerificationStatus() {
if (verificationFlowState.value == VerificationFlowState.Finished) {
if (verificationFlowState.value == VerificationFlowState.DidFinish) {
// Calling `encryptionService.verificationState()` performs a network call and it will deadlock if there is no network
// So we need to check that *only* if we know there is network connection, which is the case when the verification flow just finished
Timber.d("Updating verification status: flow just finished")

View file

@ -0,0 +1,23 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.verification
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.FlowId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
import org.matrix.rustcomponents.sdk.SessionVerificationRequestDetails as RustSessionVerificationRequestDetails
fun RustSessionVerificationRequestDetails.map() = SessionVerificationRequestDetails(
senderId = UserId(senderId),
flowId = FlowId(flowId),
deviceId = DeviceId(deviceId),
displayName = displayName,
firstSeenTimestamp = firstSeenTimestamp.toLong(),
)

View file

@ -7,79 +7,84 @@
package io.element.android.libraries.matrix.test.verification
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class FakeSessionVerificationService(
initialSessionVerifiedStatus: SessionVerifiedStatus = SessionVerifiedStatus.Unknown,
private val requestVerificationLambda: () -> Unit = { lambdaError() },
private val cancelVerificationLambda: () -> Unit = { lambdaError() },
private val approveVerificationLambda: () -> Unit = { lambdaError() },
private val declineVerificationLambda: () -> Unit = { lambdaError() },
private val startVerificationLambda: () -> Unit = { lambdaError() },
private val resetLambda: (Boolean) -> Unit = { lambdaError() },
private val acknowledgeVerificationRequestLambda: (SessionVerificationRequestDetails) -> Unit = { lambdaError() },
private val acceptVerificationRequestLambda: () -> Unit = { lambdaError() },
) : SessionVerificationService {
private val _sessionVerifiedStatus = MutableStateFlow(initialSessionVerifiedStatus)
private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
private var _needsSessionVerification = MutableStateFlow(true)
var shouldFail = false
override val verificationFlowState: StateFlow<VerificationFlowState> = _verificationFlowState
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus
override val needsSessionVerification: Flow<Boolean> = _needsSessionVerification
override suspend fun requestVerification() {
if (!shouldFail) {
_verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest
} else {
_verificationFlowState.value = VerificationFlowState.Failed
}
requestVerificationLambda()
}
override suspend fun cancelVerification() {
_verificationFlowState.value = VerificationFlowState.Canceled
cancelVerificationLambda()
}
override suspend fun approveVerification() {
if (!shouldFail) {
_verificationFlowState.value = VerificationFlowState.Finished
} else {
_verificationFlowState.value = VerificationFlowState.Failed
}
approveVerificationLambda()
}
override suspend fun declineVerification() {
if (!shouldFail) {
_verificationFlowState.value = VerificationFlowState.Canceled
} else {
_verificationFlowState.value = VerificationFlowState.Failed
}
}
fun triggerReceiveVerificationData(sessionVerificationData: SessionVerificationData) {
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(sessionVerificationData)
declineVerificationLambda()
}
override suspend fun startVerification() {
_verificationFlowState.value = VerificationFlowState.StartedSasVerification
startVerificationLambda()
}
fun givenVerifiedStatus(status: SessionVerifiedStatus) {
_sessionVerifiedStatus.value = status
override suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean) {
resetLambda(cancelAnyPendingVerificationAttempt)
}
var listener: SessionVerificationServiceListener? = null
private set
override fun setListener(listener: SessionVerificationServiceListener?) {
this.listener = listener
}
override suspend fun acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) {
acknowledgeVerificationRequestLambda(details)
}
override suspend fun acceptVerificationRequest() = simulateLongTask {
acceptVerificationRequestLambda()
}
suspend fun emitVerificationFlowState(state: VerificationFlowState) {
_verificationFlowState.emit(state)
}
suspend fun emitVerifiedStatus(status: SessionVerifiedStatus) {
_sessionVerifiedStatus.emit(status)
}
fun givenVerificationFlowState(state: VerificationFlowState) {
_verificationFlowState.value = state
}
fun givenNeedsSessionVerification(needsVerification: Boolean) {
_needsSessionVerification.value = needsVerification
}
override suspend fun reset() {
_verificationFlowState.value = VerificationFlowState.Initial
suspend fun emitNeedsSessionVerification(needsVerification: Boolean) {
_needsSessionVerification.emit(needsVerification)
}
}