diff --git a/app/build.gradle b/app/build.gradle
index fbfd71c..1b8939c 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -10,8 +10,8 @@ android {
applicationId "com.zane.smapiinstaller"
minSdkVersion 19
targetSdkVersion 28
- versionCode 20
- versionName "1.3.5"
+ versionCode 21
+ versionName "1.3.6"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index b46250f..8ff6aed 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -138,6 +138,7 @@
-keep class com.zane.** { *; }
-keep class pxb.android.** { *; }
-keep class net.fornwall.apksigner.** { *; }
+-keep class com.android.apksig.** { *; }
-keep class org.spongycastle.**
-dontwarn org.spongycastle.jce.provider.X509LDAPCertStoreSpi
-dontwarn org.spongycastle.x509.util.LDAPStoreHelper
diff --git a/app/src/main/assets/apk/StardewModdingAPI.dll b/app/src/main/assets/apk/StardewModdingAPI.dll
index aa782dd..9134e07 100644
Binary files a/app/src/main/assets/apk/StardewModdingAPI.dll and b/app/src/main/assets/apk/StardewModdingAPI.dll differ
diff --git a/app/src/main/assets/apk_files_manifest.json b/app/src/main/assets/apk_files_manifest.json
index fbfbdd6..16e5356 100644
--- a/app/src/main/assets/apk_files_manifest.json
+++ b/app/src/main/assets/apk_files_manifest.json
@@ -2,6 +2,11 @@
"minBuildCode": 138,
"maxBuildCode": null,
"basePath": "",
+ "targetPackageName": [
+ "com.chucklefish.stardewvalley",
+ "com.zane.stardewvalley",
+ "com.martyrpher.stardewvalley"
+ ],
"manifestEntries": [
{
"targetPath": "classes.dex",
diff --git a/app/src/main/assets/downloadable_content_list.json b/app/src/main/assets/downloadable_content_list.json
index e49655c..bd1014d 100644
--- a/app/src/main/assets/downloadable_content_list.json
+++ b/app/src/main/assets/downloadable_content_list.json
@@ -1,5 +1,5 @@
{
- "version": 5,
+ "version": 6,
"contents": [
{
"type": "COMPAT",
@@ -9,6 +9,14 @@
"url": "http://zaneyork.cn/download/compat/smapi_137.zip",
"hash": "bd16e8e4cb52d636e24c6a2d2309b66a60e492d2b97c1b8f6a519c04ac42ebdc"
},
+ {
+ "type": "COMPAT",
+ "name": "SMAPI for Galaxy Store",
+ "assetPath": "compat/samsung_138/",
+ "description": "SMAPI compat package for game 1.4.4.138 - latest, SMAPI 3.4.0",
+ "url": "http://zaneyork.cn/download/compat/smapi_samsung_138.zip",
+ "hash": "4028512fb1ae048557b63495de6f76cc454510d488a76d35eb1330ef021ec4ec"
+ },
{
"type": "LOCALE",
"name": "Chinese Locale v2.5.1",
diff --git a/app/src/main/assets/downloadable_content_list.json.en b/app/src/main/assets/downloadable_content_list.json.en
index e49655c..bd1014d 100644
--- a/app/src/main/assets/downloadable_content_list.json.en
+++ b/app/src/main/assets/downloadable_content_list.json.en
@@ -1,5 +1,5 @@
{
- "version": 5,
+ "version": 6,
"contents": [
{
"type": "COMPAT",
@@ -9,6 +9,14 @@
"url": "http://zaneyork.cn/download/compat/smapi_137.zip",
"hash": "bd16e8e4cb52d636e24c6a2d2309b66a60e492d2b97c1b8f6a519c04ac42ebdc"
},
+ {
+ "type": "COMPAT",
+ "name": "SMAPI for Galaxy Store",
+ "assetPath": "compat/samsung_138/",
+ "description": "SMAPI compat package for game 1.4.4.138 - latest, SMAPI 3.4.0",
+ "url": "http://zaneyork.cn/download/compat/smapi_samsung_138.zip",
+ "hash": "4028512fb1ae048557b63495de6f76cc454510d488a76d35eb1330ef021ec4ec"
+ },
{
"type": "LOCALE",
"name": "Chinese Locale v2.5.1",
diff --git a/app/src/main/assets/downloadable_content_list.json.zh b/app/src/main/assets/downloadable_content_list.json.zh
index be1b984..5241411 100644
--- a/app/src/main/assets/downloadable_content_list.json.zh
+++ b/app/src/main/assets/downloadable_content_list.json.zh
@@ -1,14 +1,22 @@
{
- "version": 5,
+ "version": 6,
"contents": [
{
"type": "COMPAT",
- "name": "SMAPI for 1.4.5.137",
+ "name": "SMAPI兼容包 1.4.5.137",
"assetPath": "compat/137/",
"description": "SMAPI兼容包, 适用版本1.4.4.128 - 1.4.5.137, SMAPI 3.3.2.0",
"url": "http://zaneyork.cn/download/compat/smapi_137.zip",
"hash": "bd16e8e4cb52d636e24c6a2d2309b66a60e492d2b97c1b8f6a519c04ac42ebdc"
},
+ {
+ "type": "COMPAT",
+ "name": "SMAPI三星商店兼容包",
+ "assetPath": "compat/samsung_138/",
+ "description": "SMAPI三星商店兼容包, 适用版本1.4.4.138至今, SMAPI 3.4.0",
+ "url": "http://zaneyork.cn/download/compat/smapi_samsung_138.zip",
+ "hash": "4028512fb1ae048557b63495de6f76cc454510d488a76d35eb1330ef021ec4ec"
+ },
{
"type": "LOCALE",
"name": "中文汉化v2.5.1",
diff --git a/app/src/main/assets/package_names.json b/app/src/main/assets/package_names.json
index 78bc950..6885cd6 100644
--- a/app/src/main/assets/package_names.json
+++ b/app/src/main/assets/package_names.json
@@ -2,5 +2,6 @@
"com.chucklefish.stardewvalley",
"com.chucklefish.stardewvalleysamsung",
"com.zane.stardewvalley",
+ "com.zane.stardewvalleysamsung",
"com.martyrpher.stardewvalley"
]
\ No newline at end of file
diff --git a/app/src/main/java/com/android/apksig/ApkSigner.java b/app/src/main/java/com/android/apksig/ApkSigner.java
new file mode 100644
index 0000000..88f2617
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/ApkSigner.java
@@ -0,0 +1,1302 @@
+/*
+ * 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;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkSigningBlockNotFoundException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.apk.MinSdkVersionException;
+import com.android.apksig.internal.util.ByteBufferDataSource;
+import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.EocdRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
+import com.android.apksig.internal.zip.ZipUtils;
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSinks;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apksig.util.ReadableDataSink;
+import com.android.apksig.zip.ZipFormatException;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SignatureException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * APK signer.
+ *
+ *
The signer preserves as much of the input APK as possible. For example, it preserves the
+ * order of APK entries and preserves their contents, including compressed form and alignment of
+ * data.
+ *
+ *
Use {@link Builder} to obtain instances of this signer.
+ *
+ * @see Application Signing
+ */
+public class ApkSigner {
+
+ /**
+ * Extensible data block/field header ID used for storing information about alignment of
+ * uncompressed entries as well as for aligning the entries's data. See ZIP appnote.txt section
+ * 4.5 Extensible data fields.
+ */
+ private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = (short) 0xd935;
+
+ /**
+ * Minimum size (in bytes) of the extensible data block/field used for alignment of uncompressed
+ * entries.
+ */
+ private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES = 6;
+
+ private static final short ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096;
+
+ /**
+ * Name of the Android manifest ZIP entry in APKs.
+ */
+ private static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
+
+ private final List mSignerConfigs;
+ private final Integer mMinSdkVersion;
+ private final boolean mV1SigningEnabled;
+ private final boolean mV2SigningEnabled;
+ private final boolean mV3SigningEnabled;
+ private final boolean mDebuggableApkPermitted;
+ private final boolean mOtherSignersSignaturesPreserved;
+ private final String mCreatedBy;
+
+ private final ApkSignerEngine mSignerEngine;
+
+ private final File mInputApkFile;
+ private final DataSource mInputApkDataSource;
+
+ private final File mOutputApkFile;
+ private final DataSink mOutputApkDataSink;
+ private final DataSource mOutputApkDataSource;
+
+ private final SigningCertificateLineage mSigningCertificateLineage;
+
+ private ApkSigner(
+ List signerConfigs,
+ Integer minSdkVersion,
+ boolean v1SigningEnabled,
+ boolean v2SigningEnabled,
+ boolean v3SigningEnabled,
+ boolean debuggableApkPermitted,
+ boolean otherSignersSignaturesPreserved,
+ String createdBy,
+ ApkSignerEngine signerEngine,
+ File inputApkFile,
+ DataSource inputApkDataSource,
+ File outputApkFile,
+ DataSink outputApkDataSink,
+ DataSource outputApkDataSource,
+ SigningCertificateLineage signingCertificateLineage) {
+
+ mSignerConfigs = signerConfigs;
+ mMinSdkVersion = minSdkVersion;
+ mV1SigningEnabled = v1SigningEnabled;
+ mV2SigningEnabled = v2SigningEnabled;
+ mV3SigningEnabled = v3SigningEnabled;
+ mDebuggableApkPermitted = debuggableApkPermitted;
+ mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
+ mCreatedBy = createdBy;
+
+ mSignerEngine = signerEngine;
+
+ mInputApkFile = inputApkFile;
+ mInputApkDataSource = inputApkDataSource;
+
+ mOutputApkFile = outputApkFile;
+ mOutputApkDataSink = outputApkDataSink;
+ mOutputApkDataSource = outputApkDataSource;
+
+ mSigningCertificateLineage = signingCertificateLineage;
+ }
+
+ /**
+ * Signs the input APK and outputs the resulting signed APK. The input APK is not modified.
+ *
+ * @throws IOException if an I/O error is encountered while reading or writing the APKs
+ * @throws ApkFormatException if the input APK is malformed
+ * @throws NoSuchAlgorithmException if the APK signatures cannot be produced or verified because
+ * a required cryptographic algorithm implementation is missing
+ * @throws InvalidKeyException if a signature could not be generated because a signing key is
+ * not suitable for generating the signature
+ * @throws SignatureException if an error occurred while generating or verifying a signature
+ * @throws IllegalStateException if this signer's configuration is missing required information
+ * or if the signing engine is in an invalid state.
+ */
+ public void sign()
+ throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
+ SignatureException, IllegalStateException {
+ Closeable in = null;
+ DataSource inputApk;
+ try {
+ if (mInputApkDataSource != null) {
+ inputApk = mInputApkDataSource;
+ } else if (mInputApkFile != null) {
+ RandomAccessFile inputFile = new RandomAccessFile(mInputApkFile, "r");
+ in = inputFile;
+ inputApk = DataSources.asDataSource(inputFile);
+ } else {
+ throw new IllegalStateException("Input APK not specified");
+ }
+
+ Closeable out = null;
+ try {
+ DataSink outputApkOut;
+ DataSource outputApkIn;
+ if (mOutputApkDataSink != null) {
+ outputApkOut = mOutputApkDataSink;
+ outputApkIn = mOutputApkDataSource;
+ } else if (mOutputApkFile != null) {
+ RandomAccessFile outputFile = new RandomAccessFile(mOutputApkFile, "rw");
+ out = outputFile;
+ outputFile.setLength(0);
+ outputApkOut = DataSinks.asDataSink(outputFile);
+ outputApkIn = DataSources.asDataSource(outputFile);
+ } else {
+ throw new IllegalStateException("Output APK not specified");
+ }
+
+ sign(inputApk, outputApkOut, outputApkIn);
+ } finally {
+ if (out != null) {
+ out.close();
+ }
+ }
+ } finally {
+ if (in != null) {
+ in.close();
+ }
+ }
+ }
+
+ private void sign(
+ DataSource inputApk,
+ DataSink outputApkOut,
+ DataSource outputApkIn)
+ throws IOException, ApkFormatException, NoSuchAlgorithmException,
+ InvalidKeyException, SignatureException {
+ // Step 1. Find input APK's main ZIP sections
+ ApkUtils.ZipSections inputZipSections;
+ try {
+ inputZipSections = ApkUtils.findZipSections(inputApk);
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Malformed APK: not a ZIP archive", e);
+ }
+ long inputApkSigningBlockOffset = -1;
+ DataSource inputApkSigningBlock = null;
+ try {
+ ApkUtils.ApkSigningBlock apkSigningBlockInfo =
+ ApkUtils.findApkSigningBlock(inputApk, inputZipSections);
+ inputApkSigningBlockOffset = apkSigningBlockInfo.getStartOffset();
+ inputApkSigningBlock = apkSigningBlockInfo.getContents();
+ } catch (ApkSigningBlockNotFoundException e) {
+ // Input APK does not contain an APK Signing Block. That's OK. APKs are not required to
+ // contain this block. It's only needed if the APK is signed using APK Signature Scheme
+ // v2 and/or v3.
+ }
+ DataSource inputApkLfhSection =
+ inputApk.slice(
+ 0,
+ (inputApkSigningBlockOffset != -1)
+ ? inputApkSigningBlockOffset
+ : inputZipSections.getZipCentralDirectoryOffset());
+
+ // Step 2. Parse the input APK's ZIP Central Directory
+ ByteBuffer inputCd = getZipCentralDirectory(inputApk, inputZipSections);
+ List inputCdRecords =
+ parseZipCentralDirectory(inputCd, inputZipSections);
+
+ List pinPatterns = extractPinPatterns(inputCdRecords, inputApkLfhSection);
+ List pinByteRanges = pinPatterns == null ? null : new ArrayList<>();
+
+ // Step 3. Obtain a signer engine instance
+ ApkSignerEngine signerEngine;
+ if (mSignerEngine != null) {
+ // Use the provided signer engine
+ signerEngine = mSignerEngine;
+ } else {
+ // Construct a signer engine from the provided parameters
+ int minSdkVersion;
+ if (mMinSdkVersion != null) {
+ // No need to extract minSdkVersion from the APK's AndroidManifest.xml
+ minSdkVersion = mMinSdkVersion;
+ } else {
+ // Need to extract minSdkVersion from the APK's AndroidManifest.xml
+ minSdkVersion = getMinSdkVersionFromApk(inputCdRecords, inputApkLfhSection);
+ }
+ List engineSignerConfigs =
+ new ArrayList<>(mSignerConfigs.size());
+ for (SignerConfig signerConfig : mSignerConfigs) {
+ engineSignerConfigs.add(
+ new DefaultApkSignerEngine.SignerConfig.Builder(
+ signerConfig.getName(),
+ signerConfig.getPrivateKey(),
+ signerConfig.getCertificates())
+ .build());
+ }
+ DefaultApkSignerEngine.Builder signerEngineBuilder =
+ new DefaultApkSignerEngine.Builder(engineSignerConfigs, minSdkVersion)
+ .setV1SigningEnabled(mV1SigningEnabled)
+ .setV2SigningEnabled(mV2SigningEnabled)
+ .setV3SigningEnabled(mV3SigningEnabled)
+ .setDebuggableApkPermitted(mDebuggableApkPermitted)
+ .setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved)
+ .setSigningCertificateLineage(mSigningCertificateLineage);
+ if (mCreatedBy != null) {
+ signerEngineBuilder.setCreatedBy(mCreatedBy);
+ }
+ signerEngine = signerEngineBuilder.build();
+ }
+
+ // Step 4. Provide the signer engine with the input APK's APK Signing Block (if any)
+ if (inputApkSigningBlock != null) {
+ signerEngine.inputApkSigningBlock(inputApkSigningBlock);
+ }
+
+ // Step 5. Iterate over input APK's entries and output the Local File Header + data of those
+ // entries which need to be output. Entries are iterated in the order in which their Local
+ // File Header records are stored in the file. This is to achieve better data locality in
+ // case Central Directory entries are in the wrong order.
+ List inputCdRecordsSortedByLfhOffset =
+ new ArrayList<>(inputCdRecords);
+ Collections.sort(
+ inputCdRecordsSortedByLfhOffset,
+ CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR);
+ int lastModifiedDateForNewEntries = -1;
+ int lastModifiedTimeForNewEntries = -1;
+ long inputOffset = 0;
+ long outputOffset = 0;
+ Map outputCdRecordsByName =
+ new HashMap<>(inputCdRecords.size());
+ for (final CentralDirectoryRecord inputCdRecord : inputCdRecordsSortedByLfhOffset) {
+ String entryName = inputCdRecord.getName();
+ if (Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME.equals(entryName)) {
+ continue; // We'll re-add below if needed.
+ }
+ ApkSignerEngine.InputJarEntryInstructions entryInstructions =
+ signerEngine.inputJarEntry(entryName);
+ boolean shouldOutput;
+ switch (entryInstructions.getOutputPolicy()) {
+ case OUTPUT:
+ shouldOutput = true;
+ break;
+ case OUTPUT_BY_ENGINE:
+ case SKIP:
+ shouldOutput = false;
+ break;
+ default:
+ throw new RuntimeException(
+ "Unknown output policy: " + entryInstructions.getOutputPolicy());
+ }
+
+ long inputLocalFileHeaderStartOffset = inputCdRecord.getLocalFileHeaderOffset();
+ if (inputLocalFileHeaderStartOffset > inputOffset) {
+ // Unprocessed data in input starting at inputOffset and ending and the start of
+ // this record's LFH. We output this data verbatim because this signer is supposed
+ // to preserve as much of input as possible.
+ long chunkSize = inputLocalFileHeaderStartOffset - inputOffset;
+ inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut);
+ outputOffset += chunkSize;
+ inputOffset = inputLocalFileHeaderStartOffset;
+ }
+ LocalFileRecord inputLocalFileRecord;
+ try {
+ inputLocalFileRecord =
+ LocalFileRecord.getRecord(
+ inputApkLfhSection, inputCdRecord, inputApkLfhSection.size());
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Malformed ZIP entry: " + inputCdRecord.getName(), e);
+ }
+ inputOffset += inputLocalFileRecord.getSize();
+
+ ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+ entryInstructions.getInspectJarEntryRequest();
+ if (inspectEntryRequest != null) {
+ fulfillInspectInputJarEntryRequest(
+ inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest);
+ }
+
+ if (shouldOutput) {
+ // Find the max value of last modified, to be used for new entries added by the
+ // signer.
+ int lastModifiedDate = inputCdRecord.getLastModificationDate();
+ int lastModifiedTime = inputCdRecord.getLastModificationTime();
+ if ((lastModifiedDateForNewEntries == -1)
+ || (lastModifiedDate > lastModifiedDateForNewEntries)
+ || ((lastModifiedDate == lastModifiedDateForNewEntries)
+ && (lastModifiedTime > lastModifiedTimeForNewEntries))) {
+ lastModifiedDateForNewEntries = lastModifiedDate;
+ lastModifiedTimeForNewEntries = lastModifiedTime;
+ }
+
+ inspectEntryRequest = signerEngine.outputJarEntry(entryName);
+ if (inspectEntryRequest != null) {
+ fulfillInspectInputJarEntryRequest(
+ inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest);
+ }
+
+ // Output entry's Local File Header + data
+ long outputLocalFileHeaderOffset = outputOffset;
+ long outputLocalFileRecordSize =
+ outputInputJarEntryLfhRecordPreservingDataAlignment(
+ inputApkLfhSection,
+ inputLocalFileRecord,
+ outputApkOut,
+ outputLocalFileHeaderOffset);
+ outputOffset += outputLocalFileRecordSize;
+
+ if (pinPatterns != null) {
+ boolean pinThisFile = false;
+ for (Pattern pinPattern : pinPatterns) {
+ if (pinPattern.matcher(inputCdRecord.getName()).matches()) {
+ pinThisFile = true;
+ break;
+ }
+ }
+
+ if (pinThisFile) {
+ pinByteRanges.add(
+ new Hints.ByteRange(
+ outputLocalFileHeaderOffset,
+ outputOffset));
+ }
+ }
+
+ // Enqueue entry's Central Directory record for output
+ CentralDirectoryRecord outputCdRecord;
+ if (outputLocalFileHeaderOffset == inputLocalFileRecord.getStartOffsetInArchive()) {
+ outputCdRecord = inputCdRecord;
+ } else {
+ outputCdRecord =
+ inputCdRecord.createWithModifiedLocalFileHeaderOffset(
+ outputLocalFileHeaderOffset);
+ }
+ outputCdRecordsByName.put(entryName, outputCdRecord);
+ }
+ }
+ long inputLfhSectionSize = inputApkLfhSection.size();
+ if (inputOffset < inputLfhSectionSize) {
+ // Unprocessed data in input starting at inputOffset and ending and the end of the input
+ // APK's LFH section. We output this data verbatim because this signer is supposed
+ // to preserve as much of input as possible.
+ long chunkSize = inputLfhSectionSize - inputOffset;
+ inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut);
+ outputOffset += chunkSize;
+ inputOffset = inputLfhSectionSize;
+ }
+
+ // Step 6. Sort output APK's Central Directory records in the order in which they should
+ // appear in the output
+ List outputCdRecords = new ArrayList<>(inputCdRecords.size() + 10);
+ for (CentralDirectoryRecord inputCdRecord : inputCdRecords) {
+ String entryName = inputCdRecord.getName();
+ CentralDirectoryRecord outputCdRecord = outputCdRecordsByName.get(entryName);
+ if (outputCdRecord != null) {
+ outputCdRecords.add(outputCdRecord);
+ }
+ }
+
+ // Step 7. Generate and output JAR signatures, if necessary. This may output more Local File
+ // Header + data entries and add to the list of output Central Directory records.
+ ApkSignerEngine.OutputJarSignatureRequest outputJarSignatureRequest =
+ signerEngine.outputJarEntries();
+ if (outputJarSignatureRequest != null) {
+ if (lastModifiedDateForNewEntries == -1) {
+ lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS)
+ lastModifiedTimeForNewEntries = 0;
+ }
+ for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry :
+ outputJarSignatureRequest.getAdditionalJarEntries()) {
+ String entryName = entry.getName();
+ byte[] uncompressedData = entry.getData();
+ ZipUtils.DeflateResult deflateResult =
+ ZipUtils.deflate(ByteBuffer.wrap(uncompressedData));
+ byte[] compressedData = deflateResult.output;
+ long uncompressedDataCrc32 = deflateResult.inputCrc32;
+
+ ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+ signerEngine.outputJarEntry(entryName);
+ if (inspectEntryRequest != null) {
+ inspectEntryRequest.getDataSink().consume(
+ uncompressedData, 0, uncompressedData.length);
+ inspectEntryRequest.done();
+ }
+
+ long localFileHeaderOffset = outputOffset;
+ outputOffset +=
+ LocalFileRecord.outputRecordWithDeflateCompressedData(
+ entryName,
+ lastModifiedTimeForNewEntries,
+ lastModifiedDateForNewEntries,
+ compressedData,
+ uncompressedDataCrc32,
+ uncompressedData.length,
+ outputApkOut);
+
+
+ outputCdRecords.add(
+ CentralDirectoryRecord.createWithDeflateCompressedData(
+ entryName,
+ lastModifiedTimeForNewEntries,
+ lastModifiedDateForNewEntries,
+ uncompressedDataCrc32,
+ compressedData.length,
+ uncompressedData.length,
+ localFileHeaderOffset));
+ }
+ outputJarSignatureRequest.done();
+ }
+
+ if (pinByteRanges != null) {
+ pinByteRanges.add(new Hints.ByteRange(outputOffset, Long.MAX_VALUE)); // central dir
+ String entryName = Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME;
+ byte[] uncompressedData = Hints.encodeByteRangeList(pinByteRanges);
+ ZipUtils.DeflateResult deflateResult =
+ ZipUtils.deflate(ByteBuffer.wrap(uncompressedData));
+ byte[] compressedData = deflateResult.output;
+ long uncompressedDataCrc32 = deflateResult.inputCrc32;
+ long localFileHeaderOffset = outputOffset;
+ outputOffset +=
+ LocalFileRecord.outputRecordWithDeflateCompressedData(
+ entryName,
+ lastModifiedTimeForNewEntries,
+ lastModifiedDateForNewEntries,
+ compressedData,
+ uncompressedDataCrc32,
+ uncompressedData.length,
+ outputApkOut);
+ outputCdRecords.add(
+ CentralDirectoryRecord.createWithDeflateCompressedData(
+ entryName,
+ lastModifiedTimeForNewEntries,
+ lastModifiedDateForNewEntries,
+ uncompressedDataCrc32,
+ compressedData.length,
+ uncompressedData.length,
+ localFileHeaderOffset));
+ }
+
+ // Step 8. Construct output ZIP Central Directory in an in-memory buffer
+ long outputCentralDirSizeBytes = 0;
+ for (CentralDirectoryRecord record : outputCdRecords) {
+ outputCentralDirSizeBytes += record.getSize();
+ }
+ if (outputCentralDirSizeBytes > Integer.MAX_VALUE) {
+ throw new IOException(
+ "Output ZIP Central Directory too large: " + outputCentralDirSizeBytes
+ + " bytes");
+ }
+ ByteBuffer outputCentralDir = ByteBuffer.allocate((int) outputCentralDirSizeBytes);
+ for (CentralDirectoryRecord record : outputCdRecords) {
+ record.copyTo(outputCentralDir);
+ }
+ outputCentralDir.flip();
+ DataSource outputCentralDirDataSource = new ByteBufferDataSource(outputCentralDir);
+ long outputCentralDirStartOffset = outputOffset;
+ int outputCentralDirRecordCount = outputCdRecords.size();
+
+ // Step 9. Construct output ZIP End of Central Directory record in an in-memory buffer
+ ByteBuffer outputEocd =
+ EocdRecord.createWithModifiedCentralDirectoryInfo(
+ inputZipSections.getZipEndOfCentralDirectory(),
+ outputCentralDirRecordCount,
+ outputCentralDirDataSource.size(),
+ outputCentralDirStartOffset);
+
+ // Step 10. Generate and output APK Signature Scheme v2 and/or v3 signatures, if necessary.
+ // This may insert an APK Signing Block just before the output's ZIP Central Directory
+ ApkSignerEngine.OutputApkSigningBlockRequest2 outputApkSigningBlockRequest =
+ signerEngine.outputZipSections2(
+ outputApkIn,
+ outputCentralDirDataSource,
+ DataSources.asDataSource(outputEocd));
+
+ if (outputApkSigningBlockRequest != null) {
+ int padding = outputApkSigningBlockRequest.getPaddingSizeBeforeApkSigningBlock();
+ outputApkOut.consume(ByteBuffer.allocate(padding));
+ byte[] outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock();
+ outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length);
+ ZipUtils.setZipEocdCentralDirectoryOffset(outputEocd,
+ outputCentralDirStartOffset + padding + outputApkSigningBlock.length);
+ outputApkSigningBlockRequest.done();
+ }
+
+ // Step 11. Output ZIP Central Directory and ZIP End of Central Directory
+ outputCentralDirDataSource.feed(0, outputCentralDirDataSource.size(), outputApkOut);
+ outputApkOut.consume(outputEocd);
+ signerEngine.outputDone();
+ }
+
+ private static void fulfillInspectInputJarEntryRequest(
+ DataSource lfhSection,
+ LocalFileRecord localFileRecord,
+ ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest)
+ throws IOException, ApkFormatException {
+ try {
+ localFileRecord.outputUncompressedData(lfhSection, inspectEntryRequest.getDataSink());
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Malformed ZIP entry: " + localFileRecord.getName(), e);
+ }
+ inspectEntryRequest.done();
+ }
+
+ private static long outputInputJarEntryLfhRecordPreservingDataAlignment(
+ DataSource inputLfhSection,
+ LocalFileRecord inputRecord,
+ DataSink outputLfhSection,
+ long outputOffset) throws IOException {
+ long inputOffset = inputRecord.getStartOffsetInArchive();
+ if (inputOffset == outputOffset) {
+ // This record's data will be aligned same as in the input APK.
+ return inputRecord.outputRecord(inputLfhSection, outputLfhSection);
+ }
+ int dataAlignmentMultiple = getInputJarEntryDataAlignmentMultiple(inputRecord);
+ if ((dataAlignmentMultiple <= 1)
+ || ((inputOffset % dataAlignmentMultiple)
+ == (outputOffset % dataAlignmentMultiple))) {
+ // This record's data will be aligned same as in the input APK.
+ return inputRecord.outputRecord(inputLfhSection, outputLfhSection);
+ }
+
+ long inputDataStartOffset = inputOffset + inputRecord.getDataStartOffsetInRecord();
+ if ((inputDataStartOffset % dataAlignmentMultiple) != 0) {
+ // This record's data is not aligned in the input APK. No need to align it in the
+ // output.
+ return inputRecord.outputRecord(inputLfhSection, outputLfhSection);
+ }
+
+ // This record's data needs to be re-aligned in the output. This is achieved using the
+ // record's extra field.
+ ByteBuffer aligningExtra =
+ createExtraFieldToAlignData(
+ inputRecord.getExtra(),
+ outputOffset + inputRecord.getExtraFieldStartOffsetInsideRecord(),
+ dataAlignmentMultiple);
+ return inputRecord.outputRecordWithModifiedExtra(
+ inputLfhSection, aligningExtra, outputLfhSection);
+ }
+
+ private static int getInputJarEntryDataAlignmentMultiple(LocalFileRecord entry) {
+ if (entry.isDataCompressed()) {
+ // Compressed entries don't need to be aligned
+ return 1;
+ }
+
+ // Attempt to obtain the alignment multiple from the entry's extra field.
+ ByteBuffer extra = entry.getExtra();
+ if (extra.hasRemaining()) {
+ extra.order(ByteOrder.LITTLE_ENDIAN);
+ // FORMAT: sequence of fields. Each field consists of:
+ // * uint16 ID
+ // * uint16 size
+ // * 'size' bytes: payload
+ while (extra.remaining() >= 4) {
+ short headerId = extra.getShort();
+ int dataSize = ZipUtils.getUnsignedInt16(extra);
+ if (dataSize > extra.remaining()) {
+ // Malformed field -- insufficient input remaining
+ break;
+ }
+ if (headerId != ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) {
+ // Skip this field
+ extra.position(extra.position() + dataSize);
+ continue;
+ }
+ // This is APK alignment field.
+ // FORMAT:
+ // * uint16 alignment multiple (in bytes)
+ // * remaining bytes -- padding to achieve alignment of data which starts after
+ // the extra field
+ if (dataSize < 2) {
+ // Malformed
+ break;
+ }
+ return ZipUtils.getUnsignedInt16(extra);
+ }
+ }
+
+ // Fall back to filename-based defaults
+ return (entry.getName().endsWith(".so")) ? ANDROID_COMMON_PAGE_ALIGNMENT_BYTES : 4;
+ }
+
+ private static ByteBuffer createExtraFieldToAlignData(
+ ByteBuffer original,
+ long extraStartOffset,
+ int dataAlignmentMultiple) {
+ if (dataAlignmentMultiple <= 1) {
+ return original;
+ }
+
+ // In the worst case scenario, we'll increase the output size by 6 + dataAlignment - 1.
+ ByteBuffer result = ByteBuffer.allocate(original.remaining() + 5 + dataAlignmentMultiple);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+
+ // Step 1. Output all extra fields other than the one which is to do with alignment
+ // FORMAT: sequence of fields. Each field consists of:
+ // * uint16 ID
+ // * uint16 size
+ // * 'size' bytes: payload
+ while (original.remaining() >= 4) {
+ short headerId = original.getShort();
+ int dataSize = ZipUtils.getUnsignedInt16(original);
+ if (dataSize > original.remaining()) {
+ // Malformed field -- insufficient input remaining
+ break;
+ }
+ if (((headerId == 0) && (dataSize == 0))
+ || (headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID)) {
+ // Ignore the field if it has to do with the old APK data alignment method (filling
+ // the extra field with 0x00 bytes) or the new APK data alignment method.
+ original.position(original.position() + dataSize);
+ continue;
+ }
+ // Copy this field (including header) to the output
+ original.position(original.position() - 4);
+ int originalLimit = original.limit();
+ original.limit(original.position() + 4 + dataSize);
+ result.put(original);
+ original.limit(originalLimit);
+ }
+
+ // Step 2. Add alignment field
+ // FORMAT:
+ // * uint16 extra header ID
+ // * uint16 extra data size
+ // Payload ('data size' bytes)
+ // * uint16 alignment multiple (in bytes)
+ // * remaining bytes -- padding to achieve alignment of data which starts after the
+ // extra field
+ long dataMinStartOffset =
+ extraStartOffset + result.position()
+ + ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES;
+ int paddingSizeBytes =
+ (dataAlignmentMultiple - ((int) (dataMinStartOffset % dataAlignmentMultiple)))
+ % dataAlignmentMultiple;
+ result.putShort(ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID);
+ ZipUtils.putUnsignedInt16(result, 2 + paddingSizeBytes);
+ ZipUtils.putUnsignedInt16(result, dataAlignmentMultiple);
+ result.position(result.position() + paddingSizeBytes);
+ result.flip();
+
+ return result;
+ }
+
+ private static ByteBuffer getZipCentralDirectory(
+ DataSource apk,
+ ApkUtils.ZipSections apkSections) throws IOException, ApkFormatException {
+ long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes();
+ if (cdSizeBytes > Integer.MAX_VALUE) {
+ throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes);
+ }
+ long cdOffset = apkSections.getZipCentralDirectoryOffset();
+ ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes);
+ cd.order(ByteOrder.LITTLE_ENDIAN);
+ return cd;
+ }
+
+ private static List parseZipCentralDirectory(
+ ByteBuffer cd,
+ ApkUtils.ZipSections apkSections) throws ApkFormatException {
+ long cdOffset = apkSections.getZipCentralDirectoryOffset();
+ int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount();
+ List cdRecords = new ArrayList<>(expectedCdRecordCount);
+ Set entryNames = new HashSet<>(expectedCdRecordCount);
+ for (int i = 0; i < expectedCdRecordCount; i++) {
+ CentralDirectoryRecord cdRecord;
+ int offsetInsideCd = cd.position();
+ try {
+ cdRecord = CentralDirectoryRecord.getRecord(cd);
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException(
+ "Malformed ZIP Central Directory record #" + (i + 1)
+ + " at file offset " + (cdOffset + offsetInsideCd),
+ e);
+ }
+ String entryName = cdRecord.getName();
+ if (!entryNames.add(entryName)) {
+ throw new ApkFormatException(
+ "Multiple ZIP entries with the same name: " + entryName);
+ }
+ cdRecords.add(cdRecord);
+ }
+ if (cd.hasRemaining()) {
+ throw new ApkFormatException(
+ "Unused space at the end of ZIP Central Directory: " + cd.remaining()
+ + " bytes starting at file offset " + (cdOffset + cd.position()));
+ }
+
+ return cdRecords;
+ }
+
+ private static CentralDirectoryRecord findCdRecord(
+ List cdRecords, String name) {
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (name.equals(cdRecord.getName())) {
+ return cdRecord;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the contents of the APK's {@code AndroidManifest.xml} or {@code null} if this entry
+ * is not present in the APK.
+ */
+ static ByteBuffer getAndroidManifestFromApk(
+ List cdRecords, DataSource lhfSection)
+ throws IOException, ApkFormatException, ZipFormatException {
+ CentralDirectoryRecord androidManifestCdRecord =
+ findCdRecord(cdRecords, ANDROID_MANIFEST_ZIP_ENTRY_NAME);
+ if (androidManifestCdRecord == null) {
+ throw new ApkFormatException("Missing " + ANDROID_MANIFEST_ZIP_ENTRY_NAME);
+ }
+
+ return ByteBuffer.wrap(
+ LocalFileRecord.getUncompressedData(
+ lhfSection, androidManifestCdRecord, lhfSection.size()));
+ }
+
+ /**
+ * Return list of pin patterns embedded in the pin pattern asset
+ * file. If no such file, return {@code null}.
+ */
+ private static List extractPinPatterns(
+ List cdRecords, DataSource lhfSection)
+ throws IOException, ApkFormatException {
+ CentralDirectoryRecord pinListCdRecord =
+ findCdRecord(cdRecords, Hints.PIN_HINT_ASSET_ZIP_ENTRY_NAME);
+ List pinPatterns = null;
+ if (pinListCdRecord != null) {
+ pinPatterns = new ArrayList<>();
+ byte[] patternBlob;
+ try {
+ patternBlob = LocalFileRecord.getUncompressedData(
+ lhfSection, pinListCdRecord, lhfSection.size());
+ } catch (ZipFormatException ex) {
+ throw new ApkFormatException("Bad " + pinListCdRecord);
+ }
+ pinPatterns = Hints.parsePinPatterns(patternBlob);
+ }
+ return pinPatterns;
+ }
+
+ /**
+ * Returns the minimum Android version (API Level) supported by the provided APK. This is based
+ * on the {@code android:minSdkVersion} attributes of the APK's {@code AndroidManifest.xml}.
+ */
+ private static int getMinSdkVersionFromApk(
+ List cdRecords, DataSource lhfSection)
+ throws IOException, MinSdkVersionException {
+ ByteBuffer androidManifest;
+ try {
+ androidManifest = getAndroidManifestFromApk(cdRecords, lhfSection);
+ } catch (ZipFormatException | ApkFormatException e) {
+ throw new MinSdkVersionException(
+ "Failed to determine APK's minimum supported Android platform version",
+ e);
+ }
+ return ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest);
+ }
+
+ /**
+ * Configuration of a signer.
+ *
+ * Use {@link Builder} to obtain configuration instances.
+ */
+ public static class SignerConfig {
+ private final String mName;
+ private final PrivateKey mPrivateKey;
+ private final List mCertificates;
+
+ private SignerConfig(
+ String name,
+ PrivateKey privateKey,
+ List certificates) {
+ mName = name;
+ mPrivateKey = privateKey;
+ mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates));
+ }
+
+ /**
+ * Returns the name of this signer.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the signing key of this signer.
+ */
+ public PrivateKey getPrivateKey() {
+ return mPrivateKey;
+ }
+
+ /**
+ * Returns the certificate(s) of this signer. The first certificate's public key corresponds
+ * to this signer's private key.
+ */
+ public List getCertificates() {
+ return mCertificates;
+ }
+
+ /**
+ * Builder of {@link SignerConfig} instances.
+ */
+ public static class Builder {
+ private final String mName;
+ private final PrivateKey mPrivateKey;
+ private final List mCertificates;
+
+ /**
+ * Constructs a new {@code Builder}.
+ *
+ * @param name signer's name. The name is reflected in the name of files comprising the
+ * JAR signature of the APK.
+ * @param privateKey signing key
+ * @param certificates list of one or more X.509 certificates. The subject public key of
+ * the first certificate must correspond to the {@code privateKey}.
+ */
+ public Builder(
+ String name,
+ PrivateKey privateKey,
+ List certificates) {
+ if (name.isEmpty()) {
+ throw new IllegalArgumentException("Empty name");
+ }
+ mName = name;
+ mPrivateKey = privateKey;
+ mCertificates = new ArrayList<>(certificates);
+ }
+
+ /**
+ * Returns a new {@code SignerConfig} instance configured based on the configuration of
+ * this builder.
+ */
+ public SignerConfig build() {
+ return new SignerConfig(
+ mName,
+ mPrivateKey,
+ mCertificates);
+ }
+ }
+ }
+
+ /**
+ * Builder of {@link ApkSigner} instances.
+ *
+ * The builder requires the following information to construct a working {@code ApkSigner}:
+ *
+ * - Signer configs or {@link ApkSignerEngine} -- provided in the constructor,
+ * - APK to be signed -- see {@link #setInputApk(File) setInputApk} variants,
+ * - where to store the output signed APK -- see {@link #setOutputApk(File) setOutputApk}
+ * variants.
+ *
+ *
+ */
+ public static class Builder {
+ private final List mSignerConfigs;
+ private boolean mV1SigningEnabled = true;
+ private boolean mV2SigningEnabled = true;
+ private boolean mV3SigningEnabled = true;
+ private boolean mDebuggableApkPermitted = true;
+ private boolean mOtherSignersSignaturesPreserved;
+ private String mCreatedBy;
+ private Integer mMinSdkVersion;
+
+ private final ApkSignerEngine mSignerEngine;
+
+ private File mInputApkFile;
+ private DataSource mInputApkDataSource;
+
+ private File mOutputApkFile;
+ private DataSink mOutputApkDataSink;
+ private DataSource mOutputApkDataSource;
+
+ private SigningCertificateLineage mSigningCertificateLineage;
+
+ // APK Signature Scheme v3 only supports a single signing certificate, so to move to v3
+ // signing by default, but not require prior clients to update to explicitly disable v3
+ // signing for multiple signers, we modify the mV3SigningEnabled depending on the provided
+ // inputs (multiple signers and mSigningCertificateLineage in particular). Maintain two
+ // extra variables to record whether or not mV3SigningEnabled has been set directly by a
+ // client and so should override the default behavior.
+ private boolean mV3SigningExplicitlyDisabled = false;
+ private boolean mV3SigningExplicitlyEnabled = false;
+
+ /**
+ * Constructs a new {@code Builder} for an {@code ApkSigner} which signs using the provided
+ * signer configurations. The resulting signer may be further customized through this
+ * builder's setters, such as {@link #setMinSdkVersion(int)},
+ * {@link #setV1SigningEnabled(boolean)}, {@link #setV2SigningEnabled(boolean)},
+ * {@link #setOtherSignersSignaturesPreserved(boolean)}, {@link #setCreatedBy(String)}.
+ *
+ * {@link #Builder(ApkSignerEngine)} is an alternative for advanced use cases where
+ * more control over low-level details of signing is desired.
+ */
+ public Builder(List signerConfigs) {
+ if (signerConfigs.isEmpty()) {
+ throw new IllegalArgumentException("At least one signer config must be provided");
+ }
+ if (signerConfigs.size() > 1) {
+ // APK Signature Scheme v3 only supports single signer, unless a
+ // SigningCertificateLineage is provided, in which case this will be reset to true,
+ // since we don't yet have a v4 scheme about which to worry
+ mV3SigningEnabled = false;
+ }
+ mSignerConfigs = new ArrayList<>(signerConfigs);
+ mSignerEngine = null;
+ }
+
+ /**
+ * Constructs a new {@code Builder} for an {@code ApkSigner} which signs using the
+ * provided signing engine. This is meant for advanced use cases where more control is
+ * needed over the lower-level details of signing. For typical use cases,
+ * {@link #Builder(List)} is more appropriate.
+ */
+ public Builder(ApkSignerEngine signerEngine) {
+ if (signerEngine == null) {
+ throw new NullPointerException("signerEngine == null");
+ }
+ mSignerEngine = signerEngine;
+ mSignerConfigs = null;
+ }
+
+ /**
+ * Sets the APK to be signed.
+ *
+ * @see #setInputApk(DataSource)
+ */
+ public Builder setInputApk(File inputApk) {
+ if (inputApk == null) {
+ throw new NullPointerException("inputApk == null");
+ }
+ mInputApkFile = inputApk;
+ mInputApkDataSource = null;
+ return this;
+ }
+
+ /**
+ * Sets the APK to be signed.
+ *
+ * @see #setInputApk(File)
+ */
+ public Builder setInputApk(DataSource inputApk) {
+ if (inputApk == null) {
+ throw new NullPointerException("inputApk == null");
+ }
+ mInputApkDataSource = inputApk;
+ mInputApkFile = null;
+ return this;
+ }
+
+ /**
+ * Sets the location of the output (signed) APK. {@code ApkSigner} will create this file if
+ * it doesn't exist.
+ *
+ * @see #setOutputApk(ReadableDataSink)
+ * @see #setOutputApk(DataSink, DataSource)
+ */
+ public Builder setOutputApk(File outputApk) {
+ if (outputApk == null) {
+ throw new NullPointerException("outputApk == null");
+ }
+ mOutputApkFile = outputApk;
+ mOutputApkDataSink = null;
+ mOutputApkDataSource = null;
+ return this;
+ }
+
+ /**
+ * Sets the readable data sink which will receive the output (signed) APK. After signing,
+ * the contents of the output APK will be available via the {@link DataSource} interface of
+ * the sink.
+ *
+ * This variant of {@code setOutputApk} is useful for avoiding writing the output APK to
+ * a file. For example, an in-memory data sink, such as
+ * {@link DataSinks#newInMemoryDataSink()}, could be used instead of a file.
+ *
+ * @see #setOutputApk(File)
+ * @see #setOutputApk(DataSink, DataSource)
+ */
+ public Builder setOutputApk(ReadableDataSink outputApk) {
+ if (outputApk == null) {
+ throw new NullPointerException("outputApk == null");
+ }
+ return setOutputApk(outputApk, outputApk);
+ }
+
+ /**
+ * Sets the sink which will receive the output (signed) APK. Data received by the
+ * {@code outputApkOut} sink must be visible through the {@code outputApkIn} data source.
+ *
+ *
This is an advanced variant of {@link #setOutputApk(ReadableDataSink)}, enabling the
+ * sink and the source to be different objects.
+ *
+ * @see #setOutputApk(ReadableDataSink)
+ * @see #setOutputApk(File)
+ */
+ public Builder setOutputApk(DataSink outputApkOut, DataSource outputApkIn) {
+ if (outputApkOut == null) {
+ throw new NullPointerException("outputApkOut == null");
+ }
+ if (outputApkIn == null) {
+ throw new NullPointerException("outputApkIn == null");
+ }
+ mOutputApkFile = null;
+ mOutputApkDataSink = outputApkOut;
+ mOutputApkDataSource = outputApkIn;
+ return this;
+ }
+
+ /**
+ * Sets the minimum Android platform version (API Level) on which APK signatures produced
+ * by the signer being built must verify. This method is useful for overriding the default
+ * behavior where the minimum API Level is obtained from the {@code android:minSdkVersion}
+ * attribute of the APK's {@code AndroidManifest.xml}.
+ *
+ *
Note: This method may result in APK signatures which don't verify on some
+ * Android platform versions supported by the APK.
+ *
+ *
Note: This method may only be invoked when this builder is not initialized
+ * with an {@link ApkSignerEngine}.
+ *
+ * @throws IllegalStateException if this builder was initialized with an
+ * {@link ApkSignerEngine}
+ */
+ public Builder setMinSdkVersion(int minSdkVersion) {
+ checkInitializedWithoutEngine();
+ mMinSdkVersion = minSdkVersion;
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
+ *
+ *
By default, whether APK is signed using JAR signing is determined by
+ * {@code ApkSigner}, based on the platform versions supported by the APK or specified using
+ * {@link #setMinSdkVersion(int)}. Disabling JAR signing will result in APK signatures which
+ * don't verify on Android Marshmallow (Android 6.0, API Level 23) and lower.
+ *
+ *
Note: This method may only be invoked when this builder is not initialized
+ * with an {@link ApkSignerEngine}.
+ *
+ * @param enabled {@code true} to require the APK to be signed using JAR signing,
+ * {@code false} to require the APK to not be signed using JAR signing.
+ *
+ * @throws IllegalStateException if this builder was initialized with an
+ * {@link ApkSignerEngine}
+ *
+ * @see JAR signing
+ */
+ public Builder setV1SigningEnabled(boolean enabled) {
+ checkInitializedWithoutEngine();
+ mV1SigningEnabled = enabled;
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should be signed using APK Signature Scheme v2 (aka v2 signature
+ * scheme).
+ *
+ *
By default, whether APK is signed using APK Signature Scheme v2 is determined by
+ * {@code ApkSigner} based on the platform versions supported by the APK or specified using
+ * {@link #setMinSdkVersion(int)}.
+ *
+ *
Note: This method may only be invoked when this builder is not initialized
+ * with an {@link ApkSignerEngine}.
+ *
+ * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme
+ * v2, {@code false} to require the APK to not be signed using APK Signature Scheme
+ * v2.
+ *
+ * @throws IllegalStateException if this builder was initialized with an
+ * {@link ApkSignerEngine}
+ *
+ * @see APK Signature Scheme v2
+ */
+ public Builder setV2SigningEnabled(boolean enabled) {
+ checkInitializedWithoutEngine();
+ mV2SigningEnabled = enabled;
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should be signed using APK Signature Scheme v3 (aka v3 signature
+ * scheme).
+ *
+ *
By default, whether APK is signed using APK Signature Scheme v3 is determined by
+ * {@code ApkSigner} based on the platform versions supported by the APK or specified using
+ * {@link #setMinSdkVersion(int)}.
+ *
+ *
Note: This method may only be invoked when this builder is not initialized
+ * with an {@link ApkSignerEngine}.
+ *
Note: APK Signature Scheme v3 only supports a single signing certificate, but
+ * may take multiple signers mapping to different targeted platform versions.
+ *
+ * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme
+ * v3, {@code false} to require the APK to not be signed using APK Signature Scheme
+ * v3.
+ *
+ * @throws IllegalStateException if this builder was initialized with an
+ * {@link ApkSignerEngine}
+ */
+ public Builder setV3SigningEnabled(boolean enabled) {
+ checkInitializedWithoutEngine();
+ mV3SigningEnabled = enabled;
+ if (enabled) {
+ mV3SigningExplicitlyEnabled = true;
+ } else {
+ mV3SigningExplicitlyDisabled = true;
+ }
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should be signed even if it is marked as debuggable
+ * ({@code android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward
+ * compatibility reasons, the default value of this setting is {@code true}.
+ *
+ *
It is dangerous to sign debuggable APKs with production/release keys because Android
+ * platform loosens security checks for such APKs. For example, arbitrary unauthorized code
+ * may be executed in the context of such an app by anybody with ADB shell access.
+ *
+ *
Note: This method may only be invoked when this builder is not initialized
+ * with an {@link ApkSignerEngine}.
+ */
+ public Builder setDebuggableApkPermitted(boolean permitted) {
+ checkInitializedWithoutEngine();
+ mDebuggableApkPermitted = permitted;
+ return this;
+ }
+
+ /**
+ * Sets whether signatures produced by signers other than the ones configured in this engine
+ * should be copied from the input APK to the output APK.
+ *
+ *
By default, signatures of other signers are omitted from the output APK.
+ *
+ *
Note: This method may only be invoked when this builder is not initialized
+ * with an {@link ApkSignerEngine}.
+ *
+ * @throws IllegalStateException if this builder was initialized with an
+ * {@link ApkSignerEngine}
+ */
+ public Builder setOtherSignersSignaturesPreserved(boolean preserved) {
+ checkInitializedWithoutEngine();
+ mOtherSignersSignaturesPreserved = preserved;
+ return this;
+ }
+
+ /**
+ * Sets the value of the {@code Created-By} field in JAR signature files.
+ *
+ *
Note: This method may only be invoked when this builder is not initialized
+ * with an {@link ApkSignerEngine}.
+ *
+ * @throws IllegalStateException if this builder was initialized with an
+ * {@link ApkSignerEngine}
+ */
+ public Builder setCreatedBy(String createdBy) {
+ checkInitializedWithoutEngine();
+ if (createdBy == null) {
+ throw new NullPointerException();
+ }
+ mCreatedBy = createdBy;
+ return this;
+ }
+
+ private void checkInitializedWithoutEngine() {
+ if (mSignerEngine != null) {
+ throw new IllegalStateException(
+ "Operation is not available when builder initialized with an engine");
+ }
+ }
+
+ /**
+ * Sets the {@link SigningCertificateLineage} to use with the v3 signature scheme. This
+ * structure provides proof of signing certificate rotation linking {@link SignerConfig}
+ * objects to previous ones.
+ */
+ public Builder setSigningCertificateLineage(
+ SigningCertificateLineage signingCertificateLineage) {
+ if (signingCertificateLineage != null) {
+ mV3SigningEnabled = true;
+ mSigningCertificateLineage = signingCertificateLineage;
+ }
+ return this;
+ }
+
+ /**
+ * Returns a new {@code ApkSigner} instance initialized according to the configuration of
+ * this builder.
+ */
+ public ApkSigner build() {
+
+ if (mV3SigningExplicitlyDisabled && mV3SigningExplicitlyEnabled) {
+ throw new IllegalStateException("Builder configured to both enable and disable APK "
+ + "Signature Scheme v3 signing");
+ }
+
+ if (mV3SigningExplicitlyDisabled) {
+ mV3SigningEnabled = false;
+ }
+
+ if (mV3SigningExplicitlyEnabled) {
+ mV3SigningEnabled = true;
+ }
+
+ // TODO - if v3 signing is enabled, check provided signers and history to see if valid
+
+ return new ApkSigner(
+ mSignerConfigs,
+ mMinSdkVersion,
+ mV1SigningEnabled,
+ mV2SigningEnabled,
+ mV3SigningEnabled,
+ mDebuggableApkPermitted,
+ mOtherSignersSignaturesPreserved,
+ mCreatedBy,
+ mSignerEngine,
+ mInputApkFile,
+ mInputApkDataSource,
+ mOutputApkFile,
+ mOutputApkDataSink,
+ mOutputApkDataSource,
+ mSigningCertificateLineage);
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/ApkSignerEngine.java b/app/src/main/java/com/android/apksig/ApkSignerEngine.java
new file mode 100644
index 0000000..138bc38
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/ApkSignerEngine.java
@@ -0,0 +1,520 @@
+/*
+ * 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;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.RunnablesExecutor;
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.UnsupportedOperationException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * APK signing logic which is independent of how input and output APKs are stored, parsed, and
+ * generated.
+ *
+ *
Operating Model
+ *
+ * The abstract operating model is that there is an input APK which is being signed, thus producing
+ * an output APK. In reality, there may be just an output APK being built from scratch, or the input
+ * APK and the output APK may be the same file. Because this engine does not deal with reading and
+ * writing files, it can handle all of these scenarios.
+ *
+ * The engine is stateful and thus cannot be used for signing multiple APKs. However, once
+ * the engine signed an APK, the engine can be used to re-sign the APK after it has been modified.
+ * This may be more efficient than signing the APK using a new instance of the engine. See
+ * Incremental Operation.
+ *
+ *
In the engine's operating model, a signed APK is produced as follows.
+ *
+ * - JAR entries to be signed are output,
+ * - JAR archive is signed using JAR signing, thus adding the so-called v1 signature to the
+ * output,
+ * - JAR archive is signed using APK Signature Scheme v2, thus adding the so-called v2 signature
+ * to the output.
+ *
+ *
+ * The input APK may contain JAR entries which, depending on the engine's configuration, may or
+ * may not be output (e.g., existing signatures may need to be preserved or stripped) or which the
+ * engine will overwrite as part of signing. The engine thus offers {@link #inputJarEntry(String)}
+ * which tells the client whether the input JAR entry needs to be output. This avoids the need for
+ * the client to hard-code the aspects of APK signing which determine which parts of input must be
+ * ignored. Similarly, the engine offers {@link #inputApkSigningBlock(DataSource)} to help the
+ * client avoid dealing with preserving or stripping APK Signature Scheme v2 signature of the input
+ * APK.
+ *
+ *
To use the engine to sign an input APK (or a collection of JAR entries), follow these
+ * steps:
+ *
+ * - Obtain a new instance of the engine -- engine instances are stateful and thus cannot be used
+ * for signing multiple APKs.
+ * - Locate the input APK's APK Signing Block and provide it to
+ * {@link #inputApkSigningBlock(DataSource)}.
+ * - For each JAR entry in the input APK, invoke {@link #inputJarEntry(String)} to determine
+ * whether this entry should be output. The engine may request to inspect the entry.
+ * - For each output JAR entry, invoke {@link #outputJarEntry(String)} which may request to
+ * inspect the entry.
+ * - Once all JAR entries have been output, invoke {@link #outputJarEntries()} which may request
+ * that additional JAR entries are output. These entries comprise the output APK's JAR
+ * signature.
+ * - Locate the ZIP Central Directory and ZIP End of Central Directory sections in the output and
+ * invoke {@link #outputZipSections2(DataSource, DataSource, DataSource)} which may request that
+ * an APK Signature Block is inserted before the ZIP Central Directory. The block contains the
+ * output APK's APK Signature Scheme v2 signature.
+ * - Invoke {@link #outputDone()} to signal that the APK was output in full. The engine will
+ * confirm that the output APK is signed.
+ * - Invoke {@link #close()} to signal that the engine will no longer be used. This lets the
+ * engine free any resources it no longer needs.
+ *
+ *
+ * Some invocations of the engine may provide the client with a task to perform. The client is
+ * expected to perform all requested tasks before proceeding to the next stage of signing. See
+ * documentation of each method about the deadlines for performing the tasks requested by the
+ * method.
+ *
+ *
Incremental Operation
+ *
+ * The engine supports incremental operation where a signed APK is produced, then modified and
+ * re-signed. This may be useful for IDEs, where an app is frequently re-signed after small changes
+ * by the developer. Re-signing may be more efficient than signing from scratch.
+ *
+ * To use the engine in incremental mode, keep notifying the engine of changes to the APK through
+ * {@link #inputApkSigningBlock(DataSource)}, {@link #inputJarEntry(String)},
+ * {@link #inputJarEntryRemoved(String)}, {@link #outputJarEntry(String)},
+ * and {@link #outputJarEntryRemoved(String)}, perform the tasks requested by the engine through
+ * these methods, and, when a new signed APK is desired, run through steps 5 onwards to re-sign the
+ * APK.
+ *
+ *
Output-only Operation
+ *
+ * The engine's abstract operating model consists of an input APK and an output APK. However, it is
+ * possible to use the engine in output-only mode where the engine's {@code input...} methods are
+ * not invoked. In this mode, the engine has less control over output because it cannot request that
+ * some JAR entries are not output. Nevertheless, the engine will attempt to make the output APK
+ * signed and will report an error if cannot do so.
+ *
+ * @see Application Signing
+ */
+public interface ApkSignerEngine extends Closeable {
+
+ default void setExecutor(RunnablesExecutor executor) {
+ throw new UnsupportedOperationException("setExecutor method is not implemented");
+ }
+
+ /**
+ * Initializes the signer engine with the data already present in the apk (if any). There
+ * might already be data that can be reused if the entries has not been changed.
+ *
+ * @param manifestBytes
+ * @param entryNames
+ * @return set of entry names which were processed by the engine during the initialization, a
+ * subset of entryNames
+ */
+ default Set initWith(byte[] manifestBytes, Set entryNames) {
+ throw new UnsupportedOperationException("initWith method is not implemented");
+ }
+
+ /**
+ * Indicates to this engine that the input APK contains the provided APK Signing Block. The
+ * block may contain signatures of the input APK, such as APK Signature Scheme v2 signatures.
+ *
+ * @param apkSigningBlock APK signing block of the input APK. The provided data source is
+ * guaranteed to not be used by the engine after this method terminates.
+ *
+ * @throws IOException if an I/O error occurs while reading the APK Signing Block
+ * @throws ApkFormatException if the APK Signing Block is malformed
+ * @throws IllegalStateException if this engine is closed
+ */
+ void inputApkSigningBlock(DataSource apkSigningBlock)
+ throws IOException, ApkFormatException, IllegalStateException;
+
+ /**
+ * Indicates to this engine that the specified JAR entry was encountered in the input APK.
+ *
+ * When an input entry is updated/changed, it's OK to not invoke
+ * {@link #inputJarEntryRemoved(String)} before invoking this method.
+ *
+ * @return instructions about how to proceed with this entry
+ *
+ * @throws IllegalStateException if this engine is closed
+ */
+ InputJarEntryInstructions inputJarEntry(String entryName) throws IllegalStateException;
+
+ /**
+ * Indicates to this engine that the specified JAR entry was output.
+ *
+ *
It is unnecessary to invoke this method for entries added to output by this engine (e.g.,
+ * requested by {@link #outputJarEntries()}) provided the entries were output with exactly the
+ * data requested by the engine.
+ *
+ *
When an already output entry is updated/changed, it's OK to not invoke
+ * {@link #outputJarEntryRemoved(String)} before invoking this method.
+ *
+ * @return request to inspect the entry or {@code null} if the engine does not need to inspect
+ * the entry. The request must be fulfilled before {@link #outputJarEntries()} is
+ * invoked.
+ *
+ * @throws IllegalStateException if this engine is closed
+ */
+ InspectJarEntryRequest outputJarEntry(String entryName) throws IllegalStateException;
+
+ /**
+ * Indicates to this engine that the specified JAR entry was removed from the input. It's safe
+ * to invoke this for entries for which {@link #inputJarEntry(String)} hasn't been invoked.
+ *
+ * @return output policy of this JAR entry. The policy indicates how this input entry affects
+ * the output APK. The client of this engine should use this information to determine
+ * how the removal of this input APK's JAR entry affects the output APK.
+ *
+ * @throws IllegalStateException if this engine is closed
+ */
+ InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName)
+ throws IllegalStateException;
+
+ /**
+ * Indicates to this engine that the specified JAR entry was removed from the output. It's safe
+ * to invoke this for entries for which {@link #outputJarEntry(String)} hasn't been invoked.
+ *
+ * @throws IllegalStateException if this engine is closed
+ */
+ void outputJarEntryRemoved(String entryName) throws IllegalStateException;
+
+ /**
+ * Indicates to this engine that all JAR entries have been output.
+ *
+ * @return request to add JAR signature to the output or {@code null} if there is no need to add
+ * a JAR signature. The request will contain additional JAR entries to be output. The
+ * request must be fulfilled before
+ * {@link #outputZipSections2(DataSource, DataSource, DataSource)} is invoked.
+ *
+ * @throws ApkFormatException if the APK is malformed in a way which is preventing this engine
+ * from producing a valid signature. For example, if the engine uses the provided
+ * {@code META-INF/MANIFEST.MF} as a template and the file is malformed.
+ * @throws NoSuchAlgorithmException if a signature could not be generated because a required
+ * cryptographic algorithm implementation is missing
+ * @throws InvalidKeyException if a signature could not be generated because a signing key is
+ * not suitable for generating the signature
+ * @throws SignatureException if an error occurred while generating a signature
+ * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
+ * entries, or if the engine is closed
+ */
+ OutputJarSignatureRequest outputJarEntries()
+ throws ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
+ SignatureException, IllegalStateException;
+
+ /**
+ * Indicates to this engine that the ZIP sections comprising the output APK have been output.
+ *
+ *
The provided data sources are guaranteed to not be used by the engine after this method
+ * terminates.
+ *
+ * @deprecated This is now superseded by {@link #outputZipSections2(DataSource, DataSource,
+ * DataSource)}.
+ *
+ * @param zipEntries the section of ZIP archive containing Local File Header records and data of
+ * the ZIP entries. In a well-formed archive, this section starts at the start of the
+ * archive and extends all the way to the ZIP Central Directory.
+ * @param zipCentralDirectory ZIP Central Directory section
+ * @param zipEocd ZIP End of Central Directory (EoCD) record
+ *
+ * @return request to add an APK Signing Block to the output or {@code null} if the output must
+ * not contain an APK Signing Block. The request must be fulfilled before
+ * {@link #outputDone()} is invoked.
+ *
+ * @throws IOException if an I/O error occurs while reading the provided ZIP sections
+ * @throws ApkFormatException if the provided APK is malformed in a way which prevents this
+ * engine from producing a valid signature. For example, if the APK Signing Block
+ * provided to the engine is malformed.
+ * @throws NoSuchAlgorithmException if a signature could not be generated because a required
+ * cryptographic algorithm implementation is missing
+ * @throws InvalidKeyException if a signature could not be generated because a signing key is
+ * not suitable for generating the signature
+ * @throws SignatureException if an error occurred while generating a signature
+ * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
+ * entries or to output JAR signature, or if the engine is closed
+ */
+ @Deprecated
+ OutputApkSigningBlockRequest outputZipSections(
+ DataSource zipEntries,
+ DataSource zipCentralDirectory,
+ DataSource zipEocd)
+ throws IOException, ApkFormatException, NoSuchAlgorithmException,
+ InvalidKeyException, SignatureException, IllegalStateException;
+
+ /**
+ * Indicates to this engine that the ZIP sections comprising the output APK have been output.
+ *
+ *
The provided data sources are guaranteed to not be used by the engine after this method
+ * terminates.
+ *
+ * @param zipEntries the section of ZIP archive containing Local File Header records and data of
+ * the ZIP entries. In a well-formed archive, this section starts at the start of the
+ * archive and extends all the way to the ZIP Central Directory.
+ * @param zipCentralDirectory ZIP Central Directory section
+ * @param zipEocd ZIP End of Central Directory (EoCD) record
+ *
+ * @return request to add an APK Signing Block to the output or {@code null} if the output must
+ * not contain an APK Signing Block. The request must be fulfilled before
+ * {@link #outputDone()} is invoked.
+ *
+ * @throws IOException if an I/O error occurs while reading the provided ZIP sections
+ * @throws ApkFormatException if the provided APK is malformed in a way which prevents this
+ * engine from producing a valid signature. For example, if the APK Signing Block
+ * provided to the engine is malformed.
+ * @throws NoSuchAlgorithmException if a signature could not be generated because a required
+ * cryptographic algorithm implementation is missing
+ * @throws InvalidKeyException if a signature could not be generated because a signing key is
+ * not suitable for generating the signature
+ * @throws SignatureException if an error occurred while generating a signature
+ * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
+ * entries or to output JAR signature, or if the engine is closed
+ */
+ OutputApkSigningBlockRequest2 outputZipSections2(
+ DataSource zipEntries,
+ DataSource zipCentralDirectory,
+ DataSource zipEocd)
+ throws IOException, ApkFormatException, NoSuchAlgorithmException,
+ InvalidKeyException, SignatureException, IllegalStateException;
+
+ /**
+ * Indicates to this engine that the signed APK was output.
+ *
+ *
This does not change the output APK. The method helps the client confirm that the current
+ * output is signed.
+ *
+ * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
+ * entries or to output signatures, or if the engine is closed
+ */
+ void outputDone() throws IllegalStateException;
+
+ /**
+ * Indicates to this engine that it will no longer be used. Invoking this on an already closed
+ * engine is OK.
+ *
+ *
This does not change the output APK. For example, if the output APK is not yet fully
+ * signed, it will remain so after this method terminates.
+ */
+ @Override
+ void close();
+
+ /**
+ * Instructions about how to handle an input APK's JAR entry.
+ *
+ *
The instructions indicate whether to output the entry (see {@link #getOutputPolicy()}) and
+ * may contain a request to inspect the entry (see {@link #getInspectJarEntryRequest()}), in
+ * which case the request must be fulfilled before {@link ApkSignerEngine#outputJarEntries()} is
+ * invoked.
+ */
+ public static class InputJarEntryInstructions {
+ private final OutputPolicy mOutputPolicy;
+ private final InspectJarEntryRequest mInspectJarEntryRequest;
+
+ /**
+ * Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
+ * output policy and without a request to inspect the entry.
+ */
+ public InputJarEntryInstructions(OutputPolicy outputPolicy) {
+ this(outputPolicy, null);
+ }
+
+ /**
+ * Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
+ * output mode and with the provided request to inspect the entry.
+ *
+ * @param inspectJarEntryRequest request to inspect the entry or {@code null} if there's no
+ * need to inspect the entry.
+ */
+ public InputJarEntryInstructions(
+ OutputPolicy outputPolicy,
+ InspectJarEntryRequest inspectJarEntryRequest) {
+ mOutputPolicy = outputPolicy;
+ mInspectJarEntryRequest = inspectJarEntryRequest;
+ }
+
+ /**
+ * Returns the output policy for this entry.
+ */
+ public OutputPolicy getOutputPolicy() {
+ return mOutputPolicy;
+ }
+
+ /**
+ * Returns the request to inspect the JAR entry or {@code null} if there is no need to
+ * inspect the entry.
+ */
+ public InspectJarEntryRequest getInspectJarEntryRequest() {
+ return mInspectJarEntryRequest;
+ }
+
+ /**
+ * Output policy for an input APK's JAR entry.
+ */
+ public static enum OutputPolicy {
+ /** Entry must not be output. */
+ SKIP,
+
+ /** Entry should be output. */
+ OUTPUT,
+
+ /** Entry will be output by the engine. The client can thus ignore this input entry. */
+ OUTPUT_BY_ENGINE,
+ }
+ }
+
+ /**
+ * Request to inspect the specified JAR entry.
+ *
+ *
The entry's uncompressed data must be provided to the data sink returned by
+ * {@link #getDataSink()}. Once the entry's data has been provided to the sink, {@link #done()}
+ * must be invoked.
+ */
+ interface InspectJarEntryRequest {
+
+ /**
+ * Returns the data sink into which the entry's uncompressed data should be sent.
+ */
+ DataSink getDataSink();
+
+ /**
+ * Indicates that entry's data has been provided in full.
+ */
+ void done();
+
+ /**
+ * Returns the name of the JAR entry.
+ */
+ String getEntryName();
+ }
+
+ /**
+ * Request to add JAR signature (aka v1 signature) to the output APK.
+ *
+ *
Entries listed in {@link #getAdditionalJarEntries()} must be added to the output APK after
+ * which {@link #done()} must be invoked.
+ */
+ interface OutputJarSignatureRequest {
+
+ /**
+ * Returns JAR entries that must be added to the output APK.
+ */
+ List getAdditionalJarEntries();
+
+ /**
+ * Indicates that the JAR entries contained in this request were added to the output APK.
+ */
+ void done();
+
+ /**
+ * JAR entry.
+ */
+ public static class JarEntry {
+ private final String mName;
+ private final byte[] mData;
+
+ /**
+ * Constructs a new {@code JarEntry} with the provided name and data.
+ *
+ * @param data uncompressed data of the entry. Changes to this array will not be
+ * reflected in {@link #getData()}.
+ */
+ public JarEntry(String name, byte[] data) {
+ mName = name;
+ mData = data.clone();
+ }
+
+ /**
+ * Returns the name of this ZIP entry.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the uncompressed data of this JAR entry.
+ */
+ public byte[] getData() {
+ return mData.clone();
+ }
+ }
+ }
+
+ /**
+ * Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
+ * signature(s) of the APK are contained in this block.
+ *
+ * The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
+ * output APK such that the block is immediately before the ZIP Central Directory, the offset of
+ * ZIP Central Directory in the ZIP End of Central Directory record must be adjusted
+ * accordingly, and then {@link #done()} must be invoked.
+ *
+ *
If the output contains an APK Signing Block, that block must be replaced by the block
+ * contained in this request.
+ *
+ * @deprecated This is now superseded by {@link OutputApkSigningBlockRequest2}.
+ */
+ @Deprecated
+ interface OutputApkSigningBlockRequest {
+
+ /**
+ * Returns the APK Signing Block.
+ */
+ byte[] getApkSigningBlock();
+
+ /**
+ * Indicates that the APK Signing Block was output as requested.
+ */
+ void done();
+ }
+
+ /**
+ * Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
+ * signature(s) of the APK are contained in this block.
+ *
+ *
The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
+ * output APK such that the block is immediately before the ZIP Central Directory. Immediately
+ * before the APK Signing Block must be padding consists of the number of 0x00 bytes returned by
+ * {@link #getPaddingSizeBeforeApkSigningBlock()}. The offset of ZIP Central Directory in the
+ * ZIP End of Central Directory record must be adjusted accordingly, and then {@link #done()}
+ * must be invoked.
+ *
+ *
If the output contains an APK Signing Block, that block must be replaced by the block
+ * contained in this request.
+ */
+ interface OutputApkSigningBlockRequest2 {
+ /**
+ * Returns the APK Signing Block.
+ */
+ byte[] getApkSigningBlock();
+
+ /**
+ * Indicates that the APK Signing Block was output as requested.
+ */
+ void done();
+
+ /**
+ * Returns the number of 0x00 bytes the caller must place immediately before APK Signing
+ * Block.
+ */
+ int getPaddingSizeBeforeApkSigningBlock();
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/ApkVerifier.java b/app/src/main/java/com/android/apksig/ApkVerifier.java
new file mode 100644
index 0000000..3e1e7da
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/ApkVerifier.java
@@ -0,0 +1,1909 @@
+/*
+ * 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;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.AndroidBinXmlParser;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
+import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apksig.util.RunnablesExecutor;
+import com.android.apksig.zip.ZipFormatException;
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * APK signature verifier which mimics the behavior of the Android platform.
+ *
+ *
The verifier is designed to closely mimic the behavior of Android platforms. This is to enable
+ * the verifier to be used for checking whether an APK's signatures are expected to verify on
+ * Android.
+ *
+ *
Use {@link Builder} to obtain instances of this verifier.
+ *
+ * @see Application Signing
+ */
+public class ApkVerifier {
+
+ private static final Map SUPPORTED_APK_SIG_SCHEME_NAMES =
+ loadSupportedApkSigSchemeNames();
+
+ private static Map loadSupportedApkSigSchemeNames() {
+ Map supportedMap = new HashMap<>(2);
+ supportedMap.put(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, "APK Signature Scheme v2");
+ supportedMap.put(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3, "APK Signature Scheme v3");
+ return supportedMap;
+ }
+
+ private final File mApkFile;
+ private final DataSource mApkDataSource;
+
+ private final Integer mMinSdkVersion;
+ private final int mMaxSdkVersion;
+
+ private ApkVerifier(
+ File apkFile,
+ DataSource apkDataSource,
+ Integer minSdkVersion,
+ int maxSdkVersion) {
+ mApkFile = apkFile;
+ mApkDataSource = apkDataSource;
+ mMinSdkVersion = minSdkVersion;
+ mMaxSdkVersion = maxSdkVersion;
+ }
+
+ /**
+ * Verifies the APK's signatures and returns the result of verification. The APK can be
+ * considered verified iff the result's {@link Result#isVerified()} returns {@code true}.
+ * The verification result also includes errors, warnings, and information about signers such
+ * as their signing certificates.
+ *
+ * Verification succeeds iff the APK's signature is expected to verify on all Android
+ * platform versions specified via the {@link Builder}. If the APK's signature is expected to
+ * not verify on any of the specified platform versions, this method returns a result with one
+ * or more errors and whose {@link Result#isVerified()} returns {@code false}, or this method
+ * throws an exception.
+ *
+ * @throws IOException if an I/O error is encountered while reading the APK
+ * @throws ApkFormatException if the APK is malformed
+ * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+ * required cryptographic algorithm implementation is missing
+ * @throws IllegalStateException if this verifier's configuration is missing required
+ * information.
+ */
+ public Result verify() throws IOException, ApkFormatException, NoSuchAlgorithmException,
+ IllegalStateException {
+ Closeable in = null;
+ try {
+ DataSource apk;
+ if (mApkDataSource != null) {
+ apk = mApkDataSource;
+ } else if (mApkFile != null) {
+ RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
+ in = f;
+ apk = DataSources.asDataSource(f, 0, f.length());
+ } else {
+ throw new IllegalStateException("APK not provided");
+ }
+ return verify(apk);
+ } finally {
+ if (in != null) {
+ in.close();
+ }
+ }
+ }
+
+ /**
+ * Verifies the APK's signatures and returns the result of verification. The APK can be
+ * considered verified iff the result's {@link Result#isVerified()} returns {@code true}.
+ * The verification result also includes errors, warnings, and information about signers.
+ *
+ * @param apk APK file contents
+ *
+ * @throws IOException if an I/O error is encountered while reading the APK
+ * @throws ApkFormatException if the APK is malformed
+ * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+ * required cryptographic algorithm implementation is missing
+ */
+ private Result verify(DataSource apk)
+ throws IOException, ApkFormatException, NoSuchAlgorithmException {
+ if (mMinSdkVersion != null) {
+ if (mMinSdkVersion < 0) {
+ throw new IllegalArgumentException(
+ "minSdkVersion must not be negative: " + mMinSdkVersion);
+ }
+ if ((mMinSdkVersion != null) && (mMinSdkVersion > mMaxSdkVersion)) {
+ throw new IllegalArgumentException(
+ "minSdkVersion (" + mMinSdkVersion + ") > maxSdkVersion (" + mMaxSdkVersion
+ + ")");
+ }
+ }
+ int maxSdkVersion = mMaxSdkVersion;
+
+ ApkUtils.ZipSections zipSections;
+ try {
+ zipSections = ApkUtils.findZipSections(apk);
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Malformed APK: not a ZIP archive", e);
+ }
+
+ ByteBuffer androidManifest = null;
+
+ int minSdkVersion;
+ if (mMinSdkVersion != null) {
+ // No need to obtain minSdkVersion from the APK's AndroidManifest.xml
+ minSdkVersion = mMinSdkVersion;
+ } else {
+ // Need to obtain minSdkVersion from the APK's AndroidManifest.xml
+ if (androidManifest == null) {
+ androidManifest = getAndroidManifestFromApk(apk, zipSections);
+ }
+ minSdkVersion =
+ ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest.slice());
+ if (minSdkVersion > mMaxSdkVersion) {
+ throw new IllegalArgumentException(
+ "minSdkVersion from APK (" + minSdkVersion + ") > maxSdkVersion ("
+ + mMaxSdkVersion + ")");
+ }
+ }
+
+ Result result = new Result();
+
+ // The SUPPORTED_APK_SIG_SCHEME_NAMES contains the mapping from version number to scheme
+ // name, but the verifiers use this parameter as the schemes supported by the target SDK
+ // range. Since the code below skips signature verification based on max SDK the mapping of
+ // supported schemes needs to be modified to ensure the verifiers do not report a stripped
+ // signature for an SDK range that does not support that signature version. For instance an
+ // APK with V1, V2, and V3 signatures and a max SDK of O would skip the V3 signature
+ // verification, but the SUPPORTED_APK_SIG_SCHEME_NAMES contains version 3, so when the V2
+ // verification is performed it would see the stripping protection attribute, see that V3
+ // is in the list of supported signatures, and report a stripped signature.
+ Map supportedSchemeNames;
+ if (maxSdkVersion >= AndroidSdkVersion.P) {
+ supportedSchemeNames = SUPPORTED_APK_SIG_SCHEME_NAMES;
+ } else if (maxSdkVersion >= AndroidSdkVersion.N) {
+ supportedSchemeNames = new HashMap<>(1);
+ supportedSchemeNames.put(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2,
+ SUPPORTED_APK_SIG_SCHEME_NAMES.get(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2));
+ } else {
+ supportedSchemeNames = Collections.EMPTY_MAP;
+ }
+ // Android N and newer attempts to verify APKs using the APK Signing Block, which can
+ // include v2 and/or v3 signatures. If none is found, it falls back to JAR signature
+ // verification. If the signature is found but does not verify, the APK is rejected.
+ Set foundApkSigSchemeIds = new HashSet<>(2);
+ if (maxSdkVersion >= AndroidSdkVersion.N) {
+ RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED;
+ // Android P and newer attempts to verify APKs using APK Signature Scheme v3
+ if (maxSdkVersion >= AndroidSdkVersion.P) {
+ try {
+ ApkSigningBlockUtils.Result v3Result =
+ V3SchemeVerifier.verify(
+ executor,
+ apk,
+ zipSections,
+ Math.max(minSdkVersion, AndroidSdkVersion.P),
+ maxSdkVersion);
+ foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+ result.mergeFrom(v3Result);
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ // v3 signature not required
+ }
+ if (result.containsErrors()) {
+ return result;
+ }
+ }
+
+ // Attempt to verify the APK using v2 signing if necessary. Platforms prior to Android P
+ // ignore APK Signature Scheme v3 signatures and always attempt to verify either JAR or
+ // APK Signature Scheme v2 signatures. Android P onwards verifies v2 signatures only if
+ // no APK Signature Scheme v3 (or newer scheme) signatures were found.
+ if (minSdkVersion < AndroidSdkVersion.P || foundApkSigSchemeIds.isEmpty()) {
+ try {
+ ApkSigningBlockUtils.Result v2Result =
+ V2SchemeVerifier.verify(
+ executor,
+ apk,
+ zipSections,
+ supportedSchemeNames,
+ foundApkSigSchemeIds,
+ Math.max(minSdkVersion, AndroidSdkVersion.N),
+ maxSdkVersion);
+ foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
+ result.mergeFrom(v2Result);
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ // v2 signature not required
+ }
+ if (result.containsErrors()) {
+ return result;
+ }
+ }
+ }
+
+ // Android O and newer requires that APKs targeting security sandbox version 2 and higher
+ // are signed using APK Signature Scheme v2 or newer.
+ if (maxSdkVersion >= AndroidSdkVersion.O) {
+ if (androidManifest == null) {
+ androidManifest = getAndroidManifestFromApk(apk, zipSections);
+ }
+ int targetSandboxVersion =
+ getTargetSandboxVersionFromBinaryAndroidManifest(androidManifest.slice());
+ if (targetSandboxVersion > 1) {
+ if (foundApkSigSchemeIds.isEmpty()) {
+ result.addError(
+ Issue.NO_SIG_FOR_TARGET_SANDBOX_VERSION,
+ targetSandboxVersion);
+ }
+ }
+ }
+
+ // Attempt to verify the APK using JAR signing if necessary. Platforms prior to Android N
+ // ignore APK Signature Scheme v2 signatures and always attempt to verify JAR signatures.
+ // Android N onwards verifies JAR signatures only if no APK Signature Scheme v2 (or newer
+ // scheme) signatures were found.
+ if ((minSdkVersion < AndroidSdkVersion.N) || (foundApkSigSchemeIds.isEmpty())) {
+ V1SchemeVerifier.Result v1Result =
+ V1SchemeVerifier.verify(
+ apk,
+ zipSections,
+ supportedSchemeNames,
+ foundApkSigSchemeIds,
+ minSdkVersion,
+ maxSdkVersion);
+ result.mergeFrom(v1Result);
+ }
+ if (result.containsErrors()) {
+ return result;
+ }
+
+ // Check whether v1 and v2 scheme signer identifies match, provided both v1 and v2
+ // signatures verified.
+ if ((result.isVerifiedUsingV1Scheme()) && (result.isVerifiedUsingV2Scheme())) {
+ ArrayList v1Signers =
+ new ArrayList<>(result.getV1SchemeSigners());
+ ArrayList v2Signers =
+ new ArrayList<>(result.getV2SchemeSigners());
+ ArrayList v1SignerCerts = new ArrayList<>();
+ ArrayList v2SignerCerts = new ArrayList<>();
+ for (Result.V1SchemeSignerInfo signer : v1Signers) {
+ try {
+ v1SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded()));
+ } catch (CertificateEncodingException e) {
+ throw new RuntimeException(
+ "Failed to encode JAR signer " + signer.getName() + " certs", e);
+ }
+ }
+ for (Result.V2SchemeSignerInfo signer : v2Signers) {
+ try {
+ v2SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded()));
+ } catch (CertificateEncodingException e) {
+ throw new RuntimeException(
+ "Failed to encode APK Signature Scheme v2 signer (index: "
+ + signer.getIndex() + ") certs",
+ e);
+ }
+ }
+
+ for (int i = 0; i < v1SignerCerts.size(); i++) {
+ ByteArray v1Cert = v1SignerCerts.get(i);
+ if (!v2SignerCerts.contains(v1Cert)) {
+ Result.V1SchemeSignerInfo v1Signer = v1Signers.get(i);
+ v1Signer.addError(Issue.V2_SIG_MISSING);
+ break;
+ }
+ }
+ for (int i = 0; i < v2SignerCerts.size(); i++) {
+ ByteArray v2Cert = v2SignerCerts.get(i);
+ if (!v1SignerCerts.contains(v2Cert)) {
+ Result.V2SchemeSignerInfo v2Signer = v2Signers.get(i);
+ v2Signer.addError(Issue.JAR_SIG_MISSING);
+ break;
+ }
+ }
+ }
+
+ // If there is a v3 scheme signer and an earlier scheme signer, make sure that there is a
+ // match, or in the event of signing certificate rotation, that the v1/v2 scheme signer
+ // matches the oldest signing certificate in the provided SigningCertificateLineage
+ if (result.isVerifiedUsingV3Scheme()
+ && (result.isVerifiedUsingV1Scheme() || result.isVerifiedUsingV2Scheme())) {
+ SigningCertificateLineage lineage = result.getSigningCertificateLineage();
+ X509Certificate oldSignerCert;
+ if (result.isVerifiedUsingV1Scheme()) {
+ List v1Signers = result.getV1SchemeSigners();
+ if (v1Signers.size() != 1) {
+ // APK Signature Scheme v3 only supports single-signers, error to sign with
+ // multiple and then only one
+ result.addError(Issue.V3_SIG_MULTIPLE_PAST_SIGNERS);
+ }
+ oldSignerCert = v1Signers.get(0).mCertChain.get(0);
+ } else {
+ List v2Signers = result.getV2SchemeSigners();
+ if (v2Signers.size() != 1) {
+ // APK Signature Scheme v3 only supports single-signers, error to sign with
+ // multiple and then only one
+ result.addError(Issue.V3_SIG_MULTIPLE_PAST_SIGNERS);
+ }
+ oldSignerCert = v2Signers.get(0).mCerts.get(0);
+ }
+ if (lineage == null) {
+ // no signing certificate history with which to contend, just make sure that v3
+ // matches previous versions
+ List v3Signers = result.getV3SchemeSigners();
+ if (v3Signers.size() != 1) {
+ // multiple v3 signers should never exist without rotation history, since
+ // multiple signers implies a different signer for different platform versions
+ result.addError(Issue.V3_SIG_MULTIPLE_SIGNERS);
+ }
+ try {
+ if (!Arrays.equals(oldSignerCert.getEncoded(),
+ v3Signers.get(0).mCerts.get(0).getEncoded())) {
+ result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH);
+ }
+ } catch (CertificateEncodingException e) {
+ // we just go the encoding for the v1/v2 certs above, so must be v3
+ throw new RuntimeException(
+ "Failed to encode APK Signature Scheme v3 signer cert", e);
+ }
+ } else {
+ // we have some signing history, make sure that the root of the history is the same
+ // as our v1/v2 signer
+ try {
+ lineage = lineage.getSubLineage(oldSignerCert);
+ if (lineage.size() != 1) {
+ // the v1/v2 signer was found, but not at the root of the lineage
+ result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH);
+ }
+ } catch (IllegalArgumentException e) {
+ // the v1/v2 signer was not found in the lineage
+ result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH);
+ }
+ }
+ }
+
+ if (result.containsErrors()) {
+ return result;
+ }
+
+ // Verified
+ result.setVerified();
+ if (result.isVerifiedUsingV3Scheme()) {
+ List v3Signers = result.getV3SchemeSigners();
+ result.addSignerCertificate(v3Signers.get(v3Signers.size() - 1).getCertificate());
+ } else if (result.isVerifiedUsingV2Scheme()) {
+ for (Result.V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) {
+ result.addSignerCertificate(signerInfo.getCertificate());
+ }
+ } else if (result.isVerifiedUsingV1Scheme()) {
+ for (Result.V1SchemeSignerInfo signerInfo : result.getV1SchemeSigners()) {
+ result.addSignerCertificate(signerInfo.getCertificate());
+ }
+ } else {
+ throw new RuntimeException(
+ "APK verified, but has not verified using any of v1, v2 or v3schemes");
+ }
+
+ return result;
+ }
+
+ private static ByteBuffer getAndroidManifestFromApk(
+ DataSource apk, ApkUtils.ZipSections zipSections)
+ throws IOException, ApkFormatException {
+ List cdRecords =
+ V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
+ try {
+ return ApkSigner.getAndroidManifestFromApk(
+ cdRecords,
+ apk.slice(0, zipSections.getZipCentralDirectoryOffset()));
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Failed to read AndroidManifest.xml", e);
+ }
+ }
+
+ /**
+ * Android resource ID of the {@code android:targetSandboxVersion} attribute in
+ * AndroidManifest.xml.
+ */
+ private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c;
+
+ /**
+ * Returns the security sandbox version targeted by an APK with the provided
+ * {@code AndroidManifest.xml}.
+ *
+ * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+ * resource format
+ *
+ * @throws ApkFormatException if an error occurred while determining the version
+ */
+ private static int getTargetSandboxVersionFromBinaryAndroidManifest(
+ ByteBuffer androidManifestContents) throws ApkFormatException {
+ // Return the value of the android:targetSandboxVersion attribute of the top-level manifest
+ // element
+ 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())) {
+ // In each manifest element, targetSandboxVersion defaults to 1
+ int result = 1;
+ for (int i = 0; i < parser.getAttributeCount(); i++) {
+ if (parser.getAttributeNameResourceId(i)
+ == TARGET_SANDBOX_VERSION_ATTR_ID) {
+ int valueType = parser.getAttributeValueType(i);
+ switch (valueType) {
+ case AndroidBinXmlParser.VALUE_TYPE_INT:
+ result = parser.getAttributeIntValue(i);
+ break;
+ default:
+ throw new ApkFormatException(
+ "Failed to determine APK's target sandbox version"
+ + ": unsupported value type of"
+ + " AndroidManifest.xml"
+ + " android:targetSandboxVersion"
+ + ". Only integer values supported.");
+ }
+ break;
+ }
+ }
+ return result;
+ }
+ eventType = parser.next();
+ }
+ throw new ApkFormatException(
+ "Failed to determine APK's target sandbox version"
+ + " : no manifest element in AndroidManifest.xml");
+ } catch (AndroidBinXmlParser.XmlParserException e) {
+ throw new ApkFormatException(
+ "Failed to determine APK's target sandbox version"
+ + ": malformed AndroidManifest.xml",
+ e);
+ }
+ }
+
+ /**
+ * Result of verifying an APKs signatures. The APK can be considered verified iff
+ * {@link #isVerified()} returns {@code true}.
+ */
+ public static class Result {
+ private final List mErrors = new ArrayList<>();
+ private final List mWarnings = new ArrayList<>();
+ private final List mSignerCerts = new ArrayList<>();
+ private final List mV1SchemeSigners = new ArrayList<>();
+ private final List mV1SchemeIgnoredSigners = new ArrayList<>();
+ private final List mV2SchemeSigners = new ArrayList<>();
+ private final List mV3SchemeSigners = new ArrayList<>();
+
+ private boolean mVerified;
+ private boolean mVerifiedUsingV1Scheme;
+ private boolean mVerifiedUsingV2Scheme;
+ private boolean mVerifiedUsingV3Scheme;
+ private SigningCertificateLineage mSigningCertificateLineage;
+
+ /**
+ * Returns {@code true} if the APK's signatures verified.
+ */
+ public boolean isVerified() {
+ return mVerified;
+ }
+
+ private void setVerified() {
+ mVerified = true;
+ }
+
+ /**
+ * Returns {@code true} if the APK's JAR signatures verified.
+ */
+ public boolean isVerifiedUsingV1Scheme() {
+ return mVerifiedUsingV1Scheme;
+ }
+
+ /**
+ * Returns {@code true} if the APK's APK Signature Scheme v2 signatures verified.
+ */
+ public boolean isVerifiedUsingV2Scheme() {
+ return mVerifiedUsingV2Scheme;
+ }
+
+ /**
+ * Returns {@code true} if the APK's APK Signature Scheme v3 signature verified.
+ */
+ public boolean isVerifiedUsingV3Scheme() {
+ return mVerifiedUsingV3Scheme;
+ }
+
+ /**
+ * Returns the verified signers' certificates, one per signer.
+ */
+ public List getSignerCertificates() {
+ return mSignerCerts;
+ }
+
+ private void addSignerCertificate(X509Certificate cert) {
+ mSignerCerts.add(cert);
+ }
+
+ /**
+ * Returns information about JAR signers associated with the APK's signature. These are the
+ * signers used by Android.
+ *
+ * @see #getV1SchemeIgnoredSigners()
+ */
+ public List getV1SchemeSigners() {
+ return mV1SchemeSigners;
+ }
+
+ /**
+ * Returns information about JAR signers ignored by the APK's signature verification
+ * process. These signers are ignored by Android. However, each signer's errors or warnings
+ * will contain information about why they are ignored.
+ *
+ * @see #getV1SchemeSigners()
+ */
+ public List getV1SchemeIgnoredSigners() {
+ return mV1SchemeIgnoredSigners;
+ }
+
+ /**
+ * Returns information about APK Signature Scheme v2 signers associated with the APK's
+ * signature.
+ */
+ public List getV2SchemeSigners() {
+ return mV2SchemeSigners;
+ }
+
+ /**
+ * Returns information about APK Signature Scheme v3 signers associated with the APK's
+ * signature.
+ *
+ * Multiple signers represent different targeted platform versions, not
+ * a signing identity of multiple signers. APK Signature Scheme v3 only supports single
+ * signer identities.
+ */
+ public List getV3SchemeSigners() {
+ return mV3SchemeSigners;
+ }
+
+ /**
+ * Returns the combined SigningCertificateLineage associated with this APK's APK Signature
+ * Scheme v3 signing block.
+ */
+ public SigningCertificateLineage getSigningCertificateLineage() {
+ return mSigningCertificateLineage;
+ }
+
+ void addError(Issue msg, Object... parameters) {
+ mErrors.add(new IssueWithParams(msg, parameters));
+ }
+
+ /**
+ * Returns errors encountered while verifying the APK's signatures.
+ */
+ public List getErrors() {
+ return mErrors;
+ }
+
+ /**
+ * Returns warnings encountered while verifying the APK's signatures.
+ */
+ public List getWarnings() {
+ return mWarnings;
+ }
+
+ private void mergeFrom(V1SchemeVerifier.Result source) {
+ mVerifiedUsingV1Scheme = source.verified;
+ mErrors.addAll(source.getErrors());
+ mWarnings.addAll(source.getWarnings());
+ for (V1SchemeVerifier.Result.SignerInfo signer : source.signers) {
+ mV1SchemeSigners.add(new V1SchemeSignerInfo(signer));
+ }
+ for (V1SchemeVerifier.Result.SignerInfo signer : source.ignoredSigners) {
+ mV1SchemeIgnoredSigners.add(new V1SchemeSignerInfo(signer));
+ }
+ }
+
+ private void mergeFrom(ApkSigningBlockUtils.Result source) {
+ switch (source.signatureSchemeVersion) {
+ case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2:
+ mVerifiedUsingV2Scheme = source.verified;
+ for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+ mV2SchemeSigners.add(new V2SchemeSignerInfo(signer));
+ }
+ break;
+ case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3:
+ mVerifiedUsingV3Scheme = source.verified;
+ for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+ mV3SchemeSigners.add(new V3SchemeSignerInfo(signer));
+ }
+ mSigningCertificateLineage = source.signingCertificateLineage;
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown Signing Block Scheme Id");
+ }
+ mErrors.addAll(source.getErrors());
+ mWarnings.addAll(source.getWarnings());
+ }
+
+ /**
+ * Returns {@code true} if an error was encountered while verifying the APK. Any error
+ * prevents the APK from being considered verified.
+ */
+ public boolean containsErrors() {
+ if (!mErrors.isEmpty()) {
+ return true;
+ }
+ if (!mV1SchemeSigners.isEmpty()) {
+ for (V1SchemeSignerInfo signer : mV1SchemeSigners) {
+ if (signer.containsErrors()) {
+ return true;
+ }
+ }
+ }
+ if (!mV2SchemeSigners.isEmpty()) {
+ for (V2SchemeSignerInfo signer : mV2SchemeSigners) {
+ if (signer.containsErrors()) {
+ return true;
+ }
+ }
+ }
+ if (!mV3SchemeSigners.isEmpty()) {
+ for (V3SchemeSignerInfo signer : mV3SchemeSigners) {
+ if (signer.containsErrors()) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Information about a JAR signer associated with the APK's signature.
+ */
+ public static class V1SchemeSignerInfo {
+ private final String mName;
+ private final List mCertChain;
+ private final String mSignatureBlockFileName;
+ private final String mSignatureFileName;
+
+ private final List mErrors;
+ private final List mWarnings;
+
+ private V1SchemeSignerInfo(V1SchemeVerifier.Result.SignerInfo result) {
+ mName = result.name;
+ mCertChain = result.certChain;
+ mSignatureBlockFileName = result.signatureBlockFileName;
+ mSignatureFileName = result.signatureFileName;
+ mErrors = result.getErrors();
+ mWarnings = result.getWarnings();
+ }
+
+ /**
+ * Returns a user-friendly name of the signer.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the name of the JAR entry containing this signer's JAR signature block file.
+ */
+ public String getSignatureBlockFileName() {
+ return mSignatureBlockFileName;
+ }
+
+ /**
+ * Returns the name of the JAR entry containing this signer's JAR signature file.
+ */
+ public String getSignatureFileName() {
+ return mSignatureFileName;
+ }
+
+ /**
+ * Returns this signer's signing certificate or {@code null} if not available. The
+ * certificate is guaranteed to be available if no errors were encountered during
+ * verification (see {@link #containsErrors()}.
+ *
+ * This certificate contains the signer's public key.
+ */
+ public X509Certificate getCertificate() {
+ return mCertChain.isEmpty() ? null : mCertChain.get(0);
+ }
+
+ /**
+ * Returns the certificate chain for the signer's public key. The certificate containing
+ * the public key is first, followed by the certificate (if any) which issued the
+ * signing certificate, and so forth. An empty list may be returned if an error was
+ * encountered during verification (see {@link #containsErrors()}).
+ */
+ public List getCertificateChain() {
+ return mCertChain;
+ }
+
+ /**
+ * Returns {@code true} if an error was encountered while verifying this signer's JAR
+ * signature. Any error prevents the signer's signature from being considered verified.
+ */
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+
+ /**
+ * Returns errors encountered while verifying this signer's JAR signature. Any error
+ * prevents the signer's signature from being considered verified.
+ */
+ public List getErrors() {
+ return mErrors;
+ }
+
+ /**
+ * Returns warnings encountered while verifying this signer's JAR signature. Warnings
+ * do not prevent the signer's signature from being considered verified.
+ */
+ public List getWarnings() {
+ return mWarnings;
+ }
+
+ private void addError(Issue msg, Object... parameters) {
+ mErrors.add(new IssueWithParams(msg, parameters));
+ }
+ }
+
+ /**
+ * Information about an APK Signature Scheme v2 signer associated with the APK's signature.
+ */
+ public static class V2SchemeSignerInfo {
+ private final int mIndex;
+ private final List mCerts;
+
+ private final List mErrors;
+ private final List mWarnings;
+
+ private V2SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+ mIndex = result.index;
+ mCerts = result.certs;
+ mErrors = result.getErrors();
+ mWarnings = result.getWarnings();
+ }
+
+ /**
+ * Returns this signer's {@code 0}-based index in the list of signers contained in the
+ * APK's APK Signature Scheme v2 signature.
+ */
+ public int getIndex() {
+ return mIndex;
+ }
+
+ /**
+ * Returns this signer's signing certificate or {@code null} if not available. The
+ * certificate is guaranteed to be available if no errors were encountered during
+ * verification (see {@link #containsErrors()}.
+ *
+ * This certificate contains the signer's public key.
+ */
+ public X509Certificate getCertificate() {
+ return mCerts.isEmpty() ? null : mCerts.get(0);
+ }
+
+ /**
+ * Returns this signer's certificates. The first certificate is for the signer's public
+ * key. An empty list may be returned if an error was encountered during verification
+ * (see {@link #containsErrors()}).
+ */
+ public List getCertificates() {
+ return mCerts;
+ }
+
+ private void addError(Issue msg, Object... parameters) {
+ mErrors.add(new IssueWithParams(msg, parameters));
+ }
+
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+
+ public List getErrors() {
+ return mErrors;
+ }
+
+ public List getWarnings() {
+ return mWarnings;
+ }
+ }
+
+ /**
+ * Information about an APK Signature Scheme v3 signer associated with the APK's signature.
+ */
+ public static class V3SchemeSignerInfo {
+ private final int mIndex;
+ private final List mCerts;
+
+ private final List mErrors;
+ private final List mWarnings;
+
+ private V3SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+ mIndex = result.index;
+ mCerts = result.certs;
+ mErrors = result.getErrors();
+ mWarnings = result.getWarnings();
+ }
+
+ /**
+ * Returns this signer's {@code 0}-based index in the list of signers contained in the
+ * APK's APK Signature Scheme v3 signature.
+ */
+ public int getIndex() {
+ return mIndex;
+ }
+
+ /**
+ * Returns this signer's signing certificate or {@code null} if not available. The
+ * certificate is guaranteed to be available if no errors were encountered during
+ * verification (see {@link #containsErrors()}.
+ *
+ * This certificate contains the signer's public key.
+ */
+ public X509Certificate getCertificate() {
+ return mCerts.isEmpty() ? null : mCerts.get(0);
+ }
+
+ /**
+ * Returns this signer's certificates. The first certificate is for the signer's public
+ * key. An empty list may be returned if an error was encountered during verification
+ * (see {@link #containsErrors()}).
+ */
+ public List getCertificates() {
+ return mCerts;
+ }
+
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+
+ public List getErrors() {
+ return mErrors;
+ }
+
+ public List getWarnings() {
+ return mWarnings;
+ }
+ }
+ }
+
+ /**
+ * Error or warning encountered while verifying an APK's signatures.
+ */
+ public static enum Issue {
+
+ /**
+ * APK is not JAR-signed.
+ */
+ JAR_SIG_NO_SIGNATURES("No JAR signatures"),
+
+ /**
+ * APK does not contain any entries covered by JAR signatures.
+ */
+ JAR_SIG_NO_SIGNED_ZIP_ENTRIES("No JAR entries covered by JAR signatures"),
+
+ /**
+ * APK contains multiple entries with the same name.
+ *
+ *
+ * - Parameter 1: name ({@code String})
+ *
+ */
+ JAR_SIG_DUPLICATE_ZIP_ENTRY("Duplicate entry: %1$s"),
+
+ /**
+ * JAR manifest contains a section with a duplicate name.
+ *
+ *
+ * - Parameter 1: section name ({@code String})
+ *
+ */
+ JAR_SIG_DUPLICATE_MANIFEST_SECTION("Duplicate section in META-INF/MANIFEST.MF: %1$s"),
+
+ /**
+ * JAR manifest contains a section without a name.
+ *
+ *
+ * - Parameter 1: section index (1-based) ({@code Integer})
+ *
+ */
+ JAR_SIG_UNNNAMED_MANIFEST_SECTION(
+ "Malformed META-INF/MANIFEST.MF: invidual section #%1$d does not have a name"),
+
+ /**
+ * JAR signature file contains a section without a name.
+ *
+ *
+ * - Parameter 1: signature file name ({@code String})
+ * - Parameter 2: section index (1-based) ({@code Integer})
+ *
+ */
+ JAR_SIG_UNNNAMED_SIG_FILE_SECTION(
+ "Malformed %1$s: invidual section #%2$d does not have a name"),
+
+ /** APK is missing the JAR manifest entry (META-INF/MANIFEST.MF). */
+ JAR_SIG_NO_MANIFEST("Missing META-INF/MANIFEST.MF"),
+
+ /**
+ * JAR manifest references an entry which is not there in the APK.
+ *
+ *
+ * - Parameter 1: entry name ({@code String})
+ *
+ */
+ JAR_SIG_MISSING_ZIP_ENTRY_REFERENCED_IN_MANIFEST(
+ "%1$s entry referenced by META-INF/MANIFEST.MF not found in the APK"),
+
+ /**
+ * JAR manifest does not list a digest for the specified entry.
+ *
+ *
+ * - Parameter 1: entry name ({@code String})
+ *
+ */
+ JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST("No digest for %1$s in META-INF/MANIFEST.MF"),
+
+ /**
+ * JAR signature does not list a digest for the specified entry.
+ *
+ *
+ * - Parameter 1: entry name ({@code String})
+ * - Parameter 2: signature file name ({@code String})
+ *
+ */
+ JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE("No digest for %1$s in %2$s"),
+
+ /**
+ * The specified JAR entry is not covered by JAR signature.
+ *
+ *
+ * - Parameter 1: entry name ({@code String})
+ *
+ */
+ JAR_SIG_ZIP_ENTRY_NOT_SIGNED("%1$s entry not signed"),
+
+ /**
+ * JAR signature uses different set of signers to protect the two specified ZIP entries.
+ *
+ *
+ * - Parameter 1: first entry name ({@code String})
+ * - Parameter 2: first entry signer names ({@code List})
+ * - Parameter 3: second entry name ({@code String})
+ * - Parameter 4: second entry signer names ({@code List})
+ *
+ */
+ JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH(
+ "Entries %1$s and %3$s are signed with different sets of signers"
+ + " : <%2$s> vs <%4$s>"),
+
+ /**
+ * Digest of the specified ZIP entry's data does not match the digest expected by the JAR
+ * signature.
+ *
+ *
+ * - Parameter 1: entry name ({@code String})
+ * - Parameter 2: digest algorithm (e.g., SHA-256) ({@code String})
+ * - Parameter 3: name of the entry in which the expected digest is specified
+ * ({@code String})
+ * - Parameter 4: base64-encoded actual digest ({@code String})
+ * - Parameter 5: base64-encoded expected digest ({@code String})
+ *
+ */
+ JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY(
+ "%2$s digest of %1$s does not match the digest specified in %3$s"
+ + ". Expected: <%5$s>, actual: <%4$s>"),
+
+ /**
+ * Digest of the JAR manifest main section did not verify.
+ *
+ *
+ * - Parameter 1: digest algorithm (e.g., SHA-256) ({@code String})
+ * - Parameter 2: name of the entry in which the expected digest is specified
+ * ({@code String})
+ * - Parameter 3: base64-encoded actual digest ({@code String})
+ * - Parameter 4: base64-encoded expected digest ({@code String})
+ *
+ */
+ JAR_SIG_MANIFEST_MAIN_SECTION_DIGEST_DID_NOT_VERIFY(
+ "%1$s digest of META-INF/MANIFEST.MF main section does not match the digest"
+ + " specified in %2$s. Expected: <%4$s>, actual: <%3$s>"),
+
+ /**
+ * Digest of the specified JAR manifest section does not match the digest expected by the
+ * JAR signature.
+ *
+ *
+ * - Parameter 1: section name ({@code String})
+ * - Parameter 2: digest algorithm (e.g., SHA-256) ({@code String})
+ * - Parameter 3: name of the signature file in which the expected digest is specified
+ * ({@code String})
+ * - Parameter 4: base64-encoded actual digest ({@code String})
+ * - Parameter 5: base64-encoded expected digest ({@code String})
+ *
+ */
+ JAR_SIG_MANIFEST_SECTION_DIGEST_DID_NOT_VERIFY(
+ "%2$s digest of META-INF/MANIFEST.MF section for %1$s does not match the digest"
+ + " specified in %3$s. Expected: <%5$s>, actual: <%4$s>"),
+
+ /**
+ * JAR signature file does not contain the whole-file digest of the JAR manifest file. The
+ * digest speeds up verification of JAR signature.
+ *
+ *
+ * - Parameter 1: name of the signature file ({@code String})
+ *
+ */
+ JAR_SIG_NO_MANIFEST_DIGEST_IN_SIG_FILE(
+ "%1$s does not specify digest of META-INF/MANIFEST.MF"
+ + ". This slows down verification."),
+
+ /**
+ * APK is signed using APK Signature Scheme v2 or newer, but JAR signature file does not
+ * contain protections against stripping of these newer scheme signatures.
+ *
+ *
+ * - Parameter 1: name of the signature file ({@code String})
+ *
+ */
+ JAR_SIG_NO_APK_SIG_STRIP_PROTECTION(
+ "APK is signed using APK Signature Scheme v2 but these signatures may be stripped"
+ + " without being detected because %1$s does not contain anti-stripping"
+ + " protections."),
+
+ /**
+ * JAR signature of the signer is missing a file/entry.
+ *
+ *
+ * - Parameter 1: name of the encountered file ({@code String})
+ * - Parameter 2: name of the missing file ({@code String})
+ *
+ */
+ JAR_SIG_MISSING_FILE("Partial JAR signature. Found: %1$s, missing: %2$s"),
+
+ /**
+ * An exception was encountered while verifying JAR signature contained in a signature block
+ * against the signature file.
+ *
+ *
+ * - Parameter 1: name of the signature block file ({@code String})
+ * - Parameter 2: name of the signature file ({@code String})
+ * - Parameter 3: exception ({@code Throwable})
+ *
+ */
+ JAR_SIG_VERIFY_EXCEPTION("Failed to verify JAR signature %1$s against %2$s: %3$s"),
+
+ /**
+ * JAR signature contains unsupported digest algorithm.
+ *
+ *
+ * - Parameter 1: name of the signature block file ({@code String})
+ * - Parameter 2: digest algorithm OID ({@code String})
+ * - Parameter 3: signature algorithm OID ({@code String})
+ * - Parameter 4: API Levels on which this combination of algorithms is not supported
+ * ({@code String})
+ * - Parameter 5: user-friendly variant of digest algorithm ({@code String})
+ * - Parameter 6: user-friendly variant of signature algorithm ({@code String})
+ *
+ */
+ JAR_SIG_UNSUPPORTED_SIG_ALG(
+ "JAR signature %1$s uses digest algorithm %5$s and signature algorithm %6$s which"
+ + " is not supported on API Level(s) %4$s for which this APK is being"
+ + " verified"),
+
+ /**
+ * An exception was encountered while parsing JAR signature contained in a signature block.
+ *
+ *
+ * - Parameter 1: name of the signature block file ({@code String})
+ * - Parameter 2: exception ({@code Throwable})
+ *
+ */
+ JAR_SIG_PARSE_EXCEPTION("Failed to parse JAR signature %1$s: %2$s"),
+
+ /**
+ * An exception was encountered while parsing a certificate contained in the JAR signature
+ * block.
+ *
+ *
+ * - Parameter 1: name of the signature block file ({@code String})
+ * - Parameter 2: exception ({@code Throwable})
+ *
+ */
+ JAR_SIG_MALFORMED_CERTIFICATE("Malformed certificate in JAR signature %1$s: %2$s"),
+
+ /**
+ * JAR signature contained in a signature block file did not verify against the signature
+ * file.
+ *
+ *
+ * - Parameter 1: name of the signature block file ({@code String})
+ * - Parameter 2: name of the signature file ({@code String})
+ *
+ */
+ JAR_SIG_DID_NOT_VERIFY("JAR signature %1$s did not verify against %2$s"),
+
+ /**
+ * JAR signature contains no verified signers.
+ *
+ *
+ * - Parameter 1: name of the signature block file ({@code String})
+ *
+ */
+ JAR_SIG_NO_SIGNERS("JAR signature %1$s contains no signers"),
+
+ /**
+ * JAR signature file contains a section with a duplicate name.
+ *
+ *
+ * - Parameter 1: signature file name ({@code String})
+ * - Parameter 1: section name ({@code String})
+ *
+ */
+ JAR_SIG_DUPLICATE_SIG_FILE_SECTION("Duplicate section in %1$s: %2$s"),
+
+ /**
+ * JAR signature file's main section doesn't contain the mandatory Signature-Version
+ * attribute.
+ *
+ *
+ * - Parameter 1: signature file name ({@code String})
+ *
+ */
+ JAR_SIG_MISSING_VERSION_ATTR_IN_SIG_FILE(
+ "Malformed %1$s: missing Signature-Version attribute"),
+
+ /**
+ * JAR signature file references an unknown APK signature scheme ID.
+ *
+ *
+ * - Parameter 1: name of the signature file ({@code String})
+ * - Parameter 2: unknown APK signature scheme ID ({@code} Integer)
+ *
+ */
+ JAR_SIG_UNKNOWN_APK_SIG_SCHEME_ID(
+ "JAR signature %1$s references unknown APK signature scheme ID: %2$d"),
+
+ /**
+ * JAR signature file indicates that the APK is supposed to be signed with a supported APK
+ * signature scheme (in addition to the JAR signature) but no such signature was found in
+ * the APK.
+ *
+ *
+ * - Parameter 1: name of the signature file ({@code String})
+ * - Parameter 2: APK signature scheme ID ({@code} Integer)
+ * - Parameter 3: APK signature scheme English name ({@code} String)
+ *
+ */
+ JAR_SIG_MISSING_APK_SIG_REFERENCED(
+ "JAR signature %1$s indicates the APK is signed using %3$s but no such signature"
+ + " was found. Signature stripped?"),
+
+ /**
+ * JAR entry is not covered by signature and thus unauthorized modifications to its contents
+ * will not be detected.
+ *
+ *
+ * - Parameter 1: entry name ({@code String})
+ *
+ */
+ JAR_SIG_UNPROTECTED_ZIP_ENTRY(
+ "%1$s not protected by signature. Unauthorized modifications to this JAR entry"
+ + " will not be detected. Delete or move the entry outside of META-INF/."),
+
+ /**
+ * APK which is both JAR-signed and signed using APK Signature Scheme v2 contains an APK
+ * Signature Scheme v2 signature from this signer, but does not contain a JAR signature
+ * from this signer.
+ */
+ JAR_SIG_MISSING("No JAR signature from this signer"),
+
+ /**
+ * APK is targeting a sandbox version which requires APK Signature Scheme v2 signature but
+ * no such signature was found.
+ *
+ *
+ * - Parameter 1: target sandbox version ({@code Integer})
+ *
+ */
+ NO_SIG_FOR_TARGET_SANDBOX_VERSION(
+ "Missing APK Signature Scheme v2 signature required for target sandbox version"
+ + " %1$d"),
+
+ /**
+ * APK which is both JAR-signed and signed using APK Signature Scheme v2 contains a JAR
+ * signature from this signer, but does not contain an APK Signature Scheme v2 signature
+ * from this signer.
+ */
+ V2_SIG_MISSING("No APK Signature Scheme v2 signature from this signer"),
+
+ /**
+ * Failed to parse the list of signers contained in the APK Signature Scheme v2 signature.
+ */
+ V2_SIG_MALFORMED_SIGNERS("Malformed list of signers"),
+
+ /**
+ * Failed to parse this signer's signer block contained in the APK Signature Scheme v2
+ * signature.
+ */
+ V2_SIG_MALFORMED_SIGNER("Malformed signer block"),
+
+ /**
+ * Public key embedded in the APK Signature Scheme v2 signature of this signer could not be
+ * parsed.
+ *
+ *
+ * - Parameter 1: error details ({@code Throwable})
+ *
+ */
+ V2_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"),
+
+ /**
+ * This APK Signature Scheme v2 signer's certificate could not be parsed.
+ *
+ *
+ * - Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of
+ * certificates ({@code Integer})
+ * - Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's
+ * list of certificates ({@code Integer})
+ * - Parameter 3: error details ({@code Throwable})
+ *
+ */
+ V2_SIG_MALFORMED_CERTIFICATE("Malformed certificate #%2$d: %3$s"),
+
+ /**
+ * Failed to parse this signer's signature record contained in the APK Signature Scheme v2
+ * signature.
+ *
+ *
+ * - Parameter 1: record number (first record is {@code 1}) ({@code Integer})
+ *
+ */
+ V2_SIG_MALFORMED_SIGNATURE("Malformed APK Signature Scheme v2 signature record #%1$d"),
+
+ /**
+ * Failed to parse this signer's digest record contained in the APK Signature Scheme v2
+ * signature.
+ *
+ *
+ * - Parameter 1: record number (first record is {@code 1}) ({@code Integer})
+ *
+ */
+ V2_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v2 digest record #%1$d"),
+
+ /**
+ * This APK Signature Scheme v2 signer contains a malformed additional attribute.
+ *
+ *
+ * - Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})
+ *
+ */
+ V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"),
+
+ /**
+ * APK Signature Scheme v2 signature references an unknown APK signature scheme ID.
+ *
+ *
+ * - Parameter 1: signer index ({@code Integer})
+ * - Parameter 2: unknown APK signature scheme ID ({@code} Integer)
+ *
+ */
+ V2_SIG_UNKNOWN_APK_SIG_SCHEME_ID(
+ "APK Signature Scheme v2 signer: %1$s references unknown APK signature scheme ID: "
+ + "%2$d"),
+
+ /**
+ * APK Signature Scheme v2 signature indicates that the APK is supposed to be signed with a
+ * supported APK signature scheme (in addition to the v2 signature) but no such signature
+ * was found in the APK.
+ *
+ *
+ * - Parameter 1: signer index ({@code Integer})
+ * - Parameter 2: APK signature scheme English name ({@code} String)
+ *
+ */
+ V2_SIG_MISSING_APK_SIG_REFERENCED(
+ "APK Signature Scheme v2 signature %1$s indicates the APK is signed using %2$s but "
+ + "no such signature was found. Signature stripped?"),
+
+ /**
+ * APK Signature Scheme v2 signature contains no signers.
+ */
+ V2_SIG_NO_SIGNERS("No signers in APK Signature Scheme v2 signature"),
+
+ /**
+ * This APK Signature Scheme v2 signer contains a signature produced using an unknown
+ * algorithm.
+ *
+ *
+ * - Parameter 1: algorithm ID ({@code Integer})
+ *
+ */
+ V2_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"),
+
+ /**
+ * This APK Signature Scheme v2 signer contains an unknown additional attribute.
+ *
+ *
+ * - Parameter 1: attribute ID ({@code Integer})
+ *
+ */
+ V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE("Unknown additional attribute: ID %1$#x"),
+
+ /**
+ * An exception was encountered while verifying APK Signature Scheme v2 signature of this
+ * signer.
+ *
+ *
+ * - Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+ * - Parameter 2: exception ({@code Throwable})
+ *
+ */
+ V2_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+ /**
+ * APK Signature Scheme v2 signature over this signer's signed-data block did not verify.
+ *
+ *
+ * - Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+ *
+ */
+ V2_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
+
+ /**
+ * This APK Signature Scheme v2 signer offers no signatures.
+ */
+ V2_SIG_NO_SIGNATURES("No signatures"),
+
+ /**
+ * This APK Signature Scheme v2 signer offers signatures but none of them are supported.
+ */
+ V2_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures"),
+
+ /**
+ * This APK Signature Scheme v2 signer offers no certificates.
+ */
+ V2_SIG_NO_CERTIFICATES("No certificates"),
+
+ /**
+ * This APK Signature Scheme v2 signer's public key listed in the signer's certificate does
+ * not match the public key listed in the signatures record.
+ *
+ *
+ * - Parameter 1: hex-encoded public key from certificate ({@code String})
+ * - Parameter 2: hex-encoded public key from signatures record ({@code String})
+ *
+ */
+ V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD(
+ "Public key mismatch between certificate and signature record: <%1$s> vs <%2$s>"),
+
+ /**
+ * This APK Signature Scheme v2 signer's signature algorithms listed in the signatures
+ * record do not match the signature algorithms listed in the signatures record.
+ *
+ *
+ * - Parameter 1: signature algorithms from signatures record ({@code List})
+ * - Parameter 2: signature algorithms from digests record ({@code List})
+ *
+ */
+ V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS(
+ "Signature algorithms mismatch between signatures and digests records"
+ + ": %1$s vs %2$s"),
+
+ /**
+ * The APK's digest does not match the digest contained in the APK Signature Scheme v2
+ * signature.
+ *
+ *
+ * - Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})
+ * - Parameter 2: hex-encoded expected digest of the APK ({@code String})
+ * - Parameter 3: hex-encoded actual digest of the APK ({@code String})
+ *
+ */
+ V2_SIG_APK_DIGEST_DID_NOT_VERIFY(
+ "APK integrity check failed. %1$s digest mismatch."
+ + " Expected: <%2$s>, actual: <%3$s>"),
+
+ /**
+ * Failed to parse the list of signers contained in the APK Signature Scheme v3 signature.
+ */
+ V3_SIG_MALFORMED_SIGNERS("Malformed list of signers"),
+
+ /**
+ * Failed to parse this signer's signer block contained in the APK Signature Scheme v3
+ * signature.
+ */
+ V3_SIG_MALFORMED_SIGNER("Malformed signer block"),
+
+ /**
+ * Public key embedded in the APK Signature Scheme v3 signature of this signer could not be
+ * parsed.
+ *
+ *
+ * - Parameter 1: error details ({@code Throwable})
+ *
+ */
+ V3_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"),
+
+ /**
+ * This APK Signature Scheme v3 signer's certificate could not be parsed.
+ *
+ *
+ * - Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of
+ * certificates ({@code Integer})
+ * - Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's
+ * list of certificates ({@code Integer})
+ * - Parameter 3: error details ({@code Throwable})
+ *
+ */
+ V3_SIG_MALFORMED_CERTIFICATE("Malformed certificate #%2$d: %3$s"),
+
+ /**
+ * Failed to parse this signer's signature record contained in the APK Signature Scheme v3
+ * signature.
+ *
+ *
+ * - Parameter 1: record number (first record is {@code 1}) ({@code Integer})
+ *
+ */
+ V3_SIG_MALFORMED_SIGNATURE("Malformed APK Signature Scheme v3 signature record #%1$d"),
+
+ /**
+ * Failed to parse this signer's digest record contained in the APK Signature Scheme v3
+ * signature.
+ *
+ *
+ * - Parameter 1: record number (first record is {@code 1}) ({@code Integer})
+ *
+ */
+ V3_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v3 digest record #%1$d"),
+
+ /**
+ * This APK Signature Scheme v3 signer contains a malformed additional attribute.
+ *
+ *
+ * - Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})
+ *
+ */
+ V3_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"),
+
+ /**
+ * APK Signature Scheme v3 signature contains no signers.
+ */
+ V3_SIG_NO_SIGNERS("No signers in APK Signature Scheme v3 signature"),
+
+ /**
+ * APK Signature Scheme v3 signature contains multiple signers (only one allowed per
+ * platform version).
+ */
+ V3_SIG_MULTIPLE_SIGNERS("Multiple APK Signature Scheme v3 signatures found for a single "
+ + " platform version."),
+
+ /**
+ * APK Signature Scheme v3 signature found, but multiple v1 and/or multiple v2 signers
+ * found, where only one may be used with APK Signature Scheme v3
+ */
+ V3_SIG_MULTIPLE_PAST_SIGNERS("Multiple signatures found for pre-v3 signing with an APK "
+ + " Signature Scheme v3 signer. Only one allowed."),
+
+ /**
+ * APK Signature Scheme v3 signature found, but its signer doesn't match the v1/v2 signers,
+ * or have them as the root of its signing certificate history
+ */
+ V3_SIG_PAST_SIGNERS_MISMATCH(
+ "v3 signer differs from v1/v2 signer without proper signing certificate lineage."),
+
+ /**
+ * This APK Signature Scheme v3 signer contains a signature produced using an unknown
+ * algorithm.
+ *
+ *
+ * - Parameter 1: algorithm ID ({@code Integer})
+ *
+ */
+ V3_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"),
+
+ /**
+ * This APK Signature Scheme v3 signer contains an unknown additional attribute.
+ *
+ *
+ * - Parameter 1: attribute ID ({@code Integer})
+ *
+ */
+ V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE("Unknown additional attribute: ID %1$#x"),
+
+ /**
+ * An exception was encountered while verifying APK Signature Scheme v3 signature of this
+ * signer.
+ *
+ *
+ * - Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+ * - Parameter 2: exception ({@code Throwable})
+ *
+ */
+ V3_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+ /**
+ * The APK Signature Scheme v3 signer contained an invalid value for either min or max SDK
+ * versions.
+ *
+ *
+ * - Parameter 1: minSdkVersion ({@code Integer})
+ *
- Parameter 2: maxSdkVersion ({@code Integer})
+ *
+ */
+ V3_SIG_INVALID_SDK_VERSIONS("Invalid SDK Version parameter(s) encountered in APK Signature "
+ + "scheme v3 signature: minSdkVersion %1$s maxSdkVersion: %2$s"),
+
+ /**
+ * APK Signature Scheme v3 signature over this signer's signed-data block did not verify.
+ *
+ *
+ * - Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+ *
+ */
+ V3_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
+
+ /**
+ * This APK Signature Scheme v3 signer offers no signatures.
+ */
+ V3_SIG_NO_SIGNATURES("No signatures"),
+
+ /**
+ * This APK Signature Scheme v3 signer offers signatures but none of them are supported.
+ */
+ V3_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures"),
+
+ /**
+ * This APK Signature Scheme v3 signer offers no certificates.
+ */
+ V3_SIG_NO_CERTIFICATES("No certificates"),
+
+ /**
+ * This APK Signature Scheme v3 signer's minSdkVersion listed in the signer's signed data
+ * does not match the minSdkVersion listed in the signatures record.
+ *
+ *
+ * - Parameter 1: minSdkVersion in signature record ({@code Integer})
+ * - Parameter 2: minSdkVersion in signed data ({@code Integer})
+ *
+ */
+ V3_MIN_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD(
+ "minSdkVersion mismatch between signed data and signature record:"
+ + " <%1$s> vs <%2$s>"),
+
+ /**
+ * This APK Signature Scheme v3 signer's maxSdkVersion listed in the signer's signed data
+ * does not match the maxSdkVersion listed in the signatures record.
+ *
+ *
+ * - Parameter 1: maxSdkVersion in signature record ({@code Integer})
+ * - Parameter 2: maxSdkVersion in signed data ({@code Integer})
+ *
+ */
+ V3_MAX_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD(
+ "maxSdkVersion mismatch between signed data and signature record:"
+ + " <%1$s> vs <%2$s>"),
+
+ /**
+ * This APK Signature Scheme v3 signer's public key listed in the signer's certificate does
+ * not match the public key listed in the signatures record.
+ *
+ *
+ * - Parameter 1: hex-encoded public key from certificate ({@code String})
+ * - Parameter 2: hex-encoded public key from signatures record ({@code String})
+ *
+ */
+ V3_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD(
+ "Public key mismatch between certificate and signature record: <%1$s> vs <%2$s>"),
+
+ /**
+ * This APK Signature Scheme v3 signer's signature algorithms listed in the signatures
+ * record do not match the signature algorithms listed in the signatures record.
+ *
+ *
+ * - Parameter 1: signature algorithms from signatures record ({@code List})
+ * - Parameter 2: signature algorithms from digests record ({@code List})
+ *
+ */
+ V3_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS(
+ "Signature algorithms mismatch between signatures and digests records"
+ + ": %1$s vs %2$s"),
+
+ /**
+ * The APK's digest does not match the digest contained in the APK Signature Scheme v3
+ * signature.
+ *
+ *
+ * - Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})
+ * - Parameter 2: hex-encoded expected digest of the APK ({@code String})
+ * - Parameter 3: hex-encoded actual digest of the APK ({@code String})
+ *
+ */
+ V3_SIG_APK_DIGEST_DID_NOT_VERIFY(
+ "APK integrity check failed. %1$s digest mismatch."
+ + " Expected: <%2$s>, actual: <%3$s>"),
+
+ /**
+ * The signer's SigningCertificateLineage attribute containd a proof-of-rotation record with
+ * signature(s) that did not verify.
+ */
+ V3_SIG_POR_DID_NOT_VERIFY("SigningCertificateLineage attribute containd a proof-of-rotation"
+ + " record with signature(s) that did not verify."),
+
+ /**
+ * Failed to parse the SigningCertificateLineage structure in the APK Signature Scheme v3
+ * signature's additional attributes section.
+ */
+ V3_SIG_MALFORMED_LINEAGE("Failed to parse the SigningCertificateLineage structure in the "
+ + "APK Signature Scheme v3 signature's additional attributes section."),
+
+ /**
+ * The APK's signing certificate does not match the terminal node in the provided
+ * proof-of-rotation structure describing the signing certificate history
+ */
+ V3_SIG_POR_CERT_MISMATCH(
+ "APK signing certificate differs from the associated certificate found in the "
+ + "signer's SigningCertificateLineage."),
+
+ /**
+ * The APK Signature Scheme v3 signers encountered do not offer a continuous set of
+ * supported platform versions. Either they overlap, resulting in potentially two
+ * acceptable signers for a platform version, or there are holes which would create problems
+ * in the event of platform version upgrades.
+ */
+ V3_INCONSISTENT_SDK_VERSIONS("APK Signature Scheme v3 signers supported min/max SDK "
+ + "versions are not continuous."),
+
+ /**
+ * The APK Signature Scheme v3 signers don't cover all requested SDK versions.
+ *
+ *
+ * - Parameter 1: minSdkVersion ({@code Integer})
+ *
- Parameter 2: maxSdkVersion ({@code Integer})
+ *
+ */
+ V3_MISSING_SDK_VERSIONS("APK Signature Scheme v3 signers supported min/max SDK "
+ + "versions do not cover the entire desired range. Found min: %1$s max %2$s"),
+
+ /**
+ * The SigningCertificateLineages for different platform versions using APK Signature Scheme
+ * v3 do not go together. Specifically, each should be a subset of another, with the size
+ * of each increasing as the platform level increases.
+ */
+ V3_INCONSISTENT_LINEAGES("SigningCertificateLineages targeting different platform versions"
+ + " using APK Signature Scheme v3 are not all a part of the same overall lineage."),
+
+ /**
+ * APK Signing Block contains an unknown entry.
+ *
+ *
+ * - Parameter 1: entry ID ({@code Integer})
+ *
+ */
+ APK_SIG_BLOCK_UNKNOWN_ENTRY_ID("APK Signing Block contains unknown entry: ID %1$#x");
+
+ private final String mFormat;
+
+ private Issue(String format) {
+ mFormat = format;
+ }
+
+ /**
+ * Returns the format string suitable for combining the parameters of this issue into a
+ * readable string. See {@link java.util.Formatter} for format.
+ */
+ private String getFormat() {
+ return mFormat;
+ }
+ }
+
+ /**
+ * {@link Issue} with associated parameters. {@link #toString()} produces a readable formatted
+ * form.
+ */
+ public static class IssueWithParams {
+ private final Issue mIssue;
+ private final Object[] mParams;
+
+ /**
+ * Constructs a new {@code IssueWithParams} of the specified type and with provided
+ * parameters.
+ */
+ public IssueWithParams(Issue issue, Object[] params) {
+ mIssue = issue;
+ mParams = params;
+ }
+
+ /**
+ * Returns the type of this issue.
+ */
+ public Issue getIssue() {
+ return mIssue;
+ }
+
+ /**
+ * Returns the parameters of this issue.
+ */
+ public Object[] getParams() {
+ return mParams.clone();
+ }
+
+ /**
+ * Returns a readable form of this issue.
+ */
+ @Override
+ public String toString() {
+ return String.format(mIssue.getFormat(), mParams);
+ }
+ }
+
+ /**
+ * Wrapped around {@code byte[]} which ensures that {@code equals} and {@code hashCode} operate
+ * on the contents of the arrays rather than on references.
+ */
+ private static class ByteArray {
+ private final byte[] mArray;
+ private final int mHashCode;
+
+ private ByteArray(byte[] arr) {
+ mArray = arr;
+ mHashCode = Arrays.hashCode(mArray);
+ }
+
+ @Override
+ public int hashCode() {
+ return mHashCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ ByteArray other = (ByteArray) obj;
+ if (hashCode() != other.hashCode()) {
+ return false;
+ }
+ if (!Arrays.equals(mArray, other.mArray)) {
+ return false;
+ }
+ return true;
+ }
+ }
+
+ /**
+ * Builder of {@link ApkVerifier} instances.
+ *
+ * The resulting verifier by default checks whether the APK will verify on all platform
+ * versions supported by the APK, as specified by {@code android:minSdkVersion} attributes in
+ * the APK's {@code AndroidManifest.xml}. The range of platform versions can be customized using
+ * {@link #setMinCheckedPlatformVersion(int)} and {@link #setMaxCheckedPlatformVersion(int)}.
+ */
+ public static class Builder {
+ private final File mApkFile;
+ private final DataSource mApkDataSource;
+
+ private Integer mMinSdkVersion;
+ private int mMaxSdkVersion = Integer.MAX_VALUE;
+
+ /**
+ * Constructs a new {@code Builder} for verifying the provided APK file.
+ */
+ public Builder(File apk) {
+ if (apk == null) {
+ throw new NullPointerException("apk == null");
+ }
+ mApkFile = apk;
+ mApkDataSource = null;
+ }
+
+ /**
+ * Constructs a new {@code Builder} for verifying the provided APK.
+ */
+ public Builder(DataSource apk) {
+ if (apk == null) {
+ throw new NullPointerException("apk == null");
+ }
+ mApkDataSource = apk;
+ mApkFile = null;
+ }
+
+ /**
+ * Sets the oldest Android platform version for which the APK is verified. APK verification
+ * will confirm that the APK is expected to install successfully on all known Android
+ * platforms starting from the platform version with the provided API Level. The upper end
+ * of the platform versions range can be modified via
+ * {@link #setMaxCheckedPlatformVersion(int)}.
+ *
+ *
This method is useful for overriding the default behavior which checks that the APK
+ * will verify on all platform versions supported by the APK, as specified by
+ * {@code android:minSdkVersion} attributes in the APK's {@code AndroidManifest.xml}.
+ *
+ * @param minSdkVersion API Level of the oldest platform for which to verify the APK
+ *
+ * @see #setMinCheckedPlatformVersion(int)
+ */
+ public Builder setMinCheckedPlatformVersion(int minSdkVersion) {
+ mMinSdkVersion = minSdkVersion;
+ return this;
+ }
+
+ /**
+ * Sets the newest Android platform version for which the APK is verified. APK verification
+ * will confirm that the APK is expected to install successfully on all platform versions
+ * supported by the APK up until and including the provided version. The lower end
+ * of the platform versions range can be modified via
+ * {@link #setMinCheckedPlatformVersion(int)}.
+ *
+ * @param maxSdkVersion API Level of the newest platform for which to verify the APK
+ *
+ * @see #setMinCheckedPlatformVersion(int)
+ */
+ public Builder setMaxCheckedPlatformVersion(int maxSdkVersion) {
+ mMaxSdkVersion = maxSdkVersion;
+ return this;
+ }
+
+ /**
+ * Returns an {@link ApkVerifier} initialized according to the configuration of this
+ * builder.
+ */
+ public ApkVerifier build() {
+ return new ApkVerifier(
+ mApkFile,
+ mApkDataSource,
+ mMinSdkVersion,
+ mMaxSdkVersion);
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/app/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
new file mode 100644
index 0000000..c88239e
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
@@ -0,0 +1,1506 @@
+/*
+ * 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;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.v1.DigestAlgorithm;
+import com.android.apksig.internal.apk.v1.V1SchemeSigner;
+import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
+import com.android.apksig.internal.apk.v2.V2SchemeSigner;
+import com.android.apksig.internal.apk.v3.V3SchemeSigner;
+import com.android.apksig.internal.jar.ManifestParser;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.util.TeeDataSink;
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSinks;
+import com.android.apksig.util.DataSource;
+
+import com.android.apksig.util.RunnablesExecutor;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Default implementation of {@link ApkSignerEngine}.
+ *
+ *
Use {@link Builder} to obtain instances of this engine.
+ */
+public class DefaultApkSignerEngine implements ApkSignerEngine {
+
+ // IMPLEMENTATION NOTE: This engine generates a signed APK as follows:
+ // 1. The engine asks its client to output input JAR entries which are not part of JAR
+ // signature.
+ // 2. If JAR signing (v1 signing) is enabled, the engine inspects the output JAR entries to
+ // compute their digests, to be placed into output META-INF/MANIFEST.MF. It also inspects
+ // the contents of input and output META-INF/MANIFEST.MF to borrow the main section of the
+ // file. It does not care about individual (i.e., JAR entry-specific) sections. It then
+ // emits the v1 signature (a set of JAR entries) and asks the client to output them.
+ // 3. If APK Signature Scheme v2 (v2 signing) is enabled, the engine emits an APK Signing Block
+ // from outputZipSections() and asks its client to insert this block into the output.
+ // 4. If APK Signature Scheme v3 (v3 signing) is enabled, the engine includes it in the APK
+ // Signing BLock output from outputZipSections() and asks its client to insert this block
+ // into the output. If both v2 and v3 signing is enabled, they are both added to the APK
+ // Signing Block before asking the client to insert it into the output.
+
+ private final boolean mV1SigningEnabled;
+ private final boolean mV2SigningEnabled;
+ private final boolean mV3SigningEnabled;
+ private final boolean mDebuggableApkPermitted;
+ private final boolean mOtherSignersSignaturesPreserved;
+ private final String mCreatedBy;
+ private final List mSignerConfigs;
+ private final int mMinSdkVersion;
+ private final SigningCertificateLineage mSigningCertificateLineage;
+
+ private List mV1SignerConfigs = Collections.emptyList();
+ private DigestAlgorithm mV1ContentDigestAlgorithm;
+
+ private boolean mClosed;
+
+ private boolean mV1SignaturePending;
+
+ /**
+ * Names of JAR entries which this engine is expected to output as part of v1 signing.
+ */
+ private Set mSignatureExpectedOutputJarEntryNames = Collections.emptySet();
+
+ /** Requests for digests of output JAR entries. */
+ private final Map mOutputJarEntryDigestRequests =
+ new HashMap<>();
+
+ /** Digests of output JAR entries. */
+ private final Map mOutputJarEntryDigests = new HashMap<>();
+
+ /** Data of JAR entries emitted by this engine as v1 signature. */
+ private final Map mEmittedSignatureJarEntryData = new HashMap<>();
+
+ /** Requests for data of output JAR entries which comprise the v1 signature. */
+ private final Map mOutputSignatureJarEntryDataRequests =
+ new HashMap<>();
+ /**
+ * Request to obtain the data of MANIFEST.MF or {@code null} if the request hasn't been issued.
+ */
+ private GetJarEntryDataRequest mInputJarManifestEntryDataRequest;
+
+ /**
+ * Request to obtain the data of AndroidManifest.xml or {@code null} if the request hasn't been
+ * issued.
+ */
+ private GetJarEntryDataRequest mOutputAndroidManifestEntryDataRequest;
+
+ /**
+ * Whether the package being signed is marked as {@code android:debuggable} or {@code null}
+ * if this is not yet known.
+ */
+ private Boolean mDebuggable;
+
+ /**
+ * Request to output the emitted v1 signature or {@code null} if the request hasn't been issued.
+ */
+ private OutputJarSignatureRequestImpl mAddV1SignatureRequest;
+
+ private boolean mV2SignaturePending;
+ private boolean mV3SignaturePending;
+
+ /**
+ * Request to output the emitted v2 and/or v3 signature(s) {@code null} if the request hasn't
+ * been issued.
+ */
+ private OutputApkSigningBlockRequestImpl mAddSigningBlockRequest;
+
+
+ private RunnablesExecutor mExecutor = RunnablesExecutor.SINGLE_THREADED;
+
+ private DefaultApkSignerEngine(
+ List signerConfigs,
+ int minSdkVersion,
+ boolean v1SigningEnabled,
+ boolean v2SigningEnabled,
+ boolean v3SigningEnabled,
+ boolean debuggableApkPermitted,
+ boolean otherSignersSignaturesPreserved,
+ String createdBy,
+ SigningCertificateLineage signingCertificateLineage) throws InvalidKeyException {
+ if (signerConfigs.isEmpty()) {
+ throw new IllegalArgumentException("At least one signer config must be provided");
+ }
+ if (otherSignersSignaturesPreserved) {
+ throw new UnsupportedOperationException(
+ "Preserving other signer's signatures is not yet implemented");
+ }
+
+ mV1SigningEnabled = v1SigningEnabled;
+ mV2SigningEnabled = v2SigningEnabled;
+ mV3SigningEnabled = v3SigningEnabled;
+ mV1SignaturePending = v1SigningEnabled;
+ mV2SignaturePending = v2SigningEnabled;
+ mV3SignaturePending = v3SigningEnabled;
+ mDebuggableApkPermitted = debuggableApkPermitted;
+ mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
+ mCreatedBy = createdBy;
+ mSignerConfigs = signerConfigs;
+ mMinSdkVersion = minSdkVersion;
+ mSigningCertificateLineage = signingCertificateLineage;
+
+ if (v1SigningEnabled) {
+ if (v3SigningEnabled) {
+
+ // v3 signing only supports single signers, of which the oldest (first) will be the
+ // one to use for v1 and v2 signing
+ SignerConfig oldestConfig = signerConfigs.get(0);
+
+ // in the event of signing certificate changes, make sure we have the oldest in the
+ // signing history to sign with v1
+ if (signingCertificateLineage != null) {
+ SigningCertificateLineage subLineage =
+ signingCertificateLineage.getSubLineage(
+ oldestConfig.mCertificates.get(0));
+ if (subLineage.size() != 1) {
+ throw new IllegalArgumentException(
+ "v1 signing enabled but the oldest signer in the "
+ + "SigningCertificateLineage is missing. Please provide the oldest"
+ + " signer to enable v1 signing");
+ }
+ }
+ createV1SignerConfigs(
+ Collections.singletonList(oldestConfig), minSdkVersion);
+ } else {
+ createV1SignerConfigs(signerConfigs, minSdkVersion);
+ }
+ }
+ }
+
+ private void createV1SignerConfigs(List signerConfigs, int minSdkVersion)
+ throws InvalidKeyException {
+ mV1SignerConfigs = new ArrayList<>(signerConfigs.size());
+ Map v1SignerNameToSignerIndex = new HashMap<>(signerConfigs.size());
+ DigestAlgorithm v1ContentDigestAlgorithm = null;
+ for (int i = 0; i < signerConfigs.size(); i++) {
+ SignerConfig signerConfig = signerConfigs.get(i);
+ List certificates = signerConfig.getCertificates();
+ PublicKey publicKey = certificates.get(0).getPublicKey();
+
+ String v1SignerName = V1SchemeSigner.getSafeSignerName(signerConfig.getName());
+ // Check whether the signer's name is unique among all v1 signers
+ Integer indexOfOtherSignerWithSameName =
+ v1SignerNameToSignerIndex.put(v1SignerName, i);
+ if (indexOfOtherSignerWithSameName != null) {
+ throw new IllegalArgumentException(
+ "Signers #" + (indexOfOtherSignerWithSameName + 1)
+ + " and #" + (i + 1)
+ + " have the same name: " + v1SignerName
+ + ". v1 signer names must be unique");
+ }
+
+ DigestAlgorithm v1SignatureDigestAlgorithm =
+ V1SchemeSigner.getSuggestedSignatureDigestAlgorithm(
+ publicKey, minSdkVersion);
+ V1SchemeSigner.SignerConfig v1SignerConfig = new V1SchemeSigner.SignerConfig();
+ v1SignerConfig.name = v1SignerName;
+ v1SignerConfig.privateKey = signerConfig.getPrivateKey();
+ v1SignerConfig.certificates = certificates;
+ v1SignerConfig.signatureDigestAlgorithm = v1SignatureDigestAlgorithm;
+ // For digesting contents of APK entries and of MANIFEST.MF, pick the algorithm
+ // of comparable strength to the digest algorithm used for computing the signature.
+ // When there are multiple signers, pick the strongest digest algorithm out of their
+ // signature digest algorithms. This avoids reducing the digest strength used by any
+ // of the signers to protect APK contents.
+ if (v1ContentDigestAlgorithm == null) {
+ v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm;
+ } else {
+ if (DigestAlgorithm.BY_STRENGTH_COMPARATOR.compare(
+ v1SignatureDigestAlgorithm, v1ContentDigestAlgorithm) > 0) {
+ v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm;
+ }
+ }
+ mV1SignerConfigs.add(v1SignerConfig);
+ }
+ mV1ContentDigestAlgorithm = v1ContentDigestAlgorithm;
+ mSignatureExpectedOutputJarEntryNames =
+ V1SchemeSigner.getOutputEntryNames(mV1SignerConfigs);
+ }
+
+ private List createV2SignerConfigs(
+ boolean apkSigningBlockPaddingSupported) throws InvalidKeyException {
+ if (mV3SigningEnabled) {
+
+ // v3 signing only supports single signers, of which the oldest (first) will be the one
+ // to use for v1 and v2 signing
+ List signerConfig =
+ new ArrayList<>();
+
+ SignerConfig oldestConfig = mSignerConfigs.get(0);
+
+ // first make sure that if we have signing certificate history that the oldest signer
+ // corresponds to the oldest ancestor
+ if (mSigningCertificateLineage != null) {
+ SigningCertificateLineage subLineage =
+ mSigningCertificateLineage.getSubLineage(oldestConfig.mCertificates.get(0));
+ if (subLineage.size() != 1) {
+ throw new IllegalArgumentException("v2 signing enabled but the oldest signer in"
+ + " the SigningCertificateLineage is missing. Please provide"
+ + " the oldest signer to enable v2 signing.");
+ }
+ }
+ signerConfig.add(
+ createSigningBlockSignerConfig(
+ mSignerConfigs.get(0), apkSigningBlockPaddingSupported,
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2));
+ return signerConfig;
+ } else {
+ return createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported,
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
+ }
+ }
+
+ private List createV3SignerConfigs(
+ boolean apkSigningBlockPaddingSupported) throws InvalidKeyException {
+ List rawConfigs =
+ createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported,
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+
+ List processedConfigs = new ArrayList<>();
+
+ // we have our configs, now touch them up to appropriately cover all SDK levels since APK
+ // signature scheme v3 was introduced
+ int currentMinSdk = Integer.MAX_VALUE;
+ for (int i = rawConfigs.size() - 1; i >= 0; i--) {
+ ApkSigningBlockUtils.SignerConfig config = rawConfigs.get(i);
+ if (config.signatureAlgorithms == null) {
+ // no valid algorithm was found for this signer, and we haven't yet covered all
+ // platform versions, something's wrong
+ String keyAlgorithm = config.certificates.get(0).getPublicKey().getAlgorithm();
+ throw new InvalidKeyException("Unsupported key algorithm " + keyAlgorithm + " is "
+ + "not supported for APK Signature Scheme v3 signing");
+ }
+ if (i == rawConfigs.size() - 1) {
+ // first go through the loop, config should support all future platform versions.
+ // this assumes we don't deprecate support for signers in the future. If we do,
+ // this needs to change
+ config.maxSdkVersion = Integer.MAX_VALUE;
+ } else {
+ // otherwise, we only want to use this signer up to the minimum platform version
+ // on which a newer one is acceptable
+ config.maxSdkVersion = currentMinSdk - 1;
+ }
+ config.minSdkVersion = getMinSdkFromV3SignatureAlgorithms(config.signatureAlgorithms);
+ if (mSigningCertificateLineage != null) {
+ config.mSigningCertificateLineage =
+ mSigningCertificateLineage.getSubLineage(config.certificates.get(0));
+ }
+ // we know that this config will be used, so add it to our result, order doesn't matter
+ // at this point (and likely only one will be needed
+ processedConfigs.add(config);
+ currentMinSdk = config.minSdkVersion;
+ if (currentMinSdk <= mMinSdkVersion || currentMinSdk <= AndroidSdkVersion.P) {
+ // this satisfies all we need, stop here
+ break;
+ }
+ }
+ if (currentMinSdk > AndroidSdkVersion.P && currentMinSdk > mMinSdkVersion) {
+ // we can't cover all desired SDK versions, abort
+ throw new InvalidKeyException("Provided key algorithms not supported on all desired "
+ + "Android SDK versions");
+ }
+ return processedConfigs;
+ }
+
+ private int getMinSdkFromV3SignatureAlgorithms(List algorithms) {
+ int min = Integer.MAX_VALUE;
+ for (SignatureAlgorithm algorithm : algorithms) {
+ int current = algorithm.getMinSdkVersion();
+ if (current < min) {
+ if (current <= mMinSdkVersion || current <= AndroidSdkVersion.P) {
+ // this algorithm satisfies all of our needs, no need to keep looking
+ return current;
+ } else {
+ min = current;
+ }
+ }
+ }
+ return min;
+ }
+
+ private List createSigningBlockSignerConfigs(
+ boolean apkSigningBlockPaddingSupported, int schemeId) throws InvalidKeyException {
+ List signerConfigs =
+ new ArrayList<>(mSignerConfigs.size());
+ for (int i = 0; i < mSignerConfigs.size(); i++) {
+ SignerConfig signerConfig = mSignerConfigs.get(i);
+ signerConfigs.add(
+ createSigningBlockSignerConfig(
+ signerConfig, apkSigningBlockPaddingSupported, schemeId));
+ }
+ return signerConfigs;
+ }
+
+ private ApkSigningBlockUtils.SignerConfig createSigningBlockSignerConfig(
+ SignerConfig signerConfig, boolean apkSigningBlockPaddingSupported, int schemeId)
+ throws InvalidKeyException {
+ List certificates = signerConfig.getCertificates();
+ PublicKey publicKey = certificates.get(0).getPublicKey();
+
+ ApkSigningBlockUtils.SignerConfig newSignerConfig =
+ new ApkSigningBlockUtils.SignerConfig();
+ newSignerConfig.privateKey = signerConfig.getPrivateKey();
+ newSignerConfig.certificates = certificates;
+
+ switch (schemeId) {
+ case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2:
+ newSignerConfig.signatureAlgorithms =
+ V2SchemeSigner.getSuggestedSignatureAlgorithms(publicKey, mMinSdkVersion,
+ apkSigningBlockPaddingSupported);
+ break;
+ case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3:
+ try {
+ newSignerConfig.signatureAlgorithms =
+ V3SchemeSigner.getSuggestedSignatureAlgorithms(
+ publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported);
+ } catch (InvalidKeyException e) {
+
+ // It is possible for a signer used for v1/v2 signing to not be allowed for use
+ // with v3 signing. This is ok as long as there exists a more recent v3 signer
+ // that covers all supported platform versions. Populate signatureAlgorithm
+ // with null, it will be cleaned-up in a later step.
+ newSignerConfig.signatureAlgorithms = null;
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown APK Signature Scheme ID requested");
+ }
+ return newSignerConfig;
+ }
+
+ private boolean isDebuggable(String entryName) {
+ return mDebuggableApkPermitted
+ || !ApkUtils.ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(entryName);
+ }
+
+ /**
+ * Initializes DefaultApkSignerEngine with the existing MANIFEST.MF. This reads existing digests
+ * from the MANIFEST.MF file (they are assumed correct) and stores them for the final signature
+ * without recalculation. This step has a significant performance benefit in case of incremental
+ * build.
+ *
+ * This method extracts and stored computed digest for every entry that it would compute it for
+ * in the {@link #outputJarEntry(String)} method
+ *
+ * @param manifestBytes raw representation of MANIFEST.MF file
+ * @param entryNames a set of expected entries names
+ * @return set of entry names which were processed by the engine during the initialization, a
+ * subset of entryNames
+ */
+ @Override
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ public Set initWith(byte[] manifestBytes, Set entryNames) {
+ V1SchemeVerifier.Result dummyResult = new V1SchemeVerifier.Result();
+ Pair> sections =
+ V1SchemeVerifier.parseManifest(manifestBytes, entryNames, dummyResult);
+ String alg = V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm);
+ for (Map.Entry entry: sections.getSecond().entrySet()) {
+ String entryName = entry.getKey();
+ if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entry.getKey()) &&
+ isDebuggable(entryName)) {
+
+ Optional extractedDigest =
+ V1SchemeVerifier.getDigestsToVerify(
+ entry.getValue(), "-Digest", mMinSdkVersion, Integer.MAX_VALUE)
+ .stream()
+ .filter(d -> d.jcaDigestAlgorithm == alg)
+ .findFirst();
+
+ extractedDigest.ifPresent(
+ namedDigest -> mOutputJarEntryDigests.put(entryName, namedDigest.digest));
+ }
+ }
+ return mOutputJarEntryDigests.keySet();
+ }
+
+ @Override
+ public void setExecutor(RunnablesExecutor executor) {
+ mExecutor = executor;
+ }
+
+ @Override
+ public void inputApkSigningBlock(DataSource apkSigningBlock) {
+ checkNotClosed();
+
+ if ((apkSigningBlock == null) || (apkSigningBlock.size() == 0)) {
+ return;
+ }
+
+ if (mOtherSignersSignaturesPreserved) {
+ // TODO: Preserve blocks other than APK Signature Scheme v2 blocks of signers configured
+ // in this engine.
+ return;
+ }
+ // TODO: Preserve blocks other than APK Signature Scheme v2 blocks.
+ }
+
+ @Override
+ public InputJarEntryInstructions inputJarEntry(String entryName) {
+ checkNotClosed();
+
+ InputJarEntryInstructions.OutputPolicy outputPolicy =
+ getInputJarEntryOutputPolicy(entryName);
+ switch (outputPolicy) {
+ case SKIP:
+ return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.SKIP);
+ case OUTPUT:
+ return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.OUTPUT);
+ case OUTPUT_BY_ENGINE:
+ if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) {
+ // We copy the main section of the JAR manifest from input to output. Thus, this
+ // invalidates v1 signature and we need to see the entry's data.
+ mInputJarManifestEntryDataRequest = new GetJarEntryDataRequest(entryName);
+ return new InputJarEntryInstructions(
+ InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE,
+ mInputJarManifestEntryDataRequest);
+ }
+ return new InputJarEntryInstructions(
+ InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE);
+ default:
+ throw new RuntimeException("Unsupported output policy: " + outputPolicy);
+ }
+ }
+
+ @Override
+ public InspectJarEntryRequest outputJarEntry(String entryName) {
+ checkNotClosed();
+ invalidateV2Signature();
+
+ if (!isDebuggable(entryName)) {
+ forgetOutputApkDebuggableStatus();
+ }
+
+ if (!mV1SigningEnabled) {
+ // No need to inspect JAR entries when v1 signing is not enabled.
+ if (!isDebuggable(entryName)) {
+ // To reject debuggable APKs we need to inspect the APK's AndroidManifest.xml to
+ // check whether it declares that the APK is debuggable
+ mOutputAndroidManifestEntryDataRequest = new GetJarEntryDataRequest(entryName);
+ return mOutputAndroidManifestEntryDataRequest;
+ }
+ return null;
+ }
+ // v1 signing is enabled
+
+ if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName)) {
+ // This entry is covered by v1 signature. We thus need to inspect the entry's data to
+ // compute its digest(s) for v1 signature.
+
+ // TODO: Handle the case where other signer's v1 signatures are present and need to be
+ // preserved. In that scenario we can't modify MANIFEST.MF and add/remove JAR entries
+ // covered by v1 signature.
+ invalidateV1Signature();
+ GetJarEntryDataDigestRequest dataDigestRequest =
+ new GetJarEntryDataDigestRequest(
+ entryName,
+ V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm));
+ mOutputJarEntryDigestRequests.put(entryName, dataDigestRequest);
+ mOutputJarEntryDigests.remove(entryName);
+
+ if ((!mDebuggableApkPermitted)
+ && (ApkUtils.ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(entryName))) {
+ // To reject debuggable APKs we need to inspect the APK's AndroidManifest.xml to
+ // check whether it declares that the APK is debuggable
+ mOutputAndroidManifestEntryDataRequest = new GetJarEntryDataRequest(entryName);
+ return new CompoundInspectJarEntryRequest(
+ entryName, mOutputAndroidManifestEntryDataRequest, dataDigestRequest);
+ }
+
+ return dataDigestRequest;
+ }
+
+ if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
+ // This entry is part of v1 signature generated by this engine. We need to check whether
+ // the entry's data is as output by the engine.
+ invalidateV1Signature();
+ GetJarEntryDataRequest dataRequest;
+ if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) {
+ dataRequest = new GetJarEntryDataRequest(entryName);
+ mInputJarManifestEntryDataRequest = dataRequest;
+ } else {
+ // If this entry is part of v1 signature which has been emitted by this engine,
+ // check whether the output entry's data matches what the engine emitted.
+ dataRequest =
+ (mEmittedSignatureJarEntryData.containsKey(entryName))
+ ? new GetJarEntryDataRequest(entryName) : null;
+ }
+
+ if (dataRequest != null) {
+ mOutputSignatureJarEntryDataRequests.put(entryName, dataRequest);
+ }
+ return dataRequest;
+ }
+
+ // This entry is not covered by v1 signature and isn't part of v1 signature.
+ return null;
+ }
+
+ @Override
+ public InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName) {
+ checkNotClosed();
+ return getInputJarEntryOutputPolicy(entryName);
+ }
+
+ @Override
+ public void outputJarEntryRemoved(String entryName) {
+ checkNotClosed();
+ invalidateV2Signature();
+ if (!mV1SigningEnabled) {
+ return;
+ }
+
+ if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName)) {
+ // This entry is covered by v1 signature.
+ invalidateV1Signature();
+ mOutputJarEntryDigests.remove(entryName);
+ mOutputJarEntryDigestRequests.remove(entryName);
+ mOutputSignatureJarEntryDataRequests.remove(entryName);
+ return;
+ }
+
+ if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
+ // This entry is part of the v1 signature generated by this engine.
+ invalidateV1Signature();
+ return;
+ }
+ }
+
+ @Override
+ public OutputJarSignatureRequest outputJarEntries()
+ throws ApkFormatException, InvalidKeyException, SignatureException,
+ NoSuchAlgorithmException {
+ checkNotClosed();
+
+ if (!mV1SignaturePending) {
+ return null;
+ }
+
+ if ((mInputJarManifestEntryDataRequest != null)
+ && (!mInputJarManifestEntryDataRequest.isDone())) {
+ throw new IllegalStateException(
+ "Still waiting to inspect input APK's "
+ + mInputJarManifestEntryDataRequest.getEntryName());
+ }
+
+ for (GetJarEntryDataDigestRequest digestRequest
+ : mOutputJarEntryDigestRequests.values()) {
+ String entryName = digestRequest.getEntryName();
+ if (!digestRequest.isDone()) {
+ throw new IllegalStateException(
+ "Still waiting to inspect output APK's " + entryName);
+ }
+ mOutputJarEntryDigests.put(entryName, digestRequest.getDigest());
+ }
+ mOutputJarEntryDigestRequests.clear();
+
+ for (GetJarEntryDataRequest dataRequest : mOutputSignatureJarEntryDataRequests.values()) {
+ if (!dataRequest.isDone()) {
+ throw new IllegalStateException(
+ "Still waiting to inspect output APK's " + dataRequest.getEntryName());
+ }
+ }
+
+ List apkSigningSchemeIds = new ArrayList<>();
+ if (mV2SigningEnabled) {
+ apkSigningSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
+ }
+ if (mV3SigningEnabled) {
+ apkSigningSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+ }
+ byte[] inputJarManifest =
+ (mInputJarManifestEntryDataRequest != null)
+ ? mInputJarManifestEntryDataRequest.getData() : null;
+
+ // Check whether the most recently used signature (if present) is still fine.
+ checkOutputApkNotDebuggableIfDebuggableMustBeRejected();
+ List> signatureZipEntries;
+ if ((mAddV1SignatureRequest == null) || (!mAddV1SignatureRequest.isDone())) {
+ try {
+ signatureZipEntries =
+ V1SchemeSigner.sign(
+ mV1SignerConfigs,
+ mV1ContentDigestAlgorithm,
+ mOutputJarEntryDigests,
+ apkSigningSchemeIds,
+ inputJarManifest,
+ mCreatedBy);
+ } catch (CertificateException e) {
+ throw new SignatureException("Failed to generate v1 signature", e);
+ }
+ } else {
+ V1SchemeSigner.OutputManifestFile newManifest =
+ V1SchemeSigner.generateManifestFile(
+ mV1ContentDigestAlgorithm,
+ mOutputJarEntryDigests,
+ inputJarManifest);
+ byte[] emittedSignatureManifest =
+ mEmittedSignatureJarEntryData.get(V1SchemeSigner.MANIFEST_ENTRY_NAME);
+ if (!Arrays.equals(newManifest.contents, emittedSignatureManifest)) {
+ // Emitted v1 signature is no longer valid.
+ try {
+ signatureZipEntries =
+ V1SchemeSigner.signManifest(
+ mV1SignerConfigs,
+ mV1ContentDigestAlgorithm,
+ apkSigningSchemeIds,
+ mCreatedBy,
+ newManifest);
+ } catch (CertificateException e) {
+ throw new SignatureException("Failed to generate v1 signature", e);
+ }
+ } else {
+ // Emitted v1 signature is still valid. Check whether the signature is there in the
+ // output.
+ signatureZipEntries = new ArrayList<>();
+ for (Map.Entry expectedOutputEntry
+ : mEmittedSignatureJarEntryData.entrySet()) {
+ String entryName = expectedOutputEntry.getKey();
+ byte[] expectedData = expectedOutputEntry.getValue();
+ GetJarEntryDataRequest actualDataRequest =
+ mOutputSignatureJarEntryDataRequests.get(entryName);
+ if (actualDataRequest == null) {
+ // This signature entry hasn't been output.
+ signatureZipEntries.add(Pair.of(entryName, expectedData));
+ continue;
+ }
+ byte[] actualData = actualDataRequest.getData();
+ if (!Arrays.equals(expectedData, actualData)) {
+ signatureZipEntries.add(Pair.of(entryName, expectedData));
+ }
+ }
+ if (signatureZipEntries.isEmpty()) {
+ // v1 signature in the output is valid
+ return null;
+ }
+ // v1 signature in the output is not valid.
+ }
+ }
+
+ if (signatureZipEntries.isEmpty()) {
+ // v1 signature in the output is valid
+ mV1SignaturePending = false;
+ return null;
+ }
+
+ List sigEntries =
+ new ArrayList<>(signatureZipEntries.size());
+ for (Pair entry : signatureZipEntries) {
+ String entryName = entry.getFirst();
+ byte[] entryData = entry.getSecond();
+ sigEntries.add(new OutputJarSignatureRequest.JarEntry(entryName, entryData));
+ mEmittedSignatureJarEntryData.put(entryName, entryData);
+ }
+ mAddV1SignatureRequest = new OutputJarSignatureRequestImpl(sigEntries);
+ return mAddV1SignatureRequest;
+ }
+
+ @Deprecated
+ @Override
+ public OutputApkSigningBlockRequest outputZipSections(
+ DataSource zipEntries,
+ DataSource zipCentralDirectory,
+ DataSource zipEocd)
+ throws IOException, InvalidKeyException, SignatureException,
+ NoSuchAlgorithmException {
+ return outputZipSectionsInternal(zipEntries, zipCentralDirectory, zipEocd, false);
+ }
+
+ @Override
+ public OutputApkSigningBlockRequest2 outputZipSections2(
+ DataSource zipEntries,
+ DataSource zipCentralDirectory,
+ DataSource zipEocd)
+ throws IOException, InvalidKeyException, SignatureException,
+ NoSuchAlgorithmException {
+ return outputZipSectionsInternal(zipEntries, zipCentralDirectory, zipEocd, true);
+ }
+
+ private OutputApkSigningBlockRequestImpl outputZipSectionsInternal(
+ DataSource zipEntries,
+ DataSource zipCentralDirectory,
+ DataSource zipEocd,
+ boolean apkSigningBlockPaddingSupported)
+ throws IOException, InvalidKeyException, SignatureException,
+ NoSuchAlgorithmException {
+ checkNotClosed();
+ checkV1SigningDoneIfEnabled();
+ if (!mV2SigningEnabled && !mV3SigningEnabled) {
+ return null;
+ }
+ checkOutputApkNotDebuggableIfDebuggableMustBeRejected();
+
+ // adjust to proper padding
+ Pair paddingPair =
+ ApkSigningBlockUtils.generateApkSigningBlockPadding(zipEntries,
+ apkSigningBlockPaddingSupported);
+ DataSource beforeCentralDir = paddingPair.getFirst();
+ int padSizeBeforeApkSigningBlock = paddingPair.getSecond();
+ DataSource eocd =
+ ApkSigningBlockUtils.copyWithModifiedCDOffset(beforeCentralDir, zipEocd);
+
+ List> signingSchemeBlocks = new ArrayList<>();
+
+ // create APK Signature Scheme V2 Signature if requested
+ if (mV2SigningEnabled) {
+ invalidateV2Signature();
+ List v2SignerConfigs =
+ createV2SignerConfigs(apkSigningBlockPaddingSupported);
+ signingSchemeBlocks.add(
+ V2SchemeSigner.generateApkSignatureSchemeV2Block(
+ mExecutor,
+ beforeCentralDir,
+ zipCentralDirectory,
+ eocd,
+ v2SignerConfigs,
+ mV3SigningEnabled));
+ }
+ if (mV3SigningEnabled) {
+ invalidateV3Signature();
+ List v3SignerConfigs =
+ createV3SignerConfigs(apkSigningBlockPaddingSupported);
+ signingSchemeBlocks.add(
+ V3SchemeSigner.generateApkSignatureSchemeV3Block(
+ mExecutor,
+ beforeCentralDir,
+ zipCentralDirectory,
+ eocd,
+ v3SignerConfigs));
+ }
+
+ // create APK Signing Block with v2 and/or v3 blocks
+ byte[] apkSigningBlock =
+ ApkSigningBlockUtils.generateApkSigningBlock(signingSchemeBlocks);
+
+ mAddSigningBlockRequest = new OutputApkSigningBlockRequestImpl(apkSigningBlock,
+ padSizeBeforeApkSigningBlock);
+ return mAddSigningBlockRequest;
+ }
+
+ @Override
+ public void outputDone() {
+ checkNotClosed();
+ checkV1SigningDoneIfEnabled();
+ checkSigningBlockDoneIfEnabled();
+ }
+
+ @Override
+ public void close() {
+ mClosed = true;
+
+ mAddV1SignatureRequest = null;
+ mInputJarManifestEntryDataRequest = null;
+ mOutputAndroidManifestEntryDataRequest = null;
+ mDebuggable = null;
+ mOutputJarEntryDigestRequests.clear();
+ mOutputJarEntryDigests.clear();
+ mEmittedSignatureJarEntryData.clear();
+ mOutputSignatureJarEntryDataRequests.clear();
+
+ mAddSigningBlockRequest = null;
+ }
+
+ private void invalidateV1Signature() {
+ if (mV1SigningEnabled) {
+ mV1SignaturePending = true;
+ }
+ invalidateV2Signature();
+ }
+
+ private void invalidateV2Signature() {
+ if (mV2SigningEnabled) {
+ mV2SignaturePending = true;
+ mAddSigningBlockRequest = null;
+ }
+ }
+
+ private void invalidateV3Signature() {
+ if (mV3SigningEnabled) {
+ mV3SignaturePending = true;
+ mAddSigningBlockRequest = null;
+ }
+ }
+
+ private void checkNotClosed() {
+ if (mClosed) {
+ throw new IllegalStateException("Engine closed");
+ }
+ }
+
+ private void checkV1SigningDoneIfEnabled() {
+ if (!mV1SignaturePending) {
+ return;
+ }
+
+ if (mAddV1SignatureRequest == null) {
+ throw new IllegalStateException(
+ "v1 signature (JAR signature) not yet generated. Skipped outputJarEntries()?");
+ }
+ if (!mAddV1SignatureRequest.isDone()) {
+ throw new IllegalStateException(
+ "v1 signature (JAR signature) addition requested by outputJarEntries() hasn't"
+ + " been fulfilled");
+ }
+ for (Map.Entry expectedOutputEntry
+ : mEmittedSignatureJarEntryData.entrySet()) {
+ String entryName = expectedOutputEntry.getKey();
+ byte[] expectedData = expectedOutputEntry.getValue();
+ GetJarEntryDataRequest actualDataRequest =
+ mOutputSignatureJarEntryDataRequests.get(entryName);
+ if (actualDataRequest == null) {
+ throw new IllegalStateException(
+ "APK entry " + entryName + " not yet output despite this having been"
+ + " requested");
+ } else if (!actualDataRequest.isDone()) {
+ throw new IllegalStateException(
+ "Still waiting to inspect output APK's " + entryName);
+ }
+ byte[] actualData = actualDataRequest.getData();
+ if (!Arrays.equals(expectedData, actualData)) {
+ throw new IllegalStateException(
+ "Output APK entry " + entryName + " data differs from what was requested");
+ }
+ }
+ mV1SignaturePending = false;
+ }
+
+ private void checkSigningBlockDoneIfEnabled() {
+ if (!mV2SignaturePending && !mV3SignaturePending) {
+ return;
+ }
+ if (mAddSigningBlockRequest == null) {
+ throw new IllegalStateException(
+ "Signed APK Signing BLock not yet generated. Skipped outputZipSections()?");
+ }
+ if (!mAddSigningBlockRequest.isDone()) {
+ throw new IllegalStateException(
+ "APK Signing Block addition of signature(s) requested by"
+ + " outputZipSections() hasn't been fulfilled yet");
+ }
+ mAddSigningBlockRequest = null;
+ mV2SignaturePending = false;
+ mV3SignaturePending = false;
+ }
+
+ private void checkOutputApkNotDebuggableIfDebuggableMustBeRejected()
+ throws SignatureException {
+ if (mDebuggableApkPermitted) {
+ return;
+ }
+
+ try {
+ if (isOutputApkDebuggable()) {
+ throw new SignatureException(
+ "APK is debuggable (see android:debuggable attribute) and this engine is"
+ + " configured to refuse to sign debuggable APKs");
+ }
+ } catch (ApkFormatException e) {
+ throw new SignatureException("Failed to determine whether the APK is debuggable", e);
+ }
+ }
+
+ /**
+ * Returns whether the output APK is debuggable according to its
+ * {@code android:debuggable} declaration.
+ */
+ private boolean isOutputApkDebuggable() throws ApkFormatException {
+ if (mDebuggable != null) {
+ return mDebuggable;
+ }
+
+ if (mOutputAndroidManifestEntryDataRequest == null) {
+ throw new IllegalStateException(
+ "Cannot determine debuggable status of output APK because "
+ + ApkUtils.ANDROID_MANIFEST_ZIP_ENTRY_NAME
+ + " entry contents have not yet been requested");
+ }
+
+ if (!mOutputAndroidManifestEntryDataRequest.isDone()) {
+ throw new IllegalStateException(
+ "Still waiting to inspect output APK's "
+ + mOutputAndroidManifestEntryDataRequest.getEntryName());
+ }
+ mDebuggable =
+ ApkUtils.getDebuggableFromBinaryAndroidManifest(
+ ByteBuffer.wrap(mOutputAndroidManifestEntryDataRequest.getData()));
+ return mDebuggable;
+ }
+
+ private void forgetOutputApkDebuggableStatus() {
+ mDebuggable = null;
+ }
+
+ /**
+ * Returns the output policy for the provided input JAR entry.
+ */
+ private InputJarEntryInstructions.OutputPolicy getInputJarEntryOutputPolicy(String entryName) {
+ if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
+ return InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE;
+ }
+ if ((mOtherSignersSignaturesPreserved)
+ || (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName))) {
+ return InputJarEntryInstructions.OutputPolicy.OUTPUT;
+ }
+ return InputJarEntryInstructions.OutputPolicy.SKIP;
+ }
+
+ private static class OutputJarSignatureRequestImpl implements OutputJarSignatureRequest {
+ private final List mAdditionalJarEntries;
+ private volatile boolean mDone;
+
+ private OutputJarSignatureRequestImpl(List additionalZipEntries) {
+ mAdditionalJarEntries =
+ Collections.unmodifiableList(new ArrayList<>(additionalZipEntries));
+ }
+
+ @Override
+ public List getAdditionalJarEntries() {
+ return mAdditionalJarEntries;
+ }
+
+ @Override
+ public void done() {
+ mDone = true;
+ }
+
+ private boolean isDone() {
+ return mDone;
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private static class OutputApkSigningBlockRequestImpl
+ implements OutputApkSigningBlockRequest, OutputApkSigningBlockRequest2 {
+ private final byte[] mApkSigningBlock;
+ private final int mPaddingBeforeApkSigningBlock;
+ private volatile boolean mDone;
+
+ private OutputApkSigningBlockRequestImpl(byte[] apkSigingBlock, int paddingBefore) {
+ mApkSigningBlock = apkSigingBlock.clone();
+ mPaddingBeforeApkSigningBlock = paddingBefore;
+ }
+
+ @Override
+ public byte[] getApkSigningBlock() {
+ return mApkSigningBlock.clone();
+ }
+
+ @Override
+ public void done() {
+ mDone = true;
+ }
+
+ private boolean isDone() {
+ return mDone;
+ }
+
+ @Override
+ public int getPaddingSizeBeforeApkSigningBlock() {
+ return mPaddingBeforeApkSigningBlock;
+ }
+ }
+
+ /**
+ * JAR entry inspection request which obtain the entry's uncompressed data.
+ */
+ private static class GetJarEntryDataRequest implements InspectJarEntryRequest {
+ private final String mEntryName;
+ private final Object mLock = new Object();
+
+ private boolean mDone;
+ private DataSink mDataSink;
+ private ByteArrayOutputStream mDataSinkBuf;
+
+ private GetJarEntryDataRequest(String entryName) {
+ mEntryName = entryName;
+ }
+
+ @Override
+ public String getEntryName() {
+ return mEntryName;
+ }
+
+ @Override
+ public DataSink getDataSink() {
+ synchronized (mLock) {
+ checkNotDone();
+ if (mDataSinkBuf == null) {
+ mDataSinkBuf = new ByteArrayOutputStream();
+ }
+ if (mDataSink == null) {
+ mDataSink = DataSinks.asDataSink(mDataSinkBuf);
+ }
+ return mDataSink;
+ }
+ }
+
+ @Override
+ public void done() {
+ synchronized (mLock) {
+ if (mDone) {
+ return;
+ }
+ mDone = true;
+ }
+ }
+
+ private boolean isDone() {
+ synchronized (mLock) {
+ return mDone;
+ }
+ }
+
+ private void checkNotDone() throws IllegalStateException {
+ synchronized (mLock) {
+ if (mDone) {
+ throw new IllegalStateException("Already done");
+ }
+ }
+ }
+
+ private byte[] getData() {
+ synchronized (mLock) {
+ if (!mDone) {
+ throw new IllegalStateException("Not yet done");
+ }
+ return (mDataSinkBuf != null) ? mDataSinkBuf.toByteArray() : new byte[0];
+ }
+ }
+ }
+
+ /**
+ * JAR entry inspection request which obtains the digest of the entry's uncompressed data.
+ */
+ private static class GetJarEntryDataDigestRequest implements InspectJarEntryRequest {
+ private final String mEntryName;
+ private final String mJcaDigestAlgorithm;
+ private final Object mLock = new Object();
+
+ private boolean mDone;
+ private DataSink mDataSink;
+ private MessageDigest mMessageDigest;
+ private byte[] mDigest;
+
+ private GetJarEntryDataDigestRequest(String entryName, String jcaDigestAlgorithm) {
+ mEntryName = entryName;
+ mJcaDigestAlgorithm = jcaDigestAlgorithm;
+ }
+
+ @Override
+ public String getEntryName() {
+ return mEntryName;
+ }
+
+ @Override
+ public DataSink getDataSink() {
+ synchronized (mLock) {
+ checkNotDone();
+ if (mDataSink == null) {
+ mDataSink = DataSinks.asDataSink(getMessageDigest());
+ }
+ return mDataSink;
+ }
+ }
+
+ private MessageDigest getMessageDigest() {
+ synchronized (mLock) {
+ if (mMessageDigest == null) {
+ try {
+ mMessageDigest = MessageDigest.getInstance(mJcaDigestAlgorithm);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(
+ mJcaDigestAlgorithm + " MessageDigest not available", e);
+ }
+ }
+ return mMessageDigest;
+ }
+ }
+
+ @Override
+ public void done() {
+ synchronized (mLock) {
+ if (mDone) {
+ return;
+ }
+ mDone = true;
+ mDigest = getMessageDigest().digest();
+ mMessageDigest = null;
+ mDataSink = null;
+ }
+ }
+
+ private boolean isDone() {
+ synchronized (mLock) {
+ return mDone;
+ }
+ }
+
+ private void checkNotDone() throws IllegalStateException {
+ synchronized (mLock) {
+ if (mDone) {
+ throw new IllegalStateException("Already done");
+ }
+ }
+ }
+
+ private byte[] getDigest() {
+ synchronized (mLock) {
+ if (!mDone) {
+ throw new IllegalStateException("Not yet done");
+ }
+ return mDigest.clone();
+ }
+ }
+ }
+
+ /**
+ * JAR entry inspection request which transparently satisfies multiple such requests.
+ */
+ private static class CompoundInspectJarEntryRequest implements InspectJarEntryRequest {
+ private final String mEntryName;
+ private final InspectJarEntryRequest[] mRequests;
+ private final Object mLock = new Object();
+
+ private DataSink mSink;
+
+ private CompoundInspectJarEntryRequest(
+ String entryName, InspectJarEntryRequest... requests) {
+ mEntryName = entryName;
+ mRequests = requests;
+ }
+
+ @Override
+ public String getEntryName() {
+ return mEntryName;
+ }
+
+ @Override
+ public DataSink getDataSink() {
+ synchronized (mLock) {
+ if (mSink == null) {
+ DataSink[] sinks = new DataSink[mRequests.length];
+ for (int i = 0; i < sinks.length; i++) {
+ sinks[i] = mRequests[i].getDataSink();
+ }
+ mSink = new TeeDataSink(sinks);
+ }
+ return mSink;
+ }
+ }
+
+ @Override
+ public void done() {
+ for (InspectJarEntryRequest request : mRequests) {
+ request.done();
+ }
+ }
+ }
+
+ /**
+ * Configuration of a signer.
+ *
+ * Use {@link Builder} to obtain configuration instances.
+ */
+ public static class SignerConfig {
+ private final String mName;
+ private final PrivateKey mPrivateKey;
+ private final List mCertificates;
+
+ private SignerConfig(
+ String name,
+ PrivateKey privateKey,
+ List certificates) {
+ mName = name;
+ mPrivateKey = privateKey;
+ mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates));
+ }
+
+ /**
+ * Returns the name of this signer.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the signing key of this signer.
+ */
+ public PrivateKey getPrivateKey() {
+ return mPrivateKey;
+ }
+
+ /**
+ * Returns the certificate(s) of this signer. The first certificate's public key corresponds
+ * to this signer's private key.
+ */
+ public List getCertificates() {
+ return mCertificates;
+ }
+
+ /**
+ * Builder of {@link SignerConfig} instances.
+ */
+ public static class Builder {
+ private final String mName;
+ private final PrivateKey mPrivateKey;
+ private final List mCertificates;
+
+ /**
+ * Constructs a new {@code Builder}.
+ *
+ * @param name signer's name. The name is reflected in the name of files comprising the
+ * JAR signature of the APK.
+ * @param privateKey signing key
+ * @param certificates list of one or more X.509 certificates. The subject public key of
+ * the first certificate must correspond to the {@code privateKey}.
+ */
+ public Builder(
+ String name,
+ PrivateKey privateKey,
+ List certificates) {
+ if (name.isEmpty()) {
+ throw new IllegalArgumentException("Empty name");
+ }
+ mName = name;
+ mPrivateKey = privateKey;
+ mCertificates = new ArrayList<>(certificates);
+ }
+
+ /**
+ * Returns a new {@code SignerConfig} instance configured based on the configuration of
+ * this builder.
+ */
+ public SignerConfig build() {
+ return new SignerConfig(
+ mName,
+ mPrivateKey,
+ mCertificates);
+ }
+ }
+ }
+
+ /**
+ * Builder of {@link DefaultApkSignerEngine} instances.
+ */
+ public static class Builder {
+ private List mSignerConfigs;
+ private final int mMinSdkVersion;
+
+ private boolean mV1SigningEnabled = true;
+ private boolean mV2SigningEnabled = true;
+ private boolean mV3SigningEnabled = true;
+ private boolean mDebuggableApkPermitted = true;
+ private boolean mOtherSignersSignaturesPreserved;
+ private String mCreatedBy = "1.0 (Android)";
+
+ private SigningCertificateLineage mSigningCertificateLineage;
+
+ // APK Signature Scheme v3 only supports a single signing certificate, so to move to v3
+ // signing by default, but not require prior clients to update to explicitly disable v3
+ // signing for multiple signers, we modify the mV3SigningEnabled depending on the provided
+ // inputs (multiple signers and mSigningCertificateLineage in particular). Maintain two
+ // extra variables to record whether or not mV3SigningEnabled has been set directly by a
+ // client and so should override the default behavior.
+ private boolean mV3SigningExplicitlyDisabled = false;
+ private boolean mV3SigningExplicitlyEnabled = false;
+
+ /**
+ * Constructs a new {@code Builder}.
+ *
+ * @param signerConfigs information about signers with which the APK will be signed. At
+ * least one signer configuration must be provided.
+ * @param minSdkVersion API Level of the oldest Android platform on which the APK is
+ * supposed to be installed. See {@code minSdkVersion} attribute in the APK's
+ * {@code AndroidManifest.xml}. The higher the version, the stronger signing features
+ * will be enabled.
+ */
+ public Builder(
+ List signerConfigs,
+ int minSdkVersion) {
+ if (signerConfigs.isEmpty()) {
+ throw new IllegalArgumentException("At least one signer config must be provided");
+ }
+ if (signerConfigs.size() > 1) {
+ // APK Signature Scheme v3 only supports single signer, unless a
+ // SigningCertificateLineage is provided, in which case this will be reset to true,
+ // since we don't yet have a v4 scheme about which to worry
+ mV3SigningEnabled = false;
+ }
+ mSignerConfigs = new ArrayList<>(signerConfigs);
+ mMinSdkVersion = minSdkVersion;
+ }
+
+ /**
+ * Returns a new {@code DefaultApkSignerEngine} instance configured based on the
+ * configuration of this builder.
+ */
+ public DefaultApkSignerEngine build() throws InvalidKeyException {
+
+ if (mV3SigningExplicitlyDisabled && mV3SigningExplicitlyEnabled) {
+ throw new IllegalStateException("Builder configured to both enable and disable APK "
+ + "Signature Scheme v3 signing");
+ }
+ if (mV3SigningExplicitlyDisabled) {
+ mV3SigningEnabled = false;
+ } else if (mV3SigningExplicitlyEnabled) {
+ mV3SigningEnabled = true;
+ }
+
+ // make sure our signers are appropriately setup
+ if (mSigningCertificateLineage != null) {
+ try {
+ mSignerConfigs = mSigningCertificateLineage.sortSignerConfigs(mSignerConfigs);
+ if (!mV3SigningEnabled && mSignerConfigs.size() > 1) {
+
+ // this is a strange situation: we've provided a valid rotation history, but
+ // are only signing with v1/v2. blow up, since we don't know for sure with
+ // which signer the user intended to sign
+ throw new IllegalStateException("Provided multiple signers which are part "
+ + "of the SigningCertificateLineage, but not signing with APK "
+ + "Signature Scheme v3");
+ }
+ } catch (IllegalArgumentException e) {
+ throw new IllegalStateException("Provided signer configs do not match the "
+ + "provided SigningCertificateLineage", e);
+ }
+ } else if (mV3SigningEnabled && mSignerConfigs.size() > 1) {
+ throw new IllegalStateException("Multiple signing certificates provided for use "
+ + "with APK Signature Scheme v3 without an accompanying SigningCertificateLineage");
+ }
+
+ return new DefaultApkSignerEngine(
+ mSignerConfigs,
+ mMinSdkVersion,
+ mV1SigningEnabled,
+ mV2SigningEnabled,
+ mV3SigningEnabled,
+ mDebuggableApkPermitted,
+ mOtherSignersSignaturesPreserved,
+ mCreatedBy,
+ mSigningCertificateLineage);
+ }
+
+ /**
+ * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
+ *
+ * By default, the APK will be signed using this scheme.
+ */
+ public Builder setV1SigningEnabled(boolean enabled) {
+ mV1SigningEnabled = enabled;
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should be signed using APK Signature Scheme v2 (aka v2 signature
+ * scheme).
+ *
+ *
By default, the APK will be signed using this scheme.
+ */
+ public Builder setV2SigningEnabled(boolean enabled) {
+ mV2SigningEnabled = enabled;
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should be signed using APK Signature Scheme v3 (aka v3 signature
+ * scheme).
+ *
+ *
By default, the APK will be signed using this scheme.
+ */
+ public Builder setV3SigningEnabled(boolean enabled) {
+ mV3SigningEnabled = enabled;
+ if (enabled) {
+ mV3SigningExplicitlyEnabled = true;
+ } else {
+ mV3SigningExplicitlyDisabled = true;
+ }
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should be signed even if it is marked as debuggable
+ * ({@code android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward
+ * compatibility reasons, the default value of this setting is {@code true}.
+ *
+ *
It is dangerous to sign debuggable APKs with production/release keys because Android
+ * platform loosens security checks for such APKs. For example, arbitrary unauthorized code
+ * may be executed in the context of such an app by anybody with ADB shell access.
+ */
+ public Builder setDebuggableApkPermitted(boolean permitted) {
+ mDebuggableApkPermitted = permitted;
+ return this;
+ }
+
+ /**
+ * Sets whether signatures produced by signers other than the ones configured in this engine
+ * should be copied from the input APK to the output APK.
+ *
+ *
By default, signatures of other signers are omitted from the output APK.
+ */
+ public Builder setOtherSignersSignaturesPreserved(boolean preserved) {
+ mOtherSignersSignaturesPreserved = preserved;
+ return this;
+ }
+
+ /**
+ * Sets the value of the {@code Created-By} field in JAR signature files.
+ */
+ public Builder setCreatedBy(String createdBy) {
+ if (createdBy == null) {
+ throw new NullPointerException();
+ }
+ mCreatedBy = createdBy;
+ return this;
+ }
+
+ /**
+ * Sets the {@link SigningCertificateLineage} to use with the v3 signature scheme. This
+ * structure provides proof of signing certificate rotation linking {@link SignerConfig}
+ * objects to previous ones.
+ */
+ public Builder setSigningCertificateLineage(
+ SigningCertificateLineage signingCertificateLineage) {
+ if (signingCertificateLineage != null) {
+ mV3SigningEnabled = true;
+ mSigningCertificateLineage = signingCertificateLineage;
+ }
+ return this;
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/Hints.java b/app/src/main/java/com/android/apksig/Hints.java
new file mode 100644
index 0000000..49ef2b0
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/Hints.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2018 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;
+import java.io.IOException;
+import java.io.DataOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public final class Hints {
+ /**
+ * Name of hint pattern asset file in APK.
+ */
+ public static final String PIN_HINT_ASSET_ZIP_ENTRY_NAME = "assets/com.android.hints.pins.txt";
+
+ /**
+ * Name of hint byte range data file in APK. Keep in sync with PinnerService.java.
+ */
+ public static final String PIN_BYTE_RANGE_ZIP_ENTRY_NAME = "pinlist.meta";
+
+ private static int clampToInt(long value) {
+ return (int) Math.max(0, Math.min(value, Integer.MAX_VALUE));
+ }
+
+ public static final class ByteRange {
+ final long start;
+ final long end;
+
+ public ByteRange(long start, long end) {
+ this.start = start;
+ this.end = end;
+ }
+ }
+
+ /**
+ * Create a blob of bytes that PinnerService understands as a
+ * sequence of byte ranges to pin.
+ */
+ public static byte[] encodeByteRangeList(List pinByteRanges) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream(pinByteRanges.size() * 8);
+ DataOutputStream out = new DataOutputStream(bos);
+ try {
+ for (ByteRange pinByteRange : pinByteRanges) {
+ out.writeInt(clampToInt(pinByteRange.start));
+ out.writeInt(clampToInt(pinByteRange.end - pinByteRange.start));
+ }
+ } catch (IOException ex) {
+ throw new AssertionError("impossible", ex);
+ }
+ return bos.toByteArray();
+ }
+
+ public static ArrayList parsePinPatterns(byte[] patternBlob) {
+ ArrayList pinPatterns = new ArrayList<>();
+ try {
+ for (String rawLine : new String(patternBlob, "UTF-8").split("\n")) {
+ String line = rawLine.replaceFirst("#.*", ""); // # starts a comment
+ if (!("".equals(line))) {
+ pinPatterns.add(Pattern.compile(line));
+ }
+ }
+ } catch (UnsupportedEncodingException ex) {
+ throw new RuntimeException("UTF-8 must be supported", ex);
+ }
+ return pinPatterns;
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/SigningCertificateLineage.java b/app/src/main/java/com/android/apksig/SigningCertificateLineage.java
new file mode 100644
index 0000000..54340d7
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/SigningCertificateLineage.java
@@ -0,0 +1,1086 @@
+/*
+ * Copyright (C) 2018 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;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.v3.V3SchemeSigner;
+import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage;
+import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage.SigningCertificateNode;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.util.RandomAccessFileDataSink;
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apksig.zip.ZipFormatException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * APK Signer Lineage.
+ *
+ * The signer lineage contains a history of signing certificates with each ancestor attesting to
+ * the validity of its descendant. Each additional descendant represents a new identity that can be
+ * used to sign an APK, and each generation has accompanying attributes which represent how the
+ * APK would like to view the older signing certificates, specifically how they should be trusted in
+ * certain situations.
+ *
+ *
Its primary use is to enable APK Signing Certificate Rotation. The Android platform verifies
+ * the APK Signer Lineage, and if the current signing certificate for the APK is in the Signer
+ * Lineage, and the Lineage contains the certificate the platform associates with the APK, it will
+ * allow upgrades to the new certificate.
+ *
+ * @see Application Signing
+ */
+public class SigningCertificateLineage {
+
+ public final static int MAGIC = 0x3eff39d1;
+
+ private final static int FIRST_VERSION = 1;
+
+ private static final int CURRENT_VERSION = FIRST_VERSION;
+
+ /** accept data from already installed pkg with this cert */
+ private static final int PAST_CERT_INSTALLED_DATA = 1;
+
+ /** accept sharedUserId with pkg with this cert */
+ private static final int PAST_CERT_SHARED_USER_ID = 2;
+
+ /** grant SIGNATURE permissions to pkgs with this cert */
+ private static final int PAST_CERT_PERMISSION = 4;
+
+ /**
+ * Enable updates back to this certificate. WARNING: this effectively removes any benefit of
+ * signing certificate changes, since a compromised key could retake control of an app even
+ * after change, and should only be used if there is a problem encountered when trying to ditch
+ * an older cert.
+ */
+ private static final int PAST_CERT_ROLLBACK = 8;
+
+ /**
+ * Preserve authenticator module-based access in AccountManager gated by signing certificate.
+ */
+ private static final int PAST_CERT_AUTH = 16;
+
+ private final int mMinSdkVersion;
+
+ /**
+ * The signing lineage is just a list of nodes, with the first being the original signing
+ * certificate and the most recent being the one with which the APK is to actually be signed.
+ */
+ private final List mSigningLineage;
+
+ private SigningCertificateLineage(int minSdkVersion, List list) {
+ mMinSdkVersion = minSdkVersion;
+ mSigningLineage = list;
+ }
+
+ private static SigningCertificateLineage createSigningLineage(
+ int minSdkVersion, SignerConfig parent, SignerCapabilities parentCapabilities,
+ SignerConfig child, SignerCapabilities childCapabilities)
+ throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException,
+ SignatureException {
+ SigningCertificateLineage signingCertificateLineage =
+ new SigningCertificateLineage(minSdkVersion, new ArrayList<>());
+ signingCertificateLineage =
+ signingCertificateLineage.spawnFirstDescendant(parent, parentCapabilities);
+ return signingCertificateLineage.spawnDescendant(parent, child, childCapabilities);
+ }
+
+ public static SigningCertificateLineage readFromFile(File file)
+ throws IOException {
+ if (file == null) {
+ throw new NullPointerException("file == null");
+ }
+ RandomAccessFile inputFile = new RandomAccessFile(file, "r");
+ return readFromDataSource(DataSources.asDataSource(inputFile));
+ }
+
+ public static SigningCertificateLineage readFromDataSource(DataSource dataSource)
+ throws IOException {
+ if (dataSource == null) {
+ throw new NullPointerException("dataSource == null");
+ }
+ ByteBuffer inBuff = dataSource.getByteBuffer(0, (int) dataSource.size());
+ inBuff.order(ByteOrder.LITTLE_ENDIAN);
+ return read(inBuff);
+ }
+
+ /**
+ * Extracts a Signing Certificate Lineage from a v3 signer proof-of-rotation attribute.
+ *
+ *
+ * this may not give a complete representation of an APK's signing certificate history,
+ * since the APK may have multiple signers corresponding to different platform versions.
+ * Use readFromApkFile
to handle this case.
+ *
+ * @param attrValue
+ */
+ public static SigningCertificateLineage readFromV3AttributeValue(byte[] attrValue)
+ throws IOException {
+ List parsedLineage =
+ V3SigningCertificateLineage.readSigningCertificateLineage(ByteBuffer.wrap(
+ attrValue).order(ByteOrder.LITTLE_ENDIAN));
+ int minSdkVersion = calculateMinSdkVersion(parsedLineage);
+ return new SigningCertificateLineage(minSdkVersion, parsedLineage);
+ }
+
+ /**
+ * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3
+ * signature block of the provided APK File.
+ *
+ * @throws IllegalArgumentException if the provided APK does not contain a V3 signature block,
+ * or if the V3 signature block does not contain a valid lineage.
+ */
+ public static SigningCertificateLineage readFromApkFile(File apkFile)
+ throws IOException, ApkFormatException {
+ try (RandomAccessFile f = new RandomAccessFile(apkFile, "r")) {
+ DataSource apk = DataSources.asDataSource(f, 0, f.length());
+ return readFromApkDataSource(apk);
+ }
+ }
+
+ /**
+ * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3
+ * signature block of the provided APK DataSource.
+ *
+ * @throws IllegalArgumentException if the provided APK does not contain a V3 signature block,
+ * or if the V3 signature block does not contain a valid lineage.
+ */
+ public static SigningCertificateLineage readFromApkDataSource(DataSource apk)
+ throws IOException, ApkFormatException {
+ SignatureInfo signatureInfo;
+ try {
+ ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+ ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+ signatureInfo =
+ ApkSigningBlockUtils.findSignature(apk, zipSections,
+ V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException(e.getMessage());
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
+ throw new IllegalArgumentException(
+ "The provided APK does not contain a valid V3 signature block.");
+ }
+
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed signers:
+ // * length-prefixed signed data
+ // * minSDK
+ // * maxSDK
+ // * length-prefixed sequence of length-prefixed signatures
+ // * length-prefixed public key
+ ByteBuffer signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
+ List lineages = new ArrayList<>(1);
+ while (signers.hasRemaining()) {
+ ByteBuffer signer = getLengthPrefixedSlice(signers);
+ ByteBuffer signedData = getLengthPrefixedSlice(signer);
+ try {
+ SigningCertificateLineage lineage = readFromSignedData(signedData);
+ lineages.add(lineage);
+ } catch (IllegalArgumentException ignored) {
+ // The current signer block does not contain a valid lineage, but it is possible
+ // another block will.
+ }
+ }
+ SigningCertificateLineage result;
+ if (lineages.isEmpty()) {
+ throw new IllegalArgumentException(
+ "The provided APK does not contain a valid lineage.");
+ } else if (lineages.size() > 1) {
+ result = consolidateLineages(lineages);
+ } else {
+ result = lineages.get(0);
+ }
+ return result;
+ }
+
+ /**
+ * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the provided
+ * signed data portion of a signer in a V3 signature block.
+ *
+ * @throws IllegalArgumentException if the provided signed data does not contain a valid
+ * lineage.
+ */
+ public static SigningCertificateLineage readFromSignedData(ByteBuffer signedData)
+ throws IOException, ApkFormatException {
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed digests:
+ // * length-prefixed sequence of certificates:
+ // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
+ // * uint-32: minSdkVersion
+ // * uint-32: maxSdkVersion
+ // * length-prefixed sequence of length-prefixed additional attributes:
+ // * uint32: ID
+ // * (length - 4) bytes: value
+ // * uint32: Proof-of-rotation ID: 0x3ba06f8c
+ // * length-prefixed proof-of-rotation structure
+ // consume the digests through the maxSdkVersion to reach the lineage in the attributes
+ getLengthPrefixedSlice(signedData);
+ getLengthPrefixedSlice(signedData);
+ signedData.getInt();
+ signedData.getInt();
+ // iterate over the additional attributes adding any lineages to the List
+ ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData);
+ List lineages = new ArrayList<>(1);
+ while (additionalAttributes.hasRemaining()) {
+ ByteBuffer attribute = getLengthPrefixedSlice(additionalAttributes);
+ int id = attribute.getInt();
+ if (id == V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID) {
+ byte[] value = ByteBufferUtils.toByteArray(attribute);
+ SigningCertificateLineage lineage = readFromV3AttributeValue(value);
+ lineages.add(lineage);
+ }
+ }
+ SigningCertificateLineage result;
+ // There should only be a single attribute with the lineage, but if there are multiple then
+ // attempt to consolidate the lineages.
+ if (lineages.isEmpty()) {
+ throw new IllegalArgumentException("The signed data does not contain a valid lineage.");
+ } else if (lineages.size() > 1) {
+ result = consolidateLineages(lineages);
+ } else {
+ result = lineages.get(0);
+ }
+ return result;
+ }
+
+ public void writeToFile(File file) throws IOException {
+ if (file == null) {
+ throw new NullPointerException("file == null");
+ }
+ RandomAccessFile outputFile = new RandomAccessFile(file, "rw");
+ writeToDataSink(new RandomAccessFileDataSink(outputFile));
+ }
+
+ public void writeToDataSink(DataSink dataSink) throws IOException {
+ if (dataSink == null) {
+ throw new NullPointerException("dataSink == null");
+ }
+ dataSink.consume(write());
+ }
+
+ /**
+ * Add a new signing certificate to the lineage. This effectively creates a signing certificate
+ * rotation event, forcing APKs which include this lineage to be signed by the new signer. The
+ * flags associated with the new signer are set to a default value.
+ *
+ * @param parent current signing certificate of the containing APK
+ * @param child new signing certificate which will sign the APK contents
+ */
+ public SigningCertificateLineage spawnDescendant(SignerConfig parent, SignerConfig child)
+ throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException,
+ SignatureException {
+ if (parent == null || child == null) {
+ throw new NullPointerException("can't add new descendant to lineage with null inputs");
+ }
+ SignerCapabilities signerCapabilities = new SignerCapabilities.Builder().build();
+ return spawnDescendant(parent, child, signerCapabilities);
+ }
+
+ /**
+ * Add a new signing certificate to the lineage. This effectively creates a signing certificate
+ * rotation event, forcing APKs which include this lineage to be signed by the new signer.
+ *
+ * @param parent current signing certificate of the containing APK
+ * @param child new signing certificate which will sign the APK contents
+ * @param childCapabilities flags
+ */
+ public SigningCertificateLineage spawnDescendant(
+ SignerConfig parent, SignerConfig child, SignerCapabilities childCapabilities)
+ throws CertificateEncodingException, InvalidKeyException,
+ NoSuchAlgorithmException, SignatureException {
+ if (parent == null) {
+ throw new NullPointerException("parent == null");
+ }
+ if (child == null) {
+ throw new NullPointerException("child == null");
+ }
+ if (childCapabilities == null) {
+ throw new NullPointerException("childCapabilities == null");
+ }
+ if (mSigningLineage.isEmpty()) {
+ throw new IllegalArgumentException("Cannot spawn descendant signing certificate on an"
+ + " empty SigningCertificateLineage: no parent node");
+ }
+
+ // make sure that the parent matches our newest generation (leaf node/sink)
+ SigningCertificateNode currentGeneration = mSigningLineage.get(mSigningLineage.size() - 1);
+ if (!Arrays.equals(currentGeneration.signingCert.getEncoded(),
+ parent.getCertificate().getEncoded())) {
+ throw new IllegalArgumentException("SignerConfig Certificate containing private key"
+ + " to sign the new SigningCertificateLineage record does not match the"
+ + " existing most recent record");
+ }
+
+ // create data to be signed, including the algorithm we're going to use
+ SignatureAlgorithm signatureAlgorithm = getSignatureAlgorithm(parent);
+ ByteBuffer prefixedSignedData = ByteBuffer.wrap(
+ V3SigningCertificateLineage.encodeSignedData(
+ child.getCertificate(), signatureAlgorithm.getId()));
+ prefixedSignedData.position(4);
+ ByteBuffer signedDataBuffer = ByteBuffer.allocate(prefixedSignedData.remaining());
+ signedDataBuffer.put(prefixedSignedData);
+ byte[] signedData = signedDataBuffer.array();
+
+ // create SignerConfig to do the signing
+ List certificates = new ArrayList<>(1);
+ certificates.add(parent.getCertificate());
+ ApkSigningBlockUtils.SignerConfig newSignerConfig =
+ new ApkSigningBlockUtils.SignerConfig();
+ newSignerConfig.privateKey = parent.getPrivateKey();
+ newSignerConfig.certificates = certificates;
+ newSignerConfig.signatureAlgorithms = Collections.singletonList(signatureAlgorithm);
+
+ // sign it
+ List> signatures =
+ ApkSigningBlockUtils.generateSignaturesOverData(newSignerConfig, signedData);
+
+ // finally, add it to our lineage
+ SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(signatures.get(0).getFirst());
+ byte[] signature = signatures.get(0).getSecond();
+ currentGeneration.sigAlgorithm = sigAlgorithm;
+ SigningCertificateNode childNode =
+ new SigningCertificateNode(
+ child.getCertificate(), sigAlgorithm, null,
+ signature, childCapabilities.getFlags());
+ List lineageCopy = new ArrayList<>(mSigningLineage);
+ lineageCopy.add(childNode);
+ return new SigningCertificateLineage(mMinSdkVersion, lineageCopy);
+ }
+
+ /**
+ * The number of signing certificates in the lineage, including the current signer, which means
+ * this value can also be used to V2determine the number of signing certificate rotations by
+ * subtracting 1.
+ */
+ public int size() {
+ return mSigningLineage.size();
+ }
+
+ private SignatureAlgorithm getSignatureAlgorithm(SignerConfig parent)
+ throws InvalidKeyException {
+ PublicKey publicKey = parent.getCertificate().getPublicKey();
+
+ // TODO switch to one signature algorithm selection, or add support for multiple algorithms
+ List algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
+ publicKey, mMinSdkVersion, false /* padding support */);
+ return algorithms.get(0);
+ }
+
+ private SigningCertificateLineage spawnFirstDescendant(
+ SignerConfig parent, SignerCapabilities signerCapabilities) {
+ if (!mSigningLineage.isEmpty()) {
+ throw new IllegalStateException("SigningCertificateLineage already has its first node");
+ }
+
+ // check to make sure that the public key for the first node is acceptable for our minSdk
+ try {
+ getSignatureAlgorithm(parent);
+ } catch (InvalidKeyException e) {
+ throw new IllegalArgumentException("Algorithm associated with first signing certificate"
+ + " invalid on desired platform versions", e);
+ }
+
+ // create "fake" signed data (there will be no signature over it, since there is no parent
+ SigningCertificateNode firstNode = new SigningCertificateNode(
+ parent.getCertificate(), null, null, new byte[0], signerCapabilities.getFlags());
+ return new SigningCertificateLineage(mMinSdkVersion, Collections.singletonList(firstNode));
+ }
+
+ private static SigningCertificateLineage read(ByteBuffer inputByteBuffer)
+ throws IOException {
+ ApkSigningBlockUtils.checkByteOrderLittleEndian(inputByteBuffer);
+ if (inputByteBuffer.remaining() < 8) {
+ throw new IllegalArgumentException(
+ "Improper SigningCertificateLineage format: insufficient data for header.");
+ }
+
+ if (inputByteBuffer.getInt() != MAGIC) {
+ throw new IllegalArgumentException(
+ "Improper SigningCertificateLineage format: MAGIC header mismatch.");
+ }
+ return read(inputByteBuffer, inputByteBuffer.getInt());
+ }
+
+ private static SigningCertificateLineage read(ByteBuffer inputByteBuffer, int version)
+ throws IOException {
+ switch (version) {
+ case FIRST_VERSION:
+ try {
+ List nodes =
+ V3SigningCertificateLineage.readSigningCertificateLineage(
+ getLengthPrefixedSlice(inputByteBuffer));
+ int minSdkVersion = calculateMinSdkVersion(nodes);
+ return new SigningCertificateLineage(minSdkVersion, nodes);
+ } catch (ApkFormatException e) {
+ // unable to get a proper length-prefixed lineage slice
+ throw new IOException("Unable to read list of signing certificate nodes in "
+ + "SigningCertificateLineage", e);
+ }
+ default:
+ throw new IllegalArgumentException(
+ "Improper SigningCertificateLineage format: unrecognized version.");
+ }
+ }
+
+ private static int calculateMinSdkVersion(List nodes) {
+ if (nodes == null) {
+ throw new IllegalArgumentException("Can't calculate minimum SDK version of null nodes");
+ }
+ int minSdkVersion = AndroidSdkVersion.P; // lineage introduced in P
+ for (SigningCertificateNode node : nodes) {
+ if (node.sigAlgorithm != null) {
+ int nodeMinSdkVersion = node.sigAlgorithm.getMinSdkVersion();
+ if (nodeMinSdkVersion > minSdkVersion) {
+ minSdkVersion = nodeMinSdkVersion;
+ }
+ }
+ }
+ return minSdkVersion;
+ }
+
+ private ByteBuffer write() {
+ byte[] encodedLineage =
+ V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage);
+ int payloadSize = 4 + 4 + 4 + encodedLineage.length;
+ ByteBuffer result = ByteBuffer.allocate(payloadSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.putInt(MAGIC);
+ result.putInt(CURRENT_VERSION);
+ result.putInt(encodedLineage.length);
+ result.put(encodedLineage);
+ result.flip();
+ return result;
+ }
+
+ public byte[] generateV3SignerAttribute() {
+ // FORMAT (little endian):
+ // * length-prefixed bytes: attribute pair
+ // * uint32: ID
+ // * bytes: value - encoded V3 SigningCertificateLineage
+ byte[] encodedLineage =
+ V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage);
+ int payloadSize = 4 + 4 + encodedLineage.length;
+ ByteBuffer result = ByteBuffer.allocate(payloadSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.putInt(4 + encodedLineage.length);
+ result.putInt(V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID);
+ result.put(encodedLineage);
+ return result.array();
+ }
+
+ public List sortSignerConfigs(
+ List signerConfigs) {
+ if (signerConfigs == null) {
+ throw new NullPointerException("signerConfigs == null");
+ }
+
+ // not the most elegant sort, but we expect signerConfigs to be quite small (1 or 2 signers
+ // in most cases) and likely already sorted, so not worth the overhead of doing anything
+ // fancier
+ List sortedSignerConfigs =
+ new ArrayList<>(signerConfigs.size());
+ for (int i = 0; i < mSigningLineage.size(); i++) {
+ for (int j = 0; j < signerConfigs.size(); j++) {
+ DefaultApkSignerEngine.SignerConfig config = signerConfigs.get(j);
+ if (mSigningLineage.get(i).signingCert.equals(config.getCertificates().get(0))) {
+ sortedSignerConfigs.add(config);
+ break;
+ }
+ }
+ }
+ if (sortedSignerConfigs.size() != signerConfigs.size()) {
+ throw new IllegalArgumentException("SignerConfigs supplied which are not present in the"
+ + " SigningCertificateLineage");
+ }
+ return sortedSignerConfigs;
+ }
+
+ /**
+ * Returns the SignerCapabilities for the signer in the lineage that matches the provided
+ * config.
+ */
+ public SignerCapabilities getSignerCapabilities(SignerConfig config) {
+ if (config == null) {
+ throw new NullPointerException("config == null");
+ }
+
+ X509Certificate cert = config.getCertificate();
+ return getSignerCapabilities(cert);
+ }
+
+ /**
+ * Returns the SignerCapabilities for the signer in the lineage that matches the provided
+ * certificate.
+ */
+ public SignerCapabilities getSignerCapabilities(X509Certificate cert) {
+ if (cert == null) {
+ throw new NullPointerException("cert == null");
+ }
+
+ for (int i = 0; i < mSigningLineage.size(); i++) {
+ SigningCertificateNode lineageNode = mSigningLineage.get(i);
+ if (lineageNode.signingCert.equals(cert)) {
+ int flags = lineageNode.flags;
+ return new SignerCapabilities.Builder(flags).build();
+ }
+ }
+
+ // the provided signer certificate was not found in the lineage
+ throw new IllegalArgumentException("Certificate (" + cert.getSubjectDN()
+ + ") not found in the SigningCertificateLineage");
+ }
+
+ /**
+ * Updates the SignerCapabilities for the signer in the lineage that matches the provided
+ * config. Only those capabilities that have been modified through the setXX methods will be
+ * updated for the signer to prevent unset default values from being applied.
+ */
+ public void updateSignerCapabilities(SignerConfig config, SignerCapabilities capabilities) {
+ if (config == null) {
+ throw new NullPointerException("config == null");
+ }
+
+ X509Certificate cert = config.getCertificate();
+ for (int i = 0; i < mSigningLineage.size(); i++) {
+ SigningCertificateNode lineageNode = mSigningLineage.get(i);
+ if (lineageNode.signingCert.equals(cert)) {
+ int flags = lineageNode.flags;
+ SignerCapabilities newCapabilities = new SignerCapabilities.Builder(
+ flags).setCallerConfiguredCapabilities(capabilities).build();
+ lineageNode.flags = newCapabilities.getFlags();
+ return;
+ }
+ }
+
+ // the provided signer config was not found in the lineage
+ throw new IllegalArgumentException("Certificate (" + cert.getSubjectDN()
+ + ") not found in the SigningCertificateLineage");
+ }
+
+ /**
+ * Returns a list containing all of the certificates in the lineage.
+ */
+ public List getCertificatesInLineage() {
+ List certs = new ArrayList<>();
+ for (int i = 0; i < mSigningLineage.size(); i++) {
+ X509Certificate cert = mSigningLineage.get(i).signingCert;
+ certs.add(cert);
+ }
+ return certs;
+ }
+
+ /**
+ * Returns {@code true} if the specified config is in the lineage.
+ */
+ public boolean isSignerInLineage(SignerConfig config) {
+ if (config == null) {
+ throw new NullPointerException("config == null");
+ }
+
+ X509Certificate cert = config.getCertificate();
+ return isCertificateInLineage(cert);
+ }
+
+ /**
+ * Returns {@code true} if the specified certificate is in the lineage.
+ */
+ public boolean isCertificateInLineage(X509Certificate cert) {
+ if (cert == null) {
+ throw new NullPointerException("cert == null");
+ }
+
+ for (int i = 0; i < mSigningLineage.size(); i++) {
+ if (mSigningLineage.get(i).signingCert.equals(cert)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static int calculateDefaultFlags() {
+ return PAST_CERT_INSTALLED_DATA | PAST_CERT_PERMISSION
+ | PAST_CERT_SHARED_USER_ID | PAST_CERT_AUTH;
+ }
+
+ /**
+ * Returns a new SigingCertificateLineage which terminates at the node corresponding to the
+ * given certificate. This is useful in the event of rotating to a new signing algorithm that
+ * is only supported on some platform versions. It enables a v3 signature to be generated using
+ * this signing certificate and the shortened proof-of-rotation record from this sub lineage in
+ * conjunction with the appropriate SDK version values.
+ *
+ * @param x509Certificate the signing certificate for which to search
+ * @return A new SigningCertificateLineage if the given certificate is present.
+ *
+ * @throws IllegalArgumentException if the provided certificate is not in the lineage.
+ */
+ public SigningCertificateLineage getSubLineage(X509Certificate x509Certificate) {
+ if (x509Certificate == null) {
+ throw new NullPointerException("x509Certificate == null");
+ }
+ for (int i = 0; i < mSigningLineage.size(); i++) {
+ if (mSigningLineage.get(i).signingCert.equals(x509Certificate)) {
+ return new SigningCertificateLineage(
+ mMinSdkVersion, new ArrayList<>(mSigningLineage.subList(0, i + 1)));
+ }
+ }
+
+ // looks like we didn't find the cert,
+ throw new IllegalArgumentException("Certificate not found in SigningCertificateLineage");
+ }
+
+ /**
+ * Consolidates all of the lineages found in an APK into one lineage, which is the longest one.
+ * In so doing, it also checks that all of the smaller lineages are contained in the largest,
+ * and that they properly cover the desired platform ranges.
+ *
+ * An APK may contain multiple lineages, one for each signer, which correspond to different
+ * supported platform versions. In this event, the lineage(s) from the earlier platform
+ * version(s) need to be present in the most recent (longest) one to make sure that when a
+ * platform version changes.
+ *
+ * This does not verify that the largest lineage corresponds to the most recent supported
+ * platform version. That check requires is performed during v3 verification.
+ */
+ public static SigningCertificateLineage consolidateLineages(
+ List lineages) {
+ if (lineages == null || lineages.isEmpty()) {
+ return null;
+ }
+ int largestIndex = 0;
+ int maxSize = 0;
+
+ // determine the longest chain
+ for (int i = 0; i < lineages.size(); i++) {
+ int curSize = lineages.get(i).size();
+ if (curSize > maxSize) {
+ largestIndex = i;
+ maxSize = curSize;
+ }
+ }
+
+ List largestList = lineages.get(largestIndex).mSigningLineage;
+ // make sure all other lineages fit into this one, with the same capabilities
+ for (int i = 0; i < lineages.size(); i++) {
+ if (i == largestIndex) {
+ continue;
+ }
+ List underTest = lineages.get(i).mSigningLineage;
+ if (!underTest.equals(largestList.subList(0, underTest.size()))) {
+ throw new IllegalArgumentException("Inconsistent SigningCertificateLineages. "
+ + "Not all lineages are subsets of each other.");
+ }
+ }
+
+ // if we've made it this far, they all check out, so just return the largest
+ return lineages.get(largestIndex);
+ }
+
+ /**
+ * Representation of the capabilities the APK would like to grant to its old signing
+ * certificates. The {@code SigningCertificateLineage} provides two conceptual data structures.
+ * 1) proof of rotation - Evidence that other parties can trust an APK's current signing
+ * certificate if they trust an older one in this lineage
+ * 2) self-trust - certain capabilities may have been granted by an APK to other parties based
+ * on its own signing certificate. When it changes its signing certificate it may want to
+ * allow the other parties to retain those capabilities.
+ * {@code SignerCapabilties} provides a representation of the second structure.
+ *
+ * Use {@link Builder} to obtain configuration instances.
+ */
+ public static class SignerCapabilities {
+ private final int mFlags;
+
+ private final int mCallerConfiguredFlags;
+
+ private SignerCapabilities(int flags) {
+ this(flags, 0);
+ }
+
+ private SignerCapabilities(int flags, int callerConfiguredFlags) {
+ mFlags = flags;
+ mCallerConfiguredFlags = callerConfiguredFlags;
+ }
+
+ private int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Returns {@code true} if the capabilities of this object match those of the provided
+ * object.
+ */
+ public boolean equals(SignerCapabilities other) {
+ return this.mFlags == other.mFlags;
+ }
+
+ /**
+ * Returns {@code true} if this object has the installed data capability.
+ */
+ public boolean hasInstalledData() {
+ return (mFlags & PAST_CERT_INSTALLED_DATA) != 0;
+ }
+
+ /**
+ * Returns {@code true} if this object has the shared UID capability.
+ */
+ public boolean hasSharedUid() {
+ return (mFlags & PAST_CERT_SHARED_USER_ID) != 0;
+ }
+
+ /**
+ * Returns {@code true} if this object has the permission capability.
+ */
+ public boolean hasPermission() {
+ return (mFlags & PAST_CERT_PERMISSION) != 0;
+ }
+
+ /**
+ * Returns {@code true} if this object has the rollback capability.
+ */
+ public boolean hasRollback() {
+ return (mFlags & PAST_CERT_ROLLBACK) != 0;
+ }
+
+ /**
+ * Returns {@code true} if this object has the auth capability.
+ */
+ public boolean hasAuth() {
+ return (mFlags & PAST_CERT_AUTH) != 0;
+ }
+
+ /**
+ * Builder of {@link SignerCapabilities} instances.
+ */
+ public static class Builder {
+ private int mFlags;
+
+ private int mCallerConfiguredFlags;
+
+ /**
+ * Constructs a new {@code Builder}.
+ */
+ public Builder() {
+ mFlags = calculateDefaultFlags();
+ }
+
+ /**
+ * Constructs a new {@code Builder} with the initial capabilities set to the provided
+ * flags.
+ */
+ public Builder(int flags) {
+ mFlags = flags;
+ }
+
+ /**
+ * Set the {@code PAST_CERT_INSTALLED_DATA} flag in this capabilities object. This flag
+ * is used by the platform to determine if installed data associated with previous
+ * signing certificate should be trusted. In particular, this capability is required to
+ * perform signing certificate rotation during an upgrade on-device. Without it, the
+ * platform will not permit the app data from the old signing certificate to
+ * propagate to the new version. Typically, this flag should be set to enable signing
+ * certificate rotation, and may be unset later when the app developer is satisfied that
+ * their install base is as migrated as it will be.
+ */
+ public Builder setInstalledData(boolean enabled) {
+ mCallerConfiguredFlags |= PAST_CERT_INSTALLED_DATA;
+ if (enabled) {
+ mFlags |= PAST_CERT_INSTALLED_DATA;
+ } else {
+ mFlags &= ~PAST_CERT_INSTALLED_DATA;
+ }
+ return this;
+ }
+
+ /**
+ * Set the {@code PAST_CERT_SHARED_USER_ID} flag in this capabilities object. This flag
+ * is used by the platform to determine if this app is willing to be sharedUid with
+ * other apps which are still signed with the associated signing certificate. This is
+ * useful in situations where sharedUserId apps would like to change their signing
+ * certificate, but can't guarantee the order of updates to those apps.
+ */
+ public Builder setSharedUid(boolean enabled) {
+ mCallerConfiguredFlags |= PAST_CERT_SHARED_USER_ID;
+ if (enabled) {
+ mFlags |= PAST_CERT_SHARED_USER_ID;
+ } else {
+ mFlags &= ~PAST_CERT_SHARED_USER_ID;
+ }
+ return this;
+ }
+
+ /**
+ * Set the {@code PAST_CERT_PERMISSION} flag in this capabilities object. This flag
+ * is used by the platform to determine if this app is willing to grant SIGNATURE
+ * permissions to apps signed with the associated signing certificate. Without this
+ * capability, an application signed with the older certificate will not be granted the
+ * SIGNATURE permissions defined by this app. In addition, if multiple apps define the
+ * same SIGNATURE permission, the second one the platform sees will not be installable
+ * if this capability is not set and the signing certificates differ.
+ */
+ public Builder setPermission(boolean enabled) {
+ mCallerConfiguredFlags |= PAST_CERT_PERMISSION;
+ if (enabled) {
+ mFlags |= PAST_CERT_PERMISSION;
+ } else {
+ mFlags &= ~PAST_CERT_PERMISSION;
+ }
+ return this;
+ }
+
+ /**
+ * Set the {@code PAST_CERT_ROLLBACK} flag in this capabilities object. This flag
+ * is used by the platform to determine if this app is willing to upgrade to a new
+ * version that is signed by one of its past signing certificates.
+ *
+ * WARNING: this effectively removes any benefit of signing certificate changes,
+ * since a compromised key could retake control of an app even after change, and should
+ * only be used if there is a problem encountered when trying to ditch an older cert
+ *
+ */
+ public Builder setRollback(boolean enabled) {
+ mCallerConfiguredFlags |= PAST_CERT_ROLLBACK;
+ if (enabled) {
+ mFlags |= PAST_CERT_ROLLBACK;
+ } else {
+ mFlags &= ~PAST_CERT_ROLLBACK;
+ }
+ return this;
+ }
+
+ /**
+ * Set the {@code PAST_CERT_AUTH} flag in this capabilities object. This flag
+ * is used by the platform to determine whether or not privileged access based on
+ * authenticator module signing certificates should be granted.
+ */
+ public Builder setAuth(boolean enabled) {
+ mCallerConfiguredFlags |= PAST_CERT_AUTH;
+ if (enabled) {
+ mFlags |= PAST_CERT_AUTH;
+ } else {
+ mFlags &= ~PAST_CERT_AUTH;
+ }
+ return this;
+ }
+
+ /**
+ * Applies the capabilities that were explicitly set in the provided capabilities object
+ * to this builder. Any values that were not set will not be applied to this builder
+ * to prevent unintentinoally setting a capability back to a default value.
+ */
+ public Builder setCallerConfiguredCapabilities(SignerCapabilities capabilities) {
+ // The mCallerConfiguredFlags should have a bit set for each capability that was
+ // set by a caller. If a capability was explicitly set then the corresponding bit
+ // in mCallerConfiguredFlags should be set. This allows the provided capabilities
+ // to take effect for those set by the caller while those that were not set will
+ // be cleared by the bitwise and and the initial value for the builder will remain.
+ mFlags = (mFlags & ~capabilities.mCallerConfiguredFlags) |
+ (capabilities.mFlags & capabilities.mCallerConfiguredFlags);
+ return this;
+ }
+
+ /**
+ * Returns a new {@code SignerConfig} instance configured based on the configuration of
+ * this builder.
+ */
+ public SignerCapabilities build() {
+ return new SignerCapabilities(mFlags, mCallerConfiguredFlags);
+ }
+ }
+ }
+
+ /**
+ * Configuration of a signer. Used to add a new entry to the {@link SigningCertificateLineage}
+ *
+ *
Use {@link Builder} to obtain configuration instances.
+ */
+ public static class SignerConfig {
+ private final PrivateKey mPrivateKey;
+ private final X509Certificate mCertificate;
+
+ private SignerConfig(
+ PrivateKey privateKey,
+ X509Certificate certificate) {
+ mPrivateKey = privateKey;
+ mCertificate = certificate;
+ }
+
+ /**
+ * Returns the signing key of this signer.
+ */
+ public PrivateKey getPrivateKey() {
+ return mPrivateKey;
+ }
+
+ /**
+ * Returns the certificate(s) of this signer. The first certificate's public key corresponds
+ * to this signer's private key.
+ */
+ public X509Certificate getCertificate() {
+ return mCertificate;
+ }
+
+ /**
+ * Builder of {@link SignerConfig} instances.
+ */
+ public static class Builder {
+ private final PrivateKey mPrivateKey;
+ private final X509Certificate mCertificate;
+
+ /**
+ * Constructs a new {@code Builder}.
+ *
+ * @param privateKey signing key
+ * @param certificate the X.509 certificate with a subject public key of the
+ * {@code privateKey}.
+ */
+ public Builder(
+ PrivateKey privateKey,
+ X509Certificate certificate) {
+ mPrivateKey = privateKey;
+ mCertificate = certificate;
+ }
+
+ /**
+ * Returns a new {@code SignerConfig} instance configured based on the configuration of
+ * this builder.
+ */
+ public SignerConfig build() {
+ return new SignerConfig(
+ mPrivateKey,
+ mCertificate);
+ }
+ }
+ }
+
+ /**
+ * Builder of {@link SigningCertificateLineage} instances.
+ */
+ public static class Builder {
+ private final SignerConfig mOriginalSignerConfig;
+ private final SignerConfig mNewSignerConfig;
+ private SignerCapabilities mOriginalCapabilities;
+ private SignerCapabilities mNewCapabilities;
+ private int mMinSdkVersion;
+ /**
+ * Constructs a new {@code Builder}.
+ *
+ * @param originalSignerConfig first signer in this lineage, parent of the next
+ * @param newSignerConfig new signer in the lineage; the new signing key that the APK will
+ * use
+ */
+ public Builder(
+ SignerConfig originalSignerConfig,
+ SignerConfig newSignerConfig) {
+ if (originalSignerConfig == null || newSignerConfig == null) {
+ throw new NullPointerException("Can't pass null SignerConfigs when constructing a "
+ + "new SigningCertificateLineage");
+ }
+ mOriginalSignerConfig = originalSignerConfig;
+ mNewSignerConfig = newSignerConfig;
+ }
+
+ /**
+ * Sets the minimum Android platform version (API Level) on which this lineage is expected
+ * to validate. It is possible that newer signers in the lineage may not be recognized on
+ * the given platform, but as long as an older signer is, the lineage can still be used to
+ * sign an APK for the given platform.
+ *
+ * By default, this value is set to the value for the
+ * P release, since this structure was created for that release, and will also be set to
+ * that value if a smaller one is specified.
+ */
+ public Builder setMinSdkVersion(int minSdkVersion) {
+ mMinSdkVersion = minSdkVersion;
+ return this;
+ }
+
+ /**
+ * Sets capabilities to give {@code mOriginalSignerConfig}. These capabilities allow an
+ * older signing certificate to still be used in some situations on the platform even though
+ * the APK is now being signed by a newer signing certificate.
+ */
+ public Builder setOriginalCapabilities(SignerCapabilities signerCapabilities) {
+ if (signerCapabilities == null) {
+ throw new NullPointerException("signerCapabilities == null");
+ }
+ mOriginalCapabilities = signerCapabilities;
+ return this;
+ }
+
+ /**
+ * Sets capabilities to give {@code mNewSignerConfig}. These capabilities allow an
+ * older signing certificate to still be used in some situations on the platform even though
+ * the APK is now being signed by a newer signing certificate. By default, the new signer
+ * will have all capabilities, so when first switching to a new signing certificate, these
+ * capabilities have no effect, but they will act as the default level of trust when moving
+ * to a new signing certificate.
+ */
+ public Builder setNewCapabilities(SignerCapabilities signerCapabilities) {
+ if (signerCapabilities == null) {
+ throw new NullPointerException("signerCapabilities == null");
+ }
+ mNewCapabilities = signerCapabilities;
+ return this;
+ }
+
+ public SigningCertificateLineage build()
+ throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException,
+ SignatureException {
+ if (mMinSdkVersion < AndroidSdkVersion.P) {
+ mMinSdkVersion = AndroidSdkVersion.P;
+ }
+
+ if (mOriginalCapabilities == null) {
+ mOriginalCapabilities = new SignerCapabilities.Builder().build();
+ }
+
+ if (mNewCapabilities == null) {
+ mNewCapabilities = new SignerCapabilities.Builder().build();
+ }
+
+ return createSigningLineage(
+ mMinSdkVersion, mOriginalSignerConfig, mOriginalCapabilities,
+ mNewSignerConfig, mNewCapabilities);
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/apk/ApkFormatException.java b/app/src/main/java/com/android/apksig/apk/ApkFormatException.java
new file mode 100644
index 0000000..a780134
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/apk/ApkFormatException.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/**
+ * Indicates that an APK is not well-formed. For example, this may indicate that the APK is not a
+ * well-formed ZIP archive, in which case {@link #getCause()} will return a
+ * {@link com.android.apksig.zip.ZipFormatException ZipFormatException}, or that the APK contains
+ * multiple ZIP entries with the same name.
+ */
+public class ApkFormatException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public ApkFormatException(String message) {
+ super(message);
+ }
+
+ public ApkFormatException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/apk/ApkSigningBlockNotFoundException.java b/app/src/main/java/com/android/apksig/apk/ApkSigningBlockNotFoundException.java
new file mode 100644
index 0000000..fd961d5
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/apk/ApkSigningBlockNotFoundException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 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;
+
+/**
+ * Indicates that no APK Signing Block was found in an APK.
+ */
+public class ApkSigningBlockNotFoundException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public ApkSigningBlockNotFoundException(String message) {
+ super(message);
+ }
+
+ public ApkSigningBlockNotFoundException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/apk/ApkUtils.java b/app/src/main/java/com/android/apksig/apk/ApkUtils.java
new file mode 100644
index 0000000..135d815
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/apk/ApkUtils.java
@@ -0,0 +1,604 @@
+/*
+ * 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 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 APK Signature Scheme v2
+ */
+ 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 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[] 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);
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java b/app/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java
new file mode 100644
index 0000000..e30bc35
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java
@@ -0,0 +1,46 @@
+/*
+ * 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;
+
+/**
+ * Indicates that there was an issue determining the minimum Android platform version supported by
+ * an APK because the version is specified as a codename, rather than as API Level number, and the
+ * codename is in an unexpected format.
+ */
+public class CodenameMinSdkVersionException extends MinSdkVersionException {
+
+ private static final long serialVersionUID = 1L;
+
+ /** Encountered codename. */
+ private final String mCodename;
+
+ /**
+ * Constructs a new {@code MinSdkVersionCodenameException} with the provided message and
+ * codename.
+ */
+ public CodenameMinSdkVersionException(String message, String codename) {
+ super(message);
+ mCodename = codename;
+ }
+
+ /**
+ * Returns the codename.
+ */
+ public String getCodename() {
+ return mCodename;
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/apk/MinSdkVersionException.java b/app/src/main/java/com/android/apksig/apk/MinSdkVersionException.java
new file mode 100644
index 0000000..c4aad08
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/apk/MinSdkVersionException.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+/**
+ * Indicates that there was an issue determining the minimum Android platform version supported by
+ * an APK.
+ */
+public class MinSdkVersionException extends ApkFormatException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new {@code MinSdkVersionException} with the provided message.
+ */
+ public MinSdkVersionException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new {@code MinSdkVersionException} with the provided message and cause.
+ */
+ public MinSdkVersionException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java b/app/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java
new file mode 100644
index 0000000..bc5a457
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java
@@ -0,0 +1,869 @@
+/*
+ * 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.internal.apk;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * XML pull style parser of Android binary XML resources, such as {@code AndroidManifest.xml}.
+ *
+ * For an input document, the parser outputs an event stream (see {@code EVENT_... constants} via
+ * {@link #getEventType()} and {@link #next()} methods. Additional information about the current
+ * event can be obtained via an assortment of getters, for example, {@link #getName()} or
+ * {@link #getAttributeNameResourceId(int)}.
+ */
+public class AndroidBinXmlParser {
+
+ /** Event: start of document. */
+ public static final int EVENT_START_DOCUMENT = 1;
+
+ /** Event: end of document. */
+ public static final int EVENT_END_DOCUMENT = 2;
+
+ /** Event: start of an element. */
+ public static final int EVENT_START_ELEMENT = 3;
+
+ /** Event: end of an document. */
+ public static final int EVENT_END_ELEMENT = 4;
+
+ /** Attribute value type is not supported by this parser. */
+ public static final int VALUE_TYPE_UNSUPPORTED = 0;
+
+ /** Attribute value is a string. Use {@link #getAttributeStringValue(int)} to obtain it. */
+ public static final int VALUE_TYPE_STRING = 1;
+
+ /** Attribute value is an integer. Use {@link #getAttributeIntValue(int)} to obtain it. */
+ public static final int VALUE_TYPE_INT = 2;
+
+ /**
+ * Attribute value is a resource reference. Use {@link #getAttributeIntValue(int)} to obtain it.
+ */
+ public static final int VALUE_TYPE_REFERENCE = 3;
+
+ /** Attribute value is a boolean. Use {@link #getAttributeBooleanValue(int)} to obtain it. */
+ public static final int VALUE_TYPE_BOOLEAN = 4;
+
+ private static final long NO_NAMESPACE = 0xffffffffL;
+
+ private final ByteBuffer mXml;
+
+ private StringPool mStringPool;
+ private ResourceMap mResourceMap;
+ private int mDepth;
+ private int mCurrentEvent = EVENT_START_DOCUMENT;
+
+ private String mCurrentElementName;
+ private String mCurrentElementNamespace;
+ private int mCurrentElementAttributeCount;
+ private List mCurrentElementAttributes;
+ private ByteBuffer mCurrentElementAttributesContents;
+ private int mCurrentElementAttrSizeBytes;
+
+ /**
+ * Constructs a new parser for the provided document.
+ */
+ public AndroidBinXmlParser(ByteBuffer xml) throws XmlParserException {
+ xml.order(ByteOrder.LITTLE_ENDIAN);
+
+ Chunk resXmlChunk = null;
+ while (xml.hasRemaining()) {
+ Chunk chunk = Chunk.get(xml);
+ if (chunk == null) {
+ break;
+ }
+ if (chunk.getType() == Chunk.TYPE_RES_XML) {
+ resXmlChunk = chunk;
+ break;
+ }
+ }
+
+ if (resXmlChunk == null) {
+ throw new XmlParserException("No XML chunk in file");
+ }
+ mXml = resXmlChunk.getContents();
+ }
+
+ /**
+ * Returns the depth of the current element. Outside of the root of the document the depth is
+ * {@code 0}. The depth is incremented by {@code 1} before each {@code start element} event and
+ * is decremented by {@code 1} after each {@code end element} event.
+ */
+ public int getDepth() {
+ return mDepth;
+ }
+
+ /**
+ * Returns the type of the current event. See {@code EVENT_...} constants.
+ */
+ public int getEventType() {
+ return mCurrentEvent;
+ }
+
+ /**
+ * Returns the local name of the current element or {@code null} if the current event does not
+ * pertain to an element.
+ */
+ public String getName() {
+ if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
+ return null;
+ }
+ return mCurrentElementName;
+ }
+
+ /**
+ * Returns the namespace of the current element or {@code null} if the current event does not
+ * pertain to an element. Returns an empty string if the element is not associated with a
+ * namespace.
+ */
+ public String getNamespace() {
+ if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
+ return null;
+ }
+ return mCurrentElementNamespace;
+ }
+
+ /**
+ * Returns the number of attributes of the element associated with the current event or
+ * {@code -1} if no element is associated with the current event.
+ */
+ public int getAttributeCount() {
+ if (mCurrentEvent != EVENT_START_ELEMENT) {
+ return -1;
+ }
+
+ return mCurrentElementAttributeCount;
+ }
+
+ /**
+ * Returns the resource ID corresponding to the name of the specified attribute of the current
+ * element or {@code 0} if the name is not associated with a resource ID.
+ *
+ * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+ * {@code start element} event
+ * @throws XmlParserException if a parsing error is occurred
+ */
+ public int getAttributeNameResourceId(int index) throws XmlParserException {
+ return getAttribute(index).getNameResourceId();
+ }
+
+ /**
+ * Returns the name of the specified attribute of the current element.
+ *
+ * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+ * {@code start element} event
+ * @throws XmlParserException if a parsing error is occurred
+ */
+ public String getAttributeName(int index) throws XmlParserException {
+ return getAttribute(index).getName();
+ }
+
+ /**
+ * Returns the name of the specified attribute of the current element or an empty string if
+ * the attribute is not associated with a namespace.
+ *
+ * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+ * {@code start element} event
+ * @throws XmlParserException if a parsing error is occurred
+ */
+ public String getAttributeNamespace(int index) throws XmlParserException {
+ return getAttribute(index).getNamespace();
+ }
+
+ /**
+ * Returns the value type of the specified attribute of the current element. See
+ * {@code VALUE_TYPE_...} constants.
+ *
+ * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+ * {@code start element} event
+ * @throws XmlParserException if a parsing error is occurred
+ */
+ public int getAttributeValueType(int index) throws XmlParserException {
+ int type = getAttribute(index).getValueType();
+ switch (type) {
+ case Attribute.TYPE_STRING:
+ return VALUE_TYPE_STRING;
+ case Attribute.TYPE_INT_DEC:
+ case Attribute.TYPE_INT_HEX:
+ return VALUE_TYPE_INT;
+ case Attribute.TYPE_REFERENCE:
+ return VALUE_TYPE_REFERENCE;
+ case Attribute.TYPE_INT_BOOLEAN:
+ return VALUE_TYPE_BOOLEAN;
+ default:
+ return VALUE_TYPE_UNSUPPORTED;
+ }
+ }
+
+ /**
+ * Returns the integer value of the specified attribute of the current element. See
+ * {@code VALUE_TYPE_...} constants.
+ *
+ * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+ * {@code start element} event.
+ * @throws XmlParserException if a parsing error is occurred
+ */
+ public int getAttributeIntValue(int index) throws XmlParserException {
+ return getAttribute(index).getIntValue();
+ }
+
+ /**
+ * Returns the boolean value of the specified attribute of the current element. See
+ * {@code VALUE_TYPE_...} constants.
+ *
+ * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+ * {@code start element} event.
+ * @throws XmlParserException if a parsing error is occurred
+ */
+ public boolean getAttributeBooleanValue(int index) throws XmlParserException {
+ return getAttribute(index).getBooleanValue();
+ }
+
+ /**
+ * Returns the string value of the specified attribute of the current element. See
+ * {@code VALUE_TYPE_...} constants.
+ *
+ * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+ * {@code start element} event.
+ * @throws XmlParserException if a parsing error is occurred
+ */
+ public String getAttributeStringValue(int index) throws XmlParserException {
+ return getAttribute(index).getStringValue();
+ }
+
+ private Attribute getAttribute(int index) {
+ if (mCurrentEvent != EVENT_START_ELEMENT) {
+ throw new IndexOutOfBoundsException("Current event not a START_ELEMENT");
+ }
+ if (index < 0) {
+ throw new IndexOutOfBoundsException("index must be >= 0");
+ }
+ if (index >= mCurrentElementAttributeCount) {
+ throw new IndexOutOfBoundsException(
+ "index must be <= attr count (" + mCurrentElementAttributeCount + ")");
+ }
+ parseCurrentElementAttributesIfNotParsed();
+ return mCurrentElementAttributes.get(index);
+ }
+
+ /**
+ * Advances to the next parsing event and returns its type. See {@code EVENT_...} constants.
+ */
+ public int next() throws XmlParserException {
+ // Decrement depth if the previous event was "end element".
+ if (mCurrentEvent == EVENT_END_ELEMENT) {
+ mDepth--;
+ }
+
+ // Read events from document, ignoring events that we don't report to caller. Stop at the
+ // earliest event which we report to caller.
+ while (mXml.hasRemaining()) {
+ Chunk chunk = Chunk.get(mXml);
+ if (chunk == null) {
+ break;
+ }
+ switch (chunk.getType()) {
+ case Chunk.TYPE_STRING_POOL:
+ if (mStringPool != null) {
+ throw new XmlParserException("Multiple string pools not supported");
+ }
+ mStringPool = new StringPool(chunk);
+ break;
+
+ case Chunk.RES_XML_TYPE_START_ELEMENT:
+ {
+ if (mStringPool == null) {
+ throw new XmlParserException(
+ "Named element encountered before string pool");
+ }
+ ByteBuffer contents = chunk.getContents();
+ if (contents.remaining() < 20) {
+ throw new XmlParserException(
+ "Start element chunk too short. Need at least 20 bytes. Available: "
+ + contents.remaining() + " bytes");
+ }
+ long nsId = getUnsignedInt32(contents);
+ long nameId = getUnsignedInt32(contents);
+ int attrStartOffset = getUnsignedInt16(contents);
+ int attrSizeBytes = getUnsignedInt16(contents);
+ int attrCount = getUnsignedInt16(contents);
+ long attrEndOffset = attrStartOffset + ((long) attrCount) * attrSizeBytes;
+ contents.position(0);
+ if (attrStartOffset > contents.remaining()) {
+ throw new XmlParserException(
+ "Attributes start offset out of bounds: " + attrStartOffset
+ + ", max: " + contents.remaining());
+ }
+ if (attrEndOffset > contents.remaining()) {
+ throw new XmlParserException(
+ "Attributes end offset out of bounds: " + attrEndOffset
+ + ", max: " + contents.remaining());
+ }
+
+ mCurrentElementName = mStringPool.getString(nameId);
+ mCurrentElementNamespace =
+ (nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
+ mCurrentElementAttributeCount = attrCount;
+ mCurrentElementAttributes = null;
+ mCurrentElementAttrSizeBytes = attrSizeBytes;
+ mCurrentElementAttributesContents =
+ sliceFromTo(contents, attrStartOffset, attrEndOffset);
+
+ mDepth++;
+ mCurrentEvent = EVENT_START_ELEMENT;
+ return mCurrentEvent;
+ }
+
+ case Chunk.RES_XML_TYPE_END_ELEMENT:
+ {
+ if (mStringPool == null) {
+ throw new XmlParserException(
+ "Named element encountered before string pool");
+ }
+ ByteBuffer contents = chunk.getContents();
+ if (contents.remaining() < 8) {
+ throw new XmlParserException(
+ "End element chunk too short. Need at least 8 bytes. Available: "
+ + contents.remaining() + " bytes");
+ }
+ long nsId = getUnsignedInt32(contents);
+ long nameId = getUnsignedInt32(contents);
+ mCurrentElementName = mStringPool.getString(nameId);
+ mCurrentElementNamespace =
+ (nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
+ mCurrentEvent = EVENT_END_ELEMENT;
+ mCurrentElementAttributes = null;
+ mCurrentElementAttributesContents = null;
+ return mCurrentEvent;
+ }
+ case Chunk.RES_XML_TYPE_RESOURCE_MAP:
+ if (mResourceMap != null) {
+ throw new XmlParserException("Multiple resource maps not supported");
+ }
+ mResourceMap = new ResourceMap(chunk);
+ break;
+ default:
+ // Unknown chunk type -- ignore
+ break;
+ }
+ }
+
+ mCurrentEvent = EVENT_END_DOCUMENT;
+ return mCurrentEvent;
+ }
+
+ private void parseCurrentElementAttributesIfNotParsed() {
+ if (mCurrentElementAttributes != null) {
+ return;
+ }
+ mCurrentElementAttributes = new ArrayList<>(mCurrentElementAttributeCount);
+ for (int i = 0; i < mCurrentElementAttributeCount; i++) {
+ int startPosition = i * mCurrentElementAttrSizeBytes;
+ ByteBuffer attr =
+ sliceFromTo(
+ mCurrentElementAttributesContents,
+ startPosition,
+ startPosition + mCurrentElementAttrSizeBytes);
+ long nsId = getUnsignedInt32(attr);
+ long nameId = getUnsignedInt32(attr);
+ attr.position(attr.position() + 7); // skip ignored fields
+ int valueType = getUnsignedInt8(attr);
+ long valueData = getUnsignedInt32(attr);
+ mCurrentElementAttributes.add(
+ new Attribute(
+ nsId,
+ nameId,
+ valueType,
+ (int) valueData,
+ mStringPool,
+ mResourceMap));
+ }
+ }
+
+ private static class Attribute {
+ private static final int TYPE_REFERENCE = 1;
+ private static final int TYPE_STRING = 3;
+ private static final int TYPE_INT_DEC = 0x10;
+ private static final int TYPE_INT_HEX = 0x11;
+ private static final int TYPE_INT_BOOLEAN = 0x12;
+
+ private final long mNsId;
+ private final long mNameId;
+ private final int mValueType;
+ private final int mValueData;
+ private final StringPool mStringPool;
+ private final ResourceMap mResourceMap;
+
+ private Attribute(
+ long nsId,
+ long nameId,
+ int valueType,
+ int valueData,
+ StringPool stringPool,
+ ResourceMap resourceMap) {
+ mNsId = nsId;
+ mNameId = nameId;
+ mValueType = valueType;
+ mValueData = valueData;
+ mStringPool = stringPool;
+ mResourceMap = resourceMap;
+ }
+
+ public int getNameResourceId() {
+ return (mResourceMap != null) ? mResourceMap.getResourceId(mNameId) : 0;
+ }
+
+ public String getName() throws XmlParserException {
+ return mStringPool.getString(mNameId);
+ }
+
+ public String getNamespace() throws XmlParserException {
+ return (mNsId != NO_NAMESPACE) ? mStringPool.getString(mNsId) : "";
+ }
+
+ public int getValueType() {
+ return mValueType;
+ }
+
+ public int getIntValue() throws XmlParserException {
+ switch (mValueType) {
+ case TYPE_REFERENCE:
+ case TYPE_INT_DEC:
+ case TYPE_INT_HEX:
+ case TYPE_INT_BOOLEAN:
+ return mValueData;
+ default:
+ throw new XmlParserException("Cannot coerce to int: value type " + mValueType);
+ }
+ }
+
+ public boolean getBooleanValue() throws XmlParserException {
+ switch (mValueType) {
+ case TYPE_INT_BOOLEAN:
+ return mValueData != 0;
+ default:
+ throw new XmlParserException(
+ "Cannot coerce to boolean: value type " + mValueType);
+ }
+ }
+
+ public String getStringValue() throws XmlParserException {
+ switch (mValueType) {
+ case TYPE_STRING:
+ return mStringPool.getString(mValueData & 0xffffffffL);
+ case TYPE_INT_DEC:
+ return Integer.toString(mValueData);
+ case TYPE_INT_HEX:
+ return "0x" + Integer.toHexString(mValueData);
+ case TYPE_INT_BOOLEAN:
+ return Boolean.toString(mValueData != 0);
+ case TYPE_REFERENCE:
+ return "@" + Integer.toHexString(mValueData);
+ default:
+ throw new XmlParserException(
+ "Cannot coerce to string: value type " + mValueType);
+ }
+ }
+ }
+
+ /**
+ * Chunk of a document. Each chunk is tagged with a type and consists of a header followed by
+ * contents.
+ */
+ private static class Chunk {
+ public static final int TYPE_STRING_POOL = 1;
+ public static final int TYPE_RES_XML = 3;
+ public static final int RES_XML_TYPE_START_ELEMENT = 0x0102;
+ public static final int RES_XML_TYPE_END_ELEMENT = 0x0103;
+ public static final int RES_XML_TYPE_RESOURCE_MAP = 0x0180;
+
+ static final int HEADER_MIN_SIZE_BYTES = 8;
+
+ private final int mType;
+ private final ByteBuffer mHeader;
+ private final ByteBuffer mContents;
+
+ public Chunk(int type, ByteBuffer header, ByteBuffer contents) {
+ mType = type;
+ mHeader = header;
+ mContents = contents;
+ }
+
+ public ByteBuffer getContents() {
+ ByteBuffer result = mContents.slice();
+ result.order(mContents.order());
+ return result;
+ }
+
+ public ByteBuffer getHeader() {
+ ByteBuffer result = mHeader.slice();
+ result.order(mHeader.order());
+ return result;
+ }
+
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Consumes the chunk located at the current position of the input and returns the chunk
+ * or {@code null} if there is no chunk left in the input.
+ *
+ * @throws XmlParserException if the chunk is malformed
+ */
+ public static Chunk get(ByteBuffer input) throws XmlParserException {
+ if (input.remaining() < HEADER_MIN_SIZE_BYTES) {
+ // Android ignores the last chunk if its header is too big to fit into the file
+ input.position(input.limit());
+ return null;
+ }
+
+ int originalPosition = input.position();
+ int type = getUnsignedInt16(input);
+ int headerSize = getUnsignedInt16(input);
+ long chunkSize = getUnsignedInt32(input);
+ long chunkRemaining = chunkSize - 8;
+ if (chunkRemaining > input.remaining()) {
+ // Android ignores the last chunk if it's too big to fit into the file
+ input.position(input.limit());
+ return null;
+ }
+ if (headerSize < HEADER_MIN_SIZE_BYTES) {
+ throw new XmlParserException(
+ "Malformed chunk: header too short: " + headerSize + " bytes");
+ } else if (headerSize > chunkSize) {
+ throw new XmlParserException(
+ "Malformed chunk: header too long: " + headerSize + " bytes. Chunk size: "
+ + chunkSize + " bytes");
+ }
+ int contentStartPosition = originalPosition + headerSize;
+ long chunkEndPosition = originalPosition + chunkSize;
+ Chunk chunk =
+ new Chunk(
+ type,
+ sliceFromTo(input, originalPosition, contentStartPosition),
+ sliceFromTo(input, contentStartPosition, chunkEndPosition));
+ input.position((int) chunkEndPosition);
+ return chunk;
+ }
+ }
+
+ /**
+ * String pool of a document. Strings are referenced by their {@code 0}-based index in the pool.
+ */
+ private static class StringPool {
+ private static final int FLAG_UTF8 = 1 << 8;
+
+ private final ByteBuffer mChunkContents;
+ private final ByteBuffer mStringsSection;
+ private final int mStringCount;
+ private final boolean mUtf8Encoded;
+ private final Map mCachedStrings = new HashMap<>();
+
+ /**
+ * Constructs a new string pool from the provided chunk.
+ *
+ * @throws XmlParserException if a parsing error occurred
+ */
+ public StringPool(Chunk chunk) throws XmlParserException {
+ ByteBuffer header = chunk.getHeader();
+ int headerSizeBytes = header.remaining();
+ header.position(Chunk.HEADER_MIN_SIZE_BYTES);
+ if (header.remaining() < 20) {
+ throw new XmlParserException(
+ "XML chunk's header too short. Required at least 20 bytes. Available: "
+ + header.remaining() + " bytes");
+ }
+ long stringCount = getUnsignedInt32(header);
+ if (stringCount > Integer.MAX_VALUE) {
+ throw new XmlParserException("Too many strings: " + stringCount);
+ }
+ mStringCount = (int) stringCount;
+ long styleCount = getUnsignedInt32(header);
+ if (styleCount > Integer.MAX_VALUE) {
+ throw new XmlParserException("Too many styles: " + styleCount);
+ }
+ long flags = getUnsignedInt32(header);
+ long stringsStartOffset = getUnsignedInt32(header);
+ long stylesStartOffset = getUnsignedInt32(header);
+
+ ByteBuffer contents = chunk.getContents();
+ if (mStringCount > 0) {
+ int stringsSectionStartOffsetInContents =
+ (int) (stringsStartOffset - headerSizeBytes);
+ int stringsSectionEndOffsetInContents;
+ if (styleCount > 0) {
+ // Styles section follows the strings section
+ if (stylesStartOffset < stringsStartOffset) {
+ throw new XmlParserException(
+ "Styles offset (" + stylesStartOffset + ") < strings offset ("
+ + stringsStartOffset + ")");
+ }
+ stringsSectionEndOffsetInContents = (int) (stylesStartOffset - headerSizeBytes);
+ } else {
+ stringsSectionEndOffsetInContents = contents.remaining();
+ }
+ mStringsSection =
+ sliceFromTo(
+ contents,
+ stringsSectionStartOffsetInContents,
+ stringsSectionEndOffsetInContents);
+ } else {
+ mStringsSection = ByteBuffer.allocate(0);
+ }
+
+ mUtf8Encoded = (flags & FLAG_UTF8) != 0;
+ mChunkContents = contents;
+ }
+
+ /**
+ * Returns the string located at the specified {@code 0}-based index in this pool.
+ *
+ * @throws XmlParserException if the string does not exist or cannot be decoded
+ */
+ public String getString(long index) throws XmlParserException {
+ if (index < 0) {
+ throw new XmlParserException("Unsuported string index: " + index);
+ } else if (index >= mStringCount) {
+ throw new XmlParserException(
+ "Unsuported string index: " + index + ", max: " + (mStringCount - 1));
+ }
+
+ int idx = (int) index;
+ String result = mCachedStrings.get(idx);
+ if (result != null) {
+ return result;
+ }
+
+ long offsetInStringsSection = getUnsignedInt32(mChunkContents, idx * 4);
+ if (offsetInStringsSection >= mStringsSection.capacity()) {
+ throw new XmlParserException(
+ "Offset of string idx " + idx + " out of bounds: " + offsetInStringsSection
+ + ", max: " + (mStringsSection.capacity() - 1));
+ }
+ mStringsSection.position((int) offsetInStringsSection);
+ result =
+ (mUtf8Encoded)
+ ? getLengthPrefixedUtf8EncodedString(mStringsSection)
+ : getLengthPrefixedUtf16EncodedString(mStringsSection);
+ mCachedStrings.put(idx, result);
+ return result;
+ }
+
+ private static String getLengthPrefixedUtf16EncodedString(ByteBuffer encoded)
+ throws XmlParserException {
+ // If the length (in uint16s) is 0x7fff or lower, it is stored as a single uint16.
+ // Otherwise, it is stored as a big-endian uint32 with highest bit set. Thus, the range
+ // of supported values is 0 to 0x7fffffff inclusive.
+ int lengthChars = getUnsignedInt16(encoded);
+ if ((lengthChars & 0x8000) != 0) {
+ lengthChars = ((lengthChars & 0x7fff) << 16) | getUnsignedInt16(encoded);
+ }
+ if (lengthChars > Integer.MAX_VALUE / 2) {
+ throw new XmlParserException("String too long: " + lengthChars + " uint16s");
+ }
+ int lengthBytes = lengthChars * 2;
+
+ byte[] arr;
+ int arrOffset;
+ if (encoded.hasArray()) {
+ arr = encoded.array();
+ arrOffset = encoded.arrayOffset() + encoded.position();
+ encoded.position(encoded.position() + lengthBytes);
+ } else {
+ arr = new byte[lengthBytes];
+ arrOffset = 0;
+ encoded.get(arr);
+ }
+ // Reproduce the behavior of Android runtime which requires that the UTF-16 encoded
+ // array of bytes is NULL terminated.
+ if ((arr[arrOffset + lengthBytes] != 0)
+ || (arr[arrOffset + lengthBytes + 1] != 0)) {
+ throw new XmlParserException("UTF-16 encoded form of string not NULL terminated");
+ }
+ try {
+ return new String(arr, arrOffset, lengthBytes, "UTF-16LE");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("UTF-16LE character encoding not supported", e);
+ }
+ }
+
+ private static String getLengthPrefixedUtf8EncodedString(ByteBuffer encoded)
+ throws XmlParserException {
+ // If the length (in bytes) is 0x7f or lower, it is stored as a single uint8. Otherwise,
+ // it is stored as a big-endian uint16 with highest bit set. Thus, the range of
+ // supported values is 0 to 0x7fff inclusive.
+
+ // Skip UTF-16 encoded length (in uint16s)
+ int lengthBytes = getUnsignedInt8(encoded);
+ if ((lengthBytes & 0x80) != 0) {
+ lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
+ }
+
+ // Read UTF-8 encoded length (in bytes)
+ lengthBytes = getUnsignedInt8(encoded);
+ if ((lengthBytes & 0x80) != 0) {
+ lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
+ }
+
+ byte[] arr;
+ int arrOffset;
+ if (encoded.hasArray()) {
+ arr = encoded.array();
+ arrOffset = encoded.arrayOffset() + encoded.position();
+ encoded.position(encoded.position() + lengthBytes);
+ } else {
+ arr = new byte[lengthBytes];
+ arrOffset = 0;
+ encoded.get(arr);
+ }
+ // Reproduce the behavior of Android runtime which requires that the UTF-8 encoded array
+ // of bytes is NULL terminated.
+ if (arr[arrOffset + lengthBytes] != 0) {
+ throw new XmlParserException("UTF-8 encoded form of string not NULL terminated");
+ }
+ try {
+ return new String(arr, arrOffset, lengthBytes, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("UTF-8 character encoding not supported", e);
+ }
+ }
+ }
+
+ /**
+ * Resource map of a document. Resource IDs are referenced by their {@code 0}-based index in the
+ * map.
+ */
+ private static class ResourceMap {
+ private final ByteBuffer mChunkContents;
+ private final int mEntryCount;
+
+ /**
+ * Constructs a new resource map from the provided chunk.
+ *
+ * @throws XmlParserException if a parsing error occurred
+ */
+ public ResourceMap(Chunk chunk) throws XmlParserException {
+ mChunkContents = chunk.getContents().slice();
+ mChunkContents.order(chunk.getContents().order());
+ // Each entry of the map is four bytes long, containing the int32 resource ID.
+ mEntryCount = mChunkContents.remaining() / 4;
+ }
+
+ /**
+ * Returns the resource ID located at the specified {@code 0}-based index in this pool or
+ * {@code 0} if the index is out of range.
+ */
+ public int getResourceId(long index) {
+ if ((index < 0) || (index >= mEntryCount)) {
+ return 0;
+ }
+ int idx = (int) index;
+ // Each entry of the map is four bytes long, containing the int32 resource ID.
+ return mChunkContents.getInt(idx * 4);
+ }
+ }
+
+ /**
+ * Returns new byte buffer whose content is a shared subsequence of this buffer's content
+ * between the specified start (inclusive) and end (exclusive) positions. As opposed to
+ * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
+ * buffer's byte order.
+ */
+ private static ByteBuffer sliceFromTo(ByteBuffer source, long start, long end) {
+ if (start < 0) {
+ throw new IllegalArgumentException("start: " + start);
+ }
+ if (end < start) {
+ throw new IllegalArgumentException("end < start: " + end + " < " + start);
+ }
+ int capacity = source.capacity();
+ if (end > source.capacity()) {
+ throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
+ }
+ return sliceFromTo(source, (int) start, (int) end);
+ }
+
+ /**
+ * Returns new byte buffer whose content is a shared subsequence of this buffer's content
+ * between the specified start (inclusive) and end (exclusive) positions. As opposed to
+ * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
+ * buffer's byte order.
+ */
+ private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
+ if (start < 0) {
+ throw new IllegalArgumentException("start: " + start);
+ }
+ if (end < start) {
+ throw new IllegalArgumentException("end < start: " + end + " < " + start);
+ }
+ int capacity = source.capacity();
+ if (end > source.capacity()) {
+ throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
+ }
+ int originalLimit = source.limit();
+ int originalPosition = source.position();
+ try {
+ source.position(0);
+ source.limit(end);
+ source.position(start);
+ ByteBuffer result = source.slice();
+ result.order(source.order());
+ return result;
+ } finally {
+ source.position(0);
+ source.limit(originalLimit);
+ source.position(originalPosition);
+ }
+ }
+
+ private static int getUnsignedInt8(ByteBuffer buffer) {
+ return buffer.get() & 0xff;
+ }
+
+ private static int getUnsignedInt16(ByteBuffer buffer) {
+ return buffer.getShort() & 0xffff;
+ }
+
+ private static long getUnsignedInt32(ByteBuffer buffer) {
+ return buffer.getInt() & 0xffffffffL;
+ }
+
+ private static long getUnsignedInt32(ByteBuffer buffer, int position) {
+ return buffer.getInt(position) & 0xffffffffL;
+ }
+
+ /**
+ * Indicates that an error occurred while parsing a document.
+ */
+ public static class XmlParserException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public XmlParserException(String message) {
+ super(message);
+ }
+
+ public XmlParserException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java b/app/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
new file mode 100644
index 0000000..cc69af3
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
@@ -0,0 +1,1356 @@
+/*
+ * Copyright (C) 2018 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.internal.apk;
+
+import com.android.apksig.ApkVerifier;
+import com.android.apksig.SigningCertificateLineage;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkSigningBlockNotFoundException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.util.ByteBufferDataSource;
+import com.android.apksig.internal.util.ChainedDataSource;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.util.VerityTreeBuilder;
+import com.android.apksig.internal.zip.ZipUtils;
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSinks;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+
+import com.android.apksig.util.RunnablesExecutor;
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.DigestException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public class ApkSigningBlockUtils {
+
+ private static final char[] HEX_DIGITS = "01234567890abcdef".toCharArray();
+ private static final long CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
+ public static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096;
+ public static final byte[] APK_SIGNING_BLOCK_MAGIC =
+ new byte[] {
+ 0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20,
+ 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32,
+ };
+ private static final int VERITY_PADDING_BLOCK_ID = 0x42726577;
+
+ public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
+
+
+ /**
+ * Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
+ * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
+ */
+ public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
+ ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm();
+ ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm();
+ return compareContentDigestAlgorithm(digestAlg1, digestAlg2);
+ }
+
+ /**
+ * Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number
+ * if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference.
+ */
+ private static int compareContentDigestAlgorithm(
+ ContentDigestAlgorithm alg1,
+ ContentDigestAlgorithm alg2) {
+ switch (alg1) {
+ case CHUNKED_SHA256:
+ switch (alg2) {
+ case CHUNKED_SHA256:
+ return 0;
+ case CHUNKED_SHA512:
+ case VERITY_CHUNKED_SHA256:
+ return -1;
+ default:
+ throw new IllegalArgumentException("Unknown alg2: " + alg2);
+ }
+ case CHUNKED_SHA512:
+ switch (alg2) {
+ case CHUNKED_SHA256:
+ case VERITY_CHUNKED_SHA256:
+ return 1;
+ case CHUNKED_SHA512:
+ return 0;
+ default:
+ throw new IllegalArgumentException("Unknown alg2: " + alg2);
+ }
+ case VERITY_CHUNKED_SHA256:
+ switch (alg2) {
+ case CHUNKED_SHA256:
+ return 1;
+ case VERITY_CHUNKED_SHA256:
+ return 0;
+ case CHUNKED_SHA512:
+ return -1;
+ default:
+ throw new IllegalArgumentException("Unknown alg2: " + alg2);
+ }
+ default:
+ throw new IllegalArgumentException("Unknown alg1: " + alg1);
+ }
+ }
+
+
+
+ /**
+ * Verifies integrity of the APK outside of the APK Signing Block by computing digests of the
+ * APK and comparing them against the digests listed in APK Signing Block. The expected digests
+ * are taken from {@code SignerInfos} of the provided {@code result}.
+ *
+ * This method adds one or more errors to the {@code result} if a verification error is
+ * expected to be encountered on Android. No errors are added to the {@code result} if the APK's
+ * integrity is expected to verify on Android for each algorithm in
+ * {@code contentDigestAlgorithms}.
+ *
+ *
The reason this method is currently not parameterized by a
+ * {@code [minSdkVersion, maxSdkVersion]} range is that up until now content digest algorithms
+ * exhibit the same behavior on all Android platform versions.
+ */
+ public static void verifyIntegrity(
+ RunnablesExecutor executor,
+ DataSource beforeApkSigningBlock,
+ DataSource centralDir,
+ ByteBuffer eocd,
+ Set contentDigestAlgorithms,
+ Result result) throws IOException, NoSuchAlgorithmException {
+ if (contentDigestAlgorithms.isEmpty()) {
+ // This should never occur because this method is invoked once at least one signature
+ // is verified, meaning at least one content digest is known.
+ throw new RuntimeException("No content digests found");
+ }
+
+ // For the purposes of verifying integrity, ZIP End of Central Directory (EoCD) must be
+ // treated as though its Central Directory offset points to the start of APK Signing Block.
+ // We thus modify the EoCD accordingly.
+ ByteBuffer modifiedEocd = ByteBuffer.allocate(eocd.remaining());
+ int eocdSavedPos = eocd.position();
+ modifiedEocd.order(ByteOrder.LITTLE_ENDIAN);
+ modifiedEocd.put(eocd);
+ modifiedEocd.flip();
+
+ // restore eocd to position prior to modification in case it is to be used elsewhere
+ eocd.position(eocdSavedPos);
+ ZipUtils.setZipEocdCentralDirectoryOffset(modifiedEocd, beforeApkSigningBlock.size());
+ Map actualContentDigests;
+ try {
+ actualContentDigests =
+ computeContentDigests(
+ executor,
+ contentDigestAlgorithms,
+ beforeApkSigningBlock,
+ centralDir,
+ new ByteBufferDataSource(modifiedEocd));
+ // Special checks for the verity algorithm requirements.
+ if (actualContentDigests.containsKey(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) {
+ if ((beforeApkSigningBlock.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0)) {
+ throw new RuntimeException(
+ "APK Signing Block is not aligned on 4k boundary: " +
+ beforeApkSigningBlock.size());
+ }
+
+ long centralDirOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocd);
+ long signingBlockSize = centralDirOffset - beforeApkSigningBlock.size();
+ if (signingBlockSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) {
+ throw new RuntimeException(
+ "APK Signing Block size is not multiple of page size: " +
+ signingBlockSize);
+ }
+ }
+ } catch (DigestException e) {
+ throw new RuntimeException("Failed to compute content digests", e);
+ }
+ if (!contentDigestAlgorithms.equals(actualContentDigests.keySet())) {
+ throw new RuntimeException(
+ "Mismatch between sets of requested and computed content digests"
+ + " . Requested: " + contentDigestAlgorithms
+ + ", computed: " + actualContentDigests.keySet());
+ }
+
+ // Compare digests computed over the rest of APK against the corresponding expected digests
+ // in signer blocks.
+ for (Result.SignerInfo signerInfo : result.signers) {
+ for (Result.SignerInfo.ContentDigest expected : signerInfo.contentDigests) {
+ SignatureAlgorithm signatureAlgorithm =
+ SignatureAlgorithm.findById(expected.getSignatureAlgorithmId());
+ if (signatureAlgorithm == null) {
+ continue;
+ }
+ ContentDigestAlgorithm contentDigestAlgorithm =
+ signatureAlgorithm.getContentDigestAlgorithm();
+ // if the current digest algorithm is not in the list provided by the caller then
+ // ignore it; the signer may contain digests not recognized by the specified SDK
+ // range.
+ if (!contentDigestAlgorithms.contains(contentDigestAlgorithm)) {
+ continue;
+ }
+ byte[] expectedDigest = expected.getValue();
+ byte[] actualDigest = actualContentDigests.get(contentDigestAlgorithm);
+ if (!Arrays.equals(expectedDigest, actualDigest)) {
+ if (result.signatureSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2) {
+ signerInfo.addError(
+ ApkVerifier.Issue.V2_SIG_APK_DIGEST_DID_NOT_VERIFY,
+ contentDigestAlgorithm,
+ toHex(expectedDigest),
+ toHex(actualDigest));
+ } else if (result.signatureSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3) {
+ signerInfo.addError(
+ ApkVerifier.Issue.V3_SIG_APK_DIGEST_DID_NOT_VERIFY,
+ contentDigestAlgorithm,
+ toHex(expectedDigest),
+ toHex(actualDigest));
+ }
+ continue;
+ }
+ signerInfo.verifiedContentDigests.put(contentDigestAlgorithm, actualDigest);
+ }
+ }
+ }
+
+ public static ByteBuffer findApkSignatureSchemeBlock(
+ ByteBuffer apkSigningBlock,
+ int blockId,
+ Result result) throws SignatureNotFoundException {
+ checkByteOrderLittleEndian(apkSigningBlock);
+ // FORMAT:
+ // OFFSET DATA TYPE DESCRIPTION
+ // * @+0 bytes uint64: size in bytes (excluding this field)
+ // * @+8 bytes pairs
+ // * @-24 bytes uint64: size in bytes (same as the one above)
+ // * @-16 bytes uint128: magic
+ ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
+
+ int entryCount = 0;
+ while (pairs.hasRemaining()) {
+ entryCount++;
+ if (pairs.remaining() < 8) {
+ throw new SignatureNotFoundException(
+ "Insufficient data to read size of APK Signing Block entry #" + entryCount);
+ }
+ long lenLong = pairs.getLong();
+ if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
+ throw new SignatureNotFoundException(
+ "APK Signing Block entry #" + entryCount
+ + " size out of range: " + lenLong);
+ }
+ int len = (int) lenLong;
+ int nextEntryPos = pairs.position() + len;
+ if (len > pairs.remaining()) {
+ throw new SignatureNotFoundException(
+ "APK Signing Block entry #" + entryCount + " size out of range: " + len
+ + ", available: " + pairs.remaining());
+ }
+ int id = pairs.getInt();
+ if (id == blockId) {
+ return getByteBuffer(pairs, len - 4);
+ }
+ pairs.position(nextEntryPos);
+ }
+
+ throw new SignatureNotFoundException(
+ "No APK Signature Scheme block in APK Signing Block with ID: " + blockId);
+ }
+
+ public static void checkByteOrderLittleEndian(ByteBuffer buffer) {
+ if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
+ throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
+ }
+ }
+
+ /**
+ * Returns new byte buffer whose content is a shared subsequence of this buffer's content
+ * between the specified start (inclusive) and end (exclusive) positions. As opposed to
+ * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
+ * buffer's byte order.
+ */
+ private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
+ if (start < 0) {
+ throw new IllegalArgumentException("start: " + start);
+ }
+ if (end < start) {
+ throw new IllegalArgumentException("end < start: " + end + " < " + start);
+ }
+ int capacity = source.capacity();
+ if (end > source.capacity()) {
+ throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
+ }
+ int originalLimit = source.limit();
+ int originalPosition = source.position();
+ try {
+ source.position(0);
+ source.limit(end);
+ source.position(start);
+ ByteBuffer result = source.slice();
+ result.order(source.order());
+ return result;
+ } finally {
+ source.position(0);
+ source.limit(originalLimit);
+ source.position(originalPosition);
+ }
+ }
+
+ /**
+ * Relative get method for reading {@code size} number of bytes from the current
+ * position of this buffer.
+ *
+ * This method reads the next {@code size} bytes at this buffer's current position,
+ * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
+ * {@code size}, byte order set to this buffer's byte order; and then increments the position by
+ * {@code size}.
+ */
+ private static ByteBuffer getByteBuffer(ByteBuffer source, int size) {
+ if (size < 0) {
+ throw new IllegalArgumentException("size: " + size);
+ }
+ int originalLimit = source.limit();
+ int position = source.position();
+ int limit = position + size;
+ if ((limit < position) || (limit > originalLimit)) {
+ throw new BufferUnderflowException();
+ }
+ source.limit(limit);
+ try {
+ ByteBuffer result = source.slice();
+ result.order(source.order());
+ source.position(limit);
+ return result;
+ } finally {
+ source.limit(originalLimit);
+ }
+ }
+
+ public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException {
+ if (source.remaining() < 4) {
+ throw new ApkFormatException(
+ "Remaining buffer too short to contain length of length-prefixed field"
+ + ". Remaining: " + source.remaining());
+ }
+ int len = source.getInt();
+ if (len < 0) {
+ throw new IllegalArgumentException("Negative length");
+ } else if (len > source.remaining()) {
+ throw new ApkFormatException(
+ "Length-prefixed field longer than remaining buffer"
+ + ". Field length: " + len + ", remaining: " + source.remaining());
+ }
+ return getByteBuffer(source, len);
+ }
+
+ public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException {
+ int len = buf.getInt();
+ if (len < 0) {
+ throw new ApkFormatException("Negative length");
+ } else if (len > buf.remaining()) {
+ throw new ApkFormatException(
+ "Underflow while reading length-prefixed value. Length: " + len
+ + ", available: " + buf.remaining());
+ }
+ byte[] result = new byte[len];
+ buf.get(result);
+ return result;
+ }
+
+ public static String toHex(byte[] value) {
+ StringBuilder sb = new StringBuilder(value.length * 2);
+ int len = value.length;
+ for (int i = 0; i < len; i++) {
+ int hi = (value[i] & 0xff) >>> 4;
+ int lo = value[i] & 0x0f;
+ sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]);
+ }
+ return sb.toString();
+ }
+
+ public static Map computeContentDigests(
+ RunnablesExecutor executor,
+ Set digestAlgorithms,
+ DataSource beforeCentralDir,
+ DataSource centralDir,
+ DataSource eocd) throws IOException, NoSuchAlgorithmException, DigestException {
+ Map contentDigests = new HashMap<>();
+ Set oneMbChunkBasedAlgorithm = digestAlgorithms.stream()
+ .filter(a -> a == ContentDigestAlgorithm.CHUNKED_SHA256 ||
+ a == ContentDigestAlgorithm.CHUNKED_SHA512)
+ .collect(Collectors.toSet());
+ computeOneMbChunkContentDigests(
+ executor,
+ oneMbChunkBasedAlgorithm,
+ new DataSource[] { beforeCentralDir, centralDir, eocd },
+ contentDigests);
+
+ if (digestAlgorithms.contains(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) {
+ computeApkVerityDigest(beforeCentralDir, centralDir, eocd, contentDigests);
+ }
+ return contentDigests;
+ }
+
+ static void computeOneMbChunkContentDigests(
+ Set digestAlgorithms,
+ DataSource[] contents,
+ Map outputContentDigests)
+ throws IOException, NoSuchAlgorithmException, DigestException {
+ // For each digest algorithm the result is computed as follows:
+ // 1. Each segment of contents is split into consecutive chunks of 1 MB in size.
+ // The final chunk will be shorter iff the length of segment is not a multiple of 1 MB.
+ // No chunks are produced for empty (zero length) segments.
+ // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's
+ // length in bytes (uint32 little-endian) and the chunk's contents.
+ // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of
+ // chunks (uint32 little-endian) and the concatenation of digests of chunks of all
+ // segments in-order.
+
+ long chunkCountLong = 0;
+ for (DataSource input : contents) {
+ chunkCountLong +=
+ getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
+ }
+ if (chunkCountLong > Integer.MAX_VALUE) {
+ throw new DigestException("Input too long: " + chunkCountLong + " chunks");
+ }
+ int chunkCount = (int) chunkCountLong;
+
+ ContentDigestAlgorithm[] digestAlgorithmsArray =
+ digestAlgorithms.toArray(new ContentDigestAlgorithm[digestAlgorithms.size()]);
+ MessageDigest[] mds = new MessageDigest[digestAlgorithmsArray.length];
+ byte[][] digestsOfChunks = new byte[digestAlgorithmsArray.length][];
+ int[] digestOutputSizes = new int[digestAlgorithmsArray.length];
+ for (int i = 0; i < digestAlgorithmsArray.length; i++) {
+ ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i];
+ int digestOutputSizeBytes = digestAlgorithm.getChunkDigestOutputSizeBytes();
+ digestOutputSizes[i] = digestOutputSizeBytes;
+ byte[] concatenationOfChunkCountAndChunkDigests =
+ new byte[5 + chunkCount * digestOutputSizeBytes];
+ concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
+ setUnsignedInt32LittleEndian(
+ chunkCount, concatenationOfChunkCountAndChunkDigests, 1);
+ digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests;
+ String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm();
+ mds[i] = MessageDigest.getInstance(jcaAlgorithm);
+ }
+
+ DataSink mdSink = DataSinks.asDataSink(mds);
+ byte[] chunkContentPrefix = new byte[5];
+ chunkContentPrefix[0] = (byte) 0xa5;
+ int chunkIndex = 0;
+ // Optimization opportunity: digests of chunks can be computed in parallel. However,
+ // determining the number of computations to be performed in parallel is non-trivial. This
+ // depends on a wide range of factors, such as data source type (e.g., in-memory or fetched
+ // from file), CPU/memory/disk cache bandwidth and latency, interconnect architecture of CPU
+ // cores, load on the system from other threads of execution and other processes, size of
+ // input.
+ // For now, we compute these digests sequentially and thus have the luxury of improving
+ // performance by writing the digest of each chunk into a pre-allocated buffer at exactly
+ // the right position. This avoids unnecessary allocations, copying, and enables the final
+ // digest to be more efficient because it's presented with all of its input in one go.
+ for (DataSource input : contents) {
+ long inputOffset = 0;
+ long inputRemaining = input.size();
+ while (inputRemaining > 0) {
+ int chunkSize =
+ (int) Math.min(inputRemaining, CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
+ setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1);
+ for (int i = 0; i < mds.length; i++) {
+ mds[i].update(chunkContentPrefix);
+ }
+ try {
+ input.feed(inputOffset, chunkSize, mdSink);
+ } catch (IOException e) {
+ throw new IOException("Failed to read chunk #" + chunkIndex, e);
+ }
+ for (int i = 0; i < digestAlgorithmsArray.length; i++) {
+ MessageDigest md = mds[i];
+ byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i];
+ int expectedDigestSizeBytes = digestOutputSizes[i];
+ int actualDigestSizeBytes =
+ md.digest(
+ concatenationOfChunkCountAndChunkDigests,
+ 5 + chunkIndex * expectedDigestSizeBytes,
+ expectedDigestSizeBytes);
+ if (actualDigestSizeBytes != expectedDigestSizeBytes) {
+ throw new RuntimeException(
+ "Unexpected output size of " + md.getAlgorithm()
+ + " digest: " + actualDigestSizeBytes);
+ }
+ }
+ inputOffset += chunkSize;
+ inputRemaining -= chunkSize;
+ chunkIndex++;
+ }
+ }
+
+ for (int i = 0; i < digestAlgorithmsArray.length; i++) {
+ ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i];
+ byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i];
+ MessageDigest md = mds[i];
+ byte[] digest = md.digest(concatenationOfChunkCountAndChunkDigests);
+ outputContentDigests.put(digestAlgorithm, digest);
+ }
+ }
+
+ static void computeOneMbChunkContentDigests(
+ RunnablesExecutor executor,
+ Set digestAlgorithms,
+ DataSource[] contents,
+ Map outputContentDigests)
+ throws NoSuchAlgorithmException, DigestException {
+ long chunkCountLong = 0;
+ for (DataSource input : contents) {
+ chunkCountLong +=
+ getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
+ }
+ if (chunkCountLong > Integer.MAX_VALUE) {
+ throw new DigestException("Input too long: " + chunkCountLong + " chunks");
+ }
+ int chunkCount = (int) chunkCountLong;
+
+ List chunkDigestsList = new ArrayList<>(digestAlgorithms.size());
+ for (ContentDigestAlgorithm algorithms : digestAlgorithms) {
+ chunkDigestsList.add(new ChunkDigests(algorithms, chunkCount));
+ }
+
+ ChunkSupplier chunkSupplier = new ChunkSupplier(contents);
+ executor.execute(() -> new ChunkDigester(chunkSupplier, chunkDigestsList));
+
+ // Compute and write out final digest for each algorithm.
+ for (ChunkDigests chunkDigests : chunkDigestsList) {
+ MessageDigest messageDigest = chunkDigests.createMessageDigest();
+ outputContentDigests.put(
+ chunkDigests.algorithm,
+ messageDigest.digest(chunkDigests.concatOfDigestsOfChunks));
+ }
+ }
+
+ private static class ChunkDigests {
+ private final ContentDigestAlgorithm algorithm;
+ private final int digestOutputSize;
+ private final byte[] concatOfDigestsOfChunks;
+
+ private ChunkDigests(ContentDigestAlgorithm algorithm, int chunkCount) {
+ this.algorithm = algorithm;
+ digestOutputSize = this.algorithm.getChunkDigestOutputSizeBytes();
+ concatOfDigestsOfChunks = new byte[1 + 4 + chunkCount * digestOutputSize];
+
+ // Fill the initial values of the concatenated digests of chunks, which is
+ // {0x5a, 4-bytes-of-little-endian-chunk-count, digests*...}.
+ concatOfDigestsOfChunks[0] = 0x5a;
+ setUnsignedInt32LittleEndian(chunkCount, concatOfDigestsOfChunks, 1);
+ }
+
+ private MessageDigest createMessageDigest() throws NoSuchAlgorithmException {
+ return MessageDigest.getInstance(algorithm.getJcaMessageDigestAlgorithm());
+ }
+
+ private int getOffset(int chunkIndex) {
+ return 1 + 4 + chunkIndex * digestOutputSize;
+ }
+ }
+
+ /**
+ * A per-thread digest worker.
+ */
+ private static class ChunkDigester implements Runnable {
+ private final ChunkSupplier dataSupplier;
+ private final List chunkDigests;
+ private final List messageDigests;
+ private final DataSink mdSink;
+
+ private ChunkDigester(ChunkSupplier dataSupplier, List chunkDigests) {
+ this.dataSupplier = dataSupplier;
+ this.chunkDigests = chunkDigests;
+ messageDigests = new ArrayList<>(chunkDigests.size());
+ for (ChunkDigests chunkDigest : chunkDigests) {
+ try {
+ messageDigests.add(chunkDigest.createMessageDigest());
+ } catch (NoSuchAlgorithmException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+ mdSink = DataSinks.asDataSink(messageDigests.toArray(new MessageDigest[0]));
+ }
+
+ @Override
+ public void run() {
+ byte[] chunkContentPrefix = new byte[5];
+ chunkContentPrefix[0] = (byte) 0xa5;
+
+ try {
+ for (ChunkSupplier.Chunk chunk = dataSupplier.get();
+ chunk != null;
+ chunk = dataSupplier.get()) {
+ long size = chunk.dataSource.size();
+ if (size > CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES) {
+ throw new RuntimeException("Chunk size greater than expected: " + size);
+ }
+
+ // First update with the chunk prefix.
+ setUnsignedInt32LittleEndian((int)size, chunkContentPrefix, 1);
+ mdSink.consume(chunkContentPrefix, 0, chunkContentPrefix.length);
+
+ // Then update with the chunk data.
+ chunk.dataSource.feed(0, size, mdSink);
+
+ // Now finalize chunk for all algorithms.
+ for (int i = 0; i < chunkDigests.size(); i++) {
+ ChunkDigests chunkDigest = chunkDigests.get(i);
+ int actualDigestSize = messageDigests.get(i).digest(
+ chunkDigest.concatOfDigestsOfChunks,
+ chunkDigest.getOffset(chunk.chunkIndex),
+ chunkDigest.digestOutputSize);
+ if (actualDigestSize != chunkDigest.digestOutputSize) {
+ throw new RuntimeException(
+ "Unexpected output size of " + chunkDigest.algorithm
+ + " digest: " + actualDigestSize);
+ }
+ }
+ }
+ } catch (IOException | DigestException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * Thread-safe 1MB DataSource chunk supplier. When bounds are met in a
+ * supplied {@link DataSource}, the data from the next {@link DataSource}
+ * are NOT concatenated. Only the next call to get() will fetch from the
+ * next {@link DataSource} in the input {@link DataSource} array.
+ */
+ private static class ChunkSupplier implements Supplier {
+ private final DataSource[] dataSources;
+ private final int[] chunkCounts;
+ private final int totalChunkCount;
+ private final AtomicInteger nextIndex;
+
+ private ChunkSupplier(DataSource[] dataSources) {
+ this.dataSources = dataSources;
+ chunkCounts = new int[dataSources.length];
+ int totalChunkCount = 0;
+ for (int i = 0; i < dataSources.length; i++) {
+ long chunkCount = getChunkCount(dataSources[i].size(),
+ CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
+ if (chunkCount > Integer.MAX_VALUE) {
+ throw new RuntimeException(
+ String.format(
+ "Number of chunks in dataSource[%d] is greater than max int.",
+ i));
+ }
+ chunkCounts[i] = (int)chunkCount;
+ totalChunkCount += chunkCount;
+ }
+ this.totalChunkCount = totalChunkCount;
+ nextIndex = new AtomicInteger(0);
+ }
+
+ /**
+ * We map an integer index to the termination-adjusted dataSources 1MB chunks.
+ * Note that {@link Chunk}s could be less than 1MB, namely the last 1MB-aligned
+ * blocks in each input {@link DataSource} (unless the DataSource itself is
+ * 1MB-aligned).
+ */
+ @Override
+ public ChunkSupplier.Chunk get() {
+ int index = nextIndex.getAndIncrement();
+ if (index < 0 || index >= totalChunkCount) {
+ return null;
+ }
+
+ int dataSourceIndex = 0;
+ int dataSourceChunkOffset = index;
+ for (; dataSourceIndex < dataSources.length; dataSourceIndex++) {
+ if (dataSourceChunkOffset < chunkCounts[dataSourceIndex]) {
+ break;
+ }
+ dataSourceChunkOffset -= chunkCounts[dataSourceIndex];
+ }
+
+ long remainingSize = Math.min(
+ dataSources[dataSourceIndex].size() -
+ dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES,
+ CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
+ // Note that slicing may involve its own locking. We may wish to reimplement the
+ // underlying mechanism to get rid of that lock (e.g. ByteBufferDataSource should
+ // probably get reimplemented to a delegate model, such that grabbing a slice
+ // doesn't incur a lock).
+ return new Chunk(
+ dataSources[dataSourceIndex].slice(
+ dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES,
+ remainingSize),
+ index);
+ }
+
+ static class Chunk {
+ private final int chunkIndex;
+ private final DataSource dataSource;
+
+ private Chunk(DataSource parentSource, int chunkIndex) {
+ this.chunkIndex = chunkIndex;
+ dataSource = parentSource;
+ }
+ }
+ }
+
+ private static void computeApkVerityDigest(DataSource beforeCentralDir, DataSource centralDir,
+ DataSource eocd, Map outputContentDigests)
+ throws IOException, NoSuchAlgorithmException {
+ // FORMAT:
+ // OFFSET DATA TYPE DESCRIPTION
+ // * @+0 bytes uint8[32] Merkle tree root hash of SHA-256
+ // * @+32 bytes int64 Length of source data
+ int backBufferSize =
+ ContentDigestAlgorithm.VERITY_CHUNKED_SHA256.getChunkDigestOutputSizeBytes() +
+ Long.SIZE / Byte.SIZE;
+ ByteBuffer encoded = ByteBuffer.allocate(backBufferSize);
+ encoded.order(ByteOrder.LITTLE_ENDIAN);
+
+ // Use 0s as salt for now. This also needs to be consistent in the fsverify header for
+ // kernel to use.
+ VerityTreeBuilder builder = new VerityTreeBuilder(new byte[8]);
+ byte[] rootHash = builder.generateVerityTreeRootHash(beforeCentralDir, centralDir, eocd);
+ encoded.put(rootHash);
+ encoded.putLong(beforeCentralDir.size() + centralDir.size() + eocd.size());
+
+ outputContentDigests.put(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256, encoded.array());
+ }
+
+ private static long getChunkCount(long inputSize, long chunkSize) {
+ return (inputSize + chunkSize - 1) / chunkSize;
+ }
+
+ private static void setUnsignedInt32LittleEndian(int value, byte[] result, int offset) {
+ result[offset] = (byte) (value & 0xff);
+ result[offset + 1] = (byte) ((value >> 8) & 0xff);
+ result[offset + 2] = (byte) ((value >> 16) & 0xff);
+ result[offset + 3] = (byte) ((value >> 24) & 0xff);
+ }
+
+ public static byte[] encodePublicKey(PublicKey publicKey)
+ throws InvalidKeyException, NoSuchAlgorithmException {
+ byte[] encodedPublicKey = null;
+ if ("X.509".equals(publicKey.getFormat())) {
+ encodedPublicKey = publicKey.getEncoded();
+ }
+ if (encodedPublicKey == null) {
+ try {
+ encodedPublicKey =
+ KeyFactory.getInstance(publicKey.getAlgorithm())
+ .getKeySpec(publicKey, X509EncodedKeySpec.class)
+ .getEncoded();
+ } catch (InvalidKeySpecException e) {
+ throw new InvalidKeyException(
+ "Failed to obtain X.509 encoded form of public key " + publicKey
+ + " of class " + publicKey.getClass().getName(),
+ e);
+ }
+ }
+ if ((encodedPublicKey == null) || (encodedPublicKey.length == 0)) {
+ throw new InvalidKeyException(
+ "Failed to obtain X.509 encoded form of public key " + publicKey
+ + " of class " + publicKey.getClass().getName());
+ }
+ return encodedPublicKey;
+ }
+
+ public static List encodeCertificates(List certificates)
+ throws CertificateEncodingException {
+ List result = new ArrayList<>(certificates.size());
+ for (X509Certificate certificate : certificates) {
+ result.add(certificate.getEncoded());
+ }
+ return result;
+ }
+
+ public static byte[] encodeAsLengthPrefixedElement(byte[] bytes) {
+ byte[][] adapterBytes = new byte[1][];
+ adapterBytes[0] = bytes;
+ return encodeAsSequenceOfLengthPrefixedElements(adapterBytes);
+ }
+
+ public static byte[] encodeAsSequenceOfLengthPrefixedElements(List sequence) {
+ return encodeAsSequenceOfLengthPrefixedElements(
+ sequence.toArray(new byte[sequence.size()][]));
+ }
+
+ public static byte[] encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence) {
+ int payloadSize = 0;
+ for (byte[] element : sequence) {
+ payloadSize += 4 + element.length;
+ }
+ ByteBuffer result = ByteBuffer.allocate(payloadSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ for (byte[] element : sequence) {
+ result.putInt(element.length);
+ result.put(element);
+ }
+ return result.array();
+ }
+
+ public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ List> sequence) {
+ int resultSize = 0;
+ for (Pair element : sequence) {
+ resultSize += 12 + element.getSecond().length;
+ }
+ ByteBuffer result = ByteBuffer.allocate(resultSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ for (Pair element : sequence) {
+ byte[] second = element.getSecond();
+ result.putInt(8 + second.length);
+ result.putInt(element.getFirst());
+ result.putInt(second.length);
+ result.put(second);
+ }
+ return result.array();
+ }
+
+ /**
+ * Returns the APK Signature Scheme block contained in the provided APK file for the given ID
+ * and the additional information relevant for verifying the block against the file.
+ *
+ * @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs
+ * identifying the appropriate block to find, e.g. the APK Signature Scheme v2
+ * block ID.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme
+ * @throws IOException if an I/O error occurs while reading the APK
+ */
+ public static SignatureInfo findSignature(
+ DataSource apk, ApkUtils.ZipSections zipSections, int blockId, Result result)
+ throws IOException, SignatureNotFoundException {
+ // Find the APK Signing Block.
+ DataSource apkSigningBlock;
+ long apkSigningBlockOffset;
+ try {
+ ApkUtils.ApkSigningBlock apkSigningBlockInfo =
+ ApkUtils.findApkSigningBlock(apk, zipSections);
+ apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset();
+ apkSigningBlock = apkSigningBlockInfo.getContents();
+ } catch (ApkSigningBlockNotFoundException e) {
+ throw new SignatureNotFoundException(e.getMessage(), e);
+ }
+ ByteBuffer apkSigningBlockBuf =
+ apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size());
+ apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN);
+
+ // Find the APK Signature Scheme Block inside the APK Signing Block.
+ ByteBuffer apkSignatureSchemeBlock =
+ findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId, result);
+ return new SignatureInfo(
+ apkSignatureSchemeBlock,
+ apkSigningBlockOffset,
+ zipSections.getZipCentralDirectoryOffset(),
+ zipSections.getZipEndOfCentralDirectoryOffset(),
+ zipSections.getZipEndOfCentralDirectory());
+ }
+
+ /**
+ * Generates a new DataSource representing the APK contents before the Central Directory with
+ * padding, if padding is requested. If the existing data entries before the Central Directory
+ * are already aligned, or no padding is requested, the original DataSource is used. This
+ * padding is used to allow for verity-based APK verification.
+ *
+ * @return {@code Pair} containing the potentially new {@code DataSource} and the amount of
+ * padding used.
+ */
+ public static Pair generateApkSigningBlockPadding(
+ DataSource beforeCentralDir,
+ boolean apkSigningBlockPaddingSupported) {
+
+ // Ensure APK Signing Block starts from page boundary.
+ int padSizeBeforeSigningBlock = 0;
+ if (apkSigningBlockPaddingSupported &&
+ (beforeCentralDir.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0)) {
+ padSizeBeforeSigningBlock = (int) (
+ ANDROID_COMMON_PAGE_ALIGNMENT_BYTES -
+ beforeCentralDir.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES);
+ beforeCentralDir = new ChainedDataSource(
+ beforeCentralDir,
+ DataSources.asDataSource(
+ ByteBuffer.allocate(padSizeBeforeSigningBlock)));
+ }
+ return Pair.of(beforeCentralDir, padSizeBeforeSigningBlock);
+ }
+
+ public static DataSource copyWithModifiedCDOffset(
+ DataSource beforeCentralDir, DataSource eocd) throws IOException {
+
+ // Ensure that, when digesting, ZIP End of Central Directory record's Central Directory
+ // offset field is treated as pointing to the offset at which the APK Signing Block will
+ // start.
+ long centralDirOffsetForDigesting = beforeCentralDir.size();
+ ByteBuffer eocdBuf = ByteBuffer.allocate((int) eocd.size());
+ eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
+ eocd.copyTo(0, (int) eocd.size(), eocdBuf);
+ eocdBuf.flip();
+ ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, centralDirOffsetForDigesting);
+ return DataSources.asDataSource(eocdBuf);
+ }
+
+ public static byte[] generateApkSigningBlock(
+ List> apkSignatureSchemeBlockPairs) {
+ // FORMAT:
+ // uint64: size (excluding this field)
+ // repeated ID-value pairs:
+ // uint64: size (excluding this field)
+ // uint32: ID
+ // (size - 4) bytes: value
+ // (extra dummy ID-value for padding to make block size a multiple of 4096 bytes)
+ // uint64: size (same as the one above)
+ // uint128: magic
+
+ int blocksSize = 0;
+ for (Pair schemeBlockPair : apkSignatureSchemeBlockPairs) {
+ blocksSize += 8 + 4 + schemeBlockPair.getFirst().length; // size + id + value
+ }
+
+ int resultSize =
+ 8 // size
+ + blocksSize
+ + 8 // size
+ + 16 // magic
+ ;
+ ByteBuffer paddingPair = null;
+ if (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) {
+ int padding = ANDROID_COMMON_PAGE_ALIGNMENT_BYTES -
+ (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES);
+ if (padding < 12) { // minimum size of an ID-value pair
+ padding += ANDROID_COMMON_PAGE_ALIGNMENT_BYTES;
+ }
+ paddingPair = ByteBuffer.allocate(padding).order(ByteOrder.LITTLE_ENDIAN);
+ paddingPair.putLong(padding - 8);
+ paddingPair.putInt(VERITY_PADDING_BLOCK_ID);
+ paddingPair.rewind();
+ resultSize += padding;
+ }
+
+ ByteBuffer result = ByteBuffer.allocate(resultSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ long blockSizeFieldValue = resultSize - 8L;
+ result.putLong(blockSizeFieldValue);
+
+
+ for (Pair schemeBlockPair : apkSignatureSchemeBlockPairs) {
+ byte[] apkSignatureSchemeBlock = schemeBlockPair.getFirst();
+ int apkSignatureSchemeId = schemeBlockPair.getSecond();
+ long pairSizeFieldValue = 4L + apkSignatureSchemeBlock.length;
+ result.putLong(pairSizeFieldValue);
+ result.putInt(apkSignatureSchemeId);
+ result.put(apkSignatureSchemeBlock);
+ }
+
+ if (paddingPair != null) {
+ result.put(paddingPair);
+ }
+
+ result.putLong(blockSizeFieldValue);
+ result.put(APK_SIGNING_BLOCK_MAGIC);
+
+ return result.array();
+ }
+
+ /**
+ * Computes the digests of the given APK components according to the algorithms specified in the
+ * given SignerConfigs.
+ *
+ * @param signerConfigs signer configurations, one for each signer At least one signer config
+ * must be provided.
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
+ * missing
+ * @throws SignatureException if an error occurs when computing digests of generating
+ * signatures
+ */
+ public static Pair, Map>
+ computeContentDigests(
+ RunnablesExecutor executor,
+ DataSource beforeCentralDir,
+ DataSource centralDir,
+ DataSource eocd,
+ List signerConfigs)
+ throws IOException, NoSuchAlgorithmException, SignatureException {
+ if (signerConfigs.isEmpty()) {
+ throw new IllegalArgumentException(
+ "No signer configs provided. At least one is required");
+ }
+
+ // Figure out which digest(s) to use for APK contents.
+ Set contentDigestAlgorithms = new HashSet<>(1);
+ for (SignerConfig signerConfig : signerConfigs) {
+ for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
+ contentDigestAlgorithms.add(signatureAlgorithm.getContentDigestAlgorithm());
+ }
+ }
+
+ // Compute digests of APK contents.
+ Map contentDigests; // digest algorithm ID -> digest
+ try {
+ contentDigests =
+ computeContentDigests(
+ executor,
+ contentDigestAlgorithms,
+ beforeCentralDir,
+ centralDir,
+ eocd);
+ } catch (IOException e) {
+ throw new IOException("Failed to read APK being signed", e);
+ } catch (DigestException e) {
+ throw new SignatureException("Failed to compute digests of APK", e);
+ }
+
+ // Sign the digests and wrap the signatures and signer info into an APK Signing Block.
+ return Pair.of(signerConfigs, contentDigests);
+ }
+
+ /**
+ * Returns the subset of signatures which are expected to be verified by at least one Android
+ * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
+ * guaranteed to contain at least one signature.
+ *
+ * Each Android platform version typically verifies exactly one signature from the provided
+ * {@code signatures} set. This method returns the set of these signatures collected over all
+ * requested platform versions. As a result, the result may contain more than one signature.
+ *
+ * @throws NoSupportedSignaturesException if no supported signatures were
+ * found for an Android platform version in the range.
+ */
+ public static List getSignaturesToVerify(
+ List signatures, int minSdkVersion, int maxSdkVersion)
+ throws NoSupportedSignaturesException {
+ // Pick the signature with the strongest algorithm at all required SDK versions, to mimic
+ // Android's behavior on those versions.
+ //
+ // Here we assume that, once introduced, a signature algorithm continues to be supported in
+ // all future Android versions. We also assume that the better-than relationship between
+ // algorithms is exactly the same on all Android platform versions (except that older
+ // platforms might support fewer algorithms). If these assumption are no longer true, the
+ // logic here will need to change accordingly.
+ Map bestSigAlgorithmOnSdkVersion = new HashMap<>();
+ int minProvidedSignaturesVersion = Integer.MAX_VALUE;
+ for (SupportedSignature sig : signatures) {
+ SignatureAlgorithm sigAlgorithm = sig.algorithm;
+ int sigMinSdkVersion = sigAlgorithm.getMinSdkVersion();
+ if (sigMinSdkVersion > maxSdkVersion) {
+ continue;
+ }
+ if (sigMinSdkVersion < minProvidedSignaturesVersion) {
+ minProvidedSignaturesVersion = sigMinSdkVersion;
+ }
+
+ SupportedSignature candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion);
+ if ((candidate == null)
+ || (compareSignatureAlgorithm(
+ sigAlgorithm, candidate.algorithm) > 0)) {
+ bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig);
+ }
+ }
+
+ // Must have some supported signature algorithms for minSdkVersion.
+ if (minSdkVersion < minProvidedSignaturesVersion) {
+ throw new NoSupportedSignaturesException(
+ "Minimum provided signature version " + minProvidedSignaturesVersion +
+ " < minSdkVersion " + minSdkVersion);
+ }
+ if (bestSigAlgorithmOnSdkVersion.isEmpty()) {
+ throw new NoSupportedSignaturesException("No supported signature");
+ }
+ return bestSigAlgorithmOnSdkVersion.values().stream()
+ .sorted((sig1, sig2) -> Integer.compare(
+ sig1.algorithm.getId(), sig2.algorithm.getId()))
+ .collect(Collectors.toList());
+ }
+
+ public static class NoSupportedSignaturesException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public NoSupportedSignaturesException(String message) {
+ super(message);
+ }
+ }
+
+ public static class SignatureNotFoundException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public SignatureNotFoundException(String message) {
+ super(message);
+ }
+
+ public SignatureNotFoundException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+ /**
+ * uses the SignatureAlgorithms in the provided signerConfig to sign the provided data
+ *
+ * @return list of signature algorithm IDs and their corresponding signatures over the data.
+ */
+ public static List> generateSignaturesOverData(
+ SignerConfig signerConfig, byte[] data)
+ throws InvalidKeyException, NoSuchAlgorithmException, SignatureException {
+ List> signatures =
+ new ArrayList<>(signerConfig.signatureAlgorithms.size());
+ PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+ for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
+ Pair sigAlgAndParams =
+ signatureAlgorithm.getJcaSignatureAlgorithmAndParams();
+ String jcaSignatureAlgorithm = sigAlgAndParams.getFirst();
+ AlgorithmParameterSpec jcaSignatureAlgorithmParams = sigAlgAndParams.getSecond();
+ byte[] signatureBytes;
+ try {
+ Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
+ signature.initSign(signerConfig.privateKey);
+ if (jcaSignatureAlgorithmParams != null) {
+ signature.setParameter(jcaSignatureAlgorithmParams);
+ }
+ signature.update(data);
+ signatureBytes = signature.sign();
+ } catch (InvalidKeyException e) {
+ throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e);
+ } catch (InvalidAlgorithmParameterException | SignatureException e) {
+ throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e);
+ }
+
+ try {
+ Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
+ signature.initVerify(publicKey);
+ if (jcaSignatureAlgorithmParams != null) {
+ signature.setParameter(jcaSignatureAlgorithmParams);
+ }
+ signature.update(data);
+ if (!signature.verify(signatureBytes)) {
+ throw new SignatureException("Failed to verify generated "
+ + jcaSignatureAlgorithm
+ + " signature using public key from certificate");
+ }
+ } catch (InvalidKeyException e) {
+ throw new InvalidKeyException(
+ "Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
+ + " public key from certificate", e);
+ } catch (InvalidAlgorithmParameterException | SignatureException e) {
+ throw new SignatureException(
+ "Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
+ + " public key from certificate", e);
+ }
+
+ signatures.add(Pair.of(signatureAlgorithm.getId(), signatureBytes));
+ }
+ return signatures;
+ }
+
+ /**
+ * Signer configuration.
+ */
+ public static class SignerConfig {
+ /** Private key. */
+ public PrivateKey privateKey;
+
+ /**
+ * Certificates, with the first certificate containing the public key corresponding to
+ * {@link #privateKey}.
+ */
+ public List certificates;
+
+ /**
+ * List of signature algorithms with which to sign.
+ */
+ public List signatureAlgorithms;
+
+ public int minSdkVersion;
+ public int maxSdkVersion;
+ public SigningCertificateLineage mSigningCertificateLineage;
+ }
+
+ public static class Result {
+ public final int signatureSchemeVersion;
+
+ /** Whether the APK's APK Signature Scheme signature verifies. */
+ public boolean verified;
+
+ public final List signers = new ArrayList<>();
+ public SigningCertificateLineage signingCertificateLineage = null;
+ private final List mWarnings = new ArrayList<>();
+ private final List mErrors = new ArrayList<>();
+
+ public Result(int signatureSchemeVersion) {
+ this.signatureSchemeVersion = signatureSchemeVersion;
+ }
+
+ public boolean containsErrors() {
+ if (!mErrors.isEmpty()) {
+ return true;
+ }
+ if (!signers.isEmpty()) {
+ for (SignerInfo signer : signers) {
+ if (signer.containsErrors()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public void addError(ApkVerifier.Issue msg, Object... parameters) {
+ mErrors.add(new ApkVerifier.IssueWithParams(msg, parameters));
+ }
+
+ public void addWarning(ApkVerifier.Issue msg, Object... parameters) {
+ mWarnings.add(new ApkVerifier.IssueWithParams(msg, parameters));
+ }
+
+ public List getErrors() {
+ return mErrors;
+ }
+
+ public List getWarnings() {
+ return mWarnings;
+ }
+
+ public static class SignerInfo {
+ public int index;
+ public List certs = new ArrayList<>();
+ public List contentDigests = new ArrayList<>();
+ public Map verifiedContentDigests = new HashMap<>();
+ public List signatures = new ArrayList<>();
+ public Map verifiedSignatures = new HashMap<>();
+ public List additionalAttributes = new ArrayList<>();
+ public byte[] signedData;
+ public int minSdkVersion;
+ public int maxSdkVersion;
+ public SigningCertificateLineage signingCertificateLineage;
+
+ private final List mWarnings = new ArrayList<>();
+ private final List mErrors = new ArrayList<>();
+
+ public void addError(ApkVerifier.Issue msg, Object... parameters) {
+ mErrors.add(new ApkVerifier.IssueWithParams(msg, parameters));
+ }
+
+ public void addWarning(ApkVerifier.Issue msg, Object... parameters) {
+ mWarnings.add(new ApkVerifier.IssueWithParams(msg, parameters));
+ }
+
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+
+ public List getErrors() {
+ return mErrors;
+ }
+
+ public List getWarnings() {
+ return mWarnings;
+ }
+
+ public static class ContentDigest {
+ private final int mSignatureAlgorithmId;
+ private final byte[] mValue;
+
+ public ContentDigest(int signatureAlgorithmId, byte[] value) {
+ mSignatureAlgorithmId = signatureAlgorithmId;
+ mValue = value;
+ }
+
+ public int getSignatureAlgorithmId() {
+ return mSignatureAlgorithmId;
+ }
+
+ public byte[] getValue() {
+ return mValue;
+ }
+ }
+
+ public static class Signature {
+ private final int mAlgorithmId;
+ private final byte[] mValue;
+
+ public Signature(int algorithmId, byte[] value) {
+ mAlgorithmId = algorithmId;
+ mValue = value;
+ }
+
+ public int getAlgorithmId() {
+ return mAlgorithmId;
+ }
+
+ public byte[] getValue() {
+ return mValue;
+ }
+ }
+
+ public static class AdditionalAttribute {
+ private final int mId;
+ private final byte[] mValue;
+
+ public AdditionalAttribute(int id, byte[] value) {
+ mId = id;
+ mValue = value.clone();
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public byte[] getValue() {
+ return mValue.clone();
+ }
+ }
+ }
+ }
+
+ public static class SupportedSignature {
+ public final SignatureAlgorithm algorithm;
+ public final byte[] signature;
+
+ public SupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
+ this.algorithm = algorithm;
+ this.signature = signature;
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java b/app/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java
new file mode 100644
index 0000000..b222474
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java
@@ -0,0 +1,55 @@
+/*
+ * 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.internal.apk;
+
+/**
+ * APK Signature Scheme v2 content digest algorithm.
+ */
+public enum ContentDigestAlgorithm {
+ /** SHA2-256 over 1 MB chunks. */
+ CHUNKED_SHA256("SHA-256", 256 / 8),
+
+ /** SHA2-512 over 1 MB chunks. */
+ CHUNKED_SHA512("SHA-512", 512 / 8),
+
+ /** SHA2-256 over 4 KB chunks for APK verity. */
+ VERITY_CHUNKED_SHA256("SHA-256", 256 / 8);
+
+ private final String mJcaMessageDigestAlgorithm;
+ private final int mChunkDigestOutputSizeBytes;
+
+ private ContentDigestAlgorithm(
+ String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) {
+ mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm;
+ mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
+ }
+
+ /**
+ * Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
+ * chunks by this content digest algorithm.
+ */
+ String getJcaMessageDigestAlgorithm() {
+ return mJcaMessageDigestAlgorithm;
+ }
+
+ /**
+ * Returns the size (in bytes) of the digest of a chunk of content.
+ */
+ int getChunkDigestOutputSizeBytes() {
+ return mChunkDigestOutputSizeBytes;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java b/app/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java
new file mode 100644
index 0000000..0db8cb8
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java
@@ -0,0 +1,193 @@
+/*
+ * 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.internal.apk;
+
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.Pair;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.MGF1ParameterSpec;
+import java.security.spec.PSSParameterSpec;
+
+/**
+ * APK Signing Block signature algorithm.
+ */
+public enum SignatureAlgorithm {
+ // TODO reserve the 0x0000 ID to mean null
+ /**
+ * RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content
+ * digested using SHA2-256 in 1 MB chunks.
+ */
+ RSA_PSS_WITH_SHA256(
+ 0x0101,
+ ContentDigestAlgorithm.CHUNKED_SHA256,
+ "RSA",
+ Pair.of("SHA256withRSA/PSS",
+ new PSSParameterSpec(
+ "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)),
+ AndroidSdkVersion.N),
+
+ /**
+ * RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content
+ * digested using SHA2-512 in 1 MB chunks.
+ */
+ RSA_PSS_WITH_SHA512(
+ 0x0102,
+ ContentDigestAlgorithm.CHUNKED_SHA512,
+ "RSA",
+ Pair.of(
+ "SHA512withRSA/PSS",
+ new PSSParameterSpec(
+ "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)),
+ AndroidSdkVersion.N),
+
+ /** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
+ RSA_PKCS1_V1_5_WITH_SHA256(
+ 0x0103,
+ ContentDigestAlgorithm.CHUNKED_SHA256,
+ "RSA",
+ Pair.of("SHA256withRSA", null),
+ AndroidSdkVersion.N),
+
+ /** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
+ RSA_PKCS1_V1_5_WITH_SHA512(
+ 0x0104,
+ ContentDigestAlgorithm.CHUNKED_SHA512,
+ "RSA",
+ Pair.of("SHA512withRSA", null),
+ AndroidSdkVersion.N),
+
+ /** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
+ ECDSA_WITH_SHA256(
+ 0x0201,
+ ContentDigestAlgorithm.CHUNKED_SHA256,
+ "EC",
+ Pair.of("SHA256withECDSA", null),
+ AndroidSdkVersion.N),
+
+ /** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
+ ECDSA_WITH_SHA512(
+ 0x0202,
+ ContentDigestAlgorithm.CHUNKED_SHA512,
+ "EC",
+ Pair.of("SHA512withECDSA", null),
+ AndroidSdkVersion.N),
+
+ /** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
+ DSA_WITH_SHA256(
+ 0x0301,
+ ContentDigestAlgorithm.CHUNKED_SHA256,
+ "DSA",
+ Pair.of("SHA256withDSA", null),
+ AndroidSdkVersion.N),
+
+ /**
+ * RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in
+ * the same way fsverity operates. This digest and the content length (before digestion, 8 bytes
+ * in little endian) construct the final digest.
+ */
+ VERITY_RSA_PKCS1_V1_5_WITH_SHA256(
+ 0x0421,
+ ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
+ "RSA",
+ Pair.of("SHA256withRSA", null),
+ AndroidSdkVersion.P),
+
+ /**
+ * ECDSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
+ * fsverity operates. This digest and the content length (before digestion, 8 bytes in little
+ * endian) construct the final digest.
+ */
+ VERITY_ECDSA_WITH_SHA256(
+ 0x0423,
+ ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
+ "EC",
+ Pair.of("SHA256withECDSA", null),
+ AndroidSdkVersion.P),
+
+ /**
+ * DSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
+ * fsverity operates. This digest and the content length (before digestion, 8 bytes in little
+ * endian) construct the final digest.
+ */
+ VERITY_DSA_WITH_SHA256(
+ 0x0425,
+ ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
+ "DSA",
+ Pair.of("SHA256withDSA", null),
+ AndroidSdkVersion.P);
+
+ private final int mId;
+ private final String mJcaKeyAlgorithm;
+ private final ContentDigestAlgorithm mContentDigestAlgorithm;
+ private final Pair mJcaSignatureAlgAndParams;
+ private final int mMinSdkVersion;
+
+ SignatureAlgorithm(int id,
+ ContentDigestAlgorithm contentDigestAlgorithm,
+ String jcaKeyAlgorithm,
+ Pair jcaSignatureAlgAndParams,
+ int minSdkVersion) {
+ mId = id;
+ mContentDigestAlgorithm = contentDigestAlgorithm;
+ mJcaKeyAlgorithm = jcaKeyAlgorithm;
+ mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams;
+ mMinSdkVersion = minSdkVersion;
+ }
+
+ /**
+ * Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format.
+ */
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the content digest algorithm associated with this signature algorithm.
+ */
+ public ContentDigestAlgorithm getContentDigestAlgorithm() {
+ return mContentDigestAlgorithm;
+ }
+
+ /**
+ * Returns the JCA {@link java.security.Key} algorithm used by this signature scheme.
+ */
+ public String getJcaKeyAlgorithm() {
+ return mJcaKeyAlgorithm;
+ }
+
+ /**
+ * Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec}
+ * (or null if not needed) to parameterize the {@code Signature}.
+ */
+ public Pair getJcaSignatureAlgorithmAndParams() {
+ return mJcaSignatureAlgAndParams;
+ }
+
+ public int getMinSdkVersion() {
+ return mMinSdkVersion;
+ }
+
+ public static SignatureAlgorithm findById(int id) {
+ for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
+ if (alg.getId() == id) {
+ return alg;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java b/app/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java
new file mode 100644
index 0000000..5e26327
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2018 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.internal.apk;
+
+import java.nio.ByteBuffer;
+
+/**
+ * APK Signature Scheme block and additional information relevant to verifying the signatures
+ * contained in the block against the file.
+ */
+public class SignatureInfo {
+ /** Contents of APK Signature Scheme block. */
+ public final ByteBuffer signatureBlock;
+
+ /** Position of the APK Signing Block in the file. */
+ public final long apkSigningBlockOffset;
+
+ /** Position of the ZIP Central Directory in the file. */
+ public final long centralDirOffset;
+
+ /** Position of the ZIP End of Central Directory (EoCD) in the file. */
+ public final long eocdOffset;
+
+ /** Contents of ZIP End of Central Directory (EoCD) of the file. */
+ public final ByteBuffer eocd;
+
+ public SignatureInfo(
+ ByteBuffer signatureBlock,
+ long apkSigningBlockOffset,
+ long centralDirOffset,
+ long eocdOffset,
+ ByteBuffer eocd) {
+ this.signatureBlock = signatureBlock;
+ this.apkSigningBlockOffset = apkSigningBlockOffset;
+ this.centralDirOffset = centralDirOffset;
+ this.eocdOffset = eocdOffset;
+ this.eocd = eocd;
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java b/app/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java
new file mode 100644
index 0000000..51b9810
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java
@@ -0,0 +1,74 @@
+/*
+ * 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.internal.apk.v1;
+
+import java.util.Comparator;
+
+/**
+ * Digest algorithm used with JAR signing (aka v1 signing scheme).
+ */
+public enum DigestAlgorithm {
+ /** SHA-1 */
+ SHA1("SHA-1"),
+
+ /** SHA2-256 */
+ SHA256("SHA-256");
+
+ private final String mJcaMessageDigestAlgorithm;
+
+ private DigestAlgorithm(String jcaMessageDigestAlgoritm) {
+ mJcaMessageDigestAlgorithm = jcaMessageDigestAlgoritm;
+ }
+
+ /**
+ * Returns the {@link java.security.MessageDigest} algorithm represented by this digest
+ * algorithm.
+ */
+ String getJcaMessageDigestAlgorithm() {
+ return mJcaMessageDigestAlgorithm;
+ }
+
+ public static Comparator BY_STRENGTH_COMPARATOR = new StrengthComparator();
+
+ private static class StrengthComparator implements Comparator {
+ @Override
+ public int compare(DigestAlgorithm a1, DigestAlgorithm a2) {
+ switch (a1) {
+ case SHA1:
+ switch (a2) {
+ case SHA1:
+ return 0;
+ case SHA256:
+ return -1;
+ }
+ throw new RuntimeException("Unsupported algorithm: " + a2);
+
+ case SHA256:
+ switch (a2) {
+ case SHA1:
+ return 1;
+ case SHA256:
+ return 0;
+ }
+ throw new RuntimeException("Unsupported algorithm: " + a2);
+
+ default:
+ throw new RuntimeException("Unsupported algorithm: " + a1);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java b/app/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java
new file mode 100644
index 0000000..f900211
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java
@@ -0,0 +1,688 @@
+/*
+ * 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.internal.apk.v1;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.asn1.Asn1DerEncoder;
+import com.android.apksig.internal.asn1.Asn1EncodingException;
+import com.android.apksig.internal.asn1.Asn1OpaqueObject;
+import com.android.apksig.internal.asn1.ber.BerEncoding;
+import com.android.apksig.internal.jar.ManifestWriter;
+import com.android.apksig.internal.jar.SignatureFileWriter;
+import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
+import com.android.apksig.internal.pkcs7.ContentInfo;
+import com.android.apksig.internal.pkcs7.EncapsulatedContentInfo;
+import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber;
+import com.android.apksig.internal.pkcs7.Pkcs7Constants;
+import com.android.apksig.internal.pkcs7.SignedData;
+import com.android.apksig.internal.pkcs7.SignerIdentifier;
+import com.android.apksig.internal.pkcs7.SignerInfo;
+import com.android.apksig.internal.util.Pair;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * APK signer which uses JAR signing (aka v1 signing scheme).
+ *
+ * @see Signed JAR File
+ */
+public abstract class V1SchemeSigner {
+
+ public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
+
+ private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY =
+ new Attributes.Name("Created-By");
+ private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0";
+ private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0";
+
+ static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR = "X-Android-APK-Signed";
+ private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME =
+ new Attributes.Name(SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
+
+ /**
+ * Signer configuration.
+ */
+ public static class SignerConfig {
+ /** Name. */
+ public String name;
+
+ /** Private key. */
+ public PrivateKey privateKey;
+
+ /**
+ * Certificates, with the first certificate containing the public key corresponding to
+ * {@link #privateKey}.
+ */
+ public List certificates;
+
+ /**
+ * Digest algorithm used for the signature.
+ */
+ public DigestAlgorithm signatureDigestAlgorithm;
+ }
+
+ /** Hidden constructor to prevent instantiation. */
+ private V1SchemeSigner() {}
+
+ /**
+ * Gets the JAR signing digest algorithm to be used for signing an APK using the provided key.
+ *
+ * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
+ * AndroidManifest.xml minSdkVersion attribute)
+ *
+ * @throws InvalidKeyException if the provided key is not suitable for signing APKs using
+ * JAR signing (aka v1 signature scheme)
+ */
+ public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm(
+ PublicKey signingKey, int minSdkVersion) throws InvalidKeyException {
+ String keyAlgorithm = signingKey.getAlgorithm();
+ if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
+ // Prior to API Level 18, only SHA-1 can be used with RSA.
+ if (minSdkVersion < 18) {
+ return DigestAlgorithm.SHA1;
+ }
+ return DigestAlgorithm.SHA256;
+ } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
+ // Prior to API Level 21, only SHA-1 can be used with DSA
+ if (minSdkVersion < 21) {
+ return DigestAlgorithm.SHA1;
+ } else {
+ return DigestAlgorithm.SHA256;
+ }
+ } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
+ if (minSdkVersion < 18) {
+ throw new InvalidKeyException(
+ "ECDSA signatures only supported for minSdkVersion 18 and higher");
+ }
+ return DigestAlgorithm.SHA256;
+ } else {
+ throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
+ }
+ }
+
+ /**
+ * Returns a safe version of the provided signer name.
+ */
+ public static String getSafeSignerName(String name) {
+ if (name.isEmpty()) {
+ throw new IllegalArgumentException("Empty name");
+ }
+
+ // According to https://docs.oracle.com/javase/tutorial/deployment/jar/signing.html, the
+ // name must not be longer than 8 characters and may contain only A-Z, 0-9, _, and -.
+ StringBuilder result = new StringBuilder();
+ char[] nameCharsUpperCase = name.toUpperCase(Locale.US).toCharArray();
+ for (int i = 0; i < Math.min(nameCharsUpperCase.length, 8); i++) {
+ char c = nameCharsUpperCase[i];
+ if (((c >= 'A') && (c <= 'Z'))
+ || ((c >= '0') && (c <= '9'))
+ || (c == '-')
+ || (c == '_')) {
+ result.append(c);
+ } else {
+ result.append('_');
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Returns a new {@link MessageDigest} instance corresponding to the provided digest algorithm.
+ */
+ private static MessageDigest getMessageDigestInstance(DigestAlgorithm digestAlgorithm)
+ throws NoSuchAlgorithmException {
+ String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm();
+ return MessageDigest.getInstance(jcaAlgorithm);
+ }
+
+ /**
+ * Returns the JCA {@link MessageDigest} algorithm corresponding to the provided digest
+ * algorithm.
+ */
+ public static String getJcaMessageDigestAlgorithm(DigestAlgorithm digestAlgorithm) {
+ return digestAlgorithm.getJcaMessageDigestAlgorithm();
+ }
+
+ /**
+ * Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's
+ * manifest.
+ */
+ public static boolean isJarEntryDigestNeededInManifest(String entryName) {
+ // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File
+
+ // Entries which represent directories sould not be listed in the manifest.
+ if (entryName.endsWith("/")) {
+ return false;
+ }
+
+ // Entries outside of META-INF must be listed in the manifest.
+ if (!entryName.startsWith("META-INF/")) {
+ return true;
+ }
+ // Entries in subdirectories of META-INF must be listed in the manifest.
+ if (entryName.indexOf('/', "META-INF/".length()) != -1) {
+ return true;
+ }
+
+ // Ignored file names (case-insensitive) in META-INF directory:
+ // MANIFEST.MF
+ // *.SF
+ // *.RSA
+ // *.DSA
+ // *.EC
+ // SIG-*
+ String fileNameLowerCase =
+ entryName.substring("META-INF/".length()).toLowerCase(Locale.US);
+ if (("manifest.mf".equals(fileNameLowerCase))
+ || (fileNameLowerCase.endsWith(".sf"))
+ || (fileNameLowerCase.endsWith(".rsa"))
+ || (fileNameLowerCase.endsWith(".dsa"))
+ || (fileNameLowerCase.endsWith(".ec"))
+ || (fileNameLowerCase.startsWith("sig-"))) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
+ * JAR entries which need to be added to the APK as part of the signature.
+ *
+ * @param signerConfigs signer configurations, one for each signer. At least one signer config
+ * must be provided.
+ *
+ * @throws ApkFormatException if the source manifest is malformed
+ * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
+ * missing
+ * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
+ * cannot be used in general
+ * @throws SignatureException if an error occurs when computing digests of generating
+ * signatures
+ */
+ public static List> sign(
+ List signerConfigs,
+ DigestAlgorithm jarEntryDigestAlgorithm,
+ Map jarEntryDigests,
+ List apkSigningSchemeIds,
+ byte[] sourceManifestBytes,
+ String createdBy)
+ throws NoSuchAlgorithmException, ApkFormatException, InvalidKeyException,
+ CertificateException, SignatureException {
+ if (signerConfigs.isEmpty()) {
+ throw new IllegalArgumentException("At least one signer config must be provided");
+ }
+ OutputManifestFile manifest =
+ generateManifestFile(
+ jarEntryDigestAlgorithm, jarEntryDigests, sourceManifestBytes);
+
+ return signManifest(
+ signerConfigs, jarEntryDigestAlgorithm, apkSigningSchemeIds, createdBy, manifest);
+ }
+
+ /**
+ * Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
+ * JAR entries which need to be added to the APK as part of the signature.
+ *
+ * @param signerConfigs signer configurations, one for each signer. At least one signer config
+ * must be provided.
+ *
+ * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
+ * cannot be used in general
+ * @throws SignatureException if an error occurs when computing digests of generating
+ * signatures
+ */
+ public static List> signManifest(
+ List signerConfigs,
+ DigestAlgorithm digestAlgorithm,
+ List apkSigningSchemeIds,
+ String createdBy,
+ OutputManifestFile manifest)
+ throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
+ SignatureException {
+ if (signerConfigs.isEmpty()) {
+ throw new IllegalArgumentException("At least one signer config must be provided");
+ }
+
+ // For each signer output .SF and .(RSA|DSA|EC) file, then output MANIFEST.MF.
+ List> signatureJarEntries =
+ new ArrayList<>(2 * signerConfigs.size() + 1);
+ byte[] sfBytes =
+ generateSignatureFile(apkSigningSchemeIds, digestAlgorithm, createdBy, manifest);
+ for (SignerConfig signerConfig : signerConfigs) {
+ String signerName = signerConfig.name;
+ byte[] signatureBlock;
+ try {
+ signatureBlock = generateSignatureBlock(signerConfig, sfBytes);
+ } catch (InvalidKeyException e) {
+ throw new InvalidKeyException(
+ "Failed to sign using signer \"" + signerName + "\"", e);
+ } catch (CertificateException e) {
+ throw new CertificateException(
+ "Failed to sign using signer \"" + signerName + "\"", e);
+ } catch (SignatureException e) {
+ throw new SignatureException(
+ "Failed to sign using signer \"" + signerName + "\"", e);
+ }
+ signatureJarEntries.add(Pair.of("META-INF/" + signerName + ".SF", sfBytes));
+ PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+ String signatureBlockFileName =
+ "META-INF/" + signerName + "."
+ + publicKey.getAlgorithm().toUpperCase(Locale.US);
+ signatureJarEntries.add(
+ Pair.of(signatureBlockFileName, signatureBlock));
+ }
+ signatureJarEntries.add(Pair.of(MANIFEST_ENTRY_NAME, manifest.contents));
+ return signatureJarEntries;
+ }
+
+ /**
+ * Returns the names of JAR entries which this signer will produce as part of v1 signature.
+ */
+ public static Set getOutputEntryNames(List signerConfigs) {
+ Set result = new HashSet<>(2 * signerConfigs.size() + 1);
+ for (SignerConfig signerConfig : signerConfigs) {
+ String signerName = signerConfig.name;
+ result.add("META-INF/" + signerName + ".SF");
+ PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+ String signatureBlockFileName =
+ "META-INF/" + signerName + "."
+ + publicKey.getAlgorithm().toUpperCase(Locale.US);
+ result.add(signatureBlockFileName);
+ }
+ result.add(MANIFEST_ENTRY_NAME);
+ return result;
+ }
+
+ /**
+ * Generated and returns the {@code META-INF/MANIFEST.MF} file based on the provided (optional)
+ * input {@code MANIFEST.MF} and digests of JAR entries covered by the manifest.
+ */
+ public static OutputManifestFile generateManifestFile(
+ DigestAlgorithm jarEntryDigestAlgorithm,
+ Map jarEntryDigests,
+ byte[] sourceManifestBytes) throws ApkFormatException {
+ Manifest sourceManifest = null;
+ if (sourceManifestBytes != null) {
+ try {
+ sourceManifest = new Manifest(new ByteArrayInputStream(sourceManifestBytes));
+ } catch (IOException e) {
+ throw new ApkFormatException("Malformed source META-INF/MANIFEST.MF", e);
+ }
+ }
+ ByteArrayOutputStream manifestOut = new ByteArrayOutputStream();
+ Attributes mainAttrs = new Attributes();
+ // Copy the main section from the source manifest (if provided). Otherwise use defaults.
+ // NOTE: We don't output our own Created-By header because this signer did not create the
+ // JAR/APK being signed -- the signer only adds signatures to the already existing
+ // JAR/APK.
+ if (sourceManifest != null) {
+ mainAttrs.putAll(sourceManifest.getMainAttributes());
+ } else {
+ mainAttrs.put(Attributes.Name.MANIFEST_VERSION, ATTRIBUTE_VALUE_MANIFEST_VERSION);
+ }
+
+ try {
+ ManifestWriter.writeMainSection(manifestOut, mainAttrs);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
+ }
+
+ List sortedEntryNames = new ArrayList<>(jarEntryDigests.keySet());
+ Collections.sort(sortedEntryNames);
+ SortedMap invidualSectionsContents = new TreeMap<>();
+ String entryDigestAttributeName = getEntryDigestAttributeName(jarEntryDigestAlgorithm);
+ for (String entryName : sortedEntryNames) {
+ checkEntryNameValid(entryName);
+ byte[] entryDigest = jarEntryDigests.get(entryName);
+ Attributes entryAttrs = new Attributes();
+ entryAttrs.putValue(
+ entryDigestAttributeName,
+ Base64.getEncoder().encodeToString(entryDigest));
+ ByteArrayOutputStream sectionOut = new ByteArrayOutputStream();
+ byte[] sectionBytes;
+ try {
+ ManifestWriter.writeIndividualSection(sectionOut, entryName, entryAttrs);
+ sectionBytes = sectionOut.toByteArray();
+ manifestOut.write(sectionBytes);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
+ }
+ invidualSectionsContents.put(entryName, sectionBytes);
+ }
+
+ OutputManifestFile result = new OutputManifestFile();
+ result.contents = manifestOut.toByteArray();
+ result.mainSectionAttributes = mainAttrs;
+ result.individualSectionsContents = invidualSectionsContents;
+ return result;
+ }
+
+ private static void checkEntryNameValid(String name) throws ApkFormatException {
+ // JAR signing spec says CR, LF, and NUL are not permitted in entry names
+ // CR or LF in entry names will result in malformed MANIFEST.MF and .SF files because there
+ // is no way to escape characters in MANIFEST.MF and .SF files. NUL can, presumably, cause
+ // issues when parsing using C and C++ like languages.
+ for (char c : name.toCharArray()) {
+ if ((c == '\r') || (c == '\n') || (c == 0)) {
+ throw new ApkFormatException(
+ String.format(
+ "Unsupported character 0x%1$02x in ZIP entry name \"%2$s\"",
+ (int) c,
+ name));
+ }
+ }
+ }
+
+ public static class OutputManifestFile {
+ public byte[] contents;
+ public SortedMap individualSectionsContents;
+ public Attributes mainSectionAttributes;
+ }
+
+ private static byte[] generateSignatureFile(
+ List apkSignatureSchemeIds,
+ DigestAlgorithm manifestDigestAlgorithm,
+ String createdBy,
+ OutputManifestFile manifest) throws NoSuchAlgorithmException {
+ Manifest sf = new Manifest();
+ Attributes mainAttrs = sf.getMainAttributes();
+ mainAttrs.put(Attributes.Name.SIGNATURE_VERSION, ATTRIBUTE_VALUE_SIGNATURE_VERSION);
+ mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, createdBy);
+ if (!apkSignatureSchemeIds.isEmpty()) {
+ // Add APK Signature Scheme v2 (and newer) signature stripping protection.
+ // This attribute indicates that this APK is supposed to have been signed using one or
+ // more APK-specific signature schemes in addition to the standard JAR signature scheme
+ // used by this code. APK signature verifier should reject the APK if it does not
+ // contain a signature for the signature scheme the verifier prefers out of this set.
+ StringBuilder attrValue = new StringBuilder();
+ for (int id : apkSignatureSchemeIds) {
+ if (attrValue.length() > 0) {
+ attrValue.append(", ");
+ }
+ attrValue.append(String.valueOf(id));
+ }
+ mainAttrs.put(
+ SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME,
+ attrValue.toString());
+ }
+
+ // Add main attribute containing the digest of MANIFEST.MF.
+ MessageDigest md = getMessageDigestInstance(manifestDigestAlgorithm);
+ mainAttrs.putValue(
+ getManifestDigestAttributeName(manifestDigestAlgorithm),
+ Base64.getEncoder().encodeToString(md.digest(manifest.contents)));
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try {
+ SignatureFileWriter.writeMainSection(out, mainAttrs);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to write in-memory .SF file", e);
+ }
+ String entryDigestAttributeName = getEntryDigestAttributeName(manifestDigestAlgorithm);
+ for (Map.Entry manifestSection
+ : manifest.individualSectionsContents.entrySet()) {
+ String sectionName = manifestSection.getKey();
+ byte[] sectionContents = manifestSection.getValue();
+ byte[] sectionDigest = md.digest(sectionContents);
+ Attributes attrs = new Attributes();
+ attrs.putValue(
+ entryDigestAttributeName,
+ Base64.getEncoder().encodeToString(sectionDigest));
+
+ try {
+ SignatureFileWriter.writeIndividualSection(out, sectionName, attrs);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to write in-memory .SF file", e);
+ }
+ }
+
+ // A bug in the java.util.jar implementation of Android platforms up to version 1.6 will
+ // cause a spurious IOException to be thrown if the length of the signature file is a
+ // multiple of 1024 bytes. As a workaround, add an extra CRLF in this case.
+ if ((out.size() > 0) && ((out.size() % 1024) == 0)) {
+ try {
+ SignatureFileWriter.writeSectionDelimiter(out);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to write to ByteArrayOutputStream", e);
+ }
+ }
+
+ return out.toByteArray();
+ }
+
+ /** ASN.1 DER-encoded {@code NULL}. */
+ private static final Asn1OpaqueObject ASN1_DER_NULL =
+ new Asn1OpaqueObject(new byte[] {BerEncoding.TAG_NUMBER_NULL, 0});
+
+ /**
+ * Generates the CMS PKCS #7 signature block corresponding to the provided signature file and
+ * signing configuration.
+ */
+ private static byte[] generateSignatureBlock(
+ SignerConfig signerConfig, byte[] signatureFileBytes)
+ throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
+ SignatureException {
+ // Obtain relevant bits of signing configuration
+ List signerCerts = signerConfig.certificates;
+ X509Certificate signingCert = signerCerts.get(0);
+ PublicKey publicKey = signingCert.getPublicKey();
+ DigestAlgorithm digestAlgorithm = signerConfig.signatureDigestAlgorithm;
+ Pair signatureAlgs =
+ getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm);
+ String jcaSignatureAlgorithm = signatureAlgs.getFirst();
+
+ // Generate the cryptographic signature of the signature file
+ byte[] signatureBytes;
+ try {
+ Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
+ signature.initSign(signerConfig.privateKey);
+ signature.update(signatureFileBytes);
+ signatureBytes = signature.sign();
+ } catch (InvalidKeyException e) {
+ throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e);
+ } catch (SignatureException e) {
+ throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e);
+ }
+
+ // Verify the signature against the public key in the signing certificate
+ try {
+ Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
+ signature.initVerify(publicKey);
+ signature.update(signatureFileBytes);
+ if (!signature.verify(signatureBytes)) {
+ throw new SignatureException("Signature did not verify");
+ }
+ } catch (InvalidKeyException e) {
+ throw new InvalidKeyException(
+ "Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
+ + " public key from certificate",
+ e);
+ } catch (SignatureException e) {
+ throw new SignatureException(
+ "Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
+ + " public key from certificate",
+ e);
+ }
+
+ // Wrap the signature into the JAR signature block which is created according to CMS PKCS #7
+ // RFC 5652.
+ // The high-level simplified structure is as follows:
+ // ContentInfo
+ // digestAlgorithm
+ // SignedData
+ // bag of certificates
+ // SignerInfo
+ // signing cert issuer and serial number (for locating the cert in the above bag)
+ // digestAlgorithm
+ // signatureAlgorithm
+ // signature
+ try {
+ SignerInfo signerInfo = new SignerInfo();
+ signerInfo.version = 1;
+ X500Principal signerCertIssuer = signingCert.getIssuerX500Principal();
+ signerInfo.sid =
+ new SignerIdentifier(
+ new IssuerAndSerialNumber(
+ new Asn1OpaqueObject(signerCertIssuer.getEncoded()),
+ signingCert.getSerialNumber()));
+ AlgorithmIdentifier digestAlgorithmId =
+ getSignerInfoDigestAlgorithmOid(digestAlgorithm);
+ AlgorithmIdentifier signatureAlgorithmId = signatureAlgs.getSecond();
+ signerInfo.digestAlgorithm = digestAlgorithmId;
+ signerInfo.signatureAlgorithm = signatureAlgorithmId;
+ signerInfo.signature = ByteBuffer.wrap(signatureBytes);
+
+ SignedData signedData = new SignedData();
+ signedData.certificates = new ArrayList<>(signerCerts.size());
+ for (X509Certificate cert : signerCerts) {
+ signedData.certificates.add(new Asn1OpaqueObject(cert.getEncoded()));
+ }
+ signedData.version = 1;
+ signedData.digestAlgorithms = Collections.singletonList(digestAlgorithmId);
+ signedData.encapContentInfo = new EncapsulatedContentInfo(Pkcs7Constants.OID_DATA);
+ signedData.signerInfos = Collections.singletonList(signerInfo);
+
+ ContentInfo contentInfo = new ContentInfo();
+ contentInfo.contentType = Pkcs7Constants.OID_SIGNED_DATA;
+ contentInfo.content = new Asn1OpaqueObject(Asn1DerEncoder.encode(signedData));
+ return Asn1DerEncoder.encode(contentInfo);
+ } catch (Asn1EncodingException e) {
+ throw new SignatureException("Failed to encode signature block", e);
+ }
+ }
+
+ /**
+ * Returns the PKCS #7 {@code DigestAlgorithm} to use when signing using the specified digest
+ * algorithm.
+ */
+ private static AlgorithmIdentifier getSignerInfoDigestAlgorithmOid(
+ DigestAlgorithm digestAlgorithm) {
+ switch (digestAlgorithm) {
+ case SHA1:
+ return new AlgorithmIdentifier(
+ V1SchemeVerifier.Signer.OID_DIGEST_SHA1, ASN1_DER_NULL);
+ case SHA256:
+ return new AlgorithmIdentifier(
+ V1SchemeVerifier.Signer.OID_DIGEST_SHA256, ASN1_DER_NULL);
+ default:
+ throw new RuntimeException("Unsupported digest algorithm: " + digestAlgorithm);
+ }
+ }
+
+ /**
+ * Returns the JCA {@link Signature} algorithm and PKCS #7 {@code SignatureAlgorithm} to use
+ * when signing with the specified key and digest algorithm.
+ */
+ private static Pair getSignerInfoSignatureAlgorithm(
+ PublicKey publicKey, DigestAlgorithm digestAlgorithm) throws InvalidKeyException {
+ String keyAlgorithm = publicKey.getAlgorithm();
+ String jcaDigestPrefixForSigAlg;
+ switch (digestAlgorithm) {
+ case SHA1:
+ jcaDigestPrefixForSigAlg = "SHA1";
+ break;
+ case SHA256:
+ jcaDigestPrefixForSigAlg = "SHA256";
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unexpected digest algorithm: " + digestAlgorithm);
+ }
+ if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
+ return Pair.of(
+ jcaDigestPrefixForSigAlg + "withRSA",
+ new AlgorithmIdentifier(V1SchemeVerifier.Signer.OID_SIG_RSA, ASN1_DER_NULL));
+ } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
+ AlgorithmIdentifier sigAlgId;
+ switch (digestAlgorithm) {
+ case SHA1:
+ sigAlgId =
+ new AlgorithmIdentifier(
+ V1SchemeVerifier.Signer.OID_SIG_DSA, ASN1_DER_NULL);
+ break;
+ case SHA256:
+ // DSA signatures with SHA-256 in SignedData are accepted by Android API Level
+ // 21 and higher. However, there are two ways to specify their SignedData
+ // SignatureAlgorithm: dsaWithSha256 (2.16.840.1.101.3.4.3.2) and
+ // dsa (1.2.840.10040.4.1). The latter works only on API Level 22+. Thus, we use
+ // the former.
+ sigAlgId =
+ new AlgorithmIdentifier(
+ V1SchemeVerifier.Signer.OID_SIG_SHA256_WITH_DSA, ASN1_DER_NULL);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unexpected digest algorithm: " + digestAlgorithm);
+ }
+ return Pair.of(jcaDigestPrefixForSigAlg + "withDSA", sigAlgId);
+ } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
+ return Pair.of(
+ jcaDigestPrefixForSigAlg + "withECDSA",
+ new AlgorithmIdentifier(
+ V1SchemeVerifier.Signer.OID_SIG_EC_PUBLIC_KEY, ASN1_DER_NULL));
+ } else {
+ throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
+ }
+ }
+
+ private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) {
+ switch (digestAlgorithm) {
+ case SHA1:
+ return "SHA1-Digest";
+ case SHA256:
+ return "SHA-256-Digest";
+ default:
+ throw new IllegalArgumentException(
+ "Unexpected content digest algorithm: " + digestAlgorithm);
+ }
+ }
+
+ private static String getManifestDigestAttributeName(DigestAlgorithm digestAlgorithm) {
+ switch (digestAlgorithm) {
+ case SHA1:
+ return "SHA1-Digest-Manifest";
+ case SHA256:
+ return "SHA-256-Digest-Manifest";
+ default:
+ throw new IllegalArgumentException(
+ "Unexpected content digest algorithm: " + digestAlgorithm);
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java b/app/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
new file mode 100644
index 0000000..47d5b01
--- /dev/null
+++ b/app/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
@@ -0,0 +1,2099 @@
+/*
+ * 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.internal.apk.v1;
+
+import com.android.apksig.ApkVerifier.Issue;
+import com.android.apksig.ApkVerifier.IssueWithParams;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.asn1.Asn1BerParser;
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1DecodingException;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1OpaqueObject;
+import com.android.apksig.internal.asn1.Asn1Type;
+import com.android.apksig.internal.jar.ManifestParser;
+import com.android.apksig.internal.pkcs7.Attribute;
+import com.android.apksig.internal.pkcs7.ContentInfo;
+import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber;
+import com.android.apksig.internal.pkcs7.Pkcs7Constants;
+import com.android.apksig.internal.pkcs7.Pkcs7DecodingException;
+import com.android.apksig.internal.pkcs7.SignedData;
+import com.android.apksig.internal.pkcs7.SignerIdentifier;
+import com.android.apksig.internal.pkcs7.SignerInfo;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.X509CertificateUtils;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.InclusiveIntRange;
+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.util.DataSinks;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipFormatException;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Base64.Decoder;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.jar.Attributes;
+
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * APK verifier which uses JAR signing (aka v1 signing scheme).
+ *
+ * @see Signed JAR File
+ */
+public abstract class V1SchemeVerifier {
+
+ private static final String MANIFEST_ENTRY_NAME = V1SchemeSigner.MANIFEST_ENTRY_NAME;
+
+ private V1SchemeVerifier() {}
+
+ /**
+ * Verifies the provided APK's JAR signatures and returns the result of verification. APK is
+ * considered verified only if {@link Result#verified} is {@code true}. If verification fails,
+ * the result will contain errors -- see {@link Result#getErrors()}.
+ *
+ * Verification succeeds iff the APK's JAR signatures are expected to verify on all Android
+ * platform versions in the {@code [minSdkVersion, maxSdkVersion]} range. If the APK's signature
+ * is expected to not verify on any of the specified platform versions, this method returns a
+ * result with one or more errors and whose {@code Result.verified == false}, or this method
+ * throws an exception.
+ *
+ * @throws ApkFormatException if the APK is malformed
+ * @throws IOException if an I/O error occurs when reading the APK
+ * @throws NoSuchAlgorithmException if the APK's JAR signatures cannot be verified because a
+ * required cryptographic algorithm implementation is missing
+ */
+ public static Result verify(
+ DataSource apk,
+ ApkUtils.ZipSections apkSections,
+ Map