mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
[PM-9410] Introduce FIDO 2 Get Credentials Request special circumstance (#3637)
This commit is contained in:
parent
39250e5cb4
commit
b0079fca5c
9 changed files with 208 additions and 9 deletions
|
@ -0,0 +1,33 @@
|
|||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import android.content.pm.SigningInfo
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Models a FIDO 2 request to retrieve FIDO credentials parsed from the launching intent.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Fido2GetCredentialsRequest(
|
||||
val candidateQueryData: Bundle,
|
||||
val id: String,
|
||||
val requestJson: String,
|
||||
val clientDataHash: ByteArray? = null,
|
||||
val packageName: String,
|
||||
val signingInfo: SigningInfo,
|
||||
val origin: String?,
|
||||
) : Parcelable {
|
||||
val callingAppInfo: CallingAppInfo
|
||||
get() = CallingAppInfo(packageName, signingInfo, origin)
|
||||
|
||||
val getCredentialsRequest: BeginGetPublicKeyCredentialOption
|
||||
get() = BeginGetPublicKeyCredentialOption(
|
||||
candidateQueryData,
|
||||
id,
|
||||
requestJson,
|
||||
clientDataHash,
|
||||
)
|
||||
}
|
|
@ -3,6 +3,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.fido2.model.Fido2GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
|
@ -67,6 +68,15 @@ sealed class SpecialCircumstance : Parcelable {
|
|||
val fido2AssertionRequest: Fido2CredentialAssertionRequest,
|
||||
) : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched via the credential manager framework request to retrieve passkeys
|
||||
* associated with the requesting entity.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Fido2GetCredentials(
|
||||
val fido2GetCredentialsRequest: Fido2GetCredentialsRequest,
|
||||
) : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched via deeplink to the generator.
|
||||
*/
|
||||
|
|
|
@ -2,6 +2,7 @@ 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.fido2.model.Fido2GetCredentialsRequest
|
||||
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
|
||||
|
@ -19,6 +20,7 @@ fun SpecialCircumstance.toAutofillSaveItemOrNull(): AutofillSaveItem? =
|
|||
SpecialCircumstance.VaultShortcut -> null
|
||||
is SpecialCircumstance.Fido2Save -> null
|
||||
is SpecialCircumstance.Fido2Assertion -> null
|
||||
is SpecialCircumstance.Fido2GetCredentials -> null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -34,6 +36,7 @@ fun SpecialCircumstance.toAutofillSelectionDataOrNull(): AutofillSelectionData?
|
|||
SpecialCircumstance.VaultShortcut -> null
|
||||
is SpecialCircumstance.Fido2Save -> null
|
||||
is SpecialCircumstance.Fido2Assertion -> null
|
||||
is SpecialCircumstance.Fido2GetCredentials -> null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -53,3 +56,12 @@ fun SpecialCircumstance.toFido2AssertionRequestOrNull(): Fido2CredentialAssertio
|
|||
is SpecialCircumstance.Fido2Assertion -> this.fido2AssertionRequest
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [Fido2CredentialAssertionRequest] when contained in the given [SpecialCircumstance].
|
||||
*/
|
||||
fun SpecialCircumstance.toFido2GetCredentialsRequestOrNull(): Fido2GetCredentialsRequest? =
|
||||
when (this) {
|
||||
is SpecialCircumstance.Fido2GetCredentials -> this.fido2GetCredentialsRequest
|
||||
else -> null
|
||||
}
|
||||
|
|
|
@ -108,6 +108,7 @@ fun RootNavScreen(
|
|||
is RootNavState.VaultUnlockedForAuthRequest,
|
||||
is RootNavState.VaultUnlockedForFido2Save,
|
||||
is RootNavState.VaultUnlockedForFido2Assertion,
|
||||
is RootNavState.VaultUnlockedForFido2GetCredentials,
|
||||
-> VAULT_UNLOCKED_GRAPH_ROUTE
|
||||
}
|
||||
val currentRoute = navController.currentDestination?.rootLevelRoute()
|
||||
|
@ -186,15 +187,10 @@ fun RootNavScreen(
|
|||
)
|
||||
}
|
||||
|
||||
is RootNavState.VaultUnlockedForFido2Save -> {
|
||||
navController.navigateToVaultUnlockedGraph(rootNavOptions)
|
||||
navController.navigateToVaultItemListingAsRoot(
|
||||
vaultItemListingType = VaultItemListingType.Login,
|
||||
navOptions = rootNavOptions,
|
||||
)
|
||||
}
|
||||
|
||||
is RootNavState.VaultUnlockedForFido2Assertion -> {
|
||||
is RootNavState.VaultUnlockedForFido2Save,
|
||||
is RootNavState.VaultUnlockedForFido2Assertion,
|
||||
is RootNavState.VaultUnlockedForFido2GetCredentials,
|
||||
-> {
|
||||
navController.navigateToVaultUnlockedGraph(rootNavOptions)
|
||||
navController.navigateToVaultItemListingAsRoot(
|
||||
vaultItemListingType = VaultItemListingType.Login,
|
||||
|
|
|
@ -6,6 +6,7 @@ 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.fido2.model.Fido2GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
|
@ -109,6 +110,13 @@ class RootNavViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
is SpecialCircumstance.Fido2GetCredentials -> {
|
||||
RootNavState.VaultUnlockedForFido2GetCredentials(
|
||||
activeUserId = userState.activeUserId,
|
||||
fido2GetCredentialsRequest = specialCircumstance.fido2GetCredentialsRequest,
|
||||
)
|
||||
}
|
||||
|
||||
SpecialCircumstance.GeneratorShortcut,
|
||||
SpecialCircumstance.VaultShortcut,
|
||||
null,
|
||||
|
@ -211,6 +219,16 @@ sealed class RootNavState : Parcelable {
|
|||
val fido2CredentialAssertionRequest: Fido2CredentialAssertionRequest,
|
||||
) : RootNavState()
|
||||
|
||||
/**
|
||||
* App should unlock the user's vault and retrieve FIDO 2 credentials associated to the relying
|
||||
* party.
|
||||
*/
|
||||
@Parcelize
|
||||
data class VaultUnlockedForFido2GetCredentials(
|
||||
val activeUserId: String,
|
||||
val fido2GetCredentialsRequest: Fido2GetCredentialsRequest,
|
||||
) : RootNavState()
|
||||
|
||||
/**
|
||||
* App should show the new send screen for an unlocked user.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import android.content.pm.SigningInfo
|
||||
import android.os.Bundle
|
||||
|
||||
fun createMockFido2GetCredentialsRequest(
|
||||
number: Int,
|
||||
signingInfo: SigningInfo = SigningInfo(),
|
||||
origin: String? = null,
|
||||
): Fido2GetCredentialsRequest = Fido2GetCredentialsRequest(
|
||||
candidateQueryData = Bundle(),
|
||||
id = "mockId-$number",
|
||||
requestJson = "requestJson-$number",
|
||||
clientDataHash = null,
|
||||
packageName = "mockPackageName-$number",
|
||||
signingInfo = signingInfo,
|
||||
origin = origin,
|
||||
)
|
|
@ -3,6 +3,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.fido2.model.createMockFido2GetCredentialsRequest
|
||||
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
|
||||
|
@ -47,6 +48,9 @@ class SpecialCircumstanceExtensionsTest {
|
|||
SpecialCircumstance.Fido2Assertion(
|
||||
fido2AssertionRequest = mockk(),
|
||||
),
|
||||
SpecialCircumstance.Fido2GetCredentials(
|
||||
fido2GetCredentialsRequest = mockk(),
|
||||
),
|
||||
SpecialCircumstance.GeneratorShortcut,
|
||||
SpecialCircumstance.VaultShortcut,
|
||||
)
|
||||
|
@ -92,6 +96,9 @@ class SpecialCircumstanceExtensionsTest {
|
|||
SpecialCircumstance.Fido2Assertion(
|
||||
fido2AssertionRequest = mockk(),
|
||||
),
|
||||
SpecialCircumstance.Fido2GetCredentials(
|
||||
fido2GetCredentialsRequest = mockk(),
|
||||
),
|
||||
SpecialCircumstance.GeneratorShortcut,
|
||||
SpecialCircumstance.VaultShortcut,
|
||||
)
|
||||
|
@ -121,6 +128,9 @@ class SpecialCircumstanceExtensionsTest {
|
|||
SpecialCircumstance.Fido2Assertion(
|
||||
fido2AssertionRequest = mockk(),
|
||||
),
|
||||
SpecialCircumstance.Fido2GetCredentials(
|
||||
fido2GetCredentialsRequest = mockk(),
|
||||
),
|
||||
SpecialCircumstance.GeneratorShortcut,
|
||||
SpecialCircumstance.VaultShortcut,
|
||||
)
|
||||
|
@ -184,6 +194,9 @@ class SpecialCircumstanceExtensionsTest {
|
|||
SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = mockk(),
|
||||
),
|
||||
SpecialCircumstance.Fido2GetCredentials(
|
||||
fido2GetCredentialsRequest = mockk(),
|
||||
),
|
||||
SpecialCircumstance.GeneratorShortcut,
|
||||
SpecialCircumstance.VaultShortcut,
|
||||
)
|
||||
|
@ -191,4 +204,50 @@ class SpecialCircumstanceExtensionsTest {
|
|||
assertNull(specialCircumstance.toFido2AssertionRequestOrNull())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toFido2GetCredentialsRequestOrNull should return a non-null value for Fido2GetCredentials`() {
|
||||
val fido2GetCredentialsRequest = createMockFido2GetCredentialsRequest(number = 1)
|
||||
assertEquals(
|
||||
fido2GetCredentialsRequest,
|
||||
SpecialCircumstance
|
||||
.Fido2GetCredentials(
|
||||
fido2GetCredentialsRequest = fido2GetCredentialsRequest,
|
||||
)
|
||||
.toFido2GetCredentialsRequestOrNull(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toFido2GetCredentialsRequestOrNull 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.Fido2Assertion(
|
||||
fido2AssertionRequest = mockk(),
|
||||
),
|
||||
SpecialCircumstance.GeneratorShortcut,
|
||||
SpecialCircumstance.VaultShortcut,
|
||||
)
|
||||
.forEach { specialCircumstance ->
|
||||
assertNull(specialCircumstance.toFido2GetCredentialsRequestOrNull())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -178,5 +178,19 @@ class RootNavScreenTest : BaseComposeTest() {
|
|||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
|
||||
// Make sure navigating to vault unlocked for Fido2GetCredentials works as expected:
|
||||
rootNavStateFlow.value =
|
||||
RootNavState.VaultUnlockedForFido2GetCredentials(
|
||||
activeUserId = "activeUserId",
|
||||
fido2GetCredentialsRequest = mockk(),
|
||||
)
|
||||
composeTestRule
|
||||
.runOnIdle {
|
||||
fakeNavHostController.assertLastNavigation(
|
||||
route = "vault_item_listing_as_root/login",
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ 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.fido2.model.createMockFido2GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
||||
|
@ -490,6 +491,44 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when the active user has an unlocked vault but there is a Fido2GetCredentials special circumstance the nav state should be VaultUnlockedForFido2GetCredentials`() {
|
||||
val fido2GetCredentialsRequest = createMockFido2GetCredentialsRequest(number = 1)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2GetCredentials(fido2GetCredentialsRequest)
|
||||
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.VaultUnlockedForFido2GetCredentials(
|
||||
activeUserId = "activeUserId",
|
||||
fido2GetCredentialsRequest = fido2GetCredentialsRequest,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when the active user has a locked vault the nav state should be VaultLocked`() {
|
||||
mutableUserStateFlow.tryEmit(
|
||||
|
|
Loading…
Reference in a new issue