1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.credentialmanager.ktx
18 
19 import android.app.slice.Slice
20 import android.content.ComponentName
21 import android.content.Context
22 import android.content.pm.PackageInfo
23 import android.content.pm.PackageManager
24 import android.credentials.Credential
25 import android.credentials.flags.Flags
26 import android.credentials.selection.AuthenticationEntry
27 import android.credentials.selection.Entry
28 import android.credentials.selection.GetCredentialProviderData
29 import android.graphics.drawable.Drawable
30 import android.os.Bundle
31 import android.text.TextUtils
32 import android.util.Log
33 import androidx.activity.result.IntentSenderRequest
34 import androidx.credentials.PasswordCredential
35 import androidx.credentials.PublicKeyCredential
36 import androidx.credentials.provider.Action
37 import androidx.credentials.provider.AuthenticationAction
38 import androidx.credentials.provider.CredentialEntry
39 import androidx.credentials.provider.CustomCredentialEntry
40 import androidx.credentials.provider.PasswordCredentialEntry
41 import androidx.credentials.provider.PublicKeyCredentialEntry
42 import androidx.credentials.provider.RemoteEntry
43 import com.android.credentialmanager.IS_AUTO_SELECTED_KEY
44 import com.android.credentialmanager.model.get.ActionEntryInfo
45 import com.android.credentialmanager.model.get.AuthenticationEntryInfo
46 import com.android.credentialmanager.model.get.CredentialEntryInfo
47 import com.android.credentialmanager.model.CredentialType
48 import com.android.credentialmanager.model.get.ProviderInfo
49 import com.android.credentialmanager.model.get.RemoteEntryInfo
50 import com.android.credentialmanager.shared.R
51 import com.android.credentialmanager.TAG
52 import com.android.credentialmanager.model.BiometricRequestInfo
53 import com.android.credentialmanager.model.EntryInfo
54 
55 const val CREDENTIAL_ENTRY_PREFIX = "androidx.credentials.provider.credentialEntry."
56 
57 fun EntryInfo.getIntentSenderRequest(
58     isAutoSelected: Boolean = false
59 ): IntentSenderRequest? {
60     val entryIntent = fillInIntent?.putExtra(IS_AUTO_SELECTED_KEY, isAutoSelected)
61 
62     return pendingIntent?.let{
63         IntentSenderRequest
64             .Builder(pendingIntent = it)
65             .setFillInIntent(entryIntent)
66             .build()
67     }
68 }
69 
70 // Returns the list (potentially empty) of enabled provider.
toProviderListnull71 fun List<GetCredentialProviderData>.toProviderList(
72     context: Context,
73 ): List<ProviderInfo> {
74     val providerList: MutableList<ProviderInfo> = mutableListOf()
75     this.forEach {
76         val providerLabelAndIcon = getServiceLabelAndIcon(
77             context.packageManager,
78             it.providerFlattenedComponentName
79         ) ?: return@forEach
80         val (providerLabel, providerIcon) = providerLabelAndIcon
81         providerList.add(
82             ProviderInfo(
83                 id = it.providerFlattenedComponentName,
84                 icon = providerIcon,
85                 displayName = providerLabel,
86                 credentialEntryList = getCredentialOptionInfoList(
87                     providerId = it.providerFlattenedComponentName,
88                     providerLabel = providerLabel,
89                     credentialEntries = it.credentialEntries,
90                     context = context
91                 ),
92                 authenticationEntryList = getAuthenticationEntryList(
93                     it.providerFlattenedComponentName,
94                     providerLabel,
95                     providerIcon,
96                     it.authenticationEntries),
97                 remoteEntry = getRemoteEntry(
98                     it.providerFlattenedComponentName,
99                     it.remoteEntry
100                 ),
101                 actionEntryList = getActionEntryList(
102                     it.providerFlattenedComponentName, it.actionChips, providerIcon
103                 ),
104             )
105         )
106     }
107     return providerList
108 }
109 
110 /**
111  * Note: caller required handle empty list due to parsing error.
112  */
getCredentialOptionInfoListnull113 private fun getCredentialOptionInfoList(
114     providerId: String,
115     providerLabel: String,
116     credentialEntries: List<Entry>,
117     context: Context,
118 ): List<CredentialEntryInfo> {
119     val result: MutableList<CredentialEntryInfo> = mutableListOf()
120     credentialEntries.forEach {
121         val credentialEntry = it.slice.credentialEntry
122         when (credentialEntry) {
123             is PasswordCredentialEntry -> {
124                 result.add(
125                     CredentialEntryInfo(
126                     providerId = providerId,
127                     providerDisplayName = providerLabel,
128                     entryKey = it.key,
129                     entrySubkey = it.subkey,
130                     pendingIntent = credentialEntry.pendingIntent,
131                     fillInIntent = it.frameworkExtrasIntent,
132                     credentialType = CredentialType.PASSWORD,
133                     rawCredentialType = PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
134                     credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(),
135                     userName = credentialEntry.username.toString(),
136                     displayName = credentialEntry.displayName?.toString(),
137                     icon = credentialEntry.icon.loadDrawable(context),
138                     shouldTintIcon = credentialEntry.hasDefaultIcon,
139                     lastUsedTimeMillis = credentialEntry.lastUsedTime,
140                     isAutoSelectable = credentialEntry.isAutoSelectAllowed &&
141                             credentialEntry.isAutoSelectAllowedFromOption,
142                     entryGroupId = credentialEntry.entryGroupId.toString(),
143                     isDefaultIconPreferredAsSingleProvider =
144                             credentialEntry.isDefaultIconPreferredAsSingleProvider,
145                     affiliatedDomain = credentialEntry.affiliatedDomain?.toString(),
146                     biometricRequest = retrieveEntryBiometricRequest(it,
147                         CREDENTIAL_ENTRY_PREFIX),
148                 )
149                 )
150             }
151             is PublicKeyCredentialEntry -> {
152                 result.add(
153                     CredentialEntryInfo(
154                     providerId = providerId,
155                     providerDisplayName = providerLabel,
156                     entryKey = it.key,
157                     entrySubkey = it.subkey,
158                     pendingIntent = credentialEntry.pendingIntent,
159                     fillInIntent = it.frameworkExtrasIntent,
160                     credentialType = CredentialType.PASSKEY,
161                     rawCredentialType = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
162                     credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(),
163                     userName = credentialEntry.username.toString(),
164                     displayName = credentialEntry.displayName?.toString(),
165                     icon = if (credentialEntry.hasDefaultIcon)
166                         context.getDrawable(R.drawable.ic_passkey_24)
167                     else credentialEntry.icon.loadDrawable(context),
168                     shouldTintIcon = credentialEntry.hasDefaultIcon,
169                     lastUsedTimeMillis = credentialEntry.lastUsedTime,
170                     isAutoSelectable = credentialEntry.isAutoSelectAllowed &&
171                             credentialEntry.isAutoSelectAllowedFromOption,
172                     entryGroupId = credentialEntry.entryGroupId.toString(),
173                     isDefaultIconPreferredAsSingleProvider =
174                             credentialEntry.isDefaultIconPreferredAsSingleProvider,
175                     affiliatedDomain = credentialEntry.affiliatedDomain?.toString(),
176                     biometricRequest = retrieveEntryBiometricRequest(it,
177                         CREDENTIAL_ENTRY_PREFIX),
178                 )
179                 )
180             }
181             is CustomCredentialEntry -> {
182                 result.add(
183                     CredentialEntryInfo(
184                     providerId = providerId,
185                     providerDisplayName = providerLabel,
186                     entryKey = it.key,
187                     entrySubkey = it.subkey,
188                     pendingIntent = credentialEntry.pendingIntent,
189                     fillInIntent = it.frameworkExtrasIntent,
190                     credentialType = CredentialType.UNKNOWN,
191                     rawCredentialType = credentialEntry.type,
192                     credentialTypeDisplayName =
193                     credentialEntry.typeDisplayName?.toString().orEmpty(),
194                     userName = credentialEntry.title.toString(),
195                     displayName = credentialEntry.subtitle?.toString(),
196                     icon = credentialEntry.icon.loadDrawable(context),
197                     shouldTintIcon = credentialEntry.hasDefaultIcon,
198                     lastUsedTimeMillis = credentialEntry.lastUsedTime,
199                     isAutoSelectable = credentialEntry.isAutoSelectAllowed &&
200                             credentialEntry.isAutoSelectAllowedFromOption,
201                     entryGroupId = credentialEntry.entryGroupId.toString(),
202                     isDefaultIconPreferredAsSingleProvider =
203                             credentialEntry.isDefaultIconPreferredAsSingleProvider,
204                     affiliatedDomain = credentialEntry.affiliatedDomain?.toString(),
205                     biometricRequest = retrieveEntryBiometricRequest(it,
206                         CREDENTIAL_ENTRY_PREFIX),
207                 )
208                 )
209             }
210             else -> Log.d(
211                 TAG,
212                 "Encountered unrecognized credential entry ${it.slice.spec?.type}"
213             )
214         }
215     }
216     return result
217 }
218 
219 /**
220  * This validates if the entry calling this method contains biometric info, and if so, returns a
221  * [BiometricRequestInfo]. Namely, the biometric flow must have at least the
222  * ALLOWED_AUTHENTICATORS bit passed from Jetpack.
223  * Note that the required values, such as the provider info's icon or display name, or the entries
224  * credential type or userName, and finally the display info's app name, are non-null and must
225  * exist to run through the flow.
226  *
227  * @param hintPrefix a string prefix indicating the type of entry being utilized, since both create
228  * and get flows utilize slice params; includes the final '.' before the name of the type (e.g.
229  * androidx.credentials.provider.credentialEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS must have
230  * 'hintPrefix' up to "androidx.credentials.provider.credentialEntry.")
231  */
retrieveEntryBiometricRequestnull232 fun retrieveEntryBiometricRequest(
233     entry: Entry,
234     hintPrefix: String
235 ): BiometricRequestInfo? {
236     // TODO(b/326243754) : When available, use the official jetpack structured typLo
237     val biometricPromptDataBundleKey = "SLICE_HINT_BIOMETRIC_PROMPT_DATA"
238     val biometricPromptDataBundle: Bundle = entry.slice.items.firstOrNull {
239         it.hasHint(hintPrefix + biometricPromptDataBundleKey)
240     }?.bundle ?: return null
241 
242     val allowedAuthConstantKey = "androidx.credentials.provider.BUNDLE_HINT_ALLOWED_AUTHENTICATORS"
243     val cryptoOpIdKey = "androidx.credentials.provider.BUNDLE_HINT_CRYPTO_OP_ID"
244 
245     if (!biometricPromptDataBundle.containsKey(allowedAuthConstantKey)) {
246         return null
247     }
248 
249     val allowedAuthenticators: Int = biometricPromptDataBundle.getInt(allowedAuthConstantKey)
250 
251     // This is optional and does not affect validating the biometric flow in any case
252     val opId: Long? = if (biometricPromptDataBundle.containsKey(cryptoOpIdKey))
253         biometricPromptDataBundle.getLong(cryptoOpIdKey) else null
254 
255     return BiometricRequestInfo(opId = opId, allowedAuthenticators = allowedAuthenticators)
256 }
257 
258 val Slice.credentialEntry: CredentialEntry?
259     get() =
260         try {
261             when (spec?.type) {
262                 Credential.TYPE_PASSWORD_CREDENTIAL -> PasswordCredentialEntry.fromSlice(this)!!
263                 PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
264                     PublicKeyCredentialEntry.fromSlice(this)!!
265 
266                 else -> CustomCredentialEntry.fromSlice(this)!!
267             }
268         } catch (e: Exception) {
269             // Try CustomCredentialEntry.fromSlice one last time in case the cause was a failed
270             // password / passkey parsing attempt.
271             CustomCredentialEntry.fromSlice(this)
272         }
273 
274 /**
275  * Note: caller required handle empty list due to parsing error.
276  */
getAuthenticationEntryListnull277 private fun getAuthenticationEntryList(
278     providerId: String,
279     providerDisplayName: String,
280     providerIcon: Drawable,
281     authEntryList: List<AuthenticationEntry>,
282 ): List<AuthenticationEntryInfo> {
283     val result: MutableList<AuthenticationEntryInfo> = mutableListOf()
284     authEntryList.forEach { entry ->
285         val structuredAuthEntry =
286             AuthenticationAction.fromSlice(entry.slice) ?: return@forEach
287 
288         val title: String =
289             structuredAuthEntry.title.toString().ifEmpty { providerDisplayName }
290 
291         result.add(
292             AuthenticationEntryInfo(
293             providerId = providerId,
294             entryKey = entry.key,
295             entrySubkey = entry.subkey,
296             pendingIntent = structuredAuthEntry.pendingIntent,
297             fillInIntent = entry.frameworkExtrasIntent,
298             title = title,
299             providerDisplayName = providerDisplayName,
300             icon = providerIcon,
301             isUnlockedAndEmpty = entry.status != AuthenticationEntry.STATUS_LOCKED,
302             isLastUnlocked =
303             entry.status == AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT
304         )
305         )
306     }
307     return result
308 }
309 
getRemoteEntrynull310 private fun getRemoteEntry(providerId: String, remoteEntry: Entry?): RemoteEntryInfo? {
311     if (remoteEntry == null) {
312         return null
313     }
314     val structuredRemoteEntry = RemoteEntry.fromSlice(remoteEntry.slice)
315         ?: return null
316     return RemoteEntryInfo(
317         providerId = providerId,
318         entryKey = remoteEntry.key,
319         entrySubkey = remoteEntry.subkey,
320         pendingIntent = structuredRemoteEntry.pendingIntent,
321         fillInIntent = remoteEntry.frameworkExtrasIntent,
322     )
323 }
324 
325 /**
326  * Note: caller required handle empty list due to parsing error.
327  */
getActionEntryListnull328 private fun getActionEntryList(
329     providerId: String,
330     actionEntries: List<Entry>,
331     providerIcon: Drawable,
332 ): List<ActionEntryInfo> {
333     val result: MutableList<ActionEntryInfo> = mutableListOf()
334     actionEntries.forEach {
335         val actionEntryUi = Action.fromSlice(it.slice) ?: return@forEach
336         result.add(
337             ActionEntryInfo(
338             providerId = providerId,
339             entryKey = it.key,
340             entrySubkey = it.subkey,
341             pendingIntent = actionEntryUi.pendingIntent,
342             fillInIntent = it.frameworkExtrasIntent,
343             title = actionEntryUi.title.toString(),
344             icon = providerIcon,
345             subTitle = actionEntryUi.subtitle?.toString(),
346         )
347         )
348     }
349     return result
350 }
351 
352 
353 
getServiceLabelAndIconnull354 private fun getServiceLabelAndIcon(
355     pm: PackageManager,
356     providerFlattenedComponentName: String
357 ): Pair<String, Drawable>? {
358     var providerLabel: String? = null
359     var providerIcon: Drawable? = null
360     val component = ComponentName.unflattenFromString(providerFlattenedComponentName)
361     if (component == null) {
362         // Test data has only package name not component name.
363         // For test data usage only.
364         try {
365             val pkgInfo = if (Flags.instantAppsEnabled()) {
366                 getPackageInfo(pm, providerFlattenedComponentName)
367             } else {
368                 pm.getPackageInfo(
369                     providerFlattenedComponentName,
370                     PackageManager.PackageInfoFlags.of(0)
371                 )
372             }
373             val applicationInfo = checkNotNull(pkgInfo.applicationInfo)
374             providerLabel =
375                 applicationInfo.loadSafeLabel(
376                     pm, 0f,
377                     TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
378                 ).toString()
379             providerIcon = applicationInfo.loadIcon(pm)
380         } catch (e: Exception) {
381             Log.e(TAG, "Provider package info not found", e)
382         }
383     } else {
384         try {
385             val si = pm.getServiceInfo(component, PackageManager.ComponentInfoFlags.of(0))
386             providerLabel = si.loadSafeLabel(
387                 pm, 0f,
388                 TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
389             ).toString()
390             providerIcon = si.loadIcon(pm)
391         } catch (e: PackageManager.NameNotFoundException) {
392             Log.e(TAG, "Provider service info not found", e)
393             // Added for mdoc use case where the provider may not need to register a service and
394             // instead only relies on the registration api.
395             try {
396                 val pkgInfo = if (Flags.instantAppsEnabled()) {
397                     getPackageInfo(pm, providerFlattenedComponentName)
398                 } else {
399                     pm.getPackageInfo(
400                         component.packageName,
401                         PackageManager.PackageInfoFlags.of(0)
402                     )
403                 }
404                 val applicationInfo = checkNotNull(pkgInfo.applicationInfo)
405                 providerLabel =
406                     applicationInfo.loadSafeLabel(
407                         pm, 0f,
408                         TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
409                     ).toString()
410                 providerIcon = applicationInfo.loadIcon(pm)
411             } catch (e: Exception) {
412                 Log.e(TAG, "Provider package info not found", e)
413             }
414         }
415     }
416     return if (providerLabel == null || providerIcon == null) {
417         Log.d(
418             TAG,
419             "Failed to load provider label/icon for provider $providerFlattenedComponentName"
420         )
421         null
422     } else {
423         Pair(providerLabel, providerIcon)
424     }
425 }
426 
getPackageInfonull427 private fun getPackageInfo(
428     pm: PackageManager,
429     packageName: String
430 ): PackageInfo {
431     val packageManagerFlags = PackageManager.MATCH_INSTANT
432 
433     return pm.getPackageInfo(
434         packageName,
435         PackageManager.PackageInfoFlags.of(
436             (packageManagerFlags).toLong())
437     )
438 }
439