forked from openkylin/platform_build
Merge \\\\"Move apksigner library to tools/apksig.\\\\" am: c78147c650
am: ee7d7e3506
am: 188a4191d9
am: 7f1782342f
Change-Id: I47cb4584ebe489028af7c0c8457bcc73a30315de
This commit is contained in:
commit
da31196c6e
|
@ -1,19 +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.
|
|
||||||
#
|
|
||||||
|
|
||||||
LOCAL_PATH := $(call my-dir)
|
|
||||||
|
|
||||||
include $(call all-makefiles-under,$(LOCAL_PATH))
|
|
|
@ -1,29 +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.
|
|
||||||
#
|
|
||||||
LOCAL_PATH := $(call my-dir)
|
|
||||||
|
|
||||||
# apksigner library, for signing APKs and verification signatures of APKs
|
|
||||||
# ============================================================
|
|
||||||
include $(CLEAR_VARS)
|
|
||||||
LOCAL_MODULE := apksigner-core
|
|
||||||
LOCAL_SRC_FILES := $(call all-java-files-under, src)
|
|
||||||
|
|
||||||
# Disable warnnings about our use of internal proprietary OpenJDK API.
|
|
||||||
# TODO: Remove this workaround by moving to our own implementation of PKCS #7
|
|
||||||
# SignedData block generation, parsing, and verification.
|
|
||||||
LOCAL_JAVACFLAGS := -XDignore.symbol.file
|
|
||||||
|
|
||||||
include $(BUILD_HOST_JAVA_LIBRARY)
|
|
|
@ -1,711 +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;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,417 +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;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.SignatureException;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.android.apksigner.core.util.DataSink;
|
|
||||||
import com.android.apksigner.core.util.DataSource;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APK signing logic which is independent of how input and output APKs are stored, parsed, and
|
|
||||||
* generated.
|
|
||||||
*
|
|
||||||
* <p><h3>Operating Model</h3>
|
|
||||||
*
|
|
||||||
* The abstract operating model is that there is an input APK which is being signed, thus producing
|
|
||||||
* an output APK. In reality, there may be just an output APK being built from scratch, or the input
|
|
||||||
* APK and the output APK may be the same file. Because this engine does not deal with reading and
|
|
||||||
* writing files, it can handle all of these scenarios.
|
|
||||||
*
|
|
||||||
* <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.
|
|
||||||
* This may be more efficient than signing the APK using a new instance of the engine. See
|
|
||||||
* <a href="#incremental">Incremental Operation</a>.
|
|
||||||
*
|
|
||||||
* <p>In the engine's operating model, a signed APK is produced as follows.
|
|
||||||
* <ol>
|
|
||||||
* <li>JAR entries to be signed are output,</li>
|
|
||||||
* <li>JAR archive is signed using JAR signing, thus adding the so-called v1 signature to the
|
|
||||||
* output,</li>
|
|
||||||
* <li>JAR archive is signed using APK Signature Scheme v2, thus adding the so-called v2 signature
|
|
||||||
* to the output.</li>
|
|
||||||
* </ol>
|
|
||||||
*
|
|
||||||
* <p>The input APK may contain JAR entries which, depending on the engine's configuration, may or
|
|
||||||
* may not be output (e.g., existing signatures may need to be preserved or stripped) or which the
|
|
||||||
* engine will overwrite as part of signing. The engine thus offers {@link #inputJarEntry(String)}
|
|
||||||
* which tells the client whether the input JAR entry needs to be output. This avoids the need for
|
|
||||||
* the client to hard-code the aspects of APK signing which determine which parts of input must be
|
|
||||||
* ignored. Similarly, the engine offers {@link #inputApkSigningBlock(DataSource)} to help the
|
|
||||||
* client avoid dealing with preserving or stripping APK Signature Scheme v2 signature of the input
|
|
||||||
* APK.
|
|
||||||
*
|
|
||||||
* <p>To use the engine to sign an input APK (or a collection of JAR entries), follow these
|
|
||||||
* steps:
|
|
||||||
* <ol>
|
|
||||||
* <li>Obtain a new instance of the engine -- engine instances are stateful and thus cannot be used
|
|
||||||
* for signing multiple APKs.</li>
|
|
||||||
* <li>Locate the input APK's APK Signing Block and provide it to
|
|
||||||
* {@link #inputApkSigningBlock(DataSource)}.</li>
|
|
||||||
* <li>For each JAR entry in the input APK, invoke {@link #inputJarEntry(String)} to determine
|
|
||||||
* whether this entry should be output. The engine may request to inspect the entry.</li>
|
|
||||||
* <li>For each output JAR entry, invoke {@link #outputJarEntry(String)} which may request to
|
|
||||||
* inspect the entry.</li>
|
|
||||||
* <li>Once all JAR entries have been output, invoke {@link #outputJarEntries()} which may request
|
|
||||||
* that additional JAR entries are output. These entries comprise the output APK's JAR
|
|
||||||
* signature.</li>
|
|
||||||
* <li>Locate the ZIP Central Directory and ZIP End of Central Directory sections in the output and
|
|
||||||
* invoke {@link #outputZipSections(DataSource, DataSource, DataSource)} which may request that
|
|
||||||
* an APK Signature Block is inserted before the ZIP Central Directory. The block contains the
|
|
||||||
* output APK's APK Signature Scheme v2 signature.</li>
|
|
||||||
* <li>Invoke {@link #outputDone()} to signal that the APK was output in full. The engine will
|
|
||||||
* confirm that the output APK is signed.</li>
|
|
||||||
* <li>Invoke {@link #close()} to signal that the engine will no longer be used. This lets the
|
|
||||||
* engine free any resources it no longer needs.
|
|
||||||
* </ol>
|
|
||||||
*
|
|
||||||
* <p>Some invocations of the engine may provide the client with a task to perform. The client is
|
|
||||||
* expected to perform all requested tasks before proceeding to the next stage of signing. See
|
|
||||||
* documentation of each method about the deadlines for performing the tasks requested by the
|
|
||||||
* method.
|
|
||||||
*
|
|
||||||
* <p><h3 id="incremental">Incremental Operation</h3></a>
|
|
||||||
*
|
|
||||||
* The engine supports incremental operation where a signed APK is produced, then modified and
|
|
||||||
* re-signed. This may be useful for IDEs, where an app is frequently re-signed after small changes
|
|
||||||
* by the developer. Re-signing may be more efficient than signing from scratch.
|
|
||||||
*
|
|
||||||
* <p>To use the engine in incremental mode, keep notifying the engine of changes to the APK through
|
|
||||||
* {@link #inputApkSigningBlock(DataSource)}, {@link #inputJarEntry(String)},
|
|
||||||
* {@link #inputJarEntryRemoved(String)}, {@link #outputJarEntry(String)},
|
|
||||||
* and {@link #outputJarEntryRemoved(String)}, perform the tasks requested by the engine through
|
|
||||||
* these methods, and, when a new signed APK is desired, run through steps 5 onwards to re-sign the
|
|
||||||
* APK.
|
|
||||||
*
|
|
||||||
* <p><h3>Output-only Operation</h3>
|
|
||||||
*
|
|
||||||
* The engine's abstract operating model consists of an input APK and an output APK. However, it is
|
|
||||||
* possible to use the engine in output-only mode where the engine's {@code input...} methods are
|
|
||||||
* not invoked. In this mode, the engine has less control over output because it cannot request that
|
|
||||||
* some JAR entries are not output. Nevertheless, the engine will attempt to make the output APK
|
|
||||||
* signed and will report an error if cannot do so.
|
|
||||||
*/
|
|
||||||
public interface ApkSignerEngine extends Closeable {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates to this engine that the input APK contains the provided APK Signing Block. The
|
|
||||||
* block may contain signatures of the input APK, such as APK Signature Scheme v2 signatures.
|
|
||||||
*
|
|
||||||
* @param apkSigningBlock APK signing block of the input APK. The provided data source is
|
|
||||||
* guaranteed to not be used by the engine after this method terminates.
|
|
||||||
*
|
|
||||||
* @throws IOException if an I/O error occurs while reading the APK Signing Block
|
|
||||||
* @throws IllegalStateException if this engine is closed
|
|
||||||
*/
|
|
||||||
void inputApkSigningBlock(DataSource apkSigningBlock) throws IOException, IllegalStateException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates to this engine that the specified JAR entry was encountered in the input APK.
|
|
||||||
*
|
|
||||||
* <p>When an input entry is updated/changed, it's OK to not invoke
|
|
||||||
* {@link #inputJarEntryRemoved(String)} before invoking this method.
|
|
||||||
*
|
|
||||||
* @return instructions about how to proceed with this entry
|
|
||||||
*
|
|
||||||
* @throws IllegalStateException if this engine is closed
|
|
||||||
*/
|
|
||||||
InputJarEntryInstructions inputJarEntry(String entryName) throws IllegalStateException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates to this engine that the specified JAR entry was output.
|
|
||||||
*
|
|
||||||
* <p>It is unnecessary to invoke this method for entries added to output by this engine (e.g.,
|
|
||||||
* requested by {@link #outputJarEntries()}) provided the entries were output with exactly the
|
|
||||||
* data requested by the engine.
|
|
||||||
*
|
|
||||||
* <p>When an already output entry is updated/changed, it's OK to not invoke
|
|
||||||
* {@link #outputJarEntryRemoved(String)} before invoking this method.
|
|
||||||
*
|
|
||||||
* @return request to inspect the entry or {@code null} if the engine does not need to inspect
|
|
||||||
* the entry. The request must be fulfilled before {@link #outputJarEntries()} is
|
|
||||||
* invoked.
|
|
||||||
*
|
|
||||||
* @throws IllegalStateException if this engine is closed
|
|
||||||
*/
|
|
||||||
InspectJarEntryRequest outputJarEntry(String entryName) throws IllegalStateException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates to this engine that the specified JAR entry was removed from the input. It's safe
|
|
||||||
* to invoke this for entries for which {@link #inputJarEntry(String)} hasn't been invoked.
|
|
||||||
*
|
|
||||||
* @return output policy of this JAR entry. The policy indicates how this input entry affects
|
|
||||||
* the output APK. The client of this engine should use this information to determine
|
|
||||||
* how the removal of this input APK's JAR entry affects the output APK.
|
|
||||||
*
|
|
||||||
* @throws IllegalStateException if this engine is closed
|
|
||||||
*/
|
|
||||||
InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName)
|
|
||||||
throws IllegalStateException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates to this engine that the specified JAR entry was removed from the output. It's safe
|
|
||||||
* to invoke this for entries for which {@link #outputJarEntry(String)} hasn't been invoked.
|
|
||||||
*
|
|
||||||
* @throws IllegalStateException if this engine is closed
|
|
||||||
*/
|
|
||||||
void outputJarEntryRemoved(String entryName) throws IllegalStateException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates to this engine that all JAR entries have been output.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return request to add JAR signature to the output or {@code null} if there is no need to add
|
|
||||||
* a JAR signature. The request will contain additional JAR entries to be output. The
|
|
||||||
* request must be fulfilled before
|
|
||||||
* {@link #outputZipSections(DataSource, DataSource, DataSource)} is invoked.
|
|
||||||
*
|
|
||||||
* @throws NoSuchAlgorithmException if a signature could not be generated 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 a signature
|
|
||||||
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
|
||||||
* entries, or if the engine is closed
|
|
||||||
*/
|
|
||||||
OutputJarSignatureRequest outputJarEntries()
|
|
||||||
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
|
|
||||||
IllegalStateException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates to this engine that the ZIP sections comprising the output APK have been output.
|
|
||||||
*
|
|
||||||
* <p>The provided data sources are guaranteed to not be used by the engine after this method
|
|
||||||
* terminates.
|
|
||||||
*
|
|
||||||
* @param zipEntries the section of ZIP archive containing Local File Header records and data of
|
|
||||||
* the ZIP entries. In a well-formed archive, this section starts at the start of the
|
|
||||||
* archive and extends all the way to the ZIP Central Directory.
|
|
||||||
* @param zipCentralDirectory ZIP Central Directory section
|
|
||||||
* @param zipEocd ZIP End of Central Directory (EoCD) record
|
|
||||||
*
|
|
||||||
* @return request to add an APK Signing Block to the output or {@code null} if the output must
|
|
||||||
* not contain an APK Signing Block. The request must be fulfilled before
|
|
||||||
* {@link #outputDone()} is invoked.
|
|
||||||
*
|
|
||||||
* @throws IOException if an I/O error occurs while reading the provided ZIP sections
|
|
||||||
* @throws NoSuchAlgorithmException if a signature could not be generated 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 a signature
|
|
||||||
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
|
||||||
* entries or to output JAR signature, or if the engine is closed
|
|
||||||
*/
|
|
||||||
OutputApkSigningBlockRequest outputZipSections(
|
|
||||||
DataSource zipEntries,
|
|
||||||
DataSource zipCentralDirectory,
|
|
||||||
DataSource zipEocd)
|
|
||||||
throws IOException, NoSuchAlgorithmException, InvalidKeyException,
|
|
||||||
SignatureException, IllegalStateException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates to this engine that the signed APK was output.
|
|
||||||
*
|
|
||||||
* <p>This does not change the output APK. The method helps the client confirm that the current
|
|
||||||
* output is signed.
|
|
||||||
*
|
|
||||||
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
|
||||||
* entries or to output signatures, or if the engine is closed
|
|
||||||
*/
|
|
||||||
void outputDone() throws IllegalStateException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates to this engine that it will no longer be used. Invoking this on an already closed
|
|
||||||
* engine is OK.
|
|
||||||
*
|
|
||||||
* <p>This does not change the output APK. For example, if the output APK is not yet fully
|
|
||||||
* signed, it will remain so after this method terminates.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
void close();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instructions about how to handle an input APK's JAR entry.
|
|
||||||
*
|
|
||||||
* <p>The instructions indicate whether to output the entry (see {@link #getOutputPolicy()}) and
|
|
||||||
* may contain a request to inspect the entry (see {@link #getInspectJarEntryRequest()}), in
|
|
||||||
* which case the request must be fulfilled before {@link ApkSignerEngine#outputJarEntries()} is
|
|
||||||
* invoked.
|
|
||||||
*/
|
|
||||||
public static class InputJarEntryInstructions {
|
|
||||||
private final OutputPolicy mOutputPolicy;
|
|
||||||
private final InspectJarEntryRequest mInspectJarEntryRequest;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
|
|
||||||
* output policy and without a request to inspect the entry.
|
|
||||||
*/
|
|
||||||
public InputJarEntryInstructions(OutputPolicy outputPolicy) {
|
|
||||||
this(outputPolicy, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
|
|
||||||
* output mode and with the provided request to inspect the entry.
|
|
||||||
*
|
|
||||||
* @param inspectJarEntryRequest request to inspect the entry or {@code null} if there's no
|
|
||||||
* need to inspect the entry.
|
|
||||||
*/
|
|
||||||
public InputJarEntryInstructions(
|
|
||||||
OutputPolicy outputPolicy,
|
|
||||||
InspectJarEntryRequest inspectJarEntryRequest) {
|
|
||||||
mOutputPolicy = outputPolicy;
|
|
||||||
mInspectJarEntryRequest = inspectJarEntryRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the output policy for this entry.
|
|
||||||
*/
|
|
||||||
public OutputPolicy getOutputPolicy() {
|
|
||||||
return mOutputPolicy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request to inspect the JAR entry or {@code null} if there is no need to
|
|
||||||
* inspect the entry.
|
|
||||||
*/
|
|
||||||
public InspectJarEntryRequest getInspectJarEntryRequest() {
|
|
||||||
return mInspectJarEntryRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Output policy for an input APK's JAR entry.
|
|
||||||
*/
|
|
||||||
public static enum OutputPolicy {
|
|
||||||
/** Entry must not be output. */
|
|
||||||
SKIP,
|
|
||||||
|
|
||||||
/** Entry should be output. */
|
|
||||||
OUTPUT,
|
|
||||||
|
|
||||||
/** Entry will be output by the engine. The client can thus ignore this input entry. */
|
|
||||||
OUTPUT_BY_ENGINE,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request to inspect the specified JAR entry.
|
|
||||||
*
|
|
||||||
* <p>The entry's uncompressed data must be provided to the data sink returned by
|
|
||||||
* {@link #getDataSink()}. Once the entry's data has been provided to the sink, {@link #done()}
|
|
||||||
* must be invoked.
|
|
||||||
*/
|
|
||||||
interface InspectJarEntryRequest {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the data sink into which the entry's uncompressed data should be sent.
|
|
||||||
*/
|
|
||||||
DataSink getDataSink();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates that entry's data has been provided in full.
|
|
||||||
*/
|
|
||||||
void done();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the name of the JAR entry.
|
|
||||||
*/
|
|
||||||
String getEntryName();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request to add JAR signature (aka v1 signature) to the output APK.
|
|
||||||
*
|
|
||||||
* <p>Entries listed in {@link #getAdditionalJarEntries()} must be added to the output APK after
|
|
||||||
* which {@link #done()} must be invoked.
|
|
||||||
*/
|
|
||||||
interface OutputJarSignatureRequest {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns JAR entries that must be added to the output APK.
|
|
||||||
*/
|
|
||||||
List<JarEntry> getAdditionalJarEntries();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates that the JAR entries contained in this request were added to the output APK.
|
|
||||||
*/
|
|
||||||
void done();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JAR entry.
|
|
||||||
*/
|
|
||||||
public static class JarEntry {
|
|
||||||
private final String mName;
|
|
||||||
private final byte[] mData;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@code JarEntry} with the provided name and data.
|
|
||||||
*
|
|
||||||
* @param data uncompressed data of the entry. Changes to this array will not be
|
|
||||||
* reflected in {@link #getData()}.
|
|
||||||
*/
|
|
||||||
public JarEntry(String name, byte[] data) {
|
|
||||||
mName = name;
|
|
||||||
mData = data.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the name of this ZIP entry.
|
|
||||||
*/
|
|
||||||
public String getName() {
|
|
||||||
return mName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the uncompressed data of this JAR entry.
|
|
||||||
*/
|
|
||||||
public byte[] getData() {
|
|
||||||
return mData.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
|
|
||||||
* signature(s) of the APK are contained in this block.
|
|
||||||
*
|
|
||||||
* <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
|
|
||||||
* output APK such that the block is immediately before the ZIP Central Directory, the offset of
|
|
||||||
* ZIP Central Directory in the ZIP End of Central Directory record must be adjusted
|
|
||||||
* accordingly, and then {@link #done()} must be invoked.
|
|
||||||
*
|
|
||||||
* <p>If the output contains an APK Signing Block, that block must be replaced by the block
|
|
||||||
* contained in this request.
|
|
||||||
*/
|
|
||||||
interface OutputApkSigningBlockRequest {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the APK Signing Block.
|
|
||||||
*/
|
|
||||||
byte[] getApkSigningBlock();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates that the APK Signing Block was output as requested.
|
|
||||||
*/
|
|
||||||
void done();
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,900 +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;
|
|
||||||
|
|
||||||
import com.android.apksigner.core.internal.apk.v1.DigestAlgorithm;
|
|
||||||
import com.android.apksigner.core.internal.apk.v1.V1SchemeSigner;
|
|
||||||
import com.android.apksigner.core.internal.apk.v2.V2SchemeSigner;
|
|
||||||
import com.android.apksigner.core.internal.util.MessageDigestSink;
|
|
||||||
import com.android.apksigner.core.internal.util.Pair;
|
|
||||||
import com.android.apksigner.core.util.DataSink;
|
|
||||||
import com.android.apksigner.core.util.DataSinks;
|
|
||||||
import com.android.apksigner.core.util.DataSource;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.PrivateKey;
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.SignatureException;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default implementation of {@link ApkSignerEngine}.
|
|
||||||
*
|
|
||||||
* <p>Use {@link Builder} to obtain instances of this engine.
|
|
||||||
*/
|
|
||||||
public class DefaultApkSignerEngine implements ApkSignerEngine {
|
|
||||||
|
|
||||||
// IMPLEMENTATION NOTE: This engine generates a signed APK as follows:
|
|
||||||
// 1. The engine asks its client to output input JAR entries which are not part of JAR
|
|
||||||
// signature.
|
|
||||||
// 2. If JAR signing (v1 signing) is enabled, the engine inspects the output JAR entries to
|
|
||||||
// compute their digests, to be placed into output META-INF/MANIFEST.MF. It also inspects
|
|
||||||
// the contents of input and output META-INF/MANIFEST.MF to borrow the main section of the
|
|
||||||
// file. It does not care about individual (i.e., JAR entry-specific) sections. It then
|
|
||||||
// emits the v1 signature (a set of JAR entries) and asks the client to output them.
|
|
||||||
// 3. If APK Signature Scheme v2 (v2 signing) is enabled, the engine emits an APK Signing Block
|
|
||||||
// from outputZipSections() and asks its client to insert this block into the output.
|
|
||||||
|
|
||||||
private final boolean mV1SigningEnabled;
|
|
||||||
private final boolean mV2SigningEnabled;
|
|
||||||
private final boolean mOtherSignersSignaturesPreserved;
|
|
||||||
private final List<V1SchemeSigner.SignerConfig> mV1SignerConfigs;
|
|
||||||
private final DigestAlgorithm mV1ContentDigestAlgorithm;
|
|
||||||
private final List<V2SchemeSigner.SignerConfig> mV2SignerConfigs;
|
|
||||||
|
|
||||||
private boolean mClosed;
|
|
||||||
|
|
||||||
private boolean mV1SignaturePending;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Names of JAR entries which this engine is expected to output as part of v1 signing.
|
|
||||||
*/
|
|
||||||
private final Set<String> mSignatureExpectedOutputJarEntryNames;
|
|
||||||
|
|
||||||
/** Requests for digests of output JAR entries. */
|
|
||||||
private final Map<String, GetJarEntryDataDigestRequest> mOutputJarEntryDigestRequests =
|
|
||||||
new HashMap<>();
|
|
||||||
|
|
||||||
/** Digests of output JAR entries. */
|
|
||||||
private final Map<String, byte[]> mOutputJarEntryDigests = new HashMap<>();
|
|
||||||
|
|
||||||
/** Data of JAR entries emitted by this engine as v1 signature. */
|
|
||||||
private final Map<String, byte[]> mEmittedSignatureJarEntryData = new HashMap<>();
|
|
||||||
|
|
||||||
/** Requests for data of output JAR entries which comprise the v1 signature. */
|
|
||||||
private final Map<String, GetJarEntryDataRequest> mOutputSignatureJarEntryDataRequests =
|
|
||||||
new HashMap<>();
|
|
||||||
/**
|
|
||||||
* Request to obtain the data of MANIFEST.MF or {@code null} if the request hasn't been issued.
|
|
||||||
*/
|
|
||||||
private GetJarEntryDataRequest mInputJarManifestEntryDataRequest;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request to output the emitted v1 signature or {@code null} if the request hasn't been issued.
|
|
||||||
*/
|
|
||||||
private OutputJarSignatureRequestImpl mAddV1SignatureRequest;
|
|
||||||
|
|
||||||
private boolean mV2SignaturePending;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request to output the emitted v2 signature or {@code null} if the request hasn't been issued.
|
|
||||||
*/
|
|
||||||
private OutputApkSigningBlockRequestImpl mAddV2SignatureRequest;
|
|
||||||
|
|
||||||
private DefaultApkSignerEngine(
|
|
||||||
List<SignerConfig> signerConfigs,
|
|
||||||
int minSdkVersion,
|
|
||||||
boolean v1SigningEnabled,
|
|
||||||
boolean v2SigningEnabled,
|
|
||||||
boolean otherSignersSignaturesPreserved) throws InvalidKeyException {
|
|
||||||
if (signerConfigs.isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("At least one signer config must be provided");
|
|
||||||
}
|
|
||||||
if (otherSignersSignaturesPreserved) {
|
|
||||||
throw new UnsupportedOperationException(
|
|
||||||
"Preserving other signer's signatures is not yet implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
mV1SigningEnabled = v1SigningEnabled;
|
|
||||||
mV2SigningEnabled = v2SigningEnabled;
|
|
||||||
mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
|
|
||||||
mV1SignerConfigs =
|
|
||||||
(v1SigningEnabled)
|
|
||||||
? new ArrayList<>(signerConfigs.size()) : Collections.emptyList();
|
|
||||||
mV2SignerConfigs =
|
|
||||||
(v2SigningEnabled)
|
|
||||||
? new ArrayList<>(signerConfigs.size()) : Collections.emptyList();
|
|
||||||
mV1ContentDigestAlgorithm =
|
|
||||||
(v1SigningEnabled)
|
|
||||||
? V1SchemeSigner.getSuggestedContentDigestAlgorithm(minSdkVersion) : null;
|
|
||||||
for (SignerConfig signerConfig : signerConfigs) {
|
|
||||||
List<X509Certificate> certificates = signerConfig.getCertificates();
|
|
||||||
PublicKey publicKey = certificates.get(0).getPublicKey();
|
|
||||||
|
|
||||||
if (v1SigningEnabled) {
|
|
||||||
DigestAlgorithm v1SignatureDigestAlgorithm =
|
|
||||||
V1SchemeSigner.getSuggestedSignatureDigestAlgorithm(
|
|
||||||
publicKey, minSdkVersion);
|
|
||||||
V1SchemeSigner.SignerConfig v1SignerConfig = new V1SchemeSigner.SignerConfig();
|
|
||||||
v1SignerConfig.name = signerConfig.getName();
|
|
||||||
v1SignerConfig.privateKey = signerConfig.getPrivateKey();
|
|
||||||
v1SignerConfig.certificates = certificates;
|
|
||||||
v1SignerConfig.contentDigestAlgorithm = mV1ContentDigestAlgorithm;
|
|
||||||
v1SignerConfig.signatureDigestAlgorithm = v1SignatureDigestAlgorithm;
|
|
||||||
mV1SignerConfigs.add(v1SignerConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (v2SigningEnabled) {
|
|
||||||
V2SchemeSigner.SignerConfig v2SignerConfig = new V2SchemeSigner.SignerConfig();
|
|
||||||
v2SignerConfig.privateKey = signerConfig.getPrivateKey();
|
|
||||||
v2SignerConfig.certificates = certificates;
|
|
||||||
v2SignerConfig.signatureAlgorithms =
|
|
||||||
V2SchemeSigner.getSuggestedSignatureAlgorithms(publicKey, minSdkVersion);
|
|
||||||
mV2SignerConfigs.add(v2SignerConfig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mSignatureExpectedOutputJarEntryNames =
|
|
||||||
(v1SigningEnabled)
|
|
||||||
? V1SchemeSigner.getOutputEntryNames(mV1SignerConfigs)
|
|
||||||
: Collections.emptySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void inputApkSigningBlock(DataSource apkSigningBlock) {
|
|
||||||
checkNotClosed();
|
|
||||||
|
|
||||||
if ((apkSigningBlock == null) || (apkSigningBlock.size() == 0)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mOtherSignersSignaturesPreserved) {
|
|
||||||
// TODO: Preserve blocks other than APK Signature Scheme v2 blocks of signers configured
|
|
||||||
// in this engine.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// TODO: Preserve blocks other than APK Signature Scheme v2 blocks.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InputJarEntryInstructions inputJarEntry(String entryName) {
|
|
||||||
checkNotClosed();
|
|
||||||
|
|
||||||
InputJarEntryInstructions.OutputPolicy outputPolicy =
|
|
||||||
getInputJarEntryOutputPolicy(entryName);
|
|
||||||
switch (outputPolicy) {
|
|
||||||
case SKIP:
|
|
||||||
return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.SKIP);
|
|
||||||
case OUTPUT:
|
|
||||||
return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.OUTPUT);
|
|
||||||
case OUTPUT_BY_ENGINE:
|
|
||||||
if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) {
|
|
||||||
// We copy the main section of the JAR manifest from input to output. Thus, this
|
|
||||||
// invalidates v1 signature and we need to see the entry's data.
|
|
||||||
mInputJarManifestEntryDataRequest = new GetJarEntryDataRequest(entryName);
|
|
||||||
return new InputJarEntryInstructions(
|
|
||||||
InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE,
|
|
||||||
mInputJarManifestEntryDataRequest);
|
|
||||||
}
|
|
||||||
return new InputJarEntryInstructions(
|
|
||||||
InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE);
|
|
||||||
default:
|
|
||||||
throw new RuntimeException("Unsupported output policy: " + outputPolicy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InspectJarEntryRequest outputJarEntry(String entryName) {
|
|
||||||
checkNotClosed();
|
|
||||||
invalidateV2Signature();
|
|
||||||
if (!mV1SigningEnabled) {
|
|
||||||
// No need to inspect JAR entries when v1 signing is not enabled.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// v1 signing is enabled
|
|
||||||
|
|
||||||
if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName)) {
|
|
||||||
// This entry is covered by v1 signature. We thus need to inspect the entry's data to
|
|
||||||
// compute its digest(s) for v1 signature.
|
|
||||||
|
|
||||||
// TODO: Handle the case where other signer's v1 signatures are present and need to be
|
|
||||||
// preserved. In that scenario we can't modify MANIFEST.MF and add/remove JAR entries
|
|
||||||
// covered by v1 signature.
|
|
||||||
invalidateV1Signature();
|
|
||||||
GetJarEntryDataDigestRequest dataDigestRequest =
|
|
||||||
new GetJarEntryDataDigestRequest(
|
|
||||||
entryName,
|
|
||||||
V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm));
|
|
||||||
mOutputJarEntryDigestRequests.put(entryName, dataDigestRequest);
|
|
||||||
mOutputJarEntryDigests.remove(entryName);
|
|
||||||
return dataDigestRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
|
|
||||||
// This entry is part of v1 signature generated by this engine. We need to check whether
|
|
||||||
// the entry's data is as output by the engine.
|
|
||||||
invalidateV1Signature();
|
|
||||||
GetJarEntryDataRequest dataRequest;
|
|
||||||
if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) {
|
|
||||||
dataRequest = new GetJarEntryDataRequest(entryName);
|
|
||||||
mInputJarManifestEntryDataRequest = dataRequest;
|
|
||||||
} else {
|
|
||||||
// If this entry is part of v1 signature which has been emitted by this engine,
|
|
||||||
// check whether the output entry's data matches what the engine emitted.
|
|
||||||
dataRequest =
|
|
||||||
(mEmittedSignatureJarEntryData.containsKey(entryName))
|
|
||||||
? new GetJarEntryDataRequest(entryName) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataRequest != null) {
|
|
||||||
mOutputSignatureJarEntryDataRequests.put(entryName, dataRequest);
|
|
||||||
}
|
|
||||||
return dataRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This entry is not covered by v1 signature and isn't part of v1 signature.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName) {
|
|
||||||
checkNotClosed();
|
|
||||||
return getInputJarEntryOutputPolicy(entryName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void outputJarEntryRemoved(String entryName) {
|
|
||||||
checkNotClosed();
|
|
||||||
invalidateV2Signature();
|
|
||||||
if (!mV1SigningEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName)) {
|
|
||||||
// This entry is covered by v1 signature.
|
|
||||||
invalidateV1Signature();
|
|
||||||
mOutputJarEntryDigests.remove(entryName);
|
|
||||||
mOutputJarEntryDigestRequests.remove(entryName);
|
|
||||||
mOutputSignatureJarEntryDataRequests.remove(entryName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
|
|
||||||
// This entry is part of the v1 signature generated by this engine.
|
|
||||||
invalidateV1Signature();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OutputJarSignatureRequest outputJarEntries()
|
|
||||||
throws InvalidKeyException, SignatureException, NoSuchAlgorithmException {
|
|
||||||
checkNotClosed();
|
|
||||||
|
|
||||||
if (!mV1SignaturePending) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((mInputJarManifestEntryDataRequest != null)
|
|
||||||
&& (!mInputJarManifestEntryDataRequest.isDone())) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"Still waiting to inspect input APK's "
|
|
||||||
+ mInputJarManifestEntryDataRequest.getEntryName());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (GetJarEntryDataDigestRequest digestRequest
|
|
||||||
: mOutputJarEntryDigestRequests.values()) {
|
|
||||||
String entryName = digestRequest.getEntryName();
|
|
||||||
if (!digestRequest.isDone()) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"Still waiting to inspect output APK's " + entryName);
|
|
||||||
}
|
|
||||||
mOutputJarEntryDigests.put(entryName, digestRequest.getDigest());
|
|
||||||
}
|
|
||||||
mOutputJarEntryDigestRequests.clear();
|
|
||||||
|
|
||||||
for (GetJarEntryDataRequest dataRequest : mOutputSignatureJarEntryDataRequests.values()) {
|
|
||||||
if (!dataRequest.isDone()) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"Still waiting to inspect output APK's " + dataRequest.getEntryName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Integer> apkSigningSchemeIds =
|
|
||||||
(mV2SigningEnabled) ? Collections.singletonList(2) : Collections.emptyList();
|
|
||||||
byte[] inputJarManifest =
|
|
||||||
(mInputJarManifestEntryDataRequest != null)
|
|
||||||
? mInputJarManifestEntryDataRequest.getData() : null;
|
|
||||||
|
|
||||||
// Check whether the most recently used signature (if present) is still fine.
|
|
||||||
List<Pair<String, byte[]>> signatureZipEntries;
|
|
||||||
if ((mAddV1SignatureRequest == null) || (!mAddV1SignatureRequest.isDone())) {
|
|
||||||
try {
|
|
||||||
signatureZipEntries =
|
|
||||||
V1SchemeSigner.sign(
|
|
||||||
mV1SignerConfigs,
|
|
||||||
mV1ContentDigestAlgorithm,
|
|
||||||
mOutputJarEntryDigests,
|
|
||||||
apkSigningSchemeIds,
|
|
||||||
inputJarManifest);
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
throw new SignatureException("Failed to generate v1 signature", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
V1SchemeSigner.OutputManifestFile newManifest =
|
|
||||||
V1SchemeSigner.generateManifestFile(
|
|
||||||
mV1ContentDigestAlgorithm, mOutputJarEntryDigests, inputJarManifest);
|
|
||||||
byte[] emittedSignatureManifest =
|
|
||||||
mEmittedSignatureJarEntryData.get(V1SchemeSigner.MANIFEST_ENTRY_NAME);
|
|
||||||
if (!Arrays.equals(newManifest.contents, emittedSignatureManifest)) {
|
|
||||||
// Emitted v1 signature is no longer valid.
|
|
||||||
try {
|
|
||||||
signatureZipEntries =
|
|
||||||
V1SchemeSigner.signManifest(
|
|
||||||
mV1SignerConfigs,
|
|
||||||
mV1ContentDigestAlgorithm,
|
|
||||||
apkSigningSchemeIds,
|
|
||||||
newManifest);
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
throw new SignatureException("Failed to generate v1 signature", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Emitted v1 signature is still valid. Check whether the signature is there in the
|
|
||||||
// output.
|
|
||||||
signatureZipEntries = new ArrayList<>();
|
|
||||||
for (Map.Entry<String, byte[]> expectedOutputEntry
|
|
||||||
: mEmittedSignatureJarEntryData.entrySet()) {
|
|
||||||
String entryName = expectedOutputEntry.getKey();
|
|
||||||
byte[] expectedData = expectedOutputEntry.getValue();
|
|
||||||
GetJarEntryDataRequest actualDataRequest =
|
|
||||||
mOutputSignatureJarEntryDataRequests.get(entryName);
|
|
||||||
if (actualDataRequest == null) {
|
|
||||||
// This signature entry hasn't been output.
|
|
||||||
signatureZipEntries.add(Pair.of(entryName, expectedData));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
byte[] actualData = actualDataRequest.getData();
|
|
||||||
if (!Arrays.equals(expectedData, actualData)) {
|
|
||||||
signatureZipEntries.add(Pair.of(entryName, expectedData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (signatureZipEntries.isEmpty()) {
|
|
||||||
// v1 signature in the output is valid
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// v1 signature in the output is not valid.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signatureZipEntries.isEmpty()) {
|
|
||||||
// v1 signature in the output is valid
|
|
||||||
mV1SignaturePending = false;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<OutputJarSignatureRequest.JarEntry> sigEntries =
|
|
||||||
new ArrayList<>(signatureZipEntries.size());
|
|
||||||
for (Pair<String, byte[]> entry : signatureZipEntries) {
|
|
||||||
String entryName = entry.getFirst();
|
|
||||||
byte[] entryData = entry.getSecond();
|
|
||||||
sigEntries.add(new OutputJarSignatureRequest.JarEntry(entryName, entryData));
|
|
||||||
mEmittedSignatureJarEntryData.put(entryName, entryData);
|
|
||||||
}
|
|
||||||
mAddV1SignatureRequest = new OutputJarSignatureRequestImpl(sigEntries);
|
|
||||||
return mAddV1SignatureRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OutputApkSigningBlockRequest outputZipSections(
|
|
||||||
DataSource zipEntries,
|
|
||||||
DataSource zipCentralDirectory,
|
|
||||||
DataSource zipEocd)
|
|
||||||
throws IOException, InvalidKeyException, SignatureException,
|
|
||||||
NoSuchAlgorithmException {
|
|
||||||
checkNotClosed();
|
|
||||||
checkV1SigningDoneIfEnabled();
|
|
||||||
if (!mV2SigningEnabled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
invalidateV2Signature();
|
|
||||||
|
|
||||||
byte[] apkSigningBlock =
|
|
||||||
V2SchemeSigner.generateApkSigningBlock(
|
|
||||||
zipEntries, zipCentralDirectory, zipEocd, mV2SignerConfigs);
|
|
||||||
|
|
||||||
mAddV2SignatureRequest = new OutputApkSigningBlockRequestImpl(apkSigningBlock);
|
|
||||||
return mAddV2SignatureRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void outputDone() {
|
|
||||||
checkNotClosed();
|
|
||||||
checkV1SigningDoneIfEnabled();
|
|
||||||
checkV2SigningDoneIfEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
mClosed = true;
|
|
||||||
|
|
||||||
mAddV1SignatureRequest = null;
|
|
||||||
mInputJarManifestEntryDataRequest = null;
|
|
||||||
mOutputJarEntryDigestRequests.clear();
|
|
||||||
mOutputJarEntryDigests.clear();
|
|
||||||
mEmittedSignatureJarEntryData.clear();
|
|
||||||
mOutputSignatureJarEntryDataRequests.clear();
|
|
||||||
|
|
||||||
mAddV2SignatureRequest = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void invalidateV1Signature() {
|
|
||||||
if (mV1SigningEnabled) {
|
|
||||||
mV1SignaturePending = true;
|
|
||||||
}
|
|
||||||
invalidateV2Signature();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void invalidateV2Signature() {
|
|
||||||
if (mV2SigningEnabled) {
|
|
||||||
mV2SignaturePending = true;
|
|
||||||
mAddV2SignatureRequest = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkNotClosed() {
|
|
||||||
if (mClosed) {
|
|
||||||
throw new IllegalStateException("Engine closed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkV1SigningDoneIfEnabled() {
|
|
||||||
if (!mV1SignaturePending) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mAddV1SignatureRequest == null) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"v1 signature (JAR signature) not yet generated. Skipped outputJarEntries()?");
|
|
||||||
}
|
|
||||||
if (!mAddV1SignatureRequest.isDone()) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"v1 signature (JAR signature) addition requested by outputJarEntries() hasn't"
|
|
||||||
+ " been fulfilled");
|
|
||||||
}
|
|
||||||
for (Map.Entry<String, byte[]> expectedOutputEntry
|
|
||||||
: mEmittedSignatureJarEntryData.entrySet()) {
|
|
||||||
String entryName = expectedOutputEntry.getKey();
|
|
||||||
byte[] expectedData = expectedOutputEntry.getValue();
|
|
||||||
GetJarEntryDataRequest actualDataRequest =
|
|
||||||
mOutputSignatureJarEntryDataRequests.get(entryName);
|
|
||||||
if (actualDataRequest == null) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"APK entry " + entryName + " not yet output despite this having been"
|
|
||||||
+ " requested");
|
|
||||||
} else if (!actualDataRequest.isDone()) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"Still waiting to inspect output APK's " + entryName);
|
|
||||||
}
|
|
||||||
byte[] actualData = actualDataRequest.getData();
|
|
||||||
if (!Arrays.equals(expectedData, actualData)) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"Output APK entry " + entryName + " data differs from what was requested");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mV1SignaturePending = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkV2SigningDoneIfEnabled() {
|
|
||||||
if (!mV2SignaturePending) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mAddV2SignatureRequest == null) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"v2 signature (APK Signature Scheme v2 signature) not yet generated."
|
|
||||||
+ " Skipped outputZipSections()?");
|
|
||||||
}
|
|
||||||
if (!mAddV2SignatureRequest.isDone()) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"v2 signature (APK Signature Scheme v2 signature) addition requested by"
|
|
||||||
+ " outputZipSections() hasn't been fulfilled yet");
|
|
||||||
}
|
|
||||||
mAddV2SignatureRequest = null;
|
|
||||||
mV2SignaturePending = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the output policy for the provided input JAR entry.
|
|
||||||
*/
|
|
||||||
private InputJarEntryInstructions.OutputPolicy getInputJarEntryOutputPolicy(String entryName) {
|
|
||||||
if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
|
|
||||||
return InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE;
|
|
||||||
}
|
|
||||||
if ((mOtherSignersSignaturesPreserved)
|
|
||||||
|| (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName))) {
|
|
||||||
return InputJarEntryInstructions.OutputPolicy.OUTPUT;
|
|
||||||
}
|
|
||||||
return InputJarEntryInstructions.OutputPolicy.SKIP;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class OutputJarSignatureRequestImpl implements OutputJarSignatureRequest {
|
|
||||||
private final List<JarEntry> mAdditionalJarEntries;
|
|
||||||
private volatile boolean mDone;
|
|
||||||
|
|
||||||
private OutputJarSignatureRequestImpl(List<JarEntry> additionalZipEntries) {
|
|
||||||
mAdditionalJarEntries =
|
|
||||||
Collections.unmodifiableList(new ArrayList<>(additionalZipEntries));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<JarEntry> getAdditionalJarEntries() {
|
|
||||||
return mAdditionalJarEntries;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void done() {
|
|
||||||
mDone = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDone() {
|
|
||||||
return mDone;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class OutputApkSigningBlockRequestImpl implements OutputApkSigningBlockRequest {
|
|
||||||
private final byte[] mApkSigningBlock;
|
|
||||||
private volatile boolean mDone;
|
|
||||||
|
|
||||||
private OutputApkSigningBlockRequestImpl(byte[] apkSigingBlock) {
|
|
||||||
mApkSigningBlock = apkSigingBlock.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] getApkSigningBlock() {
|
|
||||||
return mApkSigningBlock.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void done() {
|
|
||||||
mDone = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDone() {
|
|
||||||
return mDone;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JAR entry inspection request which obtain the entry's uncompressed data.
|
|
||||||
*/
|
|
||||||
private static class GetJarEntryDataRequest implements InspectJarEntryRequest {
|
|
||||||
private final String mEntryName;
|
|
||||||
private final Object mLock = new Object();
|
|
||||||
|
|
||||||
private boolean mDone;
|
|
||||||
private DataSink mDataSink;
|
|
||||||
private ByteArrayOutputStream mDataSinkBuf;
|
|
||||||
|
|
||||||
private GetJarEntryDataRequest(String entryName) {
|
|
||||||
mEntryName = entryName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getEntryName() {
|
|
||||||
return mEntryName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DataSink getDataSink() {
|
|
||||||
synchronized (mLock) {
|
|
||||||
checkNotDone();
|
|
||||||
if (mDataSinkBuf == null) {
|
|
||||||
mDataSinkBuf = new ByteArrayOutputStream();
|
|
||||||
}
|
|
||||||
if (mDataSink == null) {
|
|
||||||
mDataSink = DataSinks.asDataSink(mDataSinkBuf);
|
|
||||||
}
|
|
||||||
return mDataSink;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void done() {
|
|
||||||
synchronized (mLock) {
|
|
||||||
if (mDone) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mDone = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDone() {
|
|
||||||
synchronized (mLock) {
|
|
||||||
return mDone;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkNotDone() throws IllegalStateException {
|
|
||||||
synchronized (mLock) {
|
|
||||||
if (mDone) {
|
|
||||||
throw new IllegalStateException("Already done");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] getData() {
|
|
||||||
synchronized (mLock) {
|
|
||||||
if (!mDone) {
|
|
||||||
throw new IllegalStateException("Not yet done");
|
|
||||||
}
|
|
||||||
return (mDataSinkBuf != null) ? mDataSinkBuf.toByteArray() : new byte[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JAR entry inspection request which obtains the digest of the entry's uncompressed data.
|
|
||||||
*/
|
|
||||||
private static class GetJarEntryDataDigestRequest implements InspectJarEntryRequest {
|
|
||||||
private final String mEntryName;
|
|
||||||
private final String mJcaDigestAlgorithm;
|
|
||||||
private final Object mLock = new Object();
|
|
||||||
|
|
||||||
private boolean mDone;
|
|
||||||
private DataSink mDataSink;
|
|
||||||
private MessageDigest mMessageDigest;
|
|
||||||
private byte[] mDigest;
|
|
||||||
|
|
||||||
private GetJarEntryDataDigestRequest(String entryName, String jcaDigestAlgorithm) {
|
|
||||||
mEntryName = entryName;
|
|
||||||
mJcaDigestAlgorithm = jcaDigestAlgorithm;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getEntryName() {
|
|
||||||
return mEntryName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DataSink getDataSink() {
|
|
||||||
synchronized (mLock) {
|
|
||||||
checkNotDone();
|
|
||||||
if (mDataSink == null) {
|
|
||||||
mDataSink = new MessageDigestSink(new MessageDigest[] {getMessageDigest()});
|
|
||||||
}
|
|
||||||
return mDataSink;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private MessageDigest getMessageDigest() {
|
|
||||||
synchronized (mLock) {
|
|
||||||
if (mMessageDigest == null) {
|
|
||||||
try {
|
|
||||||
mMessageDigest = MessageDigest.getInstance(mJcaDigestAlgorithm);
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
mJcaDigestAlgorithm + " MessageDigest not available", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mMessageDigest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void done() {
|
|
||||||
synchronized (mLock) {
|
|
||||||
if (mDone) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mDone = true;
|
|
||||||
mDigest = getMessageDigest().digest();
|
|
||||||
mMessageDigest = null;
|
|
||||||
mDataSink = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDone() {
|
|
||||||
synchronized (mLock) {
|
|
||||||
return mDone;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkNotDone() throws IllegalStateException {
|
|
||||||
synchronized (mLock) {
|
|
||||||
if (mDone) {
|
|
||||||
throw new IllegalStateException("Already done");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] getDigest() {
|
|
||||||
synchronized (mLock) {
|
|
||||||
if (!mDone) {
|
|
||||||
throw new IllegalStateException("Not yet done");
|
|
||||||
}
|
|
||||||
return mDigest.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration of a signer.
|
|
||||||
*
|
|
||||||
* <p>Use {@link Builder} to obtain configuration instances.
|
|
||||||
*/
|
|
||||||
public static class SignerConfig {
|
|
||||||
private final String mName;
|
|
||||||
private final PrivateKey mPrivateKey;
|
|
||||||
private final List<X509Certificate> mCertificates;
|
|
||||||
|
|
||||||
private SignerConfig(
|
|
||||||
String name,
|
|
||||||
PrivateKey privateKey,
|
|
||||||
List<X509Certificate> certificates) {
|
|
||||||
mName = name;
|
|
||||||
mPrivateKey = privateKey;
|
|
||||||
mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the name of this signer.
|
|
||||||
*/
|
|
||||||
public String getName() {
|
|
||||||
return mName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the signing key of this signer.
|
|
||||||
*/
|
|
||||||
public PrivateKey getPrivateKey() {
|
|
||||||
return mPrivateKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the certificate(s) of this signer. The first certificate's public key corresponds
|
|
||||||
* to this signer's private key.
|
|
||||||
*/
|
|
||||||
public List<X509Certificate> getCertificates() {
|
|
||||||
return mCertificates;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builder of {@link SignerConfig} instances.
|
|
||||||
*/
|
|
||||||
public static class Builder {
|
|
||||||
private final String mName;
|
|
||||||
private final PrivateKey mPrivateKey;
|
|
||||||
private final List<X509Certificate> mCertificates;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@code Builder}.
|
|
||||||
*
|
|
||||||
* @param name signer's name. The name is reflected in the name of files comprising the
|
|
||||||
* JAR signature of the APK.
|
|
||||||
* @param privateKey signing key
|
|
||||||
* @param certificates list of one or more X.509 certificates. The subject public key of
|
|
||||||
* the first certificate must correspond to the {@code privateKey}.
|
|
||||||
*/
|
|
||||||
public Builder(
|
|
||||||
String name,
|
|
||||||
PrivateKey privateKey,
|
|
||||||
List<X509Certificate> certificates) {
|
|
||||||
mName = name;
|
|
||||||
mPrivateKey = privateKey;
|
|
||||||
mCertificates = new ArrayList<>(certificates);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a new {@code SignerConfig} instance configured based on the configuration of
|
|
||||||
* this builder.
|
|
||||||
*/
|
|
||||||
public SignerConfig build() {
|
|
||||||
return new SignerConfig(
|
|
||||||
mName,
|
|
||||||
mPrivateKey,
|
|
||||||
mCertificates);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builder of {@link DefaultApkSignerEngine} instances.
|
|
||||||
*/
|
|
||||||
public static class Builder {
|
|
||||||
private final List<SignerConfig> mSignerConfigs;
|
|
||||||
private final int mMinSdkVersion;
|
|
||||||
|
|
||||||
private boolean mV1SigningEnabled = true;
|
|
||||||
private boolean mV2SigningEnabled = true;
|
|
||||||
private boolean mOtherSignersSignaturesPreserved;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@code Builder}.
|
|
||||||
*
|
|
||||||
* @param signerConfigs information about signers with which the APK will be signed. At
|
|
||||||
* least one signer configuration must be provided.
|
|
||||||
* @param minSdkVersion API Level of the oldest Android platform on which the APK is
|
|
||||||
* supposed to be installed. See {@code minSdkVersion} attribute in the APK's
|
|
||||||
* {@code AndroidManifest.xml}. The higher the version, the stronger signing features
|
|
||||||
* will be enabled.
|
|
||||||
*/
|
|
||||||
public Builder(
|
|
||||||
List<SignerConfig> signerConfigs,
|
|
||||||
int minSdkVersion) {
|
|
||||||
if (signerConfigs.isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("At least one signer config must be provided");
|
|
||||||
}
|
|
||||||
mSignerConfigs = new ArrayList<>(signerConfigs);
|
|
||||||
mMinSdkVersion = minSdkVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a new {@code DefaultApkSignerEngine} instance configured based on the
|
|
||||||
* configuration of this builder.
|
|
||||||
*/
|
|
||||||
public DefaultApkSignerEngine build() throws InvalidKeyException {
|
|
||||||
return new DefaultApkSignerEngine(
|
|
||||||
mSignerConfigs,
|
|
||||||
mMinSdkVersion,
|
|
||||||
mV1SigningEnabled,
|
|
||||||
mV2SigningEnabled,
|
|
||||||
mOtherSignersSignaturesPreserved);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
|
|
||||||
*
|
|
||||||
* <p>By default, the APK will be signed using this scheme.
|
|
||||||
*/
|
|
||||||
public Builder setV1SigningEnabled(boolean enabled) {
|
|
||||||
mV1SigningEnabled = enabled;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the APK should be signed using APK Signature Scheme v2 (aka v2 signature
|
|
||||||
* scheme).
|
|
||||||
*
|
|
||||||
* <p>By default, the APK will be signed using this scheme.
|
|
||||||
*/
|
|
||||||
public Builder setV2SigningEnabled(boolean enabled) {
|
|
||||||
mV2SigningEnabled = enabled;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether signatures produced by signers other than the ones configured in this engine
|
|
||||||
* should be copied from the input APK to the output APK.
|
|
||||||
*
|
|
||||||
* <p>By default, signatures of other signers are omitted from the output APK.
|
|
||||||
*/
|
|
||||||
public Builder setOtherSignersSignaturesPreserved(boolean preserved) {
|
|
||||||
mOtherSignersSignaturesPreserved = preserved;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,158 +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.apk;
|
|
||||||
|
|
||||||
import com.android.apksigner.core.internal.util.Pair;
|
|
||||||
import com.android.apksigner.core.internal.zip.ZipUtils;
|
|
||||||
import com.android.apksigner.core.util.DataSource;
|
|
||||||
import com.android.apksigner.core.zip.ZipFormatException;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APK utilities.
|
|
||||||
*/
|
|
||||||
public class ApkUtils {
|
|
||||||
|
|
||||||
private ApkUtils() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the main ZIP sections of the provided APK.
|
|
||||||
*
|
|
||||||
* @throws IOException if an I/O error occurred while reading the APK
|
|
||||||
* @throws ZipFormatException if the APK is malformed
|
|
||||||
*/
|
|
||||||
public static ZipSections findZipSections(DataSource apk)
|
|
||||||
throws IOException, ZipFormatException {
|
|
||||||
Pair<ByteBuffer, Long> eocdAndOffsetInFile =
|
|
||||||
ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
|
|
||||||
if (eocdAndOffsetInFile == null) {
|
|
||||||
throw new ZipFormatException("ZIP End of Central Directory record not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
|
|
||||||
long eocdOffset = eocdAndOffsetInFile.getSecond();
|
|
||||||
if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
|
|
||||||
throw new ZipFormatException("ZIP64 APK not supported");
|
|
||||||
}
|
|
||||||
eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
|
|
||||||
if (cdStartOffset >= eocdOffset) {
|
|
||||||
throw new ZipFormatException(
|
|
||||||
"ZIP Central Directory start offset out of range: " + cdStartOffset
|
|
||||||
+ ". ZIP End of Central Directory offset: " + eocdOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
|
|
||||||
long cdEndOffset = cdStartOffset + cdSizeBytes;
|
|
||||||
if (cdEndOffset > eocdOffset) {
|
|
||||||
throw new ZipFormatException(
|
|
||||||
"ZIP Central Directory overlaps with End of Central Directory"
|
|
||||||
+ ". CD end: " + cdEndOffset
|
|
||||||
+ ", EoCD start: " + eocdOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
|
|
||||||
|
|
||||||
return new ZipSections(
|
|
||||||
cdStartOffset,
|
|
||||||
cdSizeBytes,
|
|
||||||
cdRecordCount,
|
|
||||||
eocdOffset,
|
|
||||||
eocdBuf);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Information about the ZIP sections of an APK.
|
|
||||||
*/
|
|
||||||
public static class ZipSections {
|
|
||||||
private final long mCentralDirectoryOffset;
|
|
||||||
private final long mCentralDirectorySizeBytes;
|
|
||||||
private final int mCentralDirectoryRecordCount;
|
|
||||||
private final long mEocdOffset;
|
|
||||||
private final ByteBuffer mEocd;
|
|
||||||
|
|
||||||
public ZipSections(
|
|
||||||
long centralDirectoryOffset,
|
|
||||||
long centralDirectorySizeBytes,
|
|
||||||
int centralDirectoryRecordCount,
|
|
||||||
long eocdOffset,
|
|
||||||
ByteBuffer eocd) {
|
|
||||||
mCentralDirectoryOffset = centralDirectoryOffset;
|
|
||||||
mCentralDirectorySizeBytes = centralDirectorySizeBytes;
|
|
||||||
mCentralDirectoryRecordCount = centralDirectoryRecordCount;
|
|
||||||
mEocdOffset = eocdOffset;
|
|
||||||
mEocd = eocd;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the start offset of the ZIP Central Directory. This value is taken from the
|
|
||||||
* ZIP End of Central Directory record.
|
|
||||||
*/
|
|
||||||
public long getZipCentralDirectoryOffset() {
|
|
||||||
return mCentralDirectoryOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the
|
|
||||||
* ZIP End of Central Directory record.
|
|
||||||
*/
|
|
||||||
public long getZipCentralDirectorySizeBytes() {
|
|
||||||
return mCentralDirectorySizeBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of records in the ZIP Central Directory. This value is taken from the
|
|
||||||
* ZIP End of Central Directory record.
|
|
||||||
*/
|
|
||||||
public int getZipCentralDirectoryRecordCount() {
|
|
||||||
return mCentralDirectoryRecordCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the start offset of the ZIP End of Central Directory record. The record extends
|
|
||||||
* until the very end of the APK.
|
|
||||||
*/
|
|
||||||
public long getZipEndOfCentralDirectoryOffset() {
|
|
||||||
return mEocdOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the contents of the ZIP End of Central Directory.
|
|
||||||
*/
|
|
||||||
public ByteBuffer getZipEndOfCentralDirectory() {
|
|
||||||
return mEocd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the offset of the start of the ZIP Central Directory in the APK's ZIP End of Central
|
|
||||||
* Directory record.
|
|
||||||
*
|
|
||||||
* @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
|
|
||||||
* @param offset offset of the ZIP Central Directory relative to the start of the archive. Must
|
|
||||||
* be between {@code 0} and {@code 2^32 - 1} inclusive.
|
|
||||||
*/
|
|
||||||
public static void setZipEocdCentralDirectoryOffset(
|
|
||||||
ByteBuffer zipEndOfCentralDirectory, long offset) {
|
|
||||||
ByteBuffer eocd = zipEndOfCentralDirectory.slice();
|
|
||||||
eocd.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +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.apk.v1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Digest algorithm used with JAR signing (aka v1 signing scheme).
|
|
||||||
*/
|
|
||||||
public enum DigestAlgorithm {
|
|
||||||
/** SHA-1 */
|
|
||||||
SHA1("SHA-1"),
|
|
||||||
|
|
||||||
/** SHA2-256 */
|
|
||||||
SHA256("SHA-256");
|
|
||||||
|
|
||||||
private final String mJcaMessageDigestAlgorithm;
|
|
||||||
|
|
||||||
private DigestAlgorithm(String jcaMessageDigestAlgoritm) {
|
|
||||||
mJcaMessageDigestAlgorithm = jcaMessageDigestAlgoritm;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the {@link java.security.MessageDigest} algorithm represented by this digest
|
|
||||||
* algorithm.
|
|
||||||
*/
|
|
||||||
String getJcaMessageDigestAlgorithm() {
|
|
||||||
return mJcaMessageDigestAlgorithm;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,620 +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.apk.v1;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.PrivateKey;
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.Signature;
|
|
||||||
import java.security.SignatureException;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.CertificateParsingException;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.SortedMap;
|
|
||||||
import java.util.TreeMap;
|
|
||||||
import java.util.jar.Attributes;
|
|
||||||
import java.util.jar.Manifest;
|
|
||||||
|
|
||||||
import sun.security.pkcs.ContentInfo;
|
|
||||||
import sun.security.pkcs.PKCS7;
|
|
||||||
import sun.security.pkcs.SignerInfo;
|
|
||||||
import sun.security.x509.AlgorithmId;
|
|
||||||
import sun.security.x509.X500Name;
|
|
||||||
|
|
||||||
import com.android.apksigner.core.internal.jar.ManifestWriter;
|
|
||||||
import com.android.apksigner.core.internal.jar.SignatureFileWriter;
|
|
||||||
import com.android.apksigner.core.internal.util.Pair;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APK signer which uses JAR signing (aka v1 signing scheme).
|
|
||||||
*
|
|
||||||
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a>
|
|
||||||
*/
|
|
||||||
public abstract class V1SchemeSigner {
|
|
||||||
|
|
||||||
public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
|
|
||||||
|
|
||||||
private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY =
|
|
||||||
new Attributes.Name("Created-By");
|
|
||||||
private static final String ATTRIBUTE_DEFALT_VALUE_CREATED_BY = "1.0 (Android apksigner)";
|
|
||||||
private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0";
|
|
||||||
private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0";
|
|
||||||
|
|
||||||
static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR = "X-Android-APK-Signed";
|
|
||||||
private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME =
|
|
||||||
new Attributes.Name(SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signer configuration.
|
|
||||||
*/
|
|
||||||
public static class SignerConfig {
|
|
||||||
/** Name. */
|
|
||||||
public String name;
|
|
||||||
|
|
||||||
/** Private key. */
|
|
||||||
public PrivateKey privateKey;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Certificates, with the first certificate containing the public key corresponding to
|
|
||||||
* {@link #privateKey}.
|
|
||||||
*/
|
|
||||||
public List<X509Certificate> certificates;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Digest algorithm used for the signature.
|
|
||||||
*/
|
|
||||||
public DigestAlgorithm signatureDigestAlgorithm;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Digest algorithm used for digests of JAR entries and MANIFEST.MF.
|
|
||||||
*/
|
|
||||||
public DigestAlgorithm contentDigestAlgorithm;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Hidden constructor to prevent instantiation. */
|
|
||||||
private V1SchemeSigner() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the JAR signing digest algorithm to be used for signing an APK using the provided key.
|
|
||||||
*
|
|
||||||
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
|
|
||||||
* AndroidManifest.xml minSdkVersion attribute)
|
|
||||||
*
|
|
||||||
* @throws InvalidKeyException if the provided key is not suitable for signing APKs using
|
|
||||||
* JAR signing (aka v1 signature scheme)
|
|
||||||
*/
|
|
||||||
public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm(
|
|
||||||
PublicKey signingKey, int minSdkVersion) throws InvalidKeyException {
|
|
||||||
String keyAlgorithm = signingKey.getAlgorithm();
|
|
||||||
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
|
|
||||||
// Prior to API Level 18, only SHA-1 can be used with RSA.
|
|
||||||
if (minSdkVersion < 18) {
|
|
||||||
return DigestAlgorithm.SHA1;
|
|
||||||
}
|
|
||||||
return DigestAlgorithm.SHA256;
|
|
||||||
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
|
||||||
// Prior to API Level 21, only SHA-1 can be used with DSA
|
|
||||||
if (minSdkVersion < 21) {
|
|
||||||
return DigestAlgorithm.SHA1;
|
|
||||||
} else {
|
|
||||||
return DigestAlgorithm.SHA256;
|
|
||||||
}
|
|
||||||
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
|
||||||
if (minSdkVersion < 18) {
|
|
||||||
throw new InvalidKeyException(
|
|
||||||
"ECDSA signatures only supported for minSdkVersion 18 and higher");
|
|
||||||
}
|
|
||||||
// Prior to API Level 21, only SHA-1 can be used with ECDSA
|
|
||||||
if (minSdkVersion < 21) {
|
|
||||||
return DigestAlgorithm.SHA1;
|
|
||||||
} else {
|
|
||||||
return DigestAlgorithm.SHA256;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the JAR signing digest algorithm to be used for JAR entry digests.
|
|
||||||
*
|
|
||||||
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
|
|
||||||
* AndroidManifest.xml minSdkVersion attribute)
|
|
||||||
*/
|
|
||||||
public static DigestAlgorithm getSuggestedContentDigestAlgorithm(int minSdkVersion) {
|
|
||||||
return (minSdkVersion >= 18) ? DigestAlgorithm.SHA256 : DigestAlgorithm.SHA1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a new {@link MessageDigest} instance corresponding to the provided digest algorithm.
|
|
||||||
*/
|
|
||||||
private static MessageDigest getMessageDigestInstance(DigestAlgorithm digestAlgorithm)
|
|
||||||
throws NoSuchAlgorithmException {
|
|
||||||
String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm();
|
|
||||||
return MessageDigest.getInstance(jcaAlgorithm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the JCA {@link MessageDigest} algorithm corresponding to the provided digest
|
|
||||||
* algorithm.
|
|
||||||
*/
|
|
||||||
public static String getJcaMessageDigestAlgorithm(DigestAlgorithm digestAlgorithm) {
|
|
||||||
return digestAlgorithm.getJcaMessageDigestAlgorithm();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's
|
|
||||||
* manifest.
|
|
||||||
*/
|
|
||||||
public static boolean isJarEntryDigestNeededInManifest(String entryName) {
|
|
||||||
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File
|
|
||||||
|
|
||||||
// Entries outside of META-INF must be listed in the manifest.
|
|
||||||
if (!entryName.startsWith("META-INF/")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Entries in subdirectories of META-INF must be listed in the manifest.
|
|
||||||
if (entryName.indexOf('/', "META-INF/".length()) != -1) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignored file names (case-insensitive) in META-INF directory:
|
|
||||||
// MANIFEST.MF
|
|
||||||
// *.SF
|
|
||||||
// *.RSA
|
|
||||||
// *.DSA
|
|
||||||
// *.EC
|
|
||||||
// SIG-*
|
|
||||||
String fileNameLowerCase =
|
|
||||||
entryName.substring("META-INF/".length()).toLowerCase(Locale.US);
|
|
||||||
if (("manifest.mf".equals(fileNameLowerCase))
|
|
||||||
|| (fileNameLowerCase.endsWith(".sf"))
|
|
||||||
|| (fileNameLowerCase.endsWith(".rsa"))
|
|
||||||
|| (fileNameLowerCase.endsWith(".dsa"))
|
|
||||||
|| (fileNameLowerCase.endsWith(".ec"))
|
|
||||||
|| (fileNameLowerCase.startsWith("sig-"))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
|
|
||||||
* JAR entries which need to be added to the APK as part of the signature.
|
|
||||||
*
|
|
||||||
* @param signerConfigs signer configurations, one for each signer. At least one signer config
|
|
||||||
* must be provided.
|
|
||||||
*
|
|
||||||
* @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
|
|
||||||
* missing
|
|
||||||
* @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
|
|
||||||
* cannot be used in general
|
|
||||||
* @throws SignatureException if an error occurs when computing digests of generating
|
|
||||||
* signatures
|
|
||||||
*/
|
|
||||||
public static List<Pair<String, byte[]>> sign(
|
|
||||||
List<SignerConfig> signerConfigs,
|
|
||||||
DigestAlgorithm jarEntryDigestAlgorithm,
|
|
||||||
Map<String, byte[]> jarEntryDigests,
|
|
||||||
List<Integer> apkSigningSchemeIds,
|
|
||||||
byte[] sourceManifestBytes)
|
|
||||||
throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
|
|
||||||
SignatureException {
|
|
||||||
if (signerConfigs.isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("At least one signer config must be provided");
|
|
||||||
}
|
|
||||||
OutputManifestFile manifest =
|
|
||||||
generateManifestFile(jarEntryDigestAlgorithm, jarEntryDigests, sourceManifestBytes);
|
|
||||||
|
|
||||||
return signManifest(signerConfigs, jarEntryDigestAlgorithm, apkSigningSchemeIds, manifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
|
|
||||||
* JAR entries which need to be added to the APK as part of the signature.
|
|
||||||
*
|
|
||||||
* @param signerConfigs signer configurations, one for each signer. At least one signer config
|
|
||||||
* must be provided.
|
|
||||||
*
|
|
||||||
* @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
|
|
||||||
* cannot be used in general
|
|
||||||
* @throws SignatureException if an error occurs when computing digests of generating
|
|
||||||
* signatures
|
|
||||||
*/
|
|
||||||
public static List<Pair<String, byte[]>> signManifest(
|
|
||||||
List<SignerConfig> signerConfigs,
|
|
||||||
DigestAlgorithm digestAlgorithm,
|
|
||||||
List<Integer> apkSigningSchemeIds,
|
|
||||||
OutputManifestFile manifest)
|
|
||||||
throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
|
|
||||||
SignatureException {
|
|
||||||
if (signerConfigs.isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("At least one signer config must be provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
// For each signer output .SF and .(RSA|DSA|EC) file, then output MANIFEST.MF.
|
|
||||||
List<Pair<String, byte[]>> signatureJarEntries =
|
|
||||||
new ArrayList<>(2 * signerConfigs.size() + 1);
|
|
||||||
byte[] sfBytes =
|
|
||||||
generateSignatureFile(apkSigningSchemeIds, digestAlgorithm, manifest);
|
|
||||||
for (SignerConfig signerConfig : signerConfigs) {
|
|
||||||
String signerName = signerConfig.name;
|
|
||||||
byte[] signatureBlock;
|
|
||||||
try {
|
|
||||||
signatureBlock = generateSignatureBlock(signerConfig, sfBytes);
|
|
||||||
} catch (InvalidKeyException e) {
|
|
||||||
throw new InvalidKeyException(
|
|
||||||
"Failed to sign using signer \"" + signerName + "\"", e);
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
throw new CertificateException(
|
|
||||||
"Failed to sign using signer \"" + signerName + "\"", e);
|
|
||||||
} catch (SignatureException e) {
|
|
||||||
throw new SignatureException(
|
|
||||||
"Failed to sign using signer \"" + signerName + "\"", e);
|
|
||||||
}
|
|
||||||
signatureJarEntries.add(Pair.of("META-INF/" + signerName + ".SF", sfBytes));
|
|
||||||
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
|
||||||
String signatureBlockFileName =
|
|
||||||
"META-INF/" + signerName + "."
|
|
||||||
+ publicKey.getAlgorithm().toUpperCase(Locale.US);
|
|
||||||
signatureJarEntries.add(
|
|
||||||
Pair.of(signatureBlockFileName, signatureBlock));
|
|
||||||
}
|
|
||||||
signatureJarEntries.add(Pair.of(MANIFEST_ENTRY_NAME, manifest.contents));
|
|
||||||
return signatureJarEntries;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the names of JAR entries which this signer will produce as part of v1 signature.
|
|
||||||
*/
|
|
||||||
public static Set<String> getOutputEntryNames(List<SignerConfig> signerConfigs) {
|
|
||||||
Set<String> result = new HashSet<>(2 * signerConfigs.size() + 1);
|
|
||||||
for (SignerConfig signerConfig : signerConfigs) {
|
|
||||||
String signerName = signerConfig.name;
|
|
||||||
result.add("META-INF/" + signerName + ".SF");
|
|
||||||
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
|
||||||
String signatureBlockFileName =
|
|
||||||
"META-INF/" + signerName + "."
|
|
||||||
+ publicKey.getAlgorithm().toUpperCase(Locale.US);
|
|
||||||
result.add(signatureBlockFileName);
|
|
||||||
}
|
|
||||||
result.add(MANIFEST_ENTRY_NAME);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generated and returns the {@code META-INF/MANIFEST.MF} file based on the provided (optional)
|
|
||||||
* input {@code MANIFEST.MF} and digests of JAR entries covered by the manifest.
|
|
||||||
*/
|
|
||||||
public static OutputManifestFile generateManifestFile(
|
|
||||||
DigestAlgorithm jarEntryDigestAlgorithm,
|
|
||||||
Map<String, byte[]> jarEntryDigests,
|
|
||||||
byte[] sourceManifestBytes) {
|
|
||||||
Manifest sourceManifest = null;
|
|
||||||
if (sourceManifestBytes != null) {
|
|
||||||
try {
|
|
||||||
sourceManifest = new Manifest(new ByteArrayInputStream(sourceManifestBytes));
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IllegalArgumentException("Failed to parse source MANIFEST.MF", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ByteArrayOutputStream manifestOut = new ByteArrayOutputStream();
|
|
||||||
Attributes mainAttrs = new Attributes();
|
|
||||||
// Copy the main section from the source manifest (if provided). Otherwise use defaults.
|
|
||||||
if (sourceManifest != null) {
|
|
||||||
mainAttrs.putAll(sourceManifest.getMainAttributes());
|
|
||||||
} else {
|
|
||||||
mainAttrs.put(Attributes.Name.MANIFEST_VERSION, ATTRIBUTE_VALUE_MANIFEST_VERSION);
|
|
||||||
mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, ATTRIBUTE_DEFALT_VALUE_CREATED_BY);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
ManifestWriter.writeMainSection(manifestOut, mainAttrs);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> sortedEntryNames = new ArrayList<>(jarEntryDigests.keySet());
|
|
||||||
Collections.sort(sortedEntryNames);
|
|
||||||
SortedMap<String, byte[]> invidualSectionsContents = new TreeMap<>();
|
|
||||||
String entryDigestAttributeName = getEntryDigestAttributeName(jarEntryDigestAlgorithm);
|
|
||||||
for (String entryName : sortedEntryNames) {
|
|
||||||
byte[] entryDigest = jarEntryDigests.get(entryName);
|
|
||||||
Attributes entryAttrs = new Attributes();
|
|
||||||
entryAttrs.putValue(
|
|
||||||
entryDigestAttributeName,
|
|
||||||
Base64.getEncoder().encodeToString(entryDigest));
|
|
||||||
ByteArrayOutputStream sectionOut = new ByteArrayOutputStream();
|
|
||||||
byte[] sectionBytes;
|
|
||||||
try {
|
|
||||||
ManifestWriter.writeIndividualSection(sectionOut, entryName, entryAttrs);
|
|
||||||
sectionBytes = sectionOut.toByteArray();
|
|
||||||
manifestOut.write(sectionBytes);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
|
|
||||||
}
|
|
||||||
invidualSectionsContents.put(entryName, sectionBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
OutputManifestFile result = new OutputManifestFile();
|
|
||||||
result.contents = manifestOut.toByteArray();
|
|
||||||
result.mainSectionAttributes = mainAttrs;
|
|
||||||
result.individualSectionsContents = invidualSectionsContents;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class OutputManifestFile {
|
|
||||||
public byte[] contents;
|
|
||||||
public SortedMap<String, byte[]> individualSectionsContents;
|
|
||||||
public Attributes mainSectionAttributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] generateSignatureFile(
|
|
||||||
List<Integer> apkSignatureSchemeIds,
|
|
||||||
DigestAlgorithm manifestDigestAlgorithm,
|
|
||||||
OutputManifestFile manifest) throws NoSuchAlgorithmException {
|
|
||||||
Manifest sf = new Manifest();
|
|
||||||
Attributes mainAttrs = sf.getMainAttributes();
|
|
||||||
mainAttrs.put(Attributes.Name.SIGNATURE_VERSION, ATTRIBUTE_VALUE_SIGNATURE_VERSION);
|
|
||||||
mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, ATTRIBUTE_DEFALT_VALUE_CREATED_BY);
|
|
||||||
if (!apkSignatureSchemeIds.isEmpty()) {
|
|
||||||
// Add APK Signature Scheme v2 (and newer) signature stripping protection.
|
|
||||||
// This attribute indicates that this APK is supposed to have been signed using one or
|
|
||||||
// more APK-specific signature schemes in addition to the standard JAR signature scheme
|
|
||||||
// used by this code. APK signature verifier should reject the APK if it does not
|
|
||||||
// contain a signature for the signature scheme the verifier prefers out of this set.
|
|
||||||
StringBuilder attrValue = new StringBuilder();
|
|
||||||
for (int id : apkSignatureSchemeIds) {
|
|
||||||
if (attrValue.length() > 0) {
|
|
||||||
attrValue.append(", ");
|
|
||||||
}
|
|
||||||
attrValue.append(String.valueOf(id));
|
|
||||||
}
|
|
||||||
mainAttrs.put(
|
|
||||||
SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME,
|
|
||||||
attrValue.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add main attribute containing the digest of MANIFEST.MF.
|
|
||||||
MessageDigest md = getMessageDigestInstance(manifestDigestAlgorithm);
|
|
||||||
mainAttrs.putValue(
|
|
||||||
getManifestDigestAttributeName(manifestDigestAlgorithm),
|
|
||||||
Base64.getEncoder().encodeToString(md.digest(manifest.contents)));
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
||||||
try {
|
|
||||||
SignatureFileWriter.writeMainSection(out, mainAttrs);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("Failed to write in-memory .SF file", e);
|
|
||||||
}
|
|
||||||
String entryDigestAttributeName = getEntryDigestAttributeName(manifestDigestAlgorithm);
|
|
||||||
for (Map.Entry<String, byte[]> manifestSection
|
|
||||||
: manifest.individualSectionsContents.entrySet()) {
|
|
||||||
String sectionName = manifestSection.getKey();
|
|
||||||
byte[] sectionContents = manifestSection.getValue();
|
|
||||||
byte[] sectionDigest = md.digest(sectionContents);
|
|
||||||
Attributes attrs = new Attributes();
|
|
||||||
attrs.putValue(
|
|
||||||
entryDigestAttributeName,
|
|
||||||
Base64.getEncoder().encodeToString(sectionDigest));
|
|
||||||
|
|
||||||
try {
|
|
||||||
SignatureFileWriter.writeIndividualSection(out, sectionName, attrs);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("Failed to write in-memory .SF file", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A bug in the java.util.jar implementation of Android platforms up to version 1.6 will
|
|
||||||
// cause a spurious IOException to be thrown if the length of the signature file is a
|
|
||||||
// multiple of 1024 bytes. As a workaround, add an extra CRLF in this case.
|
|
||||||
if ((out.size() > 0) && ((out.size() % 1024) == 0)) {
|
|
||||||
try {
|
|
||||||
SignatureFileWriter.writeSectionDelimiter(out);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("Failed to write to ByteArrayOutputStream", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out.toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("restriction")
|
|
||||||
private static byte[] generateSignatureBlock(
|
|
||||||
SignerConfig signerConfig, byte[] signatureFileBytes)
|
|
||||||
throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
|
|
||||||
SignatureException {
|
|
||||||
List<X509Certificate> signerCerts = signerConfig.certificates;
|
|
||||||
X509Certificate signerCert = signerCerts.get(0);
|
|
||||||
PublicKey signerPublicKey = signerCert.getPublicKey();
|
|
||||||
DigestAlgorithm digestAlgorithm = signerConfig.signatureDigestAlgorithm;
|
|
||||||
Pair<String, AlgorithmId> signatureAlgs =
|
|
||||||
getSignerInfoSignatureAlgorithm(signerPublicKey, digestAlgorithm);
|
|
||||||
String jcaSignatureAlgorithm = signatureAlgs.getFirst();
|
|
||||||
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
|
|
||||||
signature.initSign(signerConfig.privateKey);
|
|
||||||
signature.update(signatureFileBytes);
|
|
||||||
byte[] signatureBytes = signature.sign();
|
|
||||||
|
|
||||||
X500Name issuerName;
|
|
||||||
try {
|
|
||||||
issuerName = new X500Name(signerCert.getIssuerX500Principal().getName());
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new CertificateParsingException(
|
|
||||||
"Failed to parse signer certificate issuer name", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
AlgorithmId digestAlgorithmId = getSignerInfoDigestAlgorithm(digestAlgorithm);
|
|
||||||
SignerInfo signerInfo =
|
|
||||||
new SignerInfo(
|
|
||||||
issuerName,
|
|
||||||
signerCert.getSerialNumber(),
|
|
||||||
digestAlgorithmId,
|
|
||||||
signatureAlgs.getSecond(),
|
|
||||||
signatureBytes);
|
|
||||||
PKCS7 pkcs7 =
|
|
||||||
new PKCS7(
|
|
||||||
new AlgorithmId[] {digestAlgorithmId},
|
|
||||||
new ContentInfo(ContentInfo.DATA_OID, null),
|
|
||||||
signerCerts.toArray(new X509Certificate[signerCerts.size()]),
|
|
||||||
new SignerInfo[] {signerInfo});
|
|
||||||
|
|
||||||
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
|
||||||
try {
|
|
||||||
pkcs7.encodeSignedData(result);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new SignatureException("Failed to encode PKCS#7 signed data", e);
|
|
||||||
}
|
|
||||||
return result.toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("restriction")
|
|
||||||
private static final AlgorithmId OID_DIGEST_SHA1 = getSupportedAlgorithmId("1.3.14.3.2.26");
|
|
||||||
@SuppressWarnings("restriction")
|
|
||||||
private static final AlgorithmId OID_DIGEST_SHA256 =
|
|
||||||
getSupportedAlgorithmId("2.16.840.1.101.3.4.2.1");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the {@code SignerInfo} {@code DigestAlgorithm} to use for {@code SignerInfo} signing
|
|
||||||
* using the specified digest algorithm.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("restriction")
|
|
||||||
private static AlgorithmId getSignerInfoDigestAlgorithm(DigestAlgorithm digestAlgorithm) {
|
|
||||||
switch (digestAlgorithm) {
|
|
||||||
case SHA1:
|
|
||||||
return OID_DIGEST_SHA1;
|
|
||||||
case SHA256:
|
|
||||||
return OID_DIGEST_SHA256;
|
|
||||||
default:
|
|
||||||
throw new RuntimeException("Unsupported digest algorithm: " + digestAlgorithm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the JCA {@link Signature} algorithm and {@code SignerInfo} {@code SignatureAlgorithm}
|
|
||||||
* to use for {@code SignerInfo} which signs with the specified key and digest algorithms.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("restriction")
|
|
||||||
private static Pair<String, AlgorithmId> getSignerInfoSignatureAlgorithm(
|
|
||||||
PublicKey publicKey, DigestAlgorithm digestAlgorithm) throws InvalidKeyException {
|
|
||||||
// NOTE: This method on purpose uses hard-coded OIDs instead of
|
|
||||||
// Algorithm.getId(JCA Signature Algorithm). This is to ensure that the generated SignedData
|
|
||||||
// is compatible with all targeted Android platforms and is not dependent on changes in the
|
|
||||||
// JCA Signature Algorithm -> OID mappings maintained by AlgorithmId.get(String).
|
|
||||||
|
|
||||||
String keyAlgorithm = publicKey.getAlgorithm();
|
|
||||||
String digestPrefixForSigAlg;
|
|
||||||
switch (digestAlgorithm) {
|
|
||||||
case SHA1:
|
|
||||||
digestPrefixForSigAlg = "SHA1";
|
|
||||||
break;
|
|
||||||
case SHA256:
|
|
||||||
digestPrefixForSigAlg = "SHA256";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Unexpected digest algorithm: " + digestAlgorithm);
|
|
||||||
}
|
|
||||||
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
|
|
||||||
return Pair.of(
|
|
||||||
digestPrefixForSigAlg + "withRSA",
|
|
||||||
getSupportedAlgorithmId("1.2.840.113549.1.1.1") // RSA encryption
|
|
||||||
);
|
|
||||||
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
|
||||||
AlgorithmId sigAlgId;
|
|
||||||
switch (digestAlgorithm) {
|
|
||||||
case SHA1:
|
|
||||||
sigAlgId = getSupportedAlgorithmId("1.2.840.10040.4.1"); // DSA
|
|
||||||
break;
|
|
||||||
case SHA256:
|
|
||||||
// DSA signatures with SHA-256 in SignedData are accepted by Android API Level
|
|
||||||
// 21 and higher. However, there are two ways to specify their SignedData
|
|
||||||
// SignatureAlgorithm: dsaWithSha256 (2.16.840.1.101.3.4.3.2) and
|
|
||||||
// dsa (1.2.840.10040.4.1). The latter works only on API Level 22+. Thus, we use
|
|
||||||
// the former.
|
|
||||||
sigAlgId =
|
|
||||||
getSupportedAlgorithmId("2.16.840.1.101.3.4.3.2"); // DSA with SHA-256
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Unexpected digest algorithm: " + digestAlgorithm);
|
|
||||||
}
|
|
||||||
return Pair.of(digestPrefixForSigAlg + "withDSA", sigAlgId);
|
|
||||||
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
|
||||||
AlgorithmId sigAlgId;
|
|
||||||
switch (digestAlgorithm) {
|
|
||||||
case SHA1:
|
|
||||||
sigAlgId = getSupportedAlgorithmId("1.2.840.10045.4.1"); // ECDSA with SHA-1
|
|
||||||
break;
|
|
||||||
case SHA256:
|
|
||||||
sigAlgId = getSupportedAlgorithmId("1.2.840.10045.4.3.2"); // ECDSA with SHA-256
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Unexpected digest algorithm: " + digestAlgorithm);
|
|
||||||
}
|
|
||||||
return Pair.of(digestPrefixForSigAlg + "withECDSA", sigAlgId);
|
|
||||||
} else {
|
|
||||||
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("restriction")
|
|
||||||
private static AlgorithmId getSupportedAlgorithmId(String oid) {
|
|
||||||
try {
|
|
||||||
return AlgorithmId.get(oid);
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new RuntimeException("Unsupported OID: " + oid, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) {
|
|
||||||
switch (digestAlgorithm) {
|
|
||||||
case SHA1:
|
|
||||||
return "SHA1-Digest";
|
|
||||||
case SHA256:
|
|
||||||
return "SHA-256-Digest";
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Unexpected content digest algorithm: " + digestAlgorithm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getManifestDigestAttributeName(DigestAlgorithm digestAlgorithm) {
|
|
||||||
switch (digestAlgorithm) {
|
|
||||||
case SHA1:
|
|
||||||
return "SHA1-Digest-Manifest";
|
|
||||||
case SHA256:
|
|
||||||
return "SHA-256-Digest-Manifest";
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Unexpected content digest algorithm: " + digestAlgorithm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,52 +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.apk.v2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APK Signature Scheme v2 content digest algorithm.
|
|
||||||
*/
|
|
||||||
public enum ContentDigestAlgorithm {
|
|
||||||
/** SHA2-256 over 1 MB chunks. */
|
|
||||||
CHUNKED_SHA256("SHA-256", 256 / 8),
|
|
||||||
|
|
||||||
/** SHA2-512 over 1 MB chunks. */
|
|
||||||
CHUNKED_SHA512("SHA-512", 512 / 8);
|
|
||||||
|
|
||||||
private final String mJcaMessageDigestAlgorithm;
|
|
||||||
private final int mChunkDigestOutputSizeBytes;
|
|
||||||
|
|
||||||
private ContentDigestAlgorithm(
|
|
||||||
String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) {
|
|
||||||
mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm;
|
|
||||||
mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
|
|
||||||
* chunks by this content digest algorithm.
|
|
||||||
*/
|
|
||||||
String getJcaMessageDigestAlgorithm() {
|
|
||||||
return mJcaMessageDigestAlgorithm;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the size (in bytes) of the digest of a chunk of content.
|
|
||||||
*/
|
|
||||||
int getChunkDigestOutputSizeBytes() {
|
|
||||||
return mChunkDigestOutputSizeBytes;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,142 +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.apk.v2;
|
|
||||||
|
|
||||||
import com.android.apksigner.core.internal.util.Pair;
|
|
||||||
|
|
||||||
import java.security.spec.AlgorithmParameterSpec;
|
|
||||||
import java.security.spec.MGF1ParameterSpec;
|
|
||||||
import java.security.spec.PSSParameterSpec;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APK Signature Scheme v2 signature algorithm.
|
|
||||||
*/
|
|
||||||
public enum SignatureAlgorithm {
|
|
||||||
/**
|
|
||||||
* RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content
|
|
||||||
* digested using SHA2-256 in 1 MB chunks.
|
|
||||||
*/
|
|
||||||
RSA_PSS_WITH_SHA256(
|
|
||||||
0x0101,
|
|
||||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
|
||||||
"RSA",
|
|
||||||
Pair.of("SHA256withRSA/PSS",
|
|
||||||
new PSSParameterSpec(
|
|
||||||
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1))),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content
|
|
||||||
* digested using SHA2-512 in 1 MB chunks.
|
|
||||||
*/
|
|
||||||
RSA_PSS_WITH_SHA512(
|
|
||||||
0x0102,
|
|
||||||
ContentDigestAlgorithm.CHUNKED_SHA512,
|
|
||||||
"RSA",
|
|
||||||
Pair.of(
|
|
||||||
"SHA512withRSA/PSS",
|
|
||||||
new PSSParameterSpec(
|
|
||||||
"SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1))),
|
|
||||||
|
|
||||||
/** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
|
|
||||||
RSA_PKCS1_V1_5_WITH_SHA256(
|
|
||||||
0x0103,
|
|
||||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
|
||||||
"RSA",
|
|
||||||
Pair.of("SHA256withRSA", null)),
|
|
||||||
|
|
||||||
/** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
|
|
||||||
RSA_PKCS1_V1_5_WITH_SHA512(
|
|
||||||
0x0104,
|
|
||||||
ContentDigestAlgorithm.CHUNKED_SHA512,
|
|
||||||
"RSA",
|
|
||||||
Pair.of("SHA512withRSA", null)),
|
|
||||||
|
|
||||||
/** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
|
|
||||||
ECDSA_WITH_SHA256(
|
|
||||||
0x0201,
|
|
||||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
|
||||||
"EC",
|
|
||||||
Pair.of("SHA256withECDSA", null)),
|
|
||||||
|
|
||||||
/** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
|
|
||||||
ECDSA_WITH_SHA512(
|
|
||||||
0x0202,
|
|
||||||
ContentDigestAlgorithm.CHUNKED_SHA512,
|
|
||||||
"EC",
|
|
||||||
Pair.of("SHA512withECDSA", null)),
|
|
||||||
|
|
||||||
/** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
|
|
||||||
DSA_WITH_SHA256(
|
|
||||||
0x0301,
|
|
||||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
|
||||||
"DSA",
|
|
||||||
Pair.of("SHA256withDSA", null));
|
|
||||||
|
|
||||||
private final int mId;
|
|
||||||
private final String mJcaKeyAlgorithm;
|
|
||||||
private final ContentDigestAlgorithm mContentDigestAlgorithm;
|
|
||||||
private final Pair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams;
|
|
||||||
|
|
||||||
private SignatureAlgorithm(int id,
|
|
||||||
ContentDigestAlgorithm contentDigestAlgorithm,
|
|
||||||
String jcaKeyAlgorithm,
|
|
||||||
Pair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams) {
|
|
||||||
mId = id;
|
|
||||||
mContentDigestAlgorithm = contentDigestAlgorithm;
|
|
||||||
mJcaKeyAlgorithm = jcaKeyAlgorithm;
|
|
||||||
mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format.
|
|
||||||
*/
|
|
||||||
int getId() {
|
|
||||||
return mId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the content digest algorithm associated with this signature algorithm.
|
|
||||||
*/
|
|
||||||
ContentDigestAlgorithm getContentDigestAlgorithm() {
|
|
||||||
return mContentDigestAlgorithm;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the JCA {@link java.security.Key} algorithm used by this signature scheme.
|
|
||||||
*/
|
|
||||||
String getJcaKeyAlgorithm() {
|
|
||||||
return mJcaKeyAlgorithm;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec}
|
|
||||||
* (or null if not needed) to parameterize the {@code Signature}.
|
|
||||||
*/
|
|
||||||
Pair<String, ? extends AlgorithmParameterSpec> getJcaSignatureAlgorithmAndParams() {
|
|
||||||
return mJcaSignatureAlgAndParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
static SignatureAlgorithm findById(int id) {
|
|
||||||
for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
|
|
||||||
if (alg.getId() == id) {
|
|
||||||
return alg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,600 +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.apk.v2;
|
|
||||||
|
|
||||||
import com.android.apksigner.core.internal.util.MessageDigestSink;
|
|
||||||
import com.android.apksigner.core.internal.util.Pair;
|
|
||||||
import com.android.apksigner.core.internal.zip.ZipUtils;
|
|
||||||
import com.android.apksigner.core.util.DataSource;
|
|
||||||
import com.android.apksigner.core.util.DataSources;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.security.DigestException;
|
|
||||||
import java.security.InvalidAlgorithmParameterException;
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.KeyFactory;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.PrivateKey;
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.Signature;
|
|
||||||
import java.security.SignatureException;
|
|
||||||
import java.security.cert.CertificateEncodingException;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.security.interfaces.ECKey;
|
|
||||||
import java.security.interfaces.RSAKey;
|
|
||||||
import java.security.spec.AlgorithmParameterSpec;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.security.spec.X509EncodedKeySpec;
|
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APK Signature Scheme v2 signer.
|
|
||||||
*
|
|
||||||
* <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
|
|
||||||
* bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
|
|
||||||
* uncompressed contents of ZIP entries.
|
|
||||||
*
|
|
||||||
* <p>TODO: Link to APK Signature Scheme v2 documentation once it's available.
|
|
||||||
*/
|
|
||||||
public abstract class V2SchemeSigner {
|
|
||||||
/*
|
|
||||||
* The two main goals of APK Signature Scheme v2 are:
|
|
||||||
* 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature
|
|
||||||
* cover every byte of the APK being signed.
|
|
||||||
* 2. Enable much faster signature and integrity verification. This is achieved by requiring
|
|
||||||
* only a minimal amount of APK parsing before the signature is verified, thus completely
|
|
||||||
* bypassing ZIP entry decompression and by making integrity verification parallelizable by
|
|
||||||
* employing a hash tree.
|
|
||||||
*
|
|
||||||
* The generated signature block is wrapped into an APK Signing Block and inserted into the
|
|
||||||
* original APK immediately before the start of ZIP Central Directory. This is to ensure that
|
|
||||||
* JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for
|
|
||||||
* extensibility. For example, a future signature scheme could insert its signatures there as
|
|
||||||
* well. The contract of the APK Signing Block is that all contents outside of the block must be
|
|
||||||
* protected by signatures inside the block.
|
|
||||||
*/
|
|
||||||
|
|
||||||
private static final int CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
|
|
||||||
|
|
||||||
private static final byte[] APK_SIGNING_BLOCK_MAGIC =
|
|
||||||
new byte[] {
|
|
||||||
0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20,
|
|
||||||
0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32,
|
|
||||||
};
|
|
||||||
private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signer configuration.
|
|
||||||
*/
|
|
||||||
public static class SignerConfig {
|
|
||||||
/** Private key. */
|
|
||||||
public PrivateKey privateKey;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Certificates, with the first certificate containing the public key corresponding to
|
|
||||||
* {@link #privateKey}.
|
|
||||||
*/
|
|
||||||
public List<X509Certificate> certificates;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of signature algorithms with which to sign.
|
|
||||||
*/
|
|
||||||
public List<SignatureAlgorithm> signatureAlgorithms;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Hidden constructor to prevent instantiation. */
|
|
||||||
private V2SchemeSigner() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the APK Signature Scheme v2 signature algorithms to be used for signing an APK using the
|
|
||||||
* provided key.
|
|
||||||
*
|
|
||||||
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
|
|
||||||
* AndroidManifest.xml minSdkVersion attribute).
|
|
||||||
*
|
|
||||||
* @throws InvalidKeyException if the provided key is not suitable for signing APKs using
|
|
||||||
* APK Signature Scheme v2
|
|
||||||
*/
|
|
||||||
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(
|
|
||||||
PublicKey signingKey, int minSdkVersion) throws InvalidKeyException {
|
|
||||||
String keyAlgorithm = signingKey.getAlgorithm();
|
|
||||||
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
|
|
||||||
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
|
|
||||||
// deterministic signatures which make life easier for OTA updates (fewer files
|
|
||||||
// changed when deterministic signature schemes are used).
|
|
||||||
|
|
||||||
// Pick a digest which is no weaker than the key.
|
|
||||||
int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
|
|
||||||
if (modulusLengthBits <= 3072) {
|
|
||||||
// 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
|
|
||||||
return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
|
|
||||||
} else {
|
|
||||||
// Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
|
|
||||||
// digest being the weak link. SHA-512 is the next strongest supported digest.
|
|
||||||
return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
|
|
||||||
}
|
|
||||||
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
|
||||||
// DSA is supported only with SHA-256.
|
|
||||||
return Collections.singletonList(SignatureAlgorithm.DSA_WITH_SHA256);
|
|
||||||
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
|
||||||
// Pick a digest which is no weaker than the key.
|
|
||||||
int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
|
|
||||||
if (keySizeBits <= 256) {
|
|
||||||
// 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
|
|
||||||
return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA256);
|
|
||||||
} else {
|
|
||||||
// Keys longer than 256 bit need to be paired with a stronger digest to avoid the
|
|
||||||
// digest being the weak link. SHA-512 is the next strongest supported digest.
|
|
||||||
return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signs the provided APK using APK Signature Scheme v2 and returns the APK Signing Block
|
|
||||||
* containing the signature.
|
|
||||||
*
|
|
||||||
* @param signerConfigs signer configurations, one for each signer At least one signer config
|
|
||||||
* must be provided.
|
|
||||||
*
|
|
||||||
* @throws IOException if an I/O error occurs
|
|
||||||
* @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
|
|
||||||
* missing
|
|
||||||
* @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
|
|
||||||
* cannot be used in general
|
|
||||||
* @throws SignatureException if an error occurs when computing digests of generating
|
|
||||||
* signatures
|
|
||||||
*/
|
|
||||||
public static byte[] generateApkSigningBlock(
|
|
||||||
DataSource beforeCentralDir,
|
|
||||||
DataSource centralDir,
|
|
||||||
DataSource eocd,
|
|
||||||
List<SignerConfig> signerConfigs)
|
|
||||||
throws IOException, NoSuchAlgorithmException, InvalidKeyException,
|
|
||||||
SignatureException {
|
|
||||||
if (signerConfigs.isEmpty()) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"No signer configs provided. At least one is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Figure out which digest(s) to use for APK contents.
|
|
||||||
Set<ContentDigestAlgorithm> contentDigestAlgorithms = new HashSet<>(1);
|
|
||||||
for (SignerConfig signerConfig : signerConfigs) {
|
|
||||||
for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
|
|
||||||
contentDigestAlgorithms.add(signatureAlgorithm.getContentDigestAlgorithm());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that, when digesting, ZIP End of Central Directory record's Central Directory
|
|
||||||
// offset field is treated as pointing to the offset at which the APK Signing Block will
|
|
||||||
// start.
|
|
||||||
long centralDirOffsetForDigesting = beforeCentralDir.size();
|
|
||||||
ByteBuffer eocdBuf = ByteBuffer.allocate((int) eocd.size());
|
|
||||||
eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
eocd.copyTo(0, (int) eocd.size(), eocdBuf);
|
|
||||||
eocdBuf.flip();
|
|
||||||
ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, centralDirOffsetForDigesting);
|
|
||||||
|
|
||||||
// Compute digests of APK contents.
|
|
||||||
Map<ContentDigestAlgorithm, byte[]> contentDigests; // digest algorithm ID -> digest
|
|
||||||
try {
|
|
||||||
contentDigests =
|
|
||||||
computeContentDigests(
|
|
||||||
contentDigestAlgorithms,
|
|
||||||
new DataSource[] {
|
|
||||||
beforeCentralDir,
|
|
||||||
centralDir,
|
|
||||||
DataSources.asDataSource(eocdBuf)});
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IOException("Failed to read APK being signed", e);
|
|
||||||
} catch (DigestException e) {
|
|
||||||
throw new SignatureException("Failed to compute digests of APK", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign the digests and wrap the signatures and signer info into an APK Signing Block.
|
|
||||||
return generateApkSigningBlock(signerConfigs, contentDigests);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
|
|
||||||
Set<ContentDigestAlgorithm> digestAlgorithms,
|
|
||||||
DataSource[] contents) throws IOException, NoSuchAlgorithmException, DigestException {
|
|
||||||
// For each digest algorithm the result is computed as follows:
|
|
||||||
// 1. Each segment of contents is split into consecutive chunks of 1 MB in size.
|
|
||||||
// The final chunk will be shorter iff the length of segment is not a multiple of 1 MB.
|
|
||||||
// No chunks are produced for empty (zero length) segments.
|
|
||||||
// 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's
|
|
||||||
// length in bytes (uint32 little-endian) and the chunk's contents.
|
|
||||||
// 3. The output digest is computed over the concatenation of the byte 0x5a, the number of
|
|
||||||
// chunks (uint32 little-endian) and the concatenation of digests of chunks of all
|
|
||||||
// segments in-order.
|
|
||||||
|
|
||||||
long chunkCountLong = 0;
|
|
||||||
for (DataSource input : contents) {
|
|
||||||
chunkCountLong +=
|
|
||||||
getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
|
|
||||||
}
|
|
||||||
if (chunkCountLong > Integer.MAX_VALUE) {
|
|
||||||
throw new DigestException("Input too long: " + chunkCountLong + " chunks");
|
|
||||||
}
|
|
||||||
int chunkCount = (int) chunkCountLong;
|
|
||||||
|
|
||||||
ContentDigestAlgorithm[] digestAlgorithmsArray =
|
|
||||||
digestAlgorithms.toArray(new ContentDigestAlgorithm[digestAlgorithms.size()]);
|
|
||||||
MessageDigest[] mds = new MessageDigest[digestAlgorithmsArray.length];
|
|
||||||
byte[][] digestsOfChunks = new byte[digestAlgorithmsArray.length][];
|
|
||||||
int[] digestOutputSizes = new int[digestAlgorithmsArray.length];
|
|
||||||
for (int i = 0; i < digestAlgorithmsArray.length; i++) {
|
|
||||||
ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i];
|
|
||||||
int digestOutputSizeBytes = digestAlgorithm.getChunkDigestOutputSizeBytes();
|
|
||||||
digestOutputSizes[i] = digestOutputSizeBytes;
|
|
||||||
byte[] concatenationOfChunkCountAndChunkDigests =
|
|
||||||
new byte[5 + chunkCount * digestOutputSizeBytes];
|
|
||||||
concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
|
|
||||||
setUnsignedInt32LittleEndian(
|
|
||||||
chunkCount, concatenationOfChunkCountAndChunkDigests, 1);
|
|
||||||
digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests;
|
|
||||||
String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm();
|
|
||||||
mds[i] = MessageDigest.getInstance(jcaAlgorithm);
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageDigestSink mdSink = new MessageDigestSink(mds);
|
|
||||||
byte[] chunkContentPrefix = new byte[5];
|
|
||||||
chunkContentPrefix[0] = (byte) 0xa5;
|
|
||||||
int chunkIndex = 0;
|
|
||||||
// Optimization opportunity: digests of chunks can be computed in parallel. However,
|
|
||||||
// determining the number of computations to be performed in parallel is non-trivial. This
|
|
||||||
// depends on a wide range of factors, such as data source type (e.g., in-memory or fetched
|
|
||||||
// from file), CPU/memory/disk cache bandwidth and latency, interconnect architecture of CPU
|
|
||||||
// cores, load on the system from other threads of execution and other processes, size of
|
|
||||||
// input.
|
|
||||||
// For now, we compute these digests sequentially and thus have the luxury of improving
|
|
||||||
// performance by writing the digest of each chunk into a pre-allocated buffer at exactly
|
|
||||||
// the right position. This avoids unnecessary allocations, copying, and enables the final
|
|
||||||
// digest to be more efficient because it's presented with all of its input in one go.
|
|
||||||
for (DataSource input : contents) {
|
|
||||||
long inputOffset = 0;
|
|
||||||
long inputRemaining = input.size();
|
|
||||||
while (inputRemaining > 0) {
|
|
||||||
int chunkSize =
|
|
||||||
(int) Math.min(inputRemaining, CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
|
|
||||||
setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1);
|
|
||||||
for (int i = 0; i < mds.length; i++) {
|
|
||||||
mds[i].update(chunkContentPrefix);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
input.feed(inputOffset, chunkSize, mdSink);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IOException("Failed to read chunk #" + chunkIndex, e);
|
|
||||||
}
|
|
||||||
for (int i = 0; i < digestAlgorithmsArray.length; i++) {
|
|
||||||
MessageDigest md = mds[i];
|
|
||||||
byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i];
|
|
||||||
int expectedDigestSizeBytes = digestOutputSizes[i];
|
|
||||||
int actualDigestSizeBytes =
|
|
||||||
md.digest(
|
|
||||||
concatenationOfChunkCountAndChunkDigests,
|
|
||||||
5 + chunkIndex * expectedDigestSizeBytes,
|
|
||||||
expectedDigestSizeBytes);
|
|
||||||
if (actualDigestSizeBytes != expectedDigestSizeBytes) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"Unexpected output size of " + md.getAlgorithm()
|
|
||||||
+ " digest: " + actualDigestSizeBytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inputOffset += chunkSize;
|
|
||||||
inputRemaining -= chunkSize;
|
|
||||||
chunkIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<ContentDigestAlgorithm, byte[]> result = new HashMap<>(digestAlgorithmsArray.length);
|
|
||||||
for (int i = 0; i < digestAlgorithmsArray.length; i++) {
|
|
||||||
ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i];
|
|
||||||
byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i];
|
|
||||||
MessageDigest md = mds[i];
|
|
||||||
byte[] digest = md.digest(concatenationOfChunkCountAndChunkDigests);
|
|
||||||
result.put(digestAlgorithm, digest);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final long getChunkCount(long inputSize, int chunkSize) {
|
|
||||||
return (inputSize + chunkSize - 1) / chunkSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void setUnsignedInt32LittleEndian(int value, byte[] result, int offset) {
|
|
||||||
result[offset] = (byte) (value & 0xff);
|
|
||||||
result[offset + 1] = (byte) ((value >> 8) & 0xff);
|
|
||||||
result[offset + 2] = (byte) ((value >> 16) & 0xff);
|
|
||||||
result[offset + 3] = (byte) ((value >> 24) & 0xff);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] generateApkSigningBlock(
|
|
||||||
List<SignerConfig> signerConfigs,
|
|
||||||
Map<ContentDigestAlgorithm, byte[]> contentDigests)
|
|
||||||
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
|
||||||
byte[] apkSignatureSchemeV2Block =
|
|
||||||
generateApkSignatureSchemeV2Block(signerConfigs, contentDigests);
|
|
||||||
return generateApkSigningBlock(apkSignatureSchemeV2Block);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] generateApkSigningBlock(byte[] apkSignatureSchemeV2Block) {
|
|
||||||
// FORMAT:
|
|
||||||
// uint64: size (excluding this field)
|
|
||||||
// repeated ID-value pairs:
|
|
||||||
// uint64: size (excluding this field)
|
|
||||||
// uint32: ID
|
|
||||||
// (size - 4) bytes: value
|
|
||||||
// uint64: size (same as the one above)
|
|
||||||
// uint128: magic
|
|
||||||
|
|
||||||
int resultSize =
|
|
||||||
8 // size
|
|
||||||
+ 8 + 4 + apkSignatureSchemeV2Block.length // v2Block as ID-value pair
|
|
||||||
+ 8 // size
|
|
||||||
+ 16 // magic
|
|
||||||
;
|
|
||||||
ByteBuffer result = ByteBuffer.allocate(resultSize);
|
|
||||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
long blockSizeFieldValue = resultSize - 8;
|
|
||||||
result.putLong(blockSizeFieldValue);
|
|
||||||
|
|
||||||
long pairSizeFieldValue = 4 + apkSignatureSchemeV2Block.length;
|
|
||||||
result.putLong(pairSizeFieldValue);
|
|
||||||
result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
|
|
||||||
result.put(apkSignatureSchemeV2Block);
|
|
||||||
|
|
||||||
result.putLong(blockSizeFieldValue);
|
|
||||||
result.put(APK_SIGNING_BLOCK_MAGIC);
|
|
||||||
|
|
||||||
return result.array();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] generateApkSignatureSchemeV2Block(
|
|
||||||
List<SignerConfig> signerConfigs,
|
|
||||||
Map<ContentDigestAlgorithm, byte[]> contentDigests)
|
|
||||||
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
|
||||||
// FORMAT:
|
|
||||||
// * length-prefixed sequence of length-prefixed signer blocks.
|
|
||||||
|
|
||||||
List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
|
|
||||||
int signerNumber = 0;
|
|
||||||
for (SignerConfig signerConfig : signerConfigs) {
|
|
||||||
signerNumber++;
|
|
||||||
byte[] signerBlock;
|
|
||||||
try {
|
|
||||||
signerBlock = generateSignerBlock(signerConfig, contentDigests);
|
|
||||||
} catch (InvalidKeyException e) {
|
|
||||||
throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
|
|
||||||
} catch (SignatureException e) {
|
|
||||||
throw new SignatureException("Signer #" + signerNumber + " failed", e);
|
|
||||||
}
|
|
||||||
signerBlocks.add(signerBlock);
|
|
||||||
}
|
|
||||||
|
|
||||||
return encodeAsSequenceOfLengthPrefixedElements(
|
|
||||||
new byte[][] {
|
|
||||||
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] generateSignerBlock(
|
|
||||||
SignerConfig signerConfig,
|
|
||||||
Map<ContentDigestAlgorithm, byte[]> contentDigests)
|
|
||||||
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
|
||||||
if (signerConfig.certificates.isEmpty()) {
|
|
||||||
throw new SignatureException("No certificates configured for signer");
|
|
||||||
}
|
|
||||||
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
|
||||||
|
|
||||||
byte[] encodedPublicKey = encodePublicKey(publicKey);
|
|
||||||
|
|
||||||
V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData();
|
|
||||||
try {
|
|
||||||
signedData.certificates = encodeCertificates(signerConfig.certificates);
|
|
||||||
} catch (CertificateEncodingException e) {
|
|
||||||
throw new SignatureException("Failed to encode certificates", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Pair<Integer, byte[]>> digests =
|
|
||||||
new ArrayList<>(signerConfig.signatureAlgorithms.size());
|
|
||||||
for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
|
|
||||||
ContentDigestAlgorithm contentDigestAlgorithm =
|
|
||||||
signatureAlgorithm.getContentDigestAlgorithm();
|
|
||||||
byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
|
|
||||||
if (contentDigest == null) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
contentDigestAlgorithm + " content digest for " + signatureAlgorithm
|
|
||||||
+ " not computed");
|
|
||||||
}
|
|
||||||
digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest));
|
|
||||||
}
|
|
||||||
signedData.digests = digests;
|
|
||||||
|
|
||||||
V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer();
|
|
||||||
// FORMAT:
|
|
||||||
// * length-prefixed sequence of length-prefixed digests:
|
|
||||||
// * uint32: signature algorithm ID
|
|
||||||
// * length-prefixed bytes: digest of contents
|
|
||||||
// * length-prefixed sequence of certificates:
|
|
||||||
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
|
|
||||||
// * length-prefixed sequence of length-prefixed additional attributes:
|
|
||||||
// * uint32: ID
|
|
||||||
// * (length - 4) bytes: value
|
|
||||||
signer.signedData = encodeAsSequenceOfLengthPrefixedElements(new byte[][] {
|
|
||||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(signedData.digests),
|
|
||||||
encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
|
|
||||||
// additional attributes
|
|
||||||
new byte[0],
|
|
||||||
});
|
|
||||||
signer.publicKey = encodedPublicKey;
|
|
||||||
signer.signatures = new ArrayList<>(signerConfig.signatureAlgorithms.size());
|
|
||||||
for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
|
|
||||||
Pair<String, ? extends AlgorithmParameterSpec> sigAlgAndParams =
|
|
||||||
signatureAlgorithm.getJcaSignatureAlgorithmAndParams();
|
|
||||||
String jcaSignatureAlgorithm = sigAlgAndParams.getFirst();
|
|
||||||
AlgorithmParameterSpec jcaSignatureAlgorithmParams = sigAlgAndParams.getSecond();
|
|
||||||
byte[] signatureBytes;
|
|
||||||
try {
|
|
||||||
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
|
|
||||||
signature.initSign(signerConfig.privateKey);
|
|
||||||
if (jcaSignatureAlgorithmParams != null) {
|
|
||||||
signature.setParameter(jcaSignatureAlgorithmParams);
|
|
||||||
}
|
|
||||||
signature.update(signer.signedData);
|
|
||||||
signatureBytes = signature.sign();
|
|
||||||
} catch (InvalidKeyException e) {
|
|
||||||
throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e);
|
|
||||||
} catch (InvalidAlgorithmParameterException | SignatureException e) {
|
|
||||||
throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
|
|
||||||
signature.initVerify(publicKey);
|
|
||||||
if (jcaSignatureAlgorithmParams != null) {
|
|
||||||
signature.setParameter(jcaSignatureAlgorithmParams);
|
|
||||||
}
|
|
||||||
signature.update(signer.signedData);
|
|
||||||
if (!signature.verify(signatureBytes)) {
|
|
||||||
throw new SignatureException("Signature did not verify");
|
|
||||||
}
|
|
||||||
} catch (InvalidKeyException e) {
|
|
||||||
throw new InvalidKeyException(
|
|
||||||
"Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
|
|
||||||
+ " public key from certificate", e);
|
|
||||||
} catch (InvalidAlgorithmParameterException | SignatureException e) {
|
|
||||||
throw new SignatureException(
|
|
||||||
"Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
|
|
||||||
+ " public key from certificate", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
signer.signatures.add(Pair.of(signatureAlgorithm.getId(), signatureBytes));
|
|
||||||
}
|
|
||||||
|
|
||||||
// FORMAT:
|
|
||||||
// * length-prefixed signed data
|
|
||||||
// * length-prefixed sequence of length-prefixed signatures:
|
|
||||||
// * uint32: signature algorithm ID
|
|
||||||
// * length-prefixed bytes: signature of signed data
|
|
||||||
// * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
|
|
||||||
return encodeAsSequenceOfLengthPrefixedElements(
|
|
||||||
new byte[][] {
|
|
||||||
signer.signedData,
|
|
||||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
|
||||||
signer.signatures),
|
|
||||||
signer.publicKey,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class V2SignatureSchemeBlock {
|
|
||||||
private static final class Signer {
|
|
||||||
public byte[] signedData;
|
|
||||||
public List<Pair<Integer, byte[]>> signatures;
|
|
||||||
public byte[] publicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class SignedData {
|
|
||||||
public List<Pair<Integer, byte[]>> digests;
|
|
||||||
public List<byte[]> certificates;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] encodePublicKey(PublicKey publicKey)
|
|
||||||
throws InvalidKeyException, NoSuchAlgorithmException {
|
|
||||||
byte[] encodedPublicKey = null;
|
|
||||||
if ("X.509".equals(publicKey.getFormat())) {
|
|
||||||
encodedPublicKey = publicKey.getEncoded();
|
|
||||||
}
|
|
||||||
if (encodedPublicKey == null) {
|
|
||||||
try {
|
|
||||||
encodedPublicKey =
|
|
||||||
KeyFactory.getInstance(publicKey.getAlgorithm())
|
|
||||||
.getKeySpec(publicKey, X509EncodedKeySpec.class)
|
|
||||||
.getEncoded();
|
|
||||||
} catch (InvalidKeySpecException e) {
|
|
||||||
throw new InvalidKeyException(
|
|
||||||
"Failed to obtain X.509 encoded form of public key " + publicKey
|
|
||||||
+ " of class " + publicKey.getClass().getName(),
|
|
||||||
e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((encodedPublicKey == null) || (encodedPublicKey.length == 0)) {
|
|
||||||
throw new InvalidKeyException(
|
|
||||||
"Failed to obtain X.509 encoded form of public key " + publicKey
|
|
||||||
+ " of class " + publicKey.getClass().getName());
|
|
||||||
}
|
|
||||||
return encodedPublicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<byte[]> encodeCertificates(List<X509Certificate> certificates)
|
|
||||||
throws CertificateEncodingException {
|
|
||||||
List<byte[]> result = new ArrayList<>(certificates.size());
|
|
||||||
for (X509Certificate certificate : certificates) {
|
|
||||||
result.add(certificate.getEncoded());
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] encodeAsSequenceOfLengthPrefixedElements(List<byte[]> sequence) {
|
|
||||||
return encodeAsSequenceOfLengthPrefixedElements(
|
|
||||||
sequence.toArray(new byte[sequence.size()][]));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence) {
|
|
||||||
int payloadSize = 0;
|
|
||||||
for (byte[] element : sequence) {
|
|
||||||
payloadSize += 4 + element.length;
|
|
||||||
}
|
|
||||||
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
|
||||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
for (byte[] element : sequence) {
|
|
||||||
result.putInt(element.length);
|
|
||||||
result.put(element);
|
|
||||||
}
|
|
||||||
return result.array();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
|
||||||
List<Pair<Integer, byte[]>> sequence) {
|
|
||||||
int resultSize = 0;
|
|
||||||
for (Pair<Integer, byte[]> element : sequence) {
|
|
||||||
resultSize += 12 + element.getSecond().length;
|
|
||||||
}
|
|
||||||
ByteBuffer result = ByteBuffer.allocate(resultSize);
|
|
||||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
for (Pair<Integer, byte[]> element : sequence) {
|
|
||||||
byte[] second = element.getSecond();
|
|
||||||
result.putInt(8 + second.length);
|
|
||||||
result.putInt(element.getFirst());
|
|
||||||
result.putInt(second.length);
|
|
||||||
result.put(second);
|
|
||||||
}
|
|
||||||
return result.array();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,955 +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.apk.v2;
|
|
||||||
|
|
||||||
import com.android.apksigner.core.ApkVerifier.Issue;
|
|
||||||
import com.android.apksigner.core.ApkVerifier.IssueWithParams;
|
|
||||||
import com.android.apksigner.core.apk.ApkUtils;
|
|
||||||
import com.android.apksigner.core.internal.util.ByteBufferDataSource;
|
|
||||||
import com.android.apksigner.core.internal.util.DelegatingX509Certificate;
|
|
||||||
import com.android.apksigner.core.internal.util.Pair;
|
|
||||||
import com.android.apksigner.core.internal.zip.ZipUtils;
|
|
||||||
import com.android.apksigner.core.util.DataSource;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.BufferUnderflowException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.security.DigestException;
|
|
||||||
import java.security.InvalidAlgorithmParameterException;
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.KeyFactory;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.Signature;
|
|
||||||
import java.security.SignatureException;
|
|
||||||
import java.security.cert.CertificateEncodingException;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.CertificateFactory;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.security.spec.AlgorithmParameterSpec;
|
|
||||||
import java.security.spec.X509EncodedKeySpec;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APK Signature Scheme v2 verifier.
|
|
||||||
*
|
|
||||||
* <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
|
|
||||||
* bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
|
|
||||||
* uncompressed contents of ZIP entries.
|
|
||||||
*
|
|
||||||
* <p>TODO: Link to APK Signature Scheme v2 documentation once it's available.
|
|
||||||
*/
|
|
||||||
public abstract class V2SchemeVerifier {
|
|
||||||
|
|
||||||
private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
|
|
||||||
private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
|
|
||||||
private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
|
|
||||||
|
|
||||||
private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
|
|
||||||
|
|
||||||
/** Hidden constructor to prevent instantiation. */
|
|
||||||
private V2SchemeVerifier() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies the provided APK's APK Signature Scheme v2 signatures and returns the result of
|
|
||||||
* verification. APK is considered verified only if {@link Result#verified} is {@code true}. If
|
|
||||||
* verification fails, the result will contain errors -- see {@link Result#getErrors()}.
|
|
||||||
*
|
|
||||||
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
|
|
||||||
* required cryptographic algorithm implementation is missing
|
|
||||||
* @throws SignatureNotFoundException if no APK Signature Scheme v2 signatures are found
|
|
||||||
* @throws IOException if an I/O error occurs when reading the APK
|
|
||||||
*/
|
|
||||||
public static Result verify(DataSource apk, ApkUtils.ZipSections zipSections)
|
|
||||||
throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
|
|
||||||
Result result = new Result();
|
|
||||||
SignatureInfo signatureInfo = findSignature(apk, zipSections, result);
|
|
||||||
|
|
||||||
DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
|
|
||||||
DataSource centralDir =
|
|
||||||
apk.slice(
|
|
||||||
signatureInfo.centralDirOffset,
|
|
||||||
signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
|
|
||||||
ByteBuffer eocd = signatureInfo.eocd;
|
|
||||||
|
|
||||||
verify(beforeApkSigningBlock,
|
|
||||||
signatureInfo.signatureBlock,
|
|
||||||
centralDir,
|
|
||||||
eocd,
|
|
||||||
result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies the provided APK's v2 signatures and outputs the results into the provided
|
|
||||||
* {@code result}. APK is considered verified only if there are no errors reported in the
|
|
||||||
* {@code result}.
|
|
||||||
*/
|
|
||||||
private static void verify(
|
|
||||||
DataSource beforeApkSigningBlock,
|
|
||||||
ByteBuffer apkSignatureSchemeV2Block,
|
|
||||||
DataSource centralDir,
|
|
||||||
ByteBuffer eocd,
|
|
||||||
Result result) throws IOException, NoSuchAlgorithmException {
|
|
||||||
Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
|
|
||||||
parseSigners(apkSignatureSchemeV2Block, contentDigestsToVerify, result);
|
|
||||||
if (result.containsErrors()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
verifyIntegrity(
|
|
||||||
beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
|
|
||||||
if (!result.containsErrors()) {
|
|
||||||
result.verified = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses each signer in the provided APK Signature Scheme v2 block and populates
|
|
||||||
* {@code signerInfos} of the provided {@code result}.
|
|
||||||
*
|
|
||||||
* <p>This verifies signatures over {@code signed-data} block contained in each signer block.
|
|
||||||
* However, this does not verify the integrity of the rest of the APK but rather simply reports
|
|
||||||
* the expected digests of the rest of the APK (see {@code contentDigestsToVerify}).
|
|
||||||
*/
|
|
||||||
private static void parseSigners(
|
|
||||||
ByteBuffer apkSignatureSchemeV2Block,
|
|
||||||
Set<ContentDigestAlgorithm> contentDigestsToVerify,
|
|
||||||
Result result) throws NoSuchAlgorithmException {
|
|
||||||
ByteBuffer signers;
|
|
||||||
try {
|
|
||||||
signers = getLengthPrefixedSlice(apkSignatureSchemeV2Block);
|
|
||||||
} catch (IOException e) {
|
|
||||||
result.addError(Issue.V2_SIG_MALFORMED_SIGNERS);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!signers.hasRemaining()) {
|
|
||||||
result.addError(Issue.V2_SIG_NO_SIGNERS);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CertificateFactory certFactory;
|
|
||||||
try {
|
|
||||||
certFactory = CertificateFactory.getInstance("X.509");
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
|
|
||||||
}
|
|
||||||
int signerCount = 0;
|
|
||||||
while (signers.hasRemaining()) {
|
|
||||||
int signerIndex = signerCount;
|
|
||||||
signerCount++;
|
|
||||||
Result.SignerInfo signerInfo = new Result.SignerInfo();
|
|
||||||
signerInfo.index = signerIndex;
|
|
||||||
result.signers.add(signerInfo);
|
|
||||||
try {
|
|
||||||
ByteBuffer signer = getLengthPrefixedSlice(signers);
|
|
||||||
parseSigner(signer, certFactory, signerInfo, contentDigestsToVerify);
|
|
||||||
} catch (IOException | BufferUnderflowException e) {
|
|
||||||
signerInfo.addError(Issue.V2_SIG_MALFORMED_SIGNER);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the provided signer block and populates the {@code result}.
|
|
||||||
*
|
|
||||||
* <p>This verifies signatures over {@code signed-data} contained in this block but does not
|
|
||||||
* verify the integrity of the rest of the APK. Rather, this method adds to the
|
|
||||||
* {@code contentDigestsToVerify}.
|
|
||||||
*/
|
|
||||||
private static void parseSigner(
|
|
||||||
ByteBuffer signerBlock,
|
|
||||||
CertificateFactory certFactory,
|
|
||||||
Result.SignerInfo result,
|
|
||||||
Set<ContentDigestAlgorithm> contentDigestsToVerify)
|
|
||||||
throws IOException, NoSuchAlgorithmException {
|
|
||||||
ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
|
|
||||||
byte[] signedDataBytes = new byte[signedData.remaining()];
|
|
||||||
signedData.get(signedDataBytes);
|
|
||||||
signedData.flip();
|
|
||||||
result.signedData = signedDataBytes;
|
|
||||||
|
|
||||||
ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
|
|
||||||
byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
|
|
||||||
|
|
||||||
// Parse the signatures block and identify supported signatures
|
|
||||||
int signatureCount = 0;
|
|
||||||
List<SupportedSignature> supportedSignatures = new ArrayList<>(1);
|
|
||||||
while (signatures.hasRemaining()) {
|
|
||||||
signatureCount++;
|
|
||||||
try {
|
|
||||||
ByteBuffer signature = getLengthPrefixedSlice(signatures);
|
|
||||||
int sigAlgorithmId = signature.getInt();
|
|
||||||
byte[] sigBytes = readLengthPrefixedByteArray(signature);
|
|
||||||
result.signatures.add(
|
|
||||||
new Result.SignerInfo.Signature(sigAlgorithmId, sigBytes));
|
|
||||||
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
|
|
||||||
if (signatureAlgorithm == null) {
|
|
||||||
result.addWarning(Issue.V2_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
supportedSignatures.add(new SupportedSignature(signatureAlgorithm, sigBytes));
|
|
||||||
} catch (IOException | BufferUnderflowException e) {
|
|
||||||
result.addError(Issue.V2_SIG_MALFORMED_SIGNATURE, signatureCount);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (result.signatures.isEmpty()) {
|
|
||||||
result.addError(Issue.V2_SIG_NO_SIGNATURES);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify signatures over signed-data block using the public key
|
|
||||||
List<SupportedSignature> signaturesToVerify = getSignaturesToVerify(supportedSignatures);
|
|
||||||
if (signaturesToVerify.isEmpty()) {
|
|
||||||
result.addError(Issue.V2_SIG_NO_SUPPORTED_SIGNATURES);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (SupportedSignature signature : signaturesToVerify) {
|
|
||||||
SignatureAlgorithm signatureAlgorithm = signature.algorithm;
|
|
||||||
String jcaSignatureAlgorithm =
|
|
||||||
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
|
||||||
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
|
||||||
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
|
||||||
String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
|
|
||||||
PublicKey publicKey;
|
|
||||||
try {
|
|
||||||
publicKey =
|
|
||||||
KeyFactory.getInstance(keyAlgorithm).generatePublic(
|
|
||||||
new X509EncodedKeySpec(publicKeyBytes));
|
|
||||||
} catch (Exception e) {
|
|
||||||
result.addError(Issue.V2_SIG_MALFORMED_PUBLIC_KEY, e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
|
||||||
sig.initVerify(publicKey);
|
|
||||||
if (jcaSignatureAlgorithmParams != null) {
|
|
||||||
sig.setParameter(jcaSignatureAlgorithmParams);
|
|
||||||
}
|
|
||||||
signedData.position(0);
|
|
||||||
sig.update(signedData);
|
|
||||||
byte[] sigBytes = signature.signature;
|
|
||||||
if (!sig.verify(sigBytes)) {
|
|
||||||
result.addError(Issue.V2_SIG_DID_NOT_VERIFY, signatureAlgorithm);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
|
|
||||||
contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
|
|
||||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException
|
|
||||||
| SignatureException e) {
|
|
||||||
result.addError(Issue.V2_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// At least one signature over signedData has verified. We can now parse signed-data.
|
|
||||||
signedData.position(0);
|
|
||||||
ByteBuffer digests = getLengthPrefixedSlice(signedData);
|
|
||||||
ByteBuffer certificates = getLengthPrefixedSlice(signedData);
|
|
||||||
ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData);
|
|
||||||
|
|
||||||
// Parse the certificates block
|
|
||||||
int certificateIndex = -1;
|
|
||||||
while (certificates.hasRemaining()) {
|
|
||||||
certificateIndex++;
|
|
||||||
byte[] encodedCert = readLengthPrefixedByteArray(certificates);
|
|
||||||
X509Certificate certificate;
|
|
||||||
try {
|
|
||||||
certificate =
|
|
||||||
(X509Certificate)
|
|
||||||
certFactory.generateCertificate(
|
|
||||||
new ByteArrayInputStream(encodedCert));
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
result.addError(
|
|
||||||
Issue.V2_SIG_MALFORMED_CERTIFICATE,
|
|
||||||
certificateIndex,
|
|
||||||
certificateIndex + 1,
|
|
||||||
e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
|
|
||||||
// form. Without this, getEncoded may return a different form from what was stored in
|
|
||||||
// the signature. This is becase some X509Certificate(Factory) implementations re-encode
|
|
||||||
// certificates.
|
|
||||||
certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
|
|
||||||
result.certs.add(certificate);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.certs.isEmpty()) {
|
|
||||||
result.addError(Issue.V2_SIG_NO_CERTIFICATES);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
X509Certificate mainCertificate = result.certs.get(0);
|
|
||||||
byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
|
|
||||||
if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
|
|
||||||
result.addError(
|
|
||||||
Issue.V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
|
|
||||||
toHex(certificatePublicKeyBytes),
|
|
||||||
toHex(publicKeyBytes));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the digests block
|
|
||||||
int digestCount = 0;
|
|
||||||
while (digests.hasRemaining()) {
|
|
||||||
digestCount++;
|
|
||||||
try {
|
|
||||||
ByteBuffer digest = getLengthPrefixedSlice(digests);
|
|
||||||
int sigAlgorithmId = digest.getInt();
|
|
||||||
byte[] digestBytes = readLengthPrefixedByteArray(digest);
|
|
||||||
result.contentDigests.add(
|
|
||||||
new Result.SignerInfo.ContentDigest(sigAlgorithmId, digestBytes));
|
|
||||||
} catch (IOException | BufferUnderflowException e) {
|
|
||||||
result.addError(Issue.V2_SIG_MALFORMED_DIGEST, digestCount);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size());
|
|
||||||
for (Result.SignerInfo.Signature signature : result.signatures) {
|
|
||||||
sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId());
|
|
||||||
}
|
|
||||||
List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size());
|
|
||||||
for (Result.SignerInfo.ContentDigest digest : result.contentDigests) {
|
|
||||||
sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) {
|
|
||||||
result.addError(
|
|
||||||
Issue.V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS,
|
|
||||||
sigAlgsFromSignaturesRecord,
|
|
||||||
sigAlgsFromDigestsRecord);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the additional attributes block.
|
|
||||||
int additionalAttributeCount = 0;
|
|
||||||
while (additionalAttributes.hasRemaining()) {
|
|
||||||
additionalAttributeCount++;
|
|
||||||
try {
|
|
||||||
ByteBuffer attribute = getLengthPrefixedSlice(additionalAttributes);
|
|
||||||
int id = attribute.getInt();
|
|
||||||
byte[] value = readLengthPrefixedByteArray(attribute);
|
|
||||||
result.additionalAttributes.add(
|
|
||||||
new Result.SignerInfo.AdditionalAttribute(id, value));
|
|
||||||
result.addWarning(Issue.V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
|
|
||||||
} catch (IOException | BufferUnderflowException e) {
|
|
||||||
result.addError(
|
|
||||||
Issue.V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<SupportedSignature> getSignaturesToVerify(
|
|
||||||
List<SupportedSignature> signatures) {
|
|
||||||
// Pick the signature with the strongest algorithm, to mimic Android's behavior.
|
|
||||||
SignatureAlgorithm bestSigAlgorithm = null;
|
|
||||||
byte[] bestSigAlgorithmSignatureBytes = null;
|
|
||||||
for (SupportedSignature sig : signatures) {
|
|
||||||
SignatureAlgorithm sigAlgorithm = sig.algorithm;
|
|
||||||
if ((bestSigAlgorithm == null)
|
|
||||||
|| (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) {
|
|
||||||
bestSigAlgorithm = sigAlgorithm;
|
|
||||||
bestSigAlgorithmSignatureBytes = sig.signature;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bestSigAlgorithm == null) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
} else {
|
|
||||||
return Collections.singletonList(
|
|
||||||
new SupportedSignature(bestSigAlgorithm, bestSigAlgorithmSignatureBytes));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class SupportedSignature {
|
|
||||||
private final SignatureAlgorithm algorithm;
|
|
||||||
private final byte[] signature;
|
|
||||||
|
|
||||||
private SupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
|
|
||||||
this.algorithm = algorithm;
|
|
||||||
this.signature = signature;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
|
|
||||||
* {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
|
|
||||||
*/
|
|
||||||
private static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
|
|
||||||
ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm();
|
|
||||||
ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm();
|
|
||||||
return compareContentDigestAlgorithm(digestAlg1, digestAlg2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
|
|
||||||
* {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
|
|
||||||
*/
|
|
||||||
private static int compareContentDigestAlgorithm(
|
|
||||||
ContentDigestAlgorithm alg1,
|
|
||||||
ContentDigestAlgorithm alg2) {
|
|
||||||
switch (alg1) {
|
|
||||||
case CHUNKED_SHA256:
|
|
||||||
switch (alg2) {
|
|
||||||
case CHUNKED_SHA256:
|
|
||||||
return 0;
|
|
||||||
case CHUNKED_SHA512:
|
|
||||||
return -1;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Unknown alg2: " + alg2);
|
|
||||||
}
|
|
||||||
case CHUNKED_SHA512:
|
|
||||||
switch (alg2) {
|
|
||||||
case CHUNKED_SHA256:
|
|
||||||
return 1;
|
|
||||||
case CHUNKED_SHA512:
|
|
||||||
return 0;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Unknown alg2: " + alg2);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Unknown alg1: " + alg1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies integrity of the APK outside of the APK Signing Block by computing digests of the
|
|
||||||
* APK and comparing them against the digests listed in APK Signing Block. The expected digests
|
|
||||||
* taken from {@code v2SchemeSignerInfos} of the provided {@code result}.
|
|
||||||
*/
|
|
||||||
private static void verifyIntegrity(
|
|
||||||
DataSource beforeApkSigningBlock,
|
|
||||||
DataSource centralDir,
|
|
||||||
ByteBuffer eocd,
|
|
||||||
Set<ContentDigestAlgorithm> contentDigestAlgorithms,
|
|
||||||
Result result) throws IOException, NoSuchAlgorithmException {
|
|
||||||
if (contentDigestAlgorithms.isEmpty()) {
|
|
||||||
// This should never occur because this method is invoked once at least one signature
|
|
||||||
// is verified, meaning at least one content digest is known.
|
|
||||||
throw new RuntimeException("No content digests found");
|
|
||||||
}
|
|
||||||
|
|
||||||
// For the purposes of verifying integrity, ZIP End of Central Directory (EoCD) must be
|
|
||||||
// treated as though its Central Directory offset points to the start of APK Signing Block.
|
|
||||||
// We thus modify the EoCD accordingly.
|
|
||||||
ByteBuffer modifiedEocd = ByteBuffer.allocate(eocd.remaining());
|
|
||||||
modifiedEocd.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
modifiedEocd.put(eocd);
|
|
||||||
modifiedEocd.flip();
|
|
||||||
ZipUtils.setZipEocdCentralDirectoryOffset(modifiedEocd, beforeApkSigningBlock.size());
|
|
||||||
Map<ContentDigestAlgorithm, byte[]> actualContentDigests;
|
|
||||||
try {
|
|
||||||
actualContentDigests =
|
|
||||||
V2SchemeSigner.computeContentDigests(
|
|
||||||
contentDigestAlgorithms,
|
|
||||||
new DataSource[] {
|
|
||||||
beforeApkSigningBlock,
|
|
||||||
centralDir,
|
|
||||||
new ByteBufferDataSource(modifiedEocd)
|
|
||||||
});
|
|
||||||
} catch (DigestException e) {
|
|
||||||
throw new RuntimeException("Failed to compute content digests", e);
|
|
||||||
}
|
|
||||||
if (!contentDigestAlgorithms.equals(actualContentDigests.keySet())) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"Mismatch between sets of requested and computed content digests"
|
|
||||||
+ " . Requested: " + contentDigestAlgorithms
|
|
||||||
+ ", computed: " + actualContentDigests.keySet());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare digests computed over the rest of APK against the corresponding expected digests
|
|
||||||
// in signer blocks.
|
|
||||||
for (Result.SignerInfo signerInfo : result.signers) {
|
|
||||||
for (Result.SignerInfo.ContentDigest expected : signerInfo.contentDigests) {
|
|
||||||
SignatureAlgorithm signatureAlgorithm =
|
|
||||||
SignatureAlgorithm.findById(expected.getSignatureAlgorithmId());
|
|
||||||
if (signatureAlgorithm == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ContentDigestAlgorithm contentDigestAlgorithm =
|
|
||||||
signatureAlgorithm.getContentDigestAlgorithm();
|
|
||||||
byte[] expectedDigest = expected.getValue();
|
|
||||||
byte[] actualDigest = actualContentDigests.get(contentDigestAlgorithm);
|
|
||||||
if (!Arrays.equals(expectedDigest, actualDigest)) {
|
|
||||||
signerInfo.addError(
|
|
||||||
Issue.V2_SIG_APK_DIGEST_DID_NOT_VERIFY,
|
|
||||||
contentDigestAlgorithm,
|
|
||||||
toHex(expectedDigest),
|
|
||||||
toHex(actualDigest));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
signerInfo.verifiedContentDigests.put(contentDigestAlgorithm, actualDigest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APK Signature Scheme v2 block and additional information relevant to verifying the signatures
|
|
||||||
* contained in the block against the file.
|
|
||||||
*/
|
|
||||||
private static class SignatureInfo {
|
|
||||||
/** Contents of APK Signature Scheme v2 block. */
|
|
||||||
private final ByteBuffer signatureBlock;
|
|
||||||
|
|
||||||
/** Position of the APK Signing Block in the file. */
|
|
||||||
private final long apkSigningBlockOffset;
|
|
||||||
|
|
||||||
/** Position of the ZIP Central Directory in the file. */
|
|
||||||
private final long centralDirOffset;
|
|
||||||
|
|
||||||
/** Position of the ZIP End of Central Directory (EoCD) in the file. */
|
|
||||||
private final long eocdOffset;
|
|
||||||
|
|
||||||
/** Contents of ZIP End of Central Directory (EoCD) of the file. */
|
|
||||||
private final ByteBuffer eocd;
|
|
||||||
|
|
||||||
private SignatureInfo(
|
|
||||||
ByteBuffer signatureBlock,
|
|
||||||
long apkSigningBlockOffset,
|
|
||||||
long centralDirOffset,
|
|
||||||
long eocdOffset,
|
|
||||||
ByteBuffer eocd) {
|
|
||||||
this.signatureBlock = signatureBlock;
|
|
||||||
this.apkSigningBlockOffset = apkSigningBlockOffset;
|
|
||||||
this.centralDirOffset = centralDirOffset;
|
|
||||||
this.eocdOffset = eocdOffset;
|
|
||||||
this.eocd = eocd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the APK Signature Scheme v2 block contained in the provided APK file and the
|
|
||||||
* additional information relevant for verifying the block against the file.
|
|
||||||
*
|
|
||||||
* @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2
|
|
||||||
* @throws IOException if an I/O error occurs while reading the APK
|
|
||||||
*/
|
|
||||||
private static SignatureInfo findSignature(
|
|
||||||
DataSource apk, ApkUtils.ZipSections zipSections, Result result)
|
|
||||||
throws IOException, SignatureNotFoundException {
|
|
||||||
// Find the APK Signing Block. The block immediately precedes the Central Directory.
|
|
||||||
ByteBuffer eocd = zipSections.getZipEndOfCentralDirectory();
|
|
||||||
Pair<DataSource, Long> apkSigningBlockAndOffset = findApkSigningBlock(apk, zipSections);
|
|
||||||
DataSource apkSigningBlock = apkSigningBlockAndOffset.getFirst();
|
|
||||||
long apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond();
|
|
||||||
ByteBuffer apkSigningBlockBuf =
|
|
||||||
apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size());
|
|
||||||
apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
|
|
||||||
// Find the APK Signature Scheme v2 Block inside the APK Signing Block.
|
|
||||||
ByteBuffer apkSignatureSchemeV2Block =
|
|
||||||
findApkSignatureSchemeV2Block(apkSigningBlockBuf, result);
|
|
||||||
|
|
||||||
return new SignatureInfo(
|
|
||||||
apkSignatureSchemeV2Block,
|
|
||||||
apkSigningBlockOffset,
|
|
||||||
zipSections.getZipCentralDirectoryOffset(),
|
|
||||||
zipSections.getZipEndOfCentralDirectoryOffset(),
|
|
||||||
eocd);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the APK Signing Block and its offset in the provided APK.
|
|
||||||
*
|
|
||||||
* @throws SignatureNotFoundException if the APK does not contain an APK Signing Block
|
|
||||||
*/
|
|
||||||
public static Pair<DataSource, Long> findApkSigningBlock(
|
|
||||||
DataSource apk, ApkUtils.ZipSections zipSections)
|
|
||||||
throws IOException, SignatureNotFoundException {
|
|
||||||
// FORMAT:
|
|
||||||
// OFFSET DATA TYPE DESCRIPTION
|
|
||||||
// * @+0 bytes uint64: size in bytes (excluding this field)
|
|
||||||
// * @+8 bytes payload
|
|
||||||
// * @-24 bytes uint64: size in bytes (same as the one above)
|
|
||||||
// * @-16 bytes uint128: magic
|
|
||||||
|
|
||||||
long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
|
|
||||||
long centralDirEndOffset =
|
|
||||||
centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
|
|
||||||
long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
|
|
||||||
if (centralDirEndOffset != eocdStartOffset) {
|
|
||||||
throw new SignatureNotFoundException(
|
|
||||||
"ZIP Central Directory is not immediately followed by End of Central Directory"
|
|
||||||
+ ". CD end: " + centralDirEndOffset
|
|
||||||
+ ", EoCD start: " + eocdStartOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) {
|
|
||||||
throw new SignatureNotFoundException(
|
|
||||||
"APK too small for APK Signing Block. ZIP Central Directory offset: "
|
|
||||||
+ centralDirStartOffset);
|
|
||||||
}
|
|
||||||
// Read the magic and offset in file from the footer section of the block:
|
|
||||||
// * uint64: size of block
|
|
||||||
// * 16 bytes: magic
|
|
||||||
ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24);
|
|
||||||
footer.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
|
|
||||||
|| (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
|
|
||||||
throw new SignatureNotFoundException(
|
|
||||||
"No APK Signing Block before ZIP Central Directory");
|
|
||||||
}
|
|
||||||
// Read and compare size fields
|
|
||||||
long apkSigBlockSizeInFooter = footer.getLong(0);
|
|
||||||
if ((apkSigBlockSizeInFooter < footer.capacity())
|
|
||||||
|| (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
|
|
||||||
throw new SignatureNotFoundException(
|
|
||||||
"APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
|
|
||||||
}
|
|
||||||
int totalSize = (int) (apkSigBlockSizeInFooter + 8);
|
|
||||||
long apkSigBlockOffset = centralDirStartOffset - totalSize;
|
|
||||||
if (apkSigBlockOffset < 0) {
|
|
||||||
throw new SignatureNotFoundException(
|
|
||||||
"APK Signing Block offset out of range: " + apkSigBlockOffset);
|
|
||||||
}
|
|
||||||
ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8);
|
|
||||||
apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
|
|
||||||
if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
|
|
||||||
throw new SignatureNotFoundException(
|
|
||||||
"APK Signing Block sizes in header and footer do not match: "
|
|
||||||
+ apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
|
|
||||||
}
|
|
||||||
return Pair.of(apk.slice(apkSigBlockOffset, totalSize), apkSigBlockOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ByteBuffer findApkSignatureSchemeV2Block(
|
|
||||||
ByteBuffer apkSigningBlock,
|
|
||||||
Result result) throws SignatureNotFoundException {
|
|
||||||
checkByteOrderLittleEndian(apkSigningBlock);
|
|
||||||
// FORMAT:
|
|
||||||
// OFFSET DATA TYPE DESCRIPTION
|
|
||||||
// * @+0 bytes uint64: size in bytes (excluding this field)
|
|
||||||
// * @+8 bytes pairs
|
|
||||||
// * @-24 bytes uint64: size in bytes (same as the one above)
|
|
||||||
// * @-16 bytes uint128: magic
|
|
||||||
ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
|
|
||||||
|
|
||||||
int entryCount = 0;
|
|
||||||
while (pairs.hasRemaining()) {
|
|
||||||
entryCount++;
|
|
||||||
if (pairs.remaining() < 8) {
|
|
||||||
throw new SignatureNotFoundException(
|
|
||||||
"Insufficient data to read size of APK Signing Block entry #" + entryCount);
|
|
||||||
}
|
|
||||||
long lenLong = pairs.getLong();
|
|
||||||
if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
|
|
||||||
throw new SignatureNotFoundException(
|
|
||||||
"APK Signing Block entry #" + entryCount
|
|
||||||
+ " size out of range: " + lenLong);
|
|
||||||
}
|
|
||||||
int len = (int) lenLong;
|
|
||||||
int nextEntryPos = pairs.position() + len;
|
|
||||||
if (len > pairs.remaining()) {
|
|
||||||
throw new SignatureNotFoundException(
|
|
||||||
"APK Signing Block entry #" + entryCount + " size out of range: " + len
|
|
||||||
+ ", available: " + pairs.remaining());
|
|
||||||
}
|
|
||||||
int id = pairs.getInt();
|
|
||||||
if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
|
|
||||||
return getByteBuffer(pairs, len - 4);
|
|
||||||
}
|
|
||||||
result.addWarning(Issue.APK_SIG_BLOCK_UNKNOWN_ENTRY_ID, id);
|
|
||||||
pairs.position(nextEntryPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new SignatureNotFoundException(
|
|
||||||
"No APK Signature Scheme v2 block in APK Signing Block");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void checkByteOrderLittleEndian(ByteBuffer buffer) {
|
|
||||||
if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
|
|
||||||
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class SignatureNotFoundException extends Exception {
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
public SignatureNotFoundException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SignatureNotFoundException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
|
|
||||||
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
|
|
||||||
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
|
|
||||||
* buffer's byte order.
|
|
||||||
*/
|
|
||||||
private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
|
|
||||||
if (start < 0) {
|
|
||||||
throw new IllegalArgumentException("start: " + start);
|
|
||||||
}
|
|
||||||
if (end < start) {
|
|
||||||
throw new IllegalArgumentException("end < start: " + end + " < " + start);
|
|
||||||
}
|
|
||||||
int capacity = source.capacity();
|
|
||||||
if (end > source.capacity()) {
|
|
||||||
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
|
|
||||||
}
|
|
||||||
int originalLimit = source.limit();
|
|
||||||
int originalPosition = source.position();
|
|
||||||
try {
|
|
||||||
source.position(0);
|
|
||||||
source.limit(end);
|
|
||||||
source.position(start);
|
|
||||||
ByteBuffer result = source.slice();
|
|
||||||
result.order(source.order());
|
|
||||||
return result;
|
|
||||||
} finally {
|
|
||||||
source.position(0);
|
|
||||||
source.limit(originalLimit);
|
|
||||||
source.position(originalPosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Relative <em>get</em> method for reading {@code size} number of bytes from the current
|
|
||||||
* position of this buffer.
|
|
||||||
*
|
|
||||||
* <p>This method reads the next {@code size} bytes at this buffer's current position,
|
|
||||||
* returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
|
|
||||||
* {@code size}, byte order set to this buffer's byte order; and then increments the position by
|
|
||||||
* {@code size}.
|
|
||||||
*/
|
|
||||||
private static ByteBuffer getByteBuffer(ByteBuffer source, int size)
|
|
||||||
throws BufferUnderflowException {
|
|
||||||
if (size < 0) {
|
|
||||||
throw new IllegalArgumentException("size: " + size);
|
|
||||||
}
|
|
||||||
int originalLimit = source.limit();
|
|
||||||
int position = source.position();
|
|
||||||
int limit = position + size;
|
|
||||||
if ((limit < position) || (limit > originalLimit)) {
|
|
||||||
throw new BufferUnderflowException();
|
|
||||||
}
|
|
||||||
source.limit(limit);
|
|
||||||
try {
|
|
||||||
ByteBuffer result = source.slice();
|
|
||||||
result.order(source.order());
|
|
||||||
source.position(limit);
|
|
||||||
return result;
|
|
||||||
} finally {
|
|
||||||
source.limit(originalLimit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws IOException {
|
|
||||||
if (source.remaining() < 4) {
|
|
||||||
throw new IOException(
|
|
||||||
"Remaining buffer too short to contain length of length-prefixed field."
|
|
||||||
+ " Remaining: " + source.remaining());
|
|
||||||
}
|
|
||||||
int len = source.getInt();
|
|
||||||
if (len < 0) {
|
|
||||||
throw new IllegalArgumentException("Negative length");
|
|
||||||
} else if (len > source.remaining()) {
|
|
||||||
throw new IOException("Length-prefixed field longer than remaining buffer."
|
|
||||||
+ " Field length: " + len + ", remaining: " + source.remaining());
|
|
||||||
}
|
|
||||||
return getByteBuffer(source, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws IOException {
|
|
||||||
int len = buf.getInt();
|
|
||||||
if (len < 0) {
|
|
||||||
throw new IOException("Negative length");
|
|
||||||
} else if (len > buf.remaining()) {
|
|
||||||
throw new IOException("Underflow while reading length-prefixed value. Length: " + len
|
|
||||||
+ ", available: " + buf.remaining());
|
|
||||||
}
|
|
||||||
byte[] result = new byte[len];
|
|
||||||
buf.get(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link X509Certificate} whose {@link #getEncoded()} returns the data provided at construction
|
|
||||||
* time.
|
|
||||||
*/
|
|
||||||
private static class GuaranteedEncodedFormX509Certificate extends DelegatingX509Certificate {
|
|
||||||
private byte[] mEncodedForm;
|
|
||||||
|
|
||||||
public GuaranteedEncodedFormX509Certificate(X509Certificate wrapped, byte[] encodedForm) {
|
|
||||||
super(wrapped);
|
|
||||||
this.mEncodedForm = (encodedForm != null) ? encodedForm.clone() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] getEncoded() throws CertificateEncodingException {
|
|
||||||
return (mEncodedForm != null) ? mEncodedForm.clone() : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final char[] HEX_DIGITS = "01234567890abcdef".toCharArray();
|
|
||||||
|
|
||||||
private static String toHex(byte[] value) {
|
|
||||||
StringBuilder sb = new StringBuilder(value.length * 2);
|
|
||||||
int len = value.length;
|
|
||||||
for (int i = 0; i < len; i++) {
|
|
||||||
int hi = (value[i] & 0xff) >>> 4;
|
|
||||||
int lo = value[i] & 0x0f;
|
|
||||||
sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]);
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Result {
|
|
||||||
|
|
||||||
/** Whether the APK's APK Signature Scheme v2 signature verifies. */
|
|
||||||
public boolean verified;
|
|
||||||
|
|
||||||
public final List<SignerInfo> signers = new ArrayList<>();
|
|
||||||
private final List<IssueWithParams> mWarnings = new ArrayList<>();
|
|
||||||
private final List<IssueWithParams> mErrors = new ArrayList<>();
|
|
||||||
|
|
||||||
public boolean containsErrors() {
|
|
||||||
if (!mErrors.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!signers.isEmpty()) {
|
|
||||||
for (SignerInfo signer : signers) {
|
|
||||||
if (signer.containsErrors()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addError(Issue msg, Object... parameters) {
|
|
||||||
mErrors.add(new IssueWithParams(msg, parameters));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addWarning(Issue msg, Object... parameters) {
|
|
||||||
mWarnings.add(new IssueWithParams(msg, parameters));
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<IssueWithParams> getErrors() {
|
|
||||||
return mErrors;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<IssueWithParams> getWarnings() {
|
|
||||||
return mWarnings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class SignerInfo {
|
|
||||||
public int index;
|
|
||||||
public List<X509Certificate> certs = new ArrayList<>();
|
|
||||||
public List<ContentDigest> contentDigests = new ArrayList<>();
|
|
||||||
public Map<ContentDigestAlgorithm, byte[]> verifiedContentDigests = new HashMap<>();
|
|
||||||
public List<Signature> signatures = new ArrayList<>();
|
|
||||||
public Map<SignatureAlgorithm, byte[]> verifiedSignatures = new HashMap<>();
|
|
||||||
public List<AdditionalAttribute> additionalAttributes = new ArrayList<>();
|
|
||||||
public byte[] signedData;
|
|
||||||
|
|
||||||
private final List<IssueWithParams> mWarnings = new ArrayList<>();
|
|
||||||
private final List<IssueWithParams> mErrors = new ArrayList<>();
|
|
||||||
|
|
||||||
public void addError(Issue msg, Object... parameters) {
|
|
||||||
mErrors.add(new IssueWithParams(msg, parameters));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addWarning(Issue msg, Object... parameters) {
|
|
||||||
mWarnings.add(new IssueWithParams(msg, parameters));
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean containsErrors() {
|
|
||||||
return !mErrors.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<IssueWithParams> getErrors() {
|
|
||||||
return mErrors;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<IssueWithParams> getWarnings() {
|
|
||||||
return mWarnings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ContentDigest {
|
|
||||||
private final int mSignatureAlgorithmId;
|
|
||||||
private final byte[] mValue;
|
|
||||||
|
|
||||||
public ContentDigest(int signatureAlgorithmId, byte[] value) {
|
|
||||||
mSignatureAlgorithmId = signatureAlgorithmId;
|
|
||||||
mValue = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSignatureAlgorithmId() {
|
|
||||||
return mSignatureAlgorithmId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getValue() {
|
|
||||||
return mValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Signature {
|
|
||||||
private final int mAlgorithmId;
|
|
||||||
private final byte[] mValue;
|
|
||||||
|
|
||||||
public Signature(int algorithmId, byte[] value) {
|
|
||||||
mAlgorithmId = algorithmId;
|
|
||||||
mValue = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getAlgorithmId() {
|
|
||||||
return mAlgorithmId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getValue() {
|
|
||||||
return mValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AdditionalAttribute {
|
|
||||||
private final int mId;
|
|
||||||
private final byte[] mValue;
|
|
||||||
|
|
||||||
public AdditionalAttribute(int id, byte[] value) {
|
|
||||||
mId = id;
|
|
||||||
mValue = value.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getId() {
|
|
||||||
return mId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getValue() {
|
|
||||||
return mValue.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,335 +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.jar;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.jar.Attributes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JAR manifest and signature file parser.
|
|
||||||
*
|
|
||||||
* <p>These files consist of a main section followed by individual sections. Individual sections
|
|
||||||
* are named, their names referring to JAR entries.
|
|
||||||
*
|
|
||||||
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
|
|
||||||
*/
|
|
||||||
public class ManifestParser {
|
|
||||||
|
|
||||||
private final byte[] mManifest;
|
|
||||||
private int mOffset;
|
|
||||||
private int mEndOffset;
|
|
||||||
|
|
||||||
private String mBufferedLine;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@code ManifestParser} with the provided input.
|
|
||||||
*/
|
|
||||||
public ManifestParser(byte[] data) {
|
|
||||||
this(data, 0, data.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@code ManifestParser} with the provided input.
|
|
||||||
*/
|
|
||||||
public ManifestParser(byte[] data, int offset, int length) {
|
|
||||||
mManifest = data;
|
|
||||||
mOffset = offset;
|
|
||||||
mEndOffset = offset + length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the remaining sections of this file.
|
|
||||||
*/
|
|
||||||
public List<Section> readAllSections() {
|
|
||||||
List<Section> sections = new ArrayList<>();
|
|
||||||
Section section;
|
|
||||||
while ((section = readSection()) != null) {
|
|
||||||
sections.add(section);
|
|
||||||
}
|
|
||||||
return sections;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the next section from this file or {@code null} if end of file has been reached.
|
|
||||||
*/
|
|
||||||
public Section readSection() {
|
|
||||||
// Locate the first non-empty line
|
|
||||||
int sectionStartOffset;
|
|
||||||
String attr;
|
|
||||||
do {
|
|
||||||
sectionStartOffset = mOffset;
|
|
||||||
attr = readAttribute();
|
|
||||||
if (attr == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} while (attr.length() == 0);
|
|
||||||
List<Attribute> attrs = new ArrayList<>();
|
|
||||||
attrs.add(parseAttr(attr));
|
|
||||||
|
|
||||||
// Read attributes until end of section reached
|
|
||||||
while (true) {
|
|
||||||
attr = readAttribute();
|
|
||||||
if ((attr == null) || (attr.length() == 0)) {
|
|
||||||
// End of section
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
attrs.add(parseAttr(attr));
|
|
||||||
}
|
|
||||||
|
|
||||||
int sectionEndOffset = mOffset;
|
|
||||||
int sectionSizeBytes = sectionEndOffset - sectionStartOffset;
|
|
||||||
|
|
||||||
return new Section(sectionStartOffset, sectionSizeBytes, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Attribute parseAttr(String attr) {
|
|
||||||
int delimiterIndex = attr.indexOf(':');
|
|
||||||
if (delimiterIndex == -1) {
|
|
||||||
return new Attribute(attr.trim(), "");
|
|
||||||
} else {
|
|
||||||
return new Attribute(
|
|
||||||
attr.substring(0, delimiterIndex).trim(),
|
|
||||||
attr.substring(delimiterIndex + 1).trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the next attribute or empty {@code String} if end of section has been reached or
|
|
||||||
* {@code null} if end of input has been reached.
|
|
||||||
*/
|
|
||||||
private String readAttribute() {
|
|
||||||
// Check whether end of section was reached during previous invocation
|
|
||||||
if ((mBufferedLine != null) && (mBufferedLine.length() == 0)) {
|
|
||||||
mBufferedLine = null;
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the next line
|
|
||||||
String line = readLine();
|
|
||||||
if (line == null) {
|
|
||||||
// End of input
|
|
||||||
if (mBufferedLine != null) {
|
|
||||||
String result = mBufferedLine;
|
|
||||||
mBufferedLine = null;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consume the read line
|
|
||||||
if (line.length() == 0) {
|
|
||||||
// End of section
|
|
||||||
if (mBufferedLine != null) {
|
|
||||||
String result = mBufferedLine;
|
|
||||||
mBufferedLine = "";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
StringBuilder attrLine;
|
|
||||||
if (mBufferedLine == null) {
|
|
||||||
attrLine = new StringBuilder(line);
|
|
||||||
} else {
|
|
||||||
if (!line.startsWith(" ")) {
|
|
||||||
// The most common case: buffered line is a full attribute
|
|
||||||
String result = mBufferedLine;
|
|
||||||
mBufferedLine = line;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
attrLine = new StringBuilder(mBufferedLine);
|
|
||||||
mBufferedLine = null;
|
|
||||||
attrLine.append(line.substring(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Everything's buffered in attrLine now. mBufferedLine is null
|
|
||||||
|
|
||||||
// Read more lines
|
|
||||||
while (true) {
|
|
||||||
line = readLine();
|
|
||||||
if (line == null) {
|
|
||||||
// End of input
|
|
||||||
return attrLine.toString();
|
|
||||||
} else if (line.length() == 0) {
|
|
||||||
// End of section
|
|
||||||
mBufferedLine = ""; // make this method return "end of section" next time
|
|
||||||
return attrLine.toString();
|
|
||||||
}
|
|
||||||
if (line.startsWith(" ")) {
|
|
||||||
// Continuation line
|
|
||||||
attrLine.append(line.substring(1));
|
|
||||||
} else {
|
|
||||||
// Next attribute
|
|
||||||
mBufferedLine = line;
|
|
||||||
return attrLine.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the next line (without line delimiter characters) or {@code null} if end of input has
|
|
||||||
* been reached.
|
|
||||||
*/
|
|
||||||
private String readLine() {
|
|
||||||
if (mOffset >= mEndOffset) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
int startOffset = mOffset;
|
|
||||||
int newlineStartOffset = -1;
|
|
||||||
int newlineEndOffset = -1;
|
|
||||||
for (int i = startOffset; i < mEndOffset; i++) {
|
|
||||||
byte b = mManifest[i];
|
|
||||||
if (b == '\r') {
|
|
||||||
newlineStartOffset = i;
|
|
||||||
int nextIndex = i + 1;
|
|
||||||
if ((nextIndex < mEndOffset) && (mManifest[nextIndex] == '\n')) {
|
|
||||||
newlineEndOffset = nextIndex + 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
newlineEndOffset = nextIndex;
|
|
||||||
break;
|
|
||||||
} else if (b == '\n') {
|
|
||||||
newlineStartOffset = i;
|
|
||||||
newlineEndOffset = i + 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (newlineStartOffset == -1) {
|
|
||||||
newlineStartOffset = mEndOffset;
|
|
||||||
newlineEndOffset = mEndOffset;
|
|
||||||
}
|
|
||||||
mOffset = newlineEndOffset;
|
|
||||||
|
|
||||||
int lineLengthBytes = newlineStartOffset - startOffset;
|
|
||||||
if (lineLengthBytes == 0) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return new String(mManifest, startOffset, lineLengthBytes, StandardCharsets.UTF_8);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attribute.
|
|
||||||
*/
|
|
||||||
public static class Attribute {
|
|
||||||
private final String mName;
|
|
||||||
private final String mValue;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@code Attribute} with the provided name and value.
|
|
||||||
*/
|
|
||||||
public Attribute(String name, String value) {
|
|
||||||
mName = name;
|
|
||||||
mValue = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns this attribute's name.
|
|
||||||
*/
|
|
||||||
public String getName() {
|
|
||||||
return mName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns this attribute's value.
|
|
||||||
*/
|
|
||||||
public String getValue() {
|
|
||||||
return mValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Section.
|
|
||||||
*/
|
|
||||||
public static class Section {
|
|
||||||
private final int mStartOffset;
|
|
||||||
private final int mSizeBytes;
|
|
||||||
private final String mName;
|
|
||||||
private final List<Attribute> mAttributes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@code Section}.
|
|
||||||
*
|
|
||||||
* @param startOffset start offset (in bytes) of the section in the input file
|
|
||||||
* @param sizeBytes size (in bytes) of the section in the input file
|
|
||||||
* @param attrs attributes contained in the section
|
|
||||||
*/
|
|
||||||
public Section(int startOffset, int sizeBytes, List<Attribute> attrs) {
|
|
||||||
mStartOffset = startOffset;
|
|
||||||
mSizeBytes = sizeBytes;
|
|
||||||
String sectionName = null;
|
|
||||||
if (!attrs.isEmpty()) {
|
|
||||||
Attribute firstAttr = attrs.get(0);
|
|
||||||
if ("Name".equalsIgnoreCase(firstAttr.getName())) {
|
|
||||||
sectionName = firstAttr.getValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mName = sectionName;
|
|
||||||
mAttributes = Collections.unmodifiableList(new ArrayList<>(attrs));
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return mName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the offset (in bytes) at which this section starts in the input.
|
|
||||||
*/
|
|
||||||
public int getStartOffset() {
|
|
||||||
return mStartOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the size (in bytes) of this section in the input.
|
|
||||||
*/
|
|
||||||
public int getSizeBytes() {
|
|
||||||
return mSizeBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns this section's attributes, in the order in which they appear in the input.
|
|
||||||
*/
|
|
||||||
public List<Attribute> getAttributes() {
|
|
||||||
return mAttributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the value of the specified attribute in this section or {@code null} if this
|
|
||||||
* section does not contain a matching attribute.
|
|
||||||
*/
|
|
||||||
public String getAttributeValue(Attributes.Name name) {
|
|
||||||
return getAttributeValue(name.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the value of the specified attribute in this section or {@code null} if this
|
|
||||||
* section does not contain a matching attribute.
|
|
||||||
*
|
|
||||||
* @param name name of the attribute. Attribute names are case-insensitive.
|
|
||||||
*/
|
|
||||||
public String getAttributeValue(String name) {
|
|
||||||
for (Attribute attr : mAttributes) {
|
|
||||||
if (attr.getName().equalsIgnoreCase(name)) {
|
|
||||||
return attr.getValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,127 +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.jar;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.SortedMap;
|
|
||||||
import java.util.TreeMap;
|
|
||||||
import java.util.jar.Attributes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Producer of {@code META-INF/MANIFEST.MF} file.
|
|
||||||
*
|
|
||||||
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
|
|
||||||
*/
|
|
||||||
public abstract class ManifestWriter {
|
|
||||||
|
|
||||||
private static final byte[] CRLF = new byte[] {'\r', '\n'};
|
|
||||||
private static final int MAX_LINE_LENGTH = 70;
|
|
||||||
|
|
||||||
private ManifestWriter() {}
|
|
||||||
|
|
||||||
public static void writeMainSection(OutputStream out, Attributes attributes)
|
|
||||||
throws IOException {
|
|
||||||
|
|
||||||
// Main section must start with the Manifest-Version attribute.
|
|
||||||
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File.
|
|
||||||
String manifestVersion = attributes.getValue(Attributes.Name.MANIFEST_VERSION);
|
|
||||||
if (manifestVersion == null) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Mandatory " + Attributes.Name.MANIFEST_VERSION + " attribute missing");
|
|
||||||
}
|
|
||||||
writeAttribute(out, Attributes.Name.MANIFEST_VERSION, manifestVersion);
|
|
||||||
|
|
||||||
if (attributes.size() > 1) {
|
|
||||||
SortedMap<String, String> namedAttributes = getAttributesSortedByName(attributes);
|
|
||||||
namedAttributes.remove(Attributes.Name.MANIFEST_VERSION.toString());
|
|
||||||
writeAttributes(out, namedAttributes);
|
|
||||||
}
|
|
||||||
writeSectionDelimiter(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void writeIndividualSection(OutputStream out, String name, Attributes attributes)
|
|
||||||
throws IOException {
|
|
||||||
writeAttribute(out, "Name", name);
|
|
||||||
|
|
||||||
if (!attributes.isEmpty()) {
|
|
||||||
writeAttributes(out, getAttributesSortedByName(attributes));
|
|
||||||
}
|
|
||||||
writeSectionDelimiter(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void writeSectionDelimiter(OutputStream out) throws IOException {
|
|
||||||
out.write(CRLF);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void writeAttribute(OutputStream out, Attributes.Name name, String value)
|
|
||||||
throws IOException {
|
|
||||||
writeAttribute(out, name.toString(), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeAttribute(OutputStream out, String name, String value)
|
|
||||||
throws IOException {
|
|
||||||
writeLine(out, name + ": " + value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeLine(OutputStream out, String line) throws IOException {
|
|
||||||
byte[] lineBytes = line.getBytes(StandardCharsets.UTF_8);
|
|
||||||
int offset = 0;
|
|
||||||
int remaining = lineBytes.length;
|
|
||||||
boolean firstLine = true;
|
|
||||||
while (remaining > 0) {
|
|
||||||
int chunkLength;
|
|
||||||
if (firstLine) {
|
|
||||||
// First line
|
|
||||||
chunkLength = Math.min(remaining, MAX_LINE_LENGTH);
|
|
||||||
} else {
|
|
||||||
// Continuation line
|
|
||||||
out.write(CRLF);
|
|
||||||
out.write(' ');
|
|
||||||
chunkLength = Math.min(remaining, MAX_LINE_LENGTH - 1);
|
|
||||||
}
|
|
||||||
out.write(lineBytes, offset, chunkLength);
|
|
||||||
offset += chunkLength;
|
|
||||||
remaining -= chunkLength;
|
|
||||||
firstLine = false;
|
|
||||||
}
|
|
||||||
out.write(CRLF);
|
|
||||||
}
|
|
||||||
|
|
||||||
static SortedMap<String, String> getAttributesSortedByName(Attributes attributes) {
|
|
||||||
Set<Map.Entry<Object, Object>> attributesEntries = attributes.entrySet();
|
|
||||||
SortedMap<String, String> namedAttributes = new TreeMap<String, String>();
|
|
||||||
for (Map.Entry<Object, Object> attribute : attributesEntries) {
|
|
||||||
String attrName = attribute.getKey().toString();
|
|
||||||
String attrValue = attribute.getValue().toString();
|
|
||||||
namedAttributes.put(attrName, attrValue);
|
|
||||||
}
|
|
||||||
return namedAttributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void writeAttributes(
|
|
||||||
OutputStream out, SortedMap<String, String> attributesSortedByName) throws IOException {
|
|
||||||
for (Map.Entry<String, String> attribute : attributesSortedByName.entrySet()) {
|
|
||||||
String attrName = attribute.getKey();
|
|
||||||
String attrValue = attribute.getValue();
|
|
||||||
writeAttribute(out, attrName, attrValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +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.jar;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.SortedMap;
|
|
||||||
import java.util.jar.Attributes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Producer of JAR signature file ({@code *.SF}).
|
|
||||||
*
|
|
||||||
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
|
|
||||||
*/
|
|
||||||
public abstract class SignatureFileWriter {
|
|
||||||
private SignatureFileWriter() {}
|
|
||||||
|
|
||||||
public static void writeMainSection(OutputStream out, Attributes attributes)
|
|
||||||
throws IOException {
|
|
||||||
|
|
||||||
// Main section must start with the Signature-Version attribute.
|
|
||||||
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File.
|
|
||||||
String signatureVersion = attributes.getValue(Attributes.Name.SIGNATURE_VERSION);
|
|
||||||
if (signatureVersion == null) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Mandatory " + Attributes.Name.SIGNATURE_VERSION + " attribute missing");
|
|
||||||
}
|
|
||||||
ManifestWriter.writeAttribute(out, Attributes.Name.SIGNATURE_VERSION, signatureVersion);
|
|
||||||
|
|
||||||
if (attributes.size() > 1) {
|
|
||||||
SortedMap<String, String> namedAttributes =
|
|
||||||
ManifestWriter.getAttributesSortedByName(attributes);
|
|
||||||
namedAttributes.remove(Attributes.Name.SIGNATURE_VERSION.toString());
|
|
||||||
ManifestWriter.writeAttributes(out, namedAttributes);
|
|
||||||
}
|
|
||||||
writeSectionDelimiter(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void writeIndividualSection(OutputStream out, String name, Attributes attributes)
|
|
||||||
throws IOException {
|
|
||||||
ManifestWriter.writeIndividualSection(out, name, attributes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void writeSectionDelimiter(OutputStream out) throws IOException {
|
|
||||||
ManifestWriter.writeSectionDelimiter(out);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +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.util;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Android SDK version / API Level constants.
|
|
||||||
*/
|
|
||||||
public abstract class AndroidSdkVersion {
|
|
||||||
|
|
||||||
/** Hidden constructor to prevent instantiation. */
|
|
||||||
private AndroidSdkVersion() {}
|
|
||||||
|
|
||||||
/** Android 2.3. */
|
|
||||||
public static final int GINGERBREAD = 9;
|
|
||||||
|
|
||||||
/** Android 4.3. The revenge of the beans. */
|
|
||||||
public static final int JELLY_BEAN_MR2 = 18;
|
|
||||||
|
|
||||||
/** Android 5.0. A flat one with beautiful shadows. But still tasty. */
|
|
||||||
public static final int LOLLIPOP = 21;
|
|
||||||
|
|
||||||
// TODO: Update Javadoc / constant name once N is assigned a proper name / version code.
|
|
||||||
/** Android N. */
|
|
||||||
public static final int N = 24;
|
|
||||||
}
|
|
|
@ -1,126 +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.util;
|
|
||||||
|
|
||||||
import com.android.apksigner.core.util.DataSink;
|
|
||||||
import com.android.apksigner.core.util.DataSource;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link DataSource} backed by a {@link ByteBuffer}.
|
|
||||||
*/
|
|
||||||
public class ByteBufferDataSource implements DataSource {
|
|
||||||
|
|
||||||
private final ByteBuffer mBuffer;
|
|
||||||
private final int mSize;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided
|
|
||||||
* buffer between the buffer's position and limit.
|
|
||||||
*/
|
|
||||||
public ByteBufferDataSource(ByteBuffer buffer) {
|
|
||||||
this(buffer, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided
|
|
||||||
* buffer between the buffer's position and limit.
|
|
||||||
*/
|
|
||||||
private ByteBufferDataSource(ByteBuffer buffer, boolean sliceRequired) {
|
|
||||||
mBuffer = (sliceRequired) ? buffer.slice() : buffer;
|
|
||||||
mSize = buffer.remaining();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long size() {
|
|
||||||
return mSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ByteBuffer getByteBuffer(long offset, int size) {
|
|
||||||
checkChunkValid(offset, size);
|
|
||||||
|
|
||||||
// checkChunkValid ensures that it's OK to cast offset to int.
|
|
||||||
int chunkPosition = (int) offset;
|
|
||||||
int chunkLimit = chunkPosition + size;
|
|
||||||
// Creating a slice of ByteBuffer modifies the state of the source ByteBuffer (position
|
|
||||||
// and limit fields, to be more specific). We thus use synchronization around these
|
|
||||||
// state-changing operations to make instances of this class thread-safe.
|
|
||||||
synchronized (mBuffer) {
|
|
||||||
// ByteBuffer.limit(int) and .position(int) check that that the position >= limit
|
|
||||||
// invariant is not broken. Thus, the only way to safely change position and limit
|
|
||||||
// without caring about their current values is to first set position to 0 or set the
|
|
||||||
// limit to capacity.
|
|
||||||
mBuffer.position(0);
|
|
||||||
|
|
||||||
mBuffer.limit(chunkLimit);
|
|
||||||
mBuffer.position(chunkPosition);
|
|
||||||
return mBuffer.slice();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void copyTo(long offset, int size, ByteBuffer dest) {
|
|
||||||
dest.put(getByteBuffer(offset, size));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void feed(long offset, long size, DataSink sink) throws IOException {
|
|
||||||
if ((size < 0) || (size > mSize)) {
|
|
||||||
throw new IllegalArgumentException("size: " + size + ", source size: " + mSize);
|
|
||||||
}
|
|
||||||
sink.consume(getByteBuffer(offset, (int) size));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ByteBufferDataSource slice(long offset, long size) {
|
|
||||||
if ((offset == 0) && (size == mSize)) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if ((size < 0) || (size > mSize)) {
|
|
||||||
throw new IllegalArgumentException("size: " + size + ", source size: " + mSize);
|
|
||||||
}
|
|
||||||
return new ByteBufferDataSource(
|
|
||||||
getByteBuffer(offset, (int) size),
|
|
||||||
false // no need to slice -- it's already a slice
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkChunkValid(long offset, long size) {
|
|
||||||
if (offset < 0) {
|
|
||||||
throw new IllegalArgumentException("offset: " + offset);
|
|
||||||
}
|
|
||||||
if (size < 0) {
|
|
||||||
throw new IllegalArgumentException("size: " + size);
|
|
||||||
}
|
|
||||||
if (offset > mSize) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"offset (" + offset + ") > source size (" + mSize + ")");
|
|
||||||
}
|
|
||||||
long endOffset = offset + size;
|
|
||||||
if (endOffset < offset) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"offset (" + offset + ") + size (" + size + ") overflow");
|
|
||||||
}
|
|
||||||
if (endOffset > mSize) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"offset (" + offset + ") + size (" + size + ") > source size (" + mSize +")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +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.util;
|
|
||||||
|
|
||||||
import com.android.apksigner.core.util.DataSink;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.BufferOverflowException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data sink which stores all received data into the associated {@link ByteBuffer}.
|
|
||||||
*/
|
|
||||||
public class ByteBufferSink implements DataSink {
|
|
||||||
|
|
||||||
private final ByteBuffer mBuffer;
|
|
||||||
|
|
||||||
public ByteBufferSink(ByteBuffer buffer) {
|
|
||||||
mBuffer = buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void consume(byte[] buf, int offset, int length) throws IOException {
|
|
||||||
try {
|
|
||||||
mBuffer.put(buf, offset, length);
|
|
||||||
} catch (BufferOverflowException e) {
|
|
||||||
throw new IOException(
|
|
||||||
"Insufficient space in output buffer for " + length + " bytes", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void consume(ByteBuffer buf) throws IOException {
|
|
||||||
int length = buf.remaining();
|
|
||||||
try {
|
|
||||||
mBuffer.put(buf);
|
|
||||||
} catch (BufferOverflowException e) {
|
|
||||||
throw new IOException(
|
|
||||||
"Insufficient space in output buffer for " + length + " bytes", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,179 +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.util;
|
|
||||||
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.NoSuchProviderException;
|
|
||||||
import java.security.Principal;
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.SignatureException;
|
|
||||||
import java.security.cert.CertificateEncodingException;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.CertificateExpiredException;
|
|
||||||
import java.security.cert.CertificateNotYetValidException;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link X509Certificate} which delegates all method invocations to the provided delegate
|
|
||||||
* {@code X509Certificate}.
|
|
||||||
*/
|
|
||||||
public class DelegatingX509Certificate extends X509Certificate {
|
|
||||||
private final X509Certificate mDelegate;
|
|
||||||
|
|
||||||
public DelegatingX509Certificate(X509Certificate delegate) {
|
|
||||||
this.mDelegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<String> getCriticalExtensionOIDs() {
|
|
||||||
return mDelegate.getCriticalExtensionOIDs();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] getExtensionValue(String oid) {
|
|
||||||
return mDelegate.getExtensionValue(oid);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<String> getNonCriticalExtensionOIDs() {
|
|
||||||
return mDelegate.getNonCriticalExtensionOIDs();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasUnsupportedCriticalExtension() {
|
|
||||||
return mDelegate.hasUnsupportedCriticalExtension();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void checkValidity()
|
|
||||||
throws CertificateExpiredException, CertificateNotYetValidException {
|
|
||||||
mDelegate.checkValidity();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void checkValidity(Date date)
|
|
||||||
throws CertificateExpiredException, CertificateNotYetValidException {
|
|
||||||
mDelegate.checkValidity(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getVersion() {
|
|
||||||
return mDelegate.getVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BigInteger getSerialNumber() {
|
|
||||||
return mDelegate.getSerialNumber();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Principal getIssuerDN() {
|
|
||||||
return mDelegate.getIssuerDN();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Principal getSubjectDN() {
|
|
||||||
return mDelegate.getSubjectDN();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Date getNotBefore() {
|
|
||||||
return mDelegate.getNotBefore();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Date getNotAfter() {
|
|
||||||
return mDelegate.getNotAfter();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] getTBSCertificate() throws CertificateEncodingException {
|
|
||||||
return mDelegate.getTBSCertificate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] getSignature() {
|
|
||||||
return mDelegate.getSignature();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getSigAlgName() {
|
|
||||||
return mDelegate.getSigAlgName();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getSigAlgOID() {
|
|
||||||
return mDelegate.getSigAlgOID();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] getSigAlgParams() {
|
|
||||||
return mDelegate.getSigAlgParams();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean[] getIssuerUniqueID() {
|
|
||||||
return mDelegate.getIssuerUniqueID();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean[] getSubjectUniqueID() {
|
|
||||||
return mDelegate.getSubjectUniqueID();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean[] getKeyUsage() {
|
|
||||||
return mDelegate.getKeyUsage();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getBasicConstraints() {
|
|
||||||
return mDelegate.getBasicConstraints();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] getEncoded() throws CertificateEncodingException {
|
|
||||||
return mDelegate.getEncoded();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException,
|
|
||||||
InvalidKeyException, NoSuchProviderException, SignatureException {
|
|
||||||
mDelegate.verify(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void verify(PublicKey key, String sigProvider)
|
|
||||||
throws CertificateException, NoSuchAlgorithmException, InvalidKeyException,
|
|
||||||
NoSuchProviderException, SignatureException {
|
|
||||||
mDelegate.verify(key, sigProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return mDelegate.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PublicKey getPublicKey() {
|
|
||||||
return mDelegate.getPublicKey();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,89 +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.util;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inclusive interval of integers.
|
|
||||||
*/
|
|
||||||
public class InclusiveIntRange {
|
|
||||||
private final int min;
|
|
||||||
private final int max;
|
|
||||||
|
|
||||||
private InclusiveIntRange(int min, int max) {
|
|
||||||
this.min = min;
|
|
||||||
this.max = max;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getMin() {
|
|
||||||
return min;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getMax() {
|
|
||||||
return max;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static InclusiveIntRange fromTo(int min, int max) {
|
|
||||||
return new InclusiveIntRange(min, max);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static InclusiveIntRange from(int min) {
|
|
||||||
return new InclusiveIntRange(min, Integer.MAX_VALUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<InclusiveIntRange> getValuesNotIn(
|
|
||||||
List<InclusiveIntRange> sortedNonOverlappingRanges) {
|
|
||||||
if (sortedNonOverlappingRanges.isEmpty()) {
|
|
||||||
return Collections.singletonList(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
int testValue = min;
|
|
||||||
List<InclusiveIntRange> result = null;
|
|
||||||
for (InclusiveIntRange range : sortedNonOverlappingRanges) {
|
|
||||||
int rangeMax = range.max;
|
|
||||||
if (testValue > rangeMax) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int rangeMin = range.min;
|
|
||||||
if (testValue < range.min) {
|
|
||||||
if (result == null) {
|
|
||||||
result = new ArrayList<>();
|
|
||||||
}
|
|
||||||
result.add(fromTo(testValue, rangeMin - 1));
|
|
||||||
}
|
|
||||||
if (rangeMax >= max) {
|
|
||||||
return (result != null) ? result : Collections.emptyList();
|
|
||||||
}
|
|
||||||
testValue = rangeMax + 1;
|
|
||||||
}
|
|
||||||
if (testValue <= max) {
|
|
||||||
if (result == null) {
|
|
||||||
result = new ArrayList<>(1);
|
|
||||||
}
|
|
||||||
result.add(fromTo(testValue, max));
|
|
||||||
}
|
|
||||||
return (result != null) ? result : Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "[" + min + ", " + ((max < Integer.MAX_VALUE) ? (max + "]") : "\u221e)");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +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.util;
|
|
||||||
|
|
||||||
import com.android.apksigner.core.util.DataSink;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data sink which feeds all received data into the associated {@link MessageDigest} instances. Each
|
|
||||||
* {@code MessageDigest} instance receives the same data.
|
|
||||||
*/
|
|
||||||
public class MessageDigestSink implements DataSink {
|
|
||||||
|
|
||||||
private final MessageDigest[] mMessageDigests;
|
|
||||||
|
|
||||||
public MessageDigestSink(MessageDigest[] digests) {
|
|
||||||
mMessageDigests = digests;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void consume(byte[] buf, int offset, int length) {
|
|
||||||
for (MessageDigest md : mMessageDigests) {
|
|
||||||
md.update(buf, offset, length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void consume(ByteBuffer buf) {
|
|
||||||
int originalPosition = buf.position();
|
|
||||||
for (MessageDigest md : mMessageDigests) {
|
|
||||||
// Reset the position back to the original because the previous iteration's
|
|
||||||
// MessageDigest.update set the buffer's position to the buffer's limit.
|
|
||||||
buf.position(originalPosition);
|
|
||||||
md.update(buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +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.util;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
|
|
||||||
import com.android.apksigner.core.util.DataSink;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link DataSink} which outputs received data into the associated {@link OutputStream}.
|
|
||||||
*/
|
|
||||||
public class OutputStreamDataSink implements DataSink {
|
|
||||||
|
|
||||||
private static final int MAX_READ_CHUNK_SIZE = 65536;
|
|
||||||
|
|
||||||
private final OutputStream mOut;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@code OutputStreamDataSink} which outputs received data into the provided
|
|
||||||
* {@link OutputStream}.
|
|
||||||
*/
|
|
||||||
public OutputStreamDataSink(OutputStream out) {
|
|
||||||
if (out == null) {
|
|
||||||
throw new NullPointerException("out == null");
|
|
||||||
}
|
|
||||||
mOut = out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns {@link OutputStream} into which this data sink outputs received data.
|
|
||||||
*/
|
|
||||||
public OutputStream getOutputStream() {
|
|
||||||
return mOut;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void consume(byte[] buf, int offset, int length) throws IOException {
|
|
||||||
mOut.write(buf, offset, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void consume(ByteBuffer buf) throws IOException {
|
|
||||||
if (!buf.hasRemaining()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buf.hasArray()) {
|
|
||||||
mOut.write(
|
|
||||||
buf.array(),
|
|
||||||
buf.arrayOffset() + buf.position(),
|
|
||||||
buf.remaining());
|
|
||||||
buf.position(buf.limit());
|
|
||||||
} else {
|
|
||||||
byte[] tmp = new byte[Math.min(buf.remaining(), MAX_READ_CHUNK_SIZE)];
|
|
||||||
while (buf.hasRemaining()) {
|
|
||||||
int chunkSize = Math.min(buf.remaining(), tmp.length);
|
|
||||||
buf.get(tmp, 0, chunkSize);
|
|
||||||
mOut.write(tmp, 0, chunkSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,81 +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.util;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pair of two elements.
|
|
||||||
*/
|
|
||||||
public final class Pair<A, B> {
|
|
||||||
private final A mFirst;
|
|
||||||
private final B mSecond;
|
|
||||||
|
|
||||||
private Pair(A first, B second) {
|
|
||||||
mFirst = first;
|
|
||||||
mSecond = second;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <A, B> Pair<A, B> of(A first, B second) {
|
|
||||||
return new Pair<A, B>(first, second);
|
|
||||||
}
|
|
||||||
|
|
||||||
public A getFirst() {
|
|
||||||
return mFirst;
|
|
||||||
}
|
|
||||||
|
|
||||||
public B getSecond() {
|
|
||||||
return mSecond;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
final int prime = 31;
|
|
||||||
int result = 1;
|
|
||||||
result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode());
|
|
||||||
result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object obj) {
|
|
||||||
if (this == obj) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (obj == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (getClass() != obj.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
@SuppressWarnings("rawtypes")
|
|
||||||
Pair other = (Pair) obj;
|
|
||||||
if (mFirst == null) {
|
|
||||||
if (other.mFirst != null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if (!mFirst.equals(other.mFirst)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (mSecond == null) {
|
|
||||||
if (other.mSecond != null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if (!mSecond.equals(other.mSecond)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,87 +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.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,165 +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.util;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.channels.FileChannel;
|
|
||||||
|
|
||||||
import com.android.apksigner.core.util.DataSink;
|
|
||||||
import com.android.apksigner.core.util.DataSource;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link DataSource} backed by a {@link RandomAccessFile}.
|
|
||||||
*/
|
|
||||||
public class RandomAccessFileDataSource implements DataSource {
|
|
||||||
|
|
||||||
private static final int MAX_READ_CHUNK_SIZE = 65536;
|
|
||||||
|
|
||||||
private final RandomAccessFile mFile;
|
|
||||||
private final long mOffset;
|
|
||||||
private final long mSize;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@code RandomAccessFileDataSource} based on the data contained in the
|
|
||||||
* specified the whole file. Changes to the contents of the file, including the size of the
|
|
||||||
* file, will be visible in this data source.
|
|
||||||
*/
|
|
||||||
public RandomAccessFileDataSource(RandomAccessFile file) {
|
|
||||||
mFile = file;
|
|
||||||
mOffset = 0;
|
|
||||||
mSize = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@code RandomAccessFileDataSource} based on the data contained in the
|
|
||||||
* specified region of the provided file. Changes to the contents of the file will be visible in
|
|
||||||
* this data source.
|
|
||||||
*/
|
|
||||||
public RandomAccessFileDataSource(RandomAccessFile file, long offset, long size) {
|
|
||||||
if (offset < 0) {
|
|
||||||
throw new IllegalArgumentException("offset: " + size);
|
|
||||||
}
|
|
||||||
if (size < 0) {
|
|
||||||
throw new IllegalArgumentException("size: " + size);
|
|
||||||
}
|
|
||||||
mFile = file;
|
|
||||||
mOffset = offset;
|
|
||||||
mSize = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long size() {
|
|
||||||
if (mSize == -1) {
|
|
||||||
try {
|
|
||||||
return mFile.length();
|
|
||||||
} catch (IOException e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return mSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RandomAccessFileDataSource slice(long offset, long size) {
|
|
||||||
long sourceSize = size();
|
|
||||||
checkChunkValid(offset, size, sourceSize);
|
|
||||||
if ((offset == 0) && (size == sourceSize)) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RandomAccessFileDataSource(mFile, mOffset + offset, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void feed(long offset, long size, DataSink sink) throws IOException {
|
|
||||||
long sourceSize = size();
|
|
||||||
checkChunkValid(offset, size, sourceSize);
|
|
||||||
if (size == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
long chunkOffsetInFile = mOffset + offset;
|
|
||||||
long remaining = size;
|
|
||||||
byte[] buf = new byte[(int) Math.min(remaining, MAX_READ_CHUNK_SIZE)];
|
|
||||||
while (remaining > 0) {
|
|
||||||
int chunkSize = (int) Math.min(remaining, buf.length);
|
|
||||||
synchronized (mFile) {
|
|
||||||
mFile.seek(chunkOffsetInFile);
|
|
||||||
mFile.readFully(buf, 0, chunkSize);
|
|
||||||
}
|
|
||||||
sink.consume(buf, 0, chunkSize);
|
|
||||||
chunkOffsetInFile += chunkSize;
|
|
||||||
remaining -= chunkSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
|
|
||||||
long sourceSize = size();
|
|
||||||
checkChunkValid(offset, size, sourceSize);
|
|
||||||
if (size == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
long offsetInFile = mOffset + offset;
|
|
||||||
int remaining = size;
|
|
||||||
FileChannel fileChannel = mFile.getChannel();
|
|
||||||
while (remaining > 0) {
|
|
||||||
int chunkSize;
|
|
||||||
synchronized (mFile) {
|
|
||||||
fileChannel.position(offsetInFile);
|
|
||||||
chunkSize = fileChannel.read(dest);
|
|
||||||
}
|
|
||||||
offsetInFile += chunkSize;
|
|
||||||
remaining -= chunkSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
|
|
||||||
ByteBuffer result = ByteBuffer.allocate(size);
|
|
||||||
copyTo(offset, size, result);
|
|
||||||
result.flip();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void checkChunkValid(long offset, long size, long sourceSize) {
|
|
||||||
if (offset < 0) {
|
|
||||||
throw new IllegalArgumentException("offset: " + offset);
|
|
||||||
}
|
|
||||||
if (size < 0) {
|
|
||||||
throw new IllegalArgumentException("size: " + size);
|
|
||||||
}
|
|
||||||
if (offset > sourceSize) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"offset (" + offset + ") > source size (" + sourceSize + ")");
|
|
||||||
}
|
|
||||||
long endOffset = offset + size;
|
|
||||||
if (endOffset < offset) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"offset (" + offset + ") + size (" + size + ") overflow");
|
|
||||||
}
|
|
||||||
if (endOffset > sourceSize) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"offset (" + offset + ") + size (" + size
|
|
||||||
+ ") > source size (" + sourceSize +")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,281 +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 com.android.apksigner.core.zip.ZipFormatException;
|
|
||||||
|
|
||||||
import java.nio.BufferUnderflowException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Comparator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ZIP Central Directory (CD) Record.
|
|
||||||
*/
|
|
||||||
public class CentralDirectoryRecord {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Comparator which compares records by the offset of the corresponding Local File Header in the
|
|
||||||
* archive.
|
|
||||||
*/
|
|
||||||
public static final Comparator<CentralDirectoryRecord> BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR =
|
|
||||||
new ByLocalFileHeaderOffsetComparator();
|
|
||||||
|
|
||||||
private static final int RECORD_SIGNATURE = 0x02014b50;
|
|
||||||
private static final int HEADER_SIZE_BYTES = 46;
|
|
||||||
|
|
||||||
private static final int LAST_MODIFICATION_TIME_OFFSET = 12;
|
|
||||||
private static final int LOCAL_FILE_HEADER_OFFSET_OFFSET = 42;
|
|
||||||
private static final int NAME_OFFSET = HEADER_SIZE_BYTES;
|
|
||||||
|
|
||||||
private final ByteBuffer mData;
|
|
||||||
private final int mLastModificationTime;
|
|
||||||
private final int mLastModificationDate;
|
|
||||||
private final long mCrc32;
|
|
||||||
private final long mCompressedSize;
|
|
||||||
private final long mUncompressedSize;
|
|
||||||
private final long mLocalFileHeaderOffset;
|
|
||||||
private final String mName;
|
|
||||||
private final int mNameSizeBytes;
|
|
||||||
|
|
||||||
private CentralDirectoryRecord(
|
|
||||||
ByteBuffer data,
|
|
||||||
int lastModificationTime,
|
|
||||||
int lastModificationDate,
|
|
||||||
long crc32,
|
|
||||||
long compressedSize,
|
|
||||||
long uncompressedSize,
|
|
||||||
long localFileHeaderOffset,
|
|
||||||
String name,
|
|
||||||
int nameSizeBytes) {
|
|
||||||
mData = data;
|
|
||||||
mLastModificationDate = lastModificationDate;
|
|
||||||
mLastModificationTime = lastModificationTime;
|
|
||||||
mCrc32 = crc32;
|
|
||||||
mCompressedSize = compressedSize;
|
|
||||||
mUncompressedSize = uncompressedSize;
|
|
||||||
mLocalFileHeaderOffset = localFileHeaderOffset;
|
|
||||||
mName = name;
|
|
||||||
mNameSizeBytes = nameSizeBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSize() {
|
|
||||||
return mData.remaining();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return mName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getNameSizeBytes() {
|
|
||||||
return mNameSizeBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getLastModificationTime() {
|
|
||||||
return mLastModificationTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getLastModificationDate() {
|
|
||||||
return mLastModificationDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getCrc32() {
|
|
||||||
return mCrc32;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getCompressedSize() {
|
|
||||||
return mCompressedSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getUncompressedSize() {
|
|
||||||
return mUncompressedSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getLocalFileHeaderOffset() {
|
|
||||||
return mLocalFileHeaderOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Central Directory Record starting at the current position of the provided buffer
|
|
||||||
* and advances the buffer's position immediately past the end of the record.
|
|
||||||
*/
|
|
||||||
public static CentralDirectoryRecord getRecord(ByteBuffer buf) throws ZipFormatException {
|
|
||||||
ZipUtils.assertByteOrderLittleEndian(buf);
|
|
||||||
if (buf.remaining() < HEADER_SIZE_BYTES) {
|
|
||||||
throw new ZipFormatException(
|
|
||||||
"Input too short. Need at least: " + HEADER_SIZE_BYTES
|
|
||||||
+ " bytes, available: " + buf.remaining() + " bytes",
|
|
||||||
new BufferUnderflowException());
|
|
||||||
}
|
|
||||||
int originalPosition = buf.position();
|
|
||||||
int recordSignature = buf.getInt();
|
|
||||||
if (recordSignature != RECORD_SIGNATURE) {
|
|
||||||
throw new ZipFormatException(
|
|
||||||
"Not a Central Directory record. Signature: 0x"
|
|
||||||
+ Long.toHexString(recordSignature & 0xffffffffL));
|
|
||||||
}
|
|
||||||
buf.position(originalPosition + LAST_MODIFICATION_TIME_OFFSET);
|
|
||||||
int lastModificationTime = ZipUtils.getUnsignedInt16(buf);
|
|
||||||
int lastModificationDate = ZipUtils.getUnsignedInt16(buf);
|
|
||||||
long crc32 = ZipUtils.getUnsignedInt32(buf);
|
|
||||||
long compressedSize = ZipUtils.getUnsignedInt32(buf);
|
|
||||||
long uncompressedSize = ZipUtils.getUnsignedInt32(buf);
|
|
||||||
int nameSize = ZipUtils.getUnsignedInt16(buf);
|
|
||||||
int extraSize = ZipUtils.getUnsignedInt16(buf);
|
|
||||||
int commentSize = ZipUtils.getUnsignedInt16(buf);
|
|
||||||
buf.position(originalPosition + LOCAL_FILE_HEADER_OFFSET_OFFSET);
|
|
||||||
long localFileHeaderOffset = ZipUtils.getUnsignedInt32(buf);
|
|
||||||
buf.position(originalPosition);
|
|
||||||
int recordSize = HEADER_SIZE_BYTES + nameSize + extraSize + commentSize;
|
|
||||||
if (recordSize > buf.remaining()) {
|
|
||||||
throw new ZipFormatException(
|
|
||||||
"Input too short. Need: " + recordSize + " bytes, available: "
|
|
||||||
+ buf.remaining() + " bytes",
|
|
||||||
new BufferUnderflowException());
|
|
||||||
}
|
|
||||||
String name = getName(buf, originalPosition + NAME_OFFSET, nameSize);
|
|
||||||
buf.position(originalPosition);
|
|
||||||
int originalLimit = buf.limit();
|
|
||||||
int recordEndInBuf = originalPosition + recordSize;
|
|
||||||
ByteBuffer recordBuf;
|
|
||||||
try {
|
|
||||||
buf.limit(recordEndInBuf);
|
|
||||||
recordBuf = buf.slice();
|
|
||||||
} finally {
|
|
||||||
buf.limit(originalLimit);
|
|
||||||
}
|
|
||||||
// Consume this record
|
|
||||||
buf.position(recordEndInBuf);
|
|
||||||
return new CentralDirectoryRecord(
|
|
||||||
recordBuf,
|
|
||||||
lastModificationTime,
|
|
||||||
lastModificationDate,
|
|
||||||
crc32,
|
|
||||||
compressedSize,
|
|
||||||
uncompressedSize,
|
|
||||||
localFileHeaderOffset,
|
|
||||||
name,
|
|
||||||
nameSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void copyTo(ByteBuffer output) {
|
|
||||||
output.put(mData.slice());
|
|
||||||
}
|
|
||||||
|
|
||||||
public CentralDirectoryRecord createWithModifiedLocalFileHeaderOffset(
|
|
||||||
long localFileHeaderOffset) {
|
|
||||||
ByteBuffer result = ByteBuffer.allocate(mData.remaining());
|
|
||||||
result.put(mData.slice());
|
|
||||||
result.flip();
|
|
||||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
ZipUtils.setUnsignedInt32(result, LOCAL_FILE_HEADER_OFFSET_OFFSET, localFileHeaderOffset);
|
|
||||||
return new CentralDirectoryRecord(
|
|
||||||
result,
|
|
||||||
mLastModificationTime,
|
|
||||||
mLastModificationDate,
|
|
||||||
mCrc32,
|
|
||||||
mCompressedSize,
|
|
||||||
mUncompressedSize,
|
|
||||||
localFileHeaderOffset,
|
|
||||||
mName,
|
|
||||||
mNameSizeBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CentralDirectoryRecord createWithDeflateCompressedData(
|
|
||||||
String name,
|
|
||||||
int lastModifiedTime,
|
|
||||||
int lastModifiedDate,
|
|
||||||
long crc32,
|
|
||||||
long compressedSize,
|
|
||||||
long uncompressedSize,
|
|
||||||
long localFileHeaderOffset) {
|
|
||||||
byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8);
|
|
||||||
int recordSize = HEADER_SIZE_BYTES + nameBytes.length;
|
|
||||||
ByteBuffer result = ByteBuffer.allocate(recordSize);
|
|
||||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
result.putInt(RECORD_SIGNATURE);
|
|
||||||
ZipUtils.putUnsignedInt16(result, 0x14); // Version made by
|
|
||||||
ZipUtils.putUnsignedInt16(result, 0x14); // Minimum version needed to extract
|
|
||||||
result.putShort(ZipUtils.GP_FLAG_EFS); // UTF-8 character encoding used for entry name
|
|
||||||
result.putShort(ZipUtils.COMPRESSION_METHOD_DEFLATED);
|
|
||||||
ZipUtils.putUnsignedInt16(result, lastModifiedTime);
|
|
||||||
ZipUtils.putUnsignedInt16(result, lastModifiedDate);
|
|
||||||
ZipUtils.putUnsignedInt32(result, crc32);
|
|
||||||
ZipUtils.putUnsignedInt32(result, compressedSize);
|
|
||||||
ZipUtils.putUnsignedInt32(result, uncompressedSize);
|
|
||||||
ZipUtils.putUnsignedInt16(result, nameBytes.length);
|
|
||||||
ZipUtils.putUnsignedInt16(result, 0); // Extra field length
|
|
||||||
ZipUtils.putUnsignedInt16(result, 0); // File comment length
|
|
||||||
ZipUtils.putUnsignedInt16(result, 0); // Disk number
|
|
||||||
ZipUtils.putUnsignedInt16(result, 0); // Internal file attributes
|
|
||||||
ZipUtils.putUnsignedInt32(result, 0); // External file attributes
|
|
||||||
ZipUtils.putUnsignedInt32(result, localFileHeaderOffset);
|
|
||||||
result.put(nameBytes);
|
|
||||||
|
|
||||||
if (result.hasRemaining()) {
|
|
||||||
throw new RuntimeException("pos: " + result.position() + ", limit: " + result.limit());
|
|
||||||
}
|
|
||||||
result.flip();
|
|
||||||
return new CentralDirectoryRecord(
|
|
||||||
result,
|
|
||||||
lastModifiedTime,
|
|
||||||
lastModifiedDate,
|
|
||||||
crc32,
|
|
||||||
compressedSize,
|
|
||||||
uncompressedSize,
|
|
||||||
localFileHeaderOffset,
|
|
||||||
name,
|
|
||||||
nameBytes.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String getName(ByteBuffer record, int position, int nameLengthBytes) {
|
|
||||||
byte[] nameBytes;
|
|
||||||
int nameBytesOffset;
|
|
||||||
if (record.hasArray()) {
|
|
||||||
nameBytes = record.array();
|
|
||||||
nameBytesOffset = record.arrayOffset() + position;
|
|
||||||
} else {
|
|
||||||
nameBytes = new byte[nameLengthBytes];
|
|
||||||
nameBytesOffset = 0;
|
|
||||||
int originalPosition = record.position();
|
|
||||||
try {
|
|
||||||
record.position(position);
|
|
||||||
record.get(nameBytes);
|
|
||||||
} finally {
|
|
||||||
record.position(originalPosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new String(nameBytes, nameBytesOffset, nameLengthBytes, StandardCharsets.UTF_8);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ByLocalFileHeaderOffsetComparator
|
|
||||||
implements Comparator<CentralDirectoryRecord> {
|
|
||||||
@Override
|
|
||||||
public int compare(CentralDirectoryRecord r1, CentralDirectoryRecord r2) {
|
|
||||||
long offset1 = r1.getLocalFileHeaderOffset();
|
|
||||||
long offset2 = r2.getLocalFileHeaderOffset();
|
|
||||||
if (offset1 > offset2) {
|
|
||||||
return 1;
|
|
||||||
} else if (offset1 < offset2) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +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.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,540 +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 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,353 +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.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.util.zip.CRC32;
|
|
||||||
import java.util.zip.Deflater;
|
|
||||||
|
|
||||||
import com.android.apksigner.core.internal.util.Pair;
|
|
||||||
import com.android.apksigner.core.util.DataSource;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assorted ZIP format helpers.
|
|
||||||
*
|
|
||||||
* <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte
|
|
||||||
* order of these buffers is little-endian.
|
|
||||||
*/
|
|
||||||
public abstract class ZipUtils {
|
|
||||||
private ZipUtils() {}
|
|
||||||
|
|
||||||
public static final short COMPRESSION_METHOD_STORED = 0;
|
|
||||||
public static final short COMPRESSION_METHOD_DEFLATED = 8;
|
|
||||||
|
|
||||||
public static final short GP_FLAG_DATA_DESCRIPTOR_USED = 0x08;
|
|
||||||
public static final short GP_FLAG_EFS = 0x0800;
|
|
||||||
|
|
||||||
private static final int ZIP_EOCD_REC_MIN_SIZE = 22;
|
|
||||||
private static final int ZIP_EOCD_REC_SIG = 0x06054b50;
|
|
||||||
private static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10;
|
|
||||||
private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12;
|
|
||||||
private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
|
|
||||||
private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20;
|
|
||||||
|
|
||||||
private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;
|
|
||||||
private static final int ZIP64_EOCD_LOCATOR_SIG = 0x07064b50;
|
|
||||||
|
|
||||||
private static final int UINT16_MAX_VALUE = 0xffff;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the offset of the start of the ZIP Central Directory in the archive.
|
|
||||||
*
|
|
||||||
* <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
|
|
||||||
*/
|
|
||||||
public static void setZipEocdCentralDirectoryOffset(
|
|
||||||
ByteBuffer zipEndOfCentralDirectory, long offset) {
|
|
||||||
assertByteOrderLittleEndian(zipEndOfCentralDirectory);
|
|
||||||
setUnsignedInt32(
|
|
||||||
zipEndOfCentralDirectory,
|
|
||||||
zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET,
|
|
||||||
offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the offset of the start of the ZIP Central Directory in the archive.
|
|
||||||
*
|
|
||||||
* <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
|
|
||||||
*/
|
|
||||||
public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) {
|
|
||||||
assertByteOrderLittleEndian(zipEndOfCentralDirectory);
|
|
||||||
return getUnsignedInt32(
|
|
||||||
zipEndOfCentralDirectory,
|
|
||||||
zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the size (in bytes) of the ZIP Central Directory.
|
|
||||||
*
|
|
||||||
* <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
|
|
||||||
*/
|
|
||||||
public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) {
|
|
||||||
assertByteOrderLittleEndian(zipEndOfCentralDirectory);
|
|
||||||
return getUnsignedInt32(
|
|
||||||
zipEndOfCentralDirectory,
|
|
||||||
zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the total number of records in ZIP Central Directory.
|
|
||||||
*
|
|
||||||
* <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
|
|
||||||
*/
|
|
||||||
public static int getZipEocdCentralDirectoryTotalRecordCount(
|
|
||||||
ByteBuffer zipEndOfCentralDirectory) {
|
|
||||||
assertByteOrderLittleEndian(zipEndOfCentralDirectory);
|
|
||||||
return getUnsignedInt16(
|
|
||||||
zipEndOfCentralDirectory,
|
|
||||||
zipEndOfCentralDirectory.position()
|
|
||||||
+ ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the ZIP End of Central Directory record of the provided ZIP file.
|
|
||||||
*
|
|
||||||
* @return contents of the ZIP End of Central Directory record and the record's offset in the
|
|
||||||
* file or {@code null} if the file does not contain the record.
|
|
||||||
*
|
|
||||||
* @throws IOException if an I/O error occurs while reading the file.
|
|
||||||
*/
|
|
||||||
public static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(DataSource zip)
|
|
||||||
throws IOException {
|
|
||||||
// ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
|
|
||||||
// The record can be identified by its 4-byte signature/magic which is located at the very
|
|
||||||
// beginning of the record. A complication is that the record is variable-length because of
|
|
||||||
// the comment field.
|
|
||||||
// The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
|
|
||||||
// end of the buffer for the EOCD record signature. Whenever we find a signature, we check
|
|
||||||
// the candidate record's comment length is such that the remainder of the record takes up
|
|
||||||
// exactly the remaining bytes in the buffer. The search is bounded because the maximum
|
|
||||||
// size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
|
|
||||||
|
|
||||||
long fileSize = zip.size();
|
|
||||||
if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus
|
|
||||||
// the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily
|
|
||||||
// reading more data.
|
|
||||||
Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0);
|
|
||||||
if (result != null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// EoCD does not start where we expected it to. Perhaps it contains a non-empty comment
|
|
||||||
// field. Expand the search. The maximum size of the comment field in EoCD is 65535 because
|
|
||||||
// the comment length field is an unsigned 16-bit number.
|
|
||||||
return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the ZIP End of Central Directory record of the provided ZIP file.
|
|
||||||
*
|
|
||||||
* @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted
|
|
||||||
* value is from 0 to 65535 inclusive. The smaller the value, the faster this method
|
|
||||||
* locates the record, provided its comment field is no longer than this value.
|
|
||||||
*
|
|
||||||
* @return contents of the ZIP End of Central Directory record and the record's offset in the
|
|
||||||
* file or {@code null} if the file does not contain the record.
|
|
||||||
*
|
|
||||||
* @throws IOException if an I/O error occurs while reading the file.
|
|
||||||
*/
|
|
||||||
private static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(
|
|
||||||
DataSource zip, int maxCommentSize) throws IOException {
|
|
||||||
// ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
|
|
||||||
// The record can be identified by its 4-byte signature/magic which is located at the very
|
|
||||||
// beginning of the record. A complication is that the record is variable-length because of
|
|
||||||
// the comment field.
|
|
||||||
// The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
|
|
||||||
// end of the buffer for the EOCD record signature. Whenever we find a signature, we check
|
|
||||||
// the candidate record's comment length is such that the remainder of the record takes up
|
|
||||||
// exactly the remaining bytes in the buffer. The search is bounded because the maximum
|
|
||||||
// size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
|
|
||||||
|
|
||||||
if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) {
|
|
||||||
throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
long fileSize = zip.size();
|
|
||||||
if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
|
|
||||||
// No space for EoCD record in the file.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// Lower maxCommentSize if the file is too small.
|
|
||||||
maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE);
|
|
||||||
|
|
||||||
int maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize;
|
|
||||||
long bufOffsetInFile = fileSize - maxEocdSize;
|
|
||||||
ByteBuffer buf = zip.getByteBuffer(bufOffsetInFile, maxEocdSize);
|
|
||||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf);
|
|
||||||
if (eocdOffsetInBuf == -1) {
|
|
||||||
// No EoCD record found in the buffer
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// EoCD found
|
|
||||||
buf.position(eocdOffsetInBuf);
|
|
||||||
ByteBuffer eocd = buf.slice();
|
|
||||||
eocd.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
return Pair.of(eocd, bufOffsetInFile + eocdOffsetInBuf);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the position at which ZIP End of Central Directory record starts in the provided
|
|
||||||
* buffer or {@code -1} if the record is not present.
|
|
||||||
*
|
|
||||||
* <p>NOTE: Byte order of {@code zipContents} must be little-endian.
|
|
||||||
*/
|
|
||||||
private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
|
|
||||||
assertByteOrderLittleEndian(zipContents);
|
|
||||||
|
|
||||||
// ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
|
|
||||||
// The record can be identified by its 4-byte signature/magic which is located at the very
|
|
||||||
// beginning of the record. A complication is that the record is variable-length because of
|
|
||||||
// the comment field.
|
|
||||||
// The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
|
|
||||||
// end of the buffer for the EOCD record signature. Whenever we find a signature, we check
|
|
||||||
// the candidate record's comment length is such that the remainder of the record takes up
|
|
||||||
// exactly the remaining bytes in the buffer. The search is bounded because the maximum
|
|
||||||
// size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
|
|
||||||
|
|
||||||
int archiveSize = zipContents.capacity();
|
|
||||||
if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE);
|
|
||||||
int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
|
|
||||||
for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength;
|
|
||||||
expectedCommentLength++) {
|
|
||||||
int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength;
|
|
||||||
if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) {
|
|
||||||
int actualCommentLength =
|
|
||||||
getUnsignedInt16(
|
|
||||||
zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET);
|
|
||||||
if (actualCommentLength == expectedCommentLength) {
|
|
||||||
return eocdStartPos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns {@code true} if the provided file contains a ZIP64 End of Central Directory
|
|
||||||
* Locator.
|
|
||||||
*
|
|
||||||
* @param zipEndOfCentralDirectoryPosition offset of the ZIP End of Central Directory record
|
|
||||||
* in the file.
|
|
||||||
*
|
|
||||||
* @throws IOException if an I/O error occurs while reading the data source
|
|
||||||
*/
|
|
||||||
public static final boolean isZip64EndOfCentralDirectoryLocatorPresent(
|
|
||||||
DataSource zip, long zipEndOfCentralDirectoryPosition) throws IOException {
|
|
||||||
|
|
||||||
// ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central
|
|
||||||
// Directory Record.
|
|
||||||
long locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE;
|
|
||||||
if (locatorPosition < 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteBuffer sig = zip.getByteBuffer(locatorPosition, 4);
|
|
||||||
sig.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
return sig.getInt(0) == ZIP64_EOCD_LOCATOR_SIG;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void assertByteOrderLittleEndian(ByteBuffer buffer) {
|
|
||||||
if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
|
|
||||||
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getUnsignedInt16(ByteBuffer buffer, int offset) {
|
|
||||||
return buffer.getShort(offset) & 0xffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getUnsignedInt16(ByteBuffer buffer) {
|
|
||||||
return buffer.getShort() & 0xffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void setUnsignedInt16(ByteBuffer buffer, int offset, int value) {
|
|
||||||
if ((value < 0) || (value > 0xffff)) {
|
|
||||||
throw new IllegalArgumentException("uint16 value of out range: " + value);
|
|
||||||
}
|
|
||||||
buffer.putShort(offset, (short) value);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) {
|
|
||||||
if ((value < 0) || (value > 0xffffffffL)) {
|
|
||||||
throw new IllegalArgumentException("uint32 value of out range: " + value);
|
|
||||||
}
|
|
||||||
buffer.putInt(offset, (int) value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void putUnsignedInt16(ByteBuffer buffer, int value) {
|
|
||||||
if ((value < 0) || (value > 0xffff)) {
|
|
||||||
throw new IllegalArgumentException("uint16 value of out range: " + value);
|
|
||||||
}
|
|
||||||
buffer.putShort((short) value);
|
|
||||||
}
|
|
||||||
|
|
||||||
static long getUnsignedInt32(ByteBuffer buffer, int offset) {
|
|
||||||
return buffer.getInt(offset) & 0xffffffffL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static long getUnsignedInt32(ByteBuffer buffer) {
|
|
||||||
return buffer.getInt() & 0xffffffffL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void putUnsignedInt32(ByteBuffer buffer, long value) {
|
|
||||||
if ((value < 0) || (value > 0xffffffffL)) {
|
|
||||||
throw new IllegalArgumentException("uint32 value of out range: " + value);
|
|
||||||
}
|
|
||||||
buffer.putInt((int) value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DeflateResult deflate(ByteBuffer input) {
|
|
||||||
byte[] inputBuf;
|
|
||||||
int inputOffset;
|
|
||||||
int inputLength = input.remaining();
|
|
||||||
if (input.hasArray()) {
|
|
||||||
inputBuf = input.array();
|
|
||||||
inputOffset = input.arrayOffset() + input.position();
|
|
||||||
input.position(input.limit());
|
|
||||||
} else {
|
|
||||||
inputBuf = new byte[inputLength];
|
|
||||||
inputOffset = 0;
|
|
||||||
input.get(inputBuf);
|
|
||||||
}
|
|
||||||
CRC32 crc32 = new CRC32();
|
|
||||||
crc32.update(inputBuf, inputOffset, inputLength);
|
|
||||||
long crc32Value = crc32.getValue();
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
||||||
Deflater deflater = new Deflater(9, true);
|
|
||||||
deflater.setInput(inputBuf, inputOffset, inputLength);
|
|
||||||
deflater.finish();
|
|
||||||
byte[] buf = new byte[65536];
|
|
||||||
while (!deflater.finished()) {
|
|
||||||
int chunkSize = deflater.deflate(buf);
|
|
||||||
out.write(buf, 0, chunkSize);
|
|
||||||
}
|
|
||||||
return new DeflateResult(inputLength, crc32Value, out.toByteArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class DeflateResult {
|
|
||||||
public final int inputSizeBytes;
|
|
||||||
public final long inputCrc32;
|
|
||||||
public final byte[] output;
|
|
||||||
|
|
||||||
public DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output) {
|
|
||||||
this.inputSizeBytes = inputSizeBytes;
|
|
||||||
this.inputCrc32 = inputCrc32;
|
|
||||||
this.output = output;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +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.util;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consumer of input data which may be provided in one go or in chunks.
|
|
||||||
*/
|
|
||||||
public interface DataSink {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consumes the provided chunk of data.
|
|
||||||
*
|
|
||||||
* <p>This data sink guarantees to not hold references to the provided buffer after this method
|
|
||||||
* terminates.
|
|
||||||
*/
|
|
||||||
void consume(byte[] buf, int offset, int length) throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consumes all remaining data in the provided buffer and advances the buffer's position
|
|
||||||
* to the buffer's limit.
|
|
||||||
*
|
|
||||||
* <p>This data sink guarantees to not hold references to the provided buffer after this method
|
|
||||||
* terminates.
|
|
||||||
*/
|
|
||||||
void consume(ByteBuffer buf) throws IOException;
|
|
||||||
}
|
|
|
@ -1,46 +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.util;
|
|
||||||
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
|
|
||||||
import com.android.apksigner.core.internal.util.OutputStreamDataSink;
|
|
||||||
import com.android.apksigner.core.internal.util.RandomAccessFileDataSink;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility methods for working with {@link DataSink} abstraction.
|
|
||||||
*/
|
|
||||||
public abstract class DataSinks {
|
|
||||||
private DataSinks() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a {@link DataSink} which outputs received data into the provided
|
|
||||||
* {@link OutputStream}.
|
|
||||||
*/
|
|
||||||
public static DataSink asDataSink(OutputStream out) {
|
|
||||||
return new OutputStreamDataSink(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a {@link DataSink} which outputs received data into the provided file, sequentially,
|
|
||||||
* starting at the beginning of the file.
|
|
||||||
*/
|
|
||||||
public static DataSink asDataSink(RandomAccessFile file) {
|
|
||||||
return new RandomAccessFileDataSink(file);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,98 +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.util;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract representation of a source of data.
|
|
||||||
*
|
|
||||||
* <p>This abstraction serves three purposes:
|
|
||||||
* <ul>
|
|
||||||
* <li>Transparent handling of different types of sources, such as {@code byte[]},
|
|
||||||
* {@link java.nio.ByteBuffer}, {@link java.io.RandomAccessFile}, memory-mapped file.</li>
|
|
||||||
* <li>Support sources larger than 2 GB. If all sources were smaller than 2 GB, {@code ByteBuffer}
|
|
||||||
* may have worked as the unifying abstraction.</li>
|
|
||||||
* <li>Support sources which do not fit into logical memory as a contiguous region.</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* <p>There are following ways to obtain a chunk of data from the data source:
|
|
||||||
* <ul>
|
|
||||||
* <li>Stream the chunk's data into a {@link DataSink} using
|
|
||||||
* {@link #feed(long, long, DataSink) feed}. This is best suited for scenarios where there is no
|
|
||||||
* need to have the chunk's data accessible at the same time, for example, when computing the
|
|
||||||
* digest of the chunk. If you need to keep the chunk's data around after {@code feed}
|
|
||||||
* completes, you must create a copy during {@code feed}. However, in that case the following
|
|
||||||
* methods of obtaining the chunk's data may be more appropriate.</li>
|
|
||||||
* <li>Obtain a {@link ByteBuffer} containing the chunk's data using
|
|
||||||
* {@link #getByteBuffer(long, int) getByteBuffer}. Depending on the data source, the chunk's
|
|
||||||
* data may or may not be copied by this operation. This is best suited for scenarios where
|
|
||||||
* you need to access the chunk's data in arbitrary order, but don't need to modify the data and
|
|
||||||
* thus don't require a copy of the data.</li>
|
|
||||||
* <li>Copy the chunk's data to a {@link ByteBuffer} using
|
|
||||||
* {@link #copyTo(long, int, ByteBuffer) copyTo}. This is best suited for scenarios where
|
|
||||||
* you require a copy of the chunk's data, such as to when you need to modify the data.
|
|
||||||
* </li>
|
|
||||||
* </ul>
|
|
||||||
*/
|
|
||||||
public interface DataSource {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the amount of data (in bytes) contained in this data source.
|
|
||||||
*/
|
|
||||||
long size();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Feeds the specified chunk from this data source into the provided sink.
|
|
||||||
*
|
|
||||||
* @param offset index (in bytes) at which the chunk starts inside data source
|
|
||||||
* @param size size (in bytes) of the chunk
|
|
||||||
*/
|
|
||||||
void feed(long offset, long size, DataSink sink) throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a buffer holding the contents of the specified chunk of data from this data source.
|
|
||||||
* Changes to the data source are not guaranteed to be reflected in the returned buffer.
|
|
||||||
* Similarly, changes in the buffer are not guaranteed to be reflected in the data source.
|
|
||||||
*
|
|
||||||
* <p>The returned buffer's position is {@code 0}, and the buffer's limit and capacity is
|
|
||||||
* {@code size}.
|
|
||||||
*
|
|
||||||
* @param offset index (in bytes) at which the chunk starts inside data source
|
|
||||||
* @param size size (in bytes) of the chunk
|
|
||||||
*/
|
|
||||||
ByteBuffer getByteBuffer(long offset, int size) throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies the specified chunk from this data source into the provided destination buffer,
|
|
||||||
* advancing the destination buffer's position by {@code size}.
|
|
||||||
*
|
|
||||||
* @param offset index (in bytes) at which the chunk starts inside data source
|
|
||||||
* @param size size (in bytes) of the chunk
|
|
||||||
*/
|
|
||||||
void copyTo(long offset, int size, ByteBuffer dest) throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a data source representing the specified region of data of this data source. Changes
|
|
||||||
* to data represented by this data source will also be visible in the returned data source.
|
|
||||||
*
|
|
||||||
* @param offset index (in bytes) at which the region starts inside data source
|
|
||||||
* @param size size (in bytes) of the region
|
|
||||||
*/
|
|
||||||
DataSource slice(long offset, long size);
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
package com.android.apksigner.core.util;
|
|
||||||
|
|
||||||
import com.android.apksigner.core.internal.util.ByteBufferDataSource;
|
|
||||||
import com.android.apksigner.core.internal.util.RandomAccessFileDataSource;
|
|
||||||
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility methods for working with {@link DataSource} abstraction.
|
|
||||||
*/
|
|
||||||
public abstract class DataSources {
|
|
||||||
private DataSources() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a {@link DataSource} backed by the provided {@link ByteBuffer}. The data source
|
|
||||||
* represents the data contained between the position and limit of the buffer. Changes to the
|
|
||||||
* buffer's contents will be visible in the data source.
|
|
||||||
*/
|
|
||||||
public static DataSource asDataSource(ByteBuffer buffer) {
|
|
||||||
if (buffer == null) {
|
|
||||||
throw new NullPointerException();
|
|
||||||
}
|
|
||||||
return new ByteBufferDataSource(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a {@link DataSource} backed by the provided {@link RandomAccessFile}. Changes to the
|
|
||||||
* file, including changes to size of file, will be visible in the data source.
|
|
||||||
*/
|
|
||||||
public static DataSource asDataSource(RandomAccessFile file) {
|
|
||||||
if (file == null) {
|
|
||||||
throw new NullPointerException();
|
|
||||||
}
|
|
||||||
return new RandomAccessFileDataSource(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a {@link DataSource} backed by the provided region of the {@link RandomAccessFile}.
|
|
||||||
* Changes to the file will be visible in the data source.
|
|
||||||
*/
|
|
||||||
public static DataSource asDataSource(RandomAccessFile file, long offset, long size) {
|
|
||||||
if (file == null) {
|
|
||||||
throw new NullPointerException();
|
|
||||||
}
|
|
||||||
return new RandomAccessFileDataSource(file, offset, size);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +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.zip;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates that a ZIP archive is not well-formed.
|
|
||||||
*/
|
|
||||||
public class ZipFormatException extends Exception {
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
public ZipFormatException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ZipFormatException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -22,7 +22,7 @@ LOCAL_MODULE := signapk
|
||||||
LOCAL_SRC_FILES := $(call all-java-files-under, src)
|
LOCAL_SRC_FILES := $(call all-java-files-under, src)
|
||||||
LOCAL_JAR_MANIFEST := SignApk.mf
|
LOCAL_JAR_MANIFEST := SignApk.mf
|
||||||
LOCAL_STATIC_JAVA_LIBRARIES := \
|
LOCAL_STATIC_JAVA_LIBRARIES := \
|
||||||
apksigner-core \
|
apksig \
|
||||||
bouncycastle-host \
|
bouncycastle-host \
|
||||||
bouncycastle-bcpkix-host \
|
bouncycastle-bcpkix-host \
|
||||||
conscrypt-host
|
conscrypt-host
|
||||||
|
|
|
@ -34,12 +34,12 @@ import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
||||||
import org.conscrypt.OpenSSLProvider;
|
import org.conscrypt.OpenSSLProvider;
|
||||||
|
|
||||||
import com.android.apksigner.core.ApkSignerEngine;
|
import com.android.apksig.ApkSignerEngine;
|
||||||
import com.android.apksigner.core.DefaultApkSignerEngine;
|
import com.android.apksig.DefaultApkSignerEngine;
|
||||||
import com.android.apksigner.core.apk.ApkUtils;
|
import com.android.apksig.apk.ApkUtils;
|
||||||
import com.android.apksigner.core.util.DataSink;
|
import com.android.apksig.util.DataSink;
|
||||||
import com.android.apksigner.core.util.DataSources;
|
import com.android.apksig.util.DataSources;
|
||||||
import com.android.apksigner.core.zip.ZipFormatException;
|
import com.android.apksig.zip.ZipFormatException;
|
||||||
|
|
||||||
import java.io.Console;
|
import java.io.Console;
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
|
Loading…
Reference in New Issue