From 9d815d26b49e502084148bb4cf96385e10ed8e7b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Oct 2024 15:36:56 +0200 Subject: [PATCH] Pin user identity. --- .../identity/IdentityChangeStatePresenter.kt | 46 ++++++++++--------- .../messages/impl/MessagesPresenterTest.kt | 2 + .../IdentityChangeStatePresenterTest.kt | 23 ++++++++++ .../api/encryption/EncryptionService.kt | 6 +++ .../impl/encryption/RustEncryptionService.kt | 6 +++ .../matrix/impl/mapper/IdentityState.kt | 2 +- .../test/encryption/FakeEncryptionService.kt | 6 +++ 7 files changed, 69 insertions(+), 22 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt index 1d08125b44..d40e025484 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt @@ -12,33 +12,35 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.rememberCoroutineScope import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.roomMembers -import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject class IdentityChangeStatePresenter @Inject constructor( private val room: MatrixRoom, + private val encryptionService: EncryptionService, ) : Presenter { @Composable override fun present(): IdentityChangeState { + val coroutineScope = rememberCoroutineScope() val roomMemberIdentityStateChange = remember { - mutableStateOf(emptyList()) - } - - // Keep the ignored alert locally for now - val ignoredUserIdChange = rememberSaveable { - mutableStateOf(emptyList()) + mutableStateOf(persistentListOf()) } LaunchedEffect(Unit) { @@ -47,39 +49,41 @@ class IdentityChangeStatePresenter @Inject constructor( fun handleEvent(event: IdentityChangeEvent) { when (event) { - is IdentityChangeEvent.Submit -> { - ignoredUserIdChange.value += event.userId - // TODO notify the SDK - } + is IdentityChangeEvent.Submit -> coroutineScope.pinUserIdentity(event.userId) } } return IdentityChangeState( - roomMemberIdentityStateChanges = roomMemberIdentityStateChange.value - .filter { it.roomMember.userId !in ignoredUserIdChange.value } - .toImmutableList(), + roomMemberIdentityStateChanges = roomMemberIdentityStateChange.value, eventSink = ::handleEvent, ) } - private fun CoroutineScope.observeRoomMemberIdentityStateChange(roomMemberIdentityStateChange: MutableState>) { + private fun CoroutineScope.observeRoomMemberIdentityStateChange(roomMemberIdentityStateChange: MutableState>) { combine(room.identityStateChangesFlow, room.membersStateFlow) { IdentityStateChanges, membersState -> - IdentityStateChanges.map { IdentityStateChange -> + IdentityStateChanges.map { identityStateChange -> val member = membersState.roomMembers() - ?.firstOrNull { roomMember -> roomMember.userId == IdentityStateChange.userId } - ?: createDefaultRoomMemberForIdentityChange(IdentityStateChange.userId) + ?.firstOrNull { roomMember -> roomMember.userId == identityStateChange.userId } + ?: createDefaultRoomMemberForIdentityChange(identityStateChange.userId) RoomMemberIdentityStateChange( roomMember = member, - identityState = IdentityStateChange.identityState, + identityState = identityStateChange.identityState, ) } } .distinctUntilChanged() .onEach { roomMemberIdentityStateChanges -> - roomMemberIdentityStateChange.value = roomMemberIdentityStateChanges + roomMemberIdentityStateChange.value = roomMemberIdentityStateChanges.toPersistentList() } .launchIn(this) } + + private fun CoroutineScope.pinUserIdentity(userId: UserId) = launch { + encryptionService.pinUserIdentity(userId) + .onFailure { + Timber.e(it, "Failed to pin identity for user $userId") + } + } } /** diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 1b22c59c25..0388a5e194 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -64,6 +64,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID_2 import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_UNIQUE_ID import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo @@ -1019,6 +1020,7 @@ class MessagesPresenterTest { val featureFlagService = FakeFeatureFlagService() val identityChangeStatePresenter = IdentityChangeStatePresenter( room = matrixRoom, + encryptionService = FakeEncryptionService(), ) return MessagesPresenter( room = matrixRoom, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt index 539e9f11a2..edf559a261 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt @@ -9,13 +9,19 @@ package io.element.android.features.messages.impl.crypto.identity import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.typing.aTypingRoomMember +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.test.runTest @@ -94,11 +100,28 @@ class IdentityChangeStatePresenterTest { } } + @Test + fun `present - when the user pin the identity, the presenter invokes the encryption service api`() = + runTest { + val lambda = lambdaRecorder> { Result.success(Unit) } + val encryptionService = FakeEncryptionService( + pinUserIdentityResult = lambda, + ) + val presenter = createIdentityChangeStatePresenter(encryptionService = encryptionService) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(IdentityChangeEvent.Submit(A_USER_ID)) + lambda.assertions().isCalledOnce().with(value(A_USER_ID)) + } + } + private fun createIdentityChangeStatePresenter( room: MatrixRoom = FakeMatrixRoom(), + encryptionService: EncryptionService = FakeEncryptionService(), ): IdentityChangeStatePresenter { return IdentityChangeStatePresenter( room = room, + encryptionService = encryptionService, ) } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt index 86ddef753a..0bfce8a8d2 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.api.encryption +import io.element.android.libraries.matrix.api.core.UserId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -58,6 +59,11 @@ interface EncryptionService { * Starts the identity reset process. This will return a handle that can be used to reset the identity. */ suspend fun startIdentityReset(): Result + + /** + * Remember this identity, ensuring it does not result in a pin violation. + */ + suspend fun pinUserIdentity(userId: UserId): Result } /** diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt index f4e4af7b4f..b356ce7715 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt @@ -11,6 +11,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.flatMap import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress @@ -202,4 +203,9 @@ internal class RustEncryptionService( RustIdentityResetHandleFactory.create(sessionId, handle) } } + + override suspend fun pinUserIdentity(userId: UserId): Result = runCatching { + val userIdentity = service.getUserIdentity(userId.value) + userIdentity.pin() + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/IdentityState.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/IdentityState.kt index 66e6b0e5e6..24b8bbfadd 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/IdentityState.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/IdentityState.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.matrix.impl.mapper import io.element.android.libraries.matrix.api.encryption.identity.IdentityState -import org.matrix.rustcomponents.sdk.IdentityState as RustIdentityState +import uniffi.matrix_sdk_crypto.IdentityState as RustIdentityState fun RustIdentityState.map(): IdentityState = when (this) { RustIdentityState.VERIFIED -> IdentityState.Verified diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt index 935beaf067..6778eb5838 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.test.encryption +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress @@ -21,6 +22,7 @@ import kotlinx.coroutines.flow.flowOf class FakeEncryptionService( var startIdentityResetLambda: () -> Result = { lambdaError() }, + private val pinUserIdentityResult: (UserId) -> Result = { lambdaError() }, ) : EncryptionService { private var disableRecoveryFailure: Exception? = null override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(BackupState.UNKNOWN) @@ -117,6 +119,10 @@ class FakeEncryptionService( return startIdentityResetLambda() } + override suspend fun pinUserIdentity(userId: UserId): Result { + return pinUserIdentityResult(userId) + } + companion object { const val FAKE_RECOVERY_KEY = "fake" }