Pin user identity.

This commit is contained in:
Benoit Marty 2024-10-04 15:36:56 +02:00 committed by Benoit Marty
parent 9b94edcfa3
commit 9d815d26b4
7 changed files with 69 additions and 22 deletions

View file

@ -12,33 +12,35 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.architecture.Presenter
import io.element.android.libraries.matrix.api.core.UserId 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.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMember 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.RoomMembershipState
import io.element.android.libraries.matrix.api.room.roomMembers 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.CoroutineScope
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class IdentityChangeStatePresenter @Inject constructor( class IdentityChangeStatePresenter @Inject constructor(
private val room: MatrixRoom, private val room: MatrixRoom,
private val encryptionService: EncryptionService,
) : Presenter<IdentityChangeState> { ) : Presenter<IdentityChangeState> {
@Composable @Composable
override fun present(): IdentityChangeState { override fun present(): IdentityChangeState {
val coroutineScope = rememberCoroutineScope()
val roomMemberIdentityStateChange = remember { val roomMemberIdentityStateChange = remember {
mutableStateOf(emptyList<RoomMemberIdentityStateChange>()) mutableStateOf(persistentListOf<RoomMemberIdentityStateChange>())
}
// Keep the ignored alert locally for now
val ignoredUserIdChange = rememberSaveable {
mutableStateOf(emptyList<UserId>())
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -47,39 +49,41 @@ class IdentityChangeStatePresenter @Inject constructor(
fun handleEvent(event: IdentityChangeEvent) { fun handleEvent(event: IdentityChangeEvent) {
when (event) { when (event) {
is IdentityChangeEvent.Submit -> { is IdentityChangeEvent.Submit -> coroutineScope.pinUserIdentity(event.userId)
ignoredUserIdChange.value += event.userId
// TODO notify the SDK
}
} }
} }
return IdentityChangeState( return IdentityChangeState(
roomMemberIdentityStateChanges = roomMemberIdentityStateChange.value roomMemberIdentityStateChanges = roomMemberIdentityStateChange.value,
.filter { it.roomMember.userId !in ignoredUserIdChange.value }
.toImmutableList(),
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }
private fun CoroutineScope.observeRoomMemberIdentityStateChange(roomMemberIdentityStateChange: MutableState<List<RoomMemberIdentityStateChange>>) { private fun CoroutineScope.observeRoomMemberIdentityStateChange(roomMemberIdentityStateChange: MutableState<PersistentList<RoomMemberIdentityStateChange>>) {
combine(room.identityStateChangesFlow, room.membersStateFlow) { IdentityStateChanges, membersState -> combine(room.identityStateChangesFlow, room.membersStateFlow) { IdentityStateChanges, membersState ->
IdentityStateChanges.map { IdentityStateChange -> IdentityStateChanges.map { identityStateChange ->
val member = membersState.roomMembers() val member = membersState.roomMembers()
?.firstOrNull { roomMember -> roomMember.userId == IdentityStateChange.userId } ?.firstOrNull { roomMember -> roomMember.userId == identityStateChange.userId }
?: createDefaultRoomMemberForIdentityChange(IdentityStateChange.userId) ?: createDefaultRoomMemberForIdentityChange(identityStateChange.userId)
RoomMemberIdentityStateChange( RoomMemberIdentityStateChange(
roomMember = member, roomMember = member,
identityState = IdentityStateChange.identityState, identityState = identityStateChange.identityState,
) )
} }
} }
.distinctUntilChanged() .distinctUntilChanged()
.onEach { roomMemberIdentityStateChanges -> .onEach { roomMemberIdentityStateChanges ->
roomMemberIdentityStateChange.value = roomMemberIdentityStateChanges roomMemberIdentityStateChange.value = roomMemberIdentityStateChanges.toPersistentList()
} }
.launchIn(this) .launchIn(this)
} }
private fun CoroutineScope.pinUserIdentity(userId: UserId) = launch {
encryptionService.pinUserIdentity(userId)
.onFailure {
Timber.e(it, "Failed to pin identity for user $userId")
}
}
} }
/** /**

View file

@ -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_THROWABLE
import io.element.android.libraries.matrix.test.A_UNIQUE_ID 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.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.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomInfo
@ -1019,6 +1020,7 @@ class MessagesPresenterTest {
val featureFlagService = FakeFeatureFlagService() val featureFlagService = FakeFeatureFlagService()
val identityChangeStatePresenter = IdentityChangeStatePresenter( val identityChangeStatePresenter = IdentityChangeStatePresenter(
room = matrixRoom, room = matrixRoom,
encryptionService = FakeEncryptionService(),
) )
return MessagesPresenter( return MessagesPresenter(
room = matrixRoom, room = matrixRoom,

View file

@ -9,13 +9,19 @@ package io.element.android.features.messages.impl.crypto.identity
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.typing.aTypingRoomMember 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.IdentityState
import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange 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.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState 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.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.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.tests.testutils.WarmUpRule 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 io.element.android.tests.testutils.test
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.test.runTest 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<UserId, Result<Unit>> { 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( private fun createIdentityChangeStatePresenter(
room: MatrixRoom = FakeMatrixRoom(), room: MatrixRoom = FakeMatrixRoom(),
encryptionService: EncryptionService = FakeEncryptionService(),
): IdentityChangeStatePresenter { ): IdentityChangeStatePresenter {
return IdentityChangeStatePresenter( return IdentityChangeStatePresenter(
room = room, room = room,
encryptionService = encryptionService,
) )
} }
} }

View file

@ -7,6 +7,7 @@
package io.element.android.libraries.matrix.api.encryption 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.Flow
import kotlinx.coroutines.flow.StateFlow 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. * Starts the identity reset process. This will return a handle that can be used to reset the identity.
*/ */
suspend fun startIdentityReset(): Result<IdentityResetHandle?> suspend fun startIdentityReset(): Result<IdentityResetHandle?>
/**
* Remember this identity, ensuring it does not result in a pin violation.
*/
suspend fun pinUserIdentity(userId: UserId): Result<Unit>
} }
/** /**

View file

@ -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.flatMap
import io.element.android.libraries.core.extensions.mapFailure 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.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.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
@ -202,4 +203,9 @@ internal class RustEncryptionService(
RustIdentityResetHandleFactory.create(sessionId, handle) RustIdentityResetHandleFactory.create(sessionId, handle)
} }
} }
override suspend fun pinUserIdentity(userId: UserId): Result<Unit> = runCatching {
val userIdentity = service.getUserIdentity(userId.value)
userIdentity.pin()
}
} }

View file

@ -8,7 +8,7 @@
package io.element.android.libraries.matrix.impl.mapper package io.element.android.libraries.matrix.impl.mapper
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState 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) { fun RustIdentityState.map(): IdentityState = when (this) {
RustIdentityState.VERIFIED -> IdentityState.Verified RustIdentityState.VERIFIED -> IdentityState.Verified

View file

@ -7,6 +7,7 @@
package io.element.android.libraries.matrix.test.encryption 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.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
@ -21,6 +22,7 @@ import kotlinx.coroutines.flow.flowOf
class FakeEncryptionService( class FakeEncryptionService(
var startIdentityResetLambda: () -> Result<IdentityResetHandle?> = { lambdaError() }, var startIdentityResetLambda: () -> Result<IdentityResetHandle?> = { lambdaError() },
private val pinUserIdentityResult: (UserId) -> Result<Unit> = { lambdaError() },
) : EncryptionService { ) : EncryptionService {
private var disableRecoveryFailure: Exception? = null private var disableRecoveryFailure: Exception? = null
override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(BackupState.UNKNOWN) override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(BackupState.UNKNOWN)
@ -117,6 +119,10 @@ class FakeEncryptionService(
return startIdentityResetLambda() return startIdentityResetLambda()
} }
override suspend fun pinUserIdentity(userId: UserId): Result<Unit> {
return pinUserIdentityResult(userId)
}
companion object { companion object {
const val FAKE_RECOVERY_KEY = "fake" const val FAKE_RECOVERY_KEY = "fake"
} }