• 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      * 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