Merge "APK signer primitive."

This commit is contained in:
Treehugger Robot 2016-07-06 22:59:32 +00:00 committed by Gerrit Code Review
commit d81beca2b2
11 changed files with 1655 additions and 374 deletions

View file

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

View file

@ -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.

View file

@ -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));

View file

@ -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<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(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<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)
// * @+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<ByteBuffer, Long> 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<ByteBuffer, Long> 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(

View file

@ -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;
}
}
}

View file

@ -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) {

View file

@ -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;
}
}

View file

@ -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");
}
}
}
}

View file

@ -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");
}
}
}
}

View file

@ -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;
}
}
}

View file

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