mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
PM-9682: Verify with PIN on item listing (#3600)
This commit is contained in:
parent
779cd1356a
commit
7cf7536857
9 changed files with 853 additions and 74 deletions
|
@ -22,6 +22,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||||
|
@ -342,6 +343,11 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||||
*/
|
*/
|
||||||
suspend fun validatePassword(password: String): ValidatePasswordResult
|
suspend fun validatePassword(password: String): ValidatePasswordResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the PIN for the current logged in user.
|
||||||
|
*/
|
||||||
|
suspend fun validatePin(pin: String): ValidatePinResult
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the given [password] against the master password
|
* Validates the given [password] against the master password
|
||||||
* policies for the current user.
|
* policies for the current user.
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.repository
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import com.bitwarden.core.AuthRequestMethod
|
import com.bitwarden.core.AuthRequestMethod
|
||||||
import com.bitwarden.core.InitUserCryptoMethod
|
import com.bitwarden.core.InitUserCryptoMethod
|
||||||
|
import com.bitwarden.core.InitUserCryptoRequest
|
||||||
import com.bitwarden.crypto.HashPurpose
|
import com.bitwarden.crypto.HashPurpose
|
||||||
import com.bitwarden.crypto.Kdf
|
import com.bitwarden.crypto.Kdf
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||||
|
@ -54,6 +55,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
|
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||||
|
@ -87,6 +89,7 @@ import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||||
|
@ -1103,6 +1106,51 @@ class AuthRepositoryImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun validatePin(pin: String): ValidatePinResult {
|
||||||
|
val activeAccount = authDiskSource
|
||||||
|
.userState
|
||||||
|
?.activeAccount
|
||||||
|
?.profile
|
||||||
|
?: return ValidatePinResult.Error
|
||||||
|
val privateKey = authDiskSource
|
||||||
|
.getPrivateKey(userId = activeAccount.userId)
|
||||||
|
?: return ValidatePinResult.Error
|
||||||
|
val pinProtectedUserKey = authDiskSource
|
||||||
|
.getPinProtectedUserKey(userId = activeAccount.userId)
|
||||||
|
?: return ValidatePinResult.Error
|
||||||
|
|
||||||
|
// HACK: As the SDK doesn't provide a way to directly validate the pin yet, we instead
|
||||||
|
// try to initialize the user crypto, and if it succeeds then the PIN is correct, otherwise
|
||||||
|
// the PIN is incorrect.
|
||||||
|
return vaultSdkSource
|
||||||
|
.initializeCrypto(
|
||||||
|
userId = activeAccount.userId,
|
||||||
|
request = InitUserCryptoRequest(
|
||||||
|
kdfParams = activeAccount.toSdkParams(),
|
||||||
|
email = activeAccount.email,
|
||||||
|
privateKey = privateKey,
|
||||||
|
method = InitUserCryptoMethod.Pin(
|
||||||
|
pin = pin,
|
||||||
|
pinProtectedUserKey = pinProtectedUserKey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.fold(
|
||||||
|
onSuccess = {
|
||||||
|
when (it) {
|
||||||
|
InitializeCryptoResult.Success -> {
|
||||||
|
ValidatePinResult.Success(isValid = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
InitializeCryptoResult.AuthenticationError -> {
|
||||||
|
ValidatePinResult.Success(isValid = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailure = { ValidatePinResult.Error },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun validatePasswordAgainstPolicies(
|
override suspend fun validatePasswordAgainstPolicies(
|
||||||
password: String,
|
password: String,
|
||||||
): Boolean = passwordPolicies
|
): Boolean = passwordPolicies
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.x8bit.bitwarden.data.auth.repository.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models result of determining if a PIN is valid.
|
||||||
|
*/
|
||||||
|
sealed class ValidatePinResult {
|
||||||
|
/**
|
||||||
|
* The validity of the PIN was checked successfully and [isValid].
|
||||||
|
*/
|
||||||
|
data class Success(
|
||||||
|
val isValid: Boolean,
|
||||||
|
) : ValidatePinResult()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There was an error determining if the validity of the PIN.
|
||||||
|
*/
|
||||||
|
data object Error : ValidatePinResult()
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.components.dialog
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.semantics.testTag
|
||||||
|
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Bitwarden-styled dialog for the user to enter their PIN.
|
||||||
|
*
|
||||||
|
* @param onConfirmClick called when the confirm button is clicked and emits the entered PIN.
|
||||||
|
* @param onDismissRequest called when the user attempts to dismiss the dialog (for example by
|
||||||
|
* tapping outside of it).
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
@Composable
|
||||||
|
fun BitwardenPinDialog(
|
||||||
|
onConfirmClick: (masterPassword: String) -> Unit,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
) {
|
||||||
|
var pin by remember { mutableStateOf("") }
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
dismissButton = {
|
||||||
|
BitwardenTextButton(
|
||||||
|
label = stringResource(id = R.string.cancel),
|
||||||
|
onClick = onDismissRequest,
|
||||||
|
modifier = Modifier.testTag("DismissAlertButton"),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
BitwardenTextButton(
|
||||||
|
label = stringResource(id = R.string.submit),
|
||||||
|
isEnabled = pin.isNotEmpty(),
|
||||||
|
onClick = { onConfirmClick(pin) },
|
||||||
|
modifier = Modifier.testTag("AcceptAlertButton"),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.verify_pin),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
modifier = Modifier.testTag("AlertTitleText"),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
BitwardenPasswordField(
|
||||||
|
label = stringResource(id = R.string.pin),
|
||||||
|
value = pin,
|
||||||
|
onValueChange = { pin = it },
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag("AlertInputField")
|
||||||
|
.imePadding(),
|
||||||
|
autoFocus = true,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
modifier = Modifier.semantics {
|
||||||
|
testTagsAsResourceId = true
|
||||||
|
testTag = "AlertPopup"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -43,6 +43,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog
|
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog
|
||||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenOverwritePasskeyConfirmationDialog
|
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenOverwritePasskeyConfirmationDialog
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenPinDialog
|
||||||
import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState
|
import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState
|
||||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||||
|
@ -197,13 +198,6 @@ fun VaultItemListingScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDismissFido2PasswordVerification = remember(viewModel) {
|
|
||||||
{
|
|
||||||
viewModel.trySendAction(
|
|
||||||
VaultItemListingsAction.DismissFido2PasswordVerificationDialogClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRetryFido2PasswordVerification = remember(viewModel) {
|
onRetryFido2PasswordVerification = remember(viewModel) {
|
||||||
{
|
{
|
||||||
viewModel.trySendAction(
|
viewModel.trySendAction(
|
||||||
|
@ -211,6 +205,30 @@ fun VaultItemListingScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onSubmitPinFido2Verification = remember(viewModel) {
|
||||||
|
{ pin, cipherId ->
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemListingsAction.PinFido2VerificationSubmit(
|
||||||
|
pin = pin,
|
||||||
|
selectedCipherId = cipherId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRetryFido2PinVerification = remember(viewModel) {
|
||||||
|
{
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemListingsAction.RetryFido2PinVerificationClick(it),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismissFido2Verification = remember(viewModel) {
|
||||||
|
{
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemListingsAction.DismissFido2VerificationDialogClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
VaultItemListingScaffold(
|
VaultItemListingScaffold(
|
||||||
|
@ -222,6 +240,7 @@ fun VaultItemListingScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("LongMethod")
|
||||||
@Composable
|
@Composable
|
||||||
private fun VaultItemListingDialogs(
|
private fun VaultItemListingDialogs(
|
||||||
dialogState: VaultItemListingState.DialogState?,
|
dialogState: VaultItemListingState.DialogState?,
|
||||||
|
@ -229,8 +248,10 @@ private fun VaultItemListingDialogs(
|
||||||
onDismissFido2ErrorDialog: () -> Unit,
|
onDismissFido2ErrorDialog: () -> Unit,
|
||||||
onConfirmOverwriteExistingPasskey: (cipherViewId: String) -> Unit,
|
onConfirmOverwriteExistingPasskey: (cipherViewId: String) -> Unit,
|
||||||
onSubmitMasterPasswordFido2Verification: (password: String, cipherId: String) -> Unit,
|
onSubmitMasterPasswordFido2Verification: (password: String, cipherId: String) -> Unit,
|
||||||
onDismissFido2PasswordVerification: () -> Unit,
|
|
||||||
onRetryFido2PasswordVerification: (cipherViewId: String) -> Unit,
|
onRetryFido2PasswordVerification: (cipherViewId: String) -> Unit,
|
||||||
|
onSubmitPinFido2Verification: (pin: String, cipherId: String) -> Unit,
|
||||||
|
onRetryFido2PinVerification: (cipherViewId: String) -> Unit,
|
||||||
|
onDismissFido2Verification: () -> Unit,
|
||||||
) {
|
) {
|
||||||
when (dialogState) {
|
when (dialogState) {
|
||||||
is VaultItemListingState.DialogState.Error -> BitwardenBasicDialog(
|
is VaultItemListingState.DialogState.Error -> BitwardenBasicDialog(
|
||||||
|
@ -268,7 +289,7 @@ private fun VaultItemListingDialogs(
|
||||||
dialogState.selectedCipherId,
|
dialogState.selectedCipherId,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onDismissRequest = onDismissFido2PasswordVerification,
|
onDismissRequest = onDismissFido2Verification,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,6 +305,30 @@ private fun VaultItemListingDialogs(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is VaultItemListingState.DialogState.Fido2PinPrompt -> {
|
||||||
|
BitwardenPinDialog(
|
||||||
|
onConfirmClick = { pin ->
|
||||||
|
onSubmitPinFido2Verification(
|
||||||
|
pin,
|
||||||
|
dialogState.selectedCipherId,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDismissRequest = onDismissFido2Verification,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is VaultItemListingState.DialogState.Fido2PinError -> {
|
||||||
|
BitwardenBasicDialog(
|
||||||
|
visibilityState = BasicDialogState.Shown(
|
||||||
|
title = dialogState.title,
|
||||||
|
message = dialogState.message,
|
||||||
|
),
|
||||||
|
onDismissRequest = {
|
||||||
|
onRetryFido2PinVerification(dialogState.selectedCipherId)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
null -> Unit
|
null -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import com.bitwarden.vault.CipherView
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
||||||
|
@ -189,14 +190,22 @@ class VaultItemListingViewModel @Inject constructor(
|
||||||
handleMasterPasswordFido2VerificationSubmit(action)
|
handleMasterPasswordFido2VerificationSubmit(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
VaultItemListingsAction.DismissFido2PasswordVerificationDialogClick -> {
|
|
||||||
handleDismissFido2PasswordVerificationDialogClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
is VaultItemListingsAction.RetryFido2PasswordVerificationClick -> {
|
is VaultItemListingsAction.RetryFido2PasswordVerificationClick -> {
|
||||||
handleRetryFido2PasswordVerificationClick(action)
|
handleRetryFido2PasswordVerificationClick(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is VaultItemListingsAction.PinFido2VerificationSubmit -> {
|
||||||
|
handlePinFido2VerificationSubmit(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
is VaultItemListingsAction.RetryFido2PinVerificationClick -> {
|
||||||
|
handleRetryFido2PinVerificationClick(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
VaultItemListingsAction.DismissFido2VerificationDialogClick -> {
|
||||||
|
handleDismissFido2VerificationDialogClick()
|
||||||
|
}
|
||||||
|
|
||||||
is VaultItemListingsAction.BackClick -> handleBackClick()
|
is VaultItemListingsAction.BackClick -> handleBackClick()
|
||||||
is VaultItemListingsAction.FolderClick -> handleFolderClick(action)
|
is VaultItemListingsAction.FolderClick -> handleFolderClick(action)
|
||||||
is VaultItemListingsAction.CollectionClick -> handleCollectionClick(action)
|
is VaultItemListingsAction.CollectionClick -> handleCollectionClick(action)
|
||||||
|
@ -344,7 +353,13 @@ class VaultItemListingViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeAccount.vaultUnlockType == VaultUnlockType.PIN) {
|
if (activeAccount.vaultUnlockType == VaultUnlockType.PIN) {
|
||||||
// TODO: https://bitwarden.atlassian.net/browse/PM-9682
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = VaultItemListingState.DialogState.Fido2PinPrompt(
|
||||||
|
selectedCipherId = selectedCipherId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
} else if (activeAccount.hasMasterPassword) {
|
} else if (activeAccount.hasMasterPassword) {
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
@ -373,10 +388,6 @@ class VaultItemListingViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDismissFido2PasswordVerificationDialogClick() {
|
|
||||||
showFido2ErrorDialog()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleRetryFido2PasswordVerificationClick(
|
private fun handleRetryFido2PasswordVerificationClick(
|
||||||
action: VaultItemListingsAction.RetryFido2PasswordVerificationClick,
|
action: VaultItemListingsAction.RetryFido2PasswordVerificationClick,
|
||||||
) {
|
) {
|
||||||
|
@ -389,6 +400,36 @@ class VaultItemListingViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handlePinFido2VerificationSubmit(
|
||||||
|
action: VaultItemListingsAction.PinFido2VerificationSubmit,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result = authRepository.validatePin(action.pin)
|
||||||
|
sendAction(
|
||||||
|
VaultItemListingsAction.Internal.ValidateFido2PinResultReceive(
|
||||||
|
result = result,
|
||||||
|
selectedCipherId = action.selectedCipherId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRetryFido2PinVerificationClick(
|
||||||
|
action: VaultItemListingsAction.RetryFido2PinVerificationClick,
|
||||||
|
) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = VaultItemListingState.DialogState.Fido2PinPrompt(
|
||||||
|
selectedCipherId = action.selectedCipherId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDismissFido2VerificationDialogClick() {
|
||||||
|
showFido2ErrorDialog()
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleCopySendUrlClick(action: ListingItemOverflowAction.SendAction.CopyUrlClick) {
|
private fun handleCopySendUrlClick(action: ListingItemOverflowAction.SendAction.CopyUrlClick) {
|
||||||
clipboardManager.setText(text = action.sendUrl)
|
clipboardManager.setText(text = action.sendUrl)
|
||||||
}
|
}
|
||||||
|
@ -659,7 +700,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDismissDialogClick() {
|
private fun handleDismissDialogClick() {
|
||||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
clearDialogState()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDismissFido2ErrorDialogClick() {
|
private fun handleDismissFido2ErrorDialogClick() {
|
||||||
|
@ -791,6 +832,10 @@ class VaultItemListingViewModel @Inject constructor(
|
||||||
handleValidateFido2PasswordResultReceive(action)
|
handleValidateFido2PasswordResultReceive(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is VaultItemListingsAction.Internal.ValidateFido2PinResultReceive -> {
|
||||||
|
handleValidateFido2PinResultReceive(action)
|
||||||
|
}
|
||||||
|
|
||||||
is VaultItemListingsAction.Internal.PolicyUpdateReceive -> {
|
is VaultItemListingsAction.Internal.PolicyUpdateReceive -> {
|
||||||
handlePolicyUpdateReceive(action)
|
handlePolicyUpdateReceive(action)
|
||||||
}
|
}
|
||||||
|
@ -829,7 +874,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteSendResult.Success -> {
|
DeleteSendResult.Success -> {
|
||||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
clearDialogState()
|
||||||
sendEvent(VaultItemListingEvent.ShowToast(R.string.send_deleted.asText()))
|
sendEvent(VaultItemListingEvent.ShowToast(R.string.send_deleted.asText()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -854,7 +899,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is RemovePasswordSendResult.Success -> {
|
is RemovePasswordSendResult.Success -> {
|
||||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
clearDialogState()
|
||||||
sendEvent(
|
sendEvent(
|
||||||
VaultItemListingEvent.ShowToast(
|
VaultItemListingEvent.ShowToast(
|
||||||
text = R.string.send_password_removed.asText(),
|
text = R.string.send_password_removed.asText(),
|
||||||
|
@ -908,7 +953,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||||
private fun handleMasterPasswordRepromptResultReceive(
|
private fun handleMasterPasswordRepromptResultReceive(
|
||||||
action: VaultItemListingsAction.Internal.ValidatePasswordResultReceive,
|
action: VaultItemListingsAction.Internal.ValidatePasswordResultReceive,
|
||||||
) {
|
) {
|
||||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
clearDialogState()
|
||||||
|
|
||||||
when (val result = action.result) {
|
when (val result = action.result) {
|
||||||
ValidatePasswordResult.Error -> {
|
ValidatePasswordResult.Error -> {
|
||||||
|
@ -962,7 +1007,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||||
private fun handleValidateFido2PasswordResultReceive(
|
private fun handleValidateFido2PasswordResultReceive(
|
||||||
action: VaultItemListingsAction.Internal.ValidateFido2PasswordResultReceive,
|
action: VaultItemListingsAction.Internal.ValidateFido2PasswordResultReceive,
|
||||||
) {
|
) {
|
||||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
clearDialogState()
|
||||||
|
|
||||||
when (action.result) {
|
when (action.result) {
|
||||||
ValidatePasswordResult.Error -> {
|
ValidatePasswordResult.Error -> {
|
||||||
|
@ -970,39 +1015,77 @@ class VaultItemListingViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is ValidatePasswordResult.Success -> {
|
is ValidatePasswordResult.Success -> {
|
||||||
if (!action.result.isValid) {
|
if (action.result.isValid) {
|
||||||
fido2CredentialManager.authenticationAttempts += 1
|
handleValidAuthentication(action.selectedCipherId)
|
||||||
if (fido2CredentialManager.hasAuthenticationAttemptsRemaining()) {
|
} else {
|
||||||
mutableStateFlow.update {
|
handleInvalidAuthentication(
|
||||||
it.copy(
|
errorDialogState = VaultItemListingState
|
||||||
dialogState = VaultItemListingState
|
.DialogState
|
||||||
.DialogState
|
.Fido2MasterPasswordError(
|
||||||
.Fido2MasterPasswordError(
|
title = null,
|
||||||
title = null,
|
message = R.string.invalid_master_password.asText(),
|
||||||
message = R.string.invalid_master_password.asText(),
|
selectedCipherId = action.selectedCipherId,
|
||||||
selectedCipherId = action.selectedCipherId,
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showFido2ErrorDialog()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fido2CredentialManager.isUserVerified = true
|
|
||||||
fido2CredentialManager.authenticationAttempts = 0
|
|
||||||
|
|
||||||
val cipherView = getCipherViewOrNull(cipherId = action.selectedCipherId)
|
|
||||||
?: run {
|
|
||||||
showFido2ErrorDialog()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
getRequestAndRegisterCredential(cipherView = cipherView)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleValidateFido2PinResultReceive(
|
||||||
|
action: VaultItemListingsAction.Internal.ValidateFido2PinResultReceive,
|
||||||
|
) {
|
||||||
|
clearDialogState()
|
||||||
|
|
||||||
|
when (action.result) {
|
||||||
|
ValidatePinResult.Error -> {
|
||||||
|
showFido2ErrorDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
is ValidatePinResult.Success -> {
|
||||||
|
if (action.result.isValid) {
|
||||||
|
handleValidAuthentication(action.selectedCipherId)
|
||||||
|
} else {
|
||||||
|
handleInvalidAuthentication(
|
||||||
|
errorDialogState = VaultItemListingState
|
||||||
|
.DialogState
|
||||||
|
.Fido2PinError(
|
||||||
|
title = null,
|
||||||
|
message = R.string.invalid_pin.asText(),
|
||||||
|
selectedCipherId = action.selectedCipherId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleInvalidAuthentication(
|
||||||
|
errorDialogState: VaultItemListingState.DialogState,
|
||||||
|
) {
|
||||||
|
fido2CredentialManager.authenticationAttempts += 1
|
||||||
|
if (fido2CredentialManager.hasAuthenticationAttemptsRemaining()) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(dialogState = errorDialogState)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showFido2ErrorDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleValidAuthentication(selectedCipherId: String) {
|
||||||
|
fido2CredentialManager.isUserVerified = true
|
||||||
|
fido2CredentialManager.authenticationAttempts = 0
|
||||||
|
|
||||||
|
val cipherView = getCipherViewOrNull(cipherId = selectedCipherId)
|
||||||
|
?: run {
|
||||||
|
showFido2ErrorDialog()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequestAndRegisterCredential(cipherView = cipherView)
|
||||||
|
}
|
||||||
|
|
||||||
//endregion VaultItemListing Handlers
|
//endregion VaultItemListing Handlers
|
||||||
|
|
||||||
private fun vaultErrorReceive(vaultData: DataState.Error<VaultData>) {
|
private fun vaultErrorReceive(vaultData: DataState.Error<VaultData>) {
|
||||||
|
@ -1068,7 +1151,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||||
private fun handleFido2RegisterCredentialResultReceive(
|
private fun handleFido2RegisterCredentialResultReceive(
|
||||||
action: VaultItemListingsAction.Internal.Fido2RegisterCredentialResultReceive,
|
action: VaultItemListingsAction.Internal.Fido2RegisterCredentialResultReceive,
|
||||||
) {
|
) {
|
||||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
clearDialogState()
|
||||||
when (action.result) {
|
when (action.result) {
|
||||||
is Fido2RegisterCredentialResult.Error -> {
|
is Fido2RegisterCredentialResult.Error -> {
|
||||||
sendEvent(VaultItemListingEvent.ShowToast(R.string.an_error_has_occurred.asText()))
|
sendEvent(VaultItemListingEvent.ShowToast(R.string.an_error_has_occurred.asText()))
|
||||||
|
@ -1393,6 +1476,26 @@ data class VaultItemListingState(
|
||||||
val message: Text,
|
val message: Text,
|
||||||
val selectedCipherId: String,
|
val selectedCipherId: String,
|
||||||
) : DialogState()
|
) : DialogState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a dialog to prompt the user for their PIN as part of the FIDO 2
|
||||||
|
* user verification flow.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Fido2PinPrompt(
|
||||||
|
val selectedCipherId: String,
|
||||||
|
) : DialogState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a dialog to alert the user that their PIN for the FIDO 2 user
|
||||||
|
* verification flow was incorrect and to retry.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Fido2PinError(
|
||||||
|
val title: Text?,
|
||||||
|
val message: Text,
|
||||||
|
val selectedCipherId: String,
|
||||||
|
) : DialogState()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1780,9 +1883,9 @@ sealed class VaultItemListingsAction {
|
||||||
) : VaultItemListingsAction()
|
) : VaultItemListingsAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Click to dismiss the FIDO 2 password verification dialog.
|
* Click to dismiss the FIDO 2 password or PIN verification dialog.
|
||||||
*/
|
*/
|
||||||
data object DismissFido2PasswordVerificationDialogClick : VaultItemListingsAction()
|
data object DismissFido2VerificationDialogClick : VaultItemListingsAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Click to retry the FIDO 2 password verification.
|
* Click to retry the FIDO 2 password verification.
|
||||||
|
@ -1791,6 +1894,21 @@ sealed class VaultItemListingsAction {
|
||||||
val selectedCipherId: String,
|
val selectedCipherId: String,
|
||||||
) : VaultItemListingsAction()
|
) : VaultItemListingsAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click to submit the PIN for FIDO 2 verification.
|
||||||
|
*/
|
||||||
|
data class PinFido2VerificationSubmit(
|
||||||
|
val pin: String,
|
||||||
|
val selectedCipherId: String,
|
||||||
|
) : VaultItemListingsAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click to retry the FIDO 2 PIN verification.
|
||||||
|
*/
|
||||||
|
data class RetryFido2PinVerificationClick(
|
||||||
|
val selectedCipherId: String,
|
||||||
|
) : VaultItemListingsAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Click the refresh button.
|
* Click the refresh button.
|
||||||
*/
|
*/
|
||||||
|
@ -1959,6 +2077,15 @@ sealed class VaultItemListingsAction {
|
||||||
val selectedCipherId: String,
|
val selectedCipherId: String,
|
||||||
) : Internal()
|
) : Internal()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that a result for verifying the user's PIN as part of the FIDO 2
|
||||||
|
* user verification flow has been received.
|
||||||
|
*/
|
||||||
|
data class ValidateFido2PinResultReceive(
|
||||||
|
val result: ValidatePinResult,
|
||||||
|
val selectedCipherId: String,
|
||||||
|
) : Internal()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that a policy update has been received.
|
* Indicates that a policy update has been received.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -71,6 +71,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||||
|
@ -98,6 +99,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization
|
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy
|
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||||
|
@ -4646,6 +4648,168 @@ class AuthRepositoryTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `validatePin returns ValidatePinResult Error when no active account found`() = runTest {
|
||||||
|
val pin = "PIN"
|
||||||
|
fakeAuthDiskSource.userState = null
|
||||||
|
|
||||||
|
val result = repository.validatePin(pin = pin)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
ValidatePinResult.Error,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `validatePin returns ValidatePinResult Error when no private key found`() = runTest {
|
||||||
|
val pin = "PIN"
|
||||||
|
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||||
|
fakeAuthDiskSource.storePrivateKey(
|
||||||
|
userId = SINGLE_USER_STATE_1.activeUserId,
|
||||||
|
privateKey = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = repository.validatePin(pin = pin)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
ValidatePinResult.Error,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `validatePin returns ValidatePinResult Error when no pin protected user key found`() =
|
||||||
|
runTest {
|
||||||
|
val pin = "PIN"
|
||||||
|
val privateKey = "privateKey"
|
||||||
|
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||||
|
fakeAuthDiskSource.storePrivateKey(
|
||||||
|
userId = SINGLE_USER_STATE_1.activeUserId,
|
||||||
|
privateKey = privateKey,
|
||||||
|
)
|
||||||
|
fakeAuthDiskSource.storePinProtectedUserKey(
|
||||||
|
userId = SINGLE_USER_STATE_1.activeUserId,
|
||||||
|
pinProtectedUserKey = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = repository.validatePin(pin = pin)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
ValidatePinResult.Error,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `validatePin returns ValidatePinResult Error when initialize crypto fails`() = runTest {
|
||||||
|
val pin = "PIN"
|
||||||
|
val privateKey = "privateKey"
|
||||||
|
val pinProtectedUserKey = "pinProtectedUserKey"
|
||||||
|
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||||
|
fakeAuthDiskSource.storePrivateKey(
|
||||||
|
userId = SINGLE_USER_STATE_1.activeUserId,
|
||||||
|
privateKey = privateKey,
|
||||||
|
)
|
||||||
|
fakeAuthDiskSource.storePinProtectedUserKey(
|
||||||
|
userId = SINGLE_USER_STATE_1.activeUserId,
|
||||||
|
pinProtectedUserKey = pinProtectedUserKey,
|
||||||
|
)
|
||||||
|
coEvery {
|
||||||
|
vaultSdkSource.initializeCrypto(
|
||||||
|
userId = SINGLE_USER_STATE_1.activeUserId,
|
||||||
|
request = any(),
|
||||||
|
)
|
||||||
|
} returns Throwable().asFailure()
|
||||||
|
|
||||||
|
val result = repository.validatePin(pin = pin)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
ValidatePinResult.Error,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
coVerify {
|
||||||
|
vaultSdkSource.initializeCrypto(
|
||||||
|
userId = SINGLE_USER_STATE_1.activeUserId,
|
||||||
|
request = any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `validatePin returns ValidatePinResult Success with valid false when initialize cryto returns AuthenticationError`() =
|
||||||
|
runTest {
|
||||||
|
val pin = "PIN"
|
||||||
|
val privateKey = "privateKey"
|
||||||
|
val pinProtectedUserKey = "pinProtectedUserKey"
|
||||||
|
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||||
|
fakeAuthDiskSource.storePrivateKey(
|
||||||
|
userId = SINGLE_USER_STATE_1.activeUserId,
|
||||||
|
privateKey = privateKey,
|
||||||
|
)
|
||||||
|
fakeAuthDiskSource.storePinProtectedUserKey(
|
||||||
|
userId = SINGLE_USER_STATE_1.activeUserId,
|
||||||
|
pinProtectedUserKey = pinProtectedUserKey,
|
||||||
|
)
|
||||||
|
coEvery {
|
||||||
|
vaultSdkSource.initializeCrypto(
|
||||||
|
userId = SINGLE_USER_STATE_1.activeUserId,
|
||||||
|
request = any(),
|
||||||
|
)
|
||||||
|
} returns InitializeCryptoResult.AuthenticationError.asSuccess()
|
||||||
|
|
||||||
|
val result = repository.validatePin(pin = pin)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
ValidatePinResult.Success(isValid = false),
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
coVerify {
|
||||||
|
vaultSdkSource.initializeCrypto(
|
||||||
|
userId = SINGLE_USER_STATE_1.activeUserId,
|
||||||
|
request = any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `validatePin returns ValidatePinResult Success with valid true when initialize cryto returns Success`() =
|
||||||
|
runTest {
|
||||||
|
val pin = "PIN"
|
||||||
|
val privateKey = "privateKey"
|
||||||
|
val pinProtectedUserKey = "pinProtectedUserKey"
|
||||||
|
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||||
|
fakeAuthDiskSource.storePrivateKey(
|
||||||
|
userId = SINGLE_USER_STATE_1.activeUserId,
|
||||||
|
privateKey = privateKey,
|
||||||
|
)
|
||||||
|
fakeAuthDiskSource.storePinProtectedUserKey(
|
||||||
|
userId = SINGLE_USER_STATE_1.activeUserId,
|
||||||
|
pinProtectedUserKey = pinProtectedUserKey,
|
||||||
|
)
|
||||||
|
coEvery {
|
||||||
|
vaultSdkSource.initializeCrypto(
|
||||||
|
userId = SINGLE_USER_STATE_1.activeUserId,
|
||||||
|
request = any(),
|
||||||
|
)
|
||||||
|
} returns InitializeCryptoResult.Success.asSuccess()
|
||||||
|
|
||||||
|
val result = repository.validatePin(pin = pin)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
ValidatePinResult.Success(isValid = true),
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
coVerify {
|
||||||
|
vaultSdkSource.initializeCrypto(
|
||||||
|
userId = SINGLE_USER_STATE_1.activeUserId,
|
||||||
|
request = any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `logOutFlow emission for action account should call logout on the UserLogoutManager`() {
|
fun `logOutFlow emission for action account should call logout on the UserLogoutManager`() {
|
||||||
val userId = USER_ID_1
|
val userId = USER_ID_1
|
||||||
|
|
|
@ -1510,7 +1510,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
||||||
.performClick()
|
.performClick()
|
||||||
verify {
|
verify {
|
||||||
viewModel.trySendAction(
|
viewModel.trySendAction(
|
||||||
VaultItemListingsAction.DismissFido2PasswordVerificationDialogClick,
|
VaultItemListingsAction.DismissFido2VerificationDialogClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1520,7 +1520,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
||||||
.performClick()
|
.performClick()
|
||||||
verify {
|
verify {
|
||||||
viewModel.trySendAction(
|
viewModel.trySendAction(
|
||||||
VaultItemListingsAction.DismissFido2PasswordVerificationDialogClick,
|
VaultItemListingsAction.DismissFido2VerificationDialogClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1578,6 +1578,100 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fido2 pin prompt dialog should display and function according to state`() {
|
||||||
|
val selectedCipherId = "selectedCipherId"
|
||||||
|
val dialogTitle = "Verify PIN"
|
||||||
|
composeTestRule.onNode(isDialog()).assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithText(dialogTitle).assertDoesNotExist()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = VaultItemListingState.DialogState.Fido2PinPrompt(
|
||||||
|
selectedCipherId = selectedCipherId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(dialogTitle)
|
||||||
|
.assertIsDisplayed()
|
||||||
|
.assert(hasAnyAncestor(isDialog()))
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "Cancel")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.performClick()
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemListingsAction.DismissFido2VerificationDialogClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "Cancel")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.performClick()
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemListingsAction.DismissFido2VerificationDialogClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "PIN")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.performTextInput("PIN")
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "Submit")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemListingsAction.PinFido2VerificationSubmit(
|
||||||
|
pin = "PIN",
|
||||||
|
selectedCipherId = selectedCipherId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fido2 pin error dialog should display and function according to state`() {
|
||||||
|
val selectedCipherId = "selectedCipherId"
|
||||||
|
val dialogMessage = "Invalid PIN. Try again."
|
||||||
|
composeTestRule.onNode(isDialog()).assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithText(dialogMessage).assertDoesNotExist()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = VaultItemListingState.DialogState.Fido2PinError(
|
||||||
|
title = null,
|
||||||
|
message = dialogMessage.asText(),
|
||||||
|
selectedCipherId = selectedCipherId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(dialogMessage)
|
||||||
|
.assertIsDisplayed()
|
||||||
|
.assert(hasAnyAncestor(isDialog()))
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "Ok")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.performClick()
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemListingsAction.RetryFido2PinVerificationClick(
|
||||||
|
selectedCipherId = selectedCipherId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `CompleteFido2Registration event should call Fido2CompletionManager with result`() {
|
fun `CompleteFido2Registration event should call Fido2CompletionManager with result`() {
|
||||||
val result = Fido2RegisterCredentialResult.Success("mockResponse")
|
val result = Fido2RegisterCredentialResult.Success("mockResponse")
|
||||||
|
|
|
@ -9,7 +9,9 @@ import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
||||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||||
|
@ -2385,6 +2387,33 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `UserVerificationNotSupported should display Fido2PinPrompt when user has pin`() {
|
||||||
|
val viewModel = createVaultItemListingViewModel()
|
||||||
|
val selectedCipherId = "selectedCipherId"
|
||||||
|
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
|
||||||
|
accounts = listOf(
|
||||||
|
DEFAULT_ACCOUNT.copy(
|
||||||
|
vaultUnlockType = VaultUnlockType.PIN,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemListingsAction.UserVerificationNotSupported(
|
||||||
|
selectedCipherId = selectedCipherId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
verify { fido2CredentialManager.isUserVerified = false }
|
||||||
|
assertEquals(
|
||||||
|
VaultItemListingState.DialogState.Fido2PinPrompt(
|
||||||
|
selectedCipherId = selectedCipherId,
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value.dialogState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `UserVerificationNotSupported should display Fido2MasterPasswordPrompt when user has password but no pin`() {
|
fun `UserVerificationNotSupported should display Fido2MasterPasswordPrompt when user has password but no pin`() {
|
||||||
|
@ -2558,23 +2587,6 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `DismissFido2PasswordVerificationDialogClick should display Fido2ErrorDialog`() {
|
|
||||||
val viewModel = createVaultItemListingViewModel()
|
|
||||||
viewModel.trySendAction(
|
|
||||||
VaultItemListingsAction.DismissFido2PasswordVerificationDialogClick,
|
|
||||||
)
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
VaultItemListingState.DialogState.Fido2CreationFail(
|
|
||||||
title = R.string.an_error_has_occurred.asText(),
|
|
||||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
|
|
||||||
.asText(),
|
|
||||||
),
|
|
||||||
viewModel.stateFlow.value.dialogState,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `RetryFido2PasswordVerificationClick should display Fido2MasterPasswordPrompt`() {
|
fun `RetryFido2PasswordVerificationClick should display Fido2MasterPasswordPrompt`() {
|
||||||
val viewModel = createVaultItemListingViewModel()
|
val viewModel = createVaultItemListingViewModel()
|
||||||
|
@ -2594,6 +2606,193 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `PinFido2VerificationSubmit should display Fido2ErrorDialog when pin verification fails`() {
|
||||||
|
val viewModel = createVaultItemListingViewModel()
|
||||||
|
val selectedCipherId = "selectedCipherId"
|
||||||
|
val pin = "PIN"
|
||||||
|
coEvery {
|
||||||
|
authRepository.validatePin(pin = pin)
|
||||||
|
} returns ValidatePinResult.Error
|
||||||
|
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemListingsAction.PinFido2VerificationSubmit(
|
||||||
|
pin = pin,
|
||||||
|
selectedCipherId = selectedCipherId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
VaultItemListingState.DialogState.Fido2CreationFail(
|
||||||
|
title = R.string.an_error_has_occurred.asText(),
|
||||||
|
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||||
|
.asText(),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value.dialogState,
|
||||||
|
)
|
||||||
|
coVerify {
|
||||||
|
authRepository.validatePin(pin = pin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `PinFido2VerificationSubmit should display Fido2PinError when user has retries remaining`() {
|
||||||
|
val viewModel = createVaultItemListingViewModel()
|
||||||
|
val selectedCipherId = "selectedCipherId"
|
||||||
|
val pin = "PIN"
|
||||||
|
coEvery {
|
||||||
|
authRepository.validatePin(pin = pin)
|
||||||
|
} returns ValidatePinResult.Success(isValid = false)
|
||||||
|
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemListingsAction.PinFido2VerificationSubmit(
|
||||||
|
pin = pin,
|
||||||
|
selectedCipherId = selectedCipherId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
VaultItemListingState.DialogState.Fido2PinError(
|
||||||
|
title = null,
|
||||||
|
message = R.string.invalid_pin.asText(),
|
||||||
|
selectedCipherId = selectedCipherId,
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value.dialogState,
|
||||||
|
)
|
||||||
|
coVerify {
|
||||||
|
authRepository.validatePin(pin = pin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `PinFido2VerificationSubmit should display Fido2ErrorDialog when user has no retries remaining`() {
|
||||||
|
val viewModel = createVaultItemListingViewModel()
|
||||||
|
val selectedCipherId = "selectedCipherId"
|
||||||
|
val pin = "PIN"
|
||||||
|
every { fido2CredentialManager.hasAuthenticationAttemptsRemaining() } returns false
|
||||||
|
coEvery {
|
||||||
|
authRepository.validatePin(pin = pin)
|
||||||
|
} returns ValidatePinResult.Success(isValid = false)
|
||||||
|
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemListingsAction.PinFido2VerificationSubmit(
|
||||||
|
pin = pin,
|
||||||
|
selectedCipherId = selectedCipherId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
VaultItemListingState.DialogState.Fido2CreationFail(
|
||||||
|
title = R.string.an_error_has_occurred.asText(),
|
||||||
|
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||||
|
.asText(),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value.dialogState,
|
||||||
|
)
|
||||||
|
coVerify {
|
||||||
|
authRepository.validatePin(pin = pin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `PinFido2VerificationSubmit should display Fido2ErrorDialog when cipher not found`() {
|
||||||
|
val viewModel = createVaultItemListingViewModel()
|
||||||
|
val selectedCipherId = "selectedCipherId"
|
||||||
|
val pin = "PIN"
|
||||||
|
coEvery {
|
||||||
|
authRepository.validatePin(pin = pin)
|
||||||
|
} returns ValidatePinResult.Success(isValid = true)
|
||||||
|
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemListingsAction.PinFido2VerificationSubmit(
|
||||||
|
pin = pin,
|
||||||
|
selectedCipherId = selectedCipherId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
VaultItemListingState.DialogState.Fido2CreationFail(
|
||||||
|
title = R.string.an_error_has_occurred.asText(),
|
||||||
|
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||||
|
.asText(),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value.dialogState,
|
||||||
|
)
|
||||||
|
coVerify {
|
||||||
|
authRepository.validatePin(pin = pin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `PinFido2VerificationSubmit should register credential when pin authenticated successfully`() {
|
||||||
|
setupMockUri()
|
||||||
|
val viewModel = createVaultItemListingViewModel()
|
||||||
|
val cipherView = createMockCipherView(number = 1)
|
||||||
|
val selectedCipherId = cipherView.id ?: ""
|
||||||
|
val pin = "PIN"
|
||||||
|
mutableVaultDataStateFlow.value = DataState.Loaded(
|
||||||
|
data = VaultData(
|
||||||
|
cipherViewList = listOf(cipherView),
|
||||||
|
collectionViewList = emptyList(),
|
||||||
|
folderViewList = emptyList(),
|
||||||
|
sendViewList = emptyList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
coEvery {
|
||||||
|
authRepository.validatePin(pin = pin)
|
||||||
|
} returns ValidatePinResult.Success(isValid = true)
|
||||||
|
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemListingsAction.PinFido2VerificationSubmit(
|
||||||
|
pin = pin,
|
||||||
|
selectedCipherId = selectedCipherId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
coVerify {
|
||||||
|
authRepository.validatePin(pin = pin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `RetryFido2PinVerificationClick should display FidoPinPrompt`() {
|
||||||
|
val viewModel = createVaultItemListingViewModel()
|
||||||
|
val selectedCipherId = "selectedCipherId"
|
||||||
|
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemListingsAction.RetryFido2PinVerificationClick(
|
||||||
|
selectedCipherId = selectedCipherId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
VaultItemListingState.DialogState.Fido2PinPrompt(
|
||||||
|
selectedCipherId = selectedCipherId,
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value.dialogState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `DismissFido2VerificationDialogClick should display Fido2ErrorDialog`() {
|
||||||
|
val viewModel = createVaultItemListingViewModel()
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemListingsAction.DismissFido2VerificationDialogClick,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
VaultItemListingState.DialogState.Fido2CreationFail(
|
||||||
|
title = R.string.an_error_has_occurred.asText(),
|
||||||
|
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||||
|
.asText(),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value.dialogState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ConfirmOverwriteExistingPasskeyClick should check if user is verified`() =
|
fun `ConfirmOverwriteExistingPasskeyClick should check if user is verified`() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|
Loading…
Reference in a new issue