Merge pull request #3733 from element-hq/feature/bma/incomingVerification
Incoming session verification
This commit is contained in:
commit
666cedd66e
85 changed files with 2275 additions and 465 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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.designsystem.atomic.molecules
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
@Composable
|
||||
fun TextWithLabelMolecule(
|
||||
label: String,
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Text(
|
||||
text = label,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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(),
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue