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