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.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<IdentityChangeState> {
@Composable
override fun present(): IdentityChangeState {
val coroutineScope = rememberCoroutineScope()
val roomMemberIdentityStateChange = remember {
mutableStateOf(emptyList<RoomMemberIdentityStateChange>())
}
// Keep the ignored alert locally for now
val ignoredUserIdChange = rememberSaveable {
mutableStateOf(emptyList<UserId>())
mutableStateOf(persistentListOf<RoomMemberIdentityStateChange>())
}
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<List<RoomMemberIdentityStateChange>>) {
private fun CoroutineScope.observeRoomMemberIdentityStateChange(roomMemberIdentityStateChange: MutableState<PersistentList<RoomMemberIdentityStateChange>>) {
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")
}
}
}
/**

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_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,

View File

@@ -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<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(
room: MatrixRoom = FakeMatrixRoom(),
encryptionService: EncryptionService = FakeEncryptionService(),
): IdentityChangeStatePresenter {
return IdentityChangeStatePresenter(
room = room,
encryptionService = encryptionService,
)
}
}

View File

@@ -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<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.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<Unit> = runCatching {
val userIdentity = service.getUserIdentity(userId.value)
userIdentity.pin()
}
}

View File

@@ -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

View File

@@ -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<IdentityResetHandle?> = { lambdaError() },
private val pinUserIdentityResult: (UserId) -> Result<Unit> = { lambdaError() },
) : EncryptionService {
private var disableRecoveryFailure: Exception? = null
override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(BackupState.UNKNOWN)
@@ -117,6 +119,10 @@ class FakeEncryptionService(
return startIdentityResetLambda()
}
override suspend fun pinUserIdentity(userId: UserId): Result<Unit> {
return pinUserIdentityResult(userId)
}
companion object {
const val FAKE_RECOVERY_KEY = "fake"
}