[PM-9410] Filter matching FIDO 2 credentials after vault unlock (#3648)

This commit is contained in:
Patrick Honkonen 2024-07-30 13:45:36 -04:00 committed by GitHub
parent deb8f811e5
commit b26e1a082e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 369 additions and 23 deletions

View file

@ -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

View file

@ -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,

View file

@ -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<Fido2CredentialAutofillView>,
) : Fido2GetCredentialResult()
) : Fido2GetCredentialsResult()
/**
* Indicates an error was encountered when querying for matching credentials.
*/
data object Error : Fido2GetCredentialResult()
data object Error : Fido2GetCredentialsResult()
}

View file

@ -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(

View file

@ -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,
)
}

View file

@ -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)
}

View file

@ -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,

View file

@ -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
}

View file

@ -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 =

View file

@ -169,6 +169,10 @@ fun VaultItemListingScreen(
is VaultItemListingEvent.CompleteFido2Assertion -> {
fido2CompletionManager.completeFido2Assertion(event.result)
}
is VaultItemListingEvent.CompleteFido2GetCredentialsRequest -> {
fido2CompletionManager.completeFido2GetCredentialRequest(event.result)
}
}
}

View file

@ -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<VaultData>) {
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<VaultData>.filterForFidoGetCredentialsIfNecessary(): DataState<VaultData> {
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()
}
/**

View file

@ -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<Intent> {
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
}

View file

@ -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<Intent>()
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<Intent>()
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<Intent> {
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<Intent>()
every { PendingIntentHandler.retrieveBeginGetCredentialRequest(intent) } returns mockk {
every { beginGetCredentialOptions } returns listOf(mockk<BeginGetPasswordOption>())
}
val result = intent.getFido2GetCredentialsRequestOrNull()
assertNull(result)
}
@Test
fun `getFido2GetCredentialRequestOrNull should return null when calling app info is null`() {
val intent = mockk<Intent>()
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),
)

View file

@ -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<Fido2GetCredentialResult>()
val mockGetCredentialResult = mockk<Fido2GetCredentialsResult>()
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())
}

View file

@ -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<VaultItemListingEvent>()
@ -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 {

View file

@ -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 {