Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-13789] add credential manager provider for passwords #4110

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
fc882d2
add BitwardenPasswordProviderService as CredentialProviderService forโ€ฆ
Nailik Oct 17, 2024
1f301f2
add intents for PasswordProviderProcessor
Nailik Oct 17, 2024
ab9161c
initial test to provide Password Credential Entry
Nailik Oct 17, 2024
ab361c9
PasswordIntentUtils according to Fido2IntentUtils
Nailik Oct 19, 2024
c3473b9
add SpecialCircumstance to handle PasswordCredential intents
Nailik Oct 19, 2024
cab85a9
add RootNavState for PasswordCredential
Nailik Oct 19, 2024
c74bc86
finish Save PasswordCredential via Credential Manager Api
Nailik Oct 19, 2024
5a6e528
GetPasswordCredentialRequest handling
Nailik Oct 20, 2024
b30ebd9
empty username/password instead of error on credential selection
Nailik Oct 20, 2024
94cd7af
renamed BitwardenFido2ProviderService.kt to BitwardenCredentialProvidโ€ฆ
Nailik Oct 20, 2024
1ec7bdd
combined fido2 and password credential provider into BitwardenCredentโ€ฆ
Nailik Oct 20, 2024
c0190c0
Revert "combined fido2 and password credential provider into Bitwardeโ€ฆ
Nailik Oct 20, 2024
9d422e2
remove unused
Nailik Oct 20, 2024
9398697
Revert "Revert "combined fido2 and password credential provider into โ€ฆ
Nailik Oct 20, 2024
07949fd
combine provider service
Nailik Oct 20, 2024
040e4f3
password assertion
Nailik Oct 20, 2024
52f02d1
rework Fido2CompletionManager.kt to CredentialCompletionManager to suโ€ฆ
Nailik Oct 20, 2024
0a838fa
WIP improve saving password credentials
Nailik Oct 20, 2024
12b37c5
WIP overwriting already present username/password combinations (dialogs)
Nailik Oct 20, 2024
bf7a725
dialogs
Nailik Oct 21, 2024
9d5e340
overwrite confirmation dialog state text
Nailik Oct 21, 2024
d13a582
removed unused import
Nailik Oct 21, 2024
6eb6907
reuse open bitwarden action
Nailik Oct 21, 2024
e9aae37
handle password error in unlock screen
Nailik Oct 21, 2024
38465ed
clean up
Nailik Oct 21, 2024
b344e71
fix manifest
Nailik Oct 21, 2024
4c7f7ae
comments
Nailik Oct 21, 2024
518d60a
reformat code
Nailik Oct 21, 2024
1dd1c4e
Merge branch 'main' into feature/add-password-credential-manager-provโ€ฆ
Nailik Oct 22, 2024
3525469
Merge branch 'main' into feature/add-password-credential-manager-provโ€ฆ
Nailik Nov 1, 2024
8538e57
Merge branch 'bitwarden:main' into feature/add-password-credential-maโ€ฆ
Nailik Nov 6, 2024
77f9f40
Merge branch 'main' into feature/add-password-credential-manager-provโ€ฆ
Nailik Nov 19, 2024
ebf448d
Merge branch 'main' into feature/add-password-credential-manager-provโ€ฆ
Nailik Nov 20, 2024
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
12 changes: 11 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,20 @@
<data android:host="*.bitwarden.pw" />
<data android:pathPattern="/redirect-connector.*" />
</intent-filter>
<intent-filter>
<action android:name="com.x8bit.bitwarden.data.autofill.credential.ACTION_UNLOCK_ACCOUNT" />

<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="com.x8bit.bitwarden.fido2.ACTION_CREATE_PASSKEY" />
<action android:name="com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY" />
<action android:name="com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT" />

<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="com.x8bit.bitwarden.data.autofill.password.ACTION_CREATE_PASSWORD" />
<action android:name="com.x8bit.bitwarden.data.autofill.password.ACTION_GET_PASSWORD" />

