1 /* 2 * Copyright 2018 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.core.content.pm; 18 19 import android.annotation.SuppressLint; 20 import android.content.pm.PackageInfo; 21 import android.content.pm.PackageManager; 22 import android.content.pm.Signature; 23 import android.content.pm.SigningInfo; 24 import android.os.Build; 25 26 import androidx.annotation.RequiresApi; 27 import androidx.annotation.Size; 28 29 import org.jspecify.annotations.NonNull; 30 import org.jspecify.annotations.Nullable; 31 32 import java.security.MessageDigest; 33 import java.security.NoSuchAlgorithmException; 34 import java.util.Arrays; 35 import java.util.Collections; 36 import java.util.List; 37 import java.util.Map; 38 import java.util.Set; 39 40 /** Helper for accessing features in {@link PackageInfo}. */ 41 public final class PackageInfoCompat { 42 /** 43 * Return {@link android.R.attr#versionCode} and {@link android.R.attr#versionCodeMajor} 44 * combined together as a single long value. The {@code versionCodeMajor} is placed in the 45 * upper 32 bits on Android P or newer, otherwise these bits are all set to 0. 46 * 47 * @see PackageInfo#getLongVersionCode() 48 */ 49 @SuppressWarnings("deprecation") getLongVersionCode(@onNull PackageInfo info)50 public static long getLongVersionCode(@NonNull PackageInfo info) { 51 if (Build.VERSION.SDK_INT >= 28) { 52 return Api28Impl.getLongVersionCode(info); 53 } 54 return info.versionCode; 55 } 56 57 /** 58 * Retrieve the {@link Signature} array for the given package. This returns some of 59 * certificates, depending on whether the package in question is multi-signed or has signing 60 * history. 61 * 62 * <note> 63 * <p> 64 * Security/identity verification should <b>not</b> be done with this method. This is only 65 * intended to return some array of certificates that correspond to a package. 66 * </p> 67 * <p> 68 * If verification if required, either use 69 * {@link #hasSignatures(PackageManager, String, Map, boolean)} or manually verify the set of 70 * certificates using {@link PackageManager#GET_SIGNING_CERTIFICATES} or 71 * {@link PackageManager#GET_SIGNATURES}. 72 * </p> 73 * </note> 74 * 75 * @param packageManager The {@link PackageManager} instance to query against. 76 * @param packageName The package to query the {@param packageManager} for. Query by app 77 * UID is only supported by manually choosing a package name 78 * returned in {@link PackageManager#getPackagesForUid(int)}. 79 * @return an array of certificates the app is signed with 80 * @throws PackageManager.NameNotFoundException if the package cannot be found through the 81 * provided {@param packageManager} 82 */ 83 @SuppressWarnings("deprecation") getSignatures(@onNull PackageManager packageManager, @NonNull String packageName)84 public static @NonNull List<Signature> getSignatures(@NonNull PackageManager packageManager, 85 @NonNull String packageName) throws PackageManager.NameNotFoundException { 86 Signature[] array; 87 if (Build.VERSION.SDK_INT >= 28) { 88 PackageInfo pkgInfo = packageManager.getPackageInfo(packageName, 89 PackageManager.GET_SIGNING_CERTIFICATES); 90 SigningInfo signingInfo = pkgInfo.signingInfo; 91 if (Api28Impl.hasMultipleSigners(signingInfo)) { 92 array = Api28Impl.getApkContentsSigners(signingInfo); 93 } else { 94 array = Api28Impl.getSigningCertificateHistory(signingInfo); 95 } 96 } else { 97 // Lint warning's vulnerability is explicitly not handled for this method. 98 @SuppressLint("PackageManagerGetSignatures") 99 PackageInfo pkgInfo = packageManager.getPackageInfo(packageName, 100 PackageManager.GET_SIGNATURES); 101 array = pkgInfo.signatures; 102 } 103 104 // Framework code implies nullable/empty, although it may be impossible in practice. 105 if (array == null) { 106 return Collections.emptyList(); 107 } else { 108 return Arrays.asList(array); 109 } 110 } 111 112 /** 113 * Check if a package on device contains set of a certificates. Supported types are raw X509 or 114 * SHA-256 bytes. 115 * 116 * @param packageManager The {@link PackageManager} instance to query against. 117 * @param packageName The package to query the {@param packageManager} for. Query by 118 * app UID is only supported by manually choosing a package name 119 * returned in {@link PackageManager#getPackagesForUid(int)}. 120 * @param certificatesAndType The bytes of the certificate mapped to the type, either 121 * {@link PackageManager#CERT_INPUT_RAW_X509} or 122 * {@link PackageManager#CERT_INPUT_SHA256}. A single or multiple 123 * certificates may be included. 124 * @param matchExact Whether or not to check for presence of all signatures exactly. 125 * If false, then the check will succeed if the query contains a 126 * subset of the package certificates. Matching exactly is strongly 127 * recommended when running on devices below 128 * {@link Build.VERSION_CODES#LOLLIPOP} due to the fake ID 129 * vulnerability that allows a package to be modified to include 130 * an unverified signature. 131 * @return true if the package is considered signed by the given certificate set, or false 132 * otherwise 133 * @throws PackageManager.NameNotFoundException if the package cannot be found through the 134 * provided {@param packageManager} 135 */ hasSignatures(@onNull PackageManager packageManager, @NonNull String packageName, @Size(min = 1) @NonNull Map<byte[], Integer> certificatesAndType, boolean matchExact)136 public static boolean hasSignatures(@NonNull PackageManager packageManager, 137 @NonNull String packageName, 138 @Size(min = 1) @NonNull Map<byte[], Integer> certificatesAndType, boolean matchExact) 139 throws PackageManager.NameNotFoundException { 140 // If empty is passed in, return false to prevent accidentally succeeding 141 if (certificatesAndType.isEmpty()) { 142 return false; 143 } 144 145 Set<byte[]> expectedCertBytes = certificatesAndType.keySet(); 146 147 // The type has to be checked before any API level branching. If a new type is ever added, 148 // this code should fail and will have to be updated manually. To do otherwise would 149 // introduce a behavioral difference between the API level that added the new type and 150 // devices on prior API levels, which may not be caught by a developer calling this 151 // method if they do not test on an old API level. 152 for (byte[] bytes : expectedCertBytes) { 153 if (bytes == null) { 154 throw new IllegalArgumentException("Cert byte array cannot be null when verifying " 155 + packageName); 156 } 157 Integer type = certificatesAndType.get(bytes); 158 if (type == null) { 159 throw new IllegalArgumentException("Type must be specified for cert when verifying " 160 + packageName); 161 } 162 163 switch (type) { 164 case PackageManager.CERT_INPUT_RAW_X509: 165 case PackageManager.CERT_INPUT_SHA256: 166 break; 167 default: 168 throw new IllegalArgumentException("Unsupported certificate type " + type 169 + " when verifying " + packageName); 170 } 171 } 172 173 // getSignatures is called first to throw NameNotFoundException if necessary 174 final List<Signature> signers = getSignatures(packageManager, packageName); 175 176 // The vulnerability requiring matchExact is not necessary on P, but the signatures 177 // must still be checked manually in order to match the behavior described by the 178 // method. Otherwise matchExact == true will allow additional certificates if run 179 // on a device >= P. 180 if (!matchExact && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 181 // If not matching exact, delegate to the API 28 PackageManager API for checking 182 // individual certificates. This is less performant, but goes through a formally 183 // supported API. 184 for (byte[] bytes : expectedCertBytes) { 185 Integer type = certificatesAndType.get(bytes); 186 //noinspection ConstantConditions type cannot be null 187 if (!Api28Impl.hasSigningCertificate(packageManager, packageName, bytes, type)) { 188 return false; 189 } 190 } 191 192 return true; 193 } 194 195 // Fail if the query is larger than the actual set, or the size doesn't match and it should. 196 if (signers.size() == 0 197 || certificatesAndType.size() > signers.size() 198 || (matchExact && certificatesAndType.size() != signers.size())) { 199 return false; 200 } 201 202 @SuppressLint("InlinedApi") 203 boolean hasSha256 = certificatesAndType.containsValue(PackageManager.CERT_INPUT_SHA256); 204 byte[][] sha256Digests = null; 205 if (hasSha256) { 206 // Since the search does several array contains checks, cache the SHA256 digests here. 207 sha256Digests = new byte[signers.size()][]; 208 for (int index = 0; index < signers.size(); index++) { 209 sha256Digests[index] = computeSHA256Digest(signers.get(index).toByteArray()); 210 } 211 } 212 213 for (byte[] bytes : expectedCertBytes) { 214 Integer type = certificatesAndType.get(bytes); 215 //noinspection ConstantConditions type cannot be null 216 switch (type) { 217 case PackageManager.CERT_INPUT_RAW_X509: 218 // RAW_X509 is the type that Signatures are and always have been stored as, 219 // so defer to the Signature equals method for the platform. 220 Signature expectedSignature = new Signature(bytes); 221 if (!signers.contains(expectedSignature)) { 222 return false; 223 } 224 break; 225 case PackageManager.CERT_INPUT_SHA256: 226 // sha256Digests cannot be null due to pre-checked containsValue for its type 227 //noinspection ConstantConditions 228 if (!byteArrayContains(sha256Digests, bytes)) { 229 return false; 230 } 231 break; 232 default: 233 // Impossible to reach this point due to check at beginning of method. 234 throw new IllegalArgumentException("Unsupported certificate type " + type); 235 } 236 237 // If this point is reached, all searches have succeeded 238 return true; 239 } 240 241 return false; 242 } 243 byteArrayContains(byte @NonNull [][] array, byte @NonNull [] expected)244 private static boolean byteArrayContains(byte @NonNull [][] array, byte @NonNull [] expected) { 245 for (byte[] item : array) { 246 if (Arrays.equals(expected, item)) { 247 return true; 248 } 249 } 250 return false; 251 } 252 computeSHA256Digest(byte[] bytes)253 private static byte[] computeSHA256Digest(byte[] bytes) { 254 try { 255 return MessageDigest.getInstance("SHA256").digest(bytes); 256 } catch (NoSuchAlgorithmException e) { 257 // Can't happen, SHA256 required since API level 1 258 throw new RuntimeException("Device doesn't support SHA256 cert checking", e); 259 } 260 } 261 PackageInfoCompat()262 private PackageInfoCompat() { 263 } 264 265 @RequiresApi(28) 266 private static class Api28Impl { Api28Impl()267 private Api28Impl() { 268 // This class is not instantiable. 269 } 270 hasSigningCertificate(@onNull PackageManager packageManager, @NonNull String packageName, byte @NonNull [] bytes, int type)271 static boolean hasSigningCertificate(@NonNull PackageManager packageManager, 272 @NonNull String packageName, byte @NonNull [] bytes, int type) { 273 return packageManager.hasSigningCertificate(packageName, bytes, type); 274 } 275 hasMultipleSigners(@onNull SigningInfo signingInfo)276 static boolean hasMultipleSigners(@NonNull SigningInfo signingInfo) { 277 return signingInfo.hasMultipleSigners(); 278 } 279 getApkContentsSigners(@onNull SigningInfo signingInfo)280 static Signature @Nullable [] getApkContentsSigners(@NonNull SigningInfo signingInfo) { 281 return signingInfo.getApkContentsSigners(); 282 } 283 getSigningCertificateHistory( @onNull SigningInfo signingInfo)284 static Signature @Nullable [] getSigningCertificateHistory( 285 @NonNull SigningInfo signingInfo) { 286 return signingInfo.getSigningCertificateHistory(); 287 } 288 getLongVersionCode(PackageInfo packageInfo)289 static long getLongVersionCode(PackageInfo packageInfo) { 290 return packageInfo.getLongVersionCode(); 291 } 292 } 293 } 294