Merge \"APK signer primitive.\"
am: d81beca2b2
Change-Id: I9570df7b5f7a70b4fdd04cbbdeae80d3e5bf9616
This commit is contained in:
@@ -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>
|
* <p><h3>Operating Model</h3>
|
||||||
*
|
*
|
||||||
* The abstract operating model is that there is an input APK which is being signed, thus producing
|
* 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
|
* an output APK. In reality, there may be just an output APK being built from scratch, or the input
|
||||||
* the output APK may be the same file. Because this engine does not deal with reading and writing
|
* APK and the output APK may be the same file. Because this engine does not deal with reading and
|
||||||
* files, it can handle all of these scenarios.
|
* 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
|
* <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.
|
* 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
|
* @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.
|
* 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
|
* @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.
|
* 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.InclusiveIntRange;
|
||||||
import com.android.apksigner.core.internal.util.MessageDigestSink;
|
import com.android.apksigner.core.internal.util.MessageDigestSink;
|
||||||
import com.android.apksigner.core.internal.zip.CentralDirectoryRecord;
|
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.util.DataSource;
|
||||||
import com.android.apksigner.core.zip.ZipFormatException;
|
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.
|
// Parse the JAR manifest and check that all JAR entries it references exist in the APK.
|
||||||
byte[] manifestBytes =
|
byte[] manifestBytes =
|
||||||
LocalFileHeader.getUncompressedData(
|
LocalFileRecord.getUncompressedData(apk, manifestEntry, cdStartOffset);
|
||||||
apk, 0,
|
|
||||||
manifestEntry,
|
|
||||||
cdStartOffset);
|
|
||||||
Map<String, ManifestParser.Section> entryNameToManifestSection = null;
|
Map<String, ManifestParser.Section> entryNameToManifestSection = null;
|
||||||
ManifestParser manifest = new ManifestParser(manifestBytes);
|
ManifestParser manifest = new ManifestParser(manifestBytes);
|
||||||
ManifestParser.Section manifestMainSection = manifest.readSection();
|
ManifestParser.Section manifestMainSection = manifest.readSection();
|
||||||
@@ -411,15 +408,9 @@ public abstract class V1SchemeVerifier {
|
|||||||
DataSource apk, long cdStartOffset, int minSdkVersion, int maxSdkVersion)
|
DataSource apk, long cdStartOffset, int minSdkVersion, int maxSdkVersion)
|
||||||
throws IOException, ZipFormatException, NoSuchAlgorithmException {
|
throws IOException, ZipFormatException, NoSuchAlgorithmException {
|
||||||
byte[] sigBlockBytes =
|
byte[] sigBlockBytes =
|
||||||
LocalFileHeader.getUncompressedData(
|
LocalFileRecord.getUncompressedData(apk, mSignatureBlockEntry, cdStartOffset);
|
||||||
apk, 0,
|
|
||||||
mSignatureBlockEntry,
|
|
||||||
cdStartOffset);
|
|
||||||
mSigFileBytes =
|
mSigFileBytes =
|
||||||
LocalFileHeader.getUncompressedData(
|
LocalFileRecord.getUncompressedData(apk, mSignatureFileEntry, cdStartOffset);
|
||||||
apk, 0,
|
|
||||||
mSignatureFileEntry,
|
|
||||||
cdStartOffset);
|
|
||||||
PKCS7 sigBlock;
|
PKCS7 sigBlock;
|
||||||
try {
|
try {
|
||||||
sigBlock = new PKCS7(sigBlockBytes);
|
sigBlock = new PKCS7(sigBlockBytes);
|
||||||
@@ -1412,8 +1403,8 @@ public abstract class V1SchemeVerifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
LocalFileHeader.sendUncompressedData(
|
LocalFileRecord.outputUncompressedData(
|
||||||
apk, 0,
|
apk,
|
||||||
cdRecord,
|
cdRecord,
|
||||||
cdOffsetInApk,
|
cdOffsetInApk,
|
||||||
new MessageDigestSink(mds));
|
new MessageDigestSink(mds));
|
||||||
|
@@ -553,6 +553,42 @@ public abstract class V2SchemeVerifier {
|
|||||||
private static SignatureInfo findSignature(
|
private static SignatureInfo findSignature(
|
||||||
DataSource apk, ApkUtils.ZipSections zipSections, Result result)
|
DataSource apk, ApkUtils.ZipSections zipSections, Result result)
|
||||||
throws IOException, SignatureNotFoundException {
|
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 centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
|
||||||
long centralDirEndOffset =
|
long centralDirEndOffset =
|
||||||
centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
|
centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
|
||||||
@@ -564,43 +600,15 @@ public abstract class V2SchemeVerifier {
|
|||||||
+ ", EoCD start: " + eocdStartOffset);
|
+ ", EoCD start: " + eocdStartOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the APK Signing Block. The block immediately precedes the Central Directory.
|
if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) {
|
||||||
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) {
|
|
||||||
throw new SignatureNotFoundException(
|
throw new SignatureNotFoundException(
|
||||||
"APK too small for APK Signing Block. ZIP Central Directory offset: "
|
"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:
|
// Read the magic and offset in file from the footer section of the block:
|
||||||
// * uint64: size of block
|
// * uint64: size of block
|
||||||
// * 16 bytes: magic
|
// * 16 bytes: magic
|
||||||
ByteBuffer footer = apk.getByteBuffer(centralDirOffset - 24, 24);
|
ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24);
|
||||||
footer.order(ByteOrder.LITTLE_ENDIAN);
|
footer.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
|
if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
|
||||||
|| (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
|
|| (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
|
||||||
@@ -615,12 +623,12 @@ public abstract class V2SchemeVerifier {
|
|||||||
"APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
|
"APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
|
||||||
}
|
}
|
||||||
int totalSize = (int) (apkSigBlockSizeInFooter + 8);
|
int totalSize = (int) (apkSigBlockSizeInFooter + 8);
|
||||||
long apkSigBlockOffset = centralDirOffset - totalSize;
|
long apkSigBlockOffset = centralDirStartOffset - totalSize;
|
||||||
if (apkSigBlockOffset < 0) {
|
if (apkSigBlockOffset < 0) {
|
||||||
throw new SignatureNotFoundException(
|
throw new SignatureNotFoundException(
|
||||||
"APK Signing Block offset out of range: " + apkSigBlockOffset);
|
"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);
|
apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
|
long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
|
||||||
if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
|
if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
|
||||||
@@ -628,7 +636,7 @@ public abstract class V2SchemeVerifier {
|
|||||||
"APK Signing Block sizes in header and footer do not match: "
|
"APK Signing Block sizes in header and footer do not match: "
|
||||||
+ apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
|
+ apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
|
||||||
}
|
}
|
||||||
return Pair.of(apkSigBlock, apkSigBlockOffset);
|
return Pair.of(apk.slice(apkSigBlockOffset, totalSize), apkSigBlockOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ByteBuffer findApkSignatureSchemeV2Block(
|
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.BufferUnderflowException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
|
||||||
@@ -38,52 +39,59 @@ public class CentralDirectoryRecord {
|
|||||||
private static final int RECORD_SIGNATURE = 0x02014b50;
|
private static final int RECORD_SIGNATURE = 0x02014b50;
|
||||||
private static final int HEADER_SIZE_BYTES = 46;
|
private static final int HEADER_SIZE_BYTES = 46;
|
||||||
|
|
||||||
private static final int GP_FLAGS_OFFSET = 8;
|
private static final int LAST_MODIFICATION_TIME_OFFSET = 12;
|
||||||
private static final int COMPRESSION_METHOD_OFFSET = 10;
|
private static final int LOCAL_FILE_HEADER_OFFSET_OFFSET = 42;
|
||||||
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 NAME_OFFSET = HEADER_SIZE_BYTES;
|
private static final int NAME_OFFSET = HEADER_SIZE_BYTES;
|
||||||
|
|
||||||
private final short mGpFlags;
|
private final ByteBuffer mData;
|
||||||
private final short mCompressionMethod;
|
private final int mLastModificationTime;
|
||||||
|
private final int mLastModificationDate;
|
||||||
private final long mCrc32;
|
private final long mCrc32;
|
||||||
private final long mCompressedSize;
|
private final long mCompressedSize;
|
||||||
private final long mUncompressedSize;
|
private final long mUncompressedSize;
|
||||||
private final long mLocalFileHeaderOffset;
|
private final long mLocalFileHeaderOffset;
|
||||||
private final String mName;
|
private final String mName;
|
||||||
|
private final int mNameSizeBytes;
|
||||||
|
|
||||||
private CentralDirectoryRecord(
|
private CentralDirectoryRecord(
|
||||||
short gpFlags,
|
ByteBuffer data,
|
||||||
short compressionMethod,
|
int lastModificationTime,
|
||||||
|
int lastModificationDate,
|
||||||
long crc32,
|
long crc32,
|
||||||
long compressedSize,
|
long compressedSize,
|
||||||
long uncompressedSize,
|
long uncompressedSize,
|
||||||
long localFileHeaderOffset,
|
long localFileHeaderOffset,
|
||||||
String name) {
|
String name,
|
||||||
mGpFlags = gpFlags;
|
int nameSizeBytes) {
|
||||||
mCompressionMethod = compressionMethod;
|
mData = data;
|
||||||
|
mLastModificationDate = lastModificationDate;
|
||||||
|
mLastModificationTime = lastModificationTime;
|
||||||
mCrc32 = crc32;
|
mCrc32 = crc32;
|
||||||
mCompressedSize = compressedSize;
|
mCompressedSize = compressedSize;
|
||||||
mUncompressedSize = uncompressedSize;
|
mUncompressedSize = uncompressedSize;
|
||||||
mLocalFileHeaderOffset = localFileHeaderOffset;
|
mLocalFileHeaderOffset = localFileHeaderOffset;
|
||||||
mName = name;
|
mName = name;
|
||||||
|
mNameSizeBytes = nameSizeBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSize() {
|
||||||
|
return mData.remaining();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return mName;
|
return mName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public short getGpFlags() {
|
public int getNameSizeBytes() {
|
||||||
return mGpFlags;
|
return mNameSizeBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public short getCompressionMethod() {
|
public int getLastModificationTime() {
|
||||||
return mCompressionMethod;
|
return mLastModificationTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLastModificationDate() {
|
||||||
|
return mLastModificationDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getCrc32() {
|
public long getCrc32() {
|
||||||
@@ -114,24 +122,25 @@ public class CentralDirectoryRecord {
|
|||||||
+ " bytes, available: " + buf.remaining() + " bytes",
|
+ " bytes, available: " + buf.remaining() + " bytes",
|
||||||
new BufferUnderflowException());
|
new BufferUnderflowException());
|
||||||
}
|
}
|
||||||
int bufPosition = buf.position();
|
int originalPosition = buf.position();
|
||||||
int recordSignature = buf.getInt(bufPosition);
|
int recordSignature = buf.getInt();
|
||||||
if (recordSignature != RECORD_SIGNATURE) {
|
if (recordSignature != RECORD_SIGNATURE) {
|
||||||
throw new ZipFormatException(
|
throw new ZipFormatException(
|
||||||
"Not a Central Directory record. Signature: 0x"
|
"Not a Central Directory record. Signature: 0x"
|
||||||
+ Long.toHexString(recordSignature & 0xffffffffL));
|
+ Long.toHexString(recordSignature & 0xffffffffL));
|
||||||
}
|
}
|
||||||
short gpFlags = buf.getShort(bufPosition + GP_FLAGS_OFFSET);
|
buf.position(originalPosition + LAST_MODIFICATION_TIME_OFFSET);
|
||||||
short compressionMethod = buf.getShort(bufPosition + COMPRESSION_METHOD_OFFSET);
|
int lastModificationTime = ZipUtils.getUnsignedInt16(buf);
|
||||||
long crc32 = ZipUtils.getUnsignedInt32(buf, bufPosition + CRC32_OFFSET);
|
int lastModificationDate = ZipUtils.getUnsignedInt16(buf);
|
||||||
long compressedSize = ZipUtils.getUnsignedInt32(buf, bufPosition + COMPRESSED_SIZE_OFFSET);
|
long crc32 = ZipUtils.getUnsignedInt32(buf);
|
||||||
long uncompressedSize =
|
long compressedSize = ZipUtils.getUnsignedInt32(buf);
|
||||||
ZipUtils.getUnsignedInt32(buf, bufPosition + UNCOMPRESSED_SIZE_OFFSET);
|
long uncompressedSize = ZipUtils.getUnsignedInt32(buf);
|
||||||
int nameSize = ZipUtils.getUnsignedInt16(buf, bufPosition + NAME_LENGTH_OFFSET);
|
int nameSize = ZipUtils.getUnsignedInt16(buf);
|
||||||
int extraSize = ZipUtils.getUnsignedInt16(buf, bufPosition + EXTRA_LENGTH_OFFSET);
|
int extraSize = ZipUtils.getUnsignedInt16(buf);
|
||||||
int commentSize = ZipUtils.getUnsignedInt16(buf, bufPosition + COMMENT_LENGTH_OFFSET);
|
int commentSize = ZipUtils.getUnsignedInt16(buf);
|
||||||
long localFileHeaderOffset =
|
buf.position(originalPosition + LOCAL_FILE_HEADER_OFFSET_OFFSET);
|
||||||
ZipUtils.getUnsignedInt32(buf, bufPosition + LOCAL_FILE_HEADER_OFFSET);
|
long localFileHeaderOffset = ZipUtils.getUnsignedInt32(buf);
|
||||||
|
buf.position(originalPosition);
|
||||||
int recordSize = HEADER_SIZE_BYTES + nameSize + extraSize + commentSize;
|
int recordSize = HEADER_SIZE_BYTES + nameSize + extraSize + commentSize;
|
||||||
if (recordSize > buf.remaining()) {
|
if (recordSize > buf.remaining()) {
|
||||||
throw new ZipFormatException(
|
throw new ZipFormatException(
|
||||||
@@ -139,16 +148,99 @@ public class CentralDirectoryRecord {
|
|||||||
+ buf.remaining() + " bytes",
|
+ buf.remaining() + " bytes",
|
||||||
new BufferUnderflowException());
|
new BufferUnderflowException());
|
||||||
}
|
}
|
||||||
String name = getName(buf, bufPosition + NAME_OFFSET, nameSize);
|
String name = getName(buf, originalPosition + NAME_OFFSET, nameSize);
|
||||||
buf.position(bufPosition + recordSize);
|
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(
|
return new CentralDirectoryRecord(
|
||||||
gpFlags,
|
recordBuf,
|
||||||
compressionMethod,
|
lastModificationTime,
|
||||||
|
lastModificationDate,
|
||||||
crc32,
|
crc32,
|
||||||
compressedSize,
|
compressedSize,
|
||||||
uncompressedSize,
|
uncompressedSize,
|
||||||
localFileHeaderOffset,
|
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) {
|
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;
|
package com.android.apksigner.core.internal.zip;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
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.internal.util.Pair;
|
||||||
import com.android.apksigner.core.util.DataSource;
|
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_STORED = 0;
|
||||||
public static final short COMPRESSION_METHOD_DEFLATED = 8;
|
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_MIN_SIZE = 22;
|
||||||
private static final int ZIP_EOCD_REC_SIG = 0x06054b50;
|
private static final int ZIP_EOCD_REC_SIG = 0x06054b50;
|
||||||
private static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10;
|
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;
|
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)) {
|
if ((value < 0) || (value > 0xffffffffL)) {
|
||||||
throw new IllegalArgumentException("uint32 value of out range: " + value);
|
throw new IllegalArgumentException("uint32 value of out range: " + value);
|
||||||
}
|
}
|
||||||
buffer.putInt(offset, (int) 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) {
|
static long getUnsignedInt32(ByteBuffer buffer, int offset) {
|
||||||
return buffer.getInt(offset) & 0xffffffffL;
|
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;
|
package com.android.apksigner.core.util;
|
||||||
|
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
|
||||||
import com.android.apksigner.core.internal.util.OutputStreamDataSink;
|
import com.android.apksigner.core.internal.util.OutputStreamDataSink;
|
||||||
|
import com.android.apksigner.core.internal.util.RandomAccessFileDataSink;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility methods for working with {@link DataSink} abstraction.
|
* Utility methods for working with {@link DataSink} abstraction.
|
||||||
@@ -33,4 +35,12 @@ public abstract class DataSinks {
|
|||||||
public static DataSink asDataSink(OutputStream out) {
|
public static DataSink asDataSink(OutputStream out) {
|
||||||
return new OutputStreamDataSink(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user