Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
8e6d5e4
add capability TYPE_PASSWORD_CREDENTIAL
Nailik May 12, 2025
07e8a53
handle BeginCreatePasswordCredentialRequest in CredentialProvider
Nailik May 12, 2025
6f67403
rename CredentialProvider Passkey/Fido2 intent and functions to more …
Nailik May 12, 2025
8c83375
remove filter BeginGetPublicKeyCredentialOption in order to also hand…
Nailik May 12, 2025
3d44349
create CredentialEntry list for CredentialManager request for Passwords
Nailik May 12, 2025
f277f02
wip
Nailik May 27, 2025
715ffdf
wip
Nailik May 27, 2025
ddde493
build password credential entry list
Nailik May 27, 2025
8c14fd5
enable autofill of passwords
Nailik May 27, 2025
d91d653
cleanup
Nailik May 27, 2025
cc4dbbb
rename password assertion to password get
Nailik May 29, 2025
4832513
add emptyline
Nailik May 29, 2025
1fb0019
always validate origin
Nailik May 29, 2025
e90f1ec
remove unnecessary PasswordCredentialAssertionError
Nailik May 29, 2025
7bae772
remove create password until supported
Nailik May 29, 2025
a708de6
support TYPE_PASSWORD_CREDENTIAL for debug builds only for now (until…
Nailik May 29, 2025
a7a88b7
namings
Nailik May 29, 2025
5f8e571
namings
Nailik May 29, 2025
88b229f
removed unecessary authenticate password
Nailik May 29, 2025
a856b7c
cleanup
Nailik May 29, 2025
cab3549
todo
Nailik May 29, 2025
9c462ec
add name as display name
Nailik Jun 2, 2025
6c9f80d
cleanup
Nailik Jun 17, 2025
8388eaa
remove extra ProviderGetPasswordCredentialResultReceive action and fi…
Nailik Jun 17, 2025
1270bec
detekt formatting
Nailik Jun 17, 2025
bbb1568
fix unwanted auto selection of password if possible account list is b…
Nailik Jun 17, 2025
b5e92b6
add getProviderGetPasswordRequestOrNull tests to CredentialManagerInt…
Nailik Jun 21, 2025
aa3ef0d
remove unused error type
Nailik Jun 21, 2025
e1e1b3d
rename Fido2GetCredentialsError to GetCredentialsError and update te…
Nailik Jun 21, 2025
7c0dcfe
formatting
Nailik Jun 21, 2025
5b846ce
Fix compile errors, version check logic, & minor formatting tweaks
SaintPatrck Jul 2, 2025
1cbe3f7
initial implementation for create password request
Nailik Jun 21, 2025
07a1a39
formatting
Nailik Jun 21, 2025
26c0358
formatting and fix tests
Nailik Jun 21, 2025
12d3bfb
update texts and add password error dialog
Nailik Jun 22, 2025
c49fcd3
formatting
Nailik Jun 22, 2025
86bb5d5
added tests for BitwardenCredentialManager and removed unused properties
Nailik Jun 22, 2025
1429137
added tests for VaultAddEditViewModel
Nailik Jun 22, 2025
1b9f99c
update VaultItemListingViewModel to differentiate between public key …
Nailik Jun 22, 2025
c646701
add tests
Nailik Jun 22, 2025
02deed5
formatting
Nailik Jun 22, 2025
96a4136
fix tests
Nailik Jun 22, 2025
6119de7
fix
Nailik Jul 20, 2025
398da3c
fix merge
Nailik Jul 24, 2025
8571964
fixes
Nailik Jul 24, 2025
53baa28
rebase fixes
Nailik Sep 30, 2025
eb9832f
formatting and fixes
Nailik Sep 30, 2025
a8f8dfb
fix tests and simplify RegisterCredentialResult
Nailik Sep 30, 2025
78bb2b7
fix issue that user was presented with password overwrite even if dat…
Nailik Sep 30, 2025
cdccac6
Merge branch 'main' into feature/add-password-credential-manager-prov…
Nailik Oct 14, 2025
10e8ed6
Merge branch 'main' into feature/add-password-credential-manager-prov…
Nailik Oct 17, 2025
10c3bc8
Update app/src/test/kotlin/com/x8bit/bitwarden/data/credentials/model…
Nailik Oct 17, 2025
99080c1
Update app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/added…
Nailik Oct 17, 2025
ef6ed5f
suggestion
Nailik Oct 17, 2025
8337f65
remove unwanted change
Nailik Oct 17, 2025
1691f7b
suggestion
Nailik Oct 17, 2025
30ab5ab
revert change
Nailik Oct 17, 2025
004cdd6
rename BitwardenOverwritePasskeyConfirmationDialog to BitwardenOverwr…
Nailik Oct 17, 2025
41e2552
remove todo
Nailik Oct 17, 2025
aa9276d
Merge branch 'main' into feature/add-password-credential-manager-prov…
Nailik Oct 21, 2025
51aac83
Merge branch 'main' into feature/add-password-credential-manager-prov…
Nailik Oct 24, 2025
70de531
Merge branch 'main' into feature/add-password-credential-manager-prov…
Nailik Nov 3, 2025
ee16cd2
Merge branch 'main' into feature/add-password-credential-manager-prov…
Nailik Nov 5, 2025
a3abd84
Refactor password creation flow from Credential Manager
SaintPatrck Nov 7, 2025
7660843
Refactor Fido2Save test to use modern credential provider mocks
SaintPatrck Nov 13, 2025
f59193c
Delete unused PasswordRegisterResult class
SaintPatrck Nov 18, 2025
9affd61
Explicitly declare return type for toCreatePasskeyEntry
SaintPatrck Nov 18, 2025
01dcfca
Refactor credential creation logic in RootNavViewModel
SaintPatrck Nov 18, 2025
a891899
Update `rpName` extraction formatting in `CreateCredentialRequestExte…
SaintPatrck Nov 18, 2025
767a650
Refactor validation logic in VaultAddEditViewModel to an expression body
SaintPatrck Nov 18, 2025
57b37ea
Update named arguments for clarity in Vault and Credential Manager
SaintPatrck Nov 18, 2025
9e45d64
Rename RegisterCredentialResult to CreateCredentialResult
SaintPatrck Nov 18, 2025
bc61d0f
Add unit tests for password credential creation processing
SaintPatrck Nov 18, 2025
02f4fa6
Make `title` parameter non-nullable in confirmation dialog
SaintPatrck Nov 18, 2025
e44048a
Remove unused `getCreatePasswordCredentialRequestOrNull` extension
SaintPatrck Nov 18, 2025
28ae326
Fix VaultItemListingViewModel state dialog logic
SaintPatrck Nov 18, 2025
1ed1eec
Add tests for handling Create Password requests in RootNav
SaintPatrck Nov 18, 2025
8a40183
Fix variable name shadowing in RootNavViewModelTest
SaintPatrck Nov 19, 2025
2943eaa
Refactor `VaultItemListingViewModel` `when` expressions to use blocks
SaintPatrck Nov 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -303,9 +303,9 @@ tasks {
useJUnitPlatform()
maxHeapSize = "2g"
maxParallelForks = Runtime.getRuntime().availableProcessors()
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC" +
// Explicitly setting the user Country and Language because tests assume en-US
"-Duser.country=US" +
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC" +
// Explicitly setting the user Country and Language because tests assume en-US
"-Duser.country=US" +
"-Duser.language=en"
}
}
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
<intent-filter>
<action android:name="com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSKEY" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSWORD" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSWORD" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_UNLOCK_ACCOUNT" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ interface CredentialManagerPendingIntentManager {
userId: String,
): PendingIntent

/**
* Creates a pending intent to use when providing options for Password credential creation.
*/
fun createPasswordCreationPendingIntent(
userId: String,
): PendingIntent

/**
* Creates a pending intent to use when providing options for Password credential filling.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ class CredentialManagerPendingIntentManagerImpl(
)
}

/**
* Creates a pending intent to use when providing options for FIDO 2 credential creation.
*/
override fun createPasswordCreationPendingIntent(
userId: String,
): PendingIntent {
val intent = Intent(CREATE_PASSWORD_ACTION)
.setPackage(context.packageName)
.putExtra(EXTRA_KEY_USER_ID, userId)

return PendingIntent.getActivity(
/* context = */ context,
/* requestCode = */ Random.nextInt(),
/* intent = */ intent,
/* flags = */ PendingIntent.FLAG_UPDATE_CURRENT.toPendingIntentMutabilityFlag(),
)
}

/**
* Creates a pending intent to use when providing options for Password credential filling.
*/
Expand All @@ -101,4 +119,5 @@ class CredentialManagerPendingIntentManagerImpl(
private const val CREATE_PASSKEY_ACTION = "com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSKEY"
private const val UNLOCK_ACCOUNT_ACTION = "com.x8bit.bitwarden.credentials.ACTION_UNLOCK_ACCOUNT"
private const val GET_PASSKEY_ACTION = "com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY"
private const val CREATE_PASSWORD_ACTION = "com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSWORD"
private const val GET_PASSWORD_ACTION = "com.x8bit.bitwarden.credentials.ACTION_GET_PASSWORD"
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.credentials.model

import android.os.Bundle
import android.os.Parcelable
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.provider.CallingAppInfo
import androidx.credentials.provider.ProviderCreateCredentialRequest
Expand Down Expand Up @@ -48,6 +49,15 @@ data class CreateCredentialRequest(
providerRequest.callingRequest as? CreatePublicKeyCredentialRequest
}

/**
* The [CreatePasswordRequest] of the [providerRequest], or null if the calling
* request is not a [CreatePasswordRequest].
*/
@IgnoredOnParcel
val createPasswordCredentialRequest: CreatePasswordRequest? by lazy {
providerRequest.callingRequest as? CreatePasswordRequest
}

/**
* The [requestJson] of the [createPublicKeyCredentialRequest], or null if the calling request
* is not a [CreatePublicKeyCredentialRequest].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.AuthenticationAction
import androidx.credentials.provider.BeginCreateCredentialRequest
import androidx.credentials.provider.BeginCreateCredentialResponse
import androidx.credentials.provider.BeginCreatePasswordCredentialRequest
import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
import androidx.credentials.provider.BeginGetCredentialRequest
import androidx.credentials.provider.BeginGetCredentialResponse
Expand Down Expand Up @@ -69,7 +70,7 @@ class CredentialProviderProcessorImpl(
}

val createCredentialJob = ioScope.launch {
processCreateCredentialRequest(request = request)
(handleCreatePasskeyQuery(request) ?: handleCreatePasswordQuery(request))
?.let { callback.onResult(it) }
?: callback.onError(CreateCredentialUnknownException())
}
Expand Down Expand Up @@ -137,21 +138,11 @@ class CredentialProviderProcessorImpl(
callback.onError(ClearCredentialUnsupportedException())
}

private fun processCreateCredentialRequest(
private fun handleCreatePasskeyQuery(
request: BeginCreateCredentialRequest,
): BeginCreateCredentialResponse? {
return when (request) {
is BeginCreatePublicKeyCredentialRequest -> {
handleCreatePasskeyQuery(request)
}

else -> null
}
}
if (request !is BeginCreatePublicKeyCredentialRequest) return null

private fun handleCreatePasskeyQuery(
request: BeginCreatePublicKeyCredentialRequest,
): BeginCreateCredentialResponse? {
val requestJson = request
.candidateQueryData
.getString("androidx.credentials.BUNDLE_KEY_REQUEST_JSON")
Expand All @@ -161,14 +152,19 @@ class CredentialProviderProcessorImpl(
val userState = authRepository.userStateFlow.value ?: return null

return BeginCreateCredentialResponse.Builder()
.setCreateEntries(userState.accounts.toCreateEntries(userState.activeUserId))
.setCreateEntries(
userState.accounts.toCreatePasskeyEntry(userState.activeUserId),
)
.build()
}

private fun List<UserState.Account>.toCreateEntries(activeUserId: String) =
map { it.toCreateEntry(isActive = activeUserId == it.userId) }
private fun List<UserState.Account>.toCreatePasskeyEntry(
activeUserId: String,
): List<CreateEntry> = map { it.toCreatePasskeyEntry(isActive = activeUserId == it.userId) }

private fun UserState.Account.toCreateEntry(isActive: Boolean): CreateEntry {
private fun UserState.Account.toCreatePasskeyEntry(
isActive: Boolean,
): CreateEntry {
val accountName = name ?: email
val entryBuilder = CreateEntry
.Builder(
Expand Down Expand Up @@ -196,6 +192,54 @@ class CredentialProviderProcessorImpl(
return entryBuilder.build()
}

private fun handleCreatePasswordQuery(
request: BeginCreateCredentialRequest,
): BeginCreateCredentialResponse? {
if (request !is BeginCreatePasswordCredentialRequest) return null

val userState = authRepository.userStateFlow.value ?: return null

return BeginCreateCredentialResponse.Builder()
.setCreateEntries(
userState.accounts.toCreatePasswordEntry(userState.activeUserId),
)
.build()
}

private fun List<UserState.Account>.toCreatePasswordEntry(
activeUserId: String,
) = map { it.toCreatePasswordEntry(isActive = activeUserId == it.userId) }

private fun UserState.Account.toCreatePasswordEntry(
isActive: Boolean,
): CreateEntry {
val accountName = name ?: email
val entryBuilder = CreateEntry
.Builder(
accountName = accountName,
pendingIntent = pendingIntentManager.createPasswordCreationPendingIntent(
userId = userId,
),
)
.setDescription(
context.getString(
BitwardenString.your_password_will_be_saved_to_your_bitwarden_vault_for_x,
accountName,
),
)
// Set the last used time to "now" so the active account is the default option in the
// system prompt.
.setLastUsedTime(if (isActive) clock.instant() else null)
.setAutoSelectAllowed(true)

if (isVaultUnlocked) {
biometricsEncryptionManager
.getOrCreateCipher(userId)
?.let { entryBuilder.setBiometricPromptDataIfSupported(cipher = it) }
}
return entryBuilder.build()
}

private fun CreateEntry.Builder.setBiometricPromptDataIfSupported(
cipher: Cipher,
): CreateEntry.Builder {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
package com.x8bit.bitwarden.ui.credentials.manager

import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult

/**
* A manager for completing the FIDO 2 creation process.
* A manager for completing the credential creation process.
*/
interface CredentialProviderCompletionManager {

/**
* Completes the FIDO 2 registration process with the provided [result].
* Completes the credential registration process with the provided [result].
*/
fun completeFido2Registration(result: RegisterFido2CredentialResult)
fun completeCredentialRegistration(result: CreateCredentialResult)

/**
* Complete the FIDO 2 credential assertion process with the provided [result].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Intent
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.credentials.CreatePasswordResponse
import androidx.credentials.CreatePublicKeyCredentialResponse
import androidx.credentials.GetCredentialResponse
import androidx.credentials.PasswordCredential
Expand All @@ -15,9 +16,9 @@ import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.BeginGetCredentialResponse
import androidx.credentials.provider.PendingIntentHandler
import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult

/**
* Primary implementation of [CredentialProviderCompletionManager] when the build version is
Expand All @@ -28,11 +29,11 @@ class CredentialProviderCompletionManagerImpl(
private val activity: Activity,
) : CredentialProviderCompletionManager {

override fun completeFido2Registration(result: RegisterFido2CredentialResult) {
override fun completeCredentialRegistration(result: CreateCredentialResult) {
activity.also {
val intent = Intent()
when (result) {
is RegisterFido2CredentialResult.Error -> {
is CreateCredentialResult.Error -> {
PendingIntentHandler
.setCreateCredentialException(
intent = intent,
Expand All @@ -42,7 +43,7 @@ class CredentialProviderCompletionManagerImpl(
)
}

is RegisterFido2CredentialResult.Success -> {
is CreateCredentialResult.Success.Fido2CredentialRegistered -> {
PendingIntentHandler
.setCreateCredentialResponse(
intent = intent,
Expand All @@ -52,7 +53,15 @@ class CredentialProviderCompletionManagerImpl(
)
}

is RegisterFido2CredentialResult.Cancelled -> {
is CreateCredentialResult.Success.PasswordCreated -> {
PendingIntentHandler
.setCreateCredentialResponse(
intent = intent,
response = CreatePasswordResponse(),
)
}

is CreateCredentialResult.Cancelled -> {
PendingIntentHandler
.setCreateCredentialException(
intent = intent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ package com.x8bit.bitwarden.ui.credentials.manager
import androidx.credentials.CredentialProvider
import com.bitwarden.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult

/**
* A no-op implementation of [CredentialProviderCompletionManagerImpl] provided when the build
* version is below UPSIDE_DOWN_CAKE (34). These versions do not support [CredentialProvider].
*/
@OmitFromCoverage
object CredentialProviderCompletionManagerUnsupportedApiImpl : CredentialProviderCompletionManager {
override fun completeFido2Registration(result: RegisterFido2CredentialResult) = Unit
override fun completeCredentialRegistration(result: CreateCredentialResult) = Unit

override fun completeFido2Assertion(result: AssertFido2CredentialResult) = Unit

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.x8bit.bitwarden.ui.credentials.manager.model

import com.bitwarden.ui.util.Text

/**
* Represents the result of a credential creation attempt.
*/
sealed class CreateCredentialResult {

/**
* Represents a successful credential creation attempt.
*/
sealed class Success : CreateCredentialResult() {
/**
* Indicates that the FIDO2 registration was successful.
*/
data class Fido2CredentialRegistered(val responseJson: String) : Success()

/**
* Indicates that the Password creation was successful.
*/
data object PasswordCreated : Success()
}

/**
* Indicates that an error occurred during credential creation.
*/
data class Error(val message: Text) : CreateCredentialResult()

/**
* Indicates that credential creation was cancelled by the user.
*/
data object Cancelled : CreateCredentialResult()
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,22 @@ import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.ui.platform.resource.BitwardenString

/**
* A reusable dialog for confirming whether or not the user wants to overwrite an existing FIDO 2
* credential.
* A reusable dialog for confirming whether or not the user wants to overwrite an existing credential.
*
* @param onConfirmClick A callback for when the overwrite confirmation button is clicked.
* @param onDismissRequest A callback for when the dialog is requesting dismissal.
*/
@Suppress("MaxLineLength")
@Composable
fun BitwardenOverwritePasskeyConfirmationDialog(
fun BitwardenOverwriteCredentialConfirmationDialog(
title: String,
message: String,
onConfirmClick: () -> Unit,
onDismissRequest: () -> Unit,
) {
BitwardenTwoButtonDialog(
title = stringResource(id = BitwardenString.overwrite_passkey),
message = stringResource(id = BitwardenString.this_item_already_contains_a_passkey_are_you_sure_you_want_to_overwrite_the_current_passkey),
title = title,
message = message,
confirmButtonText = stringResource(id = BitwardenString.okay),
dismissButtonText = stringResource(id = BitwardenString.cancel),
onConfirmClick = onConfirmClick,
Expand Down
Loading
Loading