1 /*
2  * Copyright 2022 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 androidx.credentials
18 
19 import android.graphics.drawable.Icon
20 import android.os.Bundle
21 import android.text.TextUtils
22 import androidx.annotation.RequiresApi
23 import androidx.annotation.RestrictTo
24 import androidx.credentials.PublicKeyCredential.Companion.BUNDLE_KEY_SUBTYPE
25 import androidx.credentials.internal.FrameworkClassParsingException
26 
27 /**
28  * Base request class for registering a credential.
29  *
30  * An application can construct a subtype request and call [CredentialManager.createCredential] to
31  * launch framework UI flows to collect consent and any other metadata needed from the user to
32  * register a new user credential.
33  *
34  * @property type the credential type determined by the credential-type-specific subclass (e.g. the
35  *   type for [CreatePasswordRequest] is [PasswordCredential.TYPE_PASSWORD_CREDENTIAL] and for
36  *   [CreatePublicKeyCredentialRequest] is [PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL])
37  * @property credentialData the request data in the [Bundle] format
38  * @property candidateQueryData the partial request data in the [Bundle] format that will be sent to
39  *   the provider during the initial candidate query stage, which should not contain sensitive user
40  *   credential information (note: bundle keys in the form of `androidx.credentials.*` are reserved
41  *   for internal library use)
42  * @property isSystemProviderRequired true if must only be fulfilled by a system provider and false
43  *   otherwise
44  * @property isAutoSelectAllowed whether a create option will be automatically chosen if it is the
45  *   only one available to the user
46  * @property displayInfo the information to be displayed on the screen
47  * @property origin the origin of a different application if the request is being made on behalf of
48  *   that application (Note: for API level >=34, setting a non-null value for this parameter will
49  *   throw a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present)
50  * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
51  *   immediately when there is no available passkey registration offering instead of falling back to
52  *   discovering remote options, and false (preferred by default) otherwise
53  */
54 abstract class CreateCredentialRequest
55 internal constructor(
56     val type: String,
57     val credentialData: Bundle,
58     val candidateQueryData: Bundle,
59     val isSystemProviderRequired: Boolean,
60     val isAutoSelectAllowed: Boolean,
61     val displayInfo: DisplayInfo,
62     val origin: String?,
63     @get:JvmName("preferImmediatelyAvailableCredentials")
64     val preferImmediatelyAvailableCredentials: Boolean,
65 ) {
66     init {
67         credentialData.putBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, isAutoSelectAllowed)
68         credentialData.putBoolean(
69             BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
70             preferImmediatelyAvailableCredentials
71         )
72         candidateQueryData.putBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, isAutoSelectAllowed)
73     }
74 
75     /**
76      * Information that may be used for display purposes when rendering UIs to collect the user
77      * consent and choice.
78      *
79      * @property userId the user identifier of the created credential
80      * @property userDisplayName an optional display name in addition to the [userId] that may be
81      *   displayed next to the `userId` during the user consent to help your user better understand
82      *   the credential being created
83      */
84     class DisplayInfo
85     internal constructor(
86         val userId: CharSequence,
87         val userDisplayName: CharSequence?,
88         @get:RestrictTo(RestrictTo.Scope.LIBRARY) // used from java tests
89         val credentialTypeIcon: Icon?,
90         @get:RestrictTo(RestrictTo.Scope.LIBRARY) // used from java tests
91         val preferDefaultProvider: String?,
92     ) {
93 
94         /**
95          * Constructs a [DisplayInfo].
96          *
97          * @param userId the user id of the created credential
98          * @param userDisplayName an optional display name in addition to the [userId] that may be
99          *   displayed next to the `userId` during the user consent to help your user better
100          *   understand the credential being created
101          * @throws IllegalArgumentException If [userId] is empty
102          */
103         @JvmOverloads
104         constructor(
105             userId: CharSequence,
106             userDisplayName: CharSequence? = null,
107         ) : this(
108             userId,
109             userDisplayName,
110             null,
111             null,
112         )
113 
114         /**
115          * Constructs a [DisplayInfo].
116          *
117          * @param userId the user id of the created credential
118          * @param userDisplayName an optional display name in addition to the [userId] that may be
119          *   displayed next to the `userId` during the user consent to help your user better
120          *   understand the credential being created
121          * @param preferDefaultProvider the preferred default provider component name to prioritize
122          *   in the selection UI flows. Your app must have the permission
123          *   android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS to specify this, or it
124          *   would not take effect. Also this bit may not take effect for Android API level 33 and
125          *   below, depending on the pre-34 provider(s) you have chosen.
126          * @throws IllegalArgumentException If [userId] is empty
127          */
128         constructor(
129             userId: CharSequence,
130             userDisplayName: CharSequence?,
131             preferDefaultProvider: String?
132         ) : this(
133             userId,
134             userDisplayName,
135             null,
136             preferDefaultProvider,
137         )
138 
139         init {
<lambda>null140             require(userId.isNotEmpty()) { "userId should not be empty" }
141         }
142 
143         @RestrictTo(RestrictTo.Scope.LIBRARY) // used from java tests
144         @RequiresApi(23)
toBundlenull145         fun toBundle(): Bundle {
146             val bundle = Bundle()
147             bundle.putCharSequence(BUNDLE_KEY_USER_ID, userId)
148             if (!TextUtils.isEmpty(userDisplayName)) {
149                 bundle.putCharSequence(BUNDLE_KEY_USER_DISPLAY_NAME, userDisplayName)
150             }
151             if (!TextUtils.isEmpty(preferDefaultProvider)) {
152                 bundle.putString(BUNDLE_KEY_DEFAULT_PROVIDER, preferDefaultProvider)
153             }
154             // Today the type icon is determined solely within this library right before the
155             // request is passed into the framework. Later if needed a new API can be added for
156             // custom SDKs to supply their own credential type icons.
157             return bundle
158         }
159 
160         companion object {
161             @RestrictTo(RestrictTo.Scope.LIBRARY) // used from java tests
162             const val BUNDLE_KEY_REQUEST_DISPLAY_INFO =
163                 "androidx.credentials.BUNDLE_KEY_REQUEST_DISPLAY_INFO"
164             @RestrictTo(RestrictTo.Scope.LIBRARY) // used from java tests
165             const val BUNDLE_KEY_USER_ID = "androidx.credentials.BUNDLE_KEY_USER_ID"
166 
167             internal const val BUNDLE_KEY_USER_DISPLAY_NAME =
168                 "androidx.credentials.BUNDLE_KEY_USER_DISPLAY_NAME"
169             @RestrictTo(RestrictTo.Scope.LIBRARY) // used from java tests
170             const val BUNDLE_KEY_CREDENTIAL_TYPE_ICON =
171                 "androidx.credentials.BUNDLE_KEY_CREDENTIAL_TYPE_ICON"
172 
173             internal const val BUNDLE_KEY_DEFAULT_PROVIDER =
174                 "androidx.credentials.BUNDLE_KEY_DEFAULT_PROVIDER"
175 
176             /**
177              * Returns a RequestDisplayInfo from a [CreateCredentialRequest.credentialData] Bundle.
178              *
179              * It is recommended to construct a DisplayInfo by direct constructor calls, instead of
180              * using this API. This API should only be used by a small subset of system apps that
181              * reconstruct an existing object for user interactions such as collecting consents.
182              *
183              * @param from the raw display data in the Bundle format, retrieved from
184              *   [CreateCredentialRequest.credentialData]
185              */
186             @JvmStatic
187             @RequiresApi(23) // Icon dependency
createFromnull188             fun createFrom(from: Bundle): DisplayInfo {
189                 return try {
190                     val displayInfoBundle = from.getBundle(BUNDLE_KEY_REQUEST_DISPLAY_INFO)!!
191                     val userId = displayInfoBundle.getCharSequence(BUNDLE_KEY_USER_ID)
192                     val displayName =
193                         displayInfoBundle.getCharSequence(BUNDLE_KEY_USER_DISPLAY_NAME)
194                     @Suppress("DEPRECATION") // bundle.getParcelable(key)
195                     val icon: Icon? =
196                         displayInfoBundle.getParcelable(BUNDLE_KEY_CREDENTIAL_TYPE_ICON)
197                     val defaultProvider: String? =
198                         displayInfoBundle.getString(BUNDLE_KEY_DEFAULT_PROVIDER)
199                     DisplayInfo(userId!!, displayName, icon, defaultProvider)
200                 } catch (e: Exception) {
201                     throw IllegalArgumentException(e)
202                 }
203             }
204         }
205     }
206 
207     companion object {
208         @RestrictTo(RestrictTo.Scope.LIBRARY) // used from java tests
209         const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
210             "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
211         @RestrictTo(RestrictTo.Scope.LIBRARY) // used from java tests
212         const val BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED =
213             "androidx.credentials.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED"
214 
215         /**
216          * Parses the [request] into an instance of [CreateCredentialRequest].
217          *
218          * It is recommended to construct a CreateCredentialRequest by directly instantiating a
219          * CreateCredentialRequest subclass, instead of using this API. This API should only be used
220          * by a small subset of system apps that reconstruct an existing object for user
221          * interactions such as collecting consents.
222          *
223          * @param request the framework CreateCredentialRequest object
224          */
225         @JvmStatic
226         @RequiresApi(34)
createFromnull227         fun createFrom(
228             request: android.credentials.CreateCredentialRequest
229         ): CreateCredentialRequest {
230             return createFrom(
231                 request.type,
232                 request.credentialData,
233                 request.candidateQueryData,
234                 request.isSystemProviderRequired,
235                 request.origin
236             )
237         }
238 
239         /**
240          * Attempts to parse the raw data into one of [CreatePasswordRequest],
241          * [CreatePublicKeyCredentialRequest], and [CreateCustomCredentialRequest].
242          *
243          * It is recommended to construct a CreateCredentialRequest by directly instantiating a
244          * CreateCredentialRequest subclass, instead of using this API. This API should only be used
245          * by a small subset of system apps that reconstruct an existing object for user
246          * interactions such as collecting consents.
247          *
248          * @param type matches [CreateCredentialRequest.type]
249          * @param credentialData matches [CreateCredentialRequest.credentialData], the request data
250          *   in the [Bundle] format; this should be constructed and retrieved from the a given
251          *   [CreateCredentialRequest] itself and never be created from scratch
252          * @param candidateQueryData matches [CreateCredentialRequest.candidateQueryData], the
253          *   partial request data in the [Bundle] format that will be sent to the provider during
254          *   the initial candidate query stage; this should be constructed and retrieved from the a
255          *   given [CreateCredentialRequest] itself and never be created from scratch
256          * @param requireSystemProvider whether the request must only be fulfilled by a system
257          *   provider
258          * @param origin the origin of a different application if the request is being made on
259          *   behalf of that application
260          */
261         @JvmStatic
262         @JvmOverloads
263         @RequiresApi(23)
createFromnull264         fun createFrom(
265             type: String,
266             credentialData: Bundle,
267             candidateQueryData: Bundle,
268             requireSystemProvider: Boolean,
269             origin: String? = null,
270         ): CreateCredentialRequest {
271             return try {
272                 when (type) {
273                     PasswordCredential.TYPE_PASSWORD_CREDENTIAL ->
274                         CreatePasswordRequest.createFrom(credentialData, origin, candidateQueryData)
275                     PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
276                         when (credentialData.getString(BUNDLE_KEY_SUBTYPE)) {
277                             CreatePublicKeyCredentialRequest
278                                 .BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST ->
279                                 CreatePublicKeyCredentialRequest.createFrom(
280                                     credentialData,
281                                     origin,
282                                     candidateQueryData
283                                 )
284                             else -> throw FrameworkClassParsingException()
285                         }
286                     else -> throw FrameworkClassParsingException()
287                 }
288             } catch (e: FrameworkClassParsingException) {
289                 // Parsing failed but don't crash the process. Instead just output a request with
290                 // the raw framework values.
291                 CreateCustomCredentialRequest(
292                     type,
293                     credentialData,
294                     candidateQueryData,
295                     requireSystemProvider,
296                     DisplayInfo.createFrom(credentialData),
297                     credentialData.getBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, false),
298                     origin,
299                     credentialData.getBoolean(
300                         BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
301                         false
302                     ),
303                 )
304             }
305         }
306     }
307 }
308