Skip to content

Commit 23ef5b3

Browse files
authored
[PM-20508] Centralize passkey credential entry creation (#5033)
1 parent fe1fe77 commit 23ef5b3

File tree

20 files changed

+702
-478
lines changed

20 files changed

+702
-478
lines changed

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

Lines changed: 17 additions & 4 deletions
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

Lines changed: 11 additions & 0 deletions
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

Lines changed: 212 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,92 @@
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.annotation.WorkerThread
7+
import androidx.biometric.BiometricManager
8+
import androidx.biometric.BiometricPrompt
9+
import androidx.core.graphics.drawable.IconCompat
310
import androidx.credentials.CreatePublicKeyCredentialRequest
411
import androidx.credentials.GetPublicKeyCredentialOption
12+
import androidx.credentials.exceptions.GetCredentialUnknownException
13+
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
14+
import androidx.credentials.provider.BiometricPromptData
515
import androidx.credentials.provider.CallingAppInfo
16+
import androidx.credentials.provider.CredentialEntry
617
import androidx.credentials.provider.ProviderGetCredentialRequest
18+
import androidx.credentials.provider.PublicKeyCredentialEntry
19+
import com.bitwarden.core.annotation.OmitFromCoverage
20+
import com.bitwarden.core.data.repository.model.DataState
21+
import com.bitwarden.core.data.repository.util.takeUntilLoaded
22+
import com.bitwarden.core.data.util.asFailure
23+
import com.bitwarden.core.data.util.asSuccess
24+
import com.bitwarden.data.manager.DispatcherManager
25+
import com.bitwarden.data.repository.util.baseIconUrl
726
import com.bitwarden.fido.ClientData
27+
import com.bitwarden.fido.Fido2CredentialAutofillView
828
import com.bitwarden.fido.Origin
929
import com.bitwarden.fido.UnverifiedAssetLink
1030
import com.bitwarden.sdk.Fido2CredentialStore
1131
import com.bitwarden.vault.CipherView
32+
import com.bumptech.glide.Glide
33+
import com.x8bit.bitwarden.R
1234
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
1335
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
1436
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAssertionOptions
1537
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAttestationOptions
1638
import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement
39+
import com.x8bit.bitwarden.data.autofill.fido2.processor.GET_PASSKEY_INTENT
40+
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
41+
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
42+
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
43+
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
44+
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
1745
import com.x8bit.bitwarden.data.platform.util.getAppOrigin
1846
import com.x8bit.bitwarden.data.platform.util.getAppSigningSignatureFingerprint
1947
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
48+
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
2049
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
2150
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2CredentialRequest
2251
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
2352
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidAttestationResponse
2453
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
54+
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
55+
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
2556
import com.x8bit.bitwarden.ui.platform.base.util.prefixHttpsIfNecessaryOrNull
57+
import com.x8bit.bitwarden.ui.platform.components.model.IconData
58+
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
59+
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
60+
import kotlinx.coroutines.CoroutineScope
61+
import kotlinx.coroutines.flow.fold
62+
import kotlinx.coroutines.withContext
2663
import kotlinx.serialization.SerializationException
2764
import kotlinx.serialization.json.Json
2865
import timber.log.Timber
66+
import java.util.concurrent.ExecutionException
67+
import javax.crypto.Cipher
68+
import kotlin.random.Random
2969

3070
/**
3171
* Primary implementation of [Fido2CredentialManager].
3272
*/
33-
@Suppress("TooManyFunctions")
73+
@Suppress("TooManyFunctions", "LongParameterList")
3474
class Fido2CredentialManagerImpl(
75+
private val context: Context,
3576
private val vaultSdkSource: VaultSdkSource,
3677
private val fido2CredentialStore: Fido2CredentialStore,
78+
private val intentManager: IntentManager,
79+
private val featureFlagManager: FeatureFlagManager,
80+
private val biometricsEncryptionManager: BiometricsEncryptionManager,
3781
private val json: Json,
82+
private val vaultRepository: VaultRepository,
83+
private val environmentRepository: EnvironmentRepository,
84+
dispatcherManager: DispatcherManager,
3885
) : Fido2CredentialManager,
3986
Fido2CredentialStore by fido2CredentialStore {
4087

88+
private val ioScope = CoroutineScope(dispatcherManager.io)
89+
4190
override var isUserVerified: Boolean = false
4291

4392
override var authenticationAttempts: Int = 0
@@ -166,6 +215,168 @@ class Fido2CredentialManagerImpl(
166215
?.userVerification
167216
?: fallbackRequirement
168217

218+
override suspend fun getPublicKeyCredentialEntries(
219+
userId: String,
220+
option: BeginGetPublicKeyCredentialOption,
221+
): Result<List<CredentialEntry>> = withContext(ioScope.coroutineContext) {
222+
val options = getPasskeyAssertionOptionsOrNull(option.requestJson)
223+
?: return@withContext GetCredentialUnknownException("Invalid data.").asFailure()
224+
val relyingPartyId = options.relyingPartyId
225+
?: return@withContext GetCredentialUnknownException("Invalid data.").asFailure()
226+
227+
val cipherViews = vaultRepository
228+
.ciphersStateFlow
229+
.takeUntilLoaded()
230+
.fold(initial = emptyList<CipherView>()) { initial, dataState ->
231+
when (dataState) {
232+
is DataState.Loaded -> {
233+
dataState.data.filter { it.isActiveWithFido2Credentials }
234+
}
235+
236+
else -> emptyList()
237+
}
238+
}
239+
240+
if (cipherViews.isEmpty()) {
241+
return@withContext emptyList<CredentialEntry>().asSuccess()
242+
}
243+
244+
val decryptResult = vaultRepository
245+
.getDecryptedFido2CredentialAutofillViews(cipherViews)
246+
when (decryptResult) {
247+
is DecryptFido2CredentialAutofillViewResult.Error -> {
248+
GetCredentialUnknownException("Error decrypting credentials.")
249+
.asFailure()
250+
}
251+
252+
is DecryptFido2CredentialAutofillViewResult.Success -> {
253+
val baseIconUrl = environmentRepository
254+
.environment
255+
.environmentUrlData
256+
.baseIconUrl
257+
val autofillViews = decryptResult.fido2CredentialAutofillViews
258+
.filter { it.rpId == relyingPartyId }
259+
if (autofillViews.isEmpty()) {
260+
return@withContext emptyList<CredentialEntry>().asSuccess()
261+
}
262+
val cipherIdsToMatch = autofillViews
263+
.map { it.cipherId }
264+
.toSet()
265+
266+
cipherViews
267+
.filter { cipherView -> cipherView.id in cipherIdsToMatch }
268+
.associateWith { cipherView ->
269+
autofillViews.first { it.cipherId == cipherView.id }
270+
}
271+
.toPublicKeyCredentialEntryList(
272+
baseIconUrl = baseIconUrl,
273+
userId = userId,
274+
option = option,
275+
)
276+
.asSuccess()
277+
}
278+
}
279+
}
280+
281+
private suspend fun Map<CipherView, Fido2CredentialAutofillView>.toPublicKeyCredentialEntryList(
282+
baseIconUrl: String,
283+
userId: String,
284+
option: BeginGetPublicKeyCredentialOption,
285+
): List<PublicKeyCredentialEntry> = this.map { (cipherView, autofillView) ->
286+
val loginIconData = cipherView.login
287+
?.uris
288+
.toLoginIconData(
289+
// TODO: [PM-20176] Enable web icons in passkey credential entries
290+
// Leave web icons disabled until CredentialManager TransactionTooLargeExceptions
291+
// are addressed. See https://issuetracker.google.com/issues/355141766 for details.
292+
isIconLoadingDisabled = true,
293+
baseIconUrl = baseIconUrl,
294+
usePasskeyDefaultIcon = true,
295+
)
296+
val iconCompat = when (loginIconData) {
297+
is IconData.Local -> {
298+
IconCompat.createWithResource(context, loginIconData.iconRes)
299+
}
300+
301+
is IconData.Network -> {
302+
loginIconData.toIconCompat()
303+
}
304+
}
305+
306+
val pkEntryBuilder = PublicKeyCredentialEntry
307+
.Builder(
308+
context = context,
309+
username = autofillView.userNameForUi
310+
?: context.getString(R.string.no_username),
311+
pendingIntent = intentManager
312+
.createFido2GetCredentialPendingIntent(
313+
action = GET_PASSKEY_INTENT,
314+
userId = userId,
315+
credentialId = autofillView.credentialId.toString(),
316+
cipherId = autofillView.cipherId,
317+
isUserVerified = isUserVerified,
318+
requestCode = Random.nextInt(),
319+
),
320+
beginGetPublicKeyCredentialOption = option,
321+
)
322+
.setIcon(iconCompat.toIcon(context))
323+
324+
if (featureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)) {
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+
/**
356+
* Converts a network icon to an [IconCompat]. Performs a blocking network request to fetch the
357+
* icon, so only call this method from a background thread or coroutine.
358+
*/
359+
@OmitFromCoverage
360+
@WorkerThread
361+
private suspend fun IconData.Network.toIconCompat(): IconCompat = try {
362+
val futureTargetBitmap = Glide
363+
.with(context)
364+
.asBitmap()
365+
.load(this.uri)
366+
.placeholder(R.drawable.ic_bw_passkey)
367+
.submit()
368+
369+
IconCompat.createWithBitmap(futureTargetBitmap.get())
370+
} catch (_: ExecutionException) {
371+
null
372+
} catch (_: InterruptedException) {
373+
null
374+
}
375+
?: IconCompat.createWithResource(
376+
context,
377+
this.fallbackIconRes,
378+
)
379+
169380
private suspend fun registerFido2CredentialForUnprivilegedApp(
170381
userId: String,
171382
callingAppInfo: CallingAppInfo,

0 commit comments

Comments
 (0)