blob: 135d815a57af8ca122f2529e09c4aa1fb3290416 [file] [log] [blame]
/*
* 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.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";
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 {
Pair<ByteBuffer, Long> eocdAndOffsetInFile =
ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
if (eocdAndOffsetInFile == null) {
throw new ZipFormatException("ZIP End of Central Directory record not found");
}
ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
long eocdOffset = eocdAndOffsetInFile.getSecond();
eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
if (cdStartOffset > eocdOffset) {
throw new ZipFormatException(
"ZIP Central Directory start offset out of range: " + cdStartOffset
+ ". ZIP End of Central Directory offset: " + eocdOffset);
}
long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
long cdEndOffset = cdStartOffset + cdSizeBytes;
if (cdEndOffset > eocdOffset) {
throw new ZipFormatException(
"ZIP Central Directory overlaps with End of Central Directory"
+ ". CD end: " + cdEndOffset
+ ", EoCD start: " + eocdOffset);
}
int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
return new ZipSections(
cdStartOffset,
cdSizeBytes,
cdRecordCount,
eocdOffset,
eocdBuf);
}
/**
* Information about the ZIP sections of an APK.
*/
public static class ZipSections {
private final long mCentralDirectoryOffset;
private final long mCentralDirectorySizeBytes;
private final int mCentralDirectoryRecordCount;
private final long mEocdOffset;
private final ByteBuffer mEocd;
public ZipSections(
long centralDirectoryOffset,
long centralDirectorySizeBytes,
int centralDirectoryRecordCount,
long eocdOffset,
ByteBuffer eocd) {
mCentralDirectoryOffset = centralDirectoryOffset;
mCentralDirectorySizeBytes = centralDirectorySizeBytes;
mCentralDirectoryRecordCount = centralDirectoryRecordCount;
mEocdOffset = eocdOffset;
mEocd = eocd;
}
/**
* Returns the start offset of the ZIP Central Directory. This value is taken from the
* ZIP End of Central Directory record.
*/
public long getZipCentralDirectoryOffset() {
return mCentralDirectoryOffset;
}
/**
* Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the
* ZIP End of Central Directory record.
*/
public long getZipCentralDirectorySizeBytes() {
return mCentralDirectorySizeBytes;
}
/**
* Returns the number of records in the ZIP Central Directory. This value is taken from the
* ZIP End of Central Directory record.
*/
public int getZipCentralDirectoryRecordCount() {
return mCentralDirectoryRecordCount;
}
/**
* Returns the start offset of the ZIP End of Central Directory record. The record extends
* until the very end of the APK.
*/
public long getZipEndOfCentralDirectoryOffset() {
return mEocdOffset;
}
/**
* Returns the contents of the ZIP End of Central Directory.
*/
public ByteBuffer getZipEndOfCentralDirectory() {
return mEocd;
}
}
/**
* 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);
}
// See https://source.android.com/security/apksigning/v2.html
private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
/**
* 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 <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
*/
public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
throws IOException, ApkSigningBlockNotFoundException {
// FORMAT (see https://source.android.com/security/apksigning/v2.html):
// OFFSET DATA TYPE DESCRIPTION
// * @+0 bytes uint64: size in bytes (excluding this field)
// * @+8 bytes payload
// * @-24 bytes uint64: size in bytes (same as the one above)
// * @-16 bytes uint128: magic
long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
long centralDirEndOffset =
centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
if (centralDirEndOffset != eocdStartOffset) {
throw new ApkSigningBlockNotFoundException(
"ZIP Central Directory is not immediately followed by End of Central Directory"
+ ". CD end: " + centralDirEndOffset
+ ", EoCD start: " + eocdStartOffset);
}
if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) {
throw new ApkSigningBlockNotFoundException(
"APK too small for APK Signing Block. ZIP Central Directory offset: "
+ centralDirStartOffset);
}
// Read the magic and offset in file from the footer section of the block:
// * uint64: size of block
// * 16 bytes: magic
ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24);
footer.order(ByteOrder.LITTLE_ENDIAN);
if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
|| (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
throw new ApkSigningBlockNotFoundException(
"No APK Signing Block before ZIP Central Directory");
}
// Read and compare size fields
long apkSigBlockSizeInFooter = footer.getLong(0);
if ((apkSigBlockSizeInFooter < footer.capacity())
|| (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
throw new ApkSigningBlockNotFoundException(
"APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
}
int totalSize = (int) (apkSigBlockSizeInFooter + 8);
long apkSigBlockOffset = centralDirStartOffset - totalSize;
if (apkSigBlockOffset < 0) {
throw new ApkSigningBlockNotFoundException(
"APK Signing Block offset out of range: " + apkSigBlockOffset);
}
ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8);
apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
throw new ApkSigningBlockNotFoundException(
"APK Signing Block sizes in header and footer do not match: "
+ apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
}
return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize));
}
/**
* Information about the location of the APK Signing Block inside an APK.
*/
public static class ApkSigningBlock {
private final long mStartOffsetInApk;
private final DataSource mContents;
/**
* 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) {
mStartOffsetInApk = startOffsetInApk;
mContents = contents;
}
/**
* Returns the start offset (in bytes, relative to start of file) of the APK Signing Block.
*/
public long getStartOffset() {
return mStartOffsetInApk;
}
/**
* Returns the data source which provides the full contents of the APK Signing Block,
* including its footer.
*/
public DataSource getContents() {
return mContents;
}
}
/**
* 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<CentralDirectoryRecord> 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;
/**
* 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<Character, Integer>[] 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<Pair<Character, Integer>> CODENAME_FIRST_CHAR_COMPARATOR =
new ByFirstComparator();
private static class ByFirstComparator implements Comparator<Pair<Character, Integer>> {
@Override
public int compare(Pair<Character, Integer> o1, Pair<Character, Integer> o2) {
char c1 = o1.getFirst();
char c2 = o2.getFirst();
return c1 - c2;
}
}
}
/**
* Returns the API Level corresponding to the provided platform codename.
*
* <p>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<Character, Integer>[] 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<Character, Integer> 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);
}
}
}