diff --git a/tools/apksigner/core/src/com/android/apksigner/core/ApkSigner.java b/tools/apksigner/core/src/com/android/apksigner/core/ApkSigner.java new file mode 100644 index 000000000..2491302ce --- /dev/null +++ b/tools/apksigner/core/src/com/android/apksigner/core/ApkSigner.java @@ -0,0 +1,711 @@ +/* + * 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; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.android.apksigner.core.apk.ApkUtils; +import com.android.apksigner.core.internal.apk.v2.V2SchemeVerifier; +import com.android.apksigner.core.internal.util.ByteBufferDataSource; +import com.android.apksigner.core.internal.util.Pair; +import com.android.apksigner.core.internal.zip.CentralDirectoryRecord; +import com.android.apksigner.core.internal.zip.EocdRecord; +import com.android.apksigner.core.internal.zip.LocalFileRecord; +import com.android.apksigner.core.internal.zip.ZipUtils; +import com.android.apksigner.core.util.DataSink; +import com.android.apksigner.core.util.DataSinks; +import com.android.apksigner.core.util.DataSource; +import com.android.apksigner.core.util.DataSources; +import com.android.apksigner.core.zip.ZipFormatException; + +/** + * APK signer. + * + *
The signer preserves as much of the input APK as possible. For example, it preserves the + * order of APK entries and preserves their contents, including compressed form and alignment of + * data. + * + *
Use {@link Builder} to obtain instances of this signer.
+ */
+public class ApkSigner {
+
+ /**
+ * Extensible data block/field header ID used for storing information about alignment of
+ * uncompressed entries as well as for aligning the entries's data. See ZIP appnote.txt section
+ * 4.5 Extensible data fields.
+ */
+ private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = (short) 0xd935;
+
+ /**
+ * Minimum size (in bytes) of the extensible data block/field used for alignment of uncompressed
+ * entries.
+ */
+ private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES = 6;
+
+ private final ApkSignerEngine mSignerEngine;
+
+ private final File mInputApkFile;
+ private final DataSource mInputApkDataSource;
+
+ private final File mOutputApkFile;
+ private final DataSink mOutputApkDataSink;
+ private final DataSource mOutputApkDataSource;
+
+ private ApkSigner(
+ ApkSignerEngine signerEngine,
+ File inputApkFile,
+ DataSource inputApkDataSource,
+ File outputApkFile,
+ DataSink outputApkDataSink,
+ DataSource outputApkDataSource) {
+ mSignerEngine = signerEngine;
+
+ mInputApkFile = inputApkFile;
+ mInputApkDataSource = inputApkDataSource;
+
+ mOutputApkFile = outputApkFile;
+ mOutputApkDataSink = outputApkDataSink;
+ mOutputApkDataSource = outputApkDataSource;
+ }
+
+ /**
+ * Signs the input APK and outputs the resulting signed APK. The input APK is not modified.
+ *
+ * @throws IOException if an I/O error is encountered while reading or writing the APKs
+ * @throws ZipFormatException if the input APK is malformed at ZIP format level
+ * @throws NoSuchAlgorithmException if the APK signatures cannot be produced or verified because
+ * a required cryptographic algorithm implementation is missing
+ * @throws InvalidKeyException if a signature could not be generated because a signing key is
+ * not suitable for generating the signature
+ * @throws SignatureException if an error occurred while generating or verifying a signature
+ * @throws IllegalStateException if this signer's configuration is missing required information
+ * or if the signing engine is in an invalid state.
+ */
+ public void sign()
+ throws IOException, ZipFormatException, NoSuchAlgorithmException, InvalidKeyException,
+ SignatureException, IllegalStateException {
+ Closeable in = null;
+ DataSource inputApk;
+ try {
+ if (mInputApkDataSource != null) {
+ inputApk = mInputApkDataSource;
+ } else if (mInputApkFile != null) {
+ RandomAccessFile inputFile = new RandomAccessFile(mInputApkFile, "r");
+ in = inputFile;
+ inputApk = DataSources.asDataSource(inputFile);
+ } else {
+ throw new IllegalStateException("Input APK not specified");
+ }
+
+ Closeable out = null;
+ try {
+ DataSink outputApkOut;
+ DataSource outputApkIn;
+ if (mOutputApkDataSink != null) {
+ outputApkOut = mOutputApkDataSink;
+ outputApkIn = mOutputApkDataSource;
+ } else if (mOutputApkFile != null) {
+ RandomAccessFile outputFile = new RandomAccessFile(mOutputApkFile, "rw");
+ out = outputFile;
+ outputFile.setLength(0);
+ outputApkOut = DataSinks.asDataSink(outputFile);
+ outputApkIn = DataSources.asDataSource(outputFile);
+ } else {
+ throw new IllegalStateException("Output APK not specified");
+ }
+
+ sign(mSignerEngine, inputApk, outputApkOut, outputApkIn);
+ } finally {
+ if (out != null) {
+ out.close();
+ }
+ }
+ } finally {
+ if (in != null) {
+ in.close();
+ }
+ }
+ }
+
+ private static void sign(
+ ApkSignerEngine signerEngine,
+ DataSource inputApk,
+ DataSink outputApkOut,
+ DataSource outputApkIn)
+ throws IOException, ZipFormatException, NoSuchAlgorithmException,
+ InvalidKeyException, SignatureException {
+ // Step 1. Find input APK's main ZIP sections
+ ApkUtils.ZipSections inputZipSections = ApkUtils.findZipSections(inputApk);
+ long apkSigningBlockOffset = -1;
+ try {
+ Pair The following information is required to construct a working {@code ApkSigner}:
+ * The engine is stateful and thus cannot be used for signing multiple APKs. However, once
* the engine signed an APK, the engine can be used to re-sign the APK after it has been modified.
@@ -119,9 +119,10 @@ public interface ApkSignerEngine extends Closeable {
* @param apkSigningBlock APK signing block of the input APK. The provided data source is
* guaranteed to not be used by the engine after this method terminates.
*
+ * @throws IOException if an I/O error occurs while reading the APK Signing Block
* @throws IllegalStateException if this engine is closed
*/
- void inputApkSigningBlock(DataSource apkSigningBlock) throws IllegalStateException;
+ void inputApkSigningBlock(DataSource apkSigningBlock) throws IOException, IllegalStateException;
/**
* Indicates to this engine that the specified JAR entry was encountered in the input APK.
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeVerifier.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeVerifier.java
index 1bba31323..752ba7e02 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeVerifier.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeVerifier.java
@@ -47,7 +47,7 @@ import com.android.apksigner.core.internal.util.AndroidSdkVersion;
import com.android.apksigner.core.internal.util.InclusiveIntRange;
import com.android.apksigner.core.internal.util.MessageDigestSink;
import com.android.apksigner.core.internal.zip.CentralDirectoryRecord;
-import com.android.apksigner.core.internal.zip.LocalFileHeader;
+import com.android.apksigner.core.internal.zip.LocalFileRecord;
import com.android.apksigner.core.util.DataSource;
import com.android.apksigner.core.zip.ZipFormatException;
@@ -187,10 +187,7 @@ public abstract class V1SchemeVerifier {
// Parse the JAR manifest and check that all JAR entries it references exist in the APK.
byte[] manifestBytes =
- LocalFileHeader.getUncompressedData(
- apk, 0,
- manifestEntry,
- cdStartOffset);
+ LocalFileRecord.getUncompressedData(apk, manifestEntry, cdStartOffset);
Map The record consists of the Local File Header, file data, and (if present) Data Descriptor.
+ */
+public class LocalFileRecord {
+ private static final int RECORD_SIGNATURE = 0x04034b50;
+ private static final int HEADER_SIZE_BYTES = 30;
+
+ private static final int GP_FLAGS_OFFSET = 6;
+ private static final int COMPRESSION_METHOD_OFFSET = 8;
+ private static final int CRC32_OFFSET = 14;
+ private static final int COMPRESSED_SIZE_OFFSET = 18;
+ private static final int UNCOMPRESSED_SIZE_OFFSET = 22;
+ private static final int NAME_LENGTH_OFFSET = 26;
+ private static final int EXTRA_LENGTH_OFFSET = 28;
+ private static final int NAME_OFFSET = HEADER_SIZE_BYTES;
+
+ private static final int DATA_DESCRIPTOR_SIZE_BYTES_WITHOUT_SIGNATURE = 12;
+ private static final int DATA_DESCRIPTOR_SIGNATURE = 0x08074b50;
+
+ private final String mName;
+ private final int mNameSizeBytes;
+ private final ByteBuffer mExtra;
+
+ private final long mStartOffsetInArchive;
+ private final long mSize;
+
+ private final int mDataStartOffset;
+ private final long mDataSize;
+ private final boolean mDataCompressed;
+ private final long mUncompressedDataSize;
+
+ private LocalFileRecord(
+ String name,
+ int nameSizeBytes,
+ ByteBuffer extra,
+ long startOffsetInArchive,
+ long size,
+ int dataStartOffset,
+ long dataSize,
+ boolean dataCompressed,
+ long uncompressedDataSize) {
+ mName = name;
+ mNameSizeBytes = nameSizeBytes;
+ mExtra = extra;
+ mStartOffsetInArchive = startOffsetInArchive;
+ mSize = size;
+ mDataStartOffset = dataStartOffset;
+ mDataSize = dataSize;
+ mDataCompressed = dataCompressed;
+ mUncompressedDataSize = uncompressedDataSize;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public ByteBuffer getExtra() {
+ return (mExtra.capacity() > 0) ? mExtra.slice() : mExtra;
+ }
+
+ public int getExtraFieldStartOffsetInsideRecord() {
+ return HEADER_SIZE_BYTES + mNameSizeBytes;
+ }
+
+ public long getStartOffsetInArchive() {
+ return mStartOffsetInArchive;
+ }
+
+ public int getDataStartOffsetInRecord() {
+ return mDataStartOffset;
+ }
+
+ /**
+ * Returns the size (in bytes) of this record.
+ */
+ public long getSize() {
+ return mSize;
+ }
+
+ /**
+ * Returns {@code true} if this record's file data is stored in compressed form.
+ */
+ public boolean isDataCompressed() {
+ return mDataCompressed;
+ }
+
+ /**
+ * Returns the Local File record starting at the current position of the provided buffer
+ * and advances the buffer's position immediately past the end of the record. The record
+ * consists of the Local File Header, data, and (if present) Data Descriptor.
+ */
+ public static LocalFileRecord getRecord(
+ DataSource apk,
+ CentralDirectoryRecord cdRecord,
+ long cdStartOffset) throws ZipFormatException, IOException {
+ return getRecord(
+ apk,
+ cdRecord,
+ cdStartOffset,
+ true, // obtain extra field contents
+ true // include Data Descriptor (if present)
+ );
+ }
+
+ /**
+ * Returns the Local File record starting at the current position of the provided buffer
+ * and advances the buffer's position immediately past the end of the record. The record
+ * consists of the Local File Header, data, and (if present) Data Descriptor.
+ */
+ private static LocalFileRecord getRecord(
+ DataSource apk,
+ CentralDirectoryRecord cdRecord,
+ long cdStartOffset,
+ boolean extraFieldContentsNeeded,
+ boolean dataDescriptorIncluded) throws ZipFormatException, IOException {
+ // IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform
+ // exhibited when reading an APK for the purposes of verifying its signatures.
+
+ String entryName = cdRecord.getName();
+ int cdRecordEntryNameSizeBytes = cdRecord.getNameSizeBytes();
+ int headerSizeWithName = HEADER_SIZE_BYTES + cdRecordEntryNameSizeBytes;
+ long headerStartOffset = cdRecord.getLocalFileHeaderOffset();
+ long headerEndOffset = headerStartOffset + headerSizeWithName;
+ if (headerEndOffset >= cdStartOffset) {
+ throw new ZipFormatException(
+ "Local File Header of " + entryName + " extends beyond start of Central"
+ + " Directory. LFH end: " + headerEndOffset
+ + ", CD start: " + cdStartOffset);
+ }
+ ByteBuffer header;
+ try {
+ header = apk.getByteBuffer(headerStartOffset, headerSizeWithName);
+ } catch (IOException e) {
+ throw new IOException("Failed to read Local File Header of " + entryName, e);
+ }
+ header.order(ByteOrder.LITTLE_ENDIAN);
+
+ int recordSignature = header.getInt();
+ if (recordSignature != RECORD_SIGNATURE) {
+ throw new ZipFormatException(
+ "Not a Local File Header record for entry " + entryName + ". Signature: 0x"
+ + Long.toHexString(recordSignature & 0xffffffffL));
+ }
+ short gpFlags = header.getShort(GP_FLAGS_OFFSET);
+ boolean dataDescriptorUsed = (gpFlags & ZipUtils.GP_FLAG_DATA_DESCRIPTOR_USED) != 0;
+ long uncompressedDataCrc32FromCdRecord = cdRecord.getCrc32();
+ long compressedDataSizeFromCdRecord = cdRecord.getCompressedSize();
+ long uncompressedDataSizeFromCdRecord = cdRecord.getUncompressedSize();
+ if (!dataDescriptorUsed) {
+ long crc32 = ZipUtils.getUnsignedInt32(header, CRC32_OFFSET);
+ if (crc32 != uncompressedDataCrc32FromCdRecord) {
+ throw new ZipFormatException(
+ "CRC-32 mismatch between Local File Header and Central Directory for entry "
+ + entryName + ". LFH: " + crc32
+ + ", CD: " + uncompressedDataCrc32FromCdRecord);
+ }
+ long compressedSize = ZipUtils.getUnsignedInt32(header, COMPRESSED_SIZE_OFFSET);
+ if (compressedSize != compressedDataSizeFromCdRecord) {
+ throw new ZipFormatException(
+ "Compressed size mismatch between Local File Header and Central Directory"
+ + " for entry " + entryName + ". LFH: " + compressedSize
+ + ", CD: " + compressedDataSizeFromCdRecord);
+ }
+ long uncompressedSize = ZipUtils.getUnsignedInt32(header, UNCOMPRESSED_SIZE_OFFSET);
+ if (uncompressedSize != uncompressedDataSizeFromCdRecord) {
+ throw new ZipFormatException(
+ "Uncompressed size mismatch between Local File Header and Central Directory"
+ + " for entry " + entryName + ". LFH: " + uncompressedSize
+ + ", CD: " + uncompressedDataSizeFromCdRecord);
+ }
+ }
+ int nameLength = ZipUtils.getUnsignedInt16(header, NAME_LENGTH_OFFSET);
+ if (nameLength > cdRecordEntryNameSizeBytes) {
+ throw new ZipFormatException(
+ "Name mismatch between Local File Header and Central Directory for entry"
+ + entryName + ". LFH: " + nameLength
+ + " bytes, CD: " + cdRecordEntryNameSizeBytes + " bytes");
+ }
+ String name = CentralDirectoryRecord.getName(header, NAME_OFFSET, nameLength);
+ if (!entryName.equals(name)) {
+ throw new ZipFormatException(
+ "Name mismatch between Local File Header and Central Directory. LFH: \""
+ + name + "\", CD: \"" + entryName + "\"");
+ }
+ int extraLength = ZipUtils.getUnsignedInt16(header, EXTRA_LENGTH_OFFSET);
+
+ short compressionMethod = header.getShort(COMPRESSION_METHOD_OFFSET);
+ boolean compressed;
+ switch (compressionMethod) {
+ case ZipUtils.COMPRESSION_METHOD_STORED:
+ compressed = false;
+ break;
+ case ZipUtils.COMPRESSION_METHOD_DEFLATED:
+ compressed = true;
+ break;
+ default:
+ throw new ZipFormatException(
+ "Unsupported compression method of entry " + entryName
+ + ": " + (compressionMethod & 0xffff));
+ }
+
+ long dataStartOffset = headerStartOffset + HEADER_SIZE_BYTES + nameLength + extraLength;
+ long dataSize;
+ if (compressed) {
+ dataSize = compressedDataSizeFromCdRecord;
+ } else {
+ dataSize = uncompressedDataSizeFromCdRecord;
+ }
+ long dataEndOffset = dataStartOffset + dataSize;
+ if (dataEndOffset > cdStartOffset) {
+ throw new ZipFormatException(
+ "Local File Header data of " + entryName + " overlaps with Central Directory"
+ + ". LFH data start: " + dataStartOffset
+ + ", LFH data end: " + dataEndOffset + ", CD start: " + cdStartOffset);
+ }
+
+ ByteBuffer extra = EMPTY_BYTE_BUFFER;
+ if ((extraFieldContentsNeeded) && (extraLength > 0)) {
+ extra = apk.getByteBuffer(
+ headerStartOffset + HEADER_SIZE_BYTES + nameLength, extraLength);
+ }
+
+ long recordEndOffset = dataEndOffset;
+ // Include the Data Descriptor (if requested and present) into the record.
+ if ((dataDescriptorIncluded) && ((gpFlags & ZipUtils.GP_FLAG_DATA_DESCRIPTOR_USED) != 0)) {
+ // The record's data is supposed to be followed by the Data Descriptor. Unfortunately,
+ // the descriptor's size is not known in advance because the spec lets the signature
+ // field (the first four bytes) be omitted. Thus, there's no 100% reliable way to tell
+ // how long the Data Descriptor record is. Most parsers (including Android) check
+ // whether the first four bytes look like Data Descriptor record signature and, if so,
+ // assume that it is indeed the record's signature. However, this is the wrong
+ // conclusion if the record's CRC-32 (next field after the signature) has the same value
+ // as the signature. In any case, we're doing what Android is doing.
+ long dataDescriptorEndOffset =
+ dataEndOffset + DATA_DESCRIPTOR_SIZE_BYTES_WITHOUT_SIGNATURE;
+ if (dataDescriptorEndOffset > cdStartOffset) {
+ throw new ZipFormatException(
+ "Data Descriptor of " + entryName + " overlaps with Central Directory"
+ + ". Data Descriptor end: " + dataEndOffset
+ + ", CD start: " + cdStartOffset);
+ }
+ ByteBuffer dataDescriptorPotentialSig = apk.getByteBuffer(dataEndOffset, 4);
+ dataDescriptorPotentialSig.order(ByteOrder.LITTLE_ENDIAN);
+ if (dataDescriptorPotentialSig.getInt() == DATA_DESCRIPTOR_SIGNATURE) {
+ dataDescriptorEndOffset += 4;
+ if (dataDescriptorEndOffset > cdStartOffset) {
+ throw new ZipFormatException(
+ "Data Descriptor of " + entryName + " overlaps with Central Directory"
+ + ". Data Descriptor end: " + dataEndOffset
+ + ", CD start: " + cdStartOffset);
+ }
+ }
+ recordEndOffset = dataDescriptorEndOffset;
+ }
+
+ long recordSize = recordEndOffset - headerStartOffset;
+ int dataStartOffsetInRecord = HEADER_SIZE_BYTES + nameLength + extraLength;
+
+ return new LocalFileRecord(
+ entryName,
+ cdRecordEntryNameSizeBytes,
+ extra,
+ headerStartOffset,
+ recordSize,
+ dataStartOffsetInRecord,
+ dataSize,
+ compressed,
+ uncompressedDataSizeFromCdRecord);
+ }
+
+ /**
+ * Outputs this record and returns returns the number of bytes output.
+ */
+ public long outputRecord(DataSource sourceApk, DataSink output) throws IOException {
+ long size = getSize();
+ sourceApk.feed(getStartOffsetInArchive(), size, output);
+ return size;
+ }
+
+ /**
+ * Outputs this record, replacing its extra field with the provided one, and returns returns the
+ * number of bytes output.
+ */
+ public long outputRecordWithModifiedExtra(
+ DataSource sourceApk,
+ ByteBuffer extra,
+ DataSink output) throws IOException {
+ long recordStartOffsetInSource = getStartOffsetInArchive();
+ int extraStartOffsetInRecord = getExtraFieldStartOffsetInsideRecord();
+ int extraSizeBytes = extra.remaining();
+ int headerSize = extraStartOffsetInRecord + extraSizeBytes;
+ ByteBuffer header = ByteBuffer.allocate(headerSize);
+ header.order(ByteOrder.LITTLE_ENDIAN);
+ sourceApk.copyTo(recordStartOffsetInSource, extraStartOffsetInRecord, header);
+ header.put(extra.slice());
+ header.flip();
+ ZipUtils.setUnsignedInt16(header, EXTRA_LENGTH_OFFSET, extraSizeBytes);
+
+ long outputByteCount = header.remaining();
+ output.consume(header);
+ long remainingRecordSize = getSize() - mDataStartOffset;
+ sourceApk.feed(recordStartOffsetInSource + mDataStartOffset, remainingRecordSize, output);
+ outputByteCount += remainingRecordSize;
+ return outputByteCount;
+ }
+
+ /**
+ * Outputs the specified Local File Header record with its data and returns the number of bytes
+ * output.
+ */
+ public static long outputRecordWithDeflateCompressedData(
+ String name,
+ int lastModifiedTime,
+ int lastModifiedDate,
+ byte[] compressedData,
+ long crc32,
+ long uncompressedSize,
+ DataSink output) throws IOException {
+ byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8);
+ int recordSize = HEADER_SIZE_BYTES + nameBytes.length;
+ ByteBuffer result = ByteBuffer.allocate(recordSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.putInt(RECORD_SIGNATURE);
+ ZipUtils.putUnsignedInt16(result, 0x14); // Minimum version needed to extract
+ result.putShort(ZipUtils.GP_FLAG_EFS); // General purpose flag: UTF-8 encoded name
+ result.putShort(ZipUtils.COMPRESSION_METHOD_DEFLATED);
+ ZipUtils.putUnsignedInt16(result, lastModifiedTime);
+ ZipUtils.putUnsignedInt16(result, lastModifiedDate);
+ ZipUtils.putUnsignedInt32(result, crc32);
+ ZipUtils.putUnsignedInt32(result, compressedData.length);
+ ZipUtils.putUnsignedInt32(result, uncompressedSize);
+ ZipUtils.putUnsignedInt16(result, nameBytes.length);
+ ZipUtils.putUnsignedInt16(result, 0); // Extra field length
+ result.put(nameBytes);
+ if (result.hasRemaining()) {
+ throw new RuntimeException("pos: " + result.position() + ", limit: " + result.limit());
+ }
+ result.flip();
+
+ long outputByteCount = result.remaining();
+ output.consume(result);
+ outputByteCount += compressedData.length;
+ output.consume(compressedData, 0, compressedData.length);
+ return outputByteCount;
+ }
+
+ private static final ByteBuffer EMPTY_BYTE_BUFFER = ByteBuffer.allocate(0);
+
+ /**
+ * Sends uncompressed data of this record into the the provided data sink.
+ */
+ public void outputUncompressedData(
+ DataSource lfhSection,
+ DataSink sink) throws IOException, ZipFormatException {
+ long dataStartOffsetInArchive = mStartOffsetInArchive + mDataStartOffset;
+ try {
+ if (mDataCompressed) {
+ try (InflateSinkAdapter inflateAdapter = new InflateSinkAdapter(sink)) {
+ lfhSection.feed(dataStartOffsetInArchive, mDataSize, inflateAdapter);
+ long actualUncompressedSize = inflateAdapter.getOutputByteCount();
+ if (actualUncompressedSize != mUncompressedDataSize) {
+ throw new ZipFormatException(
+ "Unexpected size of uncompressed data of " + mName
+ + ". Expected: " + mUncompressedDataSize + " bytes"
+ + ", actual: " + actualUncompressedSize + " bytes");
+ }
+ }
+ } else {
+ lfhSection.feed(dataStartOffsetInArchive, mDataSize, sink);
+ // No need to check whether output size is as expected because DataSource.feed is
+ // guaranteed to output exactly the number of bytes requested.
+ }
+ } catch (IOException e) {
+ throw new IOException(
+ "Failed to read data of " + ((mDataCompressed) ? "compressed" : "uncompressed")
+ + " entry " + mName,
+ e);
+ }
+ // Interestingly, Android doesn't check that uncompressed data's CRC-32 is as expected. We
+ // thus don't check either.
+ }
+
+ /**
+ * Sends uncompressed data pointed to by the provided ZIP Central Directory (CD) record into the
+ * provided data sink.
+ */
+ public static void outputUncompressedData(
+ DataSource source,
+ CentralDirectoryRecord cdRecord,
+ long cdStartOffsetInArchive,
+ DataSink sink) throws ZipFormatException, IOException {
+ // IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform
+ // exhibited when reading an APK for the purposes of verifying its signatures.
+ // When verifying an APK, Android doesn't care reading the extra field or the Data
+ // Descriptor.
+ LocalFileRecord lfhRecord =
+ getRecord(
+ source,
+ cdRecord,
+ cdStartOffsetInArchive,
+ false, // don't care about the extra field
+ false // don't read the Data Descriptor
+ );
+ lfhRecord.outputUncompressedData(source, sink);
+ }
+
+ /**
+ * Returns the uncompressed data pointed to by the provided ZIP Central Directory (CD) record.
+ */
+ public static byte[] getUncompressedData(
+ DataSource source,
+ CentralDirectoryRecord cdRecord,
+ long cdStartOffsetInArchive) throws ZipFormatException, IOException {
+ if (cdRecord.getUncompressedSize() > Integer.MAX_VALUE) {
+ throw new IOException(
+ cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize());
+ }
+ byte[] result = new byte[(int) cdRecord.getUncompressedSize()];
+ ByteBuffer resultBuf = ByteBuffer.wrap(result);
+ ByteBufferSink resultSink = new ByteBufferSink(resultBuf);
+ outputUncompressedData(
+ source,
+ cdRecord,
+ cdStartOffsetInArchive,
+ resultSink);
+ return result;
+ }
+
+ /**
+ * {@link DataSink} which inflates received data and outputs the deflated data into the provided
+ * delegate sink.
+ */
+ private static class InflateSinkAdapter implements DataSink, Closeable {
+ private final DataSink mDelegate;
+
+ private Inflater mInflater = new Inflater(true);
+ private byte[] mOutputBuffer;
+ private byte[] mInputBuffer;
+ private long mOutputByteCount;
+ private boolean mClosed;
+
+ private InflateSinkAdapter(DataSink delegate) {
+ mDelegate = delegate;
+ }
+
+ @Override
+ public void consume(byte[] buf, int offset, int length) throws IOException {
+ checkNotClosed();
+ mInflater.setInput(buf, offset, length);
+ if (mOutputBuffer == null) {
+ mOutputBuffer = new byte[65536];
+ }
+ while (!mInflater.finished()) {
+ int outputChunkSize;
+ try {
+ outputChunkSize = mInflater.inflate(mOutputBuffer);
+ } catch (DataFormatException e) {
+ throw new IOException("Failed to inflate data", e);
+ }
+ if (outputChunkSize == 0) {
+ return;
+ }
+ mDelegate.consume(mOutputBuffer, 0, outputChunkSize);
+ mOutputByteCount += outputChunkSize;
+ }
+ }
+
+ @Override
+ public void consume(ByteBuffer buf) throws IOException {
+ checkNotClosed();
+ if (buf.hasArray()) {
+ consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
+ buf.position(buf.limit());
+ } else {
+ if (mInputBuffer == null) {
+ mInputBuffer = new byte[65536];
+ }
+ while (buf.hasRemaining()) {
+ int chunkSize = Math.min(buf.remaining(), mInputBuffer.length);
+ buf.get(mInputBuffer, 0, chunkSize);
+ consume(mInputBuffer, 0, chunkSize);
+ }
+ }
+ }
+
+ public long getOutputByteCount() {
+ return mOutputByteCount;
+ }
+
+ @Override
+ public void close() throws IOException {
+ mClosed = true;
+ mInputBuffer = null;
+ mOutputBuffer = null;
+ if (mInflater != null) {
+ mInflater.end();
+ mInflater = null;
+ }
+ }
+
+ private void checkNotClosed() {
+ if (mClosed) {
+ throw new IllegalStateException("Closed");
+ }
+ }
+ }
+}
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 118585a9a..6a0c501e9 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,12 @@
package com.android.apksigner.core.internal.zip;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.zip.CRC32;
+import java.util.zip.Deflater;
import com.android.apksigner.core.internal.util.Pair;
import com.android.apksigner.core.util.DataSource;
@@ -35,6 +38,9 @@ public abstract class ZipUtils {
public static final short COMPRESSION_METHOD_STORED = 0;
public static final short COMPRESSION_METHOD_DEFLATED = 8;
+ public static final short GP_FLAG_DATA_DESCRIPTOR_USED = 0x08;
+ public static final short GP_FLAG_EFS = 0x0800;
+
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;
@@ -265,14 +271,83 @@ public abstract class ZipUtils {
return buffer.getShort(offset) & 0xffff;
}
- private static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) {
+ public static int getUnsignedInt16(ByteBuffer buffer) {
+ return buffer.getShort() & 0xffff;
+ }
+
+ static void setUnsignedInt16(ByteBuffer buffer, int offset, int value) {
+ if ((value < 0) || (value > 0xffff)) {
+ throw new IllegalArgumentException("uint16 value of out range: " + value);
+ }
+ buffer.putShort(offset, (short) value);
+ }
+
+ 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(offset, (int) value);
}
+ public static void putUnsignedInt16(ByteBuffer buffer, int value) {
+ if ((value < 0) || (value > 0xffff)) {
+ throw new IllegalArgumentException("uint16 value of out range: " + value);
+ }
+ buffer.putShort((short) value);
+ }
+
static long getUnsignedInt32(ByteBuffer buffer, int offset) {
return buffer.getInt(offset) & 0xffffffffL;
}
+
+ static long getUnsignedInt32(ByteBuffer buffer) {
+ return buffer.getInt() & 0xffffffffL;
+ }
+
+ static void putUnsignedInt32(ByteBuffer buffer, long value) {
+ if ((value < 0) || (value > 0xffffffffL)) {
+ throw new IllegalArgumentException("uint32 value of out range: " + value);
+ }
+ buffer.putInt((int) value);
+ }
+
+ public static DeflateResult deflate(ByteBuffer input) {
+ byte[] inputBuf;
+ int inputOffset;
+ int inputLength = input.remaining();
+ if (input.hasArray()) {
+ inputBuf = input.array();
+ inputOffset = input.arrayOffset() + input.position();
+ input.position(input.limit());
+ } else {
+ inputBuf = new byte[inputLength];
+ inputOffset = 0;
+ input.get(inputBuf);
+ }
+ CRC32 crc32 = new CRC32();
+ crc32.update(inputBuf, inputOffset, inputLength);
+ long crc32Value = crc32.getValue();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ Deflater deflater = new Deflater(9, true);
+ deflater.setInput(inputBuf, inputOffset, inputLength);
+ deflater.finish();
+ byte[] buf = new byte[65536];
+ while (!deflater.finished()) {
+ int chunkSize = deflater.deflate(buf);
+ out.write(buf, 0, chunkSize);
+ }
+ return new DeflateResult(inputLength, crc32Value, out.toByteArray());
+ }
+
+ public static class DeflateResult {
+ public final int inputSizeBytes;
+ public final long inputCrc32;
+ public final byte[] output;
+
+ public DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output) {
+ this.inputSizeBytes = inputSizeBytes;
+ this.inputCrc32 = inputCrc32;
+ this.output = output;
+ }
+ }
}
\ No newline at end of file
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSinks.java b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSinks.java
index 8ee1f1388..4aefedb05 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSinks.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSinks.java
@@ -17,8 +17,10 @@
package com.android.apksigner.core.util;
import java.io.OutputStream;
+import java.io.RandomAccessFile;
import com.android.apksigner.core.internal.util.OutputStreamDataSink;
+import com.android.apksigner.core.internal.util.RandomAccessFileDataSink;
/**
* Utility methods for working with {@link DataSink} abstraction.
@@ -33,4 +35,12 @@ public abstract class DataSinks {
public static DataSink asDataSink(OutputStream out) {
return new OutputStreamDataSink(out);
}
+
+ /**
+ * Returns a {@link DataSink} which outputs received data into the provided file, sequentially,
+ * starting at the beginning of the file.
+ */
+ public static DataSink asDataSink(RandomAccessFile file) {
+ return new RandomAccessFileDataSink(file);
+ }
}
+ *
+ */
+ public static class Builder {
+ private final ApkSignerEngine mSignerEngine;
+
+ private File mInputApkFile;
+ private DataSource mInputApkDataSource;
+
+ private File mOutputApkFile;
+ private DataSink mOutputApkDataSink;
+ private DataSource mOutputApkDataSource;
+
+ /**
+ * Constructs a new {@code Builder} which will make {@code ApkSigner} use the provided
+ * signing engine.
+ */
+ public Builder(ApkSignerEngine signerEngine) {
+ mSignerEngine = signerEngine;
+ }
+
+ /**
+ * Sets the APK to be signed.
+ *
+ * @see #setInputApk(DataSource)
+ */
+ public Builder setInputApk(File inputApk) {
+ if (inputApk == null) {
+ throw new NullPointerException("inputApk == null");
+ }
+ mInputApkFile = inputApk;
+ mInputApkDataSource = null;
+ return this;
+ }
+
+ /**
+ * Sets the APK to be signed.
+ *
+ * @see #setInputApk(File)
+ */
+ public Builder setInputApk(DataSource inputApk) {
+ if (inputApk == null) {
+ throw new NullPointerException("inputApk == null");
+ }
+ mInputApkDataSource = inputApk;
+ mInputApkFile = null;
+ return this;
+ }
+
+ /**
+ * Sets the location of the output (signed) APK. {@code ApkSigner} will create this file if
+ * it doesn't exist.
+ *
+ * @see #setOutputApk(DataSink, DataSource)
+ */
+ public Builder setOutputApk(File outputApk) {
+ if (outputApk == null) {
+ throw new NullPointerException("outputApk == null");
+ }
+ mOutputApkFile = outputApk;
+ mOutputApkDataSink = null;
+ mOutputApkDataSource = null;
+ return this;
+ }
+
+ /**
+ * Sets the sink which will receive the output (signed) APK. Data received by the
+ * {@code outputApkOut} sink must be visible through the {@code outputApkIn} data source.
+ *
+ * @see #setOutputApk(File)
+ */
+ public Builder setOutputApk(DataSink outputApkOut, DataSource outputApkIn) {
+ if (outputApkOut == null) {
+ throw new NullPointerException("outputApkOut == null");
+ }
+ if (outputApkIn == null) {
+ throw new NullPointerException("outputApkIn == null");
+ }
+ mOutputApkFile = null;
+ mOutputApkDataSink = outputApkOut;
+ mOutputApkDataSource = outputApkIn;
+ return this;
+ }
+
+ /**
+ * Returns a new {@code ApkSigner} instance initialized according to the configuration of
+ * this builder.
+ */
+ public ApkSigner build() {
+ return new ApkSigner(
+ mSignerEngine,
+ mInputApkFile,
+ mInputApkDataSource,
+ mOutputApkFile,
+ mOutputApkDataSink,
+ mOutputApkDataSource);
+ }
+ }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/ApkSignerEngine.java b/tools/apksigner/core/src/com/android/apksigner/core/ApkSignerEngine.java
index 6a148ca2a..21c270640 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/ApkSignerEngine.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/ApkSignerEngine.java
@@ -33,9 +33,9 @@ import com.android.apksigner.core.util.DataSource;
* Operating Model
*
* The abstract operating model is that there is an input APK which is being signed, thus producing
- * an output APK. In reality, there may be just an output APK being built from scratch, or the input APK and
- * the output APK may be the same file. Because this engine does not deal with reading and writing
- * files, it can handle all of these scenarios.
+ * an output APK. In reality, there may be just an output APK being built from scratch, or the input
+ * APK and the output APK may be the same file. Because this engine does not deal with reading and
+ * writing files, it can handle all of these scenarios.
*
*