/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.apksig.apk; import com.android.apksig.internal.apk.AndroidBinXmlParser; import com.android.apksig.internal.apk.stamp.SourceStampConstants; import com.android.apksig.internal.apk.v1.V1SchemeVerifier; import com.android.apksig.internal.util.Pair; import com.android.apksig.internal.zip.CentralDirectoryRecord; import com.android.apksig.internal.zip.LocalFileRecord; import com.android.apksig.internal.zip.ZipUtils; import com.android.apksig.util.DataSource; import com.android.apksig.zip.ZipFormatException; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; import java.util.Comparator; import java.util.List; /** * APK utilities. */ public abstract class ApkUtils { /** * Name of the Android manifest ZIP entry in APKs. */ public static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml"; /** Name of the SourceStamp certificate hash ZIP entry in APKs. */ public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME; private ApkUtils() {} /** * Finds the main ZIP sections of the provided APK. * * @throws IOException if an I/O error occurred while reading the APK * @throws ZipFormatException if the APK is malformed */ public static ZipSections findZipSections(DataSource apk) throws IOException, ZipFormatException { com.android.apksig.zip.ZipSections zipSections = ApkUtilsLite.findZipSections(apk); return new ZipSections( zipSections.getZipCentralDirectoryOffset(), zipSections.getZipCentralDirectorySizeBytes(), zipSections.getZipCentralDirectoryRecordCount(), zipSections.getZipEndOfCentralDirectoryOffset(), zipSections.getZipEndOfCentralDirectory()); } /** * Information about the ZIP sections of an APK. */ public static class ZipSections extends com.android.apksig.zip.ZipSections { public ZipSections( long centralDirectoryOffset, long centralDirectorySizeBytes, int centralDirectoryRecordCount, long eocdOffset, ByteBuffer eocd) { super(centralDirectoryOffset, centralDirectorySizeBytes, centralDirectoryRecordCount, eocdOffset, eocd); } } /** * Sets the offset of the start of the ZIP Central Directory in the APK's ZIP End of Central * Directory record. * * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record * @param offset offset of the ZIP Central Directory relative to the start of the archive. Must * be between {@code 0} and {@code 2^32 - 1} inclusive. */ public static void setZipEocdCentralDirectoryOffset( ByteBuffer zipEndOfCentralDirectory, long offset) { ByteBuffer eocd = zipEndOfCentralDirectory.slice(); eocd.order(ByteOrder.LITTLE_ENDIAN); ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset); } /** * Updates the length of EOCD comment. * * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record */ public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) { ByteBuffer eocd = zipEndOfCentralDirectory.slice(); eocd.order(ByteOrder.LITTLE_ENDIAN); ZipUtils.updateZipEocdCommentLen(eocd); } /** * Returns the APK Signing Block of the provided {@code apk}. * * @throws ApkFormatException if the APK is not a valid ZIP archive * @throws IOException if an I/O error occurs * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK * * @see APK Signature Scheme v2 * */ public static ApkSigningBlock findApkSigningBlock(DataSource apk) throws ApkFormatException, IOException, ApkSigningBlockNotFoundException { ApkUtils.ZipSections inputZipSections; try { inputZipSections = ApkUtils.findZipSections(apk); } catch (ZipFormatException e) { throw new ApkFormatException("Malformed APK: not a ZIP archive", e); } return findApkSigningBlock(apk, inputZipSections); } /** * Returns the APK Signing Block of the provided APK. * * @throws IOException if an I/O error occurs * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK * * @see APK Signature Scheme v2 * */ public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections) throws IOException, ApkSigningBlockNotFoundException { ApkUtilsLite.ApkSigningBlock apkSigningBlock = ApkUtilsLite.findApkSigningBlock(apk, zipSections); return new ApkSigningBlock(apkSigningBlock.getStartOffset(), apkSigningBlock.getContents()); } /** * Information about the location of the APK Signing Block inside an APK. */ public static class ApkSigningBlock extends ApkUtilsLite.ApkSigningBlock { /** * Constructs a new {@code ApkSigningBlock}. * * @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK * Signing Block inside the APK file * @param contents contents of the APK Signing Block */ public ApkSigningBlock(long startOffsetInApk, DataSource contents) { super(startOffsetInApk, contents); } } /** * Returns the contents of the APK's {@code AndroidManifest.xml}. * * @throws IOException if an I/O error occurs while reading the APK * @throws ApkFormatException if the APK is malformed */ public static ByteBuffer getAndroidManifest(DataSource apk) throws IOException, ApkFormatException { ZipSections zipSections; try { zipSections = findZipSections(apk); } catch (ZipFormatException e) { throw new ApkFormatException("Not a valid ZIP archive", e); } List cdRecords = V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections); CentralDirectoryRecord androidManifestCdRecord = null; for (CentralDirectoryRecord cdRecord : cdRecords) { if (ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(cdRecord.getName())) { androidManifestCdRecord = cdRecord; break; } } if (androidManifestCdRecord == null) { throw new ApkFormatException("Missing " + ANDROID_MANIFEST_ZIP_ENTRY_NAME); } DataSource lfhSection = apk.slice(0, zipSections.getZipCentralDirectoryOffset()); try { return ByteBuffer.wrap( LocalFileRecord.getUncompressedData( lfhSection, androidManifestCdRecord, lfhSection.size())); } catch (ZipFormatException e) { throw new ApkFormatException("Failed to read " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e); } } /** * Android resource ID of the {@code android:minSdkVersion} attribute in AndroidManifest.xml. */ private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c; /** * Android resource ID of the {@code android:debuggable} attribute in AndroidManifest.xml. */ private static final int DEBUGGABLE_ATTR_ID = 0x0101000f; /** * Android resource ID of the {@code android:targetSandboxVersion} attribute in * AndroidManifest.xml. */ private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c; /** * Android resource ID of the {@code android:targetSdkVersion} attribute in * AndroidManifest.xml. */ private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270; private static final String USES_SDK_ELEMENT_TAG = "uses-sdk"; /** * Android resource ID of the {@code android:versionCode} attribute in AndroidManifest.xml. */ private static final int VERSION_CODE_ATTR_ID = 0x0101021b; private static final String MANIFEST_ELEMENT_TAG = "manifest"; /** * Android resource ID of the {@code android:versionCodeMajor} attribute in AndroidManifest.xml. */ private static final int VERSION_CODE_MAJOR_ATTR_ID = 0x01010576; /** * Returns the lowest Android platform version (API Level) supported by an APK with the * provided {@code AndroidManifest.xml}. * * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android * resource format * * @throws MinSdkVersionException if an error occurred while determining the API Level */ public static int getMinSdkVersionFromBinaryAndroidManifest( ByteBuffer androidManifestContents) throws MinSdkVersionException { // IMPLEMENTATION NOTE: Minimum supported Android platform version number is declared using // uses-sdk elements which are children of the top-level manifest element. uses-sdk element // declares the minimum supported platform version using the android:minSdkVersion attribute // whose default value is 1. // For each encountered uses-sdk element, the Android runtime checks that its minSdkVersion // is not higher than the runtime's API Level and rejects APKs if it is higher. Thus, the // effective minSdkVersion value is the maximum over the encountered minSdkVersion values. try { // If no uses-sdk elements are encountered, Android accepts the APK. We treat this // scenario as though the minimum supported API Level is 1. int result = 1; AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); int eventType = parser.getEventType(); while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) && (parser.getDepth() == 2) && ("uses-sdk".equals(parser.getName())) && (parser.getNamespace().isEmpty())) { // In each uses-sdk element, minSdkVersion defaults to 1 int minSdkVersion = 1; for (int i = 0; i < parser.getAttributeCount(); i++) { if (parser.getAttributeNameResourceId(i) == MIN_SDK_VERSION_ATTR_ID) { int valueType = parser.getAttributeValueType(i); switch (valueType) { case AndroidBinXmlParser.VALUE_TYPE_INT: minSdkVersion = parser.getAttributeIntValue(i); break; case AndroidBinXmlParser.VALUE_TYPE_STRING: minSdkVersion = getMinSdkVersionForCodename( parser.getAttributeStringValue(i)); break; default: throw new MinSdkVersionException( "Unable to determine APK's minimum supported Android" + ": unsupported value type in " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s" + " minSdkVersion" + ". Only integer values supported."); } break; } } result = Math.max(result, minSdkVersion); } eventType = parser.next(); } return result; } catch (AndroidBinXmlParser.XmlParserException e) { throw new MinSdkVersionException( "Unable to determine APK's minimum supported Android platform version" + ": malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e); } } private static class CodenamesLazyInitializer { /** * List of platform codename (first letter of) to API Level mappings. The list must be * sorted by the first letter. For codenames not in the list, the assumption is that the API * Level is incremented by one for every increase in the codename's first letter. */ @SuppressWarnings({"rawtypes", "unchecked"}) private static final Pair[] SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL = new Pair[] { Pair.of('C', 2), Pair.of('D', 3), Pair.of('E', 4), Pair.of('F', 7), Pair.of('G', 8), Pair.of('H', 10), Pair.of('I', 13), Pair.of('J', 15), Pair.of('K', 18), Pair.of('L', 20), Pair.of('M', 22), Pair.of('N', 23), Pair.of('O', 25), }; private static final Comparator> CODENAME_FIRST_CHAR_COMPARATOR = new ByFirstComparator(); private static class ByFirstComparator implements Comparator> { @Override public int compare(Pair o1, Pair o2) { char c1 = o1.getFirst(); char c2 = o2.getFirst(); return c1 - c2; } } } /** * Returns the API Level corresponding to the provided platform codename. * *

This method is pessimistic. It returns a value one lower than the API Level with which the * platform is actually released (e.g., 23 for N which was released as API Level 24). This is * because new features which first appear in an API Level are not available in the early days * of that platform version's existence, when the platform only has a codename. Moreover, this * method currently doesn't differentiate between initial and MR releases, meaning API Level * returned for MR releases may be more than one lower than the API Level with which the * platform version is actually released. * * @throws CodenameMinSdkVersionException if the {@code codename} is not supported */ static int getMinSdkVersionForCodename(String codename) throws CodenameMinSdkVersionException { char firstChar = codename.isEmpty() ? ' ' : codename.charAt(0); // Codenames are case-sensitive. Only codenames starting with A-Z are supported for now. // We only look at the first letter of the codename as this is the most important letter. if ((firstChar >= 'A') && (firstChar <= 'Z')) { Pair[] sortedCodenamesFirstCharToApiLevel = CodenamesLazyInitializer.SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL; int searchResult = Arrays.binarySearch( sortedCodenamesFirstCharToApiLevel, Pair.of(firstChar, null), // second element of the pair is ignored here CodenamesLazyInitializer.CODENAME_FIRST_CHAR_COMPARATOR); if (searchResult >= 0) { // Exact match -- searchResult is the index of the matching element return sortedCodenamesFirstCharToApiLevel[searchResult].getSecond(); } // Not an exact match -- searchResult is negative and is -(insertion index) - 1. // The element at insertionIndex - 1 (if present) is smaller than firstChar and the // element at insertionIndex (if present) is greater than firstChar. int insertionIndex = -1 - searchResult; // insertionIndex is in [0; array length] if (insertionIndex == 0) { // 'A' or 'B' -- never released to public return 1; } else { // The element at insertionIndex - 1 is the newest older codename. // API Level bumped by at least 1 for every change in the first letter of codename Pair newestOlderCodenameMapping = sortedCodenamesFirstCharToApiLevel[insertionIndex - 1]; char newestOlderCodenameFirstChar = newestOlderCodenameMapping.getFirst(); int newestOlderCodenameApiLevel = newestOlderCodenameMapping.getSecond(); return newestOlderCodenameApiLevel + (firstChar - newestOlderCodenameFirstChar); } } throw new CodenameMinSdkVersionException( "Unable to determine APK's minimum supported Android platform version" + " : Unsupported codename in " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s minSdkVersion: \"" + codename + "\"", codename); } /** * Returns {@code true} if the APK is debuggable according to its {@code AndroidManifest.xml}. * See the {@code android:debuggable} attribute of the {@code application} element. * * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android * resource format * * @throws ApkFormatException if the manifest is malformed */ public static boolean getDebuggableFromBinaryAndroidManifest( ByteBuffer androidManifestContents) throws ApkFormatException { // IMPLEMENTATION NOTE: Whether the package is debuggable is declared using the first // "application" element which is a child of the top-level manifest element. The debuggable // attribute of this application element is coerced to a boolean value. If there is no // application element or if it doesn't declare the debuggable attribute, the package is // considered not debuggable. try { AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); int eventType = parser.getEventType(); while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) && (parser.getDepth() == 2) && ("application".equals(parser.getName())) && (parser.getNamespace().isEmpty())) { for (int i = 0; i < parser.getAttributeCount(); i++) { if (parser.getAttributeNameResourceId(i) == DEBUGGABLE_ATTR_ID) { int valueType = parser.getAttributeValueType(i); switch (valueType) { case AndroidBinXmlParser.VALUE_TYPE_BOOLEAN: case AndroidBinXmlParser.VALUE_TYPE_STRING: case AndroidBinXmlParser.VALUE_TYPE_INT: String value = parser.getAttributeStringValue(i); return ("true".equals(value)) || ("TRUE".equals(value)) || ("1".equals(value)); case AndroidBinXmlParser.VALUE_TYPE_REFERENCE: // References to resources are not supported on purpose. The // reason is that the resolved value depends on the resource // configuration (e.g, MNC/MCC, locale, screen density) used // at resolution time. As a result, the same APK may appear as // debuggable in one situation and as non-debuggable in another // situation. Such APKs may put users at risk. throw new ApkFormatException( "Unable to determine whether APK is debuggable" + ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s" + " android:debuggable attribute references a" + " resource. References are not supported for" + " security reasons. Only constant boolean," + " string and int values are supported."); default: throw new ApkFormatException( "Unable to determine whether APK is debuggable" + ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s" + " android:debuggable attribute uses" + " unsupported value type. Only boolean," + " string and int values are supported."); } } } // This application element does not declare the debuggable attribute return false; } eventType = parser.next(); } // No application element found return false; } catch (AndroidBinXmlParser.XmlParserException e) { throw new ApkFormatException( "Unable to determine whether APK is debuggable: malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e); } } /** * Returns the package name of the APK according to its {@code AndroidManifest.xml} or * {@code null} if package name is not declared. See the {@code package} attribute of the * {@code manifest} element. * * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android * resource format * * @throws ApkFormatException if the manifest is malformed */ public static String getPackageNameFromBinaryAndroidManifest( ByteBuffer androidManifestContents) throws ApkFormatException { // IMPLEMENTATION NOTE: Package name is declared as the "package" attribute of the top-level // manifest element. Interestingly, as opposed to most other attributes, Android Package // Manager looks up this attribute by its name rather than by its resource ID. try { AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); int eventType = parser.getEventType(); while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) && (parser.getDepth() == 1) && ("manifest".equals(parser.getName())) && (parser.getNamespace().isEmpty())) { for (int i = 0; i < parser.getAttributeCount(); i++) { if ("package".equals(parser.getAttributeName(i)) && (parser.getNamespace().isEmpty())) { return parser.getAttributeStringValue(i); } } // No "package" attribute found return null; } eventType = parser.next(); } // No manifest element found return null; } catch (AndroidBinXmlParser.XmlParserException e) { throw new ApkFormatException( "Unable to determine APK package name: malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e); } } /** * Returns the security sandbox version targeted by an APK with the provided * {@code AndroidManifest.xml}. * *

If the security sandbox version is not specified in the manifest a default value of 1 is * returned. * * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android * resource format */ public static int getTargetSandboxVersionFromBinaryAndroidManifest( ByteBuffer androidManifestContents) { try { return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, MANIFEST_ELEMENT_TAG, TARGET_SANDBOX_VERSION_ATTR_ID); } catch (ApkFormatException e) { // An ApkFormatException indicates the target sandbox is not specified in the manifest; // return a default value of 1. return 1; } } /** * Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}. * *

If the targetSdkVersion is not specified the minimumSdkVersion is returned. If neither * value is specified then a value of 1 is returned. * * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android * resource format */ public static int getTargetSdkVersionFromBinaryAndroidManifest( ByteBuffer androidManifestContents) { // If the targetSdkVersion is not specified then the platform will use the value of the // minSdkVersion; if neither is specified then the platform will use a value of 1. int minSdkVersion = 1; try { return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, USES_SDK_ELEMENT_TAG, TARGET_SDK_VERSION_ATTR_ID); } catch (ApkFormatException e) { // Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk // element is not specified at all. } androidManifestContents.rewind(); try { minSdkVersion = getMinSdkVersionFromBinaryAndroidManifest(androidManifestContents); } catch (ApkFormatException e) { // Similar to above, expected if the APK does not contain a minSdkVersion attribute, or // the uses-sdk element is not specified at all. } return minSdkVersion; } /** * Returns the versionCode of the APK according to its {@code AndroidManifest.xml}. * *

If the versionCode is not specified in the {@code AndroidManifest.xml} or is not a valid * integer an ApkFormatException is thrown. * * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android * resource format * @throws ApkFormatException if an error occurred while determining the versionCode, or if the * versionCode attribute value is not available. */ public static int getVersionCodeFromBinaryAndroidManifest(ByteBuffer androidManifestContents) throws ApkFormatException { return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, MANIFEST_ELEMENT_TAG, VERSION_CODE_ATTR_ID); } /** * Returns the versionCode and versionCodeMajor of the APK according to its {@code * AndroidManifest.xml} combined together as a single long value. * *

The versionCodeMajor is placed in the upper 32 bits, and the versionCode is in the lower * 32 bits. If the versionCodeMajor is not specified then the versionCode is returned. * * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android * resource format * @throws ApkFormatException if an error occurred while determining the version, or if the * versionCode attribute value is not available. */ public static long getLongVersionCodeFromBinaryAndroidManifest( ByteBuffer androidManifestContents) throws ApkFormatException { // If the versionCode is not found then allow the ApkFormatException to be thrown to notify // the caller that the versionCode is not available. int versionCode = getVersionCodeFromBinaryAndroidManifest(androidManifestContents); long versionCodeMajor = 0; try { androidManifestContents.rewind(); versionCodeMajor = getAttributeValueFromBinaryAndroidManifest(androidManifestContents, MANIFEST_ELEMENT_TAG, VERSION_CODE_MAJOR_ATTR_ID); } catch (ApkFormatException e) { // This is expected if the versionCodeMajor has not been defined for the APK; in this // case the return value is just the versionCode. } return (versionCodeMajor << 32) | versionCode; } /** * Returns the integer value of the requested {@code attributeId} in the specified {@code * elementName} from the provided {@code androidManifestContents} in binary Android resource * format. * * @throws ApkFormatException if an error occurred while attempting to obtain the attribute, or * if the requested attribute is not found. */ private static int getAttributeValueFromBinaryAndroidManifest( ByteBuffer androidManifestContents, String elementName, int attributeId) throws ApkFormatException { if (elementName == null) { throw new NullPointerException("elementName cannot be null"); } try { AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); int eventType = parser.getEventType(); while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) && (elementName.equals(parser.getName()))) { for (int i = 0; i < parser.getAttributeCount(); i++) { if (parser.getAttributeNameResourceId(i) == attributeId) { int valueType = parser.getAttributeValueType(i); switch (valueType) { case AndroidBinXmlParser.VALUE_TYPE_INT: case AndroidBinXmlParser.VALUE_TYPE_STRING: return parser.getAttributeIntValue(i); default: throw new ApkFormatException( "Unsupported value type, " + valueType + ", for attribute " + String.format("0x%08X", attributeId) + " under element " + elementName); } } } } eventType = parser.next(); } throw new ApkFormatException( "Failed to determine APK's " + elementName + " attribute " + String.format("0x%08X", attributeId) + " value"); } catch (AndroidBinXmlParser.XmlParserException e) { throw new ApkFormatException( "Unable to determine value for attribute " + String.format("0x%08X", attributeId) + " under element " + elementName + "; malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e); } } public static byte[] computeSha256DigestBytes(byte[] data) { return ApkUtilsLite.computeSha256DigestBytes(data); } }