1 /* 2 * Copyright (C) 2016 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 com.android.apksig.apk; 18 19 import com.android.apksig.internal.apk.AndroidBinXmlParser; 20 import com.android.apksig.internal.apk.stamp.SourceStampConstants; 21 import com.android.apksig.internal.apk.v1.V1SchemeVerifier; 22 import com.android.apksig.internal.util.Pair; 23 import com.android.apksig.internal.zip.CentralDirectoryRecord; 24 import com.android.apksig.internal.zip.LocalFileRecord; 25 import com.android.apksig.internal.zip.ZipUtils; 26 import com.android.apksig.util.DataSource; 27 import com.android.apksig.zip.ZipFormatException; 28 29 import java.io.IOException; 30 import java.nio.ByteBuffer; 31 import java.nio.ByteOrder; 32 import java.util.Arrays; 33 import java.util.Comparator; 34 import java.util.List; 35 36 /** 37 * APK utilities. 38 */ 39 public abstract class ApkUtils { 40 41 /** 42 * Name of the Android manifest ZIP entry in APKs. 43 */ 44 public static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml"; 45 46 /** Name of the SourceStamp certificate hash ZIP entry in APKs. */ 47 public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = 48 SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME; 49 ApkUtils()50 private ApkUtils() {} 51 52 /** 53 * Finds the main ZIP sections of the provided APK. 54 * 55 * @throws IOException if an I/O error occurred while reading the APK 56 * @throws ZipFormatException if the APK is malformed 57 */ findZipSections(DataSource apk)58 public static ZipSections findZipSections(DataSource apk) 59 throws IOException, ZipFormatException { 60 com.android.apksig.zip.ZipSections zipSections = ApkUtilsLite.findZipSections(apk); 61 return new ZipSections( 62 zipSections.getZipCentralDirectoryOffset(), 63 zipSections.getZipCentralDirectorySizeBytes(), 64 zipSections.getZipCentralDirectoryRecordCount(), 65 zipSections.getZipEndOfCentralDirectoryOffset(), 66 zipSections.getZipEndOfCentralDirectory()); 67 } 68 69 /** 70 * Information about the ZIP sections of an APK. 71 */ 72 public static class ZipSections extends com.android.apksig.zip.ZipSections { ZipSections( long centralDirectoryOffset, long centralDirectorySizeBytes, int centralDirectoryRecordCount, long eocdOffset, ByteBuffer eocd)73 public ZipSections( 74 long centralDirectoryOffset, 75 long centralDirectorySizeBytes, 76 int centralDirectoryRecordCount, 77 long eocdOffset, 78 ByteBuffer eocd) { 79 super(centralDirectoryOffset, centralDirectorySizeBytes, centralDirectoryRecordCount, 80 eocdOffset, eocd); 81 } 82 } 83 84 /** 85 * Sets the offset of the start of the ZIP Central Directory in the APK's ZIP End of Central 86 * Directory record. 87 * 88 * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record 89 * @param offset offset of the ZIP Central Directory relative to the start of the archive. Must 90 * be between {@code 0} and {@code 2^32 - 1} inclusive. 91 */ setZipEocdCentralDirectoryOffset( ByteBuffer zipEndOfCentralDirectory, long offset)92 public static void setZipEocdCentralDirectoryOffset( 93 ByteBuffer zipEndOfCentralDirectory, long offset) { 94 ByteBuffer eocd = zipEndOfCentralDirectory.slice(); 95 eocd.order(ByteOrder.LITTLE_ENDIAN); 96 ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset); 97 } 98 99 /** 100 * Updates the length of EOCD comment. 101 * 102 * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record 103 */ updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory)104 public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) { 105 ByteBuffer eocd = zipEndOfCentralDirectory.slice(); 106 eocd.order(ByteOrder.LITTLE_ENDIAN); 107 ZipUtils.updateZipEocdCommentLen(eocd); 108 } 109 110 /** 111 * Returns the APK Signing Block of the provided {@code apk}. 112 * 113 * @throws ApkFormatException if the APK is not a valid ZIP archive 114 * @throws IOException if an I/O error occurs 115 * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK 116 * 117 * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2 118 * </a> 119 */ findApkSigningBlock(DataSource apk)120 public static ApkSigningBlock findApkSigningBlock(DataSource apk) 121 throws ApkFormatException, IOException, ApkSigningBlockNotFoundException { 122 ApkUtils.ZipSections inputZipSections; 123 try { 124 inputZipSections = ApkUtils.findZipSections(apk); 125 } catch (ZipFormatException e) { 126 throw new ApkFormatException("Malformed APK: not a ZIP archive", e); 127 } 128 return findApkSigningBlock(apk, inputZipSections); 129 } 130 131 /** 132 * Returns the APK Signing Block of the provided APK. 133 * 134 * @throws IOException if an I/O error occurs 135 * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK 136 * 137 * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2 138 * </a> 139 */ findApkSigningBlock(DataSource apk, ZipSections zipSections)140 public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections) 141 throws IOException, ApkSigningBlockNotFoundException { 142 ApkUtilsLite.ApkSigningBlock apkSigningBlock = ApkUtilsLite.findApkSigningBlock(apk, 143 zipSections); 144 return new ApkSigningBlock(apkSigningBlock.getStartOffset(), apkSigningBlock.getContents()); 145 } 146 147 /** 148 * Information about the location of the APK Signing Block inside an APK. 149 */ 150 public static class ApkSigningBlock extends ApkUtilsLite.ApkSigningBlock { 151 /** 152 * Constructs a new {@code ApkSigningBlock}. 153 * 154 * @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK 155 * Signing Block inside the APK file 156 * @param contents contents of the APK Signing Block 157 */ ApkSigningBlock(long startOffsetInApk, DataSource contents)158 public ApkSigningBlock(long startOffsetInApk, DataSource contents) { 159 super(startOffsetInApk, contents); 160 } 161 } 162 163 /** 164 * Returns the contents of the APK's {@code AndroidManifest.xml}. 165 * 166 * @throws IOException if an I/O error occurs while reading the APK 167 * @throws ApkFormatException if the APK is malformed 168 */ getAndroidManifest(DataSource apk)169 public static ByteBuffer getAndroidManifest(DataSource apk) 170 throws IOException, ApkFormatException { 171 ZipSections zipSections; 172 try { 173 zipSections = findZipSections(apk); 174 } catch (ZipFormatException e) { 175 throw new ApkFormatException("Not a valid ZIP archive", e); 176 } 177 List<CentralDirectoryRecord> cdRecords = 178 V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections); 179 CentralDirectoryRecord androidManifestCdRecord = null; 180 for (CentralDirectoryRecord cdRecord : cdRecords) { 181 if (ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(cdRecord.getName())) { 182 androidManifestCdRecord = cdRecord; 183 break; 184 } 185 } 186 if (androidManifestCdRecord == null) { 187 throw new ApkFormatException("Missing " + ANDROID_MANIFEST_ZIP_ENTRY_NAME); 188 } 189 DataSource lfhSection = apk.slice(0, zipSections.getZipCentralDirectoryOffset()); 190 191 try { 192 return ByteBuffer.wrap( 193 LocalFileRecord.getUncompressedData( 194 lfhSection, androidManifestCdRecord, lfhSection.size())); 195 } catch (ZipFormatException e) { 196 throw new ApkFormatException("Failed to read " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e); 197 } 198 } 199 200 /** 201 * Android resource ID of the {@code android:minSdkVersion} attribute in AndroidManifest.xml. 202 */ 203 private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c; 204 205 /** 206 * Android resource ID of the {@code android:debuggable} attribute in AndroidManifest.xml. 207 */ 208 private static final int DEBUGGABLE_ATTR_ID = 0x0101000f; 209 210 /** 211 * Android resource ID of the {@code android:targetSandboxVersion} attribute in 212 * AndroidManifest.xml. 213 */ 214 private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c; 215 216 /** 217 * Android resource ID of the {@code android:targetSdkVersion} attribute in 218 * AndroidManifest.xml. 219 */ 220 private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270; 221 private static final String USES_SDK_ELEMENT_TAG = "uses-sdk"; 222 223 /** 224 * Android resource ID of the {@code android:versionCode} attribute in AndroidManifest.xml. 225 */ 226 private static final int VERSION_CODE_ATTR_ID = 0x0101021b; 227 private static final String MANIFEST_ELEMENT_TAG = "manifest"; 228 229 /** 230 * Android resource ID of the {@code android:versionCodeMajor} attribute in AndroidManifest.xml. 231 */ 232 private static final int VERSION_CODE_MAJOR_ATTR_ID = 0x01010576; 233 234 /** 235 * Returns the lowest Android platform version (API Level) supported by an APK with the 236 * provided {@code AndroidManifest.xml}. 237 * 238 * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android 239 * resource format 240 * 241 * @throws MinSdkVersionException if an error occurred while determining the API Level 242 */ getMinSdkVersionFromBinaryAndroidManifest( ByteBuffer androidManifestContents)243 public static int getMinSdkVersionFromBinaryAndroidManifest( 244 ByteBuffer androidManifestContents) throws MinSdkVersionException { 245 // IMPLEMENTATION NOTE: Minimum supported Android platform version number is declared using 246 // uses-sdk elements which are children of the top-level manifest element. uses-sdk element 247 // declares the minimum supported platform version using the android:minSdkVersion attribute 248 // whose default value is 1. 249 // For each encountered uses-sdk element, the Android runtime checks that its minSdkVersion 250 // is not higher than the runtime's API Level and rejects APKs if it is higher. Thus, the 251 // effective minSdkVersion value is the maximum over the encountered minSdkVersion values. 252 253 try { 254 // If no uses-sdk elements are encountered, Android accepts the APK. We treat this 255 // scenario as though the minimum supported API Level is 1. 256 int result = 1; 257 258 AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); 259 int eventType = parser.getEventType(); 260 while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { 261 if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) 262 && (parser.getDepth() == 2) 263 && ("uses-sdk".equals(parser.getName())) 264 && (parser.getNamespace().isEmpty())) { 265 // In each uses-sdk element, minSdkVersion defaults to 1 266 int minSdkVersion = 1; 267 for (int i = 0; i < parser.getAttributeCount(); i++) { 268 if (parser.getAttributeNameResourceId(i) == MIN_SDK_VERSION_ATTR_ID) { 269 int valueType = parser.getAttributeValueType(i); 270 switch (valueType) { 271 case AndroidBinXmlParser.VALUE_TYPE_INT: 272 minSdkVersion = parser.getAttributeIntValue(i); 273 break; 274 case AndroidBinXmlParser.VALUE_TYPE_STRING: 275 minSdkVersion = 276 getMinSdkVersionForCodename( 277 parser.getAttributeStringValue(i)); 278 break; 279 default: 280 throw new MinSdkVersionException( 281 "Unable to determine APK's minimum supported Android" 282 + ": unsupported value type in " 283 + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s" 284 + " minSdkVersion" 285 + ". Only integer values supported."); 286 } 287 break; 288 } 289 } 290 result = Math.max(result, minSdkVersion); 291 } 292 eventType = parser.next(); 293 } 294 295 return result; 296 } catch (AndroidBinXmlParser.XmlParserException e) { 297 throw new MinSdkVersionException( 298 "Unable to determine APK's minimum supported Android platform version" 299 + ": malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, 300 e); 301 } 302 } 303 304 private static class CodenamesLazyInitializer { 305 306 /** 307 * List of platform codename (first letter of) to API Level mappings. The list must be 308 * sorted by the first letter. For codenames not in the list, the assumption is that the API 309 * Level is incremented by one for every increase in the codename's first letter. 310 */ 311 @SuppressWarnings({"rawtypes", "unchecked"}) 312 private static final Pair<Character, Integer>[] SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL = 313 new Pair[] { 314 Pair.of('C', 2), 315 Pair.of('D', 3), 316 Pair.of('E', 4), 317 Pair.of('F', 7), 318 Pair.of('G', 8), 319 Pair.of('H', 10), 320 Pair.of('I', 13), 321 Pair.of('J', 15), 322 Pair.of('K', 18), 323 Pair.of('L', 20), 324 Pair.of('M', 22), 325 Pair.of('N', 23), 326 Pair.of('O', 25), 327 }; 328 329 private static final Comparator<Pair<Character, Integer>> CODENAME_FIRST_CHAR_COMPARATOR = 330 new ByFirstComparator(); 331 332 private static class ByFirstComparator implements Comparator<Pair<Character, Integer>> { 333 @Override compare(Pair<Character, Integer> o1, Pair<Character, Integer> o2)334 public int compare(Pair<Character, Integer> o1, Pair<Character, Integer> o2) { 335 char c1 = o1.getFirst(); 336 char c2 = o2.getFirst(); 337 return c1 - c2; 338 } 339 } 340 } 341 342 /** 343 * Returns the API Level corresponding to the provided platform codename. 344 * 345 * <p>This method is pessimistic. It returns a value one lower than the API Level with which the 346 * platform is actually released (e.g., 23 for N which was released as API Level 24). This is 347 * because new features which first appear in an API Level are not available in the early days 348 * of that platform version's existence, when the platform only has a codename. Moreover, this 349 * method currently doesn't differentiate between initial and MR releases, meaning API Level 350 * returned for MR releases may be more than one lower than the API Level with which the 351 * platform version is actually released. 352 * 353 * @throws CodenameMinSdkVersionException if the {@code codename} is not supported 354 */ getMinSdkVersionForCodename(String codename)355 static int getMinSdkVersionForCodename(String codename) throws CodenameMinSdkVersionException { 356 if ("Baklava".equals(codename)) { 357 return 34; // VIC (35) was the version before Baklava, return VIC version minus one 358 } 359 360 char firstChar = codename.isEmpty() ? ' ' : codename.charAt(0); 361 // Codenames are case-sensitive. Only codenames starting with A-Z are supported for now. 362 // We only look at the first letter of the codename as this is the most important letter. 363 if ((firstChar >= 'A') && (firstChar <= 'Z')) { 364 Pair<Character, Integer>[] sortedCodenamesFirstCharToApiLevel = 365 CodenamesLazyInitializer.SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL; 366 int searchResult = 367 Arrays.binarySearch( 368 sortedCodenamesFirstCharToApiLevel, 369 Pair.of(firstChar, null), // second element of the pair is ignored here 370 CodenamesLazyInitializer.CODENAME_FIRST_CHAR_COMPARATOR); 371 if (searchResult >= 0) { 372 // Exact match -- searchResult is the index of the matching element 373 return sortedCodenamesFirstCharToApiLevel[searchResult].getSecond(); 374 } 375 // Not an exact match -- searchResult is negative and is -(insertion index) - 1. 376 // The element at insertionIndex - 1 (if present) is smaller than firstChar and the 377 // element at insertionIndex (if present) is greater than firstChar. 378 int insertionIndex = -1 - searchResult; // insertionIndex is in [0; array length] 379 if (insertionIndex == 0) { 380 // 'A' or 'B' (not Baklava) -- never released to public 381 return 1; 382 } else { 383 // The element at insertionIndex - 1 is the newest older codename. 384 // API Level bumped by at least 1 for every change in the first letter of codename 385 Pair<Character, Integer> newestOlderCodenameMapping = 386 sortedCodenamesFirstCharToApiLevel[insertionIndex - 1]; 387 char newestOlderCodenameFirstChar = newestOlderCodenameMapping.getFirst(); 388 int newestOlderCodenameApiLevel = newestOlderCodenameMapping.getSecond(); 389 return newestOlderCodenameApiLevel + (firstChar - newestOlderCodenameFirstChar); 390 } 391 } 392 393 throw new CodenameMinSdkVersionException( 394 "Unable to determine APK's minimum supported Android platform version" 395 + " : Unsupported codename in " + ANDROID_MANIFEST_ZIP_ENTRY_NAME 396 + "'s minSdkVersion: \"" + codename + "\"", 397 codename); 398 } 399 400 /** 401 * Returns {@code true} if the APK is debuggable according to its {@code AndroidManifest.xml}. 402 * See the {@code android:debuggable} attribute of the {@code application} element. 403 * 404 * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android 405 * resource format 406 * 407 * @throws ApkFormatException if the manifest is malformed 408 */ getDebuggableFromBinaryAndroidManifest( ByteBuffer androidManifestContents)409 public static boolean getDebuggableFromBinaryAndroidManifest( 410 ByteBuffer androidManifestContents) throws ApkFormatException { 411 // IMPLEMENTATION NOTE: Whether the package is debuggable is declared using the first 412 // "application" element which is a child of the top-level manifest element. The debuggable 413 // attribute of this application element is coerced to a boolean value. If there is no 414 // application element or if it doesn't declare the debuggable attribute, the package is 415 // considered not debuggable. 416 417 try { 418 AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); 419 int eventType = parser.getEventType(); 420 while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { 421 if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) 422 && (parser.getDepth() == 2) 423 && ("application".equals(parser.getName())) 424 && (parser.getNamespace().isEmpty())) { 425 for (int i = 0; i < parser.getAttributeCount(); i++) { 426 if (parser.getAttributeNameResourceId(i) == DEBUGGABLE_ATTR_ID) { 427 int valueType = parser.getAttributeValueType(i); 428 switch (valueType) { 429 case AndroidBinXmlParser.VALUE_TYPE_BOOLEAN: 430 case AndroidBinXmlParser.VALUE_TYPE_STRING: 431 case AndroidBinXmlParser.VALUE_TYPE_INT: 432 String value = parser.getAttributeStringValue(i); 433 return ("true".equals(value)) 434 || ("TRUE".equals(value)) 435 || ("1".equals(value)); 436 case AndroidBinXmlParser.VALUE_TYPE_REFERENCE: 437 // References to resources are not supported on purpose. The 438 // reason is that the resolved value depends on the resource 439 // configuration (e.g, MNC/MCC, locale, screen density) used 440 // at resolution time. As a result, the same APK may appear as 441 // debuggable in one situation and as non-debuggable in another 442 // situation. Such APKs may put users at risk. 443 throw new ApkFormatException( 444 "Unable to determine whether APK is debuggable" 445 + ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s" 446 + " android:debuggable attribute references a" 447 + " resource. References are not supported for" 448 + " security reasons. Only constant boolean," 449 + " string and int values are supported."); 450 default: 451 throw new ApkFormatException( 452 "Unable to determine whether APK is debuggable" 453 + ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s" 454 + " android:debuggable attribute uses" 455 + " unsupported value type. Only boolean," 456 + " string and int values are supported."); 457 } 458 } 459 } 460 // This application element does not declare the debuggable attribute 461 return false; 462 } 463 eventType = parser.next(); 464 } 465 466 // No application element found 467 return false; 468 } catch (AndroidBinXmlParser.XmlParserException e) { 469 throw new ApkFormatException( 470 "Unable to determine whether APK is debuggable: malformed binary resource: " 471 + ANDROID_MANIFEST_ZIP_ENTRY_NAME, 472 e); 473 } 474 } 475 476 /** 477 * Returns the package name of the APK according to its {@code AndroidManifest.xml} or 478 * {@code null} if package name is not declared. See the {@code package} attribute of the 479 * {@code manifest} element. 480 * 481 * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android 482 * resource format 483 * 484 * @throws ApkFormatException if the manifest is malformed 485 */ getPackageNameFromBinaryAndroidManifest( ByteBuffer androidManifestContents)486 public static String getPackageNameFromBinaryAndroidManifest( 487 ByteBuffer androidManifestContents) throws ApkFormatException { 488 // IMPLEMENTATION NOTE: Package name is declared as the "package" attribute of the top-level 489 // manifest element. Interestingly, as opposed to most other attributes, Android Package 490 // Manager looks up this attribute by its name rather than by its resource ID. 491 492 try { 493 AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); 494 int eventType = parser.getEventType(); 495 while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { 496 if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) 497 && (parser.getDepth() == 1) 498 && ("manifest".equals(parser.getName())) 499 && (parser.getNamespace().isEmpty())) { 500 for (int i = 0; i < parser.getAttributeCount(); i++) { 501 if ("package".equals(parser.getAttributeName(i)) 502 && (parser.getNamespace().isEmpty())) { 503 return parser.getAttributeStringValue(i); 504 } 505 } 506 // No "package" attribute found 507 return null; 508 } 509 eventType = parser.next(); 510 } 511 512 // No manifest element found 513 return null; 514 } catch (AndroidBinXmlParser.XmlParserException e) { 515 throw new ApkFormatException( 516 "Unable to determine APK package name: malformed binary resource: " 517 + ANDROID_MANIFEST_ZIP_ENTRY_NAME, 518 e); 519 } 520 } 521 522 /** 523 * Returns the security sandbox version targeted by an APK with the provided 524 * {@code AndroidManifest.xml}. 525 * 526 * <p>If the security sandbox version is not specified in the manifest a default value of 1 is 527 * returned. 528 * 529 * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android 530 * resource format 531 */ getTargetSandboxVersionFromBinaryAndroidManifest( ByteBuffer androidManifestContents)532 public static int getTargetSandboxVersionFromBinaryAndroidManifest( 533 ByteBuffer androidManifestContents) { 534 try { 535 return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, 536 MANIFEST_ELEMENT_TAG, TARGET_SANDBOX_VERSION_ATTR_ID); 537 } catch (ApkFormatException e) { 538 // An ApkFormatException indicates the target sandbox is not specified in the manifest; 539 // return a default value of 1. 540 return 1; 541 } 542 } 543 544 /** 545 * Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}. 546 * 547 * <p>If the targetSdkVersion is not specified the minimumSdkVersion is returned. If neither 548 * value is specified then a value of 1 is returned. 549 * 550 * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android 551 * resource format 552 */ getTargetSdkVersionFromBinaryAndroidManifest( ByteBuffer androidManifestContents)553 public static int getTargetSdkVersionFromBinaryAndroidManifest( 554 ByteBuffer androidManifestContents) { 555 // If the targetSdkVersion is not specified then the platform will use the value of the 556 // minSdkVersion; if neither is specified then the platform will use a value of 1. 557 int minSdkVersion = 1; 558 try { 559 return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, 560 USES_SDK_ELEMENT_TAG, TARGET_SDK_VERSION_ATTR_ID); 561 } catch (ApkFormatException e) { 562 // Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk 563 // element is not specified at all. 564 } 565 androidManifestContents.rewind(); 566 try { 567 minSdkVersion = getMinSdkVersionFromBinaryAndroidManifest(androidManifestContents); 568 } catch (ApkFormatException e) { 569 // Similar to above, expected if the APK does not contain a minSdkVersion attribute, or 570 // the uses-sdk element is not specified at all. 571 } 572 return minSdkVersion; 573 } 574 575 /** 576 * Returns the versionCode of the APK according to its {@code AndroidManifest.xml}. 577 * 578 * <p>If the versionCode is not specified in the {@code AndroidManifest.xml} or is not a valid 579 * integer an ApkFormatException is thrown. 580 * 581 * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android 582 * resource format 583 * @throws ApkFormatException if an error occurred while determining the versionCode, or if the 584 * versionCode attribute value is not available. 585 */ getVersionCodeFromBinaryAndroidManifest(ByteBuffer androidManifestContents)586 public static int getVersionCodeFromBinaryAndroidManifest(ByteBuffer androidManifestContents) 587 throws ApkFormatException { 588 return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, 589 MANIFEST_ELEMENT_TAG, VERSION_CODE_ATTR_ID); 590 } 591 592 /** 593 * Returns the versionCode and versionCodeMajor of the APK according to its {@code 594 * AndroidManifest.xml} combined together as a single long value. 595 * 596 * <p>The versionCodeMajor is placed in the upper 32 bits, and the versionCode is in the lower 597 * 32 bits. If the versionCodeMajor is not specified then the versionCode is returned. 598 * 599 * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android 600 * resource format 601 * @throws ApkFormatException if an error occurred while determining the version, or if the 602 * versionCode attribute value is not available. 603 */ getLongVersionCodeFromBinaryAndroidManifest( ByteBuffer androidManifestContents)604 public static long getLongVersionCodeFromBinaryAndroidManifest( 605 ByteBuffer androidManifestContents) throws ApkFormatException { 606 // If the versionCode is not found then allow the ApkFormatException to be thrown to notify 607 // the caller that the versionCode is not available. 608 int versionCode = getVersionCodeFromBinaryAndroidManifest(androidManifestContents); 609 long versionCodeMajor = 0; 610 try { 611 androidManifestContents.rewind(); 612 versionCodeMajor = getAttributeValueFromBinaryAndroidManifest(androidManifestContents, 613 MANIFEST_ELEMENT_TAG, VERSION_CODE_MAJOR_ATTR_ID); 614 } catch (ApkFormatException e) { 615 // This is expected if the versionCodeMajor has not been defined for the APK; in this 616 // case the return value is just the versionCode. 617 } 618 return (versionCodeMajor << 32) | versionCode; 619 } 620 621 /** 622 * Returns the integer value of the requested {@code attributeId} in the specified {@code 623 * elementName} from the provided {@code androidManifestContents} in binary Android resource 624 * format. 625 * 626 * @throws ApkFormatException if an error occurred while attempting to obtain the attribute, or 627 * if the requested attribute is not found. 628 */ getAttributeValueFromBinaryAndroidManifest( ByteBuffer androidManifestContents, String elementName, int attributeId)629 private static int getAttributeValueFromBinaryAndroidManifest( 630 ByteBuffer androidManifestContents, String elementName, int attributeId) 631 throws ApkFormatException { 632 if (elementName == null) { 633 throw new NullPointerException("elementName cannot be null"); 634 } 635 try { 636 AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); 637 int eventType = parser.getEventType(); 638 while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { 639 if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) 640 && (elementName.equals(parser.getName()))) { 641 for (int i = 0; i < parser.getAttributeCount(); i++) { 642 if (parser.getAttributeNameResourceId(i) == attributeId) { 643 int valueType = parser.getAttributeValueType(i); 644 switch (valueType) { 645 case AndroidBinXmlParser.VALUE_TYPE_INT: 646 case AndroidBinXmlParser.VALUE_TYPE_STRING: 647 return parser.getAttributeIntValue(i); 648 default: 649 throw new ApkFormatException( 650 "Unsupported value type, " + valueType 651 + ", for attribute " + String.format("0x%08X", 652 attributeId) + " under element " + elementName); 653 654 } 655 } 656 } 657 } 658 eventType = parser.next(); 659 } 660 throw new ApkFormatException( 661 "Failed to determine APK's " + elementName + " attribute " 662 + String.format("0x%08X", attributeId) + " value"); 663 } catch (AndroidBinXmlParser.XmlParserException e) { 664 throw new ApkFormatException( 665 "Unable to determine value for attribute " + String.format("0x%08X", 666 attributeId) + " under element " + elementName 667 + "; malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e); 668 } 669 } 670 computeSha256DigestBytes(byte[] data)671 public static byte[] computeSha256DigestBytes(byte[] data) { 672 return ApkUtilsLite.computeSha256DigestBytes(data); 673 } 674 } 675