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