BIT-2235: Add support for exporting vault data w/o passcode (#1281)

This commit is contained in:
Caleb Derosier
2024-04-18 13:03:57 -06:00
committed by Álison Fernandes
parent 9648f720be
commit dae98111e6
4 changed files with 281 additions and 55 deletions

View File

@@ -26,10 +26,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@@ -177,6 +178,9 @@ fun ExportVaultScreen(
onPasswordInputChanged = remember(viewModel) {
{ viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(it)) }
},
onSendCodeClicked = remember(viewModel) {
{ viewModel.trySendAction(ExportVaultAction.SendCodeClick) }
},
onExportVaultClick = { shouldShowConfirmationDialog = true },
modifier = Modifier
.padding(innerPadding)
@@ -193,6 +197,7 @@ private fun ExportVaultScreenContent(
onExportFormatOptionSelected: (ExportVaultFormat) -> Unit,
onFilePasswordInputChanged: (String) -> Unit,
onPasswordInputChanged: (String) -> Unit,
onSendCodeClicked: () -> Unit,
onExportVaultClick: () -> Unit,
modifier: Modifier = Modifier,
) {
@@ -207,7 +212,7 @@ private fun ExportVaultScreenContent(
BitwardenPolicyWarningText(
text = stringResource(id = R.string.disable_personal_vault_export_policy_in_effect),
modifier = Modifier
.semantics { testTag = "DisablePrivateVaultPolicyLabel" }
.testTag("DisablePrivateVaultPolicyLabel")
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
@@ -228,7 +233,7 @@ private fun ExportVaultScreenContent(
},
isEnabled = !state.policyPreventsExport,
modifier = Modifier
.semantics { testTag = "FileFormatPicker" }
.testTag("FileFormatPicker")
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
@@ -245,7 +250,7 @@ private fun ExportVaultScreenContent(
showPasswordChange = { showPassword = it },
hint = stringResource(id = R.string.password_used_to_export),
modifier = Modifier
.semantics { testTag = "FilePasswordEntry" }
.testTag("FilePasswordEntry")
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
@@ -264,44 +269,87 @@ private fun ExportVaultScreenContent(
showPassword = showPassword,
showPasswordChange = { showPassword = it },
modifier = Modifier
.semantics { testTag = "ConfirmFilePasswordEntry" }
.testTag("ConfirmFilePasswordEntry")
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
}
BitwardenPasswordField(
label = stringResource(id = R.string.master_password),
value = state.passwordInput,
readOnly = state.policyPreventsExport,
onValueChange = onPasswordInputChanged,
modifier = Modifier
.semantics { testTag = "MasterPasswordEntry" }
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
if (state.showSendCodeButton) {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = R.string.export_vault_master_password_description),
textAlign = TextAlign.Start,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
Text(
text = stringResource(id = R.string.send_verification_code_to_email),
textAlign = TextAlign.Start,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(8.dp))
BitwardenFilledTonalButton(
label = stringResource(R.string.send_code),
onClick = onSendCodeClicked,
isEnabled = !state.policyPreventsExport,
modifier = Modifier
.testTag("SendTOTPCodeButton")
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.verification_code),
value = state.passwordInput,
readOnly = state.policyPreventsExport,
hint = stringResource(id = R.string.confirm_your_identity),
onValueChange = onPasswordInputChanged,
keyboardType = KeyboardType.Number,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
} else {
BitwardenPasswordField(
label = stringResource(id = R.string.master_password),
value = state.passwordInput,
readOnly = state.policyPreventsExport,
onValueChange = onPasswordInputChanged,
modifier = Modifier
.testTag("MasterPasswordEntry")
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = R.string.export_vault_master_password_description),
textAlign = TextAlign.Start,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
}
BitwardenFilledTonalButton(
label = stringResource(id = R.string.export_vault),
onClick = onExportVaultClick,
isEnabled = !state.policyPreventsExport,
modifier = Modifier
.semantics { testTag = "ExportVaultButton" }
.testTag("ExportVaultButton")
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)

View File

@@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.manager.FileManager
@@ -42,7 +43,7 @@ private const val KEY_STATE = "state"
@HiltViewModel
class ExportVaultViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val policyManager: PolicyManager,
policyManager: PolicyManager,
savedStateHandle: SavedStateHandle,
private val vaultRepository: VaultRepository,
private val fileManager: FileManager,
@@ -61,6 +62,12 @@ class ExportVaultViewModel @Inject constructor(
policyPreventsExport = policyManager
.getActivePolicies(type = PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT)
.any(),
showSendCodeButton = authRepository
.userStateFlow
.value
?.activeAccount
?.trustedDevice
?.hasMasterPassword == false,
),
) {
/**
@@ -88,6 +95,7 @@ class ExportVaultViewModel @Inject constructor(
is ExportVaultAction.FilePasswordInputChange -> handleFilePasswordInputChanged(action)
is ExportVaultAction.ExportFormatOptionSelect -> handleExportFormatOptionSelect(action)
is ExportVaultAction.PasswordInputChanged -> handlePasswordInputChanged(action)
ExportVaultAction.SendCodeClick -> handleSendCodeClick()
is ExportVaultAction.ExportLocationReceive -> handleExportLocationReceive(action)
is ExportVaultAction.Internal.ReceiveValidatePasswordResult -> {
@@ -105,6 +113,10 @@ class ExportVaultViewModel @Inject constructor(
is ExportVaultAction.Internal.SaveExportDataToUriResultReceive -> {
handleExportDataFinishedSavingToDisk(action)
}
is ExportVaultAction.Internal.ReceiveVerifyOneTimePasscodeResult -> {
handleReceiveVerifyOneTimePasscodeResult(action)
}
}
}
@@ -158,11 +170,19 @@ class ExportVaultViewModel @Inject constructor(
// Otherwise, validate the password.
viewModelScope.launch {
sendAction(
ExportVaultAction.Internal.ReceiveValidatePasswordResult(
result = authRepository.validatePassword(
password = state.passwordInput,
),
),
if (state.showSendCodeButton) {
ExportVaultAction.Internal.ReceiveVerifyOneTimePasscodeResult(
result = authRepository.verifyOneTimePasscode(
oneTimePasscode = state.passwordInput,
),
)
} else {
ExportVaultAction.Internal.ReceiveValidatePasswordResult(
result = authRepository.validatePassword(
password = state.passwordInput,
),
)
},
)
}
}
@@ -248,6 +268,12 @@ class ExportVaultViewModel @Inject constructor(
}
}
private fun handleSendCodeClick() {
viewModelScope.launch {
authRepository.requestOneTimePasscode()
}
}
/**
* Show an alert or proceed to export the vault after validating the password.
*/
@@ -266,27 +292,7 @@ class ExportVaultViewModel @Inject constructor(
return
}
mutableStateFlow.update {
it.copy(dialogState = ExportVaultState.DialogState.Loading())
}
viewModelScope.launch {
val result = vaultRepository.exportVaultDataToString(
format = state.exportFormat.toExportFormat(
password = if (state.exportFormat == ExportVaultFormat.JSON_ENCRYPTED) {
state.filePasswordInput
} else {
state.passwordInput
},
),
)
sendAction(
ExportVaultAction.Internal.ReceiveExportVaultDataToStringResult(
result = result,
),
)
}
exportVaultData()
}
}
}
@@ -366,6 +372,45 @@ class ExportVaultViewModel @Inject constructor(
sendEvent(ExportVaultEvent.ShowToast(R.string.export_vault_success.asText()))
}
private fun handleReceiveVerifyOneTimePasscodeResult(
action: ExportVaultAction.Internal.ReceiveVerifyOneTimePasscodeResult,
) {
when (action.result) {
VerifyOtpResult.Verified -> exportVaultData()
is VerifyOtpResult.NotVerified -> {
updateStateWithError(R.string.generic_error_message.asText())
}
}
}
/**
* Handles exporting the vault data after all validation has finished.
*/
private fun exportVaultData() {
mutableStateFlow.update {
it.copy(dialogState = ExportVaultState.DialogState.Loading())
}
viewModelScope.launch {
val result = vaultRepository.exportVaultDataToString(
format = state.exportFormat.toExportFormat(
password = if (state.exportFormat == ExportVaultFormat.JSON_ENCRYPTED) {
state.filePasswordInput
} else {
state.passwordInput
},
),
)
sendAction(
ExportVaultAction.Internal.ReceiveExportVaultDataToStringResult(
result = result,
),
)
}
}
private fun updateStateWithError(message: Text) {
mutableStateFlow.update {
it.copy(
@@ -393,6 +438,7 @@ data class ExportVaultState(
val passwordInput: String,
val passwordStrengthState: PasswordStrengthState,
val policyPreventsExport: Boolean,
val showSendCodeButton: Boolean,
) : Parcelable {
/**
* Represents the current state of any dialogs on the screen.
@@ -484,6 +530,11 @@ sealed class ExportVaultAction {
*/
data class PasswordInputChanged(val input: String) : ExportVaultAction()
/**
* Indicates that the user pressed the button to send a code in place of entering a password.
*/
data object SendCodeClick : ExportVaultAction()
/**
* Models actions that the [ExportVaultViewModel] might send itself.
*/
@@ -516,5 +567,12 @@ sealed class ExportVaultAction {
data class ReceiveValidatePasswordResult(
val result: ValidatePasswordResult,
) : Internal()
/**
* Indicates that a result for verifying the one-time passcode has been received.
*/
data class ReceiveVerifyOneTimePasscodeResult(
val result: VerifyOtpResult,
) : Internal()
}
}