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