diff --git a/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt index 13d47c3f6..42a41436c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt @@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2AssertionRequestOrNull import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2CredentialRequestOrNull +import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2GetCredentialsRequestOrNull import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull @@ -184,6 +185,7 @@ class MainViewModel @Inject constructor( val hasVaultShortcut = intent.isMyVaultShortcut val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull() val fido2CredentialAssertionRequest = intent.getFido2AssertionRequestOrNull() + val fido2GetCredentialsRequest = intent.getFido2GetCredentialsRequestOrNull() when { passwordlessRequestData != null -> { specialCircumstanceManager.specialCircumstance = @@ -247,6 +249,13 @@ class MainViewModel @Inject constructor( ) } + fido2GetCredentialsRequest != null -> { + specialCircumstanceManager.specialCircumstance = + SpecialCircumstance.Fido2GetCredentials( + fido2GetCredentialsRequest = fido2GetCredentialsRequest, + ) + } + hasGeneratorShortcut -> { specialCircumstanceManager.specialCircumstance = SpecialCircumstance.GeneratorShortcut diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsRequest.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsRequest.kt index b253f6c5c..f0f54f59e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsRequest.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsRequest.kt @@ -23,7 +23,7 @@ data class Fido2GetCredentialsRequest( val callingAppInfo: CallingAppInfo get() = CallingAppInfo(packageName, signingInfo, origin) - val getCredentialsRequest: BeginGetPublicKeyCredentialOption + val option: BeginGetPublicKeyCredentialOption get() = BeginGetPublicKeyCredentialOption( candidateQueryData, id, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsResult.kt similarity index 86% rename from app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialResult.kt rename to app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsResult.kt index ea2358260..81bcd2ad4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsResult.kt @@ -6,7 +6,7 @@ import com.bitwarden.fido.Fido2CredentialAutofillView /** * Represents the result of a FIDO 2 Get Credentials request. */ -sealed class Fido2GetCredentialResult { +sealed class Fido2GetCredentialsResult { /** * Indicates credentials were successfully queried. * @@ -17,10 +17,10 @@ sealed class Fido2GetCredentialResult { data class Success( val options: BeginGetPublicKeyCredentialOption, val credentials: List, - ) : Fido2GetCredentialResult() + ) : Fido2GetCredentialsResult() /** * Indicates an error was encountered when querying for matching credentials. */ - data object Error : Fido2GetCredentialResult() + data object Error : Fido2GetCredentialsResult() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/PasskeyAttestationOptions.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/PasskeyAttestationOptions.kt index 86a24ac2e..b3576eae6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/PasskeyAttestationOptions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/PasskeyAttestationOptions.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable /** - * Models a FIDO 2 credential creation request options received from a Relying Party (RP). + * Models FIDO 2 credential creation request options received from a Relying Party (RP). */ @Serializable data class PasskeyAttestationOptions( diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/util/Fido2IntentUtils.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/util/Fido2IntentUtils.kt index 4d1e0c635..92788931b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/util/Fido2IntentUtils.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/util/Fido2IntentUtils.kt @@ -4,9 +4,11 @@ import android.content.Intent import android.os.Build import androidx.credentials.CreatePublicKeyCredentialRequest import androidx.credentials.GetPublicKeyCredentialOption +import androidx.credentials.provider.BeginGetPublicKeyCredentialOption import androidx.credentials.provider.PendingIntentHandler import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest +import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CIPHER_ID import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CREDENTIAL_ID @@ -72,3 +74,34 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? { origin = systemRequest.callingAppInfo.origin, ) } + +/** + * Checks if this [Intent] contains a [Fido2GetCredentialsRequest] related to an ongoing FIDO 2 + * credential lookup process. + */ +fun Intent.getFido2GetCredentialsRequestOrNull(): Fido2GetCredentialsRequest? { + if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null + + val systemRequest = PendingIntentHandler + .retrieveBeginGetCredentialRequest(this) + ?: return null + + val option: BeginGetPublicKeyCredentialOption = systemRequest + .beginGetCredentialOptions + .firstNotNullOfOrNull { it as? BeginGetPublicKeyCredentialOption } + ?: return null + + val callingAppInfo = systemRequest + .callingAppInfo + ?: return null + + return Fido2GetCredentialsRequest( + candidateQueryData = option.candidateQueryData, + id = option.id, + requestJson = option.requestJson, + clientDataHash = option.clientDataHash, + packageName = callingAppInfo.packageName, + signingInfo = callingAppInfo.signingInfo, + origin = callingAppInfo.origin, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManager.kt b/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManager.kt index 07de9c6c7..d477f93fc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManager.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManager.kt @@ -1,7 +1,7 @@ package com.x8bit.bitwarden.ui.autofill.fido2.manager import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult -import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialResult +import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult /** @@ -22,5 +22,5 @@ interface Fido2CompletionManager { /** * Complete the FIDO 2 "Get credentials" process with the provided [result]. */ - fun completeFido2GetCredentialRequest(result: Fido2GetCredentialResult) + fun completeFido2GetCredentialRequest(result: Fido2GetCredentialsResult) } 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 f92d53cfa..1b262c6eb 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 @@ -15,7 +15,7 @@ import androidx.credentials.provider.PendingIntentHandler import androidx.credentials.provider.PublicKeyCredentialEntry import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult -import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialResult +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.ui.platform.manager.intent.IntentManager @@ -93,11 +93,11 @@ class Fido2CompletionManagerImpl( } } - override fun completeFido2GetCredentialRequest(result: Fido2GetCredentialResult) { + override fun completeFido2GetCredentialRequest(result: Fido2GetCredentialsResult) { val resultIntent = Intent() val responseBuilder = BeginGetCredentialResponse.Builder() when (result) { - is Fido2GetCredentialResult.Success -> { + is Fido2GetCredentialsResult.Success -> { val entries = result .credentials .map { @@ -130,7 +130,7 @@ class Fido2CompletionManagerImpl( ) } - Fido2GetCredentialResult.Error, + Fido2GetCredentialsResult.Error, -> { PendingIntentHandler.setGetCredentialException( resultIntent, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerUnsupportedApiImpl.kt b/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerUnsupportedApiImpl.kt index a40966979..f24eddc48 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerUnsupportedApiImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerUnsupportedApiImpl.kt @@ -1,7 +1,7 @@ package com.x8bit.bitwarden.ui.autofill.fido2.manager import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult -import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialResult +import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult /** @@ -13,5 +13,5 @@ object Fido2CompletionManagerUnsupportedApiImpl : Fido2CompletionManager { override fun completeFido2Assertion(result: Fido2CredentialAssertionResult) = Unit - override fun completeFido2GetCredentialRequest(result: Fido2GetCredentialResult) = Unit + override fun completeFido2GetCredentialRequest(result: Fido2GetCredentialsResult) = Unit } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/composition/LocalManagerProvider.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/composition/LocalManagerProvider.kt index 410e3f4ea..f6d48f4a3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/composition/LocalManagerProvider.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/composition/LocalManagerProvider.kt @@ -30,7 +30,9 @@ import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManagerImp * Helper [Composable] that wraps a [content] and provides manager classes via [CompositionLocal]. */ @Composable -fun LocalManagerProvider(content: @Composable () -> Unit) { +fun LocalManagerProvider( + content: @Composable () -> Unit, +) { val activity = LocalContext.current as Activity val fido2IntentManager: IntentManager = IntentManagerImpl(activity) val fido2CompletionManager = 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 e750f0d75..112395651 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 @@ -169,6 +169,10 @@ fun VaultItemListingScreen( is VaultItemListingEvent.CompleteFido2Assertion -> { fido2CompletionManager.completeFido2Assertion(event.result) } + + is VaultItemListingEvent.CompleteFido2GetCredentialsRequest -> { + fido2CompletionManager.completeFido2GetCredentialRequest(event.result) + } } } 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 61f800d81..8cb755d04 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 @@ -14,6 +14,8 @@ import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest +import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest +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.model.Fido2ValidateOriginResult import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement @@ -104,9 +106,13 @@ class VaultItemListingViewModel @Inject constructor( val autofillSelectionData = specialCircumstance as? SpecialCircumstance.AutofillSelection val fido2CreationData = specialCircumstance as? SpecialCircumstance.Fido2Save val fido2AssertionData = specialCircumstance as? SpecialCircumstance.Fido2Assertion + val fido2GetCredentialsData = + specialCircumstance as? SpecialCircumstance.Fido2GetCredentials val shouldFinishOnComplete = autofillSelectionData ?.shouldFinishWhenComplete - ?: (fido2CreationData != null || fido2AssertionData != null) + ?: (fido2CreationData != null || + fido2AssertionData != null || + fido2GetCredentialsData != null) val dialogState = fido2CreationData ?.let { VaultItemListingState.DialogState.Loading(R.string.loading.asText()) } VaultItemListingState( @@ -130,6 +136,7 @@ class VaultItemListingViewModel @Inject constructor( hasMasterPassword = userState.activeAccount.hasMasterPassword, fido2CredentialRequest = fido2CreationData?.fido2CredentialRequest, fido2CredentialAssertionRequest = fido2AssertionData?.fido2AssertionRequest, + fido2GetCredentialsRequest = fido2GetCredentialsData?.fido2GetCredentialsRequest, isPremium = userState.activeAccount.isPremium, ) }, @@ -182,7 +189,8 @@ class VaultItemListingViewModel @Inject constructor( VaultItemListingsAction.Internal.VaultDataReceive( it .filterForAutofillIfNecessary() - .filterForFido2CreationIfNecessary(), + .filterForFido2CreationIfNecessary() + .filterForFidoGetCredentialsIfNecessary(), ) } .onEach(::sendAction) @@ -1240,7 +1248,31 @@ class VaultItemListingViewModel @Inject constructor( private fun vaultLoadedReceive(vaultData: DataState.Loaded) { updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = true) - sendEvent(VaultItemListingEvent.DismissPullToRefresh) + state.fido2GetCredentialsRequest + ?.let { fido2GetCredentialsRequest -> + val relyingPartyId = fido2CredentialManager + .getPasskeyAssertionOptionsOrNull( + requestJson = fido2GetCredentialsRequest.option.requestJson, + ) + ?.relyingPartyId + ?: run { + showFido2ErrorDialog() + return + } + sendEvent( + VaultItemListingEvent.CompleteFido2GetCredentialsRequest( + Fido2GetCredentialsResult.Success( + options = fido2GetCredentialsRequest.option, + credentials = vaultData + .data + .fido2CredentialAutofillViewList + ?.filter { it.rpId == relyingPartyId } + ?: emptyList(), + ), + ), + ) + } + ?: sendEvent(VaultItemListingEvent.DismissPullToRefresh) } private fun vaultLoadingReceive() { @@ -1531,6 +1563,27 @@ class VaultItemListingViewModel @Inject constructor( } } + /** + * Takes the given vault data and filters it for FIDO 2 credential selection. + */ + @Suppress("MaxLineLength") + private suspend fun DataState.filterForFidoGetCredentialsIfNecessary(): DataState { + val request = state.fido2GetCredentialsRequest ?: return this + return this.map { vaultData -> + val matchUri = request.origin + ?: request.packageName + .toAndroidAppUriString() + + vaultData.copy( + cipherViewList = cipherMatchingManager.filterCiphersForMatches( + ciphers = vaultData.cipherViewList, + matchUri = matchUri, + ), + fido2CredentialAutofillViewList = vaultData.toFido2CredentialAutofillViews(), + ) + } + } + /** * Decrypt and filter the fido 2 autofill credentials. */ @@ -1582,6 +1635,7 @@ data class VaultItemListingState( val autofillSelectionData: AutofillSelectionData? = null, val fido2CredentialRequest: Fido2CredentialRequest? = null, val fido2CredentialAssertionRequest: Fido2CredentialAssertionRequest? = null, + val fido2GetCredentialsRequest: Fido2GetCredentialsRequest? = null, val shouldFinishOnComplete: Boolean = false, val hasMasterPassword: Boolean, val isPremium: Boolean, @@ -2077,6 +2131,15 @@ sealed class VaultItemListingEvent { data class CompleteFido2Assertion( val result: Fido2CredentialAssertionResult, ) : VaultItemListingEvent() + + /** + * FIDO 2 credential lookup result has been received and the process is ready to be completed. + * + * @property result The result of querying for matching FIDO 2 credentials. + */ + data class CompleteFido2GetCredentialsRequest( + val result: Fido2GetCredentialsResult, + ) : VaultItemListingEvent() } /** diff --git a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt index a7859eb63..eae318fae 100644 --- a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt @@ -12,11 +12,14 @@ import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest +import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialAssertionRequest import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialRequest +import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2GetCredentialsRequest import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2AssertionRequestOrNull import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2CredentialRequestOrNull +import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2GetCredentialsRequestOrNull import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem @@ -494,6 +497,27 @@ class MainViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") + @Test + fun `on ReceiveFirstIntent with fido2 get credentials request data should set the special circumstance to Fido2GetCredentials`() { + val viewModel = createViewModel() + val mockGetCredentialsRequest = createMockFido2GetCredentialsRequest(number = 1) + val mockIntent = createMockFido2GetCredentialsIntent(mockGetCredentialsRequest) + + every { intentManager.getShareDataFromIntent(mockIntent) } returns null + + viewModel.trySendAction( + MainAction.ReceiveFirstIntent( + intent = mockIntent, + ), + ) + + assertEquals( + SpecialCircumstance.Fido2GetCredentials(mockGetCredentialsRequest), + specialCircumstanceManager.specialCircumstance, + ) + } + @Suppress("MaxLineLength") @Test fun `on ReceiveNewIntent with share data should set the special circumstance to ShareNewSend`() { @@ -731,3 +755,18 @@ private fun createMockFido2AssertionIntent( every { isMyVaultShortcut } returns false every { isPasswordGeneratorShortcut } returns false } + +private fun createMockFido2GetCredentialsIntent( + fido2GetCredentialsRequest: Fido2GetCredentialsRequest = createMockFido2GetCredentialsRequest( + number = 1, + ), +): Intent = mockk { + every { getFido2GetCredentialsRequestOrNull() } returns fido2GetCredentialsRequest + every { getPasswordlessRequestDataIntentOrNull() } returns null + every { getAutofillSelectionDataOrNull() } returns null + every { getAutofillSaveItemOrNull() } returns null + every { getFido2CredentialRequestOrNull() } returns null + every { getFido2AssertionRequestOrNull() } returns null + every { isMyVaultShortcut } returns false + every { isPasswordGeneratorShortcut } returns false +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/util/Fido2IntentUtilsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/util/Fido2IntentUtilsTest.kt index 2bebc76d0..9b1f40304 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/util/Fido2IntentUtilsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/util/Fido2IntentUtilsTest.kt @@ -2,16 +2,21 @@ package com.x8bit.bitwarden.data.autofill.fido2.util import android.content.Intent import android.content.pm.SigningInfo +import android.service.credentials.BeginGetCredentialRequest +import androidx.core.os.bundleOf import androidx.credentials.CreatePasswordRequest import androidx.credentials.CreatePublicKeyCredentialRequest import androidx.credentials.GetPasswordOption import androidx.credentials.GetPublicKeyCredentialOption +import androidx.credentials.provider.BeginGetPasswordOption +import androidx.credentials.provider.BeginGetPublicKeyCredentialOption import androidx.credentials.provider.CallingAppInfo import androidx.credentials.provider.PendingIntentHandler import androidx.credentials.provider.ProviderCreateCredentialRequest import androidx.credentials.provider.ProviderGetCredentialRequest import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest +import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CIPHER_ID import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CREDENTIAL_ID @@ -285,4 +290,102 @@ class Fido2IntentUtilsTest { assertNull(assertionRequest) } + + @Suppress("MaxLineLength") + @Test + fun `getFido2GetCredentialsRequestOrNull should return Fido2GetCredentialRequest when present`() { + val intent = mockk() + val mockOption = BeginGetPublicKeyCredentialOption( + candidateQueryData = bundleOf(), + id = "mockId", + requestJson = "mockRequestJson", + clientDataHash = byteArrayOf(0), + ) + val mockCallingAppInfo = CallingAppInfo( + packageName = "mockPackageName", + signingInfo = SigningInfo(), + origin = "mockOrigin", + ) + + every { + PendingIntentHandler.retrieveBeginGetCredentialRequest(intent) + } returns mockk { + every { beginGetCredentialOptions } returns listOf(mockOption) + every { callingAppInfo } returns mockCallingAppInfo + } + + val result = intent.getFido2GetCredentialsRequestOrNull() + + assertEquals( + Fido2GetCredentialsRequest( + candidateQueryData = mockOption.candidateQueryData, + id = mockOption.id, + requestJson = mockOption.requestJson, + clientDataHash = mockOption.clientDataHash, + packageName = mockCallingAppInfo.packageName, + signingInfo = mockCallingAppInfo.signingInfo, + origin = mockCallingAppInfo.origin, + ), + result, + ) + } + + @Test + fun `getGido2GetCredentialsRequestOrNull should return null when build version is below 34`() { + val intent = mockk() + every { isBuildVersionBelow(34) } returns true + + val result = intent.getFido2GetCredentialsRequestOrNull() + + assertNull(result) + } + + @Suppress("MaxLineLength") + @Test + fun `getFido2GetCredentialsRequestOrNull should return null when retrieveBeginGetCredentialRequest is null`() { + val intent = mockk { + every { + getParcelableExtra( + "android.service.credentials.extra.BEGIN_GET_CREDENTIAL_REQUEST", + BeginGetCredentialRequest::class.java, + ) + } returns null + } + every { PendingIntentHandler.retrieveProviderGetCredentialRequest(intent) } returns null + val result = intent.getFido2GetCredentialsRequestOrNull() + assertNull(result) + } + + @Suppress("MaxLineLength") + @Test + fun `getFido2GetCredentialRequestOrNull should return null when no passkey credential options are present`() { + val intent = mockk() + every { PendingIntentHandler.retrieveBeginGetCredentialRequest(intent) } returns mockk { + every { beginGetCredentialOptions } returns listOf(mockk()) + } + val result = intent.getFido2GetCredentialsRequestOrNull() + assertNull(result) + } + + @Test + fun `getFido2GetCredentialRequestOrNull should return null when calling app info is null`() { + val intent = mockk() + val mockOption = createMockBeginGetPublicKeyCredentialOption(number = 1) + every { PendingIntentHandler.retrieveBeginGetCredentialRequest(intent) } returns mockk { + every { beginGetCredentialOptions } returns listOf(mockOption) + every { callingAppInfo } returns null + } + val result = intent.getFido2GetCredentialsRequestOrNull() + assertNull(result) + } } + +private fun createMockBeginGetPublicKeyCredentialOption( + number: Int, +): BeginGetPublicKeyCredentialOption = + BeginGetPublicKeyCredentialOption( + candidateQueryData = bundleOf(), + id = "mockId-$number", + requestJson = "mockRequestJson-$number", + clientDataHash = byteArrayOf(0), + ) 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 0c907ab7f..39f267da9 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 @@ -9,7 +9,7 @@ import androidx.credentials.provider.PublicKeyCredentialEntry import com.bitwarden.fido.Fido2CredentialAutofillView import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult -import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialResult +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 @@ -76,7 +76,7 @@ class Fido2CompletionManagerTest { @Test fun `completeFido2GetCredentials should perform no operations`() { - val mockGetCredentialResult = mockk() + val mockGetCredentialResult = mockk() fido2CompletionManager.completeFido2GetCredentialRequest(mockGetCredentialResult) verify { mockGetCredentialResult wasNot Called @@ -174,7 +174,7 @@ class Fido2CompletionManagerTest { fun `completeFido2GetCredentials should set BeginGetCredentialResponse, set activity result, then finish activity when result is Success`() { fido2CompletionManager .completeFido2GetCredentialRequest( - Fido2GetCredentialResult.Success( + Fido2GetCredentialsResult.Success( options = mockk(), credentials = emptyList(), ), @@ -210,7 +210,7 @@ class Fido2CompletionManagerTest { fido2CompletionManager .completeFido2GetCredentialRequest( - Fido2GetCredentialResult.Success( + Fido2GetCredentialsResult.Success( options = mockk(), credentials = mockFido2AutofillViewList, ), @@ -258,7 +258,7 @@ class Fido2CompletionManagerTest { fido2CompletionManager .completeFido2GetCredentialRequest( - Fido2GetCredentialResult.Success( + Fido2GetCredentialsResult.Success( options = mockk(), credentials = mockFido2AutofillViewList, ), @@ -284,7 +284,7 @@ class Fido2CompletionManagerTest { @Test fun `completeFido2GetCredentials should set GetCredentialException, set activity result, then finish activity when result is Error`() { fido2CompletionManager - .completeFido2GetCredentialRequest(Fido2GetCredentialResult.Error) + .completeFido2GetCredentialRequest(Fido2GetCredentialsResult.Error) verifyActivityResultIsSetAndFinishedAfter { PendingIntentHandler.setGetCredentialException(any(), any()) } 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 eb83e9b43..5347ed080 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 @@ -17,6 +17,7 @@ import androidx.compose.ui.test.performTextInput import androidx.core.net.toUri import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult +import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.platform.repository.model.Environment @@ -89,6 +90,7 @@ class VaultItemListingScreenTest : BaseComposeTest() { private val fido2CompletionManager: Fido2CompletionManager = mockk { every { completeFido2Registration(any()) } just runs every { completeFido2Assertion(any()) } just runs + every { completeFido2GetCredentialRequest(any()) } just runs } private val biometricsManager: BiometricsManager = mockk() private val mutableEventFlow = bufferedMutableSharedFlow() @@ -1791,6 +1793,15 @@ class VaultItemListingScreenTest : BaseComposeTest() { } } + @Test + fun `CompleteFido2GetCredentials event should call Fido2CompletionManager with result`() { + val result = Fido2GetCredentialsResult.Success(mockk(), mockk()) + mutableEventFlow.tryEmit(VaultItemListingEvent.CompleteFido2GetCredentialsRequest(result)) + verify { + fido2CompletionManager.completeFido2GetCredentialRequest(result) + } + } + @Test fun `Fido2UserVerification event should perform user verification when it is supported`() { every { 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 0815fc70c..decd44639 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 @@ -15,6 +15,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest +import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement @@ -1381,6 +1382,87 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `vaultDataStateFlow Loaded with Fido2GetCredentials special circumstance should update ViewState to Content with filtered data`() = + runTest { + setupMockUri() + + val cipherView1 = createMockCipherView( + number = 1, + fido2Credentials = createMockSdkFido2CredentialList(number = 1), + ) + val cipherView2 = createMockCipherView( + number = 2, + fido2Credentials = createMockSdkFido2CredentialList(number = 1), + ) + + every { + fido2CredentialManager.getPasskeyAssertionOptionsOrNull(any()) + } returns createMockPasskeyAssertionOptions( + number = 1, + ) + coEvery { + vaultRepository.getDecryptedFido2CredentialAutofillViews( + cipherViewList = listOf(cipherView1, cipherView2), + ) + } returns DecryptFido2CredentialAutofillViewResult.Success(emptyList()) + + mockFilteredCiphers = listOf(cipherView1) + + val fido2GetCredentialRequest = Fido2GetCredentialsRequest( + requestJson = "{}", + packageName = "com.x8bit.bitwarden", + signingInfo = SigningInfo(), + origin = "mockOrigin", + candidateQueryData = mockk(), + clientDataHash = byteArrayOf(0), + id = "mockId", + ) + + specialCircumstanceManager.specialCircumstance = + SpecialCircumstance.Fido2GetCredentials( + fido2GetCredentialsRequest = fido2GetCredentialRequest, + ) + val dataState = DataState.Loaded( + data = VaultData( + cipherViewList = listOf(cipherView1, cipherView2), + folderViewList = listOf(createMockFolderView(number = 1)), + collectionViewList = listOf(createMockCollectionView(number = 1)), + sendViewList = listOf(createMockSendView(number = 1)), + ), + ) + + val viewModel = createVaultItemListingViewModel() + + mutableVaultDataStateFlow.value = dataState + + assertEquals( + createVaultItemListingState( + viewState = VaultItemListingState.ViewState.Content( + displayCollectionList = emptyList(), + displayItemList = listOf( + createMockDisplayItemForCipher(number = 1) + .copy( + secondSubtitleTestTag = "PasskeySite", + ), + ), + displayFolderList = emptyList(), + ), + ) + .copy( + fido2GetCredentialsRequest = fido2GetCredentialRequest, + shouldFinishOnComplete = true, + ), + viewModel.stateFlow.value, + ) + coVerify { + vaultRepository.getDecryptedFido2CredentialAutofillViews( + cipherViewList = listOf(cipherView1, cipherView2), + ) + } + } + @Test fun `vaultDataStateFlow Loaded with empty items should update ViewState to NoItems`() = runTest {