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.credentials.internal.FrameworkClassParsingException
22 import androidx.credentials.internal.RequestValidationHelper
23 
24 /**
25  * A request to get passkeys from the user's public key credential provider.
26  *
27  * @property requestJson the request in JSON format in the standard webauthn web json shown
28  *   [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
29  * @property clientDataHash a clientDataHash value to sign over in place of assembling and hashing
30  *   clientDataJSON during the signature request; meaningful only if you have set the
31  *   [GetCredentialRequest.origin]
32  * @property typePriorityHint always sets the priority of this entry to
33  *   [CredentialOption.PRIORITY_PASSKEY_OR_SIMILAR], which defines how it appears in the credential
34  *   selector, with less precedence than account ordering but more precedence than last used time;
35  *   see [CredentialOption] for more information
36  */
37 class GetPublicKeyCredentialOption
38 private constructor(
39     val requestJson: String,
40     val clientDataHash: ByteArray?,
41     allowedProviders: Set<ComponentName>,
42     requestData: Bundle,
43     candidateQueryData: Bundle,
44     typePriorityCategory: @PriorityHints Int = PRIORITY_PASSKEY_OR_SIMILAR,
45 ) :
46     CredentialOption(
47         type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
48         requestData = requestData,
49         candidateQueryData = candidateQueryData,
50         isSystemProviderRequired = false,
51         isAutoSelectAllowed = true,
52         allowedProviders = allowedProviders,
53         typePriorityHint = typePriorityCategory,
54     ) {
55 
56     /**
57      * Constructs a [GetPublicKeyCredentialOption].
58      *
59      * @param requestJson the request in JSON format in the standard webauthn web json shown
60      *   [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
61      * @param clientDataHash a clientDataHash value to sign over in place of assembling and hashing
62      *   clientDataJSON during the signature request; set only if you have set the
63      *   [GetCredentialRequest.origin]
64      * @param allowedProviders a set of provider service [ComponentName] allowed to receive this
65      *   option (Note: a [SecurityException] will be thrown if it is set as non-empty but your app
66      *   does not have android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS; for API level <
67      *   34, this property will not take effect and you should control the allowed provider via
68      *   [library dependencies](https://developer.android.com/training/sign-in/passkeys#add-dependencies))
69      * @throws NullPointerException If [requestJson] is null
70      * @throws IllegalArgumentException If [requestJson] is empty, or if it is not a valid JSON
71      */
72     @JvmOverloads
73     constructor(
74         requestJson: String,
75         clientDataHash: ByteArray? = null,
76         allowedProviders: Set<ComponentName> = emptySet(),
77     ) : this(
78         requestJson = requestJson,
79         clientDataHash = clientDataHash,
80         allowedProviders = allowedProviders,
81         requestData = toRequestDataBundle(requestJson, clientDataHash),
82         candidateQueryData = toRequestDataBundle(requestJson, clientDataHash),
83     )
84 
85     init {
<lambda>null86         require(RequestValidationHelper.isValidJSON(requestJson)) {
87             "requestJson must not be empty, and must be a valid JSON"
88         }
89     }
90 
91     internal companion object {
92         internal const val BUNDLE_KEY_CLIENT_DATA_HASH =
93             "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
94         internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
95         internal const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION =
96             "androidx.credentials.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION"
97 
98         @JvmStatic
toRequestDataBundlenull99         internal fun toRequestDataBundle(
100             requestJson: String,
101             clientDataHash: ByteArray?,
102         ): Bundle {
103             val bundle = Bundle()
104             bundle.putString(
105                 PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
106                 BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION
107             )
108             bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
109             bundle.putByteArray(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
110             return bundle
111         }
112 
113         @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
114         // boolean value from being returned.
115         @JvmStatic
createFromnull116         internal fun createFrom(
117             data: Bundle,
118             allowedProviders: Set<ComponentName>,
119             candidateQueryData: Bundle,
120         ): GetPublicKeyCredentialOption {
121             try {
122                 val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
123                 val clientDataHash = data.getByteArray(BUNDLE_KEY_CLIENT_DATA_HASH)
124                 return GetPublicKeyCredentialOption(
125                     requestJson!!,
126                     clientDataHash,
127                     allowedProviders,
128                     requestData = data,
129                     candidateQueryData = candidateQueryData,
130                     typePriorityCategory =
131                         data.getInt(BUNDLE_KEY_TYPE_PRIORITY_VALUE, PRIORITY_PASSKEY_OR_SIMILAR),
132                 )
133             } catch (e: Exception) {
134                 throw FrameworkClassParsingException()
135             }
136         }
137     }
138 }
139