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