Add Session Verification flow (#197)

This commit is contained in:
Jorge Martin Espinosa 2023-03-17 10:07:19 +01:00 committed by GitHub
parent 1795a844a1
commit dcb98f06aa
76 changed files with 2347 additions and 35 deletions

View file

@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.media.MediaResolver
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
interface MatrixClient {
val sessionId: SessionId
@ -29,6 +30,7 @@ interface MatrixClient {
fun startSync()
fun stopSync()
fun mediaResolver(): MediaResolver
fun sessionVerificationService(): SessionVerificationService
suspend fun logout()
suspend fun loadUserDisplayName(): Result<String>
suspend fun loadUserAvatarURLString(): Result<String>
@ -38,4 +40,6 @@ interface MatrixClient {
width: Long,
height: Long
): Result<ByteArray>
fun onSlidingSyncUpdate()
}

View file

@ -0,0 +1,105 @@
/*
* Copyright (c) 2023 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 io.element.android.libraries.matrix.api.verification
import kotlinx.coroutines.flow.StateFlow
interface SessionVerificationService {
/**
* State of the current verification flow ([VerificationFlowState.Initial] if not started).
*/
val verificationFlowState : StateFlow<VerificationFlowState>
/**
* The internal service that checks verification can only run after the initial sync.
* This [StateFlow] will notify consumers when the service is ready to be used.
*/
val isReady: StateFlow<Boolean>
/**
* Returns whether the current verification status is either: [SessionVerifiedStatus.Unknown], [SessionVerifiedStatus.NotVerified]
* or [SessionVerifiedStatus.Verified].
*/
val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus>
/**
* Request verification of the current session.
*/
fun requestVerification()
/**
* Cancels the current verification attempt.
*/
fun cancelVerification()
/**
* Approves the current verification. This must happen on both devices to successfully verify a session.
*/
fun approveVerification()
/**
* Declines the verification attempt because the user could not verify or does not trust the other side of the verification.
*/
fun declineVerification()
/**
* Starts the verification of the unverified session from another device.
*/
fun startVerification()
/**
* Returns the verification service state to the initial step.
*/
fun reset()
}
/** Verification status of the current session. */
sealed interface SessionVerifiedStatus {
/** Unknown status, we couldn't read the actual value from the SDK. */
object Unknown : SessionVerifiedStatus
/** Not verified session status. */
object NotVerified : SessionVerifiedStatus
/** Verified session status. */
object Verified : SessionVerifiedStatus
}
/** States produced by the [SessionVerificationService]. */
sealed interface VerificationFlowState {
/** Initial state. */
object Initial : VerificationFlowState
/** Session verification request was accepted by another device. */
object AcceptedVerificationRequest : VerificationFlowState
/** Short Authentication String (SAS) verification started between the 2 devices. */
object StartedSasVerification : VerificationFlowState
/** Verification data for the SAS verification (emojis) received. */
data class ReceivedVerificationData(val emoji: List<VerificationEmoji>) : VerificationFlowState
/** Verification completed successfully. */
object Finished : VerificationFlowState
/** Verification was cancelled by either device. */
object Canceled : VerificationFlowState
/** Verification failed with an error. */
object Failed : VerificationFlowState
}

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2023 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 io.element.android.libraries.matrix.api.verification
data class VerificationEmoji(
val code: String,
val name: String,
)

View file

@ -23,12 +23,17 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.MediaResolver
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.impl.media.RustMediaResolver
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
import io.element.android.libraries.matrix.impl.room.RustRoomSummaryDataSource
import io.element.android.libraries.matrix.impl.sync.SlidingSyncObserverProxy
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientDelegate
@ -53,6 +58,9 @@ class RustMatrixClient constructor(
override val sessionId: UserId = UserId(client.userId())
private val verificationService = RustSessionVerificationService()
private var slidingSyncUpdateJob: Job? = null
private val clientDelegate = object : ClientDelegate {
override fun didReceiveAuthError(isSoftLogout: Boolean) {
Timber.v("didReceiveAuthError()")
@ -131,6 +139,9 @@ class RustMatrixClient constructor(
client.setDelegate(clientDelegate)
rustRoomSummaryDataSource.init()
slidingSync.setObserver(slidingSyncObserverProxy)
slidingSyncUpdateJob = slidingSyncObserverProxy.updateSummaryFlow
.onEach { onSlidingSyncUpdate() }
.launchIn(coroutineScope)
}
private fun onRestartSync() {
@ -152,6 +163,8 @@ class RustMatrixClient constructor(
override fun mediaResolver(): MediaResolver = mediaResolver
override fun sessionVerificationService(): SessionVerificationService = verificationService
override fun startSync() {
if (client.isSoftLogout()) return
if (isSyncing.compareAndSet(false, true)) {
@ -166,12 +179,14 @@ class RustMatrixClient constructor(
}
private fun close() {
slidingSyncUpdateJob?.cancel()
stopSync()
slidingSync.setObserver(null)
rustRoomSummaryDataSource.close()
client.setDelegate(null)
visibleRoomsView.destroy()
slidingSync.destroy()
verificationService.destroy()
}
override suspend fun logout() = withContext(dispatchers.io) {
@ -226,6 +241,16 @@ class RustMatrixClient constructor(
}
}
override fun onSlidingSyncUpdate() {
if (!verificationService.isReady.value) {
try {
verificationService.verificationController = client.getSessionVerificationController()
} catch (e: Throwable) {
Timber.e(e, "Could not start verification service. Will try again on the next sliding sync update.")
}
}
}
private fun File.deleteSessionDirectory(userID: String): Boolean {
// Rust sanitises the user ID replacing invalid characters with an _
val sanitisedUserID = userID.replace(":", "_")

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2023 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 io.element.android.libraries.matrix.impl.di
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@Module
@ContributesTo(SessionScope::class)
object SessionMatrixModule {
@Provides
@SingleIn(SessionScope::class)
fun providesRustSessionVerificationService(matrixClient: MatrixClient): SessionVerificationService {
return matrixClient.sessionVerificationService()
}
}

View file

@ -36,7 +36,6 @@ class SlidingSyncObserverProxy(
val updateSummaryFlow: SharedFlow<UpdateSummary> = updateSummaryMutableFlow.asSharedFlow()
override fun didReceiveSyncUpdate(summary: UpdateSummary) {
if (summary.rooms.isEmpty()) return
coroutineScope.launch {
updateSummaryMutableFlow.emit(summary)
}

View file

@ -0,0 +1,132 @@
/*
* Copyright (c) 2023 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 io.element.android.libraries.matrix.impl.verification
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.matrix.rustcomponents.sdk.SessionVerificationController
import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate
import org.matrix.rustcomponents.sdk.SessionVerificationControllerInterface
import org.matrix.rustcomponents.sdk.SessionVerificationEmoji
import javax.inject.Inject
class RustSessionVerificationService @Inject constructor() : SessionVerificationService, SessionVerificationControllerDelegate {
var verificationController: SessionVerificationControllerInterface? = null
set(value) {
field = value
_isReady.value = value != null
// If status was 'Unknown', move it to either 'Verified' or 'NotVerified'
if (value != null) {
updateVerificationStatus(value.isVerified())
}
}
private val _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
override val verificationFlowState = _verificationFlowState.asStateFlow()
private val _isReady = MutableStateFlow(false)
override val isReady = _isReady.asStateFlow()
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus.asStateFlow()
override fun requestVerification() = tryOrFail {
verificationController?.setDelegate(this)
verificationController?.requestVerification()
}
override fun cancelVerification() = tryOrFail { verificationController?.cancelVerification() }
override fun approveVerification() = tryOrFail { verificationController?.approveVerification() }
override fun declineVerification() = tryOrFail { verificationController?.declineVerification() }
override fun startVerification() = tryOrFail {
verificationController?.setDelegate(this)
verificationController?.startSasVerification()
}
private fun tryOrFail(block: () -> Unit) {
runCatching {
block()
}.onFailure { didFail() }
}
// region Delegate implementation
// When verification attempt is accepted by the other device
override fun didAcceptVerificationRequest() {
_verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest
}
override fun didCancel() {
_verificationFlowState.value = VerificationFlowState.Canceled
}
override fun didFail() {
_verificationFlowState.value = VerificationFlowState.Failed
}
override fun didFinish() {
_verificationFlowState.value = VerificationFlowState.Finished
// Ideally this should be `verificationController?.isVerified().orFalse()` but for some reason it always returns false
updateVerificationStatus(isVerified = true)
}
override fun didReceiveVerificationData(data: List<SessionVerificationEmoji>) {
val emojis = data.map { emoji ->
emoji.use { VerificationEmoji(it.symbol(), it.description()) }
}
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojis)
}
// When the actual SAS verification starts
override fun didStartSasVerification() {
_verificationFlowState.value = VerificationFlowState.StartedSasVerification
}
// end-region
override fun reset() {
if (isReady.value) {
// Cancel any pending verification attempt
tryOrNull { verificationController?.cancelVerification() }
}
_verificationFlowState.value = VerificationFlowState.Initial
}
fun destroy() {
(verificationController as? SessionVerificationController)?.destroy()
verificationController = null
}
private fun updateVerificationStatus(isVerified: Boolean) {
val newValue = when {
!isReady.value -> SessionVerifiedStatus.Unknown
!isVerified -> SessionVerifiedStatus.NotVerified
else -> SessionVerifiedStatus.Verified
}
_sessionVerifiedStatus.value = newValue
}
}

View file

@ -22,16 +22,19 @@ import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.media.MediaResolver
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.test.media.FakeMediaResolver
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import kotlinx.coroutines.delay
class FakeMatrixClient(
override val sessionId: SessionId = A_SESSION_ID,
private val userDisplayName: Result<String> = Result.success(A_USER_NAME),
private val userAvatarURLString: Result<String> = Result.success(AN_AVATAR_URL),
override val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource()
override val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(),
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService()
) : MatrixClient {
private var logoutFailure: Throwable? = null
@ -72,4 +75,8 @@ class FakeMatrixClient(
override suspend fun loadMediaThumbnail(url: String, width: Long, height: Long): Result<ByteArray> {
return Result.success(ByteArray(0))
}
override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService
override fun onSlidingSyncUpdate() {}
}

View file

@ -0,0 +1,90 @@
/*
* Copyright (c) 2023 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 io.element.android.libraries.matrix.test.verification
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class FakeSessionVerificationService : SessionVerificationService {
private val _isReady = MutableStateFlow(false)
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
private var emojiList = emptyList<VerificationEmoji>()
var shouldFail = false
override val verificationFlowState: StateFlow<VerificationFlowState>
get() = _verificationFlowState
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus
override val isReady: StateFlow<Boolean> = _isReady
override fun requestVerification() {
_verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest
_verificationFlowState.value = VerificationFlowState.StartedSasVerification
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojiList)
}
override fun cancelVerification() {
_verificationFlowState.value = VerificationFlowState.Canceled
}
override fun approveVerification() {
if (!shouldFail) {
_verificationFlowState.value = VerificationFlowState.Finished
} else {
_verificationFlowState.value = VerificationFlowState.Failed
}
}
override fun declineVerification() {
if (!shouldFail) {
_verificationFlowState.value = VerificationFlowState.Canceled
} else {
_verificationFlowState.value = VerificationFlowState.Failed
}
}
override fun startVerification() {
_verificationFlowState.value = VerificationFlowState.StartedSasVerification
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojiList)
}
fun givenVerifiedStatus(status: SessionVerifiedStatus) {
_sessionVerifiedStatus.value = status
}
fun givenVerificationFlowState(state: VerificationFlowState) {
_verificationFlowState.value = state
}
fun givenIsReady(value: Boolean) {
_isReady.value = value
}
fun givenEmojiList(emojis: List<VerificationEmoji>) {
this.emojiList = emojis
}
override fun reset() {
_verificationFlowState.value = VerificationFlowState.Initial
}
}