[PM-9409] Define FIDO 2 assertion Special Circumstance (#3612)

This commit is contained in:
Patrick Honkonen 2024-07-24 16:01:22 -04:00 committed by GitHub
parent b48837e13c
commit da3d834a91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 584 additions and 7 deletions

View file

@ -8,6 +8,7 @@ import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
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.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
@ -170,6 +171,7 @@ class MainViewModel @Inject constructor(
)
}
@Suppress("LongMethod")
private fun handleIntent(
intent: Intent,
isFirstIntent: Boolean,
@ -181,6 +183,7 @@ class MainViewModel @Inject constructor(
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
val hasVaultShortcut = intent.isMyVaultShortcut
val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull()
val fido2CredentialAssertionRequest = intent.getFido2AssertionRequestOrNull()
when {
passwordlessRequestData != null -> {
specialCircumstanceManager.specialCircumstance =
@ -237,6 +240,13 @@ class MainViewModel @Inject constructor(
}
}
fido2CredentialAssertionRequest != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Assertion(
fido2AssertionRequest = fido2CredentialAssertionRequest,
)
}
hasGeneratorShortcut -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.GeneratorShortcut

View file

@ -0,0 +1,23 @@
package com.x8bit.bitwarden.data.autofill.fido2.model
import android.content.pm.SigningInfo
import android.os.Parcelable
import androidx.credentials.provider.CallingAppInfo
import kotlinx.parcelize.Parcelize
/**
* Models a FIDO 2 credential authentication request parsed from the launching intent.
*/
@Parcelize
data class Fido2CredentialAssertionRequest(
val cipherId: String?,
val credentialId: String?,
val requestJson: String,
val clientDataHash: ByteArray?,
val packageName: String,
val signingInfo: SigningInfo,
val origin: String?,
) : Parcelable {
val callingAppInfo: CallingAppInfo
get() = CallingAppInfo(packageName, signingInfo, origin)
}

View file

@ -3,26 +3,30 @@ package com.x8bit.bitwarden.data.autofill.fido2.util
import android.content.Intent
import android.os.Build
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.GetPublicKeyCredentialOption
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.platform.annotation.OmitFromCoverage
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
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_USER_ID
/**
* Checks if this [Intent] contains a [Fido2CredentialRequest] related to an ongoing FIDO 2
* credential creation process.
*/
@OmitFromCoverage
fun Intent.getFido2CredentialRequestOrNull(): Fido2CredentialRequest? {
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
val systemRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(this)
val systemRequest = PendingIntentHandler
.retrieveProviderCreateCredentialRequest(this)
?: return null
val createPublicKeyRequest =
systemRequest.callingRequest as? CreatePublicKeyCredentialRequest
?: return null
val createPublicKeyRequest = systemRequest
.callingRequest
as? CreatePublicKeyCredentialRequest
?: return null
val userId = getStringExtra(EXTRA_KEY_USER_ID)
?: return null
@ -35,3 +39,36 @@ fun Intent.getFido2CredentialRequestOrNull(): Fido2CredentialRequest? {
origin = systemRequest.callingAppInfo.origin,
)
}
/**
* Checks if this [Intent] contains a [Fido2CredentialAssertionRequest] related to an ongoing FIDO 2
* credential authentication process.
*/
fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
val systemRequest = PendingIntentHandler
.retrieveProviderGetCredentialRequest(this)
?: return null
val option: GetPublicKeyCredentialOption = systemRequest
.credentialOptions
.firstNotNullOfOrNull { it as? GetPublicKeyCredentialOption }
?: return null
val credentialId = getStringExtra(EXTRA_KEY_CREDENTIAL_ID)
?: return null
val cipherId = getStringExtra(EXTRA_KEY_CIPHER_ID)
?: return null
return Fido2CredentialAssertionRequest(
cipherId = cipherId,
credentialId = credentialId,
requestJson = option.requestJson,
clientDataHash = option.clientDataHash,
packageName = systemRequest.callingAppInfo.packageName,
signingInfo = systemRequest.callingAppInfo.signingInfo,
origin = systemRequest.callingAppInfo.origin,
)
}

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.platform.manager.model
import android.os.Parcelable
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
@ -57,6 +58,15 @@ sealed class SpecialCircumstance : Parcelable {
val fido2CredentialRequest: Fido2CredentialRequest,
) : SpecialCircumstance()
/**
* The app was launched via the credential manager framework in order to authenticate a FIDO 2
* credential saved to the user's vault.
*/
@Parcelize
data class Fido2Assertion(
val fido2AssertionRequest: Fido2CredentialAssertionRequest,
) : SpecialCircumstance()
/**
* The app was launched via deeplink to the generator.
*/

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.manager.util
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
@ -17,6 +18,7 @@ fun SpecialCircumstance.toAutofillSaveItemOrNull(): AutofillSaveItem? =
SpecialCircumstance.GeneratorShortcut -> null
SpecialCircumstance.VaultShortcut -> null
is SpecialCircumstance.Fido2Save -> null
is SpecialCircumstance.Fido2Assertion -> null
}
/**
@ -31,6 +33,7 @@ fun SpecialCircumstance.toAutofillSelectionDataOrNull(): AutofillSelectionData?
SpecialCircumstance.GeneratorShortcut -> null
SpecialCircumstance.VaultShortcut -> null
is SpecialCircumstance.Fido2Save -> null
is SpecialCircumstance.Fido2Assertion -> null
}
/**
@ -41,3 +44,12 @@ fun SpecialCircumstance.toFido2RequestOrNull(): Fido2CredentialRequest? =
is SpecialCircumstance.Fido2Save -> this.fido2CredentialRequest
else -> null
}
/**
* Returns [Fido2CredentialAssertionRequest] when contained in the given [SpecialCircumstance].
*/
fun SpecialCircumstance.toFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? =
when (this) {
is SpecialCircumstance.Fido2Assertion -> this.fido2AssertionRequest
else -> null
}

