Skip to content

Commit 54dd5ca

Browse files
committed
[PM-20508] Centralize passkey credential entry creation
Move the responsibility of building credential entries from `Fido2ProviderProcessor` to `Fido2CredentialManager`. This change centralizes the credential handling and simplifies the code in `Fido2ProviderProcessor` by delegating this task. Key changes: - **Fido2CredentialManager:** - Implemented `getCredentialEntries()` to handle the retrieval and creation of `CredentialEntry` objects. - Now uses the `VaultRepository` to fetch and decrypt credential data. - Uses `EnvironmentRepository` to get base icon url. - Now uses Glide for network image loading (currently disabled). - Includes logic to handle biometric prompts. - **Fido2ProviderProcessor:** - Removed the logic for building `CredentialEntry`. - Now relies on `Fido2CredentialManager` to provide the credential entries. - Updated logic in `handleFido2GetCredentialsRequest` to delegate credential retrieval to manager. - **Fido2CompletionManager:** - Updated to use `CredentialEntry`. - Updated logic to use new `GetFido2CredentialsResult`. - **VaultItemListingViewModel:** - Updated logic in `handleFido2GetCredentialsRequest` to delegate credential retrieval to manager. - **Tests:** - Updated unit tests to reflect the changes in `Fido2ProviderProcessor`, `VaultItemListingScreenTest`, and `Fido2CompletionManager`. - **Dependencies:** - Removed redundant dependencies. - **Cleanup:** - Removed unnecessary code and comments. - **Module dependency:** - Added dispatcher and environment modules dependencies to Fido2Provider module. - **LocalManagerProvider:** - `Fido2CompletionManager` doesn't need `IntentManager` anymore.
1 parent 3131196 commit 54dd5ca

File tree

20 files changed

+651
-472
lines changed

20 files changed

+651
-472
lines changed

app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/di/Fido2ProviderModule.kt

