1 /*
2  * Copyright 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 androidx.credentials.provider
18 
19 import android.content.pm.PackageInfo
20 import android.content.pm.Signature
21 import android.content.pm.SigningInfo
22 import android.os.Build
23 import android.os.Bundle
24 import androidx.annotation.DeprecatedSinceApi
25 import androidx.annotation.RequiresApi
26 import androidx.annotation.RestrictTo
27 import androidx.annotation.VisibleForTesting
28 import androidx.credentials.provider.utils.PrivilegedApp
29 import androidx.credentials.provider.utils.RequestValidationUtil
30 import java.security.MessageDigest
31 import org.json.JSONException
32 import org.json.JSONObject
33 
34 /**
35  * Information pertaining to the calling application.
36  *
37  * @property packageName the calling package name of the calling app
38  * @property signingInfo the signingInfo associated with the calling app, added at API level 28
39  * @property signingInfoCompat the signing information associated with the calling app, which can be
40  *   used across all Android API levels
41  */
42 class CallingAppInfo
43 private constructor(
44     val packageName: String,
45     internal val origin: String?,
46     val signingInfoCompat: SigningInfoCompat,
47     signingInfo: SigningInfo?,
48 ) {
49 
50     lateinit var signingInfo: SigningInfo
51         private set
52         @RequiresApi(28) get
53 
54     init {
55         if (Build.VERSION.SDK_INT >= 28) {
56             this.signingInfo = signingInfo!!
57         }
58     }
59 
60     /**
61      * Constructs an instance of [CallingAppInfo]
62      *
63      * @param packageName the calling package name of the calling app
64      * @param signingInfo the signingInfo associated with the calling app
65      * @param origin the origin of the calling app. This is only set when a privileged app like a
66      *   browser, calls on behalf of another application.
67      * @throws NullPointerException If [packageName] is null
68      * @throws NullPointerException If the class is initialized with a null [signingInfo] on Android
69      *   P and above
70      * @throws IllegalArgumentException If [packageName] is empty
71      */
72     @RequiresApi(28)
73     @VisibleForTesting
74     @JvmOverloads
75     constructor(
76         packageName: String,
77         signingInfo: SigningInfo,
78         origin: String? = null
79     ) : this(
80         packageName = packageName,
81         signingInfo = signingInfo,
82         origin = origin,
83         signingInfoCompat = SigningInfoCompat.fromSigningInfo(signingInfo)
84     )
85 
86     /**
87      * Constructs an instance of [CallingAppInfo]
88      *
89      * @param packageName the calling package name of the calling app
90      * @param signatures the app signatures, which should be retrieved from the app's
91      *   [PackageInfo.signatures]
92      * @param origin the origin of the calling app. This is only set when a privileged app like a
93      *   browser, calls on behalf of another application.
94      * @throws NullPointerException If [packageName] is null
95      * @throws NullPointerException If the class is initialized with a null [signingInfo] on Android
96      *   API 28 and above
97      * @throws IllegalArgumentException If [packageName] is empty
98      */
99     @JvmOverloads
100     @VisibleForTesting
101     @DeprecatedSinceApi(28, "Use the SigningInfo based constructor instead")
102     constructor(
103         packageName: String,
104         signatures: List<Signature>,
105         origin: String? = null
106     ) : this(packageName, origin, SigningInfoCompat.fromSignatures(signatures), null)
107 
108     companion object {
109         /**
110          * Constructs an instance of [CallingAppInfo]
111          *
112          * @param packageName the calling package name of the calling app
113          * @param signingInfo the signingInfo associated with the calling app
114          * @param origin the origin of the calling app. This is only set when a privileged app like
115          *   a browser, calls on behalf of another application.
116          * @throws NullPointerException If [packageName] is null
117          * @throws NullPointerException If the class is initialized with a null [signingInfo] on
118          *   Android P and above
119          * @throws IllegalArgumentException If [packageName] is empty
120          */
121         @RequiresApi(28)
122         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
createnull123         fun create(packageName: String, signingInfo: SigningInfo, origin: String? = null) =
124             CallingAppInfo(packageName, signingInfo, origin)
125 
126         /**
127          * Constructs an instance of [CallingAppInfo]
128          *
129          * @param packageName the calling package name of the calling app
130          * @param signatures the app signatures, which should be retrieved from the app's
131          *   [PackageInfo.signatures]
132          * @param origin the origin of the calling app. This is only set when a privileged app like
133          *   a browser, calls on behalf of another application.
134          * @throws NullPointerException If [packageName] is null
135          * @throws NullPointerException If the class is initialized with a null [signingInfo] on
136          *   Android API 28 and above
137          * @throws IllegalArgumentException If [packageName] is empty
138          */
139         @DeprecatedSinceApi(28, "Use the SigningInfo based constructor instead")
140         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
141         fun create(packageName: String, signatures: List<Signature>, origin: String? = null) =
142             CallingAppInfo(packageName, signatures, origin)
143 
144         internal const val EXTRA_CREDENTIAL_REQUEST_ORIGIN =
145             "androidx.credentials.provider.extra.CREDENTIAL_REQUEST_ORIGIN"
146         private const val EXTRA_CREDENTIAL_REQUEST_PACKAGE_NAME =
147             "androidx.credentials.provider.extra.CREDENTIAL_REQUEST_PACKAGE_NAME"
148         private const val EXTRA_CREDENTIAL_REQUEST_SIGNING_INFO =
149             "androidx.credentials.provider.extra.CREDENTIAL_REQUEST_SIGNING_INFO"
150         private const val EXTRA_CREDENTIAL_REQUEST_SIGNATURES =
151             "androidx.credentials.provider.extra.CREDENTIAL_REQUEST_SIGNATURES"
152 
153         internal fun Bundle.setCallingAppInfo(info: CallingAppInfo) {
154             this.putString(EXTRA_CREDENTIAL_REQUEST_ORIGIN, info.origin)
155             this.putString(EXTRA_CREDENTIAL_REQUEST_PACKAGE_NAME, info.packageName)
156             if (Build.VERSION.SDK_INT >= 28) {
157                 this.putParcelable(EXTRA_CREDENTIAL_REQUEST_SIGNING_INFO, info.signingInfo)
158             } else {
159                 this.putParcelableArray(
160                     EXTRA_CREDENTIAL_REQUEST_SIGNATURES,
161                     info.signingInfoCompat.signingCertificateHistory.toTypedArray()
162                 )
163             }
164         }
165 
166         @RestrictTo(RestrictTo.Scope.LIBRARY)
extractCallingAppInfonull167         fun extractCallingAppInfo(bundle: Bundle): CallingAppInfo? {
168             val origin = bundle.getString(EXTRA_CREDENTIAL_REQUEST_ORIGIN)
169             val packageName = bundle.getString(EXTRA_CREDENTIAL_REQUEST_PACKAGE_NAME) ?: return null
170             return if (Build.VERSION.SDK_INT >= 28) {
171                 @Suppress("DEPRECATION")
172                 val signingInfo: SigningInfo =
173                     bundle.getParcelable<SigningInfo>(EXTRA_CREDENTIAL_REQUEST_SIGNING_INFO)
174                         ?: return null
175                 create(packageName, signingInfo, origin)
176             } else {
177                 @Suppress("DEPRECATION")
178                 val signatures: List<Signature> =
179                     bundle.getParcelableArray(EXTRA_CREDENTIAL_REQUEST_SIGNATURES)?.map {
180                         it as Signature
181                     } ?: return null
182                 create(packageName, signatures, origin)
183             }
184         }
185     }
186 
187     /**
188      * Returns the origin of the calling app. This is only non-null if a privileged app like a
189      * browser calls Credential Manager APIs on behalf of another application.
190      *
191      * Additionally, in order to get the origin, the credential provider must provide an allowlist
192      * of privileged browsers/apps that it trusts. This allowlist must be in the form of a valid,
193      * non-empty JSON. The origin will only be returned if the [packageName] and the SHA256 hash of
194      * the newest signature obtained from the [signingInfo], is present in the
195      * [privilegedAllowlist].
196      *
197      * Packages that are signed with multiple signers will only receive the origin if all of the
198      * signatures are present in the [privilegedAllowlist].
199      *
200      * The format of this [privilegedAllowlist] JSON must adhere to the following sample.
201      *
202      * ```
203      * {"apps": [
204      *    {
205      *       "type": "android",
206      *       "info": {
207      *          "package_name": "com.example.myapp",
208      *          "signatures" : [
209      *          {"build": "release",
210      *              "cert_fingerprint_sha256": "59:0D:2D:7B:33:6A:BD:FB:54:CD:3D:8B:36:8C:5C:3A:
211      *              7D:22:67:5A:9A:85:9A:6A:65:47:FD:4C:8A:7C:30:32"
212      *          },
213      *          {"build": "userdebug",
214      *          "cert_fingerprint_sha256": "59:0D:2D:7B:33:6A:BD:FB:54:CD:3D:8B:36:8C:5C:3A:7D:
215      *          22:67:5A:9A:85:9A:6A:65:47:FD:4C:8A:7C:30:32"
216      *          }]
217      *       }
218      *     }
219      * ]}
220      * ```
221      *
222      * All keys in the JSON must be exactly as stated in the sample above. Note that if the build
223      * for a given fingerprint is specified as 'userdebug', that fingerprint will only be considered
224      * if the device is on a 'userdebug' build, as determined by [Build.TYPE].
225      *
226      * @throws IllegalArgumentException If [privilegedAllowlist] is empty, or an invalid JSON, or
227      *   does not follow the format detailed above
228      * @throws IllegalStateException If the origin is non-null, but the [packageName] and
229      *   [signingInfo] do not have a match in the [privilegedAllowlist]
230      */
getOriginnull231     fun getOrigin(privilegedAllowlist: String): String? {
232         if (!RequestValidationUtil.isValidJSON(privilegedAllowlist)) {
233             throw IllegalArgumentException(
234                 "privilegedAllowlist must not be " + "empty, and must be a valid JSON"
235             )
236         }
237         if (origin == null) {
238             // If origin is null, then this is not a privileged call
239             return origin
240         }
241         try {
242             if (
243                 isAppPrivileged(
244                     PrivilegedApp.extractPrivilegedApps(JSONObject(privilegedAllowlist))
245                 )
246             ) {
247                 return origin
248             }
249         } catch (_: JSONException) {
250             throw IllegalArgumentException("privilegedAllowlist must be formatted properly")
251         }
252         throw IllegalStateException(
253             "Origin is not being returned as the calling app did not" +
254                 "match the privileged allowlist"
255         )
256     }
257 
258     /**
259      * Returns true if the [origin] is populated, and false otherwise.
260      *
261      * Note that the [origin] is only populated if a privileged app like a browser calls Credential
262      * Manager APIs on behalf of another application.
263      */
isOriginPopulatednull264     fun isOriginPopulated(): Boolean {
265         return origin != null
266     }
267 
isAppPrivilegednull268     private fun isAppPrivileged(candidateApps: List<PrivilegedApp>): Boolean {
269         for (app in candidateApps) {
270             if (app.packageName == packageName) {
271                 return isAppPrivileged(app.fingerprints)
272             }
273         }
274         return false
275     }
276 
isAppPrivilegednull277     private fun isAppPrivileged(candidateFingerprints: Set<String>): Boolean {
278         return SignatureVerifier(signingInfoCompat)
279             .verifySignatureFingerprints(candidateFingerprints)
280     }
281 
282     init {
<lambda>null283         require(packageName.isNotEmpty()) { "packageName must not be empty" }
284     }
285 
286     private class SignatureVerifier(private val signingInfoCompat: SigningInfoCompat) {
287 
getSignatureFingerprintsnull288         private fun getSignatureFingerprints(): Set<String> {
289             val fingerprints = mutableSetOf<String>()
290             val apkContentsSigners = signingInfoCompat.apkContentsSigners
291             if (signingInfoCompat.hasMultipleSigners && apkContentsSigners.isNotEmpty()) {
292                 fingerprints.addAll(convertToFingerprints(apkContentsSigners))
293             } else if (signingInfoCompat.signingCertificateHistory.isNotEmpty()) {
294                 fingerprints.addAll(
295                     convertToFingerprints(listOf(signingInfoCompat.signingCertificateHistory[0]))
296                 )
297             }
298             return fingerprints
299         }
300 
convertToFingerprintsnull301         private fun convertToFingerprints(signatures: List<Signature>): Set<String> {
302             val fingerprints = mutableSetOf<String>()
303             for (signature in signatures) {
304                 val md = MessageDigest.getInstance("SHA-256")
305                 val digest = md.digest(signature.toByteArray())
306                 fingerprints.add(digest.joinToString(":") { "%02X".format(it) })
307             }
308             return fingerprints
309         }
310 
verifySignatureFingerprintsnull311         fun verifySignatureFingerprints(candidateSigFingerprints: Set<String>): Boolean {
312             val appSigFingerprints = getSignatureFingerprints()
313             return if (signingInfoCompat.hasMultipleSigners) {
314                 candidateSigFingerprints.containsAll(appSigFingerprints)
315             } else {
316                 candidateSigFingerprints.intersect(appSigFingerprints).isNotEmpty()
317             }
318         }
319     }
320 
equalsnull321     override fun equals(other: Any?): Boolean {
322         if (this === other) {
323             return true
324         }
325         if (other !is CallingAppInfo) {
326             return false
327         }
328         return packageName == other.packageName &&
329             origin == other.origin &&
330             signingInfoCompat == other.signingInfoCompat
331     }
332 
hashCodenull333     override fun hashCode(): Int {
334         var result = packageName.hashCode()
335         result = 31 * result + (origin?.hashCode() ?: 0)
336         result = 31 * result + signingInfoCompat.hashCode()
337         return result
338     }
339 }
340