[PM-11886] Update handling of unprivileged apps and improve error messaging (#4694)

This commit is contained in:
Patrick Honkonen
2025-02-12 17:52:12 -05:00
committed by GitHub
parent 2aa371a972
commit 0820061466
26 changed files with 569 additions and 340 deletions
@@ -6,6 +6,7 @@ import com.bitwarden.fido.Origin
import com.bitwarden.fido.UnverifiedAssetLink
import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
@@ -22,9 +23,11 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2Cred
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidAttestationResponse
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
import com.x8bit.bitwarden.ui.platform.base.util.toHostOrPathOrNull
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.prefixHttpsIfNecessaryOrNull
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import timber.log.Timber
/**
* Primary implementation of [Fido2CredentialManager].
@@ -47,41 +50,45 @@ class Fido2CredentialManagerImpl(
fido2CreateCredentialRequest: Fido2CreateCredentialRequest,
selectedCipherView: CipherView,
): Fido2RegisterCredentialResult {
val clientData = if (fido2CreateCredentialRequest.callingAppInfo.isOriginPopulated()) {
fido2CreateCredentialRequest
.callingAppInfo
val callingAppInfo = fido2CreateCredentialRequest.callingAppInfo
val clientData = if (fido2CreateCredentialRequest.origin.isNullOrEmpty()) {
ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.packageName)
} else {
callingAppInfo
.getAppSigningSignatureFingerprint()
?.let { ClientData.DefaultWithCustomHash(hash = it) }
?: return Fido2RegisterCredentialResult.Error
?: return Fido2RegisterCredentialResult.Error(
R.string.passkey_operation_failed_because_app_is_signed_incorrectly.asText(),
)
}
val sdkOrigin = if (fido2CreateCredentialRequest.origin.isNullOrEmpty()) {
val host = getOriginUrlFromAttestationOptionsOrNull(
requestJson = fido2CreateCredentialRequest.requestJson,
)
?: return Fido2RegisterCredentialResult.Error(
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
.asText(),
)
Origin.Android(
UnverifiedAssetLink(
packageName = callingAppInfo.packageName,
sha256CertFingerprint = callingAppInfo.getSignatureFingerprintAsHexString()
?: return Fido2RegisterCredentialResult.Error(
R.string.passkey_operation_failed_because_app_signature_is_invalid
.asText(),
),
host = host,
assetLinkUrl = host,
),
)
} else {
ClientData.DefaultWithExtraData(
androidPackageName = fido2CreateCredentialRequest
.callingAppInfo
.packageName,
)
Origin.Web(fido2CreateCredentialRequest.origin)
}
val assetLinkUrl = fido2CreateCredentialRequest
.origin
?: getOriginUrlFromAttestationOptionsOrNull(fido2CreateCredentialRequest.requestJson)
?: return Fido2RegisterCredentialResult.Error
val origin = Origin.Android(
UnverifiedAssetLink(
packageName = fido2CreateCredentialRequest.packageName,
sha256CertFingerprint = fido2CreateCredentialRequest
.callingAppInfo
.getSignatureFingerprintAsHexString()
?: return Fido2RegisterCredentialResult.Error,
host = assetLinkUrl.toHostOrPathOrNull()
?: return Fido2RegisterCredentialResult.Error,
assetLinkUrl = assetLinkUrl,
),
)
return vaultSdkSource
.registerFido2Credential(
request = RegisterFido2CredentialRequest(
userId = userId,
origin = origin,
origin = sdkOrigin,
requestJson = """{"publicKey": ${fido2CreateCredentialRequest.requestJson}}""",
clientData = clientData,
selectedCipherView = selectedCipherView,
@@ -95,7 +102,11 @@ class Fido2CredentialManagerImpl(
.mapCatching { json.encodeToString(it) }
.fold(
onSuccess = { Fido2RegisterCredentialResult.Success(it) },
onFailure = { Fido2RegisterCredentialResult.Error },
onFailure = {
Fido2RegisterCredentialResult.Error(
R.string.passkey_registration_failed_due_to_an_internal_error.asText(),
)
},
)
}
@@ -114,8 +125,10 @@ class Fido2CredentialManagerImpl(
try {
json.decodeFromString<PasskeyAttestationOptions>(requestJson)
} catch (e: SerializationException) {
Timber.e(e, "Failed to decode passkey attestation options.")
null
} catch (e: IllegalArgumentException) {
Timber.e(e, "Failed to decode passkey attestation options.")
null
}
@@ -125,11 +138,14 @@ class Fido2CredentialManagerImpl(
try {
json.decodeFromString<PasskeyAssertionOptions>(requestJson)
} catch (e: SerializationException) {
Timber.e(e, "Failed to decode passkey assertion options: $e")
null
} catch (e: IllegalArgumentException) {
Timber.e(e, "Failed to decode passkey assertion options: $e")
null
}
@Suppress("LongMethod")
override suspend fun authenticateFido2Credential(
userId: String,
request: Fido2CredentialAssertionRequest,
@@ -139,22 +155,44 @@ class Fido2CredentialManagerImpl(
val clientData = request.clientDataHash
?.let { ClientData.DefaultWithCustomHash(hash = it) }
?: ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.getAppOrigin())
val origin = callingAppInfo.origin
?: getOriginUrlFromAssertionOptionsOrNull(request.requestJson)
?: return Fido2CredentialAssertionResult.Error
val relyingPartyId = json
.decodeFromStringOrNull<PasskeyAssertionOptions>(request.requestJson)
?.relyingPartyId
?: return Fido2CredentialAssertionResult.Error
?: return Fido2CredentialAssertionResult.Error(
R.string.passkey_operation_failed_because_relying_party_cannot_be_identified
.asText(),
)
val validateOriginResult = validateOrigin(
callingAppInfo = callingAppInfo,
relyingPartyId = relyingPartyId,
)
val sdkOrigin = if (!request.origin.isNullOrEmpty()) {
Origin.Web(request.origin)
} else {
val hostUrl = getOriginUrlFromAssertionOptionsOrNull(request.requestJson)
?: return Fido2CredentialAssertionResult.Error(
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
.asText(),
)
Origin.Android(
UnverifiedAssetLink(
packageName = callingAppInfo.packageName,
sha256CertFingerprint = callingAppInfo.getSignatureFingerprintAsHexString()
?: return Fido2CredentialAssertionResult.Error(
R.string.passkey_operation_failed_because_app_signature_is_invalid
.asText(),
),
host = hostUrl,
assetLinkUrl = hostUrl,
),
)
}
return when (validateOriginResult) {
is Fido2ValidateOriginResult.Error -> {
Fido2CredentialAssertionResult.Error
Fido2CredentialAssertionResult.Error(validateOriginResult.messageResId.asText())
}
is Fido2ValidateOriginResult.Success -> {
@@ -162,16 +200,7 @@ class Fido2CredentialManagerImpl(
.authenticateFido2Credential(
request = AuthenticateFido2CredentialRequest(
userId = userId,
origin = Origin.Android(
UnverifiedAssetLink(
callingAppInfo.packageName,
callingAppInfo.getSignatureFingerprintAsHexString()
?: return Fido2CredentialAssertionResult.Error,
origin.toHostOrPathOrNull()
?: return Fido2CredentialAssertionResult.Error,
origin,
),
),
origin = sdkOrigin,
requestJson = """{"publicKey": ${request.requestJson}}""",
clientData = clientData,
selectedCipherView = selectedCipherView,
@@ -183,7 +212,13 @@ class Fido2CredentialManagerImpl(
.mapCatching { json.encodeToString(it) }
.fold(
onSuccess = { Fido2CredentialAssertionResult.Success(it) },
onFailure = { Fido2CredentialAssertionResult.Error },
onFailure = {
Timber.e(it, "Failed to authenticate FIDO2 credential.")
Fido2CredentialAssertionResult.Error(
R.string.passkey_authentication_failed_due_to_an_internal_error
.asText(),
)
},
)
}
}
@@ -195,13 +230,13 @@ class Fido2CredentialManagerImpl(
private fun getOriginUrlFromAssertionOptionsOrNull(requestJson: String) =
getPasskeyAssertionOptionsOrNull(requestJson)
?.relyingPartyId
?.let { "$HTTPS$it" }
?.prefixHttpsIfNecessaryOrNull()
private fun getOriginUrlFromAttestationOptionsOrNull(requestJson: String) =
getPasskeyAttestationOptionsOrNull(requestJson)
?.relyingParty
?.id
?.let { "$HTTPS$it" }
?.prefixHttpsIfNecessaryOrNull()
}
private const val MAX_AUTHENTICATION_ATTEMPTS = 5
@@ -20,13 +20,4 @@ interface Fido2OriginManager {
callingAppInfo: CallingAppInfo,
relyingPartyId: String,
): Fido2ValidateOriginResult
/**
* Returns the privileged app origin, or null if the calling app is not allowed.
*
* @param callingAppInfo The calling app info.
*
* @return The privileged app origin, or null.
*/
suspend fun getPrivilegedAppOriginOrNull(callingAppInfo: CallingAppInfo): String?
}
@@ -32,13 +32,6 @@ class Fido2OriginManagerImpl(
}
}
override suspend fun getPrivilegedAppOriginOrNull(callingAppInfo: CallingAppInfo): String? {
if (!callingAppInfo.isOriginPopulated()) return null
return callingAppInfo.getOrigin(getGoogleAllowListOrNull().orEmpty())
?: callingAppInfo.getOrigin(getCommunityAllowListOrNull().orEmpty())
?.takeUnless { !callingAppInfo.isOriginPopulated() }
}
private suspend fun validateCallingApplicationAssetLinks(
callingAppInfo: CallingAppInfo,
relyingPartyId: String,
@@ -123,7 +116,10 @@ class Fido2OriginManagerImpl(
}
.fold(
onSuccess = { it },
onFailure = { Fido2ValidateOriginResult.Error.Unknown },
onFailure = {
Timber.e(it, "Failed to validate privileged app: ${callingAppInfo.packageName}")
Fido2ValidateOriginResult.Error.Unknown
},
)
/**
@@ -157,16 +153,4 @@ class Fido2OriginManagerImpl(
?: false
}
.takeUnless { it.isEmpty() }
private suspend fun getGoogleAllowListOrNull(): String? =
assetManager
.readAsset(GOOGLE_ALLOW_LIST_FILE_NAME)
.onFailure { Timber.e(it, "Failed to read Google allow list.") }
.getOrNull()
private suspend fun getCommunityAllowListOrNull(): String? =
assetManager
.readAsset(COMMUNITY_ALLOW_LIST_FILE_NAME)
.onFailure { Timber.e(it, "Failed to read Community allow list.") }
.getOrNull()
}
@@ -1,5 +1,7 @@
package com.x8bit.bitwarden.data.autofill.fido2.model
import com.x8bit.bitwarden.ui.platform.base.util.Text
/**
* Represents possible outcomes of a FIDO 2 credential assertion request.
*/
@@ -13,5 +15,5 @@ sealed class Fido2CredentialAssertionResult {
/**
* Indicates there was an error and the assertion was not successful.
*/
data object Error : Fido2CredentialAssertionResult()
data class Error(val message: Text) : Fido2CredentialAssertionResult()
}
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.autofill.fido2.model
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.x8bit.bitwarden.ui.platform.base.util.Text
/**
* Represents the result of a FIDO 2 Get Credentials request.
@@ -24,5 +25,7 @@ sealed class Fido2GetCredentialsResult {
/**
* Indicates an error was encountered when querying for matching credentials.
*/
data object Error : Fido2GetCredentialsResult()
data class Error(
val message: Text,
) : Fido2GetCredentialsResult()
}
@@ -1,5 +1,7 @@
package com.x8bit.bitwarden.data.autofill.fido2.model
import com.x8bit.bitwarden.ui.platform.base.util.Text
/**
* Models the data returned from creating a FIDO 2 credential.
*/
@@ -9,13 +11,13 @@ sealed class Fido2RegisterCredentialResult {
* Indicates the credential has been successfully registered.
*/
data class Success(
val registrationResponse: String,
val responseJson: String,
) : Fido2RegisterCredentialResult()
/**
* Indicates there was an error and the credential was not registered.
*/
data object Error : Fido2RegisterCredentialResult()
data class Error(val message: Text) : Fido2RegisterCredentialResult()
/**
* Indicates the user cancelled the request.
@@ -13,5 +13,5 @@ data class PublicKeyCredentialDescriptor(
@SerialName("id")
val id: String,
@SerialName("transports")
val transports: List<String>,
val transports: List<String>?,
)
@@ -104,6 +104,69 @@ class Fido2ProviderProcessorImpl(
}
}
override fun processGetCredentialRequest(
request: BeginGetCredentialRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
) {
// If the user is not logged in, return an error.
val userState = authRepository.userStateFlow.value
if (userState == null) {
callback.onError(GetCredentialUnknownException("Active user is required."))
return
}
// Return an unlock action if the current account is locked.
if (!userState.activeAccount.isVaultUnlocked) {
val authenticationAction = AuthenticationAction(
title = context.getString(R.string.unlock),
pendingIntent = intentManager.createFido2UnlockPendingIntent(
action = UNLOCK_ACCOUNT_INTENT,
userId = userState.activeUserId,
requestCode = requestCode.getAndIncrement(),
),
)
callback.onResult(
BeginGetCredentialResponse(
authenticationActions = listOf(authenticationAction),
),
)
return
}
// Otherwise, find all matching credentials from the current vault.
val getCredentialJob = scope.launch {
try {
val credentialEntries = getMatchingFido2CredentialEntries(
userId = userState.activeUserId,
request = request,
)
callback.onResult(
BeginGetCredentialResponse(
credentialEntries = credentialEntries,
),
)
} catch (e: GetCredentialException) {
callback.onError(e)
}
}
cancellationSignal.setOnCancelListener {
callback.onError(GetCredentialCancellationException())
getCredentialJob.cancel()
}
}
override fun processClearCredentialStateRequest(
request: ProviderClearCredentialStateRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<Void?, ClearCredentialException>,
) {
// no-op: RFU
callback.onError(ClearCredentialUnsupportedException())
}
private fun processCreateCredentialRequest(
request: BeginCreateCredentialRequest,
): BeginCreateCredentialResponse? {
@@ -167,60 +230,6 @@ class Fido2ProviderProcessorImpl(
return entryBuilder.build()
}
override fun processGetCredentialRequest(
request: BeginGetCredentialRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
) {
// If the user is not logged in, return an error.
val userState = authRepository.userStateFlow.value
if (userState == null) {
callback.onError(GetCredentialUnknownException("Active user is required."))
return
}
// Return an unlock action if the current account is locked.
if (!userState.activeAccount.isVaultUnlocked) {
val authenticationAction = AuthenticationAction(
title = context.getString(R.string.unlock),
pendingIntent = intentManager.createFido2UnlockPendingIntent(
action = UNLOCK_ACCOUNT_INTENT,
userId = userState.activeUserId,
requestCode = requestCode.getAndIncrement(),
),
)
callback.onResult(
BeginGetCredentialResponse(
authenticationActions = listOf(authenticationAction),
),
)
return
}
// Otherwise, find all matching credentials from the current vault.
val getCredentialJob = scope.launch {
try {
val credentialEntries = getMatchingFido2CredentialEntries(
userId = userState.activeUserId,
request = request,
)
callback.onResult(
BeginGetCredentialResponse(
credentialEntries = credentialEntries,
),
)
} catch (e: GetCredentialException) {
callback.onError(e)
}
}
cancellationSignal.setOnCancelListener {
callback.onError(GetCredentialCancellationException())
getCredentialJob.cancel()
}
}
@Throws(GetCredentialUnsupportedException::class)
private suspend fun getMatchingFido2CredentialEntries(
userId: String,
@@ -346,13 +355,4 @@ class Fido2ProviderProcessorImpl(
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.setCryptoObject(BiometricPrompt.CryptoObject(cipher))
.build()
override fun processClearCredentialStateRequest(
request: ProviderClearCredentialStateRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<Void?, ClearCredentialException>,
) {
// no-op: RFU
callback.onError(ClearCredentialUnsupportedException())
}
}
@@ -118,15 +118,15 @@ fun VaultUnlockScreen(
)
}
VaultUnlockEvent.Fido2CredentialAssertionError -> {
is VaultUnlockEvent.Fido2CredentialAssertionError -> {
fido2CompletionManager.completeFido2Assertion(
result = Fido2CredentialAssertionResult.Error,
result = Fido2CredentialAssertionResult.Error(event.message),
)
}
VaultUnlockEvent.Fido2GetCredentialsError -> {
is VaultUnlockEvent.Fido2GetCredentialsError -> {
fido2CompletionManager.completeFido2GetCredentialRequest(
result = Fido2GetCredentialsResult.Error,
result = Fido2GetCredentialsResult.Error(message = event.message),
)
}
}
@@ -177,11 +177,21 @@ class VaultUnlockViewModel @Inject constructor(
mutableStateFlow.update { it.copy(dialog = null) }
when {
state.fido2GetCredentialsRequest != null -> {
sendEvent(VaultUnlockEvent.Fido2GetCredentialsError)
sendEvent(
VaultUnlockEvent.Fido2GetCredentialsError(
R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
),
)
}
state.fido2CredentialAssertionRequest != null -> {
sendEvent(VaultUnlockEvent.Fido2CredentialAssertionError)
sendEvent(
VaultUnlockEvent.Fido2CredentialAssertionError(
R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
),
)
}
else -> Unit
@@ -531,12 +541,12 @@ sealed class VaultUnlockEvent {
/**
* Completes the FIDO2 get credentials request with an error response.
*/
data object Fido2GetCredentialsError : VaultUnlockEvent()
data class Fido2GetCredentialsError(val message: Text) : VaultUnlockEvent()
/**
* Completes the FIDO2 credential assertion request with an error response.
*/
data object Fido2CredentialAssertionError : VaultUnlockEvent()
data class Fido2CredentialAssertionError(val message: Text) : VaultUnlockEvent()
}
/**
@@ -49,7 +49,7 @@ class Fido2CompletionManagerImpl(
.setCreateCredentialResponse(
intent = intent,
response = CreatePublicKeyCredentialResponse(
registrationResponseJson = result.registrationResponse,
registrationResponseJson = result.responseJson,
),
)
}
@@ -71,7 +71,7 @@ class Fido2CompletionManagerImpl(
activity.also {
val intent = Intent()
when (result) {
Fido2CredentialAssertionResult.Error -> {
is Fido2CredentialAssertionResult.Error -> {
PendingIntentHandler
.setGetCredentialException(
intent = intent,
@@ -139,11 +139,12 @@ class Fido2CompletionManagerImpl(
)
}
Fido2GetCredentialsResult.Error,
-> {
is Fido2GetCredentialsResult.Error -> {
PendingIntentHandler.setGetCredentialException(
resultIntent,
GetCredentialUnknownException(),
GetCredentialUnknownException(
errorMessage = result.message.toString(activity.resources),
),
)
}
}
@@ -84,7 +84,7 @@ fun AboutScreen(
AboutEvent.NavigateToRateApp -> {
intentManager.launchUri(
uri =
"https://play.google.com/store/apps/details?id=com.x8bit.bitwarden".toUri(),
"https://play.google.com/store/apps/details?id=com.x8bit.bitwarden".toUri(),
)
}
}
@@ -25,6 +25,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.autofill.fido2.manager.Fido2CompletionManager
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon
import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem
@@ -137,7 +138,9 @@ fun VaultAddEditScreen(
}
is VaultAddEditEvent.CompleteFido2Registration -> {
fido2CompletionManager.completeFido2Registration(event.result)
fido2CompletionManager.completeFido2Registration(
result = event.result,
)
}
is VaultAddEditEvent.Fido2UserVerification -> {
@@ -193,7 +196,11 @@ fun VaultAddEditScreen(
{ viewModel.trySendAction(VaultAddEditAction.Common.InitialAutofillDialogDismissed) }
},
onFido2ErrorDismiss = remember(viewModel) {
{ viewModel.trySendAction(VaultAddEditAction.Common.Fido2ErrorDialogDismissed) }
{ errorMessage ->
viewModel.trySendAction(
VaultAddEditAction.Common.Fido2ErrorDialogDismissed(message = errorMessage),
)
}
},
onConfirmOverwriteExistingPasskey = remember(viewModel) {
{
@@ -420,7 +427,7 @@ private fun VaultAddEditItemDialogs(
dialogState: VaultAddEditState.DialogState?,
onDismissRequest: () -> Unit,
onAutofillDismissRequest: () -> Unit,
onFido2ErrorDismiss: () -> Unit,
onFido2ErrorDismiss: (Text) -> Unit,
onConfirmOverwriteExistingPasskey: () -> Unit,
onSubmitMasterPasswordFido2Verification: (password: String) -> Unit,
onRetryFido2PasswordVerification: () -> Unit,
@@ -455,7 +462,7 @@ private fun VaultAddEditItemDialogs(
BitwardenBasicDialog(
title = stringResource(id = R.string.an_error_has_occurred),
message = dialogState.message(),
onDismissRequest = onFido2ErrorDismiss,
onDismissRequest = { onFido2ErrorDismiss(dialogState.message) },
)
}
@@ -306,8 +306,8 @@ class VaultAddEditViewModel @Inject constructor(
handleUserVerificationCancelled()
}
VaultAddEditAction.Common.Fido2ErrorDialogDismissed -> {
handleFido2ErrorDialogDismissed()
is VaultAddEditAction.Common.Fido2ErrorDialogDismissed -> {
handleFido2ErrorDialogDismissed(action)
}
VaultAddEditAction.Common.UserVerificationNotSupported -> {
@@ -645,12 +645,16 @@ class VaultAddEditViewModel @Inject constructor(
showFido2ErrorDialog()
}
private fun handleFido2ErrorDialogDismissed() {
private fun handleFido2ErrorDialogDismissed(
action: VaultAddEditAction.Common.Fido2ErrorDialogDismissed,
) {
fido2CredentialManager.isUserVerified = false
clearDialogState()
sendEvent(
VaultAddEditEvent.CompleteFido2Registration(
result = Fido2RegisterCredentialResult.Error,
result = Fido2RegisterCredentialResult.Error(
message = action.message,
),
),
)
}
@@ -2837,7 +2841,7 @@ sealed class VaultAddEditAction {
/**
* The user has dismissed the FIDO 2 credential error dialog.
*/
data object Fido2ErrorDialogDismissed : Common()
data class Fido2ErrorDialogDismissed(val message: Text) : Common()
/**
* User verification cannot be performed with device biometrics or credentials.
@@ -23,6 +23,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.autofill.fido2.manager.Fido2CompletionManager
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.components.account.BitwardenAccountActionItem
import com.x8bit.bitwarden.ui.platform.components.account.BitwardenAccountSwitcher
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
@@ -195,9 +196,9 @@ fun VaultItemListingScreen(
{ viewModel.trySendAction(VaultItemListingsAction.DismissDialogClick) }
},
onDismissFido2ErrorDialog = remember(viewModel) {
{
{ errorMessage ->
viewModel.trySendAction(
VaultItemListingsAction.DismissFido2ErrorDialogClick,
VaultItemListingsAction.DismissFido2ErrorDialogClick(message = errorMessage),
)
}
},
@@ -296,7 +297,7 @@ fun VaultItemListingScreen(
private fun VaultItemListingDialogs(
dialogState: VaultItemListingState.DialogState?,
onDismissRequest: () -> Unit,
onDismissFido2ErrorDialog: () -> Unit,
onDismissFido2ErrorDialog: (Text) -> Unit,
onConfirmOverwriteExistingPasskey: (cipherViewId: String) -> Unit,
onSubmitMasterPasswordFido2Verification: (password: String, cipherId: String) -> Unit,
onRetryFido2PasswordVerification: (cipherId: String) -> Unit,
@@ -321,7 +322,7 @@ private fun VaultItemListingDialogs(
is VaultItemListingState.DialogState.Fido2OperationFail -> BitwardenBasicDialog(
title = dialogState.title(),
message = dialogState.message(),
onDismissRequest = onDismissFido2ErrorDialog,
onDismissRequest = { onDismissFido2ErrorDialog(dialogState.message) },
)
is VaultItemListingState.DialogState.OverwritePasskeyConfirmationPrompt -> {
@@ -202,7 +202,7 @@ class VaultItemListingViewModel @Inject constructor(
is VaultItemListingsAction.SwitchAccountClick -> handleSwitchAccountClick(action)
is VaultItemListingsAction.DismissDialogClick -> handleDismissDialogClick()
is VaultItemListingsAction.DismissFido2ErrorDialogClick -> {
handleDismissFido2ErrorDialogClick()
handleDismissFido2ErrorDialogClick(action)
}
is VaultItemListingsAction.MasterPasswordFido2VerificationSubmit -> {
@@ -316,14 +316,19 @@ class VaultItemListingViewModel @Inject constructor(
getCipherViewOrNull(action.cipherViewId)
?.let { registerFido2Credential(it) }
?: run {
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_the_selected_item_does_not_exist
.asText(),
)
return
}
}
private fun handleUserVerificationLockOut() {
fido2CredentialManager.isUserVerified = false
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_user_is_locked_out.asText(),
)
}
private fun handleUserVerificationSuccess(
@@ -335,7 +340,9 @@ class VaultItemListingViewModel @Inject constructor(
private fun handleUserVerificationFail() {
fido2CredentialManager.isUserVerified = false
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
)
}
private fun handleUserVerificationCancelled() {
@@ -356,7 +363,9 @@ class VaultItemListingViewModel @Inject constructor(
val selectedCipherId = action
.selectedCipherId
?: run {
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
)
return
}
@@ -365,7 +374,9 @@ class VaultItemListingViewModel @Inject constructor(
.value
?.activeAccount
?: run {
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
)
return
}
@@ -488,7 +499,9 @@ class VaultItemListingViewModel @Inject constructor(
}
private fun handleDismissFido2VerificationDialogClick() {
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_user_verification_was_cancelled.asText(),
)
}
private fun handleCopySendUrlClick(action: ListingItemOverflowAction.SendAction.CopyUrlClick) {
@@ -649,7 +662,10 @@ class VaultItemListingViewModel @Inject constructor(
private fun handleFido2RegistrationRequestReceive(action: VaultItemListingsAction.ItemClick) {
val cipherView = getCipherViewOrNull(action.id)
?: run {
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_the_selected_item_does_not_exist
.asText(),
)
return
}
@@ -673,7 +689,9 @@ class VaultItemListingViewModel @Inject constructor(
// This scenario should not occur because `isFido2Creation` is false when
// `fido2CredentialRequest` is null. We show the FIDO 2 error dialog to inform
// the user and terminate the flow just in case it does occur.
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
)
return
}
mutableStateFlow.update {
@@ -702,7 +720,9 @@ class VaultItemListingViewModel @Inject constructor(
val attestationOptions = fido2CredentialManager
.getPasskeyAttestationOptionsOrNull(credentialRequest.requestJson)
?: run {
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
)
return
}
when (attestationOptions.authenticatorSelection.userVerification) {
@@ -739,7 +759,9 @@ class VaultItemListingViewModel @Inject constructor(
) {
val activeUserId = authRepository.activeUserId
?: run {
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
)
return
}
viewModelScope.launch {
@@ -762,7 +784,9 @@ class VaultItemListingViewModel @Inject constructor(
) {
val activeUserId = authRepository.activeUserId
?: run {
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
)
return
}
viewModelScope.launch {
@@ -881,13 +905,17 @@ class VaultItemListingViewModel @Inject constructor(
clearDialogState()
}
private fun handleDismissFido2ErrorDialogClick() {
private fun handleDismissFido2ErrorDialogClick(
action: VaultItemListingsAction.DismissFido2ErrorDialogClick,
) {
clearDialogState()
when {
state.fido2CreateCredentialRequest != null -> {
sendEvent(
VaultItemListingEvent.CompleteFido2Registration(
result = Fido2RegisterCredentialResult.Error,
result = Fido2RegisterCredentialResult.Error(
action.message,
),
),
)
}
@@ -895,7 +923,19 @@ class VaultItemListingViewModel @Inject constructor(
state.fido2CredentialAssertionRequest != null -> {
sendEvent(
VaultItemListingEvent.CompleteFido2Assertion(
result = Fido2CredentialAssertionResult.Error,
result = Fido2CredentialAssertionResult.Error(
message = action.message,
),
),
)
}
state.fido2GetCredentialsRequest != null -> {
sendEvent(
VaultItemListingEvent.CompleteFido2GetCredentialsRequest(
result = Fido2GetCredentialsResult.Error(
message = action.message,
),
),
)
}
@@ -905,7 +945,7 @@ class VaultItemListingViewModel @Inject constructor(
it.copy(
dialogState = VaultItemListingState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.generic_error_message.asText(),
message = action.message,
),
)
}
@@ -1242,7 +1282,7 @@ class VaultItemListingViewModel @Inject constructor(
when (action.result) {
ValidatePasswordResult.Error -> {
showFido2ErrorDialog()
showFido2UserVerificationErrorDialog()
}
is ValidatePasswordResult.Success -> {
@@ -1270,7 +1310,7 @@ class VaultItemListingViewModel @Inject constructor(
when (action.result) {
ValidatePinResult.Error -> {
showFido2ErrorDialog()
showFido2UserVerificationErrorDialog()
}
is ValidatePinResult.Success -> {
@@ -1300,7 +1340,10 @@ class VaultItemListingViewModel @Inject constructor(
it.copy(dialogState = errorDialogState)
}
} else {
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_user_verification_attempts_exceeded
.asText(),
)
}
}
@@ -1310,7 +1353,10 @@ class VaultItemListingViewModel @Inject constructor(
val cipherView = getCipherViewOrNull(cipherId = selectedCipherId)
?: run {
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_the_selected_item_does_not_exist
.asText(),
)
return
}
@@ -1342,7 +1388,11 @@ class VaultItemListingViewModel @Inject constructor(
)
}
}
?: showFido2ErrorDialog()
?: run {
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
)
}
}
//endregion VaultItemListing Handlers
@@ -1372,7 +1422,10 @@ class VaultItemListingViewModel @Inject constructor(
)
?.relyingPartyId
?: run {
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_the_request_is_invalid
.asText(),
)
return
}
sendEvent(
@@ -1446,7 +1499,9 @@ class VaultItemListingViewModel @Inject constructor(
val options = fido2CredentialManager
.getPasskeyAttestationOptionsOrNull(requestJson = action.request.requestJson)
?: run {
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
)
return@launch
}
val validateOriginResult = fido2OriginManager
@@ -1487,40 +1542,11 @@ class VaultItemListingViewModel @Inject constructor(
}
private fun handleFido2OriginValidationFail(error: Fido2ValidateOriginResult.Error) {
val messageResId = when (error) {
Fido2ValidateOriginResult.Error.ApplicationNotFound -> {
R.string.passkey_operation_failed_because_app_not_found_in_asset_links
}
Fido2ValidateOriginResult.Error.ApplicationFingerprintNotVerified -> {
R.string.passkey_operation_failed_because_app_could_not_be_verified
}
Fido2ValidateOriginResult.Error.AssetLinkNotFound -> {
R.string.passkey_operation_failed_because_of_missing_asset_links
}
Fido2ValidateOriginResult.Error.PrivilegedAppNotAllowed -> {
R.string.passkey_operation_failed_because_browser_is_not_privileged
}
Fido2ValidateOriginResult.Error.PasskeyNotSupportedForApp -> {
R.string.passkeys_not_supported_for_this_app
}
Fido2ValidateOriginResult.Error.PrivilegedAppSignatureNotFound -> {
R.string.passkey_operation_failed_because_browser_signature_does_not_match
}
Fido2ValidateOriginResult.Error.Unknown -> {
R.string.generic_error_message
}
}
mutableStateFlow.update {
it.copy(
dialogState = VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = messageResId.asText(),
message = error.messageResId.asText(),
),
)
}
@@ -1544,12 +1570,17 @@ class VaultItemListingViewModel @Inject constructor(
.orEmpty()
.filter { it.isActiveWithFido2Credentials }
if (request.cipherId.isNullOrEmpty()) {
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_no_item_was_selected.asText(),
)
} else {
val selectedCipher = ciphers
.find { it.id == request.cipherId }
?: run {
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_the_selected_item_does_not_exist
.asText(),
)
return
}
@@ -1580,13 +1611,17 @@ class VaultItemListingViewModel @Inject constructor(
val assertionOptions = fido2CredentialManager
.getPasskeyAssertionOptionsOrNull(request.requestJson)
?: run {
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
)
return
}
val relyingPartyId = assertionOptions.relyingPartyId
?: run {
showFido2ErrorDialog()
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
)
return
}
@@ -1773,14 +1808,19 @@ class VaultItemListingViewModel @Inject constructor(
as? DecryptFido2CredentialAutofillViewResult.Success)
?.fido2CredentialAutofillViews
private fun showFido2ErrorDialog() {
private fun showFido2UserVerificationErrorDialog() {
showFido2ErrorDialog(
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
)
}
private fun showFido2ErrorDialog(message: Text) {
fido2CredentialManager.authenticationAttempts = 0
mutableStateFlow.update {
it.copy(
dialogState = VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
message = message,
),
)
}
@@ -2393,7 +2433,9 @@ sealed class VaultItemListingsAction {
/**
* Click to dismiss the FIDO 2 creation error dialog.
*/
data object DismissFido2ErrorDialogClick : VaultItemListingsAction()
data class DismissFido2ErrorDialogClick(
val message: Text,
) : VaultItemListingsAction()
/**
* Click to submit the master password for FIDO 2 verification.
+13 -1
View File
@@ -1171,7 +1171,6 @@ Do you want to switch to this account?</string>
<string name="coachmark_6_of_6">6 OF 6</string>
<string name="use_these_options_to_adjust_your_password_to_your_account_requirements">Use these options to adjust your password to meet your account website\'s requirements.</string>
<string name="after_you_save_your_new_password_to_bitwarden_don_t_forget_to_update_it_on_your_account_website">"After you save your new password to Bitwarden, dont forget to update it on your account website. "</string>
<string name="link">Link</string>
<string name="error_loading_certificate">Error loading certificate</string>
<string name="certificate_alias">Certificate alias</string>
<string name="import_certificate">Import certificate</string>
@@ -1184,4 +1183,17 @@ Do you want to switch to this account?</string>
<string name="invalid_certificate_chain">Invalid certificate chain</string>
<string name="system_certificates_are_not_as_secure_as_importing_certificates_to_bitwarden">Using a system certificate is less secure than storing the certificate with Bitwarden. Continuing will display a list of available system certificates if one is already installed.</string>
<string name="certificate_alias_already_exists">Certificate alias already exists</string>
<string name="link">Link</string>
<string name="passkey_operation_failed_because_app_is_signed_incorrectly">Passkey operation failed because app is signed incorrectly.</string>
<string name="passkey_operation_failed_because_host_url_is_not_present_in_request">Passkey operation failed because host URL is not present in request.</string>
<string name="passkey_operation_failed_because_app_signature_is_invalid">Passkey operation failed because app signature is invalid.</string>
<string name="passkey_registration_failed_due_to_an_internal_error">Passkey registration failed due to an internal error.</string>
<string name="passkey_operation_failed_because_relying_party_cannot_be_identified">Passkey operation failed because relying party cannot be identified.</string>
<string name="passkey_authentication_failed_due_to_an_internal_error">Passkey authentication failed due to an internal error.</string>
<string name="passkey_operation_failed_because_the_selected_item_does_not_exist">Passkey operation failed because the selected item does not exist.</string>
<string name="passkey_operation_failed_because_user_is_locked_out">Passkey operation failed because user is locked out.</string>
<string name="passkey_operation_failed_because_user_verification_was_cancelled">Passkey operation failed because user verification was cancelled.</string>
<string name="passkey_operation_failed_because_the_request_is_invalid">Passkey operation failed because the request is invalid.</string>
<string name="passkey_operation_failed_because_user_verification_attempts_exceeded">Passkey operation failed because user verification attempts exceeded.</string>
<string name="passkey_operation_failed_because_no_item_was_selected">Passkey operation failed because no item was selected.</string>
</resources>
@@ -8,6 +8,7 @@ import com.bitwarden.fido.Origin
import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAssertionResponse
import com.bitwarden.fido.UnverifiedAssetLink
import com.bitwarden.sdk.Fido2CredentialStore
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2AttestationResponse
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
@@ -26,6 +27,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockPublicKeyAssertionResponse
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockPublicKeyAttestationResponse
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAssertionOptions
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAttestationOptions
import io.mockk.coEvery
@@ -340,7 +342,10 @@ class Fido2CredentialManagerTest {
)
assertEquals(
Fido2RegisterCredentialResult.Error,
Fido2RegisterCredentialResult.Error(
R.string.passkey_operation_failed_because_app_is_signed_incorrectly
.asText(),
),
result,
)
}
@@ -364,32 +369,10 @@ class Fido2CredentialManagerTest {
)
assertEquals(
Fido2RegisterCredentialResult.Error,
result,
)
}
@Test
fun `registerFido2Credential should return Error when toHostOrPathOrNull is null`() =
runTest {
val mockSigningInfo = mockk<SigningInfo> {
every { apkContentsSigners } returns arrayOf(Signature(DEFAULT_APP_SIGNATURE))
every { hasMultipleSigners() } returns false
}
val mockFido2CredentialRequest = createMockFido2CreateCredentialRequest(
number = 1,
origin = "illegal empty spaces",
signingInfo = mockSigningInfo,
)
val result = fido2CredentialManager.registerFido2Credential(
userId = "mockUserId",
fido2CreateCredentialRequest = mockFido2CredentialRequest,
selectedCipherView = createMockCipherView(number = 1),
)
assertEquals(
Fido2RegisterCredentialResult.Error,
Fido2RegisterCredentialResult.Error(
R.string.passkey_operation_failed_because_app_signature_is_invalid
.asText(),
),
result,
)
}
@@ -432,7 +415,10 @@ class Fido2CredentialManagerTest {
)
assertEquals(
Fido2RegisterCredentialResult.Error,
Fido2RegisterCredentialResult.Error(
R.string.passkey_registration_failed_due_to_an_internal_error
.asText(),
),
result,
)
}
@@ -465,7 +451,10 @@ class Fido2CredentialManagerTest {
}
assertEquals(
Fido2RegisterCredentialResult.Error,
Fido2RegisterCredentialResult.Error(
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
.asText(),
),
result,
)
}
@@ -518,7 +507,7 @@ class Fido2CredentialManagerTest {
assertEquals(
AuthenticateFido2CredentialRequest(
userId = "activeUserId",
origin = DEFAULT_ORIGIN,
origin = DEFAULT_WEB_ORIGIN,
requestJson = """{"publicKey": ${mockRequest.requestJson}}""",
clientData = ClientData.DefaultWithCustomHash(mockRequest.clientDataHash!!),
selectedCipherView = mockCipherView,
@@ -568,14 +557,7 @@ class Fido2CredentialManagerTest {
assertEquals(
AuthenticateFido2CredentialRequest(
userId = "activeUserId",
origin = Origin.Android(
UnverifiedAssetLink(
packageName = DEFAULT_PACKAGE_NAME,
sha256CertFingerprint = DEFAULT_CERT_FINGERPRINT,
host = DEFAULT_HOST,
assetLinkUrl = mockRequest.origin!!,
),
),
origin = DEFAULT_WEB_ORIGIN,
requestJson = """{"publicKey": ${mockRequest.requestJson}}""",
clientData = ClientData.DefaultWithExtraData(
androidPackageName = "android:apk-key-hash:$DEFAULT_APP_SIGNATURE",
@@ -626,7 +608,7 @@ class Fido2CredentialManagerTest {
UnverifiedAssetLink(
DEFAULT_PACKAGE_NAME,
DEFAULT_CERT_FINGERPRINT,
mockAssertionOptions.relyingPartyId!!,
"https://${mockAssertionOptions.relyingPartyId!!}",
"https://${mockAssertionOptions.relyingPartyId}",
),
),
@@ -664,7 +646,10 @@ class Fido2CredentialManagerTest {
}
assertEquals(
Fido2CredentialAssertionResult.Error,
Fido2CredentialAssertionResult.Error(
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
.asText(),
),
result,
)
}
@@ -749,7 +734,10 @@ class Fido2CredentialManagerTest {
}
assertEquals(
Fido2CredentialAssertionResult.Error,
Fido2CredentialAssertionResult.Error(
R.string.passkey_authentication_failed_due_to_an_internal_error
.asText(),
),
authResult,
)
}
@@ -779,7 +767,10 @@ class Fido2CredentialManagerTest {
}
assertEquals(
Fido2CredentialAssertionResult.Error,
Fido2CredentialAssertionResult.Error(
R.string.passkey_operation_failed_because_relying_party_cannot_be_identified
.asText(),
),
result,
)
}
@@ -803,7 +794,7 @@ class Fido2CredentialManagerTest {
)
assertEquals(
Fido2CredentialAssertionResult.Error,
Fido2CredentialAssertionResult.Error(R.string.generic_error_message.asText()),
result,
)
@@ -817,7 +808,7 @@ private const val DEFAULT_PACKAGE_NAME = "com.x8bit.bitwarden"
private const val DEFAULT_APP_SIGNATURE = "0987654321ABCDEF"
private const val DEFAULT_CERT_FINGERPRINT = "30:39:38:37:36:35:34:33:32:31:41:42:43:44:45:46"
private const val DEFAULT_HOST = "bitwarden.com"
private val DEFAULT_ORIGIN = Origin.Android(
private val DEFAULT_ANDROID_ORIGIN = Origin.Android(
UnverifiedAssetLink(
packageName = DEFAULT_PACKAGE_NAME,
sha256CertFingerprint = DEFAULT_CERT_FINGERPRINT,
@@ -825,6 +816,7 @@ private val DEFAULT_ORIGIN = Origin.Android(
assetLinkUrl = "bitwarden.com",
),
)
private val DEFAULT_WEB_ORIGIN = Origin.Web("bitwarden.com")
private const val DEFAULT_ALLOW_LIST = """
{
"apps": [
@@ -22,6 +22,7 @@ import androidx.credentials.provider.BeginGetCredentialRequest
import androidx.credentials.provider.BeginGetCredentialResponse
import androidx.credentials.provider.BeginGetPasswordOption
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
import androidx.credentials.provider.CallingAppInfo
import androidx.credentials.provider.PublicKeyCredentialEntry
import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.vault.CipherView
@@ -132,7 +133,14 @@ class Fido2ProviderProcessorTest {
@Test
fun `processCreateCredentialRequest should invoke callback with error when user id is null`() {
val request: BeginCreateCredentialRequest = mockk()
val request: BeginCreateCredentialRequest = mockk {
every { callingAppInfo } returns CallingAppInfo(
packageName = "com.x8bit.bitwarden.dev",
signingInfo = mockk(),
origin = null,
)
every { candidateQueryData } returns Bundle()
}
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
mockk()
val captureSlot = slot<CreateCredentialException>()
@@ -153,7 +161,14 @@ class Fido2ProviderProcessorTest {
@Suppress("MaxLineLength")
@Test
fun `processCreateCredentialRequest should invoke callback with error on password create request`() {
val request: BeginCreatePasswordCredentialRequest = mockk()
val request: BeginCreatePasswordCredentialRequest = mockk {
every { callingAppInfo } returns CallingAppInfo(
packageName = "com.x8bit.bitwarden.dev",
signingInfo = mockk(),
origin = null,
)
every { candidateQueryData } returns Bundle()
}
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
mockk()
val captureSlot = slot<CreateCredentialException>()
@@ -171,7 +186,13 @@ class Fido2ProviderProcessorTest {
@Suppress("MaxLineLength")
@Test
fun `processCreateCredentialRequest should invoke callback with error when json is null or empty`() {
val request: BeginCreatePublicKeyCredentialRequest = mockk()
val request: BeginCreatePublicKeyCredentialRequest = mockk {
every { callingAppInfo } returns CallingAppInfo(
packageName = "com.x8bit.bitwarden.dev",
signingInfo = mockk(),
origin = null,
)
}
val candidateQueryData: Bundle = mockk()
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
mockk()
@@ -193,7 +214,13 @@ class Fido2ProviderProcessorTest {
@Test
fun `processCreateCredentialRequest should invoke callback with error when user state null`() {
val request: BeginCreatePublicKeyCredentialRequest = mockk()
val request: BeginCreatePublicKeyCredentialRequest = mockk {
every { callingAppInfo } returns CallingAppInfo(
packageName = "com.x8bit.bitwarden.dev",
signingInfo = mockk(),
origin = null,
)
}
val candidateQueryData: Bundle = mockk()
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
mockk()
@@ -216,7 +243,13 @@ class Fido2ProviderProcessorTest {
@Suppress("MaxLineLength")
@Test
fun `processCreateCredentialRequest should invoke callback with result when user state is valid`() {
val request: BeginCreatePublicKeyCredentialRequest = mockk()
val request: BeginCreatePublicKeyCredentialRequest = mockk {
every { callingAppInfo } returns CallingAppInfo(
packageName = "com.x8bit.bitwarden.dev",
signingInfo = mockk(),
origin = null,
)
}
val candidateQueryData: Bundle = mockk()
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
mockk()
@@ -252,7 +285,13 @@ class Fido2ProviderProcessorTest {
@Suppress("MaxLineLength")
@Test
fun `processCreateCredentialRequest should generate correct entries based on state`() {
val request: BeginCreatePublicKeyCredentialRequest = mockk()
val request: BeginCreatePublicKeyCredentialRequest = mockk {
every { callingAppInfo } returns CallingAppInfo(
packageName = "com.x8bit.bitwarden.dev",
signingInfo = mockk(),
origin = null,
)
}
val candidateQueryData: Bundle = mockk()
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
mockk()
@@ -335,7 +374,20 @@ class Fido2ProviderProcessorTest {
@Test
fun `processGetCredentialRequest should invoke callback with error when user state is null`() {
val request: BeginGetCredentialRequest = mockk()
val request: BeginGetCredentialRequest = mockk {
every { callingAppInfo } returns CallingAppInfo(
packageName = "com.x8bit.bitwarden.dev",
signingInfo = mockk(),
origin = null,
)
every {
beginGetCredentialOptions
} returns listOf(
mockk<BeginGetPasswordOption> {
every { candidateQueryData } returns Bundle()
},
)
}
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
val captureSlot = slot<GetCredentialException>()
every { cancellationSignal.setOnCancelListener(any()) } just runs
@@ -354,7 +406,18 @@ class Fido2ProviderProcessorTest {
@Suppress("MaxLineLength")
@Test
fun `processGetCredentialRequest should invoke callback with authentication action when vault is locked`() {
val request: BeginGetCredentialRequest = mockk()
val request: BeginGetCredentialRequest = mockk {
every { beginGetCredentialOptions } returns listOf(
mockk<BeginGetPasswordOption> {
every { candidateQueryData } returns Bundle()
},
)
every { callingAppInfo } returns CallingAppInfo(
packageName = "com.x8bit.bitwarden.dev",
signingInfo = mockk(),
origin = null,
)
}
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
val captureSlot = slot<BeginGetCredentialResponse>()
val mockIntent: PendingIntent = mockk()
@@ -409,7 +472,16 @@ class Fido2ProviderProcessorTest {
@Test
fun `processGetCredentialRequest should invoke callback with error when option is not BeginGetPublicKeyCredentialOption`() {
val request: BeginGetCredentialRequest = mockk {
every { beginGetCredentialOptions } returns listOf(mockk<BeginGetPasswordOption>())
every { beginGetCredentialOptions } returns listOf(
mockk<BeginGetPasswordOption> {
every { candidateQueryData } returns Bundle()
},
)
every { callingAppInfo } returns CallingAppInfo(
packageName = "com.x8bit.bitwarden.dev",
signingInfo = mockk(),
origin = null,
)
}
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
val captureSlot = slot<GetCredentialException>()
@@ -436,6 +508,11 @@ class Fido2ProviderProcessorTest {
)
val request: BeginGetCredentialRequest = mockk {
every { beginGetCredentialOptions } returns listOf(mockOption)
every { callingAppInfo } returns CallingAppInfo(
packageName = "com.x8bit.bitwarden.dev",
signingInfo = mockk(),
origin = null,
)
}
every {
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(any())
@@ -465,6 +542,11 @@ class Fido2ProviderProcessorTest {
)
val request: BeginGetCredentialRequest = mockk {
every { beginGetCredentialOptions } returns listOf(mockOption)
every { callingAppInfo } returns CallingAppInfo(
packageName = "com.x8bit.bitwarden.dev",
signingInfo = mockk(),
origin = null,
)
}
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
val captureSlot = slot<GetCredentialException>()
@@ -511,6 +593,11 @@ class Fido2ProviderProcessorTest {
)
val request: BeginGetCredentialRequest = mockk {
every { beginGetCredentialOptions } returns listOf(mockOption)
every { callingAppInfo } returns CallingAppInfo(
packageName = "com.x8bit.bitwarden.dev",
signingInfo = mockk(),
origin = null,
)
}
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
val captureSlot = slot<BeginGetCredentialResponse>()
@@ -18,12 +18,14 @@ import androidx.compose.ui.test.performKeyPress
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.requestFocus
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.autofill.fido2.manager.Fido2CompletionManager
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed
@@ -125,10 +127,16 @@ class VaultUnlockScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `on Fido2GetCredentialsError should call completeFido2GetCredentialRequest on fido2CompletionManager`() {
mutableEventFlow.tryEmit(VaultUnlockEvent.Fido2GetCredentialsError)
mutableEventFlow.tryEmit(
VaultUnlockEvent.Fido2GetCredentialsError(
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
),
)
verify(exactly = 1) {
fido2CompletionManager.completeFido2GetCredentialRequest(
result = Fido2GetCredentialsResult.Error,
result = Fido2GetCredentialsResult.Error(
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
),
)
}
}
@@ -136,10 +144,10 @@ class VaultUnlockScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `on Fido2AssertCredentialError should call completeFido2AssertCredential on fido2CompletionManager`() {
mutableEventFlow.tryEmit(VaultUnlockEvent.Fido2CredentialAssertionError)
mutableEventFlow.tryEmit(VaultUnlockEvent.Fido2CredentialAssertionError("".asText()))
verify(exactly = 1) {
fido2CompletionManager.completeFido2Assertion(
result = Fido2CredentialAssertionResult.Error,
result = Fido2CredentialAssertionResult.Error("".asText()),
)
}
}
@@ -570,7 +570,12 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel(state = initialState)
viewModel.trySendAction(VaultUnlockAction.DismissDialog)
viewModel.eventFlow.test {
assertEquals(VaultUnlockEvent.Fido2GetCredentialsError, awaitItem())
assertEquals(
VaultUnlockEvent.Fido2GetCredentialsError(
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
),
awaitItem(),
)
}
}
@@ -586,7 +591,13 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel(state = initialState)
viewModel.trySendAction(VaultUnlockAction.DismissDialog)
viewModel.eventFlow.test {
assertEquals(VaultUnlockEvent.Fido2CredentialAssertionError, awaitItem())
assertEquals(
VaultUnlockEvent.Fido2CredentialAssertionError(
R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
),
awaitItem(),
)
}
}
@@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.processor.GET_PASSKEY_INTENT
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2CredentialAutofillView
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import io.mockk.Called
import io.mockk.MockKVerificationScope
@@ -40,6 +41,7 @@ class Fido2CompletionManagerTest {
private val mockActivity = mockk<Activity> {
every { packageName } returns "packageName"
every { resources } returns mockk(relaxed = true)
every { setResult(Activity.RESULT_OK, any()) } just runs
every { finish() } just runs
}
@@ -116,7 +118,7 @@ class Fido2CompletionManagerTest {
fido2CompletionManager
.completeFido2Registration(
Fido2RegisterCredentialResult.Success(
registrationResponse = "registrationResponse",
responseJson = "registrationResponse",
),
)
@@ -129,7 +131,7 @@ class Fido2CompletionManagerTest {
@Test
fun `completeFido2Registration should set CreateCredentialException, set activity result, then finish activity when result is Error`() {
fido2CompletionManager
.completeFido2Registration(Fido2RegisterCredentialResult.Error)
.completeFido2Registration(Fido2RegisterCredentialResult.Error("".asText()))
verifyActivityResultIsSetAndFinishedAfter {
PendingIntentHandler.setCreateCredentialException(any(), any())
@@ -162,7 +164,7 @@ class Fido2CompletionManagerTest {
@Test
fun `completeFido2Assertion should set GetCredentialException, set activity result, then finish activity when result is Error`() {
fido2CompletionManager
.completeFido2Assertion(Fido2CredentialAssertionResult.Error)
.completeFido2Assertion(Fido2CredentialAssertionResult.Error("".asText()))
verifyActivityResultIsSetAndFinishedAfter {
PendingIntentHandler.setGetCredentialException(any(), any())
@@ -291,8 +293,13 @@ class Fido2CompletionManagerTest {
@Test
fun `completeFido2GetCredentials should set GetCredentialException, set activity result, then finish activity when result is Error`() {
fido2CompletionManager
.completeFido2GetCredentialRequest(Fido2GetCredentialsResult.Error)
.completeFido2GetCredentialRequest(
Fido2GetCredentialsResult.Error(
"".asText(),
),
)
verifyActivityResultIsSetAndFinishedAfter {
mockActivity.resources
PendingIntentHandler.setGetCredentialException(any(), any())
}
}
@@ -217,7 +217,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
@Test
fun `on CompleteFido2Create event should invoke Fido2CompletionManager`() {
val result = Fido2RegisterCredentialResult.Success(
registrationResponse = "mockRegistrationResponse",
responseJson = "mockRegistrationResponse",
)
mutableEventFlow.tryEmit(VaultAddEditEvent.CompleteFido2Registration(result = result))
verify { fido2CompletionManager.completeFido2Registration(result) }
@@ -454,7 +454,11 @@ class VaultAddEditScreenTest : BaseComposeTest() {
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
verify { viewModel.trySendAction(VaultAddEditAction.Common.Fido2ErrorDialogDismissed) }
verify {
viewModel.trySendAction(
VaultAddEditAction.Common.Fido2ErrorDialogDismissed("mockMessage".asText()),
)
}
}
@Test
@@ -833,7 +833,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
),
)
val mockCreateResult = Fido2RegisterCredentialResult.Success(
registrationResponse = "mockRegistrationResponse",
responseJson = "mockRegistrationResponse",
)
val mockAttestationOptions = createMockPasskeyAttestationOptions(
number = 1,
@@ -989,7 +989,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
fido2CreateCredentialRequest = fido2CredentialRequest,
selectedCipherView = any(),
)
} returns Fido2RegisterCredentialResult.Success(registrationResponse = "mockResponse")
} returns Fido2RegisterCredentialResult.Success(responseJson = "mockResponse")
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
@@ -2045,12 +2045,19 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
),
)
viewModel.trySendAction(VaultAddEditAction.Common.Fido2ErrorDialogDismissed)
viewModel.trySendAction(
VaultAddEditAction.Common.Fido2ErrorDialogDismissed(
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
),
)
viewModel.eventFlow.test {
assertNull(viewModel.stateFlow.value.dialog)
assertEquals(
VaultAddEditEvent.CompleteFido2Registration(
result = Fido2RegisterCredentialResult.Error,
result = Fido2RegisterCredentialResult.Error(
R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
),
),
awaitItem(),
)
@@ -4149,7 +4156,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
any(),
)
} returns Fido2RegisterCredentialResult.Success(
registrationResponse = "mockResponse",
responseJson = "mockResponse",
)
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess)
@@ -4181,7 +4188,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
any(),
)
} returns Fido2RegisterCredentialResult.Success(
registrationResponse = "mockResponse",
responseJson = "mockResponse",
)
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess)
@@ -4218,7 +4225,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
runTest {
val mockRequest = createMockFido2CreateCredentialRequest(number = 1)
val mockResult = Fido2RegisterCredentialResult.Success(
registrationResponse = "mockResponse",
responseJson = "mockResponse",
)
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
fido2CreateCredentialRequest = mockRequest,
@@ -4261,7 +4268,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteFido2Registration result`() =
runTest {
val mockRequest = createMockFido2CreateCredentialRequest(number = 1)
val mockResult = Fido2RegisterCredentialResult.Error
val mockResult = Fido2RegisterCredentialResult.Error("".asText())
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
fido2CreateCredentialRequest = mockRequest,
)
@@ -4299,7 +4306,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
runTest {
val mockRequest = createMockFido2CreateCredentialRequest(number = 1)
val mockResult = Fido2RegisterCredentialResult.Success(
registrationResponse = "mockResponse",
responseJson = "mockResponse",
)
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
fido2CreateCredentialRequest = mockRequest,
@@ -1873,7 +1873,11 @@ class VaultItemListingScreenTest : BaseComposeTest() {
.performClick()
verify {
viewModel.trySendAction(VaultItemListingsAction.DismissFido2ErrorDialogClick)
viewModel.trySendAction(
VaultItemListingsAction.DismissFido2ErrorDialogClick(
message = dialogMessage.asText(),
),
)
}
}
@@ -471,7 +471,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
message = R.string.passkey_operation_failed_because_the_selected_item_does_not_exist
.asText(),
),
viewModel.stateFlow.value.dialogState,
@@ -502,8 +502,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
message = R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
),
viewModel.stateFlow.value.dialogState,
)
@@ -2639,7 +2638,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test
fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteFido2Registration result`() =
runTest {
val mockResult = Fido2RegisterCredentialResult.Error
val mockResult = Fido2RegisterCredentialResult.Error("".asText())
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(
@@ -2666,7 +2665,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
fun `Fido2RegisterCredentialResult Success should show toast and emit CompleteFido2Registration result`() =
runTest {
val mockResult = Fido2RegisterCredentialResult.Success(
registrationResponse = "mockResponse",
responseJson = "mockResponse",
)
val viewModel = createVaultItemListingViewModel()
@@ -2718,12 +2717,18 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
createMockFido2CreateCredentialRequest(number = 1),
)
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(VaultItemListingsAction.DismissFido2ErrorDialogClick)
viewModel.trySendAction(
VaultItemListingsAction.DismissFido2ErrorDialogClick(
"".asText(),
),
)
viewModel.eventFlow.test {
assertNull(viewModel.stateFlow.value.dialogState)
assertEquals(
VaultItemListingEvent.CompleteFido2Registration(
result = Fido2RegisterCredentialResult.Error,
result = Fido2RegisterCredentialResult.Error(
"".asText(),
),
),
awaitItem(),
)
@@ -2750,11 +2755,13 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
),
)
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(VaultItemListingsAction.DismissFido2ErrorDialogClick)
viewModel.trySendAction(
VaultItemListingsAction.DismissFido2ErrorDialogClick("".asText()),
)
viewModel.eventFlow.test {
assertEquals(
VaultItemListingEvent.CompleteFido2Assertion(
result = Fido2CredentialAssertionResult.Error,
result = Fido2CredentialAssertionResult.Error("".asText()),
),
awaitItem(),
)
@@ -2768,11 +2775,13 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
runTest {
specialCircumstanceManager.specialCircumstance = null
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(VaultItemListingsAction.DismissFido2ErrorDialogClick)
viewModel.trySendAction(
VaultItemListingsAction.DismissFido2ErrorDialogClick("".asText()),
)
assertEquals(
VaultItemListingState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.generic_error_message.asText(),
message = "".asText(),
),
viewModel.stateFlow.value.dialogState,
)
@@ -3007,7 +3016,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
message = R.string.passkey_operation_failed_because_the_request_is_invalid
.asText(),
),
viewModel.stateFlow.value.dialogState,
@@ -3058,8 +3067,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
message = R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
),
viewModel.stateFlow.value.dialogState,
)
@@ -3181,8 +3189,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
message =
R.string.passkey_operation_failed_because_the_selected_item_does_not_exist
.asText(),
),
viewModel.stateFlow.value.dialogState,
)
@@ -3223,8 +3232,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
message =
R.string.passkey_operation_failed_because_the_selected_item_does_not_exist
.asText(),
),
viewModel.stateFlow.value.dialogState,
)
@@ -3503,7 +3513,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
message = R.string.passkey_operation_failed_because_user_is_locked_out.asText(),
),
viewModel.stateFlow.value.dialogState,
)
@@ -3556,7 +3566,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
any(),
)
} returns Fido2RegisterCredentialResult.Success(
registrationResponse = "mockResponse",
responseJson = "mockResponse",
)
val viewModel = createVaultItemListingViewModel()
@@ -3569,7 +3579,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
message = R.string.passkey_operation_failed_because_the_request_is_invalid
.asText(),
),
viewModel.stateFlow.value.dialogState,
)
@@ -3594,7 +3605,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
any(),
)
} returns Fido2RegisterCredentialResult.Success(
registrationResponse = "mockResponse",
responseJson = "mockResponse",
)
val viewModel = createVaultItemListingViewModel()
@@ -3607,7 +3618,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
message = R.string.passkey_operation_failed_because_the_request_is_invalid
.asText(),
),
viewModel.stateFlow.value.dialogState,
)
@@ -3652,7 +3664,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
any(),
)
} returns Fido2RegisterCredentialResult.Success(
registrationResponse = "mockResponse",
responseJson = "mockResponse",
)
val viewModel = createVaultItemListingViewModel()
@@ -3939,8 +3951,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
message =
R.string.passkey_operation_failed_because_user_verification_attempts_exceeded
.asText(),
),
viewModel.stateFlow.value.dialogState,
)
@@ -3969,7 +3982,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
message = R.string.passkey_operation_failed_because_the_selected_item_does_not_exist
.asText(),
),
viewModel.stateFlow.value.dialogState,
@@ -4110,8 +4123,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
message =
R.string.passkey_operation_failed_because_user_verification_attempts_exceeded
.asText(),
),
viewModel.stateFlow.value.dialogState,
)
@@ -4139,7 +4153,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
message = R.string.passkey_operation_failed_because_the_selected_item_does_not_exist
.asText(),
),
viewModel.stateFlow.value.dialogState,
@@ -4288,7 +4302,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
message = R.string.passkey_operation_failed_because_user_verification_was_cancelled
.asText(),
),
viewModel.stateFlow.value.dialogState,
@@ -4355,7 +4369,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
R.string.an_error_has_occurred.asText(),
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
R.string.passkey_operation_failed_because_the_selected_item_does_not_exist
.asText(),
),
viewModel.stateFlow.value.dialogState,
)