aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTreehugger Robot <treehugger-gerrit@google.com>2016-07-06 22:59:32 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2016-07-06 22:59:32 +0000
commitd81beca2b2efab77b582f7fd5ee874d904c6007f (patch)
tree4abd16c8bc11448b3b4eda29e9887b1d28c1c931
parent997a6cd1a17ad4b0db404e14b3c44618737beeef (diff)
parent819e8485e42c6e6e0045982a3a5f046493d850ca (diff)
downloadbuild-d81beca2b2efab77b582f7fd5ee874d904c6007f.tar.gz
Merge "APK signer primitive."
-rw-r--r--tools/apksigner/core/src/com/android/apksigner/core/ApkSigner.java711
-rw-r--r--tools/apksigner/core/src/com/android/apksigner/core/ApkSignerEngine.java9
-rw-r--r--tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeVerifier.java21
-rw-r--r--tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java58
-rw-r--r--tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSink.java87
-rw-r--r--tools/apksigner/core/src/com/android/apksigner/core/internal/zip/CentralDirectoryRecord.java168
-rw-r--r--tools/apksigner/core/src/com/android/apksigner/core/internal/zip/EocdRecord.java48
-rw-r--r--tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileHeader.java282
-rw-r--r--tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileRecord.java540
-rw-r--r--tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java77
-rw-r--r--tools/apksigner/core/src/com/android/apksigner/core/util/DataSinks.java10
11 files changed, 1646 insertions, 365 deletions
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 0000000000..2491302cef
--- /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.
+ *
+ * <p>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.
+ *
+ * <p>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<DataSource, Long> apkSigningBlockAndOffset =
+ V2SchemeVerifier.findApkSigningBlock(inputApk, inputZipSections);
+ signerEngine.inputApkSigningBlock(apkSigningBlockAndOffset.getFirst());
+ apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond();
+ } catch (V2SchemeVerifier.SignatureNotFoundException e) {
+ // Input APK does not contain an APK Signing Block. That's OK. APKs are not required to
+ // contain this block. It's only needed if the APK is signed using APK Signature Scheme
+ // v2.
+ }
+
+ // Step 2. Parse the input APK's ZIP Central Directory
+ ByteBuffer inputCd = getZipCentralDirectory(inputApk, inputZipSections);
+ List<CentralDirectoryRecord> inputCdRecords =
+ parseZipCentralDirectory(inputCd, inputZipSections);
+
+ // Step 3. Iterate over input APK's entries and output the Local File Header + data of those
+ // entries which need to be output. Entries are iterated in the order in which their Local
+ // File Header records are stored in the file. This is to achieve better data locality in
+ // case Central Directory entries are in the wrong order.
+ List<CentralDirectoryRecord> inputCdRecordsSortedByLfhOffset =
+ new ArrayList<>(inputCdRecords);
+ Collections.sort(
+ inputCdRecordsSortedByLfhOffset,
+ CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR);
+ DataSource inputApkLfhSection =
+ inputApk.slice(
+ 0,
+ (apkSigningBlockOffset != -1)
+ ? apkSigningBlockOffset
+ : inputZipSections.getZipCentralDirectoryOffset());
+ int lastModifiedDateForNewEntries = -1;
+ int lastModifiedTimeForNewEntries = -1;
+ long inputOffset = 0;
+ long outputOffset = 0;
+ Map<String, CentralDirectoryRecord> outputCdRecordsByName =
+ new HashMap<>(inputCdRecords.size());
+ for (final CentralDirectoryRecord inputCdRecord : inputCdRecordsSortedByLfhOffset) {
+ String entryName = inputCdRecord.getName();
+ ApkSignerEngine.InputJarEntryInstructions entryInstructions =
+ signerEngine.inputJarEntry(entryName);
+ boolean shouldOutput;
+ switch (entryInstructions.getOutputPolicy()) {
+ case OUTPUT:
+ shouldOutput = true;
+ break;
+ case OUTPUT_BY_ENGINE:
+ case SKIP:
+ shouldOutput = false;
+ break;
+ default:
+ throw new RuntimeException(
+ "Unknown output policy: " + entryInstructions.getOutputPolicy());
+ }
+
+ long inputLocalFileHeaderStartOffset = inputCdRecord.getLocalFileHeaderOffset();
+ if (inputLocalFileHeaderStartOffset > inputOffset) {
+ // Unprocessed data in input starting at inputOffset and ending and the start of
+ // this record's LFH. We output this data verbatim because this signer is supposed
+ // to preserve as much of input as possible.
+ long chunkSize = inputLocalFileHeaderStartOffset - inputOffset;
+ inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut);
+ outputOffset += chunkSize;
+ inputOffset = inputLocalFileHeaderStartOffset;
+ }
+ LocalFileRecord inputLocalFileRecord =
+ LocalFileRecord.getRecord(
+ inputApkLfhSection, inputCdRecord, inputApkLfhSection.size());
+ inputOffset += inputLocalFileRecord.getSize();
+
+ ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+ entryInstructions.getInspectJarEntryRequest();
+ if (inspectEntryRequest != null) {
+ fulfillInspectInputJarEntryRequest(
+ inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest);
+ }
+
+ if (shouldOutput) {
+ // Find the max value of last modified, to be used for new entries added by the
+ // signer.
+ int lastModifiedDate = inputCdRecord.getLastModificationDate();
+ int lastModifiedTime = inputCdRecord.getLastModificationTime();
+ if ((lastModifiedDateForNewEntries == -1)
+ || (lastModifiedDate > lastModifiedDateForNewEntries)
+ || ((lastModifiedDate == lastModifiedDateForNewEntries)
+ && (lastModifiedTime > lastModifiedTimeForNewEntries))) {
+ lastModifiedDateForNewEntries = lastModifiedDate;
+ lastModifiedTimeForNewEntries = lastModifiedTime;
+ }
+
+ inspectEntryRequest = signerEngine.outputJarEntry(entryName);
+ if (inspectEntryRequest != null) {
+ fulfillInspectInputJarEntryRequest(
+ inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest);
+ }
+
+ // Output entry's Local File Header + data
+ long outputLocalFileHeaderOffset = outputOffset;
+ long outputLocalFileRecordSize =
+ outputInputJarEntryLfhRecordPreservingDataAlignment(
+ inputApkLfhSection,
+ inputLocalFileRecord,
+ outputApkOut,
+ outputLocalFileHeaderOffset);
+ outputOffset += outputLocalFileRecordSize;
+
+ // Enqueue entry's Central Directory record for output
+ CentralDirectoryRecord outputCdRecord;
+ if (outputLocalFileHeaderOffset == inputLocalFileRecord.getStartOffsetInArchive()) {
+ outputCdRecord = inputCdRecord;
+ } else {
+ outputCdRecord =
+ inputCdRecord.createWithModifiedLocalFileHeaderOffset(
+ outputLocalFileHeaderOffset);
+ }
+ outputCdRecordsByName.put(entryName, outputCdRecord);
+ }
+ }
+ long inputLfhSectionSize = inputApkLfhSection.size();
+ if (inputOffset < inputLfhSectionSize) {
+ // Unprocessed data in input starting at inputOffset and ending and the end of the input
+ // APK's LFH section. We output this data verbatim because this signer is supposed
+ // to preserve as much of input as possible.
+ long chunkSize = inputLfhSectionSize - inputOffset;
+ inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut);
+ outputOffset += chunkSize;
+ inputOffset = inputLfhSectionSize;
+ }
+
+ // Step 4. Sort output APK's Central Directory records in the order in which they should
+ // appear in the output
+ List<CentralDirectoryRecord> outputCdRecords = new ArrayList<>(inputCdRecords.size() + 10);
+ for (CentralDirectoryRecord inputCdRecord : inputCdRecords) {
+ String entryName = inputCdRecord.getName();
+ CentralDirectoryRecord outputCdRecord = outputCdRecordsByName.get(entryName);
+ if (outputCdRecord != null) {
+ outputCdRecords.add(outputCdRecord);
+ }
+ }
+
+ // Step 5. Generate and output JAR signatures, if necessary. This may output more Local File
+ // Header + data entries and add to the list of output Central Directory records.
+ ApkSignerEngine.OutputJarSignatureRequest outputJarSignatureRequest =
+ signerEngine.outputJarEntries();
+ if (outputJarSignatureRequest != null) {
+ if (lastModifiedDateForNewEntries == -1) {
+ lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS)
+ lastModifiedTimeForNewEntries = 0;
+ }
+ for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry :
+ outputJarSignatureRequest.getAdditionalJarEntries()) {
+ String entryName = entry.getName();
+ byte[] uncompressedData = entry.getData();
+ ZipUtils.DeflateResult deflateResult =
+ ZipUtils.deflate(ByteBuffer.wrap(uncompressedData));
+ byte[] compressedData = deflateResult.output;
+ long uncompressedDataCrc32 = deflateResult.inputCrc32;
+
+ ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+ signerEngine.outputJarEntry(entryName);
+ if (inspectEntryRequest != null) {
+ inspectEntryRequest.getDataSink().consume(
+ uncompressedData, 0, uncompressedData.length);
+ inspectEntryRequest.done();
+ }
+
+ long localFileHeaderOffset = outputOffset;
+ outputOffset +=
+ LocalFileRecord.outputRecordWithDeflateCompressedData(
+ entryName,
+ lastModifiedTimeForNewEntries,
+ lastModifiedDateForNewEntries,
+ compressedData,
+ uncompressedDataCrc32,
+ uncompressedData.length,
+ outputApkOut);
+
+
+ outputCdRecords.add(
+ CentralDirectoryRecord.createWithDeflateCompressedData(
+ entryName,
+ lastModifiedTimeForNewEntries,
+ lastModifiedDateForNewEntries,
+ uncompressedDataCrc32,
+ compressedData.length,
+ uncompressedData.length,
+ localFileHeaderOffset));
+ }
+ outputJarSignatureRequest.done();
+ }
+
+ // Step 6. Construct output ZIP Central Directory in an in-memory buffer
+ long outputCentralDirSizeBytes = 0;
+ for (CentralDirectoryRecord record : outputCdRecords) {
+ outputCentralDirSizeBytes += record.getSize();
+ }
+ if (outputCentralDirSizeBytes > Integer.MAX_VALUE) {
+ throw new IOException(
+ "Output ZIP Central Directory too large: " + outputCentralDirSizeBytes
+ + " bytes");
+ }
+ ByteBuffer outputCentralDir = ByteBuffer.allocate((int) outputCentralDirSizeBytes);
+ for (CentralDirectoryRecord record : outputCdRecords) {
+ record.copyTo(outputCentralDir);
+ }
+ outputCentralDir.flip();
+ DataSource outputCentralDirDataSource = new ByteBufferDataSource(outputCentralDir);
+ long outputCentralDirStartOffset = outputOffset;
+ int outputCentralDirRecordCount = outputCdRecords.size();
+
+ // Step 7. Construct output ZIP End of Central Directory record in an in-memory buffer
+ ByteBuffer outputEocd =
+ EocdRecord.createWithModifiedCentralDirectoryInfo(
+ inputZipSections.getZipEndOfCentralDirectory(),
+ outputCentralDirRecordCount,
+ outputCentralDirDataSource.size(),
+ outputCentralDirStartOffset);
+
+ // Step 8. Generate and output APK Signature Scheme v2 signatures, if necessary. This may
+ // insert an APK Signing Block just before the output's ZIP Central Directory
+ ApkSignerEngine.OutputApkSigningBlockRequest outputApkSigingBlockRequest =
+ signerEngine.outputZipSections(
+ outputApkIn,
+ outputCentralDirDataSource,
+ DataSources.asDataSource(outputEocd));
+ if (outputApkSigingBlockRequest != null) {
+ byte[] outputApkSigningBlock = outputApkSigingBlockRequest.getApkSigningBlock();
+ outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length);
+ ZipUtils.setZipEocdCentralDirectoryOffset(
+ outputEocd, outputCentralDirStartOffset + outputApkSigningBlock.length);
+ outputApkSigingBlockRequest.done();
+ }
+
+ // Step 9. Output ZIP Central Directory and ZIP End of Central Directory
+ outputCentralDirDataSource.feed(0, outputCentralDirDataSource.size(), outputApkOut);
+ outputApkOut.consume(outputEocd);
+ signerEngine.outputDone();
+ }
+
+ private static void fulfillInspectInputJarEntryRequest(
+ DataSource lfhSection,
+ LocalFileRecord localFileRecord,
+ ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest)
+ throws IOException, ZipFormatException {
+ localFileRecord.outputUncompressedData(lfhSection, inspectEntryRequest.getDataSink());
+ inspectEntryRequest.done();
+ }
+
+ private static long outputInputJarEntryLfhRecordPreservingDataAlignment(
+ DataSource inputLfhSection,
+ LocalFileRecord inputRecord,
+ DataSink outputLfhSection,
+ long outputOffset) throws IOException {
+ long inputOffset = inputRecord.getStartOffsetInArchive();
+ if (inputOffset == outputOffset) {
+ // This record's data will be aligned same as in the input APK.
+ return inputRecord.outputRecord(inputLfhSection, outputLfhSection);
+ }
+ int dataAlignmentMultiple = getInputJarEntryDataAlignmentMultiple(inputRecord);
+ if ((dataAlignmentMultiple <= 1)
+ || ((inputOffset % dataAlignmentMultiple)
+ == (outputOffset % dataAlignmentMultiple))) {
+ // This record's data will be aligned same as in the input APK.
+ return inputRecord.outputRecord(inputLfhSection, outputLfhSection);
+ }
+
+ long inputDataStartOffset = inputOffset + inputRecord.getDataStartOffsetInRecord();
+ if ((inputDataStartOffset % dataAlignmentMultiple) != 0) {
+ // This record's data is not aligned in the input APK. No need to align it in the
+ // output.
+ return inputRecord.outputRecord(inputLfhSection, outputLfhSection);
+ }
+
+ // This record's data needs to be re-aligned in the output. This is achieved using the
+ // record's extra field.
+ ByteBuffer aligningExtra =
+ createExtraFieldToAlignData(
+ inputRecord.getExtra(),
+ outputOffset + inputRecord.getExtraFieldStartOffsetInsideRecord(),
+ dataAlignmentMultiple);
+ return inputRecord.outputRecordWithModifiedExtra(
+ inputLfhSection, aligningExtra, outputLfhSection);
+ }
+
+ private static int getInputJarEntryDataAlignmentMultiple(LocalFileRecord entry) {
+ if (entry.isDataCompressed()) {
+ // Compressed entries don't need to be aligned
+ return 1;
+ }
+
+ // Attempt to obtain the alignment multiple from the entry's extra field.
+ ByteBuffer extra = entry.getExtra();
+ if (extra.hasRemaining()) {
+ extra.order(ByteOrder.LITTLE_ENDIAN);
+ // FORMAT: sequence of fields. Each field consists of:
+ // * uint16 ID
+ // * uint16 size
+ // * 'size' bytes: payload
+ while (extra.remaining() >= 4) {
+ short headerId = extra.getShort();
+ int dataSize = ZipUtils.getUnsignedInt16(extra);
+ if (dataSize > extra.remaining()) {
+ // Malformed field -- insufficient input remaining
+ break;
+ }
+ if (headerId != ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) {
+ // Skip this field
+ extra.position(extra.position() + dataSize);
+ continue;
+ }
+ // This is APK alignment field.
+ // FORMAT:
+ // * uint16 alignment multiple (in bytes)
+ // * remaining bytes -- padding to achieve alignment of data which starts after
+ // the extra field
+ if (dataSize < 2) {
+ // Malformed
+ break;
+ }
+ return ZipUtils.getUnsignedInt16(extra);
+ }
+ }
+
+ // Fall back to filename-based defaults
+ return (entry.getName().endsWith(".so")) ? 4096 : 4;
+ }
+
+ private static ByteBuffer createExtraFieldToAlignData(
+ ByteBuffer original,
+ long extraStartOffset,
+ int dataAlignmentMultiple) {
+ if (dataAlignmentMultiple <= 1) {
+ return original;
+ }
+
+ // In the worst case scenario, we'll increase the output size by 6 + dataAlignment - 1.
+ ByteBuffer result = ByteBuffer.allocate(original.remaining() + 5 + dataAlignmentMultiple);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+
+ // Step 1. Output all extra fields other than the one which is to do with alignment
+ // FORMAT: sequence of fields. Each field consists of:
+ // * uint16 ID
+ // * uint16 size
+ // * 'size' bytes: payload
+ while (original.remaining() >= 4) {
+ short headerId = original.getShort();
+ int dataSize = ZipUtils.getUnsignedInt16(original);
+ if (dataSize > original.remaining()) {
+ // Malformed field -- insufficient input remaining
+ break;
+ }
+ if (((headerId == 0) && (dataSize == 0))
+ || (headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID)) {
+ // Ignore the field if it has to do with the old APK data alignment method (filling
+ // the extra field with 0x00 bytes) or the new APK data alignment method.
+ original.position(original.position() + dataSize);
+ continue;
+ }
+ // Copy this field (including header) to the output
+ original.position(original.position() - 4);
+ int originalLimit = original.limit();
+ original.limit(original.position() + 4 + dataSize);
+ result.put(original);
+ original.limit(originalLimit);
+ }
+
+ // Step 2. Add alignment field
+ // FORMAT:
+ // * uint16 extra header ID
+ // * uint16 extra data size
+ // Payload ('data size' bytes)
+ // * uint16 alignment multiple (in bytes)
+ // * remaining bytes -- padding to achieve alignment of data which starts after the
+ // extra field
+ long dataMinStartOffset =
+ extraStartOffset + result.position()
+ + ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES;
+ int paddingSizeBytes =
+ (dataAlignmentMultiple - ((int) (dataMinStartOffset % dataAlignmentMultiple)))
+ % dataAlignmentMultiple;
+ result.putShort(ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID);
+ ZipUtils.putUnsignedInt16(result, 2 + paddingSizeBytes);
+ ZipUtils.putUnsignedInt16(result, dataAlignmentMultiple);
+ result.position(result.position() + paddingSizeBytes);
+ result.flip();
+
+ return result;
+ }
+
+ private static ByteBuffer getZipCentralDirectory(
+ DataSource apk,
+ ApkUtils.ZipSections apkSections) throws IOException, ZipFormatException {
+ long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes();
+ if (cdSizeBytes > Integer.MAX_VALUE) {
+ throw new ZipFormatException("ZIP Central Directory too large: " + cdSizeBytes);
+ }
+ long cdOffset = apkSections.getZipCentralDirectoryOffset();
+ ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes);
+ cd.order(ByteOrder.LITTLE_ENDIAN);
+ return cd;
+ }
+
+ private static List<CentralDirectoryRecord> parseZipCentralDirectory(
+ ByteBuffer cd,
+ ApkUtils.ZipSections apkSections) throws ZipFormatException {
+ long cdOffset = apkSections.getZipCentralDirectoryOffset();
+ int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount();
+ List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount);
+ Set<String> entryNames = new HashSet<>(expectedCdRecordCount);
+ for (int i = 0; i < expectedCdRecordCount; i++) {
+ CentralDirectoryRecord cdRecord;
+ int offsetInsideCd = cd.position();
+ try {
+ cdRecord = CentralDirectoryRecord.getRecord(cd);
+ } catch (ZipFormatException e) {
+ throw new ZipFormatException(
+ "Failed to parse ZIP Central Directory record #" + (i + 1)
+ + " at file offset " + (cdOffset + offsetInsideCd),
+ e);
+ }
+ String entryName = cdRecord.getName();
+ if (!entryNames.add(entryName)) {
+ throw new ZipFormatException(
+ "Malformed APK: multiple JAR entries with the same name: " + entryName);
+ }
+ cdRecords.add(cdRecord);
+ }
+ if (cd.hasRemaining()) {
+ throw new ZipFormatException(
+ "Unused space at the end of ZIP Central Directory: " + cd.remaining()
+ + " bytes starting at file offset " + (cdOffset + cd.position()));
+ }
+
+ return cdRecords;
+ }
+
+ /**
+ * Builder of {@link ApkSigner} instances.
+ *
+ * <p>The following information is required to construct a working {@code ApkSigner}:
+ * <ul>
+ * <li>{@link ApkSignerEngine} -- provided in the constructor,</li>
+ * <li>APK to be signed -- see {@link #setInputApk(File) setInputApk} variants,</li>
+ * <li>where to store the signed APK -- see {@link #setOutputApk(File) setOutputApk} variants.
+ * </li>
+ * </ul>
+ */
+ 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 6a148ca2a3..21c2706404 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;
* <p><h3>Operating Model</h3>
*
* 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.
*
* <p>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 1bba313232..752ba7e02e 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<String, ManifestParser.Section> entryNameToManifestSection = null;
ManifestParser manifest = new ManifestParser(manifestBytes);
ManifestParser.Section manifestMainSection = manifest.readSection();
@@ -411,15 +408,9 @@ public abstract class V1SchemeVerifier {
DataSource apk, long cdStartOffset, int minSdkVersion, int maxSdkVersion)
throws IOException, ZipFormatException, NoSuchAlgorithmException {
byte[] sigBlockBytes =
- LocalFileHeader.getUncompressedData(
- apk, 0,
- mSignatureBlockEntry,
- cdStartOffset);
+ LocalFileRecord.getUncompressedData(apk, mSignatureBlockEntry, cdStartOffset);
mSigFileBytes =
- LocalFileHeader.getUncompressedData(
- apk, 0,
- mSignatureFileEntry,
- cdStartOffset);
+ LocalFileRecord.getUncompressedData(apk, mSignatureFileEntry, cdStartOffset);
PKCS7 sigBlock;
try {
sigBlock = new PKCS7(sigBlockBytes);
@@ -1412,8 +1403,8 @@ public abstract class V1SchemeVerifier {
}
try {
- LocalFileHeader.sendUncompressedData(
- apk, 0,
+ LocalFileRecord.outputUncompressedData(
+ apk,
cdRecord,
cdOffsetInApk,
new MessageDigestSink(mds));
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java
index 0c303ee3dc..5e1e8fb743 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java
@@ -553,38 +553,35 @@ public abstract class V2SchemeVerifier {
private static SignatureInfo findSignature(
DataSource apk, ApkUtils.ZipSections zipSections, Result result)
throws IOException, SignatureNotFoundException {
- long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
- long centralDirEndOffset =
- centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
- long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
- if (centralDirEndOffset != eocdStartOffset) {
- throw new SignatureNotFoundException(
- "ZIP Central Directory is not immediately followed by End of Central Directory"
- + ". CD end: " + centralDirEndOffset
- + ", EoCD start: " + eocdStartOffset);
- }
-
// Find the APK Signing Block. The block immediately precedes the Central Directory.
ByteBuffer eocd = zipSections.getZipEndOfCentralDirectory();
- Pair<ByteBuffer, Long> apkSigningBlockAndOffset =
- findApkSigningBlock(apk, centralDirStartOffset);
- ByteBuffer apkSigningBlock = apkSigningBlockAndOffset.getFirst();
+ Pair<DataSource, Long> apkSigningBlockAndOffset = findApkSigningBlock(apk, zipSections);
+ DataSource apkSigningBlock = apkSigningBlockAndOffset.getFirst();
long apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond();
+ ByteBuffer apkSigningBlockBuf =
+ apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size());
+ apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN);
// Find the APK Signature Scheme v2 Block inside the APK Signing Block.
ByteBuffer apkSignatureSchemeV2Block =
- findApkSignatureSchemeV2Block(apkSigningBlock, result);
+ findApkSignatureSchemeV2Block(apkSigningBlockBuf, result);
return new SignatureInfo(
apkSignatureSchemeV2Block,
apkSigningBlockOffset,
- centralDirStartOffset,
- eocdStartOffset,
+ zipSections.getZipCentralDirectoryOffset(),
+ zipSections.getZipEndOfCentralDirectoryOffset(),
eocd);
}
- private static Pair<ByteBuffer, Long> findApkSigningBlock(
- DataSource apk, long centralDirOffset) throws IOException, SignatureNotFoundException {
+ /**
+ * Returns the APK Signing Block and its offset in the provided APK.
+ *
+ * @throws SignatureNotFoundException if the APK does not contain an APK Signing Block
+ */
+ public static Pair<DataSource, Long> findApkSigningBlock(
+ DataSource apk, ApkUtils.ZipSections zipSections)
+ throws IOException, SignatureNotFoundException {
// FORMAT:
// OFFSET DATA TYPE DESCRIPTION
// * @+0 bytes uint64: size in bytes (excluding this field)
@@ -592,15 +589,26 @@ public abstract class V2SchemeVerifier {
// * @-24 bytes uint64: size in bytes (same as the one above)
// * @-16 bytes uint128: magic
- if (centralDirOffset < APK_SIG_BLOCK_MIN_SIZE) {
+ long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
+ long centralDirEndOffset =
+ centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
+ long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
+ if (centralDirEndOffset != eocdStartOffset) {
+ throw new SignatureNotFoundException(
+ "ZIP Central Directory is not immediately followed by End of Central Directory"
+ + ". CD end: " + centralDirEndOffset
+ + ", EoCD start: " + eocdStartOffset);
+ }
+
+ if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) {
throw new SignatureNotFoundException(
"APK too small for APK Signing Block. ZIP Central Directory offset: "
- + centralDirOffset);
+ + centralDirStartOffset);
}
// Read the magic and offset in file from the footer section of the block:
// * uint64: size of block
// * 16 bytes: magic
- ByteBuffer footer = apk.getByteBuffer(centralDirOffset - 24, 24);
+ ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24);
footer.order(ByteOrder.LITTLE_ENDIAN);
if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
|| (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
@@ -615,12 +623,12 @@ public abstract class V2SchemeVerifier {
"APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
}
int totalSize = (int) (apkSigBlockSizeInFooter + 8);
- long apkSigBlockOffset = centralDirOffset - totalSize;
+ long apkSigBlockOffset = centralDirStartOffset - totalSize;
if (apkSigBlockOffset < 0) {
throw new SignatureNotFoundException(
"APK Signing Block offset out of range: " + apkSigBlockOffset);
}
- ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, totalSize);
+ ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8);
apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
@@ -628,7 +636,7 @@ public abstract class V2SchemeVerifier {
"APK Signing Block sizes in header and footer do not match: "
+ apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
}
- return Pair.of(apkSigBlock, apkSigBlockOffset);
+ return Pair.of(apk.slice(apkSigBlockOffset, totalSize), apkSigBlockOffset);
}
private static ByteBuffer findApkSignatureSchemeV2Block(
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSink.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSink.java
new file mode 100644
index 0000000000..2198492289
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSink.java
@@ -0,0 +1,87 @@
+/*
+ * 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.internal.util;
+
+import com.android.apksigner.core.util.DataSink;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+
+/**
+ * {@link DataSink} which outputs received data into the associated file, sequentially.
+ */
+public class RandomAccessFileDataSink implements DataSink {
+
+ private final RandomAccessFile mFile;
+ private final FileChannel mFileChannel;
+ private long mPosition;
+
+ /**
+ * Constructs a new {@code RandomAccessFileDataSink} which stores output starting from the
+ * beginning of the provided file.
+ */
+ public RandomAccessFileDataSink(RandomAccessFile file) {
+ this(file, 0);
+ }
+
+ /**
+ * Constructs a new {@code RandomAccessFileDataSink} which stores output starting from the
+ * specified position of the provided file.
+ */
+ public RandomAccessFileDataSink(RandomAccessFile file, long startPosition) {
+ if (file == null) {
+ throw new NullPointerException("file == null");
+ }
+ if (startPosition < 0) {
+ throw new IllegalArgumentException("startPosition: " + startPosition);
+ }
+ mFile = file;
+ mFileChannel = file.getChannel();
+ mPosition = startPosition;
+ }
+
+ @Override
+ public void consume(byte[] buf, int offset, int length) throws IOException {
+ if (length == 0) {
+ return;
+ }
+
+ synchronized (mFile) {
+ mFile.seek(mPosition);
+ mFile.write(buf, offset, length);
+ mPosition += length;
+ }
+ }
+
+ @Override
+ public void consume(ByteBuffer buf) throws IOException {
+ int length = buf.remaining();
+ if (length == 0) {
+ return;
+ }
+
+ synchronized (mFile) {
+ mFile.seek(mPosition);
+ while (buf.hasRemaining()) {
+ mFileChannel.write(buf);
+ }
+ mPosition += length;
+ }
+ }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/CentralDirectoryRecord.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/CentralDirectoryRecord.java
index 6a5b94c4ef..141d01e1c2 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/CentralDirectoryRecord.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/CentralDirectoryRecord.java
@@ -20,6 +20,7 @@ import com.android.apksigner.core.zip.ZipFormatException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.Comparator;
@@ -38,52 +39,59 @@ public class CentralDirectoryRecord {
private static final int RECORD_SIGNATURE = 0x02014b50;
private static final int HEADER_SIZE_BYTES = 46;
- private static final int GP_FLAGS_OFFSET = 8;
- private static final int COMPRESSION_METHOD_OFFSET = 10;
- private static final int CRC32_OFFSET = 16;
- private static final int COMPRESSED_SIZE_OFFSET = 20;
- private static final int UNCOMPRESSED_SIZE_OFFSET = 24;
- private static final int NAME_LENGTH_OFFSET = 28;
- private static final int EXTRA_LENGTH_OFFSET = 30;
- private static final int COMMENT_LENGTH_OFFSET = 32;
- private static final int LOCAL_FILE_HEADER_OFFSET = 42;
+ private static final int LAST_MODIFICATION_TIME_OFFSET = 12;
+ private static final int LOCAL_FILE_HEADER_OFFSET_OFFSET = 42;
private static final int NAME_OFFSET = HEADER_SIZE_BYTES;
- private final short mGpFlags;
- private final short mCompressionMethod;
+ private final ByteBuffer mData;
+ private final int mLastModificationTime;
+ private final int mLastModificationDate;
private final long mCrc32;
private final long mCompressedSize;
private final long mUncompressedSize;
private final long mLocalFileHeaderOffset;
private final String mName;
+ private final int mNameSizeBytes;
private CentralDirectoryRecord(
- short gpFlags,
- short compressionMethod,
+ ByteBuffer data,
+ int lastModificationTime,
+ int lastModificationDate,
long crc32,
long compressedSize,
long uncompressedSize,
long localFileHeaderOffset,
- String name) {
- mGpFlags = gpFlags;
- mCompressionMethod = compressionMethod;
+ String name,
+ int nameSizeBytes) {
+ mData = data;
+ mLastModificationDate = lastModificationDate;
+ mLastModificationTime = lastModificationTime;
mCrc32 = crc32;
mCompressedSize = compressedSize;
mUncompressedSize = uncompressedSize;
mLocalFileHeaderOffset = localFileHeaderOffset;
mName = name;
+ mNameSizeBytes = nameSizeBytes;
+ }
+
+ public int getSize() {
+ return mData.remaining();
}
public String getName() {
return mName;
}
- public short getGpFlags() {
- return mGpFlags;
+ public int getNameSizeBytes() {
+ return mNameSizeBytes;
+ }
+
+ public int getLastModificationTime() {
+ return mLastModificationTime;
}
- public short getCompressionMethod() {
- return mCompressionMethod;
+ public int getLastModificationDate() {
+ return mLastModificationDate;
}
public long getCrc32() {
@@ -114,24 +122,25 @@ public class CentralDirectoryRecord {
+ " bytes, available: " + buf.remaining() + " bytes",
new BufferUnderflowException());
}
- int bufPosition = buf.position();
- int recordSignature = buf.getInt(bufPosition);
+ int originalPosition = buf.position();
+ int recordSignature = buf.getInt();
if (recordSignature != RECORD_SIGNATURE) {
throw new ZipFormatException(
"Not a Central Directory record. Signature: 0x"
+ Long.toHexString(recordSignature & 0xffffffffL));
}
- short gpFlags = buf.getShort(bufPosition + GP_FLAGS_OFFSET);
- short compressionMethod = buf.getShort(bufPosition + COMPRESSION_METHOD_OFFSET);
- long crc32 = ZipUtils.getUnsignedInt32(buf, bufPosition + CRC32_OFFSET);
- long compressedSize = ZipUtils.getUnsignedInt32(buf, bufPosition + COMPRESSED_SIZE_OFFSET);
- long uncompressedSize =
- ZipUtils.getUnsignedInt32(buf, bufPosition + UNCOMPRESSED_SIZE_OFFSET);
- int nameSize = ZipUtils.getUnsignedInt16(buf, bufPosition + NAME_LENGTH_OFFSET);
- int extraSize = ZipUtils.getUnsignedInt16(buf, bufPosition + EXTRA_LENGTH_OFFSET);
- int commentSize = ZipUtils.getUnsignedInt16(buf, bufPosition + COMMENT_LENGTH_OFFSET);
- long localFileHeaderOffset =
- ZipUtils.getUnsignedInt32(buf, bufPosition + LOCAL_FILE_HEADER_OFFSET);
+ buf.position(originalPosition + LAST_MODIFICATION_TIME_OFFSET);
+ int lastModificationTime = ZipUtils.getUnsignedInt16(buf);
+ int lastModificationDate = ZipUtils.getUnsignedInt16(buf);
+ long crc32 = ZipUtils.getUnsignedInt32(buf);
+ long compressedSize = ZipUtils.getUnsignedInt32(buf);
+ long uncompressedSize = ZipUtils.getUnsignedInt32(buf);
+ int nameSize = ZipUtils.getUnsignedInt16(buf);
+ int extraSize = ZipUtils.getUnsignedInt16(buf);
+ int commentSize = ZipUtils.getUnsignedInt16(buf);
+ buf.position(originalPosition + LOCAL_FILE_HEADER_OFFSET_OFFSET);
+ long localFileHeaderOffset = ZipUtils.getUnsignedInt32(buf);
+ buf.position(originalPosition);
int recordSize = HEADER_SIZE_BYTES + nameSize + extraSize + commentSize;
if (recordSize > buf.remaining()) {
throw new ZipFormatException(
@@ -139,16 +148,99 @@ public class CentralDirectoryRecord {
+ buf.remaining() + " bytes",
new BufferUnderflowException());
}
- String name = getName(buf, bufPosition + NAME_OFFSET, nameSize);
- buf.position(bufPosition + recordSize);
+ String name = getName(buf, originalPosition + NAME_OFFSET, nameSize);
+ buf.position(originalPosition);
+ int originalLimit = buf.limit();
+ int recordEndInBuf = originalPosition + recordSize;
+ ByteBuffer recordBuf;
+ try {
+ buf.limit(recordEndInBuf);
+ recordBuf = buf.slice();
+ } finally {
+ buf.limit(originalLimit);
+ }
+ // Consume this record
+ buf.position(recordEndInBuf);
+ return new CentralDirectoryRecord(
+ recordBuf,
+ lastModificationTime,
+ lastModificationDate,
+ crc32,
+ compressedSize,
+ uncompressedSize,
+ localFileHeaderOffset,
+ name,
+ nameSize);
+ }
+
+ public void copyTo(ByteBuffer output) {
+ output.put(mData.slice());
+ }
+
+ public CentralDirectoryRecord createWithModifiedLocalFileHeaderOffset(
+ long localFileHeaderOffset) {
+ ByteBuffer result = ByteBuffer.allocate(mData.remaining());
+ result.put(mData.slice());
+ result.flip();
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ ZipUtils.setUnsignedInt32(result, LOCAL_FILE_HEADER_OFFSET_OFFSET, localFileHeaderOffset);
+ return new CentralDirectoryRecord(
+ result,
+ mLastModificationTime,
+ mLastModificationDate,
+ mCrc32,
+ mCompressedSize,
+ mUncompressedSize,
+ localFileHeaderOffset,
+ mName,
+ mNameSizeBytes);
+ }
+
+ public static CentralDirectoryRecord createWithDeflateCompressedData(
+ String name,
+ int lastModifiedTime,
+ int lastModifiedDate,
+ long crc32,
+ long compressedSize,
+ long uncompressedSize,
+ long localFileHeaderOffset) {
+ 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); // Version made by
+ ZipUtils.putUnsignedInt16(result, 0x14); // Minimum version needed to extract
+ result.putShort(ZipUtils.GP_FLAG_EFS); // UTF-8 character encoding used for entry name
+ result.putShort(ZipUtils.COMPRESSION_METHOD_DEFLATED);
+ ZipUtils.putUnsignedInt16(result, lastModifiedTime);
+ ZipUtils.putUnsignedInt16(result, lastModifiedDate);
+ ZipUtils.putUnsignedInt32(result, crc32);
+ ZipUtils.putUnsignedInt32(result, compressedSize);
+ ZipUtils.putUnsignedInt32(result, uncompressedSize);
+ ZipUtils.putUnsignedInt16(result, nameBytes.length);
+ ZipUtils.putUnsignedInt16(result, 0); // Extra field length
+ ZipUtils.putUnsignedInt16(result, 0); // File comment length
+ ZipUtils.putUnsignedInt16(result, 0); // Disk number
+ ZipUtils.putUnsignedInt16(result, 0); // Internal file attributes
+ ZipUtils.putUnsignedInt32(result, 0); // External file attributes
+ ZipUtils.putUnsignedInt32(result, localFileHeaderOffset);
+ result.put(nameBytes);
+
+ if (result.hasRemaining()) {
+ throw new RuntimeException("pos: " + result.position() + ", limit: " + result.limit());
+ }
+ result.flip();
return new CentralDirectoryRecord(
- gpFlags,
- compressionMethod,
+ result,
+ lastModifiedTime,
+ lastModifiedDate,
crc32,
compressedSize,
uncompressedSize,
localFileHeaderOffset,
- name);
+ name,
+ nameBytes.length);
}
static String getName(ByteBuffer record, int position, int nameLengthBytes) {
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/EocdRecord.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/EocdRecord.java
new file mode 100644
index 0000000000..8777591300
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/EocdRecord.java
@@ -0,0 +1,48 @@
+/*
+ * 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.internal.zip;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * ZIP End of Central Directory record.
+ */
+public class EocdRecord {
+ private static final int CD_RECORD_COUNT_ON_DISK_OFFSET = 8;
+ private static final int CD_RECORD_COUNT_TOTAL_OFFSET = 10;
+ private static final int CD_SIZE_OFFSET = 12;
+ private static final int CD_OFFSET_OFFSET = 16;
+
+ public static ByteBuffer createWithModifiedCentralDirectoryInfo(
+ ByteBuffer original,
+ int centralDirectoryRecordCount,
+ long centralDirectorySizeBytes,
+ long centralDirectoryOffset) {
+ ByteBuffer result = ByteBuffer.allocate(original.remaining());
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.put(original.slice());
+ result.flip();
+ ZipUtils.setUnsignedInt16(
+ result, CD_RECORD_COUNT_ON_DISK_OFFSET, centralDirectoryRecordCount);
+ ZipUtils.setUnsignedInt16(
+ result, CD_RECORD_COUNT_TOTAL_OFFSET, centralDirectoryRecordCount);
+ ZipUtils.setUnsignedInt32(result, CD_SIZE_OFFSET, centralDirectorySizeBytes);
+ ZipUtils.setUnsignedInt32(result, CD_OFFSET_OFFSET, centralDirectoryOffset);
+ return result;
+ }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileHeader.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileHeader.java
deleted file mode 100644
index 99a606b4ae..0000000000
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileHeader.java
+++ /dev/null
@@ -1,282 +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.apksigner.core.internal.zip;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.charset.StandardCharsets;
-import java.util.zip.DataFormatException;
-import java.util.zip.Inflater;
-
-import com.android.apksigner.core.internal.util.ByteBufferSink;
-import com.android.apksigner.core.util.DataSink;
-import com.android.apksigner.core.util.DataSource;
-import com.android.apksigner.core.zip.ZipFormatException;
-
-/**
- * ZIP Local File Header.
- */
-public class LocalFileHeader {
- 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 short GP_FLAG_DATA_DESCRIPTOR_USED = 0x08;
-
- private LocalFileHeader() {}
-
- /**
- * Returns the uncompressed data pointed to by the provided ZIP Central Directory (CD) record.
- */
- public static byte[] getUncompressedData(
- DataSource source,
- long sourceOffsetInArchive,
- 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);
- sendUncompressedData(
- source,
- sourceOffsetInArchive,
- cdRecord,
- cdStartOffsetInArchive,
- resultSink);
- if (resultBuf.hasRemaining()) {
- throw new ZipFormatException(
- "Data of " + cdRecord.getName() + " shorter than specified in Central Directory"
- + ". Expected: " + result.length + " bytes, read: "
- + resultBuf.position() + " bytes");
- }
- return result;
- }
-
- /**
- * Sends the uncompressed data pointed to by the provided ZIP Central Directory (CD) record into
- * the provided data sink.
- */
- public static void sendUncompressedData(
- DataSource source,
- long sourceOffsetInArchive,
- 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.
-
- String entryName = cdRecord.getName();
- byte[] cdNameBytes = entryName.getBytes(StandardCharsets.UTF_8);
- int headerSizeWithName = HEADER_SIZE_BYTES + cdNameBytes.length;
- long localFileHeaderOffsetInArchive = cdRecord.getLocalFileHeaderOffset();
- long headerEndInArchive = localFileHeaderOffsetInArchive + headerSizeWithName;
- if (headerEndInArchive >= cdStartOffsetInArchive) {
- throw new ZipFormatException(
- "Local File Header of " + entryName + " extends beyond start of Central"
- + " Directory. LFH end: " + headerEndInArchive
- + ", CD start: " + cdStartOffsetInArchive);
- }
- ByteBuffer header;
- try {
- header =
- source.getByteBuffer(
- localFileHeaderOffsetInArchive - sourceOffsetInArchive,
- 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(0);
- 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);
- if ((gpFlags & GP_FLAG_DATA_DESCRIPTOR_USED) == 0) {
- long crc32 = ZipUtils.getUnsignedInt32(header, CRC32_OFFSET);
- if (crc32 != cdRecord.getCrc32()) {
- throw new ZipFormatException(
- "CRC-32 mismatch between Local File Header and Central Directory for entry "
- + entryName + ". LFH: " + crc32 + ", CD: " + cdRecord.getCrc32());
- }
- long compressedSize = ZipUtils.getUnsignedInt32(header, COMPRESSED_SIZE_OFFSET);
- if (compressedSize != cdRecord.getCompressedSize()) {
- throw new ZipFormatException(
- "Compressed size mismatch between Local File Header and Central Directory"
- + " for entry " + entryName + ". LFH: " + compressedSize
- + ", CD: " + cdRecord.getCompressedSize());
- }
- long uncompressedSize = ZipUtils.getUnsignedInt32(header, UNCOMPRESSED_SIZE_OFFSET);
- if (uncompressedSize != cdRecord.getUncompressedSize()) {
- throw new ZipFormatException(
- "Uncompressed size mismatch between Local File Header and Central Directory"
- + " for entry " + entryName + ". LFH: " + uncompressedSize
- + ", CD: " + cdRecord.getUncompressedSize());
- }
- }
- int nameLength = ZipUtils.getUnsignedInt16(header, NAME_LENGTH_OFFSET);
- if (nameLength > cdNameBytes.length) {
- throw new ZipFormatException(
- "Name mismatch between Local File Header and Central Directory for entry"
- + entryName + ". LFH: " + nameLength
- + " bytes, CD: " + cdNameBytes.length + " 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 dataStartOffsetInArchive =
- localFileHeaderOffsetInArchive + HEADER_SIZE_BYTES + nameLength + extraLength;
- long dataSize;
- if (compressed) {
- dataSize = cdRecord.getCompressedSize();
- } else {
- dataSize = cdRecord.getUncompressedSize();
- }
- long dataEndOffsetInArchive = dataStartOffsetInArchive + dataSize;
- if (dataEndOffsetInArchive > cdStartOffsetInArchive) {
- throw new ZipFormatException(
- "Local File Header data of " + entryName + " extends beyond Central Directory"
- + ". LFH data start: " + dataStartOffsetInArchive
- + ", LFH data end: " + dataEndOffsetInArchive
- + ", CD start: " + cdStartOffsetInArchive);
- }
-
- long dataOffsetInSource = dataStartOffsetInArchive - sourceOffsetInArchive;
- try {
- if (compressed) {
- try (InflateSinkAdapter inflateAdapter = new InflateSinkAdapter(sink)) {
- source.feed(dataOffsetInSource, dataSize, inflateAdapter);
- }
- } else {
- source.feed(dataOffsetInSource, dataSize, sink);
- }
- } catch (IOException e) {
- throw new IOException(
- "Failed to read data of " + ((compressed) ? "compressed" : "uncompressed")
- + " entry " + entryName,
- e);
- }
- // Interestingly, Android doesn't check that uncompressed data's CRC-32 is as expected. We
- // thus don't check either.
- }
-
- private static class InflateSinkAdapter implements DataSink, Closeable {
- private final DataSink mDelegate;
-
- private Inflater mInflater = new Inflater(true);
- private byte[] mOutputBuffer;
- private byte[] mInputBuffer;
- 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);
- mDelegate.consume(ByteBuffer.wrap(mOutputBuffer, 0, 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);
- }
- }
- }
-
- @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/LocalFileRecord.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileRecord.java
new file mode 100644
index 0000000000..397a450080
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileRecord.java
@@ -0,0 +1,540 @@
+/*
+ * 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.internal.zip;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+
+import com.android.apksigner.core.internal.util.ByteBufferSink;
+import com.android.apksigner.core.util.DataSink;
+import com.android.apksigner.core.util.DataSource;
+import com.android.apksigner.core.zip.ZipFormatException;
+
+/**
+ * ZIP Local File record.
+ *
+ * <p>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 118585a9a3..6a0c501e91 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 8ee1f1388b..4aefedb05b 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);
+ }
}