View file

@ -107,6 +107,7 @@ fun RootNavScreen(
is RootNavState.VaultUnlockedForNewSend,
is RootNavState.VaultUnlockedForAuthRequest,
is RootNavState.VaultUnlockedForFido2Save,
is RootNavState.VaultUnlockedForFido2Assertion,
-> VAULT_UNLOCKED_GRAPH_ROUTE
}
val currentRoute = navController.currentDestination?.rootLevelRoute()
@ -192,6 +193,14 @@ fun RootNavScreen(
navOptions = rootNavOptions,
)
}
is RootNavState.VaultUnlockedForFido2Assertion -> {
navController.navigateToVaultUnlockedGraph(rootNavOptions)
navController.navigateToVaultItemListingAsRoot(
vaultItemListingType = VaultItemListingType.Login,
navOptions = rootNavOptions,
)
}
}
}
}

View file

@ -4,6 +4,7 @@ import android.os.Parcelable
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
@ -55,7 +56,7 @@ class RootNavViewModel @Inject constructor(
authRepository.updateLastActiveTime()
}
@Suppress("CyclomaticComplexMethod")
@Suppress("CyclomaticComplexMethod", "MaxLineLength")
private fun handleUserStateUpdateReceive(
action: RootNavAction.Internal.UserStateUpdateReceive,
) {
@ -101,6 +102,13 @@ class RootNavViewModel @Inject constructor(
)
}
is SpecialCircumstance.Fido2Assertion -> {
RootNavState.VaultUnlockedForFido2Assertion(
activeUserId = userState.activeUserId,
fido2CredentialAssertionRequest = specialCircumstance.fido2AssertionRequest,
)
}
SpecialCircumstance.GeneratorShortcut,
SpecialCircumstance.VaultShortcut,
null,
@ -194,6 +202,15 @@ sealed class RootNavState : Parcelable {
val fido2CredentialRequest: Fido2CredentialRequest,
) : RootNavState()
/**
* App should perform FIDO 2 credential assertion for the user.
*/
@Parcelize
data class VaultUnlockedForFido2Assertion(
val activeUserId: String,
val fido2CredentialAssertionRequest: Fido2CredentialAssertionRequest,
) : RootNavState()
/**
* App should show the new send screen for an unlocked user.
*/

