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