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 android.content.pm.dex; 18 19 import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_DEX_METADATA; 20 import static android.content.pm.parsing.ApkLiteParseUtils.APK_FILE_EXTENSION; 21 22 import android.content.pm.parsing.ApkLiteParseUtils; 23 import android.content.pm.parsing.PackageLite; 24 import android.content.pm.parsing.result.ParseInput; 25 import android.content.pm.parsing.result.ParseResult; 26 import android.os.SystemProperties; 27 import android.util.ArrayMap; 28 import android.util.JsonReader; 29 import android.util.Log; 30 import android.util.jar.StrictJarFile; 31 32 import com.android.internal.annotations.VisibleForTesting; 33 import com.android.internal.security.VerityUtils; 34 35 import java.io.File; 36 import java.io.IOException; 37 import java.io.InputStream; 38 import java.io.InputStreamReader; 39 import java.io.UnsupportedEncodingException; 40 import java.nio.file.Files; 41 import java.nio.file.Paths; 42 import java.util.ArrayList; 43 import java.util.Collection; 44 import java.util.List; 45 import java.util.Map; 46 import java.util.zip.ZipEntry; 47 48 /** 49 * Helper class used to compute and validate the location of dex metadata files. 50 * 51 * @hide 52 */ 53 public class DexMetadataHelper { 54 public static final String TAG = "DexMetadataHelper"; 55 /** $> adb shell 'setprop log.tag.DexMetadataHelper VERBOSE' */ 56 public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 57 /** $> adb shell 'setprop pm.dexopt.dm.require_manifest true' */ 58 private static final String PROPERTY_DM_JSON_MANIFEST_REQUIRED = 59 "pm.dexopt.dm.require_manifest"; 60 /** $> adb shell 'setprop pm.dexopt.dm.require_fsverity true' */ 61 private static final String PROPERTY_DM_FSVERITY_REQUIRED = "pm.dexopt.dm.require_fsverity"; 62 63 private static final String DEX_METADATA_FILE_EXTENSION = ".dm"; 64 DexMetadataHelper()65 private DexMetadataHelper() {} 66 67 /** Return true if the given file is a dex metadata file. */ isDexMetadataFile(File file)68 public static boolean isDexMetadataFile(File file) { 69 return isDexMetadataPath(file.getName()); 70 } 71 72 /** Return true if the given path is a dex metadata path. */ isDexMetadataPath(String path)73 private static boolean isDexMetadataPath(String path) { 74 return path.endsWith(DEX_METADATA_FILE_EXTENSION); 75 } 76 77 /** 78 * Returns whether fs-verity is required to install a dex metadata 79 */ isFsVerityRequired()80 public static boolean isFsVerityRequired() { 81 return VerityUtils.isFsVeritySupported() 82 && SystemProperties.getBoolean(PROPERTY_DM_FSVERITY_REQUIRED, false); 83 } 84 85 /** 86 * Return the size (in bytes) of all dex metadata files associated with the given package. 87 */ getPackageDexMetadataSize(PackageLite pkg)88 public static long getPackageDexMetadataSize(PackageLite pkg) { 89 long sizeBytes = 0; 90 Collection<String> dexMetadataList = DexMetadataHelper.getPackageDexMetadata(pkg).values(); 91 for (String dexMetadata : dexMetadataList) { 92 sizeBytes += new File(dexMetadata).length(); 93 } 94 return sizeBytes; 95 } 96 97 /** 98 * Search for the dex metadata file associated with the given target file. 99 * If it exists, the method returns the dex metadata file; otherwise it returns null. 100 * 101 * Note that this performs a loose matching suitable to be used in the InstallerSession logic. 102 * i.e. the method will attempt to match the {@code dmFile} regardless of {@code targetFile} 103 * extension (e.g. 'foo.dm' will match 'foo' or 'foo.apk'). 104 */ findDexMetadataForFile(File targetFile)105 public static File findDexMetadataForFile(File targetFile) { 106 String dexMetadataPath = buildDexMetadataPathForFile(targetFile); 107 File dexMetadataFile = new File(dexMetadataPath); 108 return dexMetadataFile.exists() ? dexMetadataFile : null; 109 } 110 111 /** 112 * Return the dex metadata files for the given package as a map 113 * [code path -> dex metadata path]. 114 * 115 * NOTE: involves I/O checks. 116 */ getPackageDexMetadata(PackageLite pkg)117 private static Map<String, String> getPackageDexMetadata(PackageLite pkg) { 118 return buildPackageApkToDexMetadataMap(pkg.getAllApkPaths()); 119 } 120 121 /** 122 * Look up the dex metadata files for the given code paths building the map 123 * [code path -> dex metadata]. 124 * 125 * For each code path (.apk) the method checks if a matching dex metadata file (.dm) exists. 126 * If it does it adds the pair to the returned map. 127 * 128 * Note that this method will do a loose 129 * matching based on the extension ('foo.dm' will match 'foo.apk' or 'foo'). 130 * 131 * This should only be used for code paths extracted from a package structure after the naming 132 * was enforced in the installer. 133 */ buildPackageApkToDexMetadataMap( List<String> codePaths)134 public static Map<String, String> buildPackageApkToDexMetadataMap( 135 List<String> codePaths) { 136 ArrayMap<String, String> result = new ArrayMap<>(); 137 for (int i = codePaths.size() - 1; i >= 0; i--) { 138 String codePath = codePaths.get(i); 139 String dexMetadataPath = buildDexMetadataPathForFile(new File(codePath)); 140 141 if (Files.exists(Paths.get(dexMetadataPath))) { 142 result.put(codePath, dexMetadataPath); 143 } 144 } 145 146 return result; 147 } 148 149 /** 150 * Return the dex metadata path associated with the given code path. 151 * (replaces '.apk' extension with '.dm') 152 * 153 * @throws IllegalArgumentException if the code path is not an .apk. 154 */ buildDexMetadataPathForApk(String codePath)155 public static String buildDexMetadataPathForApk(String codePath) { 156 if (!ApkLiteParseUtils.isApkPath(codePath)) { 157 throw new IllegalStateException( 158 "Corrupted package. Code path is not an apk " + codePath); 159 } 160 return codePath.substring(0, codePath.length() - APK_FILE_EXTENSION.length()) 161 + DEX_METADATA_FILE_EXTENSION; 162 } 163 164 /** 165 * Return the dex metadata path corresponding to the given {@code targetFile} using a loose 166 * matching. 167 * i.e. the method will attempt to match the {@code dmFile} regardless of {@code targetFile} 168 * extension (e.g. 'foo.dm' will match 'foo' or 'foo.apk'). 169 */ buildDexMetadataPathForFile(File targetFile)170 private static String buildDexMetadataPathForFile(File targetFile) { 171 return ApkLiteParseUtils.isApkFile(targetFile) 172 ? buildDexMetadataPathForApk(targetFile.getPath()) 173 : targetFile.getPath() + DEX_METADATA_FILE_EXTENSION; 174 } 175 176 /** 177 * Validate that the given file is a dex metadata archive. 178 * This is just a validation that the file is a zip archive that contains a manifest.json 179 * with the package name and version code. 180 */ validateDexMetadataFile(ParseInput input, String dmaPath, String packageName, long versionCode)181 public static ParseResult validateDexMetadataFile(ParseInput input, String dmaPath, 182 String packageName, long versionCode) { 183 return validateDexMetadataFile(input, dmaPath, packageName, versionCode, 184 SystemProperties.getBoolean(PROPERTY_DM_JSON_MANIFEST_REQUIRED, false)); 185 } 186 187 @VisibleForTesting validateDexMetadataFile(ParseInput input, String dmaPath, String packageName, long versionCode, boolean requireManifest)188 public static ParseResult validateDexMetadataFile(ParseInput input, String dmaPath, 189 String packageName, long versionCode, boolean requireManifest) { 190 StrictJarFile jarFile = null; 191 192 if (DEBUG) { 193 Log.v(TAG, "validateDexMetadataFile: " + dmaPath + ", " + packageName + 194 ", " + versionCode); 195 } 196 197 try { 198 jarFile = new StrictJarFile(dmaPath, false, false); 199 return validateDexMetadataManifest(input, dmaPath, jarFile, packageName, versionCode, 200 requireManifest); 201 } catch (IOException e) { 202 return input.error(INSTALL_FAILED_BAD_DEX_METADATA, "Error opening " + dmaPath, e); 203 } finally { 204 if (jarFile != null) { 205 try { 206 jarFile.close(); 207 } catch (IOException ignored) { 208 } 209 } 210 } 211 } 212 213 /** Ensure that packageName and versionCode match the manifest.json in the .dm file */ validateDexMetadataManifest(ParseInput input, String dmaPath, StrictJarFile jarFile, String packageName, long versionCode, boolean requireManifest)214 private static ParseResult validateDexMetadataManifest(ParseInput input, String dmaPath, 215 StrictJarFile jarFile, String packageName, long versionCode, boolean requireManifest) 216 throws IOException { 217 if (!requireManifest) { 218 if (DEBUG) { 219 Log.v(TAG, "validateDexMetadataManifest: " + dmaPath 220 + " manifest.json check skipped"); 221 } 222 return input.success(null); 223 } 224 225 ZipEntry zipEntry = jarFile.findEntry("manifest.json"); 226 if (zipEntry == null) { 227 return input.error(INSTALL_FAILED_BAD_DEX_METADATA, 228 "Missing manifest.json in " + dmaPath); 229 } 230 InputStream inputStream = jarFile.getInputStream(zipEntry); 231 232 JsonReader reader; 233 try { 234 reader = new JsonReader(new InputStreamReader(inputStream, "UTF-8")); 235 } catch (UnsupportedEncodingException e) { 236 return input.error(INSTALL_FAILED_BAD_DEX_METADATA, 237 "Error opening manifest.json in " + dmaPath, e); 238 } 239 String jsonPackageName = null; 240 long jsonVersionCode = -1; 241 242 reader.beginObject(); 243 while (reader.hasNext()) { 244 String name = reader.nextName(); 245 if (name.equals("packageName")) { 246 jsonPackageName = reader.nextString(); 247 } else if (name.equals("versionCode")) { 248 jsonVersionCode = reader.nextLong(); 249 } else { 250 reader.skipValue(); 251 } 252 } 253 reader.endObject(); 254 255 if (jsonPackageName == null || jsonVersionCode == -1) { 256 return input.error(INSTALL_FAILED_BAD_DEX_METADATA, 257 "manifest.json in " + dmaPath 258 + " is missing 'packageName' and/or 'versionCode'"); 259 } 260 261 if (!jsonPackageName.equals(packageName)) { 262 return input.error(INSTALL_FAILED_BAD_DEX_METADATA, 263 "manifest.json in " + dmaPath + " has invalid packageName: " + jsonPackageName 264 + ", expected: " + packageName); 265 } 266 267 if (versionCode != jsonVersionCode) { 268 return input.error(INSTALL_FAILED_BAD_DEX_METADATA, 269 "manifest.json in " + dmaPath + " has invalid versionCode: " + jsonVersionCode 270 + ", expected: " + versionCode); 271 } 272 273 if (DEBUG) { 274 Log.v(TAG, "validateDexMetadataManifest: " + dmaPath + ", " + packageName + 275 ", " + versionCode + ": successful"); 276 } 277 return input.success(null); 278 } 279 280 /** 281 * Validates that all dex metadata paths in the given list have a matching apk. 282 * (for any foo.dm there should be either a 'foo' of a 'foo.apk' file). 283 * If that's not the case it throws {@code IllegalStateException}. 284 * 285 * This is used to perform a basic check during adb install commands. 286 * (The installer does not support stand alone .dm files) 287 */ validateDexPaths(String[] paths)288 public static void validateDexPaths(String[] paths) { 289 ArrayList<String> apks = new ArrayList<>(); 290 for (int i = 0; i < paths.length; i++) { 291 if (ApkLiteParseUtils.isApkPath(paths[i])) { 292 apks.add(paths[i]); 293 } 294 } 295 ArrayList<String> unmatchedDmFiles = new ArrayList<>(); 296 for (int i = 0; i < paths.length; i++) { 297 String dmPath = paths[i]; 298 if (isDexMetadataPath(dmPath)) { 299 boolean valid = false; 300 for (int j = apks.size() - 1; j >= 0; j--) { 301 if (dmPath.equals(buildDexMetadataPathForFile(new File(apks.get(j))))) { 302 valid = true; 303 break; 304 } 305 } 306 if (!valid) { 307 unmatchedDmFiles.add(dmPath); 308 } 309 } 310 } 311 if (!unmatchedDmFiles.isEmpty()) { 312 throw new IllegalStateException("Unmatched .dm files: " + unmatchedDmFiles); 313 } 314 } 315 316 } 317