From b5ffa6eef31ba2546a8b3c2da8bedece28065395 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Oct 2023 18:02:34 +0200 Subject: [PATCH] SecureBackup: update matrix sdk module. --- .../impl/unlock/PinUnlockPresenter.kt | 2 +- .../impl/DefaultLogoutPreferencePresenter.kt | 2 +- .../libraries/matrix/api/MatrixClient.kt | 5 +- .../api/encryption/BackupUploadState.kt | 35 +++++ .../api/encryption/EnableRecoveryProgress.kt | 25 ++++ .../api/encryption/EncryptionService.kt | 52 +++++++ .../matrix/api/encryption/RecoveryState.kt | 24 ++++ .../libraries/matrix/impl/RustMatrixClient.kt | 27 +++- .../matrix/impl/di/SessionMatrixModule.kt | 6 + .../encryption/BackupUploadStateMapper.kt | 41 ++++++ .../EnableRecoveryProgressMapper.kt | 36 +++++ .../impl/encryption/RecoveryStateMapper.kt | 31 ++++ .../impl/encryption/RustEncryptionService.kt | 133 ++++++++++++++++++ .../RustSessionVerificationService.kt | 3 +- .../libraries/matrix/test/FakeMatrixClient.kt | 10 +- .../android/libraries/matrix/test/TestData.kt | 2 + .../test/encryption/FakeEncryptionService.kt | 95 +++++++++++++ 17 files changed, 518 insertions(+), 11 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/BackupUploadState.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EnableRecoveryProgress.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/RecoveryState.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapper.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/EnableRecoveryProgressMapper.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryStateMapper.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index b26f967b4c..ed8ee1933d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -142,7 +142,7 @@ class PinUnlockPresenter @Inject constructor( private fun CoroutineScope.signOut(signOutAction: MutableState>) = launch { suspend { - matrixClient.logout() + matrixClient.logout(ignoreSdkError = true) }.runCatchingUpdatingState(signOutAction) } } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt index 2fece4449b..49d0606633 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt @@ -58,7 +58,7 @@ class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixCli private fun CoroutineScope.logout(logoutAction: MutableState>) = launch { suspend { - matrixClient.logout() + matrixClient.logout(false /* TODO */) }.runCatchingUpdatingState(logoutAction) } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index fc215e4780..f9ac0d84a5 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.core.RoomId 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.createroom.CreateRoomParameters +import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService @@ -55,6 +56,7 @@ interface MatrixClient : Closeable { fun pushersService(): PushersService fun notificationService(): NotificationService fun notificationSettingsService(): NotificationSettingsService + fun encryptionService(): EncryptionService suspend fun getCacheSize(): Long /** @@ -66,8 +68,9 @@ interface MatrixClient : Closeable { * Logout the user. * Returns an optional URL. When the URL is there, it should be presented to the user after logout for * Relying Party (RP) initiated logout on their account page. + * @param ignoreSdkError if true, the SDK will ignore any error and delete the session data anyway. */ - suspend fun logout(): String? + suspend fun logout(ignoreSdkError: Boolean): String? suspend fun loadUserDisplayName(): Result suspend fun loadUserAvatarURLString(): Result suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/BackupUploadState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/BackupUploadState.kt new file mode 100644 index 0000000000..4424db4684 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/BackupUploadState.kt @@ -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.api.encryption + +sealed interface BackupUploadState { + data object Unknown : BackupUploadState + + data class CheckingIfUploadNeeded( + val backedUpCount: Int, + val totalCount: Int, + ) : BackupUploadState + + data object Waiting : BackupUploadState + + data class Uploading( + val backedUpCount: Int, + val totalCount: Int, + ) : BackupUploadState + + data object Done : BackupUploadState +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EnableRecoveryProgress.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EnableRecoveryProgress.kt new file mode 100644 index 0000000000..1ee85a1cc1 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EnableRecoveryProgress.kt @@ -0,0 +1,25 @@ +/* + * 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.encryption + +sealed interface EnableRecoveryProgress { + data object Unknown : EnableRecoveryProgress + data object CreatingRecoveryKey : EnableRecoveryProgress + data object CreatingBackup : EnableRecoveryProgress + data class BackingUp(val backedUpCount: Int, val totalCount: Int) : EnableRecoveryProgress + data class Done(val recoveryKey: String) : EnableRecoveryProgress +} 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 new file mode 100644 index 0000000000..b11cc3f9ab --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt @@ -0,0 +1,52 @@ +/* + * 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.encryption + +import kotlinx.coroutines.flow.StateFlow + +interface EncryptionService { + val backupStateStateFlow: StateFlow + val recoveryStateStateFlow: StateFlow + val backupUploadStateStateFlow: StateFlow + val enableRecoveryProgressStateFlow: StateFlow + + suspend fun enableBackups(): Result + + suspend fun isLastDevice(): Result + + /** + * Enable recovery. Observe enableProgressStateFlow to get progress and recovery key. + */ + suspend fun enableRecovery(waitForBackupsToUpload: Boolean): Result + + /** + * Change the recovery and return the new recovery key. + */ + suspend fun resetRecoveryKey(): Result + + suspend fun disableRecovery(): Result + + /** + * Note: accept bot recoveryKey and passphrase. + */ + suspend fun fixRecoveryIssues(recoveryKey: String): Result + + /** + * Observe [backupUploadStateStateFlow] to get progress. + */ + suspend fun waitForBackupUploadSteadyState(): Result +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/RecoveryState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/RecoveryState.kt new file mode 100644 index 0000000000..c033332d8f --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/RecoveryState.kt @@ -0,0 +1,24 @@ +/* + * 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.encryption + +enum class RecoveryState { + UNKNOWN, + ENABLED, + DISABLED, + INCOMPLETE, +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 67bbcb835c..c339bca26c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.createroom.RoomVisibility +import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService @@ -41,6 +42,7 @@ import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.impl.core.toProgressWatcher +import io.element.android.libraries.matrix.impl.encryption.RustEncryptionService import io.element.android.libraries.matrix.impl.mapper.toSessionData import io.element.android.libraries.matrix.impl.media.RustMediaLoader import io.element.android.libraries.matrix.impl.notification.RustNotificationService @@ -117,6 +119,7 @@ class RustMatrixClient constructor( private val notificationService = RustNotificationService(sessionId, notificationClient, dispatchers, clock) private val notificationSettingsService = RustNotificationSettingsService(notificationSettings, dispatchers) private val roomSyncSubscriber = RoomSyncSubscriber(innerRoomListService, dispatchers) + private val encryptionService = RustEncryptionService(client, dispatchers).apply { start() } private val isLoggingOut = AtomicBoolean(false) @@ -136,7 +139,7 @@ class RustMatrixClient constructor( ) sessionStore.updateData(newData) } - doLogout(doRequest = false, removeSession = false) + doLogout(doRequest = false, removeSession = false, ignoreSdkError = false) } } else { Timber.v("didReceiveAuthError -> already cleaning up") @@ -319,6 +322,8 @@ class RustMatrixClient constructor( override fun notificationService(): NotificationService = notificationService + override fun encryptionService(): EncryptionService = encryptionService + override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService override fun close() { @@ -331,6 +336,7 @@ class RustMatrixClient constructor( innerRoomListService.destroy() notificationClient.destroy() notificationProcessSetup.destroy() + encryptionService.destroy() client.destroy() } @@ -344,16 +350,29 @@ class RustMatrixClient constructor( baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false) } - override suspend fun logout(): String? = doLogout(doRequest = true, removeSession = true) + override suspend fun logout(ignoreSdkError: Boolean): String? = doLogout( + doRequest = true, + removeSession = true, + ignoreSdkError = ignoreSdkError, + ) - private suspend fun doLogout(doRequest: Boolean, removeSession: Boolean): String? { + private suspend fun doLogout( + doRequest: Boolean, + removeSession: Boolean, + ignoreSdkError: Boolean, + ): String? { var result: String? = null withContext(sessionDispatcher) { if (doRequest) { try { result = client.logout() } catch (failure: Throwable) { - Timber.e(failure, "Fail to call logout on HS. Still delete local files.") + if (ignoreSdkError) { + Timber.e(failure, "Fail to call logout on HS. Still delete local files.") + } else { + Timber.e(failure, "Fail to call logout on HS.") + throw failure + } } } close() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt index 1159f668e6..71f01f0aac 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt @@ -21,6 +21,7 @@ import dagger.Module import dagger.Provides import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.RoomMembershipObserver @@ -50,6 +51,11 @@ object SessionMatrixModule { return matrixClient.roomListService } + @Provides + fun providesEncryptionService(matrixClient: MatrixClient): EncryptionService { + return matrixClient.encryptionService() + } + @Provides fun provideMediaLoader(matrixClient: MatrixClient): MatrixMediaLoader { return matrixClient.mediaLoader diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapper.kt new file mode 100644 index 0000000000..71bc8a081a --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapper.kt @@ -0,0 +1,41 @@ +/* + * 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.encryption + +import io.element.android.libraries.matrix.api.encryption.BackupUploadState +import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState + +class BackupUploadStateMapper { + fun map(rustEnableProgress: RustBackupUploadState): BackupUploadState { + return when (rustEnableProgress) { + is RustBackupUploadState.CheckingIfUploadNeeded -> + BackupUploadState.CheckingIfUploadNeeded( + backedUpCount = rustEnableProgress.backedUpCount.toInt(), + totalCount = rustEnableProgress.totalCount.toInt(), + ) + RustBackupUploadState.Done -> + BackupUploadState.Done + is RustBackupUploadState.Uploading -> + BackupUploadState.Uploading( + backedUpCount = rustEnableProgress.backedUpCount.toInt(), + totalCount = rustEnableProgress.totalCount.toInt(), + ) + RustBackupUploadState.Waiting -> + BackupUploadState.Waiting + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/EnableRecoveryProgressMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/EnableRecoveryProgressMapper.kt new file mode 100644 index 0000000000..a50a68267d --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/EnableRecoveryProgressMapper.kt @@ -0,0 +1,36 @@ +/* + * 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.encryption + +import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress +import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress + +class EnableRecoveryProgressMapper { + fun map(rustEnableProgress: RustEnableRecoveryProgress): EnableRecoveryProgress { + return when (rustEnableProgress) { + is RustEnableRecoveryProgress.CreatingRecoveryKey -> EnableRecoveryProgress.CreatingRecoveryKey + is RustEnableRecoveryProgress.CreatingBackup -> EnableRecoveryProgress.CreatingBackup + is RustEnableRecoveryProgress.BackingUp -> EnableRecoveryProgress.BackingUp( + backedUpCount = rustEnableProgress.backedUpCount.toInt(), + totalCount = rustEnableProgress.totalCount.toInt(), + ) + is RustEnableRecoveryProgress.Done -> EnableRecoveryProgress.Done( + recoveryKey = rustEnableProgress.recoveryKey + ) + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryStateMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryStateMapper.kt new file mode 100644 index 0000000000..8d9050f22d --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryStateMapper.kt @@ -0,0 +1,31 @@ +/* + * 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.encryption + +import io.element.android.libraries.matrix.api.encryption.RecoveryState +import org.matrix.rustcomponents.sdk.RecoveryState as RustRecoveryState + +class RecoveryStateMapper { + fun map(state: RustRecoveryState): RecoveryState { + return when (state) { + RustRecoveryState.UNKNOWN -> RecoveryState.UNKNOWN + RustRecoveryState.ENABLED -> RecoveryState.ENABLED + RustRecoveryState.DISABLED -> RecoveryState.DISABLED + RustRecoveryState.INCOMPLETE -> RecoveryState.INCOMPLETE + } + } +} 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 new file mode 100644 index 0000000000..9b1e3909d7 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt @@ -0,0 +1,133 @@ +/* + * 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.encryption + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +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 +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.RecoveryState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.BackupStateListener +import org.matrix.rustcomponents.sdk.BackupSteadyStateListener +import org.matrix.rustcomponents.sdk.Client +import org.matrix.rustcomponents.sdk.EnableRecoveryProgressListener +import org.matrix.rustcomponents.sdk.Encryption +import org.matrix.rustcomponents.sdk.RecoveryStateListener +import org.matrix.rustcomponents.sdk.BackupState as RustBackupState +import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState +import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress +import org.matrix.rustcomponents.sdk.RecoveryState as RustRecoveryState + +internal class RustEncryptionService( + client: Client, + private val dispatchers: CoroutineDispatchers, +) : EncryptionService { + + private val service: Encryption = client.encryption() + + private val backupStateMapper = BackupStateMapper() + private val recoveryStateMapper = RecoveryStateMapper() + private val enableRecoveryProgressMapper = EnableRecoveryProgressMapper() + private val backupUploadStateMapper = BackupUploadStateMapper() + + override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(service.backupState().let(backupStateMapper::map)) + override val recoveryStateStateFlow: MutableStateFlow = MutableStateFlow(service.recoveryState().let(recoveryStateMapper::map)) + override val enableRecoveryProgressStateFlow: MutableStateFlow = MutableStateFlow(EnableRecoveryProgress.Unknown) + override val backupUploadStateStateFlow: MutableStateFlow = MutableStateFlow(BackupUploadState.Unknown) + + fun start() { + service.backupStateListener(object : BackupStateListener { + override fun onUpdate(status: RustBackupState) { + backupStateStateFlow.value = backupStateMapper.map(status) + } + }) + + service.recoveryStateListener(object : RecoveryStateListener { + override fun onUpdate(status: RustRecoveryState) { + recoveryStateStateFlow.value = recoveryStateMapper.map(status) + } + }) + } + + fun destroy() { + // No way to remove the listeners... + service.destroy() + } + + override suspend fun enableBackups(): Result = withContext(dispatchers.io) { + runCatching { + service.enableBackups() + } + } + + override suspend fun enableRecovery( + waitForBackupsToUpload: Boolean, + ): Result = withContext(dispatchers.io) { + runCatching { + service.enableRecovery( + waitForBackupsToUpload = waitForBackupsToUpload, + progressListener = object : EnableRecoveryProgressListener { + override fun onUpdate(status: RustEnableRecoveryProgress) { + enableRecoveryProgressStateFlow.value = enableRecoveryProgressMapper.map(status) + } + } + ) + // enableRecovery returns the encryption key, but we read it from the state flow + .let { } + } + } + + override suspend fun waitForBackupUploadSteadyState( + ): Result = withContext(dispatchers.io) { + runCatching { + service.waitForBackupUploadSteadyState( + progressListener = object : BackupSteadyStateListener { + override fun onUpdate(status: RustBackupUploadState) { + backupUploadStateStateFlow.value = backupUploadStateMapper.map(status) + } + } + ) + } + } + + override suspend fun disableRecovery(): Result = withContext(dispatchers.io) { + runCatching { + service.disableRecovery() + } + } + + override suspend fun isLastDevice(): Result = withContext(dispatchers.io) { + runCatching { + service.isLastDevice() + } + } + + override suspend fun resetRecoveryKey(): Result = withContext(dispatchers.io) { + runCatching { + service.resetRecoveryKey() + } + } + + override suspend fun fixRecoveryIssues(recoveryKey: String): Result = withContext(dispatchers.io) { + runCatching { + service.fixRecoveryIssues(recoveryKey) + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt index 6616539e95..a4dfadfa5b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt @@ -33,9 +33,8 @@ 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( +class RustSessionVerificationService( private val syncService: RustSyncService, private val sessionCoroutineScope: CoroutineScope, ) : SessionVerificationService, SessionVerificationControllerDelegate { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 2332a37fdc..e8228e806e 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.core.RoomId 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.createroom.CreateRoomParameters +import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService @@ -33,6 +34,7 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.media.FakeMediaLoader import io.element.android.libraries.matrix.test.notification.FakeNotificationService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService @@ -55,6 +57,7 @@ class FakeMatrixClient( private val notificationService: FakeNotificationService = FakeNotificationService(), private val notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), private val syncService: FakeSyncService = FakeSyncService(), + private val encryptionService: FakeEncryptionService = FakeEncryptionService(), private val accountManagementUrlString: Result = Result.success(null), ) : MatrixClient { @@ -124,9 +127,11 @@ class FakeMatrixClient( override suspend fun clearCache() { } - override suspend fun logout(): String? { + override suspend fun logout(ignoreSdkError: Boolean): String? { delay(100) - logoutFailure?.let { throw it } + if (ignoreSdkError.not()) { + logoutFailure?.let { throw it } + } return null } @@ -173,6 +178,7 @@ class FakeMatrixClient( override fun notificationService(): NotificationService = notificationService override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService + override fun encryptionService(): EncryptionService = encryptionService override fun roomMembershipObserver(): RoomMembershipObserver { return RoomMembershipObserver() diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index 8e71447716..ca55e6d514 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -72,3 +72,5 @@ const val A_FAILURE_REASON = "There has been a failure" val A_THROWABLE = Throwable(A_FAILURE_REASON) val AN_EXCEPTION = Exception(A_FAILURE_REASON) + +const val A_RECOVERY_KEY = "1234 5678" 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 new file mode 100644 index 0000000000..548b2f9165 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt @@ -0,0 +1,95 @@ +/* + * 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.encryption + +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 +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.RecoveryState +import io.element.android.tests.testutils.simulateLongTask +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeEncryptionService : EncryptionService { + private var disableRecoveryFailure: Exception? = null + override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(BackupState.UNKNOWN) + override val recoveryStateStateFlow: MutableStateFlow = MutableStateFlow(RecoveryState.UNKNOWN) + override val enableRecoveryProgressStateFlow: MutableStateFlow = MutableStateFlow(EnableRecoveryProgress.Unknown) + override val backupUploadStateStateFlow: MutableStateFlow = MutableStateFlow(BackupUploadState.Unknown) + + private var fixRecoveryIssuesFailure: Exception? = null + + override suspend fun enableBackups(): Result = simulateLongTask { + return Result.success(Unit) + } + + fun givenDisableRecoveryFailure(exception: Exception) { + disableRecoveryFailure = exception + } + + fun givenFixRecoveryIssuesFailure(exception: Exception?) { + fixRecoveryIssuesFailure = exception + } + + override suspend fun disableRecovery(): Result = simulateLongTask { + disableRecoveryFailure?.let { return Result.failure(it) } + return Result.success(Unit) + } + + override suspend fun fixRecoveryIssues(recoveryKey: String): Result = simulateLongTask { + fixRecoveryIssuesFailure?.let { return Result.failure(it) } + return Result.success(Unit) + } + + private var isLastDevice = false + + fun givenIsLastDevice(isLastDevice: Boolean) { + this.isLastDevice = isLastDevice + } + + override suspend fun isLastDevice(): Result { + return Result.success(isLastDevice) + } + + override suspend fun resetRecoveryKey(): Result = simulateLongTask { + return Result.success(fakeRecoveryKey) + } + + override suspend fun enableRecovery(waitForBackupsToUpload: Boolean): Result = simulateLongTask { + return Result.success(Unit) + } + + override suspend fun waitForBackupUploadSteadyState(): Result { + return Result.success(Unit) + } + + suspend fun emitBackupUploadState(state: BackupUploadState) { + backupUploadStateStateFlow.emit(state) + } + + suspend fun emitBackupState(state: BackupState) { + backupStateStateFlow.emit(state) + } + + suspend fun emitEnableRecoveryProgress(state: EnableRecoveryProgress) { + enableRecoveryProgressStateFlow.emit(state) + } + + companion object { + const val fakeRecoveryKey = "fake" + } +}