View file

@ -10,9 +10,12 @@ import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
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.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.util.getFido2AssertionRequestOrNull
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2CredentialRequestOrNull
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl
@ -470,6 +473,27 @@ class MainViewModelTest : BaseViewModelTest() {
verify(exactly = 0) { authRepository.switchAccount(fido2CredentialRequest.userId) }
}
@Suppress("MaxLineLength")
@Test
fun `on ReceiveFirstIntent with FIDO 2 assertion request data should set the special circumstance to Fido2Assertion`() {
val viewModel = createViewModel()
val mockAssertionRequest = createMockFido2CredentialAssertionRequest(number = 1)
val fido2AssertionIntent = createMockFido2AssertionIntent(mockAssertionRequest)
every { intentManager.getShareDataFromIntent(fido2AssertionIntent) } returns null
viewModel.trySendAction(
MainAction.ReceiveFirstIntent(
intent = fido2AssertionIntent,
),
)
assertEquals(
SpecialCircumstance.Fido2Assertion(mockAssertionRequest),
specialCircumstanceManager.specialCircumstance,
)
}
@Suppress("MaxLineLength")
@Test
fun `on ReceiveNewIntent with share data should set the special circumstance to ShareNewSend`() {
@ -695,3 +719,15 @@ private fun createMockFido2RegistrationIntent(
every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false
}
private fun createMockFido2AssertionIntent(
fido2CredentialAssertionRequest: Fido2CredentialAssertionRequest =
createMockFido2CredentialAssertionRequest(number = 1),
): Intent = mockk<Intent> {
every { getFido2AssertionRequestOrNull() } returns fido2CredentialAssertionRequest
every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSelectionDataOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null
every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false
}

View file

@ -0,0 +1,14 @@
package com.x8bit.bitwarden.data.autofill.fido2.model
import android.content.pm.SigningInfo
fun createMockFido2CredentialAssertionRequest(number: Int = 1): Fido2CredentialAssertionRequest =
Fido2CredentialAssertionRequest(
cipherId = "mockCipherId-$number",
credentialId = "mockCredentialId-$number",
requestJson = "mockRequestJson-$number",
clientDataHash = byteArrayOf(0),
packageName = "mockPackageName-$number",
signingInfo = SigningInfo(),
origin = "mockOrigin-$number",
)

View file

@ -0,0 +1,288 @@
package com.x8bit.bitwarden.data.autofill.fido2.util
import android.content.Intent
import android.content.pm.SigningInfo
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.GetPasswordOption
import androidx.credentials.GetPublicKeyCredentialOption
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.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
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_USER_ID
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.unmockkObject
import io.mockk.unmockkStatic
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class Fido2IntentUtilsTest {
@BeforeEach
fun setUp() {
mockkStatic(::isBuildVersionBelow)
mockkObject(PendingIntentHandler.Companion)
every { isBuildVersionBelow(any()) } returns false
}
@AfterEach
fun tearDown() {
unmockkStatic(::isBuildVersionBelow)
unmockkObject(PendingIntentHandler.Companion)
}
@Test
fun `getFido2CredentialRequestOrNull should return Fido2CredentialRequest when present`() {
val intent = mockk<Intent> {
every { getStringExtra(EXTRA_KEY_USER_ID) } returns "mockUserId"
}
val mockCallingRequest = mockk<CreatePublicKeyCredentialRequest> {
every { requestJson } returns "requestJson"
every { clientDataHash } returns byteArrayOf(0)
every { preferImmediatelyAvailableCredentials } returns false
every { origin } returns "mockOrigin"
every { isAutoSelectAllowed } returns true
}
val mockCallingAppInfo = CallingAppInfo(
packageName = "mockPackageName",
signingInfo = SigningInfo(),
origin = "mockOrigin",
)
val mockProviderRequest = ProviderCreateCredentialRequest(
callingRequest = mockCallingRequest,
callingAppInfo = mockCallingAppInfo,
)
every {
PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
} returns mockProviderRequest
val createRequest = intent.getFido2CredentialRequestOrNull()
assertEquals(
Fido2CredentialRequest(
userId = "mockUserId",
requestJson = mockCallingRequest.requestJson,
packageName = mockCallingAppInfo.packageName,
signingInfo = mockCallingAppInfo.signingInfo,
origin = mockCallingAppInfo.origin,
),
createRequest,
)
}
@Test
fun `getFido2CredentialRequestOrNull should return null when build version is below 34`() {
val intent = mockk<Intent>()
every { isBuildVersionBelow(34) } returns true
assertNull(intent.getFido2CredentialRequestOrNull())
}
@Suppress("MaxLineLength")
@Test
fun `getFido2CredentialRequestOrNull should return null when intent is not a provider create credential request`() {
val intent = mockk<Intent>()
every {
PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
} returns null
assertNull(intent.getFido2CredentialRequestOrNull())
}
@Suppress("MaxLineLength")
@Test
fun `getFido2CredentialRequestOrNull should return null when calling request is not a public key credential create request`() {
val intent = mockk<Intent>()
val mockCallingRequest = mockk<CreatePasswordRequest>()
val mockCallingAppInfo = CallingAppInfo(
packageName = "mockPackageName",
signingInfo = SigningInfo(),
origin = "mockOrigin",
)
val mockProviderRequest = ProviderCreateCredentialRequest(
callingRequest = mockCallingRequest,
callingAppInfo = mockCallingAppInfo,
)
every {
PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
} returns mockProviderRequest
assertNull(intent.getFido2CredentialRequestOrNull())
}
@Suppress("MaxLineLength")
@Test
fun `getFido2CredentialRequestOrNull should return null when user id is not present in extras`() {
val intent = mockk<Intent> {
every { getStringExtra(EXTRA_KEY_USER_ID) } returns null
}
val mockCallingRequest = mockk<CreatePublicKeyCredentialRequest> {
every { requestJson } returns "requestJson"
every { clientDataHash } returns byteArrayOf(0)
every { preferImmediatelyAvailableCredentials } returns false
every { origin } returns "mockOrigin"
every { isAutoSelectAllowed } returns true
}
val mockCallingAppInfo = CallingAppInfo(
packageName = "mockPackageName",
signingInfo = SigningInfo(),
origin = "mockOrigin",
)
val mockProviderRequest = ProviderCreateCredentialRequest(
callingRequest = mockCallingRequest,
callingAppInfo = mockCallingAppInfo,
)
every {
PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
} returns mockProviderRequest
assertNull(intent.getFido2CredentialRequestOrNull())
}
@Test
fun `getFido2AssertionRequestOrNull should return Fido2AssertionRequest when present`() {
val intent = mockk<Intent> {
every { getStringExtra(EXTRA_KEY_CIPHER_ID) } returns "mockCipherId"
every { getStringExtra(EXTRA_KEY_CREDENTIAL_ID) } returns "mockCredentialId"
}
val mockOption = GetPublicKeyCredentialOption(
requestJson = "requestJson",
clientDataHash = byteArrayOf(0),
allowedProviders = emptySet(),
)
val mockCallingAppInfo = CallingAppInfo(
packageName = "mockPackageName",
signingInfo = SigningInfo(),
origin = "mockOrigin",
)
val mockProviderGetCredentialRequest = ProviderGetCredentialRequest(
credentialOptions = listOf(mockOption),
callingAppInfo = mockCallingAppInfo,
)
every {
PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
} returns mockProviderGetCredentialRequest
val assertionRequest = intent.getFido2AssertionRequestOrNull()
assertNotNull(assertionRequest)
assertEquals(
Fido2CredentialAssertionRequest(
cipherId = "mockCipherId",
credentialId = "mockCredentialId",
requestJson = mockOption.requestJson,
clientDataHash = mockOption.clientDataHash,
packageName = mockCallingAppInfo.packageName,
signingInfo = mockCallingAppInfo.signingInfo,
origin = mockCallingAppInfo.origin,
),
assertionRequest,
)
}
@Test
fun `getFido2AssertionRequestOrNull should return null when build version is below 34`() {
val intent = mockk<Intent>()
every { isBuildVersionBelow(34) } returns true
val assertionRequest = intent.getFido2AssertionRequestOrNull()
assertNull(assertionRequest)
}
@Suppress("MaxLineLength")
@Test
fun `getFido2AssertionRequestOrNull should return null when retrieveProviderGetCredentialRequest is null`() {
val intent = mockk<Intent>()
every {
PendingIntentHandler.retrieveProviderGetCredentialRequest(any())
} returns null
val assertionRequest = intent.getFido2AssertionRequestOrNull()
assertNull(assertionRequest)
}
@Suppress("MaxLineLength")
@Test
fun `getFido2AssertionRequestOrNull should return null when no passkey credential options are present in request`() {
val intent = mockk<Intent>()
val mockProviderGetCredentialRequest = ProviderGetCredentialRequest(
credentialOptions = listOf(GetPasswordOption()),
callingAppInfo = mockk(),
)
every {
PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
} returns mockProviderGetCredentialRequest
val assertionRequest = intent.getFido2AssertionRequestOrNull()
assertNull(assertionRequest)
}
@Test
fun `getFido2AssertionRequestOrNull should return null when credential id is not in extras`() {
val intent = mockk<Intent> {
every { getStringExtra(EXTRA_KEY_CREDENTIAL_ID) } returns null
}
val mockOption = GetPublicKeyCredentialOption(
requestJson = "requestJson",
clientDataHash = byteArrayOf(0),
allowedProviders = emptySet(),
)
val mockProviderGetCredentialRequest = ProviderGetCredentialRequest(
credentialOptions = listOf(mockOption),
callingAppInfo = mockk(),
)
every {
PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
} returns mockProviderGetCredentialRequest
val assertionRequest = intent.getFido2AssertionRequestOrNull()
assertNull(assertionRequest)
}
@Test
fun `getFido2AssertionRequestOrNull should return null when cipher id is not in extras`() {
val intent = mockk<Intent> {
every { getStringExtra(EXTRA_KEY_CREDENTIAL_ID) } returns "mockCredentialId"
every { getStringExtra(EXTRA_KEY_CIPHER_ID) } returns null
}
val mockOption = GetPublicKeyCredentialOption(
requestJson = "requestJson",
clientDataHash = byteArrayOf(0),
allowedProviders = emptySet(),
)
val mockProviderGetCredentialRequest = ProviderGetCredentialRequest(
credentialOptions = listOf(mockOption),
callingAppInfo = mockk(),
)
every {
PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
} returns mockProviderGetCredentialRequest
val assertionRequest = intent.getFido2AssertionRequestOrNull()
assertNull(assertionRequest)
}
}

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.manager.util
import android.content.pm.SigningInfo
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
@ -43,6 +44,9 @@ class SpecialCircumstanceExtensionsTest {
SpecialCircumstance.Fido2Save(
fido2CredentialRequest = mockk(),
),
SpecialCircumstance.Fido2Assertion(
fido2AssertionRequest = mockk(),
),
SpecialCircumstance.GeneratorShortcut,
SpecialCircumstance.VaultShortcut,
)
@ -85,6 +89,9 @@ class SpecialCircumstanceExtensionsTest {
SpecialCircumstance.Fido2Save(
fido2CredentialRequest = mockk(),
),
SpecialCircumstance.Fido2Assertion(
fido2AssertionRequest = mockk(),
),
SpecialCircumstance.GeneratorShortcut,
SpecialCircumstance.VaultShortcut,
)
@ -111,6 +118,9 @@ class SpecialCircumstanceExtensionsTest {
passwordlessRequestData = mockk(),
shouldFinishWhenComplete = true,
),
SpecialCircumstance.Fido2Assertion(
fido2AssertionRequest = mockk(),
),
SpecialCircumstance.GeneratorShortcut,
SpecialCircumstance.VaultShortcut,
)
@ -137,4 +147,48 @@ class SpecialCircumstanceExtensionsTest {
.toFido2RequestOrNull(),
)
}
@Test
fun `toFido2AssertionRequestOrNull should return a non-null value for Fido2Assertion`() {
val fido2CredentialAssertionRequest =
createMockFido2CredentialAssertionRequest(number = 1)
assertEquals(
fido2CredentialAssertionRequest,
SpecialCircumstance
.Fido2Assertion(
fido2AssertionRequest = fido2CredentialAssertionRequest,
)
.toFido2AssertionRequestOrNull(),
)
}
@Test
fun `toFido2AssertionRequestOrNull should return a null value for other types`() {
listOf(
SpecialCircumstance.AutofillSelection(
autofillSelectionData = mockk(),
shouldFinishWhenComplete = true,
),
SpecialCircumstance.AutofillSave(
autofillSaveItem = mockk(),
),
SpecialCircumstance.ShareNewSend(
data = mockk(),
shouldFinishWhenComplete = true,
),
SpecialCircumstance.PasswordlessRequest(
passwordlessRequestData = mockk(),
shouldFinishWhenComplete = true,
),
SpecialCircumstance.Fido2Save(
fido2CredentialRequest = mockk(),
),
SpecialCircumstance.GeneratorShortcut,
SpecialCircumstance.VaultShortcut,
)
.forEach { specialCircumstance ->
assertNull(specialCircumstance.toFido2AssertionRequestOrNull())
}
}
}

