1 /*
2  * Copyright (C) 2020 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.appdigest;
18 
19 import static androidx.core.appdigest.Checksum.TYPE_WHOLE_MD5;
20 import static androidx.core.appdigest.Checksum.TYPE_WHOLE_MERKLE_ROOT_4K_SHA256;
21 import static androidx.core.appdigest.Checksum.TYPE_WHOLE_SHA1;
22 import static androidx.core.appdigest.Checksum.TYPE_WHOLE_SHA256;
23 import static androidx.core.appdigest.Checksum.TYPE_WHOLE_SHA512;
24 
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.pm.ApplicationInfo;
28 import android.content.pm.PackageManager;
29 import android.os.Build;
30 import android.util.Log;
31 import android.util.Pair;
32 import android.util.SparseArray;
33 
34 import androidx.concurrent.futures.ResolvableFuture;
35 import androidx.core.util.Preconditions;
36 
37 import com.google.common.util.concurrent.ListenableFuture;
38 
39 import org.jspecify.annotations.NonNull;
40 import org.jspecify.annotations.Nullable;
41 
42 import java.io.File;
43 import java.io.FileInputStream;
44 import java.io.IOException;
45 import java.security.MessageDigest;
46 import java.security.NoSuchAlgorithmException;
47 import java.security.cert.Certificate;
48 import java.security.cert.CertificateEncodingException;
49 import java.util.ArrayList;
50 import java.util.Collections;
51 import java.util.List;
52 import java.util.concurrent.Executor;
53 
54 /**
55  * Provides checksums for Android applications.
56  */
57 public final class Checksums {
58     private static final String TAG = "Checksums";
59 
60     /**
61      * Trust any Installer to provide checksums for the package.
62      * @see #getChecksums
63      */
64     public static final @NonNull List<Certificate> TRUST_ALL = Collections.singletonList(null);
65 
66     /**
67      * Don't trust any Installer to provide checksums for the package.
68      * This effectively disables optimized Installer-enforced checksums.
69      * @see #getChecksums
70      */
71     public static final @NonNull List<Certificate> TRUST_NONE = Collections.singletonList(null);
72 
73     // MessageDigest algorithms.
74     private static final String ALGO_MD5 = "MD5";
75     private static final String ALGO_SHA1 = "SHA1";
76     private static final String ALGO_SHA256 = "SHA256";
77     private static final String ALGO_SHA512 = "SHA512";
78 
79     private static final int READ_CHUNK_SIZE = 128 * 1024;
80 
Checksums()81     private Checksums() {
82     }
83 
84     /**
85      * Returns the checksums for APKs within a package.
86      *
87      * By default returns all readily available checksums:
88      * - enforced by platform,
89      * - enforced by installer.
90      * If caller needs a specific checksum type, they can specify it as required.
91      *
92      * <b>Caution: Android can not verify installer-provided checksums. Make sure you specify
93      * trusted installers.</b>
94      *
95      * @param context The application or activity context.
96      * @param includeSplits whether to include checksums for non-base splits (26+).
97      * @param packageName whose checksums to return.
98      * @param required explicitly request the checksum types. Will incur significant
99      *                 CPU/memory/disk usage.
100      * @param trustedInstallers for checksums enforced by installer, which installers are to be
101      *                          trusted.
102      *                          {@link #TRUST_ALL} will return checksums from any installer,
103      *                          {@link #TRUST_NONE} disables optimized installer-enforced checksums,
104      *                          otherwise the list has to be non-empty list of certificates.
105      * @param executor for calculating checksums.
106      * @throws IllegalArgumentException if the list of trusted installer certificates is empty.
107      * @throws PackageManager.NameNotFoundException if a package with the given name cannot be
108      *                                              found on the system.
109      */
110     @SuppressWarnings("deprecation")
getChecksums(@onNull Context context, @NonNull String packageName, boolean includeSplits, final @Checksum.Type int required, @NonNull List<Certificate> trustedInstallers, @NonNull Executor executor)111     public static @NonNull ListenableFuture<Checksum[]> getChecksums(@NonNull Context context,
112             @NonNull String packageName, boolean includeSplits, final @Checksum.Type int required,
113             @NonNull List<Certificate> trustedInstallers, @NonNull Executor executor)
114             throws CertificateEncodingException, PackageManager.NameNotFoundException {
115         Preconditions.checkNotNull(context);
116         Preconditions.checkNotNull(packageName);
117         Preconditions.checkNotNull(trustedInstallers);
118         Preconditions.checkNotNull(executor);
119 
120         if (Build.VERSION.SDK_INT >= 31) {
121             return ChecksumsApiSImpl.getChecksums(context, packageName, includeSplits, required,
122                     trustedInstallers, executor);
123         }
124 
125         final ApplicationInfo applicationInfo =
126                 context.getPackageManager().getApplicationInfo(packageName, 0);
127         if (applicationInfo == null) {
128             throw new PackageManager.NameNotFoundException(packageName);
129         }
130 
131         final ResolvableFuture<Checksum[]> result = ResolvableFuture.create();
132 
133         if (required == 0) {
134             result.set(new Checksum[0]);
135             return result;
136         }
137 
138         final List<Pair<String, File>> filesToChecksum = new ArrayList<>();
139 
140         // Adding base split.
141         final String baseSplitName = null;
142         filesToChecksum.add(Pair.create(baseSplitName, new File(applicationInfo.sourceDir)));
143 
144         // Adding other splits.
145         if (Build.VERSION.SDK_INT >= 26 && includeSplits && applicationInfo.splitNames != null) {
146             for (int i = 0, size = applicationInfo.splitNames.length; i < size; ++i) {
147                 filesToChecksum.add(Pair.create(applicationInfo.splitNames[i],
148                         new File(applicationInfo.splitSourceDirs[i])));
149             }
150         }
151 
152         for (int i = 0, size = filesToChecksum.size(); i < size; ++i) {
153             final File file = filesToChecksum.get(i).second;
154             if (!file.exists()) {
155                 throw new IllegalStateException("File not found: " + file.getPath());
156             }
157         }
158 
159         final String installerPackageName;
160         if (Build.VERSION.SDK_INT >= 31) {
161             installerPackageName = ChecksumsApiSImpl.getInstallerPackageName(context, packageName);
162         } else {
163             installerPackageName = null;
164         }
165 
166         executor.execute(new Runnable() {
167             @Override
168             public void run() {
169                 getChecksumsSync(context, filesToChecksum, required, installerPackageName,
170                         trustedInstallers, result);
171             }
172         });
173         return result;
174     }
175 
176     /**
177      * Returns the checksums for an APK. Use {@link #getChecksums} unless the package has
178      * not yet been installed on the device.
179      * A possible use case is replying to {@link Intent#ACTION_PACKAGE_NEEDS_VERIFICATION}
180      * broadcast.
181      *
182      * By default returns all readily available checksums:
183      * - enforced by platform,
184      * - enforced by installer.
185      * If caller needs a specific checksum type, they can specify it as required.
186      *
187      * <b>Caution: Android can not verify installer-provided checksums. Make sure you specify
188      * trusted installers.</b>
189      *
190      * @param context The application or activity context.
191      * @param filePath whose checksums to return.
192      * @param required explicitly request the checksum types. Will incur significant
193      *                 CPU/memory/disk usage.
194      * @param installerPackageName package name of the installer of the file
195      * @param trustedInstallers for checksums enforced by installer, which installers are to be
196      *                          trusted.
197      *                          {@link #TRUST_ALL} will return checksums from any installer,
198      *                          {@link #TRUST_NONE} disables optimized installer-enforced checksums,
199      *                          otherwise the list has to be non-empty list of certificates.
200      * @param executor for calculating checksums.
201      * @throws IllegalArgumentException if the list of trusted installer certificates is empty.
202      */
getFileChecksums(@onNull Context context, @NonNull String filePath, final @Checksum.Type int required, @Nullable String installerPackageName, @NonNull List<Certificate> trustedInstallers, @NonNull Executor executor)203     public static @NonNull ListenableFuture<Checksum[]> getFileChecksums(@NonNull Context context,
204             @NonNull String filePath, final @Checksum.Type int required,
205             @Nullable String installerPackageName, @NonNull List<Certificate> trustedInstallers,
206             @NonNull Executor executor) {
207         Preconditions.checkNotNull(context);
208         Preconditions.checkNotNull(filePath);
209         Preconditions.checkNotNull(trustedInstallers);
210         Preconditions.checkNotNull(executor);
211 
212         final ResolvableFuture<Checksum[]> result = ResolvableFuture.create();
213 
214         final List<Pair<String, File>> filesToChecksum = new ArrayList<>();
215 
216         final String splitName = null;
217         filesToChecksum.add(Pair.create(splitName, new File(filePath)));
218 
219         executor.execute(new Runnable() {
220             @Override
221             public void run() {
222                 getChecksumsSync(context, filesToChecksum, required, installerPackageName,
223                         trustedInstallers, result);
224             }
225         });
226         return result;
227     }
228 
getChecksumsSync( @onNull Context context, @NonNull List<Pair<String, File>> filesToChecksum, @Checksum.Type int required, @Nullable String installerPackageName, @Nullable List<Certificate> trustedInstallers, ResolvableFuture<Checksum[]> result)229     static void getChecksumsSync(
230             @NonNull Context context,
231             @NonNull List<Pair<String, File>> filesToChecksum,
232             @Checksum.Type int required,
233             @Nullable String installerPackageName,
234             @Nullable List<Certificate> trustedInstallers,
235             ResolvableFuture<Checksum[]> result) {
236         List<Checksum> allChecksums = new ArrayList<>();
237         for (int i = 0, isize = filesToChecksum.size(); i < isize; ++i) {
238             final String split = filesToChecksum.get(i).first;
239             final File file = filesToChecksum.get(i).second;
240             try {
241                 final SparseArray<Checksum> checksums = new SparseArray<>();
242 
243                 if (Build.VERSION.SDK_INT >= 31) {
244                     try {
245                         ChecksumsApiSImpl.getInstallerChecksums(context, split, file, required,
246                                 installerPackageName, trustedInstallers, checksums);
247                     } catch (Throwable e) {
248                         Log.e(TAG, "Installer checksum calculation error", e);
249                     }
250                 }
251 
252                 getRequiredApkChecksums(split, file, required, checksums);
253 
254                 for (int j = 0, jsize = checksums.size(); j < jsize; ++j) {
255                     allChecksums.add(checksums.valueAt(j));
256                 }
257             } catch (Throwable e) {
258                 Log.e(TAG, "Required checksum calculation error", e);
259             }
260         }
261         result.set(allChecksums.toArray(new Checksum[allChecksums.size()]));
262     }
263 
264     /**
265      * Fetch or calculate checksums for the specific file.
266      */
267     @SuppressWarnings("deprecation") /* WHOLE_MD5, WHOLE_SHA1 */
getRequiredApkChecksums(String split, File file, @Checksum.Type int required, SparseArray<Checksum> checksums)268     private static void getRequiredApkChecksums(String split, File file,
269             @Checksum.Type int required, SparseArray<Checksum> checksums) {
270         // Manually calculating required checksums if not readily available.
271         if (isRequired(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, required, checksums)) {
272             try {
273                 byte[] generatedRootHash =
274                         VerityTreeBuilder.computeChunkVerityTreeAndDigest(file.getAbsolutePath());
275                 checksums.put(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256,
276                         new Checksum(split, TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, generatedRootHash));
277             } catch (IOException | NoSuchAlgorithmException e) {
278                 Log.e(TAG, "Error calculating TYPE_WHOLE_MERKLE_ROOT_4K_SHA256", e);
279             }
280         }
281 
282         calculateChecksumIfRequired(checksums, split, file, required, TYPE_WHOLE_MD5);
283         calculateChecksumIfRequired(checksums, split, file, required, TYPE_WHOLE_SHA1);
284         calculateChecksumIfRequired(checksums, split, file, required, TYPE_WHOLE_SHA256);
285         calculateChecksumIfRequired(checksums, split, file, required, TYPE_WHOLE_SHA512);
286     }
287 
288     @SuppressWarnings("deprecation") /* WHOLE_MD5, WHOLE_SHA1 */
getMessageDigestAlgoForChecksumType(int type)289     private static String getMessageDigestAlgoForChecksumType(int type)
290             throws NoSuchAlgorithmException {
291         switch (type) {
292             case TYPE_WHOLE_MD5:
293                 return ALGO_MD5;
294             case TYPE_WHOLE_SHA1:
295                 return ALGO_SHA1;
296             case TYPE_WHOLE_SHA256:
297                 return ALGO_SHA256;
298             case TYPE_WHOLE_SHA512:
299                 return ALGO_SHA512;
300             default:
301                 throw new NoSuchAlgorithmException("Invalid checksum type: " + type);
302         }
303     }
304 
isRequired(@hecksum.Type int type, @Checksum.Type int required, SparseArray<Checksum> checksums)305     static boolean isRequired(@Checksum.Type int type,
306             @Checksum.Type int required, SparseArray<Checksum> checksums) {
307         if ((required & type) == 0) {
308             return false;
309         }
310         if (checksums.indexOfKey(type) >= 0) {
311             return false;
312         }
313         return true;
314     }
315 
calculateChecksumIfRequired(SparseArray<Checksum> checksums, String split, File file, int required, int type)316     private static void calculateChecksumIfRequired(SparseArray<Checksum> checksums,
317             String split, File file, int required, int type) {
318         if (isRequired(type, required, checksums)) {
319             final byte[] checksum = getApkChecksum(file, type);
320             if (checksum != null) {
321                 checksums.put(type, new Checksum(split, type, checksum));
322             }
323         }
324     }
325 
getApkChecksum(File file, int type)326     private static byte[] getApkChecksum(File file, int type) {
327         try {
328             FileInputStream fis = new FileInputStream(file);
329             try {
330                 byte[] dataBytes = new byte[READ_CHUNK_SIZE];
331                 int nread = 0;
332 
333                 final String algo = getMessageDigestAlgoForChecksumType(type);
334                 MessageDigest md = MessageDigest.getInstance(algo);
335                 while ((nread = fis.read(dataBytes)) != -1) {
336                     md.update(dataBytes, 0, nread);
337                 }
338 
339                 return md.digest();
340             } finally {
341                 fis.close();
342             }
343         } catch (IOException e) {
344             Log.e(TAG, "Error reading " + file.getAbsolutePath() + " to compute hash.", e);
345             return null;
346         } catch (NoSuchAlgorithmException e) {
347             Log.e(TAG, "Device does not support MessageDigest algorithm", e);
348             return null;
349         }
350     }
351 }
352