• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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