<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import androidx.annotation.Keep
import androidx.core.app.AppComponentFactory
import com.x8bit.bitwarden.data.autofill.BitwardenAutofillService
import com.x8bit.bitwarden.data.autofill.accessibility.BitwardenAccessibilityService
import com.x8bit.bitwarden.data.autofill.fido2.BitwardenFido2ProviderService
import com.x8bit.bitwarden.data.autofill.credential.BitwardenCredentialProviderService
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.tiles.BitwardenAutofillTileService
import com.x8bit.bitwarden.data.tiles.BitwardenGeneratorTileService
Expand All @@ -30,7 +30,7 @@ class BitwardenAppComponentFactory : AppComponentFactory() {
* * [BitwardenAccessibilityService]
* * [BitwardenAutofillService]
* * [BitwardenAutofillTileService]
* * [BitwardenFido2ProviderService]
* * [BitwardenCredentialProviderService]
* * [BitwardenVaultTileService]
* * [BitwardenGeneratorTileService]
*/
Expand Down Expand Up @@ -63,7 +63,7 @@ class BitwardenAppComponentFactory : AppComponentFactory() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
super.instantiateServiceCompat(
cl,
BitwardenFido2ProviderService::class.java.name,
BitwardenCredentialProviderService::class.java.name,
intent,
)
} else {
Expand Down
37 changes: 34 additions & 3 deletions app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2AssertionRequestOrNu
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2CredentialRequestOrNull
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2GetCredentialsRequestOrNull
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.password.util.getPasswordAssertionRequestOrNull
import com.x8bit.bitwarden.data.autofill.password.util.getPasswordCredentialRequestOrNull
import com.x8bit.bitwarden.data.autofill.password.util.getPasswordGetCredentialsRequestOrNull
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
Expand Down Expand Up @@ -257,10 +260,13 @@ class MainViewModel @Inject constructor(
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
val hasVaultShortcut = intent.isMyVaultShortcut
val hasAccountSecurityShortcut = intent.isAccountSecurityShortcut
val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull()
val completeRegistrationData = intent.getCompleteRegistrationDataIntentOrNull()
val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull()
val fido2CredentialAssertionRequest = intent.getFido2AssertionRequestOrNull()
val fido2GetCredentialsRequest = intent.getFido2GetCredentialsRequestOrNull()
val passwordCredentialRequestData = intent.getPasswordCredentialRequestOrNull()
val passwordCredentialAssertionRequest = intent.getPasswordAssertionRequestOrNull()
val passwordGetCredentialsRequest = intent.getPasswordGetCredentialsRequestOrNull()
when {
passwordlessRequestData != null -> {
authRepository.activeUserId?.let {
Expand Down Expand Up @@ -343,10 +349,35 @@ class MainViewModel @Inject constructor(
)
}

fido2GetCredentialsRequest != null -> {
fido2GetCredentialsRequest != null || passwordGetCredentialsRequest != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2GetCredentials(
SpecialCircumstance.GetCredentials(
fido2GetCredentialsRequest = fido2GetCredentialsRequest,
passwordGetCredentialsRequest = passwordGetCredentialsRequest,
)
}

passwordCredentialRequestData != null -> {
// Set the user's verification status when a new FIDO 2 request is received to force
// explicit verification if the user's vault is unlocked when the request is
// received.
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.PasswordSave(
passwordCredentialRequest = passwordCredentialRequestData,
)

// Switch accounts if the selected user is not the active user.
if (authRepository.activeUserId != null &&
authRepository.activeUserId != passwordCredentialRequestData.userId
) {
authRepository.switchAccount(passwordCredentialRequestData.userId)
}
}

passwordCredentialAssertionRequest != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.PasswordAssertion(
passwordAssertionRequest = passwordCredentialAssertionRequest,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.x8bit.bitwarden.data.autofill.fido2
package com.x8bit.bitwarden.data.autofill.credential

import android.os.Build
import android.os.CancellationSignal
Expand All @@ -14,11 +14,14 @@ import androidx.credentials.provider.BeginGetCredentialRequest
import androidx.credentials.provider.BeginGetCredentialResponse
import androidx.credentials.provider.CredentialProviderService
import androidx.credentials.provider.ProviderClearCredentialStateRequest
import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessor
import com.x8bit.bitwarden.data.autofill.credential.processor.BitwardenCredentialProcessor
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

const val UNLOCK_ACCOUNT_INTENT =
"com.x8bit.bitwarden.data.autofill.credential.ACTION_UNLOCK_ACCOUNT"

/**
* The [CredentialProviderService] for the app. This fulfills FIDO2 credential requests from other
* applications.
Expand All @@ -27,14 +30,14 @@ import javax.inject.Inject
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Keep
@AndroidEntryPoint
class BitwardenFido2ProviderService : CredentialProviderService() {
class BitwardenCredentialProviderService : CredentialProviderService() {

/**
* A processor to handle the FIDO2 credential fulfillment. We keep the service light because it
* isn't easily testable.
* A processor to handle the FIDO2 and/or Password credential fulfillment. We keep the service
* light because it isn't easily testable.
*/
@Inject
lateinit var processor: Fido2ProviderProcessor
lateinit var processor: BitwardenCredentialProcessor

override fun onBeginCreateCredentialRequest(
request: BeginCreateCredentialRequest,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.x8bit.bitwarden.data.autofill.credential.di

import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.autofill.credential.processor.BitwardenCredentialProcessor
import com.x8bit.bitwarden.data.autofill.credential.processor.BitwardenCredentialProcessorImpl
import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessor
import com.x8bit.bitwarden.data.autofill.password.processor.PasswordProviderProcessor
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

/**
* Provides dependencies within the credential package.
*/
@Module
@InstallIn(SingletonComponent::class)
object CredentialProviderModule {

@RequiresApi(Build.VERSION_CODES.S)
@Provides
@Singleton
fun provideCredentialProviderProcessor(
@ApplicationContext context: Context,
authRepository: AuthRepository,
intentManager: IntentManager,
fido2ProviderProcessor: Fido2ProviderProcessor,
passwordProviderProcessor: PasswordProviderProcessor,
dispatcherManager: DispatcherManager,
): BitwardenCredentialProcessor =
BitwardenCredentialProcessorImpl(
context,
authRepository,
intentManager,
fido2ProviderProcessor,
passwordProviderProcessor,
dispatcherManager,
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.x8bit.bitwarden.data.autofill.credential.model

import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.credentials.provider.Action
import com.x8bit.bitwarden.MainActivity
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.util.toPendingIntentMutabilityFlag
import kotlin.random.Random

fun getCredentialResponseAction(
context: Context,
) = Action(
title = context.getString(R.string.open_bitwarden),
pendingIntent = PendingIntent.getActivity(
context,
Random.nextInt(),
Intent(context, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT.toPendingIntentMutabilityFlag(),
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.x8bit.bitwarden.data.autofill.credential.processor

import android.os.CancellationSignal
import android.os.OutcomeReceiver
import androidx.credentials.exceptions.ClearCredentialException
import androidx.credentials.exceptions.CreateCredentialException
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.provider.BeginCreateCredentialRequest
import androidx.credentials.provider.BeginCreateCredentialResponse
import androidx.credentials.provider.BeginGetCredentialRequest
import androidx.credentials.provider.BeginGetCredentialResponse
import androidx.credentials.provider.ProviderClearCredentialStateRequest

/**
* A class to handle FIDO2 or Password credential request processing. This includes save and autofill requests.
*/
interface BitwardenCredentialProcessor {

/**
* Process the [BeginCreateCredentialRequest] and invoke the [callback] with the result.
*
* @param request The request data from the OS that contains data about the requesting provider.
* @param cancellationSignal signal for observing cancellation requests. The system will use
* this to notify us that the result is no longer needed and we should stop handling it in order
* to save our resources.
* @param callback the callback object to be used to notify the response or error
*/
fun processCreateCredentialRequest(
request: BeginCreateCredentialRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,
)

/**
* Process the [BeginGetCredentialRequest] and invoke the [callback] with the result.
*
* @param request data from the OS that contains data about the requesting provider.
* @param cancellationSignal signal for observing cancellation requests. The system will use
* this to notify us that the result is no longer needed and we should stop handling it in order
* to save our resources.
* @param callback the callback object to be used to notify the response or error
*/
fun processGetCredentialRequest(
request: BeginGetCredentialRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
)

/**
* Process the [ProviderClearCredentialStateRequest] and invoke the [callback] with the result.
*
* @param request The request data form the OS that contains data about the requesting provider.
* @param cancellationSignal signal for observing cancellation requests. The system will use
* this to notify us that the result is no longer needed and we should stop handling it in order
* to save our resources.
* @param callback the callback object to be used to notify the response or error
*/
fun processClearCredentialStateRequest(
request: ProviderClearCredentialStateRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<Void?, ClearCredentialException>,
)
}
Loading
Loading