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.SwitchAccountResult
|
||||
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.VerifyOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
|
@ -342,6 +343,11 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
|||
*/
|
||||
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
|
||||
* policies for the current user.
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.repository
|
|||
import android.os.SystemClock
|
||||
import com.bitwarden.core.AuthRequestMethod
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
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.UserOrganizations
|
||||
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.VaultUnlockType
|
||||
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.SyncResponseJson
|
||||
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.model.VaultUnlockData
|
||||
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(
|
||||
password: String,
|
||||
): 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.BitwardenMasterPasswordDialog
|
||||
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.scaffold.BitwardenScaffold
|
||||
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) {
|
||||
{
|
||||
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(
|
||||
|
@ -222,6 +240,7 @@ fun VaultItemListingScreen(
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun VaultItemListingDialogs(
|
||||
dialogState: VaultItemListingState.DialogState?,
|
||||
|
@ -229,8 +248,10 @@ private fun VaultItemListingDialogs(
|
|||
onDismissFido2ErrorDialog: () -> Unit,
|
||||
onConfirmOverwriteExistingPasskey: (cipherViewId: String) -> Unit,
|
||||
onSubmitMasterPasswordFido2Verification: (password: String, cipherId: String) -> Unit,
|
||||
onDismissFido2PasswordVerification: () -> Unit,
|
||||
onRetryFido2PasswordVerification: (cipherViewId: String) -> Unit,
|
||||
onSubmitPinFido2Verification: (pin: String, cipherId: String) -> Unit,
|
||||
onRetryFido2PinVerification: (cipherViewId: String) -> Unit,
|
||||
onDismissFido2Verification: () -> Unit,
|
||||
) {
|
||||
when (dialogState) {
|
||||
is VaultItemListingState.DialogState.Error -> BitwardenBasicDialog(
|
||||
|
@ -268,7 +289,7 @@ private fun VaultItemListingDialogs(
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import com.bitwarden.vault.CipherView
|
|||
import com.x8bit.bitwarden.R
|
||||
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.ValidatePinResult
|
||||
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.model.Fido2CredentialRequest
|
||||
|
@ -189,14 +190,22 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
handleMasterPasswordFido2VerificationSubmit(action)
|
||||
}
|
||||
|
||||
VaultItemListingsAction.DismissFido2PasswordVerificationDialogClick -> {
|
||||
handleDismissFido2PasswordVerificationDialogClick()
|
||||
}
|
||||
|
||||
is VaultItemListingsAction.RetryFido2PasswordVerificationClick -> {
|
||||
handleRetryFido2PasswordVerificationClick(action)
|
||||
}
|
||||
|
||||
is VaultItemListingsAction.PinFido2VerificationSubmit -> {
|
||||
handlePinFido2VerificationSubmit(action)
|
||||
}
|
||||
|
||||
is VaultItemListingsAction.RetryFido2PinVerificationClick -> {
|
||||
handleRetryFido2PinVerificationClick(action)
|
||||
}
|
||||
|
||||
VaultItemListingsAction.DismissFido2VerificationDialogClick -> {
|
||||
handleDismissFido2VerificationDialogClick()
|
||||
}
|
||||
|
||||
is VaultItemListingsAction.BackClick -> handleBackClick()
|
||||
is VaultItemListingsAction.FolderClick -> handleFolderClick(action)
|
||||
is VaultItemListingsAction.CollectionClick -> handleCollectionClick(action)
|
||||
|
@ -344,7 +353,13 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
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) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
|
@ -373,10 +388,6 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleDismissFido2PasswordVerificationDialogClick() {
|
||||
showFido2ErrorDialog()
|
||||
}
|
||||
|
||||
private fun handleRetryFido2PasswordVerificationClick(
|
||||
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) {
|
||||
clipboardManager.setText(text = action.sendUrl)
|
||||
}
|
||||
|
@ -659,7 +700,7 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleDismissDialogClick() {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
clearDialogState()
|
||||
}
|
||||
|
||||
private fun handleDismissFido2ErrorDialogClick() {
|
||||
|
@ -791,6 +832,10 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
handleValidateFido2PasswordResultReceive(action)
|
||||
}
|
||||
|
||||
is VaultItemListingsAction.Internal.ValidateFido2PinResultReceive -> {
|
||||
handleValidateFido2PinResultReceive(action)
|
||||
}
|
||||
|
||||
is VaultItemListingsAction.Internal.PolicyUpdateReceive -> {
|
||||
handlePolicyUpdateReceive(action)
|
||||
}
|
||||
|
@ -829,7 +874,7 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
DeleteSendResult.Success -> {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
clearDialogState()
|
||||
sendEvent(VaultItemListingEvent.ShowToast(R.string.send_deleted.asText()))
|
||||
}
|
||||
}
|
||||
|
@ -854,7 +899,7 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
is RemovePasswordSendResult.Success -> {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
clearDialogState()
|
||||
sendEvent(
|
||||
VaultItemListingEvent.ShowToast(
|
||||
text = R.string.send_password_removed.asText(),
|
||||
|
@ -908,7 +953,7 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
private fun handleMasterPasswordRepromptResultReceive(
|
||||
action: VaultItemListingsAction.Internal.ValidatePasswordResultReceive,
|
||||
) {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
clearDialogState()
|
||||
|
||||
when (val result = action.result) {
|
||||
ValidatePasswordResult.Error -> {
|
||||
|
@ -962,7 +1007,7 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
private fun handleValidateFido2PasswordResultReceive(
|
||||
action: VaultItemListingsAction.Internal.ValidateFido2PasswordResultReceive,
|
||||
) {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
clearDialogState()
|
||||
|
||||
when (action.result) {
|
||||
ValidatePasswordResult.Error -> {
|
||||
|
@ -970,39 +1015,77 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
is ValidatePasswordResult.Success -> {
|
||||
if (!action.result.isValid) {
|
||||
fido2CredentialManager.authenticationAttempts += 1
|
||||
if (fido2CredentialManager.hasAuthenticationAttemptsRemaining()) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = VaultItemListingState
|
||||
.DialogState
|
||||
.Fido2MasterPasswordError(
|
||||
title = null,
|
||||
message = R.string.invalid_master_password.asText(),
|
||||
selectedCipherId = action.selectedCipherId,
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
showFido2ErrorDialog()
|
||||
}
|
||||
return
|
||||
if (action.result.isValid) {
|
||||
handleValidAuthentication(action.selectedCipherId)
|
||||
} else {
|
||||
handleInvalidAuthentication(
|
||||
errorDialogState = VaultItemListingState
|
||||
.DialogState
|
||||
.Fido2MasterPasswordError(
|
||||
title = null,
|
||||
message = R.string.invalid_master_password.asText(),
|
||||
selectedCipherId = action.selectedCipherId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
private fun vaultErrorReceive(vaultData: DataState.Error<VaultData>) {
|
||||
|
@ -1068,7 +1151,7 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
private fun handleFido2RegisterCredentialResultReceive(
|
||||
action: VaultItemListingsAction.Internal.Fido2RegisterCredentialResultReceive,
|
||||
) {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
clearDialogState()
|
||||
when (action.result) {
|
||||
is Fido2RegisterCredentialResult.Error -> {
|
||||
sendEvent(VaultItemListingEvent.ShowToast(R.string.an_error_has_occurred.asText()))
|
||||
|
@ -1393,6 +1476,26 @@ data class VaultItemListingState(
|
|||
val message: Text,
|
||||
val selectedCipherId: String,
|
||||
) : 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()
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -1791,6 +1894,21 @@ sealed class VaultItemListingsAction {
|
|||
val selectedCipherId: String,
|
||||
) : 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.
|
||||
*/
|
||||
|
@ -1959,6 +2077,15 @@ sealed class VaultItemListingsAction {
|
|||
val selectedCipherId: String,
|
||||
) : 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.
|
||||
*/
|
||||
|
|
|
@ -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.UserOrganizations
|
||||
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.VerifyOtpResult
|
||||
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.createMockPolicy
|
||||
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.model.VaultUnlockData
|
||||
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
|
||||
fun `logOutFlow emission for action account should call logout on the UserLogoutManager`() {
|
||||
val userId = USER_ID_1
|
||||
|
|
|
@ -1510,7 +1510,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
.performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.DismissFido2PasswordVerificationDialogClick,
|
||||
VaultItemListingsAction.DismissFido2VerificationDialogClick,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1520,7 +1520,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
.performClick()
|
||||
verify {
|
||||
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
|
||||
fun `CompleteFido2Registration event should call Fido2CompletionManager with result`() {
|
||||
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.model.SwitchAccountResult
|
||||
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.VaultUnlockType
|
||||
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.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")
|
||||
@Test
|
||||
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
|
||||
fun `RetryFido2PasswordVerificationClick should display Fido2MasterPasswordPrompt`() {
|
||||
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
|
||||
fun `ConfirmOverwriteExistingPasskeyClick should check if user is verified`() =
|
||||
runTest {
|
||||
|
|
Loading…
Reference in a new issue