mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +03:00
[PM-104678] (WIP) Refactor passkey part 3 - Authenticate and select passkeys on vault unlock
This commit introduces authentication and selection of passkeys when unlocking the vault. The Fido2Screen is now used to handle this process. The Fido2CredentialStore now stores FIDO credentials and silently discovers them for use during authentication. This commit also modifies the behavior of processGetCredentialRequest to filter and discover passkeys for autofill purposes, and introduces functionality to authenticate and select passkeys in the vault unlocking flow, and ensures that the user's email address is displayed on the UI during selection of a passkey for autofill.
This commit is contained in:
parent
8fbb4b7af4
commit
222ac2f24e
4 changed files with 404 additions and 182 deletions
|
@ -0,0 +1,315 @@
|
|||
package com.x8bit.bitwarden.ui.autofill.fido2
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.toFido2GetCredentialsRequestOrNull
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.BackgroundEvent
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Models logic for [Fido2Screen].
|
||||
*/
|
||||
@Suppress("LargeClass")
|
||||
@HiltViewModel
|
||||
class Fido2ViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
vaultRepository: VaultRepository,
|
||||
private val fido2CredentialManager: Fido2CredentialManager,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
) : BaseViewModel<Fido2State, Fido2Event, Fido2Action>(
|
||||
initialState = run {
|
||||
val specialCircumstance = specialCircumstanceManager.specialCircumstance
|
||||
val fido2GetCredentialsRequest = specialCircumstance?.toFido2GetCredentialsRequestOrNull()
|
||||
val requestUserId = fido2GetCredentialsRequest?.userId
|
||||
Fido2State(
|
||||
requestUserId = requireNotNull(requestUserId),
|
||||
fido2GetCredentialsRequest = fido2GetCredentialsRequest,
|
||||
dialog = null,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
init {
|
||||
vaultRepository.vaultDataStateFlow
|
||||
.map { Fido2Action.Internal.VaultDataStateChangeReceive(it) }
|
||||
.onEach(::trySendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
authRepository.userStateFlow
|
||||
.map { Fido2Action.Internal.UserStateChangeReceive(it) }
|
||||
.onEach(::trySendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: Fido2Action) {
|
||||
when (action) {
|
||||
is Fido2Action.DismissErrorDialogClick -> {
|
||||
clearDialogState()
|
||||
sendEvent(
|
||||
Fido2Event.CompleteFido2GetCredentialsRequest(
|
||||
result = action.result,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is Fido2Action.Internal -> {
|
||||
handleInternalAction(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInternalAction(action: Fido2Action.Internal) {
|
||||
when (action) {
|
||||
is Fido2Action.Internal.VaultDataStateChangeReceive -> {
|
||||
handleVaultDataStateChangeReceive(action.vaultData)
|
||||
}
|
||||
|
||||
is Fido2Action.Internal.UserStateChangeReceive -> {
|
||||
handleUserStateChangeReceive(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUserStateChangeReceive(action: Fido2Action.Internal.UserStateChangeReceive) {
|
||||
val activeUserId = action.userState?.activeUserId ?: return
|
||||
val requestUserId = state.requestUserId
|
||||
if (requestUserId != activeUserId) {
|
||||
authRepository.switchAccount(requestUserId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleVaultDataStateChangeReceive(vaultDataState: DataState<VaultData>) {
|
||||
when (vaultDataState) {
|
||||
is DataState.Error -> mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = Fido2State.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is DataState.Loaded -> handleVaultDataLoaded()
|
||||
DataState.Loading -> handleVaultDataLoading()
|
||||
is DataState.NoNetwork -> handleNoNetwork()
|
||||
is DataState.Pending -> clearDialogState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleVaultDataLoaded() {
|
||||
clearDialogState()
|
||||
if (authRepository.activeUserId != state.requestUserId) {
|
||||
// Ignore vault data when we are waiting for the account to switch
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
state
|
||||
.fido2GetCredentialsRequest
|
||||
?.let { getCredentialsRequest ->
|
||||
getFido2CredentialAutofillViewsForSelection(getCredentialsRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getFido2CredentialAutofillViewsForSelection(
|
||||
fido2GetCredentialsRequest: Fido2GetCredentialsRequest,
|
||||
) {
|
||||
val getCredentialsResult = fido2CredentialManager
|
||||
.getFido2CredentialsForRelyingParty(fido2GetCredentialsRequest)
|
||||
|
||||
when (getCredentialsResult) {
|
||||
is Fido2GetCredentialsResult.Error -> {
|
||||
showFido2ErrorDialog(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.passkey_operation_failed_because_passkey_does_not_exist
|
||||
.asText(),
|
||||
)
|
||||
}
|
||||
|
||||
Fido2GetCredentialsResult.Cancelled,
|
||||
is Fido2GetCredentialsResult.Success,
|
||||
-> {
|
||||
sendEvent(
|
||||
Fido2Event.CompleteFido2GetCredentialsRequest(
|
||||
result = getCredentialsResult,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sendEvent(
|
||||
Fido2Event.CompleteFido2GetCredentialsRequest(
|
||||
result = getCredentialsResult,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleVaultDataLoading() {
|
||||
mutableStateFlow.update { it.copy(dialog = Fido2State.DialogState.Loading) }
|
||||
}
|
||||
|
||||
private fun handleNoNetwork() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = Fido2State.DialogState.Error(
|
||||
R.string.internet_connection_required_title.asText(),
|
||||
R.string.internet_connection_required_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFido2ErrorDialog(title: Text, message: Text) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = Fido2State.DialogState.Error(title, message),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearDialogState() {
|
||||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the UI state for [Fido2Screen].
|
||||
*
|
||||
* @property requestUserId User ID contained within the FIDO 2 request.
|
||||
* @property fido2GetCredentialsRequest Data required to discover FIDO 2 credential.
|
||||
*/
|
||||
data class Fido2State(
|
||||
val requestUserId: String,
|
||||
val fido2GetCredentialsRequest: Fido2GetCredentialsRequest?,
|
||||
val dialog: DialogState?,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Represents the dialog UI state for [Fido2Screen].
|
||||
*/
|
||||
sealed class DialogState {
|
||||
/**
|
||||
* Displays a loading dialog.
|
||||
*/
|
||||
data object Loading : DialogState()
|
||||
|
||||
/**
|
||||
* Displays a generic error dialog with a [title] and [message].
|
||||
*/
|
||||
data class Error(val title: Text, val message: Text) : DialogState()
|
||||
|
||||
/**
|
||||
* Displays a PIN entry dialog to verify the user.
|
||||
*/
|
||||
data class Fido2PinPrompt(val selectedCipherId: String) : DialogState()
|
||||
|
||||
/**
|
||||
* Displays a master password entry dialog to verify the user.
|
||||
*/
|
||||
data class Fido2MasterPasswordPrompt(val selectedCipherId: String) : DialogState()
|
||||
|
||||
/**
|
||||
* Displays a PIN creation dialog for user verification.
|
||||
*/
|
||||
data class Fido2PinSetUpPrompt(val selectedCipherId: String) : DialogState()
|
||||
|
||||
/**
|
||||
* Displays a master password validation error dialog.
|
||||
*/
|
||||
data class Fido2MasterPasswordError(
|
||||
val title: Text?,
|
||||
val message: Text,
|
||||
val selectedCipherId: String,
|
||||
) : DialogState()
|
||||
|
||||
/**
|
||||
* Displays a PIN set up error dialog.
|
||||
*/
|
||||
data class Fido2PinSetUpError(
|
||||
val title: Text?,
|
||||
val message: Text,
|
||||
val selectedCipherId: String,
|
||||
) : DialogState()
|
||||
|
||||
/**
|
||||
* Displays a PIN validation error dialog.
|
||||
*/
|
||||
data class Fido2PinError(
|
||||
val title: Text?,
|
||||
val message: Text,
|
||||
val selectedCipherId: String,
|
||||
) : DialogState()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models events for [Fido2Screen].
|
||||
*/
|
||||
sealed class Fido2Event {
|
||||
|
||||
/**
|
||||
* Completes FIDO 2 credential discovery with the given [result].
|
||||
*/
|
||||
data class CompleteFido2GetCredentialsRequest(
|
||||
val result: Fido2GetCredentialsResult,
|
||||
) : BackgroundEvent, Fido2Event()
|
||||
|
||||
/**
|
||||
* Performs device based user verification.
|
||||
*/
|
||||
data class Fido2UserVerification(
|
||||
val required: Boolean,
|
||||
val selectedCipher: CipherView,
|
||||
) : BackgroundEvent, Fido2Event()
|
||||
}
|
||||
|
||||
/**
|
||||
* Models actions for [Fido2Screen].
|
||||
*/
|
||||
sealed class Fido2Action {
|
||||
|
||||
/**
|
||||
* Indicates the user dismissed the error dialog.
|
||||
*/
|
||||
data class DismissErrorDialogClick(val result: Fido2GetCredentialsResult) : Fido2Action()
|
||||
|
||||
/**
|
||||
* Models actions [Fido2ViewModel] may itself send.
|
||||
*/
|
||||
sealed class Internal : Fido2Action() {
|
||||
|
||||
/**
|
||||
* Indicates the [userState] has changed.
|
||||
*/
|
||||
data class UserStateChangeReceive(
|
||||
val userState: UserState?,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates the [vaultData] has changed.
|
||||
*/
|
||||
data class VaultDataStateChangeReceive(
|
||||
val vaultData: DataState<VaultData>,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
|
@ -5,7 +5,6 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.vault.CipherRepromptType
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
|
@ -170,7 +169,6 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
it
|
||||
.filterForAutofillIfNecessary()
|
||||
.filterForFido2CreationIfNecessary()
|
||||
.filterForFidoGetCredentialsIfNecessary()
|
||||
.filterForTotpIfNecessary(),
|
||||
)
|
||||
}
|
||||
|
@ -518,6 +516,7 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
VaultItemListingEvent.NavigateToAddVaultItem(
|
||||
vaultItemCipherType = itemListingType.toVaultItemCipherType(),
|
||||
selectedFolderId = itemListingType.folderId,
|
||||
selectedCollectionId = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -688,16 +687,6 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun authenticateFido2Credential(request: Fido2CredentialAssertionRequest) {
|
||||
viewModelScope.launch {
|
||||
sendAction(
|
||||
VaultItemListingsAction.Internal.Fido2AssertionResultReceive(
|
||||
result = fido2CredentialManager.authenticateFido2Credential(request),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMasterPasswordRepromptSubmit(
|
||||
action: VaultItemListingsAction.MasterPasswordRepromptSubmit,
|
||||
) {
|
||||
|
@ -940,14 +929,6 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
is VaultItemListingsAction.Internal.Fido2RegisterCredentialResultReceive -> {
|
||||
handleFido2RegisterCredentialResultReceive(action)
|
||||
}
|
||||
|
||||
is VaultItemListingsAction.Internal.Fido2AssertionDataReceive -> {
|
||||
handleFido2AssertionDataReceive(action)
|
||||
}
|
||||
|
||||
is VaultItemListingsAction.Internal.Fido2AssertionResultReceive -> {
|
||||
handleFido2AssertionResultReceive(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1212,10 +1193,6 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
cipherView = cipherView,
|
||||
)
|
||||
}
|
||||
?: specialCircumstanceManager
|
||||
.specialCircumstance
|
||||
?.toFido2AssertionRequestOrNull()
|
||||
?.let { authenticateFido2Credential(request = it) }
|
||||
?: showFido2ErrorDialog()
|
||||
}
|
||||
//endregion VaultItemListing Handlers
|
||||
|
@ -1238,31 +1215,7 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
|
||||
private fun vaultLoadedReceive(vaultData: DataState.Loaded<VaultData>) {
|
||||
updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = true)
|
||||
state
|
||||
// TODO: Move FIDO 2 get credentials to vault unlock screen
|
||||
.fido2GetCredentialsRequest
|
||||
?.let { fido2GetCredentialsRequest ->
|
||||
viewModelScope.launch {
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2GetCredentialsRequest(
|
||||
fido2CredentialManager.getFido2CredentialsForRelyingParty(
|
||||
fido2GetCredentialsRequest = fido2GetCredentialsRequest,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
?: state
|
||||
// TODO: Move FIDO 2 authentication to vault unlock screen
|
||||
.fido2CredentialAssertionRequest
|
||||
?.let { fido2AssertionRequest ->
|
||||
trySendAction(
|
||||
VaultItemListingsAction.Internal.Fido2AssertionDataReceive(
|
||||
fido2AssertionRequest,
|
||||
),
|
||||
)
|
||||
}
|
||||
?: mutableStateFlow.update { it.copy(isRefreshing = false) }
|
||||
mutableStateFlow.update { it.copy(isRefreshing = false) }
|
||||
}
|
||||
|
||||
private fun vaultLoadingReceive() {
|
||||
|
@ -1324,43 +1277,6 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
sendEvent(VaultItemListingEvent.CompleteFido2Registration(action.result))
|
||||
}
|
||||
|
||||
private fun handleFido2AssertionDataReceive(
|
||||
action: VaultItemListingsAction.Internal.Fido2AssertionDataReceive,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = VaultItemListingState.DialogState.Loading(
|
||||
message = R.string.loading.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
val request = action.data
|
||||
val ciphers = vaultRepository
|
||||
.ciphersStateFlow
|
||||
.value
|
||||
.data
|
||||
.orEmpty()
|
||||
.filter { it.isActiveWithFido2Credentials }
|
||||
if (request.cipherId.isNullOrEmpty()) {
|
||||
showFido2ErrorDialog()
|
||||
} else {
|
||||
val selectedCipher = ciphers
|
||||
.find { it.id == request.cipherId }
|
||||
?: run {
|
||||
showFido2ErrorDialog()
|
||||
return
|
||||
}
|
||||
|
||||
if (state.hasMasterPassword &&
|
||||
selectedCipher.reprompt == CipherRepromptType.PASSWORD
|
||||
) {
|
||||
repromptMasterPasswordForFido2Assertion(request.cipherId)
|
||||
} else {
|
||||
verifyUserAndAuthenticateCredential(request, selectedCipher)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun repromptMasterPasswordForFido2Assertion(cipherId: String) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
|
@ -1371,43 +1287,6 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun verifyUserAndAuthenticateCredential(
|
||||
request: Fido2CredentialAssertionRequest,
|
||||
selectedCipher: CipherView,
|
||||
) {
|
||||
if (fido2CredentialManager.isUserVerified) {
|
||||
authenticateFido2Credential(request)
|
||||
return
|
||||
}
|
||||
|
||||
val userVerificationRequirement: UserVerificationRequirement = fido2CredentialManager
|
||||
.getUserVerificationRequirementForAssertion(request)
|
||||
|
||||
when (userVerificationRequirement) {
|
||||
UserVerificationRequirement.DISCOURAGED -> {
|
||||
authenticateFido2Credential(request)
|
||||
}
|
||||
|
||||
UserVerificationRequirement.PREFERRED -> {
|
||||
sendUserVerificationEvent(isRequired = false, selectedCipher = selectedCipher)
|
||||
}
|
||||
|
||||
UserVerificationRequirement.REQUIRED -> {
|
||||
sendUserVerificationEvent(isRequired = true, selectedCipher = selectedCipher)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFido2AssertionResultReceive(
|
||||
action: VaultItemListingsAction.Internal.Fido2AssertionResultReceive,
|
||||
) {
|
||||
fido2CredentialManager.isUserVerified = false
|
||||
clearDialogState()
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2Assertion(action.result),
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateStateWithVaultData(vaultData: VaultData, clearDialogState: Boolean) {
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
|
@ -1505,27 +1384,6 @@ 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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the given vault data and filters it for totp data.
|
||||
*/
|
||||
|
@ -2404,20 +2262,6 @@ sealed class VaultItemListingsAction {
|
|||
data class Fido2RegisterCredentialResultReceive(
|
||||
val result: Fido2RegisterCredentialResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that FIDO 2 assertion request data has been received.
|
||||
*/
|
||||
data class Fido2AssertionDataReceive(
|
||||
val data: Fido2CredentialAssertionRequest,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that a result of a FIDO 2 credential assertion has been received.
|
||||
*/
|
||||
data class Fido2AssertionResultReceive(
|
||||
val result: Fido2CredentialAssertionResult,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,12 +31,14 @@ import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
|||
import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2CredentialAutofillView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2Credential
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
|
@ -75,6 +77,8 @@ class Fido2ProviderProcessorTest {
|
|||
private val vaultRepository: VaultRepository = mockk {
|
||||
every { ciphersStateFlow } returns mutableCiphersStateFlow
|
||||
}
|
||||
private val environmentRepository: EnvironmentRepository = mockk {
|
||||
}
|
||||
private val passkeyAssertionOptions = createMockPasskeyAssertionOptions(number = 1)
|
||||
private val fido2CredentialManager: Fido2CredentialManager = mockk()
|
||||
private val fido2CredentialStore: Fido2CredentialStore = mockk()
|
||||
|
@ -294,11 +298,11 @@ class Fido2ProviderProcessorTest {
|
|||
)
|
||||
every { cancellationSignal.setOnCancelListener(any()) } just runs
|
||||
every { callback.onResult(capture(captureSlot)) } just runs
|
||||
every { context.getString(any()) } returns "mockTitle"
|
||||
every { context.getString(any(), any()) } returns "mockTitle"
|
||||
every {
|
||||
intentManager.createFido2UnlockPendingIntent(
|
||||
action = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT",
|
||||
userId = "mockUserId-1",
|
||||
userId = DEFAULT_USER_STATE.activeUserId,
|
||||
requestCode = any(),
|
||||
)
|
||||
} returns mockIntent
|
||||
|
@ -315,7 +319,7 @@ class Fido2ProviderProcessorTest {
|
|||
callback.onResult(any())
|
||||
intentManager.createFido2UnlockPendingIntent(
|
||||
action = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT",
|
||||
userId = "mockUserId-1",
|
||||
userId = DEFAULT_USER_STATE.activeUserId,
|
||||
requestCode = any(),
|
||||
)
|
||||
}
|
||||
|
@ -339,8 +343,16 @@ class Fido2ProviderProcessorTest {
|
|||
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
|
||||
val captureSlot = slot<GetCredentialException>()
|
||||
mutableUserStateFlow.value = DEFAULT_USER_STATE
|
||||
every { context.getString(any(), any()) } returns "mockEmail-0"
|
||||
every { cancellationSignal.setOnCancelListener(any()) } just runs
|
||||
every { callback.onError(capture(captureSlot)) } just runs
|
||||
every {
|
||||
intentManager.createFido2UnlockPendingIntent(
|
||||
action = UNLOCK_ACCOUNT_INTENT,
|
||||
userId = "mockUserId-0",
|
||||
requestCode = 0,
|
||||
)
|
||||
} returns mockk()
|
||||
|
||||
fido2Processor.processGetCredentialRequest(request, cancellationSignal, callback)
|
||||
|
||||
|
@ -365,8 +377,16 @@ class Fido2ProviderProcessorTest {
|
|||
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
|
||||
val captureSlot = slot<GetCredentialException>()
|
||||
mutableUserStateFlow.value = DEFAULT_USER_STATE
|
||||
every { context.getString(any(), any()) } returns "mockEmail-0"
|
||||
every { cancellationSignal.setOnCancelListener(any()) } just runs
|
||||
every { callback.onError(capture(captureSlot)) } just runs
|
||||
every {
|
||||
intentManager.createFido2UnlockPendingIntent(
|
||||
action = UNLOCK_ACCOUNT_INTENT,
|
||||
userId = "mockUserId-0",
|
||||
requestCode = 0,
|
||||
)
|
||||
} returns mockk()
|
||||
|
||||
fido2Processor.processGetCredentialRequest(request, cancellationSignal, callback)
|
||||
|
||||
|
@ -393,8 +413,16 @@ class Fido2ProviderProcessorTest {
|
|||
val mockCipherViews = listOf(createMockCipherView(number = 1))
|
||||
mutableUserStateFlow.value = DEFAULT_USER_STATE
|
||||
mutableCiphersStateFlow.value = DataState.Loaded(mockCipherViews)
|
||||
every { context.getString(any(), any()) } returns "mockEmail-0"
|
||||
every { cancellationSignal.setOnCancelListener(any()) } just runs
|
||||
every { callback.onError(capture(captureSlot)) } just runs
|
||||
every {
|
||||
intentManager.createFido2UnlockPendingIntent(
|
||||
action = UNLOCK_ACCOUNT_INTENT,
|
||||
userId = "mockUserId-0",
|
||||
requestCode = 0,
|
||||
)
|
||||
} returns mockk()
|
||||
coEvery {
|
||||
vaultRepository.getDecryptedFido2CredentialAutofillViews(any())
|
||||
} returns DecryptFido2CredentialAutofillViewResult.Error
|
||||
|
@ -426,6 +454,10 @@ class Fido2ProviderProcessorTest {
|
|||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `processGetCredentialRequest should invoke callback with filtered and discovered passkeys`() {
|
||||
val userState = UserState(
|
||||
activeUserId = "mockUserId-0",
|
||||
accounts = createMockAccounts(1),
|
||||
)
|
||||
val mockOption = BeginGetPublicKeyCredentialOption(
|
||||
candidateQueryData = Bundle(),
|
||||
id = "",
|
||||
|
@ -436,19 +468,28 @@ class Fido2ProviderProcessorTest {
|
|||
}
|
||||
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
|
||||
val captureSlot = slot<BeginGetCredentialResponse>()
|
||||
val mockCipherViews = listOf(createMockCipherView(number = 1))
|
||||
val mockCipherView = createMockCipherView(
|
||||
number = 1,
|
||||
fido2Credentials = listOf(createMockSdkFido2Credential(number = 1)),
|
||||
)
|
||||
val mockCipherViews = listOf(mockCipherView)
|
||||
val mockFido2CredentialAutofillViews = listOf(
|
||||
createMockFido2CredentialAutofillView(number = 1),
|
||||
createMockFido2CredentialAutofillView(
|
||||
number = 1,
|
||||
cipherId = mockCipherView.id,
|
||||
rpId = "mockRelyingPartyId-1",
|
||||
),
|
||||
)
|
||||
val mockIntent: PendingIntent = mockk()
|
||||
val mockPublicKeyCredentialEntry: PublicKeyCredentialEntry = mockk()
|
||||
mutableUserStateFlow.value = DEFAULT_USER_STATE
|
||||
mutableUserStateFlow.value = userState
|
||||
mutableCiphersStateFlow.value = DataState.Loaded(mockCipherViews)
|
||||
every { cancellationSignal.setOnCancelListener(any()) } just runs
|
||||
every { context.getString(any(), any()) } returns "mockEmail-0"
|
||||
every { callback.onResult(capture(captureSlot)) } just runs
|
||||
coEvery {
|
||||
vaultRepository.silentlyDiscoverCredentials(
|
||||
userId = DEFAULT_USER_STATE.activeUserId,
|
||||
userId = userState.activeUserId,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
relyingPartyId = "mockRelyingPartyId-1",
|
||||
)
|
||||
|
@ -458,8 +499,8 @@ class Fido2ProviderProcessorTest {
|
|||
} returns DecryptFido2CredentialAutofillViewResult.Success(mockFido2CredentialAutofillViews)
|
||||
every {
|
||||
intentManager.createFido2GetCredentialPendingIntent(
|
||||
action = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY",
|
||||
userId = DEFAULT_USER_STATE.activeUserId,
|
||||
action = GET_PASSKEY_INTENT,
|
||||
userId = userState.activeUserId,
|
||||
credentialId = mockFido2CredentialAutofillViews.first().credentialId.toString(),
|
||||
cipherId = mockFido2CredentialAutofillViews.first().cipherId,
|
||||
requestCode = any(),
|
||||
|
|
|
@ -6,12 +6,10 @@ import android.content.Intent
|
|||
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||
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.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
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import io.mockk.Called
|
||||
import io.mockk.MockKVerificationScope
|
||||
import io.mockk.Ordering
|
||||
|
@ -84,8 +82,6 @@ class Fido2CompletionManagerTest {
|
|||
@Nested
|
||||
inner class DefaultImplementation {
|
||||
|
||||
private val mockIntentManager = mockk<IntentManager>()
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
fido2CompletionManager = Fido2CompletionManagerImpl(mockActivity)
|
||||
|
@ -189,20 +185,10 @@ class Fido2CompletionManagerTest {
|
|||
mockkStatic(PendingIntent::class)
|
||||
|
||||
val mockCredentialEntry = mockk<PublicKeyCredentialEntry>()
|
||||
val mockFido2AutofillView = createMockFido2CredentialAutofillView(number = 1)
|
||||
|
||||
every {
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().build()
|
||||
} returns mockCredentialEntry
|
||||
every {
|
||||
mockIntentManager.createFido2GetCredentialPendingIntent(
|
||||
action = GET_PASSKEY_INTENT,
|
||||
userId = "mockUserId",
|
||||
credentialId = mockFido2AutofillView.credentialId.toString(),
|
||||
cipherId = mockFido2AutofillView.cipherId,
|
||||
requestCode = any(),
|
||||
)
|
||||
} returns mockk()
|
||||
every { mockActivity.getString(any()) } returns "No username"
|
||||
|
||||
fido2CompletionManager
|
||||
|
@ -230,6 +216,42 @@ class Fido2CompletionManagerTest {
|
|||
assertTrue(responseSlot.captured.authenticationActions.isEmpty())
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `completeFido2GetCredentials should set username to default value when userNameForUi is null`() {
|
||||
mockkConstructor(PublicKeyCredentialEntry.Builder::class)
|
||||
mockkStatic(PendingIntent::class)
|
||||
val mockCredentialEntry = mockk<PublicKeyCredentialEntry>()
|
||||
every {
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().build()
|
||||
} returns mockCredentialEntry
|
||||
every { mockActivity.getString(any()) } returns "No Username"
|
||||
|
||||
fido2CompletionManager
|
||||
.completeFido2GetCredentialRequest(
|
||||
Fido2GetCredentialsResult.Success(
|
||||
userId = "mockUserId",
|
||||
options = mockk(),
|
||||
credentialEntries = listOf(mockCredentialEntry),
|
||||
),
|
||||
)
|
||||
|
||||
val responseSlot = slot<BeginGetCredentialResponse>()
|
||||
verify {
|
||||
mockActivity.getString(R.string.no_username)
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().build()
|
||||
PendingIntentHandler.setBeginGetCredentialResponse(
|
||||
intent = any(),
|
||||
response = capture(responseSlot),
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
listOf(mockCredentialEntry),
|
||||
responseSlot.captured.credentialEntries,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `completeFido2GetCredentials should set GetCredentialException, set activity result, then finish activity when result is Error`() {
|
||||
|
|
Loading…
Reference in a new issue