Merge "Remove dead rgb2565"
diff --git a/core/clang/config.mk b/core/clang/config.mk
index a269839..4685514 100644
--- a/core/clang/config.mk
+++ b/core/clang/config.mk
@@ -3,6 +3,12 @@
LLVM_PREBUILTS_PATH := $(LLVM_PREBUILTS_BASE)/$(BUILD_OS)-x86/$(LLVM_PREBUILTS_VERSION)/bin
LLVM_RTLIB_PATH := $(LLVM_PREBUILTS_PATH)/../lib64/clang/$(LLVM_RELEASE_VERSION)/lib/linux/
+# These will come from Soong, drop the environment versions
+unexport CLANG
+unexport CLANG_CXX
+unexport CCC_CC
+unexport CCC_CXX
+
CLANG_TBLGEN := $(BUILD_OUT_EXECUTABLES)/clang-tblgen$(BUILD_EXECUTABLE_SUFFIX)
LLVM_TBLGEN := $(BUILD_OUT_EXECUTABLES)/llvm-tblgen$(BUILD_EXECUTABLE_SUFFIX)
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/apk/ApkUtils.java b/tools/apksigner/core/src/com/android/apksigner/core/apk/ApkUtils.java
new file mode 100644
index 0000000..8cc8c90
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/apk/ApkUtils.java
@@ -0,0 +1,158 @@
+/*
+ * 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.apksigner.core.apk;
+
+import com.android.apksigner.core.internal.util.Pair;
+import com.android.apksigner.core.internal.zip.ZipUtils;
+import com.android.apksigner.core.util.DataSource;
+import com.android.apksigner.core.zip.ZipFormatException;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * APK utilities.
+ */
+public class ApkUtils {
+
+ private ApkUtils() {}
+
+ /**
+ * Finds the main ZIP sections of the provided APK.
+ *
+ * @throws IOException if an I/O error occurred while reading the APK
+ * @throws ZipFormatException if the APK is malformed
+ */
+ public static ZipSections findZipSections(DataSource apk)
+ throws IOException, ZipFormatException {
+ Pair<ByteBuffer, Long> eocdAndOffsetInFile =
+ ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
+ if (eocdAndOffsetInFile == null) {
+ throw new ZipFormatException("ZIP End of Central Directory record not found");
+ }
+
+ ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
+ long eocdOffset = eocdAndOffsetInFile.getSecond();
+ if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
+ throw new ZipFormatException("ZIP64 APK not supported");
+ }
+ 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);
+ }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java
index e185346..103a0ec 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java
@@ -16,7 +16,6 @@
package com.android.apksigner.core.internal.apk.v2;
-import com.android.apksigner.core.internal.util.ByteBufferSink;
import com.android.apksigner.core.internal.util.Pair;
import com.android.apksigner.core.internal.zip.ZipUtils;
import com.android.apksigner.core.util.DataSource;
@@ -191,8 +190,10 @@
// offset field is treated as pointing to the offset at which the APK Signing Block will
// start.
long centralDirOffsetForDigesting = beforeCentralDir.size();
- ByteBuffer eocdBuf = copyToByteBuffer(eocd);
+ 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);
// Compute digests of APK contents.
@@ -600,15 +601,4 @@
}
return result.array();
}
-
- private static ByteBuffer copyToByteBuffer(DataSource dataSource) throws IOException {
- long dataSourceSize = dataSource.size();
- if (dataSourceSize > Integer.MAX_VALUE) {
- throw new IllegalArgumentException("Data source too large: " + dataSourceSize);
- }
- ByteBuffer result = ByteBuffer.allocate((int) dataSourceSize);
- dataSource.feed(0, result.remaining(), new ByteBufferSink(result));
- result.position(0);
- return result;
- }
}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferDataSource.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferDataSource.java
index f12f02e..b2d9ca1 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferDataSource.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferDataSource.java
@@ -35,7 +35,15 @@
* buffer between the buffer's position and limit.
*/
public ByteBufferDataSource(ByteBuffer buffer) {
- mBuffer = buffer.slice();
+ this(buffer, true);
+ }
+
+ /**
+ * Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided
+ * buffer between the buffer's position and limit.
+ */
+ private ByteBufferDataSource(ByteBuffer buffer, boolean sliceRequired) {
+ mBuffer = (sliceRequired) ? buffer.slice() : buffer;
mSize = buffer.remaining();
}
@@ -45,15 +53,12 @@
}
@Override
- public ByteBufferDataSource slice(long offset, long size) {
- if ((offset == 0) && (size == mSize)) {
- return this;
- }
+ public ByteBuffer getByteBuffer(long offset, int size) {
checkChunkValid(offset, size);
- // checkChunkValid ensures that it's OK to cast offset and size to int.
+ // checkChunkValid ensures that it's OK to cast offset to int.
int chunkPosition = (int) offset;
- int chunkLimit = (int) (chunkPosition + size);
+ int chunkLimit = chunkPosition + size;
// Creating a slice of ByteBuffer modifies the state of the source ByteBuffer (position
// and limit fields, to be more specific). We thus use synchronization around these
// state-changing operations to make instances of this class thread-safe.
@@ -66,35 +71,35 @@
mBuffer.limit(chunkLimit);
mBuffer.position(chunkPosition);
- // Create a ByteBufferDataSource for the slice of the buffer between limit and position.
- return new ByteBufferDataSource(mBuffer);
+ return mBuffer.slice();
}
}
@Override
+ public void copyTo(long offset, int size, ByteBuffer dest) {
+ dest.put(getByteBuffer(offset, size));
+ }
+
+ @Override
public void feed(long offset, long size, DataSink sink) throws IOException {
- checkChunkValid(offset, size);
-
- // checkChunkValid ensures that it's OK to cast offset and size to int.
- int chunkPosition = (int) offset;
- int chunkLimit = (int) (chunkPosition + size);
- ByteBuffer chunk;
- // Creating a slice of ByteBuffer modifies the state of the source ByteBuffer (position
- // and limit fields, to be more specific). We thus use synchronization around these
- // state-changing operations to make instances of this class thread-safe.
- synchronized (mBuffer) {
- // ByteBuffer.limit(int) and .position(int) check that that the position >= limit
- // invariant is not broken. Thus, the only way to safely change position and limit
- // without caring about their current values is to first set position to 0 or set the
- // limit to capacity.
- mBuffer.position(0);
-
- mBuffer.limit(chunkLimit);
- mBuffer.position(chunkPosition);
- chunk = mBuffer.slice();
+ if ((size < 0) || (size > mSize)) {
+ throw new IllegalArgumentException("size: " + size + ", source size: " + mSize);
}
+ sink.consume(getByteBuffer(offset, (int) size));
+ }
- sink.consume(chunk);
+ @Override
+ public ByteBufferDataSource slice(long offset, long size) {
+ if ((offset == 0) && (size == mSize)) {
+ return this;
+ }
+ if ((size < 0) || (size > mSize)) {
+ throw new IllegalArgumentException("size: " + size + ", source size: " + mSize);
+ }
+ return new ByteBufferDataSource(
+ getByteBuffer(offset, (int) size),
+ false // no need to slice -- it's already a slice
+ );
}
private void checkChunkValid(long offset, long size) {
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java
index 7b47e50..51110b6 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java
@@ -16,9 +16,14 @@
package com.android.apksigner.core.internal.zip;
+import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import com.android.apksigner.core.internal.util.ByteBufferSink;
+import com.android.apksigner.core.internal.util.Pair;
+import com.android.apksigner.core.util.DataSource;
+
/**
* Assorted ZIP format helpers.
*
@@ -28,7 +33,20 @@
public abstract class ZipUtils {
private ZipUtils() {}
+ public static final short COMPRESSION_METHOD_STORED = 0;
+ public static final short COMPRESSION_METHOD_DEFLATED = 8;
+
+ private static final int ZIP_EOCD_REC_MIN_SIZE = 22;
+ private static final int ZIP_EOCD_REC_SIG = 0x06054b50;
+ private static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10;
+ private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12;
private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
+ private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20;
+
+ private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;
+ private static final int ZIP64_EOCD_LOCATOR_SIG = 0x07064b50;
+
+ private static final int UINT16_MAX_VALUE = 0xffff;
/**
* Sets the offset of the start of the ZIP Central Directory in the archive.
@@ -44,16 +62,221 @@
offset);
}
+ /**
+ * Returns the offset of the start of the ZIP Central Directory in the archive.
+ *
+ * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
+ */
+ public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) {
+ assertByteOrderLittleEndian(zipEndOfCentralDirectory);
+ return getUnsignedInt32(
+ zipEndOfCentralDirectory,
+ zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET);
+ }
+
+ /**
+ * Returns the size (in bytes) of the ZIP Central Directory.
+ *
+ * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
+ */
+ public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) {
+ assertByteOrderLittleEndian(zipEndOfCentralDirectory);
+ return getUnsignedInt32(
+ zipEndOfCentralDirectory,
+ zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET);
+ }
+
+ /**
+ * Returns the total number of records in ZIP Central Directory.
+ *
+ * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
+ */
+ public static int getZipEocdCentralDirectoryTotalRecordCount(
+ ByteBuffer zipEndOfCentralDirectory) {
+ assertByteOrderLittleEndian(zipEndOfCentralDirectory);
+ return getUnsignedInt16(
+ zipEndOfCentralDirectory,
+ zipEndOfCentralDirectory.position()
+ + ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET);
+ }
+
+ /**
+ * Returns the ZIP End of Central Directory record of the provided ZIP file.
+ *
+ * @return contents of the ZIP End of Central Directory record and the record's offset in the
+ * file or {@code null} if the file does not contain the record.
+ *
+ * @throws IOException if an I/O error occurs while reading the file.
+ */
+ public static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(DataSource zip)
+ throws IOException {
+ // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
+ // The record can be identified by its 4-byte signature/magic which is located at the very
+ // beginning of the record. A complication is that the record is variable-length because of
+ // the comment field.
+ // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
+ // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
+ // the candidate record's comment length is such that the remainder of the record takes up
+ // exactly the remaining bytes in the buffer. The search is bounded because the maximum
+ // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
+
+ long fileSize = zip.size();
+ if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
+ return null;
+ }
+
+ // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus
+ // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily
+ // reading more data.
+ Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0);
+ if (result != null) {
+ return result;
+ }
+
+ // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment
+ // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because
+ // the comment length field is an unsigned 16-bit number.
+ return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE);
+ }
+
+ /**
+ * Returns the ZIP End of Central Directory record of the provided ZIP file.
+ *
+ * @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted
+ * value is from 0 to 65535 inclusive. The smaller the value, the faster this method
+ * locates the record, provided its comment field is no longer than this value.
+ *
+ * @return contents of the ZIP End of Central Directory record and the record's offset in the
+ * file or {@code null} if the file does not contain the record.
+ *
+ * @throws IOException if an I/O error occurs while reading the file.
+ */
+ private static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(
+ DataSource zip, int maxCommentSize) throws IOException {
+ // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
+ // The record can be identified by its 4-byte signature/magic which is located at the very
+ // beginning of the record. A complication is that the record is variable-length because of
+ // the comment field.
+ // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
+ // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
+ // the candidate record's comment length is such that the remainder of the record takes up
+ // exactly the remaining bytes in the buffer. The search is bounded because the maximum
+ // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
+
+ if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) {
+ throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize);
+ }
+
+ long fileSize = zip.size();
+ if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
+ // No space for EoCD record in the file.
+ return null;
+ }
+ // Lower maxCommentSize if the file is too small.
+ maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE);
+
+ ByteBuffer buf = ByteBuffer.allocate(ZIP_EOCD_REC_MIN_SIZE + maxCommentSize);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ long bufOffsetInFile = fileSize - buf.capacity();
+ zip.feed(bufOffsetInFile, buf.remaining(), new ByteBufferSink(buf));
+ buf.flip();
+ int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf);
+ if (eocdOffsetInBuf == -1) {
+ // No EoCD record found in the buffer
+ return null;
+ }
+ // EoCD found
+ buf.position(eocdOffsetInBuf);
+ ByteBuffer eocd = buf.slice();
+ eocd.order(ByteOrder.LITTLE_ENDIAN);
+ return Pair.of(eocd, bufOffsetInFile + eocdOffsetInBuf);
+ }
+
+ /**
+ * Returns the position at which ZIP End of Central Directory record starts in the provided
+ * buffer or {@code -1} if the record is not present.
+ *
+ * <p>NOTE: Byte order of {@code zipContents} must be little-endian.
+ */
+ private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
+ assertByteOrderLittleEndian(zipContents);
+
+ // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
+ // The record can be identified by its 4-byte signature/magic which is located at the very
+ // beginning of the record. A complication is that the record is variable-length because of
+ // the comment field.
+ // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
+ // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
+ // the candidate record's comment length is such that the remainder of the record takes up
+ // exactly the remaining bytes in the buffer. The search is bounded because the maximum
+ // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
+
+ int archiveSize = zipContents.capacity();
+ if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) {
+ return -1;
+ }
+ int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE);
+ int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
+ for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength;
+ expectedCommentLength++) {
+ int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength;
+ if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) {
+ int actualCommentLength =
+ getUnsignedInt16(
+ zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET);
+ if (actualCommentLength == expectedCommentLength) {
+ return eocdStartPos;
+ }
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns {@code true} if the provided file contains a ZIP64 End of Central Directory
+ * Locator.
+ *
+ * @param zipEndOfCentralDirectoryPosition offset of the ZIP End of Central Directory record
+ * in the file.
+ *
+ * @throws IOException if an I/O error occurs while reading the data source
+ */
+ public static final boolean isZip64EndOfCentralDirectoryLocatorPresent(
+ DataSource zip, long zipEndOfCentralDirectoryPosition) throws IOException {
+
+ // ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central
+ // Directory Record.
+ long locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE;
+ if (locatorPosition < 0) {
+ return false;
+ }
+
+ ByteBuffer sig = ByteBuffer.allocate(4);
+ sig.order(ByteOrder.LITTLE_ENDIAN);
+ zip.feed(locatorPosition, sig.remaining(), new ByteBufferSink(sig));
+ sig.flip();
+ return sig.getInt(0) == ZIP64_EOCD_LOCATOR_SIG;
+ }
+
private static void assertByteOrderLittleEndian(ByteBuffer buffer) {
if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
}
}
+ private static int getUnsignedInt16(ByteBuffer buffer, int offset) {
+ return buffer.getShort(offset) & 0xffff;
+ }
+
private static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) {
if ((value < 0) || (value > 0xffffffffL)) {
throw new IllegalArgumentException("uint32 value of out range: " + value);
}
- buffer.putInt(buffer.position() + offset, (int) value);
+ buffer.putInt(offset, (int) value);
+ }
+
+ private static long getUnsignedInt32(ByteBuffer buffer, int offset) {
+ return buffer.getInt(offset) & 0xffffffffL;
}
}
\ No newline at end of file
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSource.java b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSource.java
index 27ff7a8..e268dd2 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSource.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSource.java
@@ -17,6 +17,7 @@
package com.android.apksigner.core.util;
import java.io.IOException;
+import java.nio.ByteBuffer;
/**
* Abstract representation of a source of data.
@@ -29,6 +30,25 @@
* may have worked as the unifying abstraction.</li>
* <li>Support sources which do not fit into logical memory as a contiguous region.</li>
* </ul>
+ *
+ * <p>There are following ways to obtain a chunk of data from the data source:
+ * <ul>
+ * <li>Stream the chunk's data into a {@link DataSink} using
+ * {@link #feed(long, long, DataSink) feed}. This is best suited for scenarios where there is no
+ * need to have the chunk's data accessible at the same time, for example, when computing the
+ * digest of the chunk. If you need to keep the chunk's data around after {@code feed}
+ * completes, you must create a copy during {@code feed}. However, in that case the following
+ * methods of obtaining the chunk's data may be more appropriate.</li>
+ * <li>Obtain a {@link ByteBuffer} containing the chunk's data using
+ * {@link #getByteBuffer(long, int) getByteBuffer}. Depending on the data source, the chunk's
+ * data may or may not be copied by this operation. This is best suited for scenarios where
+ * you need to access the chunk's data in arbitrary order, but don't need to modify the data and
+ * thus don't require a copy of the data.</li>
+ * <li>Copy the chunk's data to a {@link ByteBuffer} using
+ * {@link #copyTo(long, int, ByteBuffer) copyTo}. This is best suited for scenarios where
+ * you require a copy of the chunk's data, such as to when you need to modify the data.
+ * </li>
+ * </ul>
*/
public interface DataSource {
@@ -46,8 +66,33 @@
void feed(long offset, long size, DataSink sink) throws IOException;
/**
+ * Returns a buffer holding the contents of the specified chunk of data from this data source.
+ * Changes to the data source are not guaranteed to be reflected in the returned buffer.
+ * Similarly, changes in the buffer are not guaranteed to be reflected in the data source.
+ *
+ * <p>The returned buffer's position is {@code 0}, and the buffer's limit and capacity is
+ * {@code size}.
+ *
+ * @param offset index (in bytes) at which the chunk starts inside data source
+ * @param size size (in bytes) of the chunk
+ */
+ ByteBuffer getByteBuffer(long offset, int size) throws IOException;
+
+ /**
+ * Copies the specified chunk from this data source into the provided destination buffer,
+ * advancing the destination buffer's position by {@code size}.
+ *
+ * @param offset index (in bytes) at which the chunk starts inside data source
+ * @param size size (in bytes) of the chunk
+ */
+ void copyTo(long offset, int size, ByteBuffer dest) throws IOException;
+
+ /**
* Returns a data source representing the specified region of data of this data source. Changes
* to data represented by this data source will also be visible in the returned data source.
+ *
+ * @param offset index (in bytes) at which the region starts inside data source
+ * @param size size (in bytes) of the region
*/
DataSource slice(long offset, long size);
}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/zip/ZipFormatException.java b/tools/apksigner/core/src/com/android/apksigner/core/zip/ZipFormatException.java
new file mode 100644
index 0000000..7da57d9
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/zip/ZipFormatException.java
@@ -0,0 +1,32 @@
+/*
+ * 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.apksigner.core.zip;
+
+/**
+ * Indicates that a ZIP archive is not well-formed.
+ */
+public class ZipFormatException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public ZipFormatException(String message) {
+ super(message);
+ }
+
+ public ZipFormatException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/tools/signapk/Android.mk b/tools/signapk/Android.mk
index ac217c7..eff066c 100644
--- a/tools/signapk/Android.mk
+++ b/tools/signapk/Android.mk
@@ -21,7 +21,11 @@
LOCAL_MODULE := signapk
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_JAR_MANIFEST := SignApk.mf
-LOCAL_STATIC_JAVA_LIBRARIES := bouncycastle-host bouncycastle-bcpkix-host conscrypt-host
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ apksigner-core \
+ bouncycastle-host \
+ bouncycastle-bcpkix-host \
+ conscrypt-host
LOCAL_REQUIRED_MODULES := libconscrypt_openjdk_jni
include $(BUILD_HOST_JAVA_LIBRARY)
diff --git a/tools/signapk/src/com/android/signapk/ApkSignerV2.java b/tools/signapk/src/com/android/signapk/ApkSignerV2.java
deleted file mode 100644
index 7b617db..0000000
--- a/tools/signapk/src/com/android/signapk/ApkSignerV2.java
+++ /dev/null
@@ -1,725 +0,0 @@
-/*
- * 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.signapk;
-
-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.MGF1ParameterSpec;
-import java.security.spec.PSSParameterSpec;
-import java.security.spec.X509EncodedKeySpec;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * APK Signature Scheme v2 signer.
- *
- * <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
- * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
- * uncompressed contents of ZIP entries.
- */
-public abstract class ApkSignerV2 {
- /*
- * The two main goals of APK Signature Scheme v2 are:
- * 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature
- * cover every byte of the APK being signed.
- * 2. Enable much faster signature and integrity verification. This is achieved by requiring
- * only a minimal amount of APK parsing before the signature is verified, thus completely
- * bypassing ZIP entry decompression and by making integrity verification parallelizable by
- * employing a hash tree.
- *
- * The generated signature block is wrapped into an APK Signing Block and inserted into the
- * original APK immediately before the start of ZIP Central Directory. This is to ensure that
- * JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for
- * extensibility. For example, a future signature scheme could insert its signatures there as
- * well. The contract of the APK Signing Block is that all contents outside of the block must be
- * protected by signatures inside the block.
- */
-
- public static final int SIGNATURE_RSA_PSS_WITH_SHA256 = 0x0101;
- public static final int SIGNATURE_RSA_PSS_WITH_SHA512 = 0x0102;
- public static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0103;
- public static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512 = 0x0104;
- public static final int SIGNATURE_ECDSA_WITH_SHA256 = 0x0201;
- public static final int SIGNATURE_ECDSA_WITH_SHA512 = 0x0202;
- public static final int SIGNATURE_DSA_WITH_SHA256 = 0x0301;
-
- /**
- * {@code .SF} file header section attribute indicating that the APK is signed not just with
- * JAR signature scheme but also with APK Signature Scheme v2 or newer. This attribute
- * facilitates v2 signature stripping detection.
- *
- * <p>The attribute contains a comma-separated set of signature scheme IDs.
- */
- public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed";
- public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE = "2";
-
- private static final int CONTENT_DIGEST_CHUNKED_SHA256 = 0;
- private static final int CONTENT_DIGEST_CHUNKED_SHA512 = 1;
-
- private static final int CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
-
- private 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 APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
-
- private ApkSignerV2() {}
-
- /**
- * Signer configuration.
- */
- public static final class SignerConfig {
- /** Private key. */
- public PrivateKey privateKey;
-
- /**
- * Certificates, with the first certificate containing the public key corresponding to
- * {@link #privateKey}.
- */
- public List<X509Certificate> certificates;
-
- /**
- * List of signature algorithms with which to sign (see {@code SIGNATURE_...} constants).
- */
- public List<Integer> signatureAlgorithms;
- }
-
- /**
- * Signs the provided APK using APK Signature Scheme v2 and returns the signed APK as a list of
- * consecutive chunks.
- *
- * <p>NOTE: To enable APK signature verifier to detect v2 signature stripping, header sections
- * of META-INF/*.SF files of APK being signed must contain the
- * {@code X-Android-APK-Signed: true} attribute.
- *
- * @param inputApk contents of the APK to be signed. The APK starts at the current position
- * of the buffer and ends at the limit of the buffer.
- * @param signerConfigs signer configurations, one for each signer.
- *
- * @throws ApkParseException if the APK cannot be parsed.
- * @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 ByteBuffer[] sign(
- ByteBuffer inputApk,
- List<SignerConfig> signerConfigs)
- throws ApkParseException, InvalidKeyException, SignatureException {
- // Slice/create a view in the inputApk to make sure that:
- // 1. inputApk is what's between position and limit of the original inputApk, and
- // 2. changes to position, limit, and byte order are not reflected in the original.
- ByteBuffer originalInputApk = inputApk;
- inputApk = originalInputApk.slice();
- inputApk.order(ByteOrder.LITTLE_ENDIAN);
-
- // Locate ZIP End of Central Directory (EoCD), Central Directory, and check that Central
- // Directory is immediately followed by the ZIP End of Central Directory.
- int eocdOffset = ZipUtils.findZipEndOfCentralDirectoryRecord(inputApk);
- if (eocdOffset == -1) {
- throw new ApkParseException("Failed to locate ZIP End of Central Directory");
- }
- if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(inputApk, eocdOffset)) {
- throw new ApkParseException("ZIP64 format not supported");
- }
- inputApk.position(eocdOffset);
- long centralDirSizeLong = ZipUtils.getZipEocdCentralDirectorySizeBytes(inputApk);
- if (centralDirSizeLong > Integer.MAX_VALUE) {
- throw new ApkParseException(
- "ZIP Central Directory size out of range: " + centralDirSizeLong);
- }
- int centralDirSize = (int) centralDirSizeLong;
- long centralDirOffsetLong = ZipUtils.getZipEocdCentralDirectoryOffset(inputApk);
- if (centralDirOffsetLong > Integer.MAX_VALUE) {
- throw new ApkParseException(
- "ZIP Central Directory offset in file out of range: " + centralDirOffsetLong);
- }
- int centralDirOffset = (int) centralDirOffsetLong;
- int expectedEocdOffset = centralDirOffset + centralDirSize;
- if (expectedEocdOffset < centralDirOffset) {
- throw new ApkParseException(
- "ZIP Central Directory extent too large. Offset: " + centralDirOffset
- + ", size: " + centralDirSize);
- }
- if (eocdOffset != expectedEocdOffset) {
- throw new ApkParseException(
- "ZIP Central Directory not immeiately followed by ZIP End of"
- + " Central Directory. CD end: " + expectedEocdOffset
- + ", EoCD start: " + eocdOffset);
- }
-
- // Create ByteBuffers holding the contents of everything before ZIP Central Directory,
- // ZIP Central Directory, and ZIP End of Central Directory.
- inputApk.clear();
- ByteBuffer beforeCentralDir = getByteBuffer(inputApk, centralDirOffset);
- ByteBuffer centralDir = getByteBuffer(inputApk, eocdOffset - centralDirOffset);
- // Create a copy of End of Central Directory because we'll need modify its contents later.
- byte[] eocdBytes = new byte[inputApk.remaining()];
- inputApk.get(eocdBytes);
- ByteBuffer eocd = ByteBuffer.wrap(eocdBytes);
- eocd.order(inputApk.order());
-
- // Figure which which digests to use for APK contents.
- Set<Integer> contentDigestAlgorithms = new HashSet<>();
- for (SignerConfig signerConfig : signerConfigs) {
- for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
- contentDigestAlgorithms.add(
- getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm));
- }
- }
-
- // Compute digests of APK contents.
- Map<Integer, byte[]> contentDigests; // digest algorithm ID -> digest
- try {
- contentDigests =
- computeContentDigests(
- contentDigestAlgorithms,
- new ByteBuffer[] {beforeCentralDir, centralDir, eocd});
- } 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.
- ByteBuffer apkSigningBlock =
- ByteBuffer.wrap(generateApkSigningBlock(signerConfigs, contentDigests));
-
- // Update Central Directory Offset in End of Central Directory Record. Central Directory
- // follows the APK Signing Block and thus is shifted by the size of the APK Signing Block.
- centralDirOffset += apkSigningBlock.remaining();
- eocd.clear();
- ZipUtils.setZipEocdCentralDirectoryOffset(eocd, centralDirOffset);
-
- // Follow the Java NIO pattern for ByteBuffer whose contents have been consumed.
- originalInputApk.position(originalInputApk.limit());
-
- // Reset positions (to 0) and limits (to capacity) in the ByteBuffers below to follow the
- // Java NIO pattern for ByteBuffers which are ready for their contents to be read by caller.
- // Contrary to the name, this does not clear the contents of these ByteBuffer.
- beforeCentralDir.clear();
- centralDir.clear();
- eocd.clear();
-
- // Insert APK Signing Block immediately before the ZIP Central Directory.
- return new ByteBuffer[] {
- beforeCentralDir,
- apkSigningBlock,
- centralDir,
- eocd,
- };
- }
-
- private static Map<Integer, byte[]> computeContentDigests(
- Set<Integer> digestAlgorithms,
- ByteBuffer[] contents) throws 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.
-
- int chunkCount = 0;
- for (ByteBuffer input : contents) {
- chunkCount += getChunkCount(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
- }
-
- final Map<Integer, byte[]> digestsOfChunks = new HashMap<>(digestAlgorithms.size());
- for (int digestAlgorithm : digestAlgorithms) {
- int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
- byte[] concatenationOfChunkCountAndChunkDigests =
- new byte[5 + chunkCount * digestOutputSizeBytes];
- concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
- setUnsignedInt32LittleEngian(
- chunkCount, concatenationOfChunkCountAndChunkDigests, 1);
- digestsOfChunks.put(digestAlgorithm, concatenationOfChunkCountAndChunkDigests);
- }
-
- int chunkIndex = 0;
- byte[] chunkContentPrefix = new byte[5];
- chunkContentPrefix[0] = (byte) 0xa5;
- // Optimization opportunity: digests of chunks can be computed in parallel.
- for (ByteBuffer input : contents) {
- while (input.hasRemaining()) {
- int chunkSize =
- Math.min(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
- final ByteBuffer chunk = getByteBuffer(input, chunkSize);
- for (int digestAlgorithm : digestAlgorithms) {
- String jcaAlgorithmName =
- getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
- MessageDigest md;
- try {
- md = MessageDigest.getInstance(jcaAlgorithmName);
- } catch (NoSuchAlgorithmException e) {
- throw new DigestException(
- jcaAlgorithmName + " MessageDigest not supported", e);
- }
- // Reset position to 0 and limit to capacity. Position would've been modified
- // by the preceding iteration of this loop. NOTE: Contrary to the method name,
- // this does not modify the contents of the chunk.
- chunk.clear();
- setUnsignedInt32LittleEngian(chunk.remaining(), chunkContentPrefix, 1);
- md.update(chunkContentPrefix);
- md.update(chunk);
- byte[] concatenationOfChunkCountAndChunkDigests =
- digestsOfChunks.get(digestAlgorithm);
- int expectedDigestSizeBytes =
- getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
- int actualDigestSizeBytes =
- md.digest(
- concatenationOfChunkCountAndChunkDigests,
- 5 + chunkIndex * expectedDigestSizeBytes,
- expectedDigestSizeBytes);
- if (actualDigestSizeBytes != expectedDigestSizeBytes) {
- throw new DigestException(
- "Unexpected output size of " + md.getAlgorithm()
- + " digest: " + actualDigestSizeBytes);
- }
- }
- chunkIndex++;
- }
- }
-
- Map<Integer, byte[]> result = new HashMap<>(digestAlgorithms.size());
- for (Map.Entry<Integer, byte[]> entry : digestsOfChunks.entrySet()) {
- int digestAlgorithm = entry.getKey();
- byte[] concatenationOfChunkCountAndChunkDigests = entry.getValue();
- String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
- MessageDigest md;
- try {
- md = MessageDigest.getInstance(jcaAlgorithmName);
- } catch (NoSuchAlgorithmException e) {
- throw new DigestException(jcaAlgorithmName + " MessageDigest not supported", e);
- }
- result.put(digestAlgorithm, md.digest(concatenationOfChunkCountAndChunkDigests));
- }
- return result;
- }
-
- private static final int getChunkCount(int inputSize, int chunkSize) {
- return (inputSize + chunkSize - 1) / chunkSize;
- }
-
- private static void setUnsignedInt32LittleEngian(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);
- }
-
- private static byte[] generateApkSigningBlock(
- List<SignerConfig> signerConfigs,
- Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
- byte[] apkSignatureSchemeV2Block =
- generateApkSignatureSchemeV2Block(signerConfigs, contentDigests);
- return generateApkSigningBlock(apkSignatureSchemeV2Block);
- }
-
- private static byte[] generateApkSigningBlock(byte[] apkSignatureSchemeV2Block) {
- // FORMAT:
- // uint64: size (excluding this field)
- // repeated ID-value pairs:
- // uint64: size (excluding this field)
- // uint32: ID
- // (size - 4) bytes: value
- // uint64: size (same as the one above)
- // uint128: magic
-
- int resultSize =
- 8 // size
- + 8 + 4 + apkSignatureSchemeV2Block.length // v2Block as ID-value pair
- + 8 // size
- + 16 // magic
- ;
- ByteBuffer result = ByteBuffer.allocate(resultSize);
- result.order(ByteOrder.LITTLE_ENDIAN);
- long blockSizeFieldValue = resultSize - 8;
- result.putLong(blockSizeFieldValue);
-
- long pairSizeFieldValue = 4 + apkSignatureSchemeV2Block.length;
- result.putLong(pairSizeFieldValue);
- result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
- result.put(apkSignatureSchemeV2Block);
-
- result.putLong(blockSizeFieldValue);
- result.put(APK_SIGNING_BLOCK_MAGIC);
-
- return result.array();
- }
-
- private static byte[] generateApkSignatureSchemeV2Block(
- List<SignerConfig> signerConfigs,
- Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
- // FORMAT:
- // * length-prefixed sequence of length-prefixed signer blocks.
-
- List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
- int signerNumber = 0;
- for (SignerConfig signerConfig : signerConfigs) {
- signerNumber++;
- byte[] signerBlock;
- try {
- signerBlock = generateSignerBlock(signerConfig, contentDigests);
- } catch (InvalidKeyException e) {
- throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
- } catch (SignatureException e) {
- throw new SignatureException("Signer #" + signerNumber + " failed", e);
- }
- signerBlocks.add(signerBlock);
- }
-
- return encodeAsSequenceOfLengthPrefixedElements(
- new byte[][] {
- encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
- });
- }
-
- private static byte[] generateSignerBlock(
- SignerConfig signerConfig,
- Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
- if (signerConfig.certificates.isEmpty()) {
- throw new SignatureException("No certificates configured for signer");
- }
- PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
-
- byte[] encodedPublicKey = encodePublicKey(publicKey);
-
- V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData();
- try {
- signedData.certificates = encodeCertificates(signerConfig.certificates);
- } catch (CertificateEncodingException e) {
- throw new SignatureException("Failed to encode certificates", e);
- }
-
- List<Pair<Integer, byte[]>> digests =
- new ArrayList<>(signerConfig.signatureAlgorithms.size());
- for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
- int contentDigestAlgorithm =
- getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm);
- byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
- if (contentDigest == null) {
- throw new RuntimeException(
- getContentDigestAlgorithmJcaDigestAlgorithm(contentDigestAlgorithm)
- + " content digest for "
- + getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithm)
- + " not computed");
- }
- digests.add(Pair.create(signatureAlgorithm, contentDigest));
- }
- signedData.digests = digests;
-
- V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer();
- // FORMAT:
- // * length-prefixed sequence of length-prefixed digests:
- // * uint32: signature algorithm ID
- // * length-prefixed bytes: digest of contents
- // * length-prefixed sequence of certificates:
- // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
- // * length-prefixed sequence of length-prefixed additional attributes:
- // * uint32: ID
- // * (length - 4) bytes: value
- signer.signedData = encodeAsSequenceOfLengthPrefixedElements(new byte[][] {
- encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(signedData.digests),
- encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
- // additional attributes
- new byte[0],
- });
- signer.publicKey = encodedPublicKey;
- signer.signatures = new ArrayList<>();
- for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
- Pair<String, ? extends AlgorithmParameterSpec> signatureParams =
- getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithm);
- String jcaSignatureAlgorithm = signatureParams.getFirst();
- AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureParams.getSecond();
- byte[] signatureBytes;
- try {
- Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
- signature.initSign(signerConfig.privateKey);
- if (jcaSignatureAlgorithmParams != null) {
- signature.setParameter(jcaSignatureAlgorithmParams);
- }
- signature.update(signer.signedData);
- signatureBytes = signature.sign();
- } catch (InvalidKeyException e) {
- throw new InvalidKeyException("Failed sign using " + jcaSignatureAlgorithm, e);
- } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
- | SignatureException e) {
- throw new SignatureException("Failed sign using " + jcaSignatureAlgorithm, e);
- }
-
- try {
- Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
- signature.initVerify(publicKey);
- if (jcaSignatureAlgorithmParams != null) {
- signature.setParameter(jcaSignatureAlgorithmParams);
- }
- signature.update(signer.signedData);
- 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 (NoSuchAlgorithmException | InvalidAlgorithmParameterException
- | SignatureException e) {
- throw new SignatureException("Failed to verify generated " + jcaSignatureAlgorithm
- + " signature using public key from certificate", e);
- }
-
- signer.signatures.add(Pair.create(signatureAlgorithm, signatureBytes));
- }
-
- // FORMAT:
- // * length-prefixed signed data
- // * length-prefixed sequence of length-prefixed signatures:
- // * uint32: signature algorithm ID
- // * length-prefixed bytes: signature of signed data
- // * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
- return encodeAsSequenceOfLengthPrefixedElements(
- new byte[][] {
- signer.signedData,
- encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
- signer.signatures),
- signer.publicKey,
- });
- }
-
- private static final class V2SignatureSchemeBlock {
- private static final class Signer {
- public byte[] signedData;
- public List<Pair<Integer, byte[]>> signatures;
- public byte[] publicKey;
- }
-
- private static final class SignedData {
- public List<Pair<Integer, byte[]>> digests;
- public List<byte[]> certificates;
- }
- }
-
- private static byte[] encodePublicKey(PublicKey publicKey) throws InvalidKeyException {
- 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 (NoSuchAlgorithmException | 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<byte[]> encodeCertificates(List<X509Certificate> certificates)
- throws CertificateEncodingException {
- List<byte[]> result = new ArrayList<>();
- for (X509Certificate certificate : certificates) {
- result.add(certificate.getEncoded());
- }
- return result;
- }
-
- private static byte[] encodeAsSequenceOfLengthPrefixedElements(List<byte[]> sequence) {
- return encodeAsSequenceOfLengthPrefixedElements(
- sequence.toArray(new byte[sequence.size()][]));
- }
-
- private 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();
- }
-
- private static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
- List<Pair<Integer, byte[]>> sequence) {
- int resultSize = 0;
- for (Pair<Integer, byte[]> element : sequence) {
- resultSize += 12 + element.getSecond().length;
- }
- ByteBuffer result = ByteBuffer.allocate(resultSize);
- result.order(ByteOrder.LITTLE_ENDIAN);
- for (Pair<Integer, byte[]> 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();
- }
-
- /**
- * Relative <em>get</em> method for reading {@code size} number of bytes from the current
- * position of this buffer.
- *
- * <p>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);
- }
- }
-
- private static Pair<String, ? extends AlgorithmParameterSpec>
- getSignatureAlgorithmJcaSignatureAlgorithm(int sigAlgorithm) {
- switch (sigAlgorithm) {
- case SIGNATURE_RSA_PSS_WITH_SHA256:
- return Pair.create(
- "SHA256withRSA/PSS",
- new PSSParameterSpec(
- "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1));
- case SIGNATURE_RSA_PSS_WITH_SHA512:
- return Pair.create(
- "SHA512withRSA/PSS",
- new PSSParameterSpec(
- "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1));
- case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
- return Pair.create("SHA256withRSA", null);
- case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
- return Pair.create("SHA512withRSA", null);
- case SIGNATURE_ECDSA_WITH_SHA256:
- return Pair.create("SHA256withECDSA", null);
- case SIGNATURE_ECDSA_WITH_SHA512:
- return Pair.create("SHA512withECDSA", null);
- case SIGNATURE_DSA_WITH_SHA256:
- return Pair.create("SHA256withDSA", null);
- default:
- throw new IllegalArgumentException(
- "Unknown signature algorithm: 0x"
- + Long.toHexString(sigAlgorithm & 0xffffffff));
- }
- }
-
- private static int getSignatureAlgorithmContentDigestAlgorithm(int sigAlgorithm) {
- switch (sigAlgorithm) {
- case SIGNATURE_RSA_PSS_WITH_SHA256:
- case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
- case SIGNATURE_ECDSA_WITH_SHA256:
- case SIGNATURE_DSA_WITH_SHA256:
- return CONTENT_DIGEST_CHUNKED_SHA256;
- case SIGNATURE_RSA_PSS_WITH_SHA512:
- case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
- case SIGNATURE_ECDSA_WITH_SHA512:
- return CONTENT_DIGEST_CHUNKED_SHA512;
- default:
- throw new IllegalArgumentException(
- "Unknown signature algorithm: 0x"
- + Long.toHexString(sigAlgorithm & 0xffffffff));
- }
- }
-
- private static String getContentDigestAlgorithmJcaDigestAlgorithm(int digestAlgorithm) {
- switch (digestAlgorithm) {
- case CONTENT_DIGEST_CHUNKED_SHA256:
- return "SHA-256";
- case CONTENT_DIGEST_CHUNKED_SHA512:
- return "SHA-512";
- default:
- throw new IllegalArgumentException(
- "Unknown content digest algorthm: " + digestAlgorithm);
- }
- }
-
- private static int getContentDigestAlgorithmOutputSizeBytes(int digestAlgorithm) {
- switch (digestAlgorithm) {
- case CONTENT_DIGEST_CHUNKED_SHA256:
- return 256 / 8;
- case CONTENT_DIGEST_CHUNKED_SHA512:
- return 512 / 8;
- default:
- throw new IllegalArgumentException(
- "Unknown content digest algorthm: " + digestAlgorithm);
- }
- }
-
- /**
- * Indicates that APK file could not be parsed.
- */
- public static class ApkParseException extends Exception {
- private static final long serialVersionUID = 1L;
-
- public ApkParseException(String message) {
- super(message);
- }
-
- public ApkParseException(String message, Throwable cause) {
- super(message, cause);
- }
- }
-}
diff --git a/tools/signapk/src/com/android/signapk/Pair.java b/tools/signapk/src/com/android/signapk/Pair.java
deleted file mode 100644
index e4a6c92..0000000
--- a/tools/signapk/src/com/android/signapk/Pair.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * 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.signapk;
-
-/**
- * Pair of two elements.
- */
-public final class Pair<A, B> {
- private final A mFirst;
- private final B mSecond;
-
- private Pair(A first, B second) {
- mFirst = first;
- mSecond = second;
- }
-
- public static <A, B> Pair<A, B> create(A first, B second) {
- return new Pair<A, B>(first, second);
- }
-
- public A getFirst() {
- return mFirst;
- }
-
- public B getSecond() {
- return mSecond;
- }
-
- @Override
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode());
- result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode());
- return result;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null) {
- return false;
- }
- if (getClass() != obj.getClass()) {
- return false;
- }
- @SuppressWarnings("rawtypes")
- Pair other = (Pair) obj;
- if (mFirst == null) {
- if (other.mFirst != null) {
- return false;
- }
- } else if (!mFirst.equals(other.mFirst)) {
- return false;
- }
- if (mSecond == null) {
- if (other.mSecond != null) {
- return false;
- }
- } else if (!mSecond.equals(other.mSecond)) {
- return false;
- }
- return true;
- }
-}
diff --git a/tools/signapk/src/com/android/signapk/SignApk.java b/tools/signapk/src/com/android/signapk/SignApk.java
index a7c9fc3..d84d070 100644
--- a/tools/signapk/src/com/android/signapk/SignApk.java
+++ b/tools/signapk/src/com/android/signapk/SignApk.java
@@ -23,7 +23,6 @@
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSException;
-import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.CMSTypedData;
@@ -33,9 +32,15 @@
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
-import org.bouncycastle.util.encoders.Base64;
import org.conscrypt.OpenSSLProvider;
+import com.android.apksigner.core.ApkSignerEngine;
+import com.android.apksigner.core.DefaultApkSignerEngine;
+import com.android.apksigner.core.apk.ApkUtils;
+import com.android.apksigner.core.util.DataSink;
+import com.android.apksigner.core.util.DataSources;
+import com.android.apksigner.core.zip.ZipFormatException;
+
import java.io.Console;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
@@ -49,19 +54,14 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
-import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
-import java.security.DigestOutputStream;
import java.security.GeneralSecurityException;
-import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
-import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.Provider;
-import java.security.PublicKey;
import java.security.Security;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateFactory;
@@ -71,17 +71,12 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
-import java.util.Iterator;
import java.util.List;
import java.util.Locale;
-import java.util.Map;
import java.util.TimeZone;
-import java.util.TreeMap;
-import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
-import java.util.jar.Manifest;
import java.util.regex.Pattern;
import javax.crypto.Cipher;
@@ -113,11 +108,6 @@
* APK Signature Scheme v2.
*/
class SignApk {
- private static final String CERT_SF_NAME = "META-INF/CERT.SF";
- private static final String CERT_SIG_NAME = "META-INF/CERT.%s";
- private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
- private static final String CERT_SIG_MULTI_NAME = "META-INF/CERT%d.%s";
-
private static final String OTACERT_NAME = "META-INF/com/android/otacert";
/**
@@ -137,36 +127,6 @@
private static final int USE_SHA1 = 1;
private static final int USE_SHA256 = 2;
- /** Digest algorithm used when signing the APK using APK Signature Scheme v2. */
- private static final String APK_SIG_SCHEME_V2_DIGEST_ALGORITHM = "SHA-256";
-
- /**
- * Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used
- * for v1 signing (JAR signing) an APK using the private key corresponding to the provided
- * certificate.
- *
- * @param minSdkVersion minimum Android platform API Level supported by the APK (see
- * minSdkVersion attribute in AndroidManifest.xml). The higher the minSdkVersion, the
- * stronger hash may be used for signing the APK.
- */
- private static int getV1DigestAlgorithmForApk(X509Certificate cert, int minSdkVersion) {
- String keyAlgorithm = cert.getPublicKey().getAlgorithm();
- if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
- // RSA can be used only with SHA-1 prior to API Level 18.
- return (minSdkVersion < 18) ? USE_SHA1 : USE_SHA256;
- } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
- // ECDSA cannot be used prior to API Level 18 at all. It can only be used with SHA-1
- // on API Levels 18, 19, and 20.
- if (minSdkVersion < 18) {
- throw new IllegalArgumentException(
- "ECDSA signatures only supported for minSdkVersion 18 and higher");
- }
- return (minSdkVersion < 21) ? USE_SHA1 : USE_SHA256;
- } else {
- throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
- }
- }
-
/**
* Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used
* for signing an OTA update package using the private key corresponding to the provided
@@ -187,10 +147,10 @@
/**
* Returns the JCA {@link java.security.Signature} algorithm to be used for signing and OTA
- * or v1 signing an APK using the private key corresponding to the provided certificate and the
+ * update package using the private key corresponding to the provided certificate and the
* provided digest algorithm (see {@code USE_SHA1} and {@code USE_SHA256} constants).
*/
- private static String getJcaSignatureAlgorithmForV1SigningOrOta(
+ private static String getJcaSignatureAlgorithmForOta(
X509Certificate cert, int hash) {
String sigAlgDigestPrefix;
switch (hash) {
@@ -214,11 +174,6 @@
}
}
- /* Files matching this pattern are not copied to the output. */
- private static final Pattern STRIP_PATTERN =
- Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|("
- + Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
-
private static X509Certificate readPublicKey(File file)
throws IOException, GeneralSecurityException {
FileInputStream input = new FileInputStream(file);
@@ -324,89 +279,6 @@
}
/**
- * Add the hash(es) of every file to the manifest, creating it if
- * necessary.
- */
- private static Manifest addDigestsToManifest(
- JarFile jar, Pattern ignoredFilenamePattern, int hashes)
- throws IOException, GeneralSecurityException {
- Manifest input = jar.getManifest();
- Manifest output = new Manifest();
- Attributes main = output.getMainAttributes();
- if (input != null) {
- main.putAll(input.getMainAttributes());
- } else {
- main.putValue("Manifest-Version", "1.0");
- main.putValue("Created-By", "1.0 (Android SignApk)");
- }
-
- MessageDigest md_sha1 = null;
- MessageDigest md_sha256 = null;
- if ((hashes & USE_SHA1) != 0) {
- md_sha1 = MessageDigest.getInstance("SHA1");
- }
- if ((hashes & USE_SHA256) != 0) {
- md_sha256 = MessageDigest.getInstance("SHA256");
- }
-
- byte[] buffer = new byte[4096];
- int num;
-
- // We sort the input entries by name, and add them to the
- // output manifest in sorted order. We expect that the output
- // map will be deterministic.
-
- TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
-
- for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
- JarEntry entry = e.nextElement();
- byName.put(entry.getName(), entry);
- }
-
- for (JarEntry entry: byName.values()) {
- String name = entry.getName();
- if (!entry.isDirectory()
- && (ignoredFilenamePattern == null
- || !ignoredFilenamePattern.matcher(name).matches())) {
- InputStream data = jar.getInputStream(entry);
- while ((num = data.read(buffer)) > 0) {
- if (md_sha1 != null) md_sha1.update(buffer, 0, num);
- if (md_sha256 != null) md_sha256.update(buffer, 0, num);
- }
-
- Attributes attr = null;
- if (input != null) attr = input.getAttributes(name);
- attr = attr != null ? new Attributes(attr) : new Attributes();
- // Remove any previously computed digests from this entry's attributes.
- for (Iterator<Object> i = attr.keySet().iterator(); i.hasNext();) {
- Object key = i.next();
- if (!(key instanceof Attributes.Name)) {
- continue;
- }
- String attributeNameLowerCase =
- ((Attributes.Name) key).toString().toLowerCase(Locale.US);
- if (attributeNameLowerCase.endsWith("-digest")) {
- i.remove();
- }
- }
- // Add SHA-1 digest if requested
- if (md_sha1 != null) {
- attr.putValue("SHA1-Digest",
- new String(Base64.encode(md_sha1.digest()), "ASCII"));
- }
- // Add SHA-256 digest if requested
- if (md_sha256 != null) {
- attr.putValue("SHA-256-Digest",
- new String(Base64.encode(md_sha256.digest()), "ASCII"));
- }
- output.getEntries().put(name, attr);
- }
- }
-
- return output;
- }
-
- /**
* Add a copy of the public key to the archive; this should
* exactly match one of the files in
* /system/etc/security/otacerts.zip on the device. (The same
@@ -416,7 +288,7 @@
private static void addOtacert(JarOutputStream outputJar,
File publicKeyFile,
long timestamp)
- throws IOException, GeneralSecurityException {
+ throws IOException {
JarEntry je = new JarEntry(OTACERT_NAME);
je.setTime(timestamp);
@@ -431,94 +303,6 @@
}
- /** Write to another stream and track how many bytes have been
- * written.
- */
- private static class CountOutputStream extends FilterOutputStream {
- private int mCount;
-
- public CountOutputStream(OutputStream out) {
- super(out);
- mCount = 0;
- }
-
- @Override
- public void write(int b) throws IOException {
- super.write(b);
- mCount++;
- }
-
- @Override
- public void write(byte[] b, int off, int len) throws IOException {
- super.write(b, off, len);
- mCount += len;
- }
-
- public int size() {
- return mCount;
- }
- }
-
- /** Write a .SF file with a digest of the specified manifest. */
- private static void writeSignatureFile(Manifest manifest, OutputStream out,
- int hash, boolean additionallySignedUsingAnApkSignatureScheme)
- throws IOException, GeneralSecurityException {
- Manifest sf = new Manifest();
- Attributes main = sf.getMainAttributes();
- main.putValue("Signature-Version", "1.0");
- main.putValue("Created-By", "1.0 (Android SignApk)");
- if (additionallySignedUsingAnApkSignatureScheme) {
- // Add APK Signature Scheme v2 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.
- main.putValue(
- ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME,
- ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE);
- }
-
- MessageDigest md = MessageDigest.getInstance(
- hash == USE_SHA256 ? "SHA256" : "SHA1");
- PrintStream print = new PrintStream(
- new DigestOutputStream(new ByteArrayOutputStream(), md),
- true, "UTF-8");
-
- // Digest of the entire manifest
- manifest.write(print);
- print.flush();
- main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
- new String(Base64.encode(md.digest()), "ASCII"));
-
- Map<String, Attributes> entries = manifest.getEntries();
- for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
- // Digest of the manifest stanza for this entry.
- print.print("Name: " + entry.getKey() + "\r\n");
- for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
- print.print(att.getKey() + ": " + att.getValue() + "\r\n");
- }
- print.print("\r\n");
- print.flush();
-
- Attributes sfAttr = new Attributes();
- sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest",
- new String(Base64.encode(md.digest()), "ASCII"));
- sf.getEntries().put(entry.getKey(), sfAttr);
- }
-
- CountOutputStream cout = new CountOutputStream(out);
- sf.write(cout);
-
- // 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 ((cout.size() % 1024) == 0) {
- cout.write('\r');
- cout.write('\n');
- }
- }
-
/** Sign data and write the digital signature to 'out'. */
private static void writeSignatureBlock(
CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash,
@@ -534,7 +318,7 @@
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
ContentSigner signer =
new JcaContentSignerBuilder(
- getJcaSignatureAlgorithmForV1SigningOrOta(publicKey, hash))
+ getJcaSignatureAlgorithmForOta(publicKey, hash))
.build(privateKey);
gen.addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder(
@@ -552,12 +336,39 @@
}
/**
+ * Adds ZIP entries which represent the v1 signature (JAR signature scheme).
+ */
+ private static void addV1Signature(
+ ApkSignerEngine apkSigner,
+ ApkSignerEngine.OutputJarSignatureRequest v1Signature,
+ JarOutputStream out,
+ long timestamp) throws IOException {
+ for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry
+ : v1Signature.getAdditionalJarEntries()) {
+ String entryName = entry.getName();
+ JarEntry outEntry = new JarEntry(entryName);
+ outEntry.setTime(timestamp);
+ out.putNextEntry(outEntry);
+ byte[] entryData = entry.getData();
+ out.write(entryData);
+ ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+ apkSigner.outputJarEntry(entryName);
+ if (inspectEntryRequest != null) {
+ inspectEntryRequest.getDataSink().consume(entryData, 0, entryData.length);
+ inspectEntryRequest.done();
+ }
+ }
+ }
+
+ /**
* Copy all JAR entries from input to output. We set the modification times in the output to a
* fixed time, so as to reduce variation in the output file and make incremental OTAs more
* efficient.
*/
- private static void copyFiles(JarFile in,
+ private static void copyFiles(
+ JarFile in,
Pattern ignoredFilenamePattern,
+ ApkSignerEngine apkSigner,
JarOutputStream out,
long timestamp,
int defaultAlignment) throws IOException {
@@ -589,12 +400,21 @@
// the start of the file and makes it easier to do alignment
// on them (since only stored entries are aligned).
+ List<String> remainingNames = new ArrayList<>(names.size());
for (String name : names) {
JarEntry inEntry = in.getJarEntry(name);
- JarEntry outEntry = null;
- if (inEntry.getMethod() != JarEntry.STORED) continue;
+ if (inEntry.getMethod() != JarEntry.STORED) {
+ // Defer outputting this entry until we're ready to output compressed entries.
+ remainingNames.add(name);
+ continue;
+ }
+
+ if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) {
+ continue;
+ }
+
// Preserve the STORED method of the input entry.
- outEntry = new JarEntry(inEntry);
+ JarEntry outEntry = new JarEntry(inEntry);
outEntry.setTime(timestamp);
// Discard comment and extra fields of this entry to
// simplify alignment logic below and for consistency with
@@ -638,33 +458,97 @@
offset += extra.length;
out.putNextEntry(outEntry);
+ ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+ (apkSigner != null) ? apkSigner.outputJarEntry(name) : null;
+ DataSink entryDataSink =
+ (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null;
- InputStream data = in.getInputStream(inEntry);
- while ((num = data.read(buffer)) > 0) {
- out.write(buffer, 0, num);
- offset += num;
+ try (InputStream data = in.getInputStream(inEntry)) {
+ while ((num = data.read(buffer)) > 0) {
+ out.write(buffer, 0, num);
+ if (entryDataSink != null) {
+ entryDataSink.consume(buffer, 0, num);
+ }
+ offset += num;
+ }
}
out.flush();
+ if (inspectEntryRequest != null) {
+ inspectEntryRequest.done();
+ }
}
// Copy all the non-STORED entries. We don't attempt to
// maintain the 'offset' variable past this point; we don't do
// alignment on these entries.
- for (String name : names) {
+ for (String name : remainingNames) {
JarEntry inEntry = in.getJarEntry(name);
- JarEntry outEntry = null;
- if (inEntry.getMethod() == JarEntry.STORED) continue;
+ if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) {
+ continue;
+ }
+
// Create a new entry so that the compressed len is recomputed.
- outEntry = new JarEntry(name);
+ JarEntry outEntry = new JarEntry(name);
outEntry.setTime(timestamp);
out.putNextEntry(outEntry);
+ ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+ (apkSigner != null) ? apkSigner.outputJarEntry(name) : null;
+ DataSink entryDataSink =
+ (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null;
InputStream data = in.getInputStream(inEntry);
while ((num = data.read(buffer)) > 0) {
out.write(buffer, 0, num);
+ if (entryDataSink != null) {
+ entryDataSink.consume(buffer, 0, num);
+ }
}
out.flush();
+ if (inspectEntryRequest != null) {
+ inspectEntryRequest.done();
+ }
+ }
+ }
+
+ private static boolean shouldOutputApkEntry(
+ ApkSignerEngine apkSigner, JarFile inFile, JarEntry inEntry, byte[] tmpbuf)
+ throws IOException {
+ if (apkSigner == null) {
+ return true;
+ }
+
+ ApkSignerEngine.InputJarEntryInstructions instructions =
+ apkSigner.inputJarEntry(inEntry.getName());
+ ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+ instructions.getInspectJarEntryRequest();
+ if (inspectEntryRequest != null) {
+ provideJarEntry(inFile, inEntry, inspectEntryRequest, tmpbuf);
+ }
+ switch (instructions.getOutputPolicy()) {
+ case OUTPUT:
+ return true;
+ case SKIP:
+ case OUTPUT_BY_ENGINE:
+ return false;
+ default:
+ throw new RuntimeException(
+ "Unsupported output policy: " + instructions.getOutputPolicy());
+ }
+ }
+
+ private static void provideJarEntry(
+ JarFile jarFile,
+ JarEntry jarEntry,
+ ApkSignerEngine.InspectJarEntryRequest request,
+ byte[] tmpbuf) throws IOException {
+ DataSink dataSink = request.getDataSink();
+ try (InputStream in = jarFile.getInputStream(jarEntry)) {
+ int chunkSize;
+ while ((chunkSize = in.read(tmpbuf)) > 0) {
+ dataSink.consume(tmpbuf, 0, chunkSize);
+ }
+ request.done();
}
}
@@ -756,6 +640,11 @@
private final ASN1ObjectIdentifier type;
private WholeFileSignerOutputStream signer;
+ // Files matching this pattern are not copied to the output.
+ private static final Pattern STRIP_PATTERN =
+ Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|("
+ + Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
+
public CMSSigner(JarFile inputJar, File publicKeyFile,
X509Certificate publicKey, PrivateKey privateKey, int hash,
long timestamp, OutputStream outputStream) {
@@ -789,7 +678,7 @@
signer = new WholeFileSignerOutputStream(out, outputStream);
JarOutputStream outputJar = new JarOutputStream(signer);
- copyFiles(inputJar, STRIP_PATTERN, outputJar, timestamp, 0);
+ copyFiles(inputJar, STRIP_PATTERN, null, outputJar, timestamp, 0);
addOtacert(outputJar, publicKeyFile, timestamp);
signer.notifyClosing();
@@ -883,47 +772,6 @@
temp.writeTo(outputStream);
}
- private static void signFile(Manifest manifest,
- X509Certificate[] publicKey, PrivateKey[] privateKey, int[] hash,
- long timestamp,
- boolean additionallySignedUsingAnApkSignatureScheme,
- JarOutputStream outputJar)
- throws Exception {
-
- // MANIFEST.MF
- JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
- je.setTime(timestamp);
- outputJar.putNextEntry(je);
- manifest.write(outputJar);
-
- int numKeys = publicKey.length;
- for (int k = 0; k < numKeys; ++k) {
- // CERT.SF / CERT#.SF
- je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
- (String.format(CERT_SF_MULTI_NAME, k)));
- je.setTime(timestamp);
- outputJar.putNextEntry(je);
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- writeSignatureFile(
- manifest,
- baos,
- hash[k],
- additionallySignedUsingAnApkSignatureScheme);
- byte[] signedData = baos.toByteArray();
- outputJar.write(signedData);
-
- // CERT.{EC,RSA} / CERT#.{EC,RSA}
- final String keyType = publicKey[k].getPublicKey().getAlgorithm();
- je = new JarEntry(numKeys == 1 ?
- (String.format(CERT_SIG_NAME, keyType)) :
- (String.format(CERT_SIG_MULTI_NAME, k, keyType)));
- je.setTime(timestamp);
- outputJar.putNextEntry(je);
- writeSignatureBlock(new CMSProcessableByteArray(signedData),
- publicKey[k], privateKey[k], hash[k], outputJar);
- }
- }
-
/**
* Tries to load a JSE Provider by class name. This is for custom PrivateKey
* types that might be stored in PKCS#11-like storage.
@@ -976,81 +824,68 @@
Security.insertProviderAt((Provider) o, 1);
}
- /**
- * Converts the provided lists of private keys, their X.509 certificates, and digest algorithms
- * into a list of APK Signature Scheme v2 {@code SignerConfig} instances.
- */
- public static List<ApkSignerV2.SignerConfig> createV2SignerConfigs(
- PrivateKey[] privateKeys, X509Certificate[] certificates, String[] digestAlgorithms)
- throws InvalidKeyException {
+ private static List<DefaultApkSignerEngine.SignerConfig> createSignerConfigs(
+ PrivateKey[] privateKeys, X509Certificate[] certificates) {
if (privateKeys.length != certificates.length) {
throw new IllegalArgumentException(
"The number of private keys must match the number of certificates: "
+ privateKeys.length + " vs" + certificates.length);
}
- List<ApkSignerV2.SignerConfig> result = new ArrayList<>(privateKeys.length);
+ List<DefaultApkSignerEngine.SignerConfig> signerConfigs = new ArrayList<>();
+ String signerNameFormat = (privateKeys.length == 1) ? "CERT" : "CERT%s";
for (int i = 0; i < privateKeys.length; i++) {
- PrivateKey privateKey = privateKeys[i];
- X509Certificate certificate = certificates[i];
- PublicKey publicKey = certificate.getPublicKey();
- String keyAlgorithm = privateKey.getAlgorithm();
- if (!keyAlgorithm.equalsIgnoreCase(publicKey.getAlgorithm())) {
- throw new InvalidKeyException(
- "Key algorithm of private key #" + (i + 1) + " does not match key"
- + " algorithm of public key #" + (i + 1) + ": " + keyAlgorithm
- + " vs " + publicKey.getAlgorithm());
- }
- ApkSignerV2.SignerConfig signerConfig = new ApkSignerV2.SignerConfig();
- signerConfig.privateKey = privateKey;
- signerConfig.certificates = Collections.singletonList(certificate);
- List<Integer> signatureAlgorithms = new ArrayList<>(digestAlgorithms.length);
- for (String digestAlgorithm : digestAlgorithms) {
- try {
- signatureAlgorithms.add(
- getV2SignatureAlgorithm(keyAlgorithm, digestAlgorithm));
- } catch (IllegalArgumentException e) {
- throw new InvalidKeyException(
- "Unsupported key and digest algorithm combination for signer #"
- + (i + 1),
- e);
- }
- }
- signerConfig.signatureAlgorithms = signatureAlgorithms;
- result.add(signerConfig);
+ String signerName = String.format(Locale.US, signerNameFormat, (i + 1));
+ DefaultApkSignerEngine.SignerConfig signerConfig =
+ new DefaultApkSignerEngine.SignerConfig.Builder(
+ signerName,
+ privateKeys[i],
+ Collections.singletonList(certificates[i]))
+ .build();
+ signerConfigs.add(signerConfig);
}
- return result;
+ return signerConfigs;
}
- private static int getV2SignatureAlgorithm(String keyAlgorithm, String digestAlgorithm) {
- if ("SHA-256".equalsIgnoreCase(digestAlgorithm)) {
- if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
- // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
- // deterministic signatures which make life easier for OTA updates (fewer files
- // changed when deterministic signature schemes are used).
- return ApkSignerV2.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256;
- } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
- return ApkSignerV2.SIGNATURE_ECDSA_WITH_SHA256;
- } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
- return ApkSignerV2.SIGNATURE_DSA_WITH_SHA256;
- } else {
- throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
- }
- } else if ("SHA-512".equalsIgnoreCase(digestAlgorithm)) {
- if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
- // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
- // deterministic signatures which make life easier for OTA updates (fewer files
- // changed when deterministic signature schemes are used).
- return ApkSignerV2.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512;
- } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
- return ApkSignerV2.SIGNATURE_ECDSA_WITH_SHA512;
- } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
- throw new IllegalArgumentException("SHA-512 is not supported with DSA");
- } else {
- throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
- }
- } else {
- throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm);
+ private static class ZipSections {
+ ByteBuffer beforeCentralDir;
+ ByteBuffer centralDir;
+ ByteBuffer eocd;
+ }
+
+ private static ZipSections findMainZipSections(ByteBuffer apk)
+ throws IOException, ZipFormatException {
+ apk.slice();
+ ApkUtils.ZipSections sections = ApkUtils.findZipSections(DataSources.asDataSource(apk));
+ long centralDirStartOffset = sections.getZipCentralDirectoryOffset();
+ long centralDirSizeBytes = sections.getZipCentralDirectorySizeBytes();
+ long centralDirEndOffset = centralDirStartOffset + centralDirSizeBytes;
+ long eocdStartOffset = sections.getZipEndOfCentralDirectoryOffset();
+ if (centralDirEndOffset != eocdStartOffset) {
+ throw new ZipFormatException(
+ "ZIP Central Directory is not immediately followed by End of Central Directory"
+ + ". CD end: " + centralDirEndOffset
+ + ", EoCD start: " + eocdStartOffset);
}
+ apk.position(0);
+ apk.limit((int) centralDirStartOffset);
+ ByteBuffer beforeCentralDir = apk.slice();
+
+ apk.position((int) centralDirStartOffset);
+ apk.limit((int) centralDirEndOffset);
+ ByteBuffer centralDir = apk.slice();
+
+ apk.position((int) eocdStartOffset);
+ apk.limit(apk.capacity());
+ ByteBuffer eocd = apk.slice();
+
+ apk.position(0);
+ apk.limit(apk.capacity());
+
+ ZipSections result = new ZipSections();
+ result.beforeCentralDir = beforeCentralDir;
+ result.centralDir = centralDir;
+ result.eocd = eocd;
+ return result;
}
private static void usage() {
@@ -1167,57 +1002,80 @@
timestamp,
outputFile);
} else {
- // Generate, in memory, an APK signed using standard JAR Signature Scheme.
- ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
- JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf);
- // Use maximum compression for compressed entries because the APK lives forever on
- // the system partition.
- outputJar.setLevel(9);
- int v1DigestAlgorithmBitSet = 0;
- int[] v1DigestAlgorithm = new int[numKeys];
- for (int i = 0; i < numKeys; ++i) {
- v1DigestAlgorithm[i] = getV1DigestAlgorithmForApk(publicKey[i], minSdkVersion);
- v1DigestAlgorithmBitSet |= v1DigestAlgorithm[i];
- }
- Manifest manifest =
- addDigestsToManifest(inputJar, STRIP_PATTERN, v1DigestAlgorithmBitSet);
- copyFiles(inputJar, STRIP_PATTERN, outputJar, timestamp, alignment);
- signFile(
- manifest,
- publicKey, privateKey, v1DigestAlgorithm,
- timestamp, signUsingApkSignatureSchemeV2,
- outputJar);
- outputJar.close();
- ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
- v1SignedApkBuf.reset();
+ try (ApkSignerEngine apkSigner =
+ new DefaultApkSignerEngine.Builder(
+ createSignerConfigs(privateKey, publicKey), minSdkVersion)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(signUsingApkSignatureSchemeV2)
+ .setOtherSignersSignaturesPreserved(false)
+ .build()) {
+ // We don't preserve the input APK's APK Signing Block (which contains v2
+ // signatures)
+ apkSigner.inputApkSigningBlock(null);
- ByteBuffer[] outputChunks;
- if (signUsingApkSignatureSchemeV2) {
- // Additionally sign the APK using the APK Signature Scheme v2.
- ByteBuffer apkContents = v1SignedApk;
- List<ApkSignerV2.SignerConfig> signerConfigs =
- createV2SignerConfigs(
- privateKey,
- publicKey,
- new String[] {APK_SIG_SCHEME_V2_DIGEST_ALGORITHM});
- outputChunks = ApkSignerV2.sign(apkContents, signerConfigs);
- } else {
- // Output the JAR-signed APK as is.
- outputChunks = new ByteBuffer[] {v1SignedApk};
+ // Build the output APK in memory, by copying input APK's ZIP entries across
+ // and then signing the output APK.
+ ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
+ JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf);
+ // Use maximum compression for compressed entries because the APK lives forever
+ // on the system partition.
+ outputJar.setLevel(9);
+ copyFiles(inputJar, null, apkSigner, outputJar, timestamp, alignment);
+ ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest =
+ apkSigner.outputJarEntries();
+ if (addV1SignatureRequest != null) {
+ addV1Signature(apkSigner, addV1SignatureRequest, outputJar, timestamp);
+ addV1SignatureRequest.done();
+ }
+ outputJar.close();
+ ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
+ v1SignedApkBuf.reset();
+ ByteBuffer[] outputChunks = new ByteBuffer[] {v1SignedApk};
+
+ ZipSections zipSections = findMainZipSections(v1SignedApk);
+ ApkSignerEngine.OutputApkSigningBlockRequest addV2SignatureRequest =
+ apkSigner.outputZipSections(
+ DataSources.asDataSource(zipSections.beforeCentralDir),
+ DataSources.asDataSource(zipSections.centralDir),
+ DataSources.asDataSource(zipSections.eocd));
+ if (addV2SignatureRequest != null) {
+ // Need to insert the returned APK Signing Block before ZIP Central
+ // Directory.
+ byte[] apkSigningBlock = addV2SignatureRequest.getApkSigningBlock();
+ // Because the APK Signing Block is inserted before the Central Directory,
+ // we need to adjust accordingly the offset of Central Directory inside the
+ // ZIP End of Central Directory (EoCD) record.
+ ByteBuffer modifiedEocd = ByteBuffer.allocate(zipSections.eocd.remaining());
+ modifiedEocd.put(zipSections.eocd);
+ modifiedEocd.flip();
+ modifiedEocd.order(ByteOrder.LITTLE_ENDIAN);
+ ApkUtils.setZipEocdCentralDirectoryOffset(
+ modifiedEocd,
+ zipSections.beforeCentralDir.remaining() + apkSigningBlock.length);
+ outputChunks =
+ new ByteBuffer[] {
+ zipSections.beforeCentralDir,
+ ByteBuffer.wrap(apkSigningBlock),
+ zipSections.centralDir,
+ modifiedEocd};
+ addV2SignatureRequest.done();
+ }
+
+ // This assumes outputChunks are array-backed. To avoid this assumption, the
+ // code could be rewritten to use FileChannel.
+ for (ByteBuffer outputChunk : outputChunks) {
+ outputFile.write(
+ outputChunk.array(),
+ outputChunk.arrayOffset() + outputChunk.position(),
+ outputChunk.remaining());
+ outputChunk.position(outputChunk.limit());
+ }
+
+ outputFile.close();
+ outputFile = null;
+ apkSigner.outputDone();
}
- // This assumes outputChunks are array-backed. To avoid this assumption, the
- // code could be rewritten to use FileChannel.
- for (ByteBuffer outputChunk : outputChunks) {
- outputFile.write(
- outputChunk.array(),
- outputChunk.arrayOffset() + outputChunk.position(),
- outputChunk.remaining());
- outputChunk.position(outputChunk.limit());
- }
-
- outputFile.close();
- outputFile = null;
return;
}
} catch (Exception e) {
diff --git a/tools/signapk/src/com/android/signapk/ZipUtils.java b/tools/signapk/src/com/android/signapk/ZipUtils.java
deleted file mode 100644
index 7575a77..0000000
--- a/tools/signapk/src/com/android/signapk/ZipUtils.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * 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.signapk;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-
-/**
- * Assorted ZIP format helpers.
- *
- * <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte
- * order of these buffers is little-endian.
- */
-public abstract class ZipUtils {
- private ZipUtils() {}
-
- private static final int ZIP_EOCD_REC_MIN_SIZE = 22;
- private static final int ZIP_EOCD_REC_SIG = 0x06054b50;
- private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12;
- private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
- private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20;
-
- private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;
- private static final int ZIP64_EOCD_LOCATOR_SIG = 0x07064b50;
-
- private static final int UINT16_MAX_VALUE = 0xffff;
-
- /**
- * Returns the position at which ZIP End of Central Directory record starts in the provided
- * buffer or {@code -1} if the record is not present.
- *
- * <p>NOTE: Byte order of {@code zipContents} must be little-endian.
- */
- public static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
- assertByteOrderLittleEndian(zipContents);
-
- // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
- // The record can be identified by its 4-byte signature/magic which is located at the very
- // beginning of the record. A complication is that the record is variable-length because of
- // the comment field.
- // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
- // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
- // the candidate record's comment length is such that the remainder of the record takes up
- // exactly the remaining bytes in the buffer. The search is bounded because the maximum
- // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
-
- int archiveSize = zipContents.capacity();
- if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) {
- return -1;
- }
- int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE);
- int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
- for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength;
- expectedCommentLength++) {
- int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength;
- if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) {
- int actualCommentLength =
- getUnsignedInt16(
- zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET);
- if (actualCommentLength == expectedCommentLength) {
- return eocdStartPos;
- }
- }
- }
-
- return -1;
- }
-
- /**
- * Returns {@code true} if the provided buffer contains a ZIP64 End of Central Directory
- * Locator.
- *
- * <p>NOTE: Byte order of {@code zipContents} must be little-endian.
- */
- public static final boolean isZip64EndOfCentralDirectoryLocatorPresent(
- ByteBuffer zipContents, int zipEndOfCentralDirectoryPosition) {
- assertByteOrderLittleEndian(zipContents);
-
- // ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central
- // Directory Record.
-
- int locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE;
- if (locatorPosition < 0) {
- return false;
- }
-
- return zipContents.getInt(locatorPosition) == ZIP64_EOCD_LOCATOR_SIG;
- }
-
- /**
- * Returns the offset of the start of the ZIP Central Directory in the archive.
- *
- * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
- */
- public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) {
- assertByteOrderLittleEndian(zipEndOfCentralDirectory);
- return getUnsignedInt32(
- zipEndOfCentralDirectory,
- zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET);
- }
-
- /**
- * Sets the offset of the start of the ZIP Central Directory in the archive.
- *
- * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
- */
- public static void setZipEocdCentralDirectoryOffset(
- ByteBuffer zipEndOfCentralDirectory, long offset) {
- assertByteOrderLittleEndian(zipEndOfCentralDirectory);
- setUnsignedInt32(
- zipEndOfCentralDirectory,
- zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET,
- offset);
- }
-
- /**
- * Returns the size (in bytes) of the ZIP Central Directory.
- *
- * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
- */
- public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) {
- assertByteOrderLittleEndian(zipEndOfCentralDirectory);
- return getUnsignedInt32(
- zipEndOfCentralDirectory,
- zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET);
- }
-
- private static void assertByteOrderLittleEndian(ByteBuffer buffer) {
- if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
- throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
- }
- }
-
- private static int getUnsignedInt16(ByteBuffer buffer, int offset) {
- return buffer.getShort(offset) & 0xffff;
- }
-
- private static long getUnsignedInt32(ByteBuffer buffer, int offset) {
- return buffer.getInt(offset) & 0xffffffffL;
- }
-
- private static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) {
- if ((value < 0) || (value > 0xffffffffL)) {
- throw new IllegalArgumentException("uint32 value of out range: " + value);
- }
- buffer.putInt(buffer.position() + offset, (int) value);
- }
-}