mirror of
https://github.com/bitwarden/android.git
synced 2025-03-16 03:08:50 +03:00
[PM-9409] Define FIDO 2 assertion Special Circumstance (#3612)
This commit is contained in:
parent
b48837e13c
commit
da3d834a91
13 changed files with 584 additions and 7 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Reference in a new issue