Add ability to save remote backup key to device password manager.

Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
jeffrey-signal
2025-05-30 15:23:10 -04:00
committed by Cody Henthorne
parent 7616ec1fd2
commit 015fc9be2c
12 changed files with 392 additions and 88 deletions

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.backups.remote
import org.signal.core.util.logging.Log
import org.signal.core.util.logging.logW
/**
* Handles the process of storing a backup key to the device password manager.
*/
interface BackupKeyCredentialManagerHandler {
companion object {
private val TAG = Log.tag(BackupKeyCredentialManagerHandler::class)
}
/** Updates the [BackupKeySaveState]. Implementers must update their associated state to match [newState]. */
fun updateBackupKeySaveState(newState: BackupKeySaveState?)
/** Called when the user initiates the backup key save flow. */
fun onBackupKeySaveRequested() = updateBackupKeySaveState(BackupKeySaveState.RequestingConfirmation)
/** Called when the user confirms they want to save the backup key to the password manager. */
fun onBackupKeySaveConfirmed() = updateBackupKeySaveState(BackupKeySaveState.AwaitingCredentialManager(isRetry = false))
/** Handles the password manager save operation response. */
fun onBackupKeySaveCompleted(result: CredentialManagerResult) {
when (result) {
is CredentialManagerResult.Success -> {
Log.d(TAG, "Successfully saved backup key to credential manager.")
updateBackupKeySaveState(newState = BackupKeySaveState.Success)
}
is CredentialManagerResult.UserCanceled -> {
Log.d(TAG, "User canceled saving backup key to credential manager.")
updateBackupKeySaveState(newState = null)
}
is CredentialManagerResult.Interrupted -> {
Log.i(TAG, "Retry saving backup key to credential manager after interruption.", result.exception)
updateBackupKeySaveState(newState = BackupKeySaveState.AwaitingCredentialManager(isRetry = true))
}
is CredentialManagerError.MissingCredentialManager -> {
Log.w(TAG, "Error saving backup key to credential manager: no credential manager is configured.", result.exception)
updateBackupKeySaveState(newState = BackupKeySaveState.Error(result))
}
is CredentialManagerError.Unexpected -> {
throw result.exception.logW(TAG, "Unexpected error when saving backup key to credential manager.")
}
}
}
}
/** Represents state related to saving a backup key to the device password manager. */
sealed interface BackupKeySaveState {
/** Awaiting the user to confirm they want to save the backup key. */
data object RequestingConfirmation : BackupKeySaveState
/** Awaiting the password manager's response for the backup key save operation. */
data class AwaitingCredentialManager(val isRetry: Boolean) : BackupKeySaveState
data object Success : BackupKeySaveState
data class Error(val errorType: CredentialManagerError) : BackupKeySaveState
}
sealed interface CredentialManagerResult {
data object Success : CredentialManagerResult
data object UserCanceled : CredentialManagerResult
/** The backup key save operation was interrupted and should be retried. */
data class Interrupted(val exception: Exception) : CredentialManagerResult
}
sealed class CredentialManagerError : CredentialManagerResult {
abstract val exception: Exception
/** No password manager is configured on the device. */
data class MissingCredentialManager(override val exception: Exception) : CredentialManagerError()
data class Unexpected(override val exception: Exception) : CredentialManagerError()
}

View File

@@ -6,11 +6,14 @@
package org.thoughtcrime.securesms.components.settings.app.backups.remote
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyRecordScreen
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.viewModel
/**
* Fragment which only displays the backup key to the user.
@@ -21,12 +24,20 @@ class BackupKeyDisplayFragment : ComposeFragment() {
const val CLIPBOARD_TIMEOUT_SECONDS = 60
}
private val viewModel: BackupKeyDisplayViewModel by viewModel { BackupKeyDisplayViewModel() }
@Composable
override fun FragmentContent() {
val state by viewModel.uiState.collectAsStateWithLifecycle()
MessageBackupsKeyRecordScreen(
backupKey = SignalStore.account.accountEntropyPool.displayValue,
keySaveState = state.keySaveState,
onNavigationClick = { findNavController().popBackStack() },
onCopyToClipboardClick = { Util.copyToClipboard(requireContext(), it, CLIPBOARD_TIMEOUT_SECONDS) },
onRequestSaveToPasswordManager = viewModel::onBackupKeySaveRequested,
onConfirmSaveToPasswordManager = viewModel::onBackupKeySaveConfirmed,
onSaveToPasswordManagerComplete = viewModel::onBackupKeySaveCompleted,
onNextClick = { findNavController().popBackStack() }
)
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.backups.remote
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
class BackupKeyDisplayViewModel : ViewModel(), BackupKeyCredentialManagerHandler {
private val _uiState = MutableStateFlow(BackupKeyDisplayUiState())
val uiState: StateFlow<BackupKeyDisplayUiState> = _uiState.asStateFlow()
override fun updateBackupKeySaveState(newState: BackupKeySaveState?) {
_uiState.update { it.copy(keySaveState = newState) }
}
}
data class BackupKeyDisplayUiState(
val keySaveState: BackupKeySaveState? = null
)