+17-4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessorI
1616
import com.x8bit.bitwarden.data.platform.manager.AssetManager
1717
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
1818
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
19+
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
20+
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
1921
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
2022
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
2123
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
@@ -41,8 +43,6 @@ object Fido2ProviderModule {
4143
fun provideCredentialProviderProcessor(
4244
@ApplicationContext context: Context,
4345
authRepository: AuthRepository,
44-
vaultRepository: VaultRepository,
45-
fido2CredentialStore: Fido2CredentialStore,
4646
fido2CredentialManager: Fido2CredentialManager,
4747
dispatcherManager: DispatcherManager,
4848
intentManager: IntentManager,
@@ -53,8 +53,6 @@ object Fido2ProviderModule {
5353
Fido2ProviderProcessorImpl(
5454
context,
5555
authRepository,
56-
vaultRepository,
57-
fido2CredentialStore,
5856
fido2CredentialManager,
5957
intentManager,
6058
clock,
@@ -66,14 +64,29 @@ object Fido2ProviderModule {
6664
@Provides
6765
@Singleton
6866
fun provideFido2CredentialManager(
67+
@ApplicationContext context: Context,
68+
intentManager: IntentManager,
69+
featureFlagManager: FeatureFlagManager,
70+
biometricsEncryptionManager: BiometricsEncryptionManager,
6971
vaultSdkSource: VaultSdkSource,
7072
fido2CredentialStore: Fido2CredentialStore,
7173
json: Json,
74+
environmentRepository: EnvironmentRepository,
75+
settingsRepository: SettingsRepository,
76+
vaultRepository: VaultRepository,
77+
dispatcherManager: DispatcherManager,
7278
): Fido2CredentialManager =
7379
Fido2CredentialManagerImpl(
80+
context = context,
7481
vaultSdkSource = vaultSdkSource,
7582
fido2CredentialStore = fido2CredentialStore,
83+
intentManager = intentManager,
84+
featureFlagManager = featureFlagManager,
85+
biometricsEncryptionManager = biometricsEncryptionManager,
7686
json = json,
87+
environmentRepository = environmentRepository,
88+
vaultRepository = vaultRepository,
89+
dispatcherManager = dispatcherManager,
7790
)
7891

7992
@Provides

app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2CredentialManager.kt

+11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package com.x8bit.bitwarden.data.autofill.fido2.manager
22

33
import androidx.credentials.CreatePublicKeyCredentialRequest
44
import androidx.credentials.GetPublicKeyCredentialOption
5+
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
56
import androidx.credentials.provider.CallingAppInfo
7+
import androidx.credentials.provider.CredentialEntry
68
import androidx.credentials.provider.ProviderGetCredentialRequest
79
import com.bitwarden.vault.CipherView
810
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
@@ -94,4 +96,13 @@ interface Fido2CredentialManager {
9496
request: CreatePublicKeyCredentialRequest,
9597
fallbackRequirement: UserVerificationRequirement = UserVerificationRequirement.REQUIRED,
9698
): UserVerificationRequirement
99+
100+
/**
101+
* Retrieve a list of [CredentialEntry] objects representing vault items matching the given
102+
* request [option].
103+
*/
104+
suspend fun getPublicKeyCredentialEntries(
105+
userId: String,
106+
option: BeginGetPublicKeyCredentialOption,
107+
): Result<List<CredentialEntry>>
97108
}

app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2CredentialManagerImpl.kt

+205-1
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,91 @@
11
package com.x8bit.bitwarden.data.autofill.fido2.manager
22

3+
import android.content.Context
4+
import android.os.Build
5+
import androidx.annotation.RequiresApi
6+
import androidx.biometric.BiometricManager
7+
import androidx.biometric.BiometricPrompt
8+
import androidx.core.graphics.drawable.IconCompat
39
import androidx.credentials.CreatePublicKeyCredentialRequest
410
import androidx.credentials.GetPublicKeyCredentialOption
11+
import androidx.credentials.exceptions.GetCredentialUnknownException
12+
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
13+
import androidx.credentials.provider.BiometricPromptData
514
import androidx.credentials.provider.CallingAppInfo
15+
import androidx.credentials.provider.CredentialEntry
616
import androidx.credentials.provider.ProviderGetCredentialRequest
17+
import androidx.credentials.provider.PublicKeyCredentialEntry
18+
import com.bitwarden.core.annotation.OmitFromCoverage
19+
import com.bitwarden.core.data.repository.model.DataState
20+
import com.bitwarden.core.data.repository.util.takeUntilLoaded
21+
import com.bitwarden.core.data.util.asFailure
22+
import com.bitwarden.core.data.util.asSuccess
23+
import com.bitwarden.data.manager.DispatcherManager
24+
import com.bitwarden.data.repository.util.baseIconUrl
725
import com.bitwarden.fido.ClientData
26+
import com.bitwarden.fido.Fido2CredentialAutofillView
827
import com.bitwarden.fido.Origin
928
import com.bitwarden.fido.UnverifiedAssetLink
1029
import com.bitwarden.sdk.Fido2CredentialStore
1130
import com.bitwarden.vault.CipherView
31+
import com.bumptech.glide.Glide
32+
import com.x8bit.bitwarden.R
1233
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
1334
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
1435
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAssertionOptions
1536
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAttestationOptions
1637
import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement
38+
import com.x8bit.bitwarden.data.autofill.fido2.processor.GET_PASSKEY_INTENT
39+
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
40+
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
41+
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
42+
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
43+
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
1744
import com.x8bit.bitwarden.data.platform.util.getAppOrigin
1845
import com.x8bit.bitwarden.data.platform.util.getAppSigningSignatureFingerprint
1946
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
47+
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
2048
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
2149
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2CredentialRequest
2250
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
2351
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidAttestationResponse
2452
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
53+
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
54+
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
2555
import com.x8bit.bitwarden.ui.platform.base.util.prefixHttpsIfNecessaryOrNull
56+
import com.x8bit.bitwarden.ui.platform.components.model.IconData
57+
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
58+
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
59+
import kotlinx.coroutines.CoroutineScope
60+
import kotlinx.coroutines.flow.fold
61+
import kotlinx.coroutines.withContext
2662
import kotlinx.serialization.SerializationException
2763
import kotlinx.serialization.json.Json
2864
import timber.log.Timber
65+
import java.util.concurrent.ExecutionException
66+
import javax.crypto.Cipher
67+
import kotlin.random.Random
2968

3069
/**
3170
* Primary implementation of [Fido2CredentialManager].
3271
*/
33-
@Suppress("TooManyFunctions")
72+
@Suppress("TooManyFunctions", "LongParameterList")
3473
class Fido2CredentialManagerImpl(
74+
private val context: Context,
3575
private val vaultSdkSource: VaultSdkSource,
3676
private val fido2CredentialStore: Fido2CredentialStore,
77+
private val intentManager: IntentManager,
78+
private val featureFlagManager: FeatureFlagManager,
79+
private val biometricsEncryptionManager: BiometricsEncryptionManager,
3780
private val json: Json,
81+
private val vaultRepository: VaultRepository,
82+
private val environmentRepository: EnvironmentRepository,
83+
dispatcherManager: DispatcherManager,
3884
) : Fido2CredentialManager,
3985
Fido2CredentialStore by fido2CredentialStore {
4086

87+
private val ioScope = CoroutineScope(dispatcherManager.io)
88+
4189
override var isUserVerified: Boolean = false
4290

4391
override var authenticationAttempts: Int = 0
@@ -168,6 +216,162 @@ class Fido2CredentialManagerImpl(
168216
?.userVerification
169217
?: fallbackRequirement
170218

219+
override suspend fun getPublicKeyCredentialEntries(
220+
userId: String,
221+
option: BeginGetPublicKeyCredentialOption,
222+
): Result<List<CredentialEntry>> = withContext(ioScope.coroutineContext) {
223+
val options = getPasskeyAssertionOptionsOrNull(option.requestJson)
224+
?: return@withContext GetCredentialUnknownException("Invalid data.").asFailure()
225+
val relyingPartyId = options.relyingPartyId
226+
?: return@withContext GetCredentialUnknownException("Invalid data.").asFailure()
227+
228+
val cipherViews = vaultRepository
229+
.ciphersStateFlow
230+
.takeUntilLoaded()
231+
.fold(initial = emptyList<CipherView>()) { initial, dataState ->
232+
when (dataState) {
233+
is DataState.Loaded -> {
234+
dataState.data.filter { it.isActiveWithFido2Credentials }
235+
}
236+
237+
else -> initial
238+
}
239+
}
240+
241+
if (cipherViews.isEmpty()) {
242+
return@withContext emptyList<CredentialEntry>().asSuccess()
243+
}
244+
245+
val decryptResult = vaultRepository
246+
.getDecryptedFido2CredentialAutofillViews(cipherViews)
247+
when (decryptResult) {
248+
is DecryptFido2CredentialAutofillViewResult.Error -> {
249+
GetCredentialUnknownException("Error decrypting credentials.")
250+
.asFailure()
251+
}
252+
253+
is DecryptFido2CredentialAutofillViewResult.Success -> {
254+
val baseIconUrl = environmentRepository
255+
.environment
256+
.environmentUrlData
257+
.baseIconUrl
258+
val autofillViews = decryptResult.fido2CredentialAutofillViews
259+
.filter { it.rpId == relyingPartyId }
260+
cipherViews
261+
.filter { cipherView ->
262+
cipherView.id in autofillViews.map { autofillView -> autofillView.cipherId }
263+
}
264+
.associateWith { cipherView ->
265+
autofillViews.first { it.cipherId == cipherView.id }
266+
}
267+
.toPublicKeyCredentialEntryList(
268+
baseIconUrl = baseIconUrl,
269+
userId = userId,
270+
option = option,
271+
)
272+
.asSuccess()
273+
}
274+
}
275+
}
276+
277+
private fun Map<CipherView, Fido2CredentialAutofillView>.toPublicKeyCredentialEntryList(
278+
baseIconUrl: String,
279+
userId: String,
280+
option: BeginGetPublicKeyCredentialOption,
281+
): List<PublicKeyCredentialEntry> = this.map {
282+
val autofillView = it.value
283+
val cipherView = it.key
284+
val loginIconData = cipherView.login
285+
?.uris
286+
.toLoginIconData(
287+
// TODO: [PM-20176] Enable web icons in passkey credential entries
288+
// Leave web icons disabled until CredentialManager TransactionTooLargeExceptions
289+
// are addressed. See https://issuetracker.google.com/issues/355141766 for details.
290+
isIconLoadingDisabled = true,
291+
baseIconUrl = baseIconUrl,
292+
usePasskeyDefaultIcon = true,
293+
)
294+
val iconCompat = when (loginIconData) {
295+
is IconData.Local -> {
296+
IconCompat.createWithResource(context, loginIconData.iconRes)
297+
}
298+
299+
is IconData.Network -> {
300+
loginIconData.toIconCompat()
301+
}
302+
}
303+
304+
val pkEntryBuilder = PublicKeyCredentialEntry
305+
.Builder(
306+
context = context,
307+
username = autofillView.userNameForUi
308+
?: context.getString(R.string.no_username),
309+
pendingIntent = intentManager
310+
.createFido2GetCredentialPendingIntent(
311+
action = GET_PASSKEY_INTENT,
312+
userId = userId,
313+
credentialId = autofillView.credentialId.toString(),
314+
cipherId = autofillView.cipherId,
315+
isUserVerified = isUserVerified,
316+
requestCode = Random.nextInt(),
317+
),
318+
beginGetPublicKeyCredentialOption = option,
319+
)
320+
.setIcon(iconCompat.toIcon(context))
321+
322+
if (featureFlagManager
323+
.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)
324+
) {
325+
biometricsEncryptionManager
326+
.getOrCreateCipher(userId)
327+
?.let { cipher ->
328+
pkEntryBuilder
329+
.setBiometricPromptDataIfSupported(cipher = cipher)
330+
}
331+
}
332+
333+
pkEntryBuilder.build()
334+
}
335+
336+
private fun PublicKeyCredentialEntry.Builder.setBiometricPromptDataIfSupported(
337+
cipher: Cipher,
338+
): PublicKeyCredentialEntry.Builder =
339+
if (isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM)) {
340+
this
341+
} else {
342+
setBiometricPromptData(
343+
biometricPromptData = buildPromptDataWithCipher(cipher),
344+
)
345+
}
346+
347+
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
348+
private fun buildPromptDataWithCipher(
349+
cipher: Cipher,
350+
): BiometricPromptData = BiometricPromptData.Builder()
351+
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
352+
.setCryptoObject(BiometricPrompt.CryptoObject(cipher))
353+
.build()
354+
355+
@OmitFromCoverage
356+
private fun IconData.Network.toIconCompat(): IconCompat = try {
357+
val futureTargetBitmap = Glide
358+
.with(context)
359+
.asBitmap()
360+
.load(this.uri)
361+
.placeholder(R.drawable.ic_bw_passkey)
362+
.submit()
363+
364+
IconCompat.createWithBitmap(futureTargetBitmap.get())
365+
} catch (_: ExecutionException) {
366+
null
367+
} catch (_: InterruptedException) {
368+
null
369+
}
370+
?: IconCompat.createWithResource(
371+
context,
372+
this.fallbackIconRes,
373+
)
374+
171375
private suspend fun registerFido2CredentialForUnprivilegedApp(
172376
userId: String,
173377
callingAppInfo: CallingAppInfo,

0 commit comments

Comments
 (0)