PM-33893: bug: Crash caused by empty credential password (#6693)

This commit is contained in:
David Perez
2026-03-19 17:12:59 +00:00
committed by GitHub
parent 2b9e142107
commit be8777cb8e
7 changed files with 103 additions and 25 deletions
@@ -131,8 +131,8 @@ class CredentialProviderCompletionManagerImpl(
intent = intent,
response = GetCredentialResponse(
credential = PasswordCredential(
id = result.credential.username ?: "",
password = result.credential.password ?: "",
id = result.username.orEmpty(),
password = result.password,
),
),
)
@@ -1,7 +1,6 @@
package com.x8bit.bitwarden.ui.credentials.manager.model
import com.bitwarden.ui.util.Text
import com.bitwarden.vault.LoginView
/**
* Represents the result of Password authentication.
@@ -10,10 +9,13 @@ sealed class GetPasswordCredentialResult {
/**
* Represents a successful authentication of Password credentials.
*/
data class Success(val credential: LoginView) : GetPasswordCredentialResult()
data class Success(
val username: String?,
val password: String,
) : GetPasswordCredentialResult()
/**
* Indicates the user cancelled authentication.
* Indicates the user canceled authentication.
*/
data object Cancelled : GetPasswordCredentialResult()
@@ -2549,11 +2549,28 @@ class VaultItemListingViewModel @Inject constructor(
bitwardenCredentialManager.isUserVerified = false
clearDialogState()
val event = selectedCipher.login
val event = selectedCipher
.login
?.let { credential ->
VaultItemListingEvent.CompleteProviderGetPasswordCredentialRequest(
GetPasswordCredentialResult.Success(credential = credential),
)
credential
.password
?.takeIf { it.isNotEmpty() }
?.let { password ->
VaultItemListingEvent.CompleteProviderGetPasswordCredentialRequest(
GetPasswordCredentialResult.Success(
username = credential.username,
password = password,
),
)
}
?: VaultItemListingEvent.CompleteProviderGetPasswordCredentialRequest(
@Suppress("MaxLineLength")
GetPasswordCredentialResult.Error(
message = BitwardenString
.password_operation_failed_because_the_selected_item_does_not_have_a_valid_password
.asText(),
),
)
}
?: VaultItemListingEvent.CompleteProviderGetPasswordCredentialRequest(
GetPasswordCredentialResult.Error(
@@ -14,7 +14,6 @@ import androidx.credentials.provider.PublicKeyCredentialEntry
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.credentials.manager.CredentialManagerPendingIntentManager
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2CredentialAutofillView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockPasswordCredentialAutofillCipherLogin
import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
@@ -228,8 +227,12 @@ class CredentialProviderCompletionManagerTest {
@Suppress("MaxLineLength")
@Test
fun `completePasswordGet should set GetCredentialResponse, set activity result, then finish activity when result is Success`() {
credentialProviderCompletionManager
.completePasswordGet(GetPasswordCredentialResult.Success(createMockLoginView(1)))
credentialProviderCompletionManager.completePasswordGet(
GetPasswordCredentialResult.Success(
username = "mockUsername-1",
password = "mockPassword-1",
),
)
verifyActivityResultIsSetAndFinishedAfter {
PendingIntentHandler.setGetCredentialResponse(any(), any())
@@ -50,7 +50,6 @@ import com.bitwarden.ui.util.performYesDialogButtonClick
import com.bitwarden.vault.CipherType
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginView
import com.x8bit.bitwarden.ui.credentials.manager.CredentialProviderCompletionManager
import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
@@ -2056,7 +2055,10 @@ class VaultItemListingScreenTest : BitwardenComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `GetPasswordCredentialResult event should call CredentialProviderCompletionManager with result`() {
val result = GetPasswordCredentialResult.Success(createMockLoginView(1))
val result = GetPasswordCredentialResult.Success(
username = "mockUsername-1",
password = "mockPassword-1",
)
mutableEventFlow.tryEmit(
VaultItemListingEvent.CompleteProviderGetPasswordCredentialRequest(result),
)
@@ -92,7 +92,6 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionV
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockDecryptCipherListResult
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginListView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
@@ -5531,15 +5530,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
number = 1,
totp = "mockTotp-1",
)
val mockLoginView = createMockLoginView(
number = 1,
totp = "mockTotp-1",
clock = clock,
password = "mockPassword-1",
fido2Credentials = null,
)
val mockGetPasswordRequest = createMockProviderGetPasswordCredentialRequest(1)
.copy(cipherId = "mockId-1")
val mockGetPasswordRequest = createMockProviderGetPasswordCredentialRequest(number = 1)
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderGetPasswordRequest(
passwordGetRequest = mockGetPasswordRequest,
@@ -5578,7 +5569,69 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test {
assertEquals(
VaultItemListingEvent.CompleteProviderGetPasswordCredentialRequest(
result = GetPasswordCredentialResult.Success(mockLoginView),
result = GetPasswordCredentialResult.Success(
username = "mockUsername-1",
password = "mockPassword-1",
),
),
awaitItem(),
)
assertNull(viewModel.stateFlow.value.dialogState)
}
}
@Suppress("MaxLineLength")
@Test
fun `UserVerificationSuccess should set isUserVerified to true, and emit CompleteGetPassword with error result`() =
runTest {
val mockLoginListView = createMockLoginListView(
number = 1,
totp = "mockTotp-1",
)
val mockGetPasswordRequest = createMockProviderGetPasswordCredentialRequest(number = 1)
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderGetPasswordRequest(
passwordGetRequest = mockGetPasswordRequest,
)
every {
ProviderGetCredentialRequest.fromBundle(any())
} returns mockk(relaxed = true) {
every {
credentialOptions
} returns listOf(
mockk<GetPasswordOption>(relaxed = true),
)
}
every {
vaultRepository
.decryptCipherListResultStateFlow
.value
.data
} returns createMockDecryptCipherListResult(
number = 1,
successes = listOf(
createMockCipherListView(
number = 1,
type = CipherListViewType.Login(mockLoginListView),
),
),
)
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(
VaultItemListingsAction.UserVerificationSuccess(
selectedCipherView = createMockCipherView(number = 1, password = ""),
),
)
viewModel.eventFlow.test {
assertEquals(
VaultItemListingEvent.CompleteProviderGetPasswordCredentialRequest(
result = GetPasswordCredentialResult.Error(
message = BitwardenString
.password_operation_failed_because_the_selected_item_does_not_have_a_valid_password
.asText(),
),
),
awaitItem(),
)
+1
View File
@@ -941,6 +941,7 @@ Do you want to switch to this account?</string>
<string name="passkey_operation_failed_because_no_item_was_selected">Passkey operation failed because no item was selected.</string>
<string name="passkey_operation_failed_because_the_request_is_unsupported">Passkey operation failed because the request is unsupported.</string>
<string name="password_operation_failed_because_the_selected_item_does_not_exist">Password operation failed because the selected item does not exist.</string>
<string name="password_operation_failed_because_the_selected_item_does_not_have_a_valid_password">Password operation failed because the selected item does not have a valid password.</string>
<string name="password_operation_failed_because_no_item_was_selected">Password operation failed because no item was selected.</string>
<string name="credential_operation_failed_because_the_request_is_invalid">Credential operation failed because the request is invalid.</string>
<string name="credential_operation_failed_because_the_request_is_unsupported">Credential operation failed because the request is unsupported.</string>