Merge "APK signer primitive."
This commit is contained in:
commit
d81beca2b2
11 changed files with 1655 additions and 374 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue