diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2CredentialManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2CredentialManagerImpl.kt index c301e2afad..e41eac9caa 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2CredentialManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2CredentialManagerImpl.kt @@ -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(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(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(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 diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManager.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManager.kt index 85d74b01dd..2457187540 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManager.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManager.kt @@ -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? } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManagerImpl.kt index 68eb184819..13b3da08b0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManagerImpl.kt @@ -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() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2CredentialAssertionResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2CredentialAssertionResult.kt index 57862f2da5..fe51fc9701 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2CredentialAssertionResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2CredentialAssertionResult.kt @@ -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() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsResult.kt index 0751f83be4..32d525f90b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsResult.kt @@ -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() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2RegisterCredentialResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2RegisterCredentialResult.kt index ce42a14909..7eb713a1ad 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2RegisterCredentialResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2RegisterCredentialResult.kt @@ -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. diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/PublicKeyCredentialDescriptor.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/PublicKeyCredentialDescriptor.kt index d4cb9bf663..c5e46484c0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/PublicKeyCredentialDescriptor.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/PublicKeyCredentialDescriptor.kt @@ -13,5 +13,5 @@ data class PublicKeyCredentialDescriptor( @SerialName("id") val id: String, @SerialName("transports") - val transports: List, + val transports: List?, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorImpl.kt index 0e060bed39..3e92b6979b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorImpl.kt @@ -104,6 +104,69 @@ class Fido2ProviderProcessorImpl( } } + override fun processGetCredentialRequest( + request: BeginGetCredentialRequest, + cancellationSignal: CancellationSignal, + callback: OutcomeReceiver, + ) { + // 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, + ) { + // 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, - ) { - // 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, - ) { - // no-op: RFU - callback.onError(ClearCredentialUnsupportedException()) - } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt index 3d65921cca..7d33570d6b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt @@ -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), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt index 2a4b447a9e..37db5e7183 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt @@ -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() } /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerImpl.kt index 4f4a5d7646..ace38acab2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerImpl.kt @@ -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), + ), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/about/AboutScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/about/AboutScreen.kt index e90899f607..6c4c7a4880 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/about/AboutScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/about/AboutScreen.kt @@ -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(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt index bbbd41ff4b..7bdbb6ce23 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt @@ -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) }, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt index 7decdaa710..7df5e95a48 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt @@ -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. diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt index 9015b09d62..9b15ec3d38 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt @@ -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 -> { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt index bcc1d78d01..1490f83160 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt @@ -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. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d172e1f88..dab30c8723 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1171,7 +1171,6 @@ Do you want to switch to this account? 6 OF 6 Use these options to adjust your password to meet your account website\'s requirements. "After you save your new password to Bitwarden, don’t forget to update it on your account website. " - Link Error loading certificate Certificate alias Import certificate @@ -1184,4 +1183,17 @@ Do you want to switch to this account? Invalid certificate chain 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. Certificate alias already exists + Link + Passkey operation failed because app is signed incorrectly. + Passkey operation failed because host URL is not present in request. + Passkey operation failed because app signature is invalid. + Passkey registration failed due to an internal error. + Passkey operation failed because relying party cannot be identified. + Passkey authentication failed due to an internal error. + Passkey operation failed because the selected item does not exist. + Passkey operation failed because user is locked out. + Passkey operation failed because user verification was cancelled. + Passkey operation failed because the request is invalid. + Passkey operation failed because user verification attempts exceeded. + Passkey operation failed because no item was selected. diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2CredentialManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2CredentialManagerTest.kt index 4625aba935..2e6c377358 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2CredentialManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2CredentialManagerTest.kt @@ -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 { - 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": [ diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorTest.kt index 8d081e258d..eb55f8e037 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorTest.kt @@ -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 = mockk() val captureSlot = slot() @@ -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 = mockk() val captureSlot = slot() @@ -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 = 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 = 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 = 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 = 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 { + every { candidateQueryData } returns Bundle() + }, + ) + } val callback: OutcomeReceiver = mockk() val captureSlot = slot() 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 { + every { candidateQueryData } returns Bundle() + }, + ) + every { callingAppInfo } returns CallingAppInfo( + packageName = "com.x8bit.bitwarden.dev", + signingInfo = mockk(), + origin = null, + ) + } val callback: OutcomeReceiver = mockk() val captureSlot = slot() 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()) + every { beginGetCredentialOptions } returns listOf( + mockk { + every { candidateQueryData } returns Bundle() + }, + ) + every { callingAppInfo } returns CallingAppInfo( + packageName = "com.x8bit.bitwarden.dev", + signingInfo = mockk(), + origin = null, + ) } val callback: OutcomeReceiver = mockk() val captureSlot = slot() @@ -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 = mockk() val captureSlot = slot() @@ -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 = mockk() val captureSlot = slot() diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreenTest.kt index 9cdcbc1e7e..f7be47f897 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreenTest.kt @@ -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()), ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt index 79b128995a..e573a03085 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt @@ -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(), + ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerTest.kt index 0e5008b25c..da14b89816 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerTest.kt @@ -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 { 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()) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt index b26cd7ba11..01875b252b 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt @@ -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 diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt index 1d43c64c34..eddc77f58d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt @@ -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, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt index 765cacb35c..fb5524c032 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt @@ -1873,7 +1873,11 @@ class VaultItemListingScreenTest : BaseComposeTest() { .performClick() verify { - viewModel.trySendAction(VaultItemListingsAction.DismissFido2ErrorDialogClick) + viewModel.trySendAction( + VaultItemListingsAction.DismissFido2ErrorDialogClick( + message = dialogMessage.asText(), + ), + ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt index 72ddd51863..db6acef629 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt @@ -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, )