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 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 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 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 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 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 parseZipCentralDirectory( + ByteBuffer cd, + ApkUtils.ZipSections apkSections) throws ZipFormatException { + long cdOffset = apkSections.getZipCentralDirectoryOffset(); + int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount(); + List cdRecords = new ArrayList<>(expectedCdRecordCount); + Set entryNames = new HashSet<>(expectedCdRecordCount); + for (int i = 0; i < expectedCdRecordCount; i++) { + CentralDirectoryRecord cdRecord; + int offsetInsideCd = cd.position(); + try { + cdRecord = CentralDirectoryRecord.getRecord(cd); + } catch (ZipFormatException e) { + throw new 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. + * + *

The following information is required to construct a working {@code ApkSigner}: + *

+ */ + 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. * *

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 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 0c303ee3d..5e1e8fb74 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,6 +553,42 @@ public abstract class V2SchemeVerifier { private static SignatureInfo findSignature( DataSource apk, ApkUtils.ZipSections zipSections, Result result) throws IOException, SignatureNotFoundException { + // Find the APK Signing Block. The block immediately precedes the Central Directory. + ByteBuffer eocd = zipSections.getZipEndOfCentralDirectory(); + Pair 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(apkSigningBlockBuf, result); + + return new SignatureInfo( + apkSignatureSchemeV2Block, + apkSigningBlockOffset, + zipSections.getZipCentralDirectoryOffset(), + zipSections.getZipEndOfCentralDirectoryOffset(), + eocd); + } + + /** + * 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 findApkSigningBlock( + DataSource apk, ApkUtils.ZipSections zipSections) + throws IOException, SignatureNotFoundException { + // FORMAT: + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint64: size in bytes (excluding this field) + // * @+8 bytes payload + // * @-24 bytes uint64: size in bytes (same as the one above) + // * @-16 bytes uint128: magic + long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset(); long centralDirEndOffset = centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes(); @@ -564,43 +600,15 @@ public abstract class V2SchemeVerifier { + ", EoCD start: " + eocdStartOffset); } - // Find the APK Signing Block. The block immediately precedes the Central Directory. - ByteBuffer eocd = zipSections.getZipEndOfCentralDirectory(); - Pair apkSigningBlockAndOffset = - findApkSigningBlock(apk, centralDirStartOffset); - ByteBuffer apkSigningBlock = apkSigningBlockAndOffset.getFirst(); - long apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond(); - - // Find the APK Signature Scheme v2 Block inside the APK Signing Block. - ByteBuffer apkSignatureSchemeV2Block = - findApkSignatureSchemeV2Block(apkSigningBlock, result); - - return new SignatureInfo( - apkSignatureSchemeV2Block, - apkSigningBlockOffset, - centralDirStartOffset, - eocdStartOffset, - eocd); - } - - private static Pair findApkSigningBlock( - DataSource apk, long centralDirOffset) throws IOException, SignatureNotFoundException { - // FORMAT: - // OFFSET DATA TYPE DESCRIPTION - // * @+0 bytes uint64: size in bytes (excluding this field) - // * @+8 bytes payload - // * @-24 bytes uint64: size in bytes (same as the one above) - // * @-16 bytes uint128: magic - - if (centralDirOffset < APK_SIG_BLOCK_MIN_SIZE) { + 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 000000000..219849228 --- /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 6a5b94c4e..141d01e1c 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 short getCompressionMethod() { - return mCompressionMethod; + public int getLastModificationTime() { + return mLastModificationTime; + } + + 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( - gpFlags, - compressionMethod, + recordBuf, + lastModificationTime, + lastModificationDate, crc32, compressedSize, uncompressedSize, localFileHeaderOffset, - name); + 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( + result, + lastModifiedTime, + lastModifiedDate, + crc32, + compressedSize, + uncompressedSize, + localFileHeaderOffset, + 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 000000000..877759130 --- /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 99a606b4a..000000000 --- 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 000000000..397a45008 --- /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. + * + *

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); + } }