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 char firstChar = codename.isEmpty() ? ' ' : codename.charAt(0); 357 // Codenames are case-sensitive. Only codenames starting with A-Z are supported for now. 358 // We only look at the first letter of the codename as this is the most important letter. 359 if ((firstChar >= 'A') && (firstChar <= 'Z')) { 360 Pair<Character, Integer>[] sortedCodenamesFirstCharToApiLevel = 361 CodenamesLazyInitializer.SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL; 362 int searchResult = 363 Arrays.binarySearch( 364 sortedCodenamesFirstCharToApiLevel, 365 Pair.of(firstChar, null), // second element of the pair is ignored here 366 CodenamesLazyInitializer.CODENAME_FIRST_CHAR_COMPARATOR); 367 if (searchResult >= 0) { 368 // Exact match -- searchResult is the index of the matching element 369 return sortedCodenamesFirstCharToApiLevel[searchResult].getSecond(); 370 } 371 // Not an exact match -- searchResult is negative and is -(insertion index) - 1. 372 // The element at insertionIndex - 1 (if present) is smaller than firstChar and the 373 // element at insertionIndex (if present) is greater than firstChar. 374 int insertionIndex = -1 - searchResult; // insertionIndex is in [0; array length] 375 if (insertionIndex == 0) { 376 // 'A' or 'B' -- never released to public 377 return 1; 378 } else { 379 // The element at insertionIndex - 1 is the newest older codename. 380 // API Level bumped by at least 1 for every change in the first letter of codename 381 Pair<Character, Integer> newestOlderCodenameMapping = 382 sortedCodenamesFirstCharToApiLevel[insertionIndex - 1]; 383 char newestOlderCodenameFirstChar = newestOlderCodenameMapping.getFirst(); 384 int newestOlderCodenameApiLevel = newestOlderCodenameMapping.getSecond(); 385 return newestOlderCodenameApiLevel + (firstChar - newestOlderCodenameFirstChar); 386 } 387 } 388 389 throw new CodenameMinSdkVersionException( 390 "Unable to determine APK's minimum supported Android platform version" 391 + " : Unsupported codename in " + ANDROID_MANIFEST_ZIP_ENTRY_NAME 392 + "'s minSdkVersion: \"" + codename + "\"", 393 codename); 394 } 395 396 /** 397 * Returns {@code true} if the APK is debuggable according to its {@code AndroidManifest.xml}. 398 * See the {@code android:debuggable} attribute of the {@code application} element. 399 * 400 * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android 401 * resource format 402 * 403 * @throws ApkFormatException if the manifest is malformed 404 */ getDebuggableFromBinaryAndroidManifest( ByteBuffer androidManifestContents)405 public static boolean getDebuggableFromBinaryAndroidManifest( 406 ByteBuffer androidManifestContents) throws ApkFormatException { 407 // IMPLEMENTATION NOTE: Whether the package is debuggable is declared using the first 408 // "application" element which is a child of the top-level manifest element. The debuggable 409 // attribute of this application element is coerced to a boolean value. If there is no 410 // application element or if it doesn't declare the debuggable attribute, the package is 411 // considered not debuggable. 412 413 try { 414 AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); 415 int eventType = parser.getEventType(); 416 while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { 417 if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) 418 && (parser.getDepth() == 2) 419 && ("application".equals(parser.getName())) 420 && (parser.getNamespace().isEmpty())) { 421 for (int i = 0; i < parser.getAttributeCount(); i++) { 422 if (parser.getAttributeNameResourceId(i) == DEBUGGABLE_ATTR_ID) { 423 int valueType = parser.getAttributeValueType(i); 424 switch (valueType) { 425 case AndroidBinXmlParser.VALUE_TYPE_BOOLEAN: 426 case AndroidBinXmlParser.VALUE_TYPE_STRING: 427 case AndroidBinXmlParser.VALUE_TYPE_INT: 428 String value = parser.getAttributeStringValue(i); 429 return ("true".equals(value)) 430 || ("TRUE".equals(value)) 431 || ("1".equals(value)); 432 case AndroidBinXmlParser.VALUE_TYPE_REFERENCE: 433 // References to resources are not supported on purpose. The 434 // reason is that the resolved value depends on the resource 435 // configuration (e.g, MNC/MCC, locale, screen density) used 436 // at resolution time. As a result, the same APK may appear as 437 // debuggable in one situation and as non-debuggable in another 438 // situation. Such APKs may put users at risk. 439 throw new ApkFormatException( 440 "Unable to determine whether APK is debuggable" 441 + ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s" 442 + " android:debuggable attribute references a" 443 + " resource. References are not supported for" 444 + " security reasons. Only constant boolean," 445 + " string and int values are supported."); 446 default: 447 throw new ApkFormatException( 448 "Unable to determine whether APK is debuggable" 449 + ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s" 450 + " android:debuggable attribute uses" 451 + " unsupported value type. Only boolean," 452 + " string and int values are supported."); 453 } 454 } 455 } 456 // This application element does not declare the debuggable attribute 457 return false; 458 } 459 eventType = parser.next(); 460 } 461 462 // No application element found 463 return false; 464 } catch (AndroidBinXmlParser.XmlParserException e) { 465 throw new ApkFormatException( 466 "Unable to determine whether APK is debuggable: malformed binary resource: " 467 + ANDROID_MANIFEST_ZIP_ENTRY_NAME, 468 e); 469 } 470 } 471 472 /** 473 * Returns the package name of the APK according to its {@code AndroidManifest.xml} or 474 * {@code null} if package name is not declared. See the {@code package} attribute of the 475 * {@code manifest} element. 476 * 477 * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android 478 * resource format 479 * 480 * @throws ApkFormatException if the manifest is malformed 481 */ getPackageNameFromBinaryAndroidManifest( ByteBuffer androidManifestContents)482 public static String getPackageNameFromBinaryAndroidManifest( 483 ByteBuffer androidManifestContents) throws ApkFormatException { 484 // IMPLEMENTATION NOTE: Package name is declared as the "package" attribute of the top-level 485 // manifest element. Interestingly, as opposed to most other attributes, Android Package 486 // Manager looks up this attribute by its name rather than by its resource ID. 487 488 try { 489 AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); 490 int eventType = parser.getEventType(); 491 while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { 492 if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) 493 && (parser.getDepth() == 1) 494 && ("manifest".equals(parser.getName())) 495 && (parser.getNamespace().isEmpty())) { 496 for (int i = 0; i < parser.getAttributeCount(); i++) { 497 if ("package".equals(parser.getAttributeName(i)) 498 && (parser.getNamespace().isEmpty())) { 499 return parser.getAttributeStringValue(i); 500 } 501 } 502 // No "package" attribute found 503 return null; 504 } 505 eventType = parser.next(); 506 } 507 508 // No manifest element found 509 return null; 510 } catch (AndroidBinXmlParser.XmlParserException e) { 511 throw new ApkFormatException( 512 "Unable to determine APK package name: malformed binary resource: " 513 + ANDROID_MANIFEST_ZIP_ENTRY_NAME, 514 e); 515 } 516 } 517 518 /** 519 * Returns the security sandbox version targeted by an APK with the provided 520 * {@code AndroidManifest.xml}. 521 * 522 * <p>If the security sandbox version is not specified in the manifest a default value of 1 is 523 * returned. 524 * 525 * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android 526 * resource format 527 */ getTargetSandboxVersionFromBinaryAndroidManifest( ByteBuffer androidManifestContents)528 public static int getTargetSandboxVersionFromBinaryAndroidManifest( 529 ByteBuffer androidManifestContents) { 530 try { 531 return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, 532 MANIFEST_ELEMENT_TAG, TARGET_SANDBOX_VERSION_ATTR_ID); 533 } catch (ApkFormatException e) { 534 // An ApkFormatException indicates the target sandbox is not specified in the manifest; 535 // return a default value of 1. 536 return 1; 537 } 538 } 539 540 /** 541 * Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}. 542 * 543 * <p>If the targetSdkVersion is not specified the minimumSdkVersion is returned. If neither 544 * value is specified then a value of 1 is returned. 545 * 546 * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android 547 * resource format 548 */ getTargetSdkVersionFromBinaryAndroidManifest( ByteBuffer androidManifestContents)549 public static int getTargetSdkVersionFromBinaryAndroidManifest( 550 ByteBuffer androidManifestContents) { 551 // If the targetSdkVersion is not specified then the platform will use the value of the 552 // minSdkVersion; if neither is specified then the platform will use a value of 1. 553 int minSdkVersion = 1; 554 try { 555 return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, 556 USES_SDK_ELEMENT_TAG, TARGET_SDK_VERSION_ATTR_ID); 557 } catch (ApkFormatException e) { 558 // Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk 559 // element is not specified at all. 560 } 561 androidManifestContents.rewind(); 562 try { 563 minSdkVersion = getMinSdkVersionFromBinaryAndroidManifest(androidManifestContents); 564 } catch (ApkFormatException e) { 565 // Similar to above, expected if the APK does not contain a minSdkVersion attribute, or 566 // the uses-sdk element is not specified at all. 567 } 568 return minSdkVersion; 569 } 570 571 /** 572 * Returns the versionCode of the APK according to its {@code AndroidManifest.xml}. 573 * 574 * <p>If the versionCode is not specified in the {@code AndroidManifest.xml} or is not a valid 575 * integer an ApkFormatException is thrown. 576 * 577 * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android 578 * resource format 579 * @throws ApkFormatException if an error occurred while determining the versionCode, or if the 580 * versionCode attribute value is not available. 581 */ getVersionCodeFromBinaryAndroidManifest(ByteBuffer androidManifestContents)582 public static int getVersionCodeFromBinaryAndroidManifest(ByteBuffer androidManifestContents) 583 throws ApkFormatException { 584 return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, 585 MANIFEST_ELEMENT_TAG, VERSION_CODE_ATTR_ID); 586 } 587 588 /** 589 * Returns the versionCode and versionCodeMajor of the APK according to its {@code 590 * AndroidManifest.xml} combined together as a single long value. 591 * 592 * <p>The versionCodeMajor is placed in the upper 32 bits, and the versionCode is in the lower 593 * 32 bits. If the versionCodeMajor is not specified then the versionCode is returned. 594 * 595 * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android 596 * resource format 597 * @throws ApkFormatException if an error occurred while determining the version, or if the 598 * versionCode attribute value is not available. 599 */ getLongVersionCodeFromBinaryAndroidManifest( ByteBuffer androidManifestContents)600 public static long getLongVersionCodeFromBinaryAndroidManifest( 601 ByteBuffer androidManifestContents) throws ApkFormatException { 602 // If the versionCode is not found then allow the ApkFormatException to be thrown to notify 603 // the caller that the versionCode is not available. 604 int versionCode = getVersionCodeFromBinaryAndroidManifest(androidManifestContents); 605 long versionCodeMajor = 0; 606 try { 607 androidManifestContents.rewind(); 608 versionCodeMajor = getAttributeValueFromBinaryAndroidManifest(androidManifestContents, 609 MANIFEST_ELEMENT_TAG, VERSION_CODE_MAJOR_ATTR_ID); 610 } catch (ApkFormatException e) { 611 // This is expected if the versionCodeMajor has not been defined for the APK; in this 612 // case the return value is just the versionCode. 613 } 614 return (versionCodeMajor << 32) | versionCode; 615 } 616 617 /** 618 * Returns the integer value of the requested {@code attributeId} in the specified {@code 619 * elementName} from the provided {@code androidManifestContents} in binary Android resource 620 * format. 621 * 622 * @throws ApkFormatException if an error occurred while attempting to obtain the attribute, or 623 * if the requested attribute is not found. 624 */ getAttributeValueFromBinaryAndroidManifest( ByteBuffer androidManifestContents, String elementName, int attributeId)625 private static int getAttributeValueFromBinaryAndroidManifest( 626 ByteBuffer androidManifestContents, String elementName, int attributeId) 627 throws ApkFormatException { 628 if (elementName == null) { 629 throw new NullPointerException("elementName cannot be null"); 630 } 631 try { 632 AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); 633 int eventType = parser.getEventType(); 634 while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { 635 if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) 636 && (elementName.equals(parser.getName()))) { 637 for (int i = 0; i < parser.getAttributeCount(); i++) { 638 if (parser.getAttributeNameResourceId(i) == attributeId) { 639 int valueType = parser.getAttributeValueType(i); 640 switch (valueType) { 641 case AndroidBinXmlParser.VALUE_TYPE_INT: 642 case AndroidBinXmlParser.VALUE_TYPE_STRING: 643 return parser.getAttributeIntValue(i); 644 default: 645 throw new ApkFormatException( 646 "Unsupported value type, " + valueType 647 + ", for attribute " + String.format("0x%08X", 648 attributeId) + " under element " + elementName); 649 650 } 651 } 652 } 653 } 654 eventType = parser.next(); 655 } 656 throw new ApkFormatException( 657 "Failed to determine APK's " + elementName + " attribute " 658 + String.format("0x%08X", attributeId) + " value"); 659 } catch (AndroidBinXmlParser.XmlParserException e) { 660 throw new ApkFormatException( 661 "Unable to determine value for attribute " + String.format("0x%08X", 662 attributeId) + " under element " + elementName 663 + "; malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e); 664 } 665 } 666 computeSha256DigestBytes(byte[] data)667 public static byte[] computeSha256DigestBytes(byte[] data) { 668 return ApkUtilsLite.computeSha256DigestBytes(data); 669 } 670 } 671