Skip to content

Commit e4e9836

Browse files
committed
[PM-20176] Display cipher favicon in passkey credential entry
If a passkey cipher contains a valid URI, the favicon will be displayed in the credential list bottom-sheet. Additionally, moved 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. - 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 8cbd736 commit e4e9836

File tree

15 files changed

+594
-461
lines changed

15 files changed

+594
-461
lines changed

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

+15-4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ 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
1920
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
2021
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
2122
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
@@ -41,8 +42,6 @@ object Fido2ProviderModule {
4142
fun provideCredentialProviderProcessor(
4243
@ApplicationContext context: Context,
4344
authRepository: AuthRepository,
44-
vaultRepository: VaultRepository,
45-
fido2CredentialStore: Fido2CredentialStore,
4645
fido2CredentialManager: Fido2CredentialManager,
4746
dispatcherManager: DispatcherManager,
4847
intentManager: IntentManager,
@@ -53,8 +52,6 @@ object Fido2ProviderModule {
5352
Fido2ProviderProcessorImpl(
5453
context,
5554
authRepository,
56-
vaultRepository,
57-
fido2CredentialStore,
5855
fido2CredentialManager,
5956
intentManager,
6057
clock,
@@ -66,14 +63,28 @@ object Fido2ProviderModule {
6663
@Provides
6764
@Singleton
6865
fun provideFido2CredentialManager(
66+
@ApplicationContext context: Context,
67+
intentManager: IntentManager,
68+
featureFlagManager: FeatureFlagManager,
69+
biometricsEncryptionManager: BiometricsEncryptionManager,
6970
vaultSdkSource: VaultSdkSource,
7071
fido2CredentialStore: Fido2CredentialStore,
7172
json: Json,
73+
environmentRepository: EnvironmentRepository,
74+
vaultRepository: VaultRepository,
75+
dispatcherManager: DispatcherManager,
7276
): Fido2CredentialManager =
7377
Fido2CredentialManagerImpl(
78+
context = context,
7479
vaultSdkSource = vaultSdkSource,
7580
fido2CredentialStore = fido2CredentialStore,
81+
intentManager = intentManager,
82+
featureFlagManager = featureFlagManager,
83+
biometricsEncryptionManager = biometricsEncryptionManager,
7684
json = json,
85+
environmentRepository = environmentRepository,
86+
vaultRepository = vaultRepository,
87+
dispatcherManager = dispatcherManager,
7788
)
7889

7990
@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

+201-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,158 @@ 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+
false,
288+
baseIconUrl,
289+
true,
290+
)
291+
val iconCompat = when (loginIconData) {
292+
is IconData.Local -> {
293+
IconCompat.createWithResource(context, loginIconData.iconRes)
294+
}
295+
296+
is IconData.Network -> {
297+
loginIconData.toIconCompat()
298+
}
299+
}
300+
301+
val pkEntryBuilder = PublicKeyCredentialEntry
302+
.Builder(
303+
context = context,
304+
username = autofillView.userNameForUi
305+
?: context.getString(R.string.no_username),
306+
pendingIntent = intentManager
307+
.createFido2GetCredentialPendingIntent(
308+
action = GET_PASSKEY_INTENT,
309+
userId = userId,
310+
credentialId = autofillView.credentialId.toString(),
311+
cipherId = autofillView.cipherId,
312+
requestCode = Random.nextInt(),
313+
),
314+
beginGetPublicKeyCredentialOption = option,
315+
)
316+
.setIcon(iconCompat.toIcon(context))
317+
318+
if (featureFlagManager
319+
.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)
320+
) {
321+
biometricsEncryptionManager
322+
.getOrCreateCipher(userId)
323+
?.let { cipher ->
324+
pkEntryBuilder
325+
.setBiometricPromptDataIfSupported(cipher = cipher)
326+
}
327+
}
328+
329+
pkEntryBuilder.build()
330+
}
331+
332+
private fun PublicKeyCredentialEntry.Builder.setBiometricPromptDataIfSupported(
333+
cipher: Cipher,
334+
): PublicKeyCredentialEntry.Builder =
335+
if (isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM)) {
336+
this
337+
} else {
338+
setBiometricPromptData(
339+
biometricPromptData = buildPromptDataWithCipher(cipher),
340+
)
341+
}
342+
343+
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
344+
private fun buildPromptDataWithCipher(
345+
cipher: Cipher,
346+
): BiometricPromptData = BiometricPromptData.Builder()
347+
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
348+
.setCryptoObject(BiometricPrompt.CryptoObject(cipher))
349+
.build()
350+
351+
@OmitFromCoverage
352+
private fun IconData.Network.toIconCompat(): IconCompat = try {
353+
val futureTargetBitmap = Glide
354+
.with(context)
355+
.asBitmap()
356+
.load(this.uri)
357+
.placeholder(R.drawable.ic_bw_passkey)
358+
.submit()
359+
360+
IconCompat.createWithBitmap(futureTargetBitmap.get())
361+
} catch (_: ExecutionException) {
362+
null
363+
} catch (_: InterruptedException) {
364+
null
365+
}
366+
?: IconCompat.createWithResource(
367+
context,
368+
this.fallbackIconRes,
369+
)
370+
171371
private suspend fun registerFido2CredentialForUnprivilegedApp(
172372
userId: String,
173373
callingAppInfo: CallingAppInfo,

0 commit comments

Comments
 (0)