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.content.ComponentName
20 import android.os.Bundle
21 import androidx.annotation.IntDef
22 import androidx.annotation.RequiresApi
23 import androidx.annotation.RestrictTo
24 import androidx.credentials.internal.FrameworkClassParsingException
25 
26 /**
27  * Base class for getting a specific type of credentials.
28  *
29  * [GetCredentialRequest] will be composed of a list of [CredentialOption] subclasses to indicate
30  * the specific credential types and configurations that your app accepts.
31  *
32  * The [typePriorityHint] value helps decide where the credential will be displayed on the selector.
33  * It is used with more importance than signals like 'last recently used' but with less importance
34  * than other signals, such as the ordering of displayed accounts. It is expected to be one of the
35  * defined [PriorityHints] constants. By default, [GetCustomCredentialOption] will have
36  * [PRIORITY_DEFAULT], [GetPasswordOption] will have [PRIORITY_PASSWORD_OR_SIMILAR] and
37  * [GetPublicKeyCredentialOption] will have [PRIORITY_PASSKEY_OR_SIMILAR]. It is expected that
38  * [GetCustomCredentialOption] types will remain unchanged unless strong reasons arise and cannot
39  * ever have [PRIORITY_PASSKEY_OR_SIMILAR]. Given passkeys prevent many security threats that other
40  * credentials do not, we enforce that nothing is shown higher than passkey types in order to
41  * provide end users with the safest credentials first. See the spec
42  * [here](https://w3c.github.io/webauthn/) for more information on passkeys.
43  *
44  * @property type the credential type determined by the credential-type-specific subclass (e.g. the
45  *   type for [GetPasswordOption] is [PasswordCredential.TYPE_PASSWORD_CREDENTIAL] and for
46  *   [GetPublicKeyCredentialOption] is [PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL])
47  * @property requestData the request data in the [Bundle] format
48  * @property candidateQueryData the partial request data in the [Bundle] format that will be sent to
49  *   the provider during the initial candidate query stage, which will not contain sensitive user
50  *   information
51  * @property isSystemProviderRequired true if must only be fulfilled by a system provider and false
52  *   otherwise
53  * @property isAutoSelectAllowed whether a credential entry will be automatically chosen if it is
54  *   the only one available option
55  * @property allowedProviders a set of provider service [ComponentName] allowed to receive this
56  *   option (Note: a [SecurityException] will be thrown if it is set as non-empty but your app does
57  *   not have android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS; empty means every
58  *   provider is eligible; for API level < 34, this property will not take effect and you should
59  *   control the allowed provider via
60  *   [library dependencies](https://developer.android.com/training/sign-in/passkeys#add-dependencies))
61  * @property typePriorityHint sets the priority of this entry, which defines how it appears in the
62  *   credential selector, with less precedence than account ordering but more precedence than last
63  *   used time; see [PriorityHints] for more information
64  */
65 @OptIn(ExperimentalDigitalCredentialApi::class)
66 abstract class CredentialOption
67 internal constructor(
68     val type: String,
69     val requestData: Bundle,
70     val candidateQueryData: Bundle,
71     val isSystemProviderRequired: Boolean,
72     val isAutoSelectAllowed: Boolean,
73     val allowedProviders: Set<ComponentName>,
74     val typePriorityHint: @PriorityHints Int,
75 ) {
76 
77     init {
78         requestData.putBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, isAutoSelectAllowed)
79         candidateQueryData.putBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, isAutoSelectAllowed)
80         requestData.putInt(BUNDLE_KEY_TYPE_PRIORITY_VALUE, typePriorityHint)
81         candidateQueryData.putInt(BUNDLE_KEY_TYPE_PRIORITY_VALUE, typePriorityHint)
82     }
83 
84     /** Display priority hint for each type of credentials. */
85     @Target(AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
86     @Retention(AnnotationRetention.SOURCE)
87     @RestrictTo(RestrictTo.Scope.LIBRARY)
88     @IntDef(
89         value =
90             [
91                 PRIORITY_PASSKEY_OR_SIMILAR,
92                 PRIORITY_OIDC_OR_SIMILAR,
93                 PRIORITY_PASSWORD_OR_SIMILAR,
94                 PRIORITY_DEFAULT
95             ]
96     )
97     annotation class PriorityHints
98 
99     companion object {
100         /** Value of display priority for passkeys or credentials of similar security level. */
101         const val PRIORITY_PASSKEY_OR_SIMILAR = 100
102         /** Value of display priority for OpenID credentials or those of similar security level. */
103         const val PRIORITY_OIDC_OR_SIMILAR = 500
104         /** Value of display priority for passwords or credentials of similar security level. */
105         const val PRIORITY_PASSWORD_OR_SIMILAR = 1000
106         /** Default value of display priority. */
107         const val PRIORITY_DEFAULT = 2000
108 
109         internal const val BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED =
110             "androidx.credentials.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED"
111 
112         internal const val BUNDLE_KEY_TYPE_PRIORITY_VALUE =
113             "androidx.credentials.BUNDLE_KEY_TYPE_PRIORITY_VALUE"
114 
extractAutoSelectValuenull115         internal fun extractAutoSelectValue(data: Bundle): Boolean {
116             return data.getBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED)
117         }
118 
119         /**
120          * Parses the [option] into an instance of [CredentialOption].
121          *
122          * It is recommended to construct a CredentialOption by directly instantiating a
123          * CredentialOption subclass, instead of using this API. This API should only be used by a
124          * small subset of system apps that reconstruct an existing object for user interactions
125          * such as collecting consents.
126          *
127          * @param option the framework CredentialOption object
128          */
129         @RequiresApi(34)
130         @JvmStatic
createFromnull131         fun createFrom(option: android.credentials.CredentialOption): CredentialOption {
132             return createFrom(
133                 option.type,
134                 option.credentialRetrievalData,
135                 option.candidateQueryData,
136                 option.isSystemProviderRequired,
137                 option.allowedProviders
138             )
139         }
140 
141         /**
142          * Parses the raw data into an instance of [CredentialOption].
143          *
144          * It is recommended to construct a CredentialOption by directly instantiating a
145          * CredentialOption subclass, instead of using this API. This API should only be used by a
146          * small subset of system apps that reconstruct an existing object for user interactions
147          * such as collecting consents.
148          *
149          * @param type matches [CredentialOption.type]
150          * @param requestData matches [CredentialOption.requestData], the request data in the
151          *   [Bundle] format; this should be constructed and retrieved from the a given
152          *   [CredentialOption] itself and never be created from scratch
153          * @param candidateQueryData matches [CredentialOption.candidateQueryData]; this should be
154          *   constructed and retrieved from the a given [CredentialOption] itself and never be
155          *   created from scratch
156          * @param requireSystemProvider matches [CredentialOption.isSystemProviderRequired]
157          * @param allowedProviders matches [CredentialOption.allowedProviders], empty means every
158          *   provider is eligible
159          */
160         @JvmStatic
createFromnull161         fun createFrom(
162             type: String,
163             requestData: Bundle,
164             candidateQueryData: Bundle,
165             requireSystemProvider: Boolean,
166             allowedProviders: Set<ComponentName>,
167         ): CredentialOption {
168             return try {
169                 when (type) {
170                     PasswordCredential.TYPE_PASSWORD_CREDENTIAL ->
171                         GetPasswordOption.createFrom(
172                             requestData,
173                             allowedProviders,
174                             candidateQueryData
175                         )
176                     PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
177                         when (requestData.getString(PublicKeyCredential.BUNDLE_KEY_SUBTYPE)) {
178                             GetPublicKeyCredentialOption
179                                 .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION ->
180                                 GetPublicKeyCredentialOption.createFrom(
181                                     requestData,
182                                     allowedProviders,
183                                     candidateQueryData
184                                 )
185                             else -> throw FrameworkClassParsingException()
186                         }
187                     DigitalCredential.TYPE_DIGITAL_CREDENTIAL ->
188                         GetDigitalCredentialOption.createFrom(
189                             requestData = requestData,
190                             candidateQueryData = candidateQueryData,
191                             requireSystemProvider = requireSystemProvider,
192                             allowedProviders = allowedProviders,
193                         )
194                     else -> throw FrameworkClassParsingException()
195                 }
196             } catch (e: FrameworkClassParsingException) {
197                 // Parsing failed but don't crash the process. Instead just output a request with
198                 // the raw framework values.
199                 GetCustomCredentialOption(
200                     requestData,
201                     type,
202                     candidateQueryData = candidateQueryData,
203                     isSystemProviderRequired = requireSystemProvider,
204                     isAutoSelectAllowed =
205                         requestData.getBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, false),
206                     allowedProviders = allowedProviders,
207                     typePriorityHint =
208                         requestData.getInt(BUNDLE_KEY_TYPE_PRIORITY_VALUE, PRIORITY_DEFAULT),
209                 )
210             }
211         }
212     }
213 }
214