View file

@ -151,5 +151,32 @@ class RootNavScreenTest : BaseComposeTest() {
navOptions = expectedNavOptions,
)
}
// Make sure navigating to vault unlocked for Fido2Save works as expected:
rootNavStateFlow.value =
RootNavState.VaultUnlockedForFido2Save(
activeUserId = "activeUserId",
fido2CredentialRequest = mockk(),
)
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "vault_item_listing_as_root/login",
navOptions = expectedNavOptions,
)
}
// Make sure navigating to vault unlocked for Fido2Assertion works as expected:
rootNavStateFlow.value =
RootNavState.VaultUnlockedForFido2Assertion(
activeUserId = "activeUserId",
fido2CredentialAssertionRequest = mockk(),
)
composeTestRule
.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "vault_item_listing_as_root/login",
navOptions = expectedNavOptions,
)
}
}
}

View file

@ -4,6 +4,7 @@ import android.content.pm.SigningInfo
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
@ -450,6 +451,45 @@ class RootNavViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `when the active user has an unlocked vault but there is a Fido2Assertion special circumstance the nav state should be VaultUnlockedForFido2Save`() {
val fido2CredentialAssertionRequest =
createMockFido2CredentialAssertionRequest(number = 1)
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Assertion(fido2CredentialAssertionRequest)
mutableUserStateFlow.tryEmit(
UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "name",
email = "email",
avatarColorHex = "avatarHexColor",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
isBiometricsEnabled = false,
organizations = emptyList(),
needsMasterPassword = false,
trustedDevice = null,
),
),
),
)
val viewModel = createViewModel()
assertEquals(
RootNavState.VaultUnlockedForFido2Assertion(
activeUserId = "activeUserId",
fido2CredentialAssertionRequest = fido2CredentialAssertionRequest,
),
viewModel.stateFlow.value,
)
}
@Test
fun `when the active user has a locked vault the nav state should be VaultLocked`() {
mutableUserStateFlow.tryEmit(