Implement APK pin list generation from special asset files
Test: unzip -q -c myapp.apk.signed pinlist.meta | od --endian=big -w8 -tx4
Bug: 65316207
Bug: 79259761
Change-Id: I38f8ba97e1470bb640fc07ead54c19d6b4d428bb
diff --git a/src/main/java/com/android/apksig/ApkSigner.java b/src/main/java/com/android/apksig/ApkSigner.java
index bdf57df..88f2617 100644
--- a/src/main/java/com/android/apksig/ApkSigner.java
+++ b/src/main/java/com/android/apksig/ApkSigner.java
@@ -31,7 +31,9 @@
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;
@@ -49,6 +51,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.regex.Pattern;
/**
* APK signer.
@@ -237,6 +240,9 @@
List<CentralDirectoryRecord> inputCdRecords =
parseZipCentralDirectory(inputCd, inputZipSections);
+ List<Pattern> pinPatterns = extractPinPatterns(inputCdRecords, inputApkLfhSection);
+ List<Hints.ByteRange> pinByteRanges = pinPatterns == null ? null : new ArrayList<>();
+
// Step 3. Obtain a signer engine instance
ApkSignerEngine signerEngine;
if (mSignerEngine != null) {
@@ -298,6 +304,9 @@
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;
@@ -370,6 +379,23 @@
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()) {
@@ -455,6 +481,35 @@
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) {
@@ -707,6 +762,16 @@
return cdRecords;
}
+ private static CentralDirectoryRecord findCdRecord(
+ List<CentralDirectoryRecord> 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.
@@ -714,13 +779,8 @@
static ByteBuffer getAndroidManifestFromApk(
List<CentralDirectoryRecord> cdRecords, DataSource lhfSection)
throws IOException, ApkFormatException, ZipFormatException {
- CentralDirectoryRecord androidManifestCdRecord = null;
- for (CentralDirectoryRecord cdRecord : cdRecords) {
- if (ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
- androidManifestCdRecord = cdRecord;
- break;
- }
- }
+ CentralDirectoryRecord androidManifestCdRecord =
+ findCdRecord(cdRecords, ANDROID_MANIFEST_ZIP_ENTRY_NAME);
if (androidManifestCdRecord == null) {
throw new ApkFormatException("Missing " + ANDROID_MANIFEST_ZIP_ENTRY_NAME);
}
@@ -731,6 +791,30 @@
}
/**
+ * Return list of pin patterns embedded in the pin pattern asset
+ * file. If no such file, return {@code null}.
+ */
+ private static List<Pattern> extractPinPatterns(
+ List<CentralDirectoryRecord> cdRecords, DataSource lhfSection)
+ throws IOException, ApkFormatException {
+ CentralDirectoryRecord pinListCdRecord =
+ findCdRecord(cdRecords, Hints.PIN_HINT_ASSET_ZIP_ENTRY_NAME);
+ List<Pattern> 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}.
*/
diff --git a/src/main/java/com/android/apksig/Hints.java b/src/main/java/com/android/apksig/Hints.java
new file mode 100644
index 0000000..49ef2b0
--- /dev/null
+++ b/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<ByteRange> 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<Pattern> parsePinPatterns(byte[] patternBlob) {
+ ArrayList<Pattern> 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;
+ }
+}