diff --git a/tools/apksigner/Android.mk b/tools/apksigner/Android.mk deleted file mode 100644 index a7b4414c5..000000000 --- a/tools/apksigner/Android.mk +++ /dev/null @@ -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)) diff --git a/tools/apksigner/core/Android.mk b/tools/apksigner/core/Android.mk deleted file mode 100644 index 132a6f1f0..000000000 --- a/tools/apksigner/core/Android.mk +++ /dev/null @@ -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) diff --git a/tools/apksigner/core/src/com/android/apksigner/core/ApkSigner.java b/tools/apksigner/core/src/com/android/apksigner/core/ApkSigner.java deleted file mode 100644 index 2491302ce..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/ApkSigner.java +++ /dev/null @@ -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. - * - *

The signer preserves as much of the input APK as possible. For example, it preserves the - * order of APK entries and preserves their contents, including compressed form and alignment of - * data. - * - *

Use {@link Builder} to obtain instances of this signer. - */ -public class ApkSigner { - - /** - * Extensible data block/field header ID used for storing information about alignment of - * uncompressed entries as well as for aligning the entries's data. See ZIP appnote.txt section - * 4.5 Extensible data fields. - */ - private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = (short) 0xd935; - - /** - * Minimum size (in bytes) of the extensible data block/field used for alignment of uncompressed - * entries. - */ - private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES = 6; - - private final ApkSignerEngine mSignerEngine; - - private final File mInputApkFile; - private final DataSource mInputApkDataSource; - - private final File mOutputApkFile; - private final DataSink mOutputApkDataSink; - private final DataSource mOutputApkDataSource; - - private ApkSigner( - ApkSignerEngine signerEngine, - File inputApkFile, - DataSource inputApkDataSource, - File outputApkFile, - DataSink outputApkDataSink, - DataSource outputApkDataSource) { - mSignerEngine = signerEngine; - - mInputApkFile = inputApkFile; - mInputApkDataSource = inputApkDataSource; - - mOutputApkFile = outputApkFile; - mOutputApkDataSink = outputApkDataSink; - mOutputApkDataSource = outputApkDataSource; - } - - /** - * Signs the input APK and outputs the resulting signed APK. The input APK is not modified. - * - * @throws IOException if an I/O error is encountered while reading or writing the APKs - * @throws ZipFormatException if the input APK is malformed at ZIP format level - * @throws NoSuchAlgorithmException if the APK signatures cannot be produced or verified because - * a required cryptographic algorithm implementation is missing - * @throws InvalidKeyException if a signature could not be generated because a signing key is - * not suitable for generating the signature - * @throws SignatureException if an error occurred while generating or verifying a signature - * @throws IllegalStateException if this signer's configuration is missing required information - * or if the signing engine is in an invalid state. - */ - public void sign() - throws IOException, ZipFormatException, NoSuchAlgorithmException, InvalidKeyException, - SignatureException, IllegalStateException { - Closeable in = null; - DataSource inputApk; - try { - if (mInputApkDataSource != null) { - inputApk = mInputApkDataSource; - } else if (mInputApkFile != null) { - RandomAccessFile inputFile = new RandomAccessFile(mInputApkFile, "r"); - in = inputFile; - inputApk = DataSources.asDataSource(inputFile); - } else { - throw new IllegalStateException("Input APK not specified"); - } - - Closeable out = null; - try { - DataSink outputApkOut; - DataSource outputApkIn; - if (mOutputApkDataSink != null) { - outputApkOut = mOutputApkDataSink; - outputApkIn = mOutputApkDataSource; - } else if (mOutputApkFile != null) { - RandomAccessFile outputFile = new RandomAccessFile(mOutputApkFile, "rw"); - out = outputFile; - outputFile.setLength(0); - outputApkOut = DataSinks.asDataSink(outputFile); - outputApkIn = DataSources.asDataSource(outputFile); - } else { - throw new IllegalStateException("Output APK not specified"); - } - - sign(mSignerEngine, inputApk, outputApkOut, outputApkIn); - } finally { - if (out != null) { - out.close(); - } - } - } finally { - if (in != null) { - in.close(); - } - } - } - - private static void sign( - ApkSignerEngine signerEngine, - DataSource inputApk, - DataSink outputApkOut, - DataSource outputApkIn) - throws IOException, ZipFormatException, NoSuchAlgorithmException, - InvalidKeyException, SignatureException { - // Step 1. Find input APK's main ZIP sections - ApkUtils.ZipSections inputZipSections = ApkUtils.findZipSections(inputApk); - long apkSigningBlockOffset = -1; - try { - Pair apkSigningBlockAndOffset = - V2SchemeVerifier.findApkSigningBlock(inputApk, inputZipSections); - signerEngine.inputApkSigningBlock(apkSigningBlockAndOffset.getFirst()); - apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond(); - } catch (V2SchemeVerifier.SignatureNotFoundException e) { - // Input APK does not contain an APK Signing Block. That's OK. APKs are not required to - // contain this block. It's only needed if the APK is signed using APK Signature Scheme - // v2. - } - - // Step 2. Parse the input APK's ZIP Central Directory - ByteBuffer inputCd = getZipCentralDirectory(inputApk, inputZipSections); - List inputCdRecords = - parseZipCentralDirectory(inputCd, inputZipSections); - - // Step 3. Iterate over input APK's entries and output the Local File Header + data of those - // entries which need to be output. Entries are iterated in the order in which their Local - // File Header records are stored in the file. This is to achieve better data locality in - // case Central Directory entries are in the wrong order. - List inputCdRecordsSortedByLfhOffset = - new ArrayList<>(inputCdRecords); - Collections.sort( - inputCdRecordsSortedByLfhOffset, - CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR); - DataSource inputApkLfhSection = - inputApk.slice( - 0, - (apkSigningBlockOffset != -1) - ? apkSigningBlockOffset - : inputZipSections.getZipCentralDirectoryOffset()); - int lastModifiedDateForNewEntries = -1; - int lastModifiedTimeForNewEntries = -1; - long inputOffset = 0; - long outputOffset = 0; - Map outputCdRecordsByName = - new HashMap<>(inputCdRecords.size()); - for (final CentralDirectoryRecord inputCdRecord : inputCdRecordsSortedByLfhOffset) { - String entryName = inputCdRecord.getName(); - ApkSignerEngine.InputJarEntryInstructions entryInstructions = - signerEngine.inputJarEntry(entryName); - boolean shouldOutput; - switch (entryInstructions.getOutputPolicy()) { - case OUTPUT: - shouldOutput = true; - break; - case OUTPUT_BY_ENGINE: - case SKIP: - shouldOutput = false; - break; - default: - throw new RuntimeException( - "Unknown output policy: " + entryInstructions.getOutputPolicy()); - } - - long inputLocalFileHeaderStartOffset = inputCdRecord.getLocalFileHeaderOffset(); - if (inputLocalFileHeaderStartOffset > inputOffset) { - // Unprocessed data in input starting at inputOffset and ending and the start of - // this record's LFH. We output this data verbatim because this signer is supposed - // to preserve as much of input as possible. - long chunkSize = inputLocalFileHeaderStartOffset - inputOffset; - inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut); - outputOffset += chunkSize; - inputOffset = inputLocalFileHeaderStartOffset; - } - LocalFileRecord inputLocalFileRecord = - LocalFileRecord.getRecord( - inputApkLfhSection, inputCdRecord, inputApkLfhSection.size()); - inputOffset += inputLocalFileRecord.getSize(); - - ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = - entryInstructions.getInspectJarEntryRequest(); - if (inspectEntryRequest != null) { - fulfillInspectInputJarEntryRequest( - inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest); - } - - if (shouldOutput) { - // Find the max value of last modified, to be used for new entries added by the - // signer. - int lastModifiedDate = inputCdRecord.getLastModificationDate(); - int lastModifiedTime = inputCdRecord.getLastModificationTime(); - if ((lastModifiedDateForNewEntries == -1) - || (lastModifiedDate > lastModifiedDateForNewEntries) - || ((lastModifiedDate == lastModifiedDateForNewEntries) - && (lastModifiedTime > lastModifiedTimeForNewEntries))) { - lastModifiedDateForNewEntries = lastModifiedDate; - lastModifiedTimeForNewEntries = lastModifiedTime; - } - - inspectEntryRequest = signerEngine.outputJarEntry(entryName); - if (inspectEntryRequest != null) { - fulfillInspectInputJarEntryRequest( - inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest); - } - - // Output entry's Local File Header + data - long outputLocalFileHeaderOffset = outputOffset; - long outputLocalFileRecordSize = - outputInputJarEntryLfhRecordPreservingDataAlignment( - inputApkLfhSection, - inputLocalFileRecord, - outputApkOut, - outputLocalFileHeaderOffset); - outputOffset += outputLocalFileRecordSize; - - // Enqueue entry's Central Directory record for output - CentralDirectoryRecord outputCdRecord; - if (outputLocalFileHeaderOffset == inputLocalFileRecord.getStartOffsetInArchive()) { - outputCdRecord = inputCdRecord; - } else { - outputCdRecord = - inputCdRecord.createWithModifiedLocalFileHeaderOffset( - outputLocalFileHeaderOffset); - } - outputCdRecordsByName.put(entryName, outputCdRecord); - } - } - long inputLfhSectionSize = inputApkLfhSection.size(); - if (inputOffset < inputLfhSectionSize) { - // Unprocessed data in input starting at inputOffset and ending and the end of the input - // APK's LFH section. We output this data verbatim because this signer is supposed - // to preserve as much of input as possible. - long chunkSize = inputLfhSectionSize - inputOffset; - inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut); - outputOffset += chunkSize; - inputOffset = inputLfhSectionSize; - } - - // Step 4. Sort output APK's Central Directory records in the order in which they should - // appear in the output - List outputCdRecords = new ArrayList<>(inputCdRecords.size() + 10); - for (CentralDirectoryRecord inputCdRecord : inputCdRecords) { - String entryName = inputCdRecord.getName(); - CentralDirectoryRecord outputCdRecord = outputCdRecordsByName.get(entryName); - if (outputCdRecord != null) { - outputCdRecords.add(outputCdRecord); - } - } - - // Step 5. Generate and output JAR signatures, if necessary. This may output more Local File - // Header + data entries and add to the list of output Central Directory records. - ApkSignerEngine.OutputJarSignatureRequest outputJarSignatureRequest = - signerEngine.outputJarEntries(); - if (outputJarSignatureRequest != null) { - if (lastModifiedDateForNewEntries == -1) { - lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS) - lastModifiedTimeForNewEntries = 0; - } - for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry : - outputJarSignatureRequest.getAdditionalJarEntries()) { - String entryName = entry.getName(); - byte[] uncompressedData = entry.getData(); - ZipUtils.DeflateResult deflateResult = - ZipUtils.deflate(ByteBuffer.wrap(uncompressedData)); - byte[] compressedData = deflateResult.output; - long uncompressedDataCrc32 = deflateResult.inputCrc32; - - ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = - signerEngine.outputJarEntry(entryName); - if (inspectEntryRequest != null) { - inspectEntryRequest.getDataSink().consume( - uncompressedData, 0, uncompressedData.length); - inspectEntryRequest.done(); - } - - long localFileHeaderOffset = outputOffset; - outputOffset += - LocalFileRecord.outputRecordWithDeflateCompressedData( - entryName, - lastModifiedTimeForNewEntries, - lastModifiedDateForNewEntries, - compressedData, - uncompressedDataCrc32, - uncompressedData.length, - outputApkOut); - - - outputCdRecords.add( - CentralDirectoryRecord.createWithDeflateCompressedData( - entryName, - lastModifiedTimeForNewEntries, - lastModifiedDateForNewEntries, - uncompressedDataCrc32, - compressedData.length, - uncompressedData.length, - localFileHeaderOffset)); - } - outputJarSignatureRequest.done(); - } - - // Step 6. Construct output ZIP Central Directory in an in-memory buffer - long outputCentralDirSizeBytes = 0; - for (CentralDirectoryRecord record : outputCdRecords) { - outputCentralDirSizeBytes += record.getSize(); - } - if (outputCentralDirSizeBytes > Integer.MAX_VALUE) { - throw new IOException( - "Output ZIP Central Directory too large: " + outputCentralDirSizeBytes - + " bytes"); - } - ByteBuffer outputCentralDir = ByteBuffer.allocate((int) outputCentralDirSizeBytes); - for (CentralDirectoryRecord record : outputCdRecords) { - record.copyTo(outputCentralDir); - } - outputCentralDir.flip(); - DataSource outputCentralDirDataSource = new ByteBufferDataSource(outputCentralDir); - long outputCentralDirStartOffset = outputOffset; - int outputCentralDirRecordCount = outputCdRecords.size(); - - // Step 7. Construct output ZIP End of Central Directory record in an in-memory buffer - ByteBuffer outputEocd = - EocdRecord.createWithModifiedCentralDirectoryInfo( - inputZipSections.getZipEndOfCentralDirectory(), - outputCentralDirRecordCount, - outputCentralDirDataSource.size(), - outputCentralDirStartOffset); - - // Step 8. Generate and output APK Signature Scheme v2 signatures, if necessary. This may - // insert an APK Signing Block just before the output's ZIP Central Directory - ApkSignerEngine.OutputApkSigningBlockRequest outputApkSigingBlockRequest = - signerEngine.outputZipSections( - outputApkIn, - outputCentralDirDataSource, - DataSources.asDataSource(outputEocd)); - if (outputApkSigingBlockRequest != null) { - byte[] outputApkSigningBlock = outputApkSigingBlockRequest.getApkSigningBlock(); - outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length); - ZipUtils.setZipEocdCentralDirectoryOffset( - outputEocd, outputCentralDirStartOffset + outputApkSigningBlock.length); - outputApkSigingBlockRequest.done(); - } - - // Step 9. Output ZIP Central Directory and ZIP End of Central Directory - outputCentralDirDataSource.feed(0, outputCentralDirDataSource.size(), outputApkOut); - outputApkOut.consume(outputEocd); - signerEngine.outputDone(); - } - - private static void fulfillInspectInputJarEntryRequest( - DataSource lfhSection, - LocalFileRecord localFileRecord, - ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest) - throws IOException, ZipFormatException { - localFileRecord.outputUncompressedData(lfhSection, inspectEntryRequest.getDataSink()); - inspectEntryRequest.done(); - } - - private static long outputInputJarEntryLfhRecordPreservingDataAlignment( - DataSource inputLfhSection, - LocalFileRecord inputRecord, - DataSink outputLfhSection, - long outputOffset) throws IOException { - long inputOffset = inputRecord.getStartOffsetInArchive(); - if (inputOffset == outputOffset) { - // This record's data will be aligned same as in the input APK. - return inputRecord.outputRecord(inputLfhSection, outputLfhSection); - } - int dataAlignmentMultiple = getInputJarEntryDataAlignmentMultiple(inputRecord); - if ((dataAlignmentMultiple <= 1) - || ((inputOffset % dataAlignmentMultiple) - == (outputOffset % dataAlignmentMultiple))) { - // This record's data will be aligned same as in the input APK. - return inputRecord.outputRecord(inputLfhSection, outputLfhSection); - } - - long inputDataStartOffset = inputOffset + inputRecord.getDataStartOffsetInRecord(); - if ((inputDataStartOffset % dataAlignmentMultiple) != 0) { - // This record's data is not aligned in the input APK. No need to align it in the - // output. - return inputRecord.outputRecord(inputLfhSection, outputLfhSection); - } - - // This record's data needs to be re-aligned in the output. This is achieved using the - // record's extra field. - ByteBuffer aligningExtra = - createExtraFieldToAlignData( - inputRecord.getExtra(), - outputOffset + inputRecord.getExtraFieldStartOffsetInsideRecord(), - dataAlignmentMultiple); - return inputRecord.outputRecordWithModifiedExtra( - inputLfhSection, aligningExtra, outputLfhSection); - } - - private static int getInputJarEntryDataAlignmentMultiple(LocalFileRecord entry) { - if (entry.isDataCompressed()) { - // Compressed entries don't need to be aligned - return 1; - } - - // Attempt to obtain the alignment multiple from the entry's extra field. - ByteBuffer extra = entry.getExtra(); - if (extra.hasRemaining()) { - extra.order(ByteOrder.LITTLE_ENDIAN); - // FORMAT: sequence of fields. Each field consists of: - // * uint16 ID - // * uint16 size - // * 'size' bytes: payload - while (extra.remaining() >= 4) { - short headerId = extra.getShort(); - int dataSize = ZipUtils.getUnsignedInt16(extra); - if (dataSize > extra.remaining()) { - // Malformed field -- insufficient input remaining - break; - } - if (headerId != ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) { - // Skip this field - extra.position(extra.position() + dataSize); - continue; - } - // This is APK alignment field. - // FORMAT: - // * uint16 alignment multiple (in bytes) - // * remaining bytes -- padding to achieve alignment of data which starts after - // the extra field - if (dataSize < 2) { - // Malformed - break; - } - return ZipUtils.getUnsignedInt16(extra); - } - } - - // Fall back to filename-based defaults - return (entry.getName().endsWith(".so")) ? 4096 : 4; - } - - private static ByteBuffer createExtraFieldToAlignData( - ByteBuffer original, - long extraStartOffset, - int dataAlignmentMultiple) { - if (dataAlignmentMultiple <= 1) { - return original; - } - - // In the worst case scenario, we'll increase the output size by 6 + dataAlignment - 1. - ByteBuffer result = ByteBuffer.allocate(original.remaining() + 5 + dataAlignmentMultiple); - result.order(ByteOrder.LITTLE_ENDIAN); - - // Step 1. Output all extra fields other than the one which is to do with alignment - // FORMAT: sequence of fields. Each field consists of: - // * uint16 ID - // * uint16 size - // * 'size' bytes: payload - while (original.remaining() >= 4) { - short headerId = original.getShort(); - int dataSize = ZipUtils.getUnsignedInt16(original); - if (dataSize > original.remaining()) { - // Malformed field -- insufficient input remaining - break; - } - if (((headerId == 0) && (dataSize == 0)) - || (headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID)) { - // Ignore the field if it has to do with the old APK data alignment method (filling - // the extra field with 0x00 bytes) or the new APK data alignment method. - original.position(original.position() + dataSize); - continue; - } - // Copy this field (including header) to the output - original.position(original.position() - 4); - int originalLimit = original.limit(); - original.limit(original.position() + 4 + dataSize); - result.put(original); - original.limit(originalLimit); - } - - // Step 2. Add alignment field - // FORMAT: - // * uint16 extra header ID - // * uint16 extra data size - // Payload ('data size' bytes) - // * uint16 alignment multiple (in bytes) - // * remaining bytes -- padding to achieve alignment of data which starts after the - // extra field - long dataMinStartOffset = - extraStartOffset + result.position() - + ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES; - int paddingSizeBytes = - (dataAlignmentMultiple - ((int) (dataMinStartOffset % dataAlignmentMultiple))) - % dataAlignmentMultiple; - result.putShort(ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); - ZipUtils.putUnsignedInt16(result, 2 + paddingSizeBytes); - ZipUtils.putUnsignedInt16(result, dataAlignmentMultiple); - result.position(result.position() + paddingSizeBytes); - result.flip(); - - return result; - } - - private static ByteBuffer getZipCentralDirectory( - DataSource apk, - ApkUtils.ZipSections apkSections) throws IOException, ZipFormatException { - long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes(); - if (cdSizeBytes > Integer.MAX_VALUE) { - throw new ZipFormatException("ZIP Central Directory too large: " + cdSizeBytes); - } - long cdOffset = apkSections.getZipCentralDirectoryOffset(); - ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes); - cd.order(ByteOrder.LITTLE_ENDIAN); - return cd; - } - - private static List parseZipCentralDirectory( - ByteBuffer cd, - ApkUtils.ZipSections apkSections) throws ZipFormatException { - long cdOffset = apkSections.getZipCentralDirectoryOffset(); - int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount(); - List cdRecords = new ArrayList<>(expectedCdRecordCount); - Set entryNames = new HashSet<>(expectedCdRecordCount); - for (int i = 0; i < expectedCdRecordCount; i++) { - CentralDirectoryRecord cdRecord; - int offsetInsideCd = cd.position(); - try { - cdRecord = CentralDirectoryRecord.getRecord(cd); - } catch (ZipFormatException e) { - throw new ZipFormatException( - "Failed to parse ZIP Central Directory record #" + (i + 1) - + " at file offset " + (cdOffset + offsetInsideCd), - e); - } - String entryName = cdRecord.getName(); - if (!entryNames.add(entryName)) { - throw new ZipFormatException( - "Malformed APK: multiple JAR entries with the same name: " + entryName); - } - cdRecords.add(cdRecord); - } - if (cd.hasRemaining()) { - throw new ZipFormatException( - "Unused space at the end of ZIP Central Directory: " + cd.remaining() - + " bytes starting at file offset " + (cdOffset + cd.position())); - } - - return cdRecords; - } - - /** - * Builder of {@link ApkSigner} instances. - * - *

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

- */ - public static class Builder { - private final ApkSignerEngine mSignerEngine; - - private File mInputApkFile; - private DataSource mInputApkDataSource; - - private File mOutputApkFile; - private DataSink mOutputApkDataSink; - private DataSource mOutputApkDataSource; - - /** - * Constructs a new {@code Builder} which will make {@code ApkSigner} use the provided - * signing engine. - */ - public Builder(ApkSignerEngine signerEngine) { - mSignerEngine = signerEngine; - } - - /** - * Sets the APK to be signed. - * - * @see #setInputApk(DataSource) - */ - public Builder setInputApk(File inputApk) { - if (inputApk == null) { - throw new NullPointerException("inputApk == null"); - } - mInputApkFile = inputApk; - mInputApkDataSource = null; - return this; - } - - /** - * Sets the APK to be signed. - * - * @see #setInputApk(File) - */ - public Builder setInputApk(DataSource inputApk) { - if (inputApk == null) { - throw new NullPointerException("inputApk == null"); - } - mInputApkDataSource = inputApk; - mInputApkFile = null; - return this; - } - - /** - * Sets the location of the output (signed) APK. {@code ApkSigner} will create this file if - * it doesn't exist. - * - * @see #setOutputApk(DataSink, DataSource) - */ - public Builder setOutputApk(File outputApk) { - if (outputApk == null) { - throw new NullPointerException("outputApk == null"); - } - mOutputApkFile = outputApk; - mOutputApkDataSink = null; - mOutputApkDataSource = null; - return this; - } - - /** - * Sets the sink which will receive the output (signed) APK. Data received by the - * {@code outputApkOut} sink must be visible through the {@code outputApkIn} data source. - * - * @see #setOutputApk(File) - */ - public Builder setOutputApk(DataSink outputApkOut, DataSource outputApkIn) { - if (outputApkOut == null) { - throw new NullPointerException("outputApkOut == null"); - } - if (outputApkIn == null) { - throw new NullPointerException("outputApkIn == null"); - } - mOutputApkFile = null; - mOutputApkDataSink = outputApkOut; - mOutputApkDataSource = outputApkIn; - return this; - } - - /** - * Returns a new {@code ApkSigner} instance initialized according to the configuration of - * this builder. - */ - public ApkSigner build() { - return new ApkSigner( - mSignerEngine, - mInputApkFile, - mInputApkDataSource, - mOutputApkFile, - mOutputApkDataSink, - mOutputApkDataSource); - } - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/ApkSignerEngine.java b/tools/apksigner/core/src/com/android/apksigner/core/ApkSignerEngine.java deleted file mode 100644 index 21c270640..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/ApkSignerEngine.java +++ /dev/null @@ -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. - * - *

Operating Model

- * - * The abstract operating model is that there is an input APK which is being signed, thus producing - * an output APK. In reality, there may be just an output APK being built from scratch, or the input - * APK and the output APK may be the same file. Because this engine does not deal with reading and - * writing files, it can handle all of these scenarios. - * - *

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 - * Incremental Operation. - * - *

In the engine's operating model, a signed APK is produced as follows. - *

    - *
  1. JAR entries to be signed are output,
  2. - *
  3. JAR archive is signed using JAR signing, thus adding the so-called v1 signature to the - * output,
  4. - *
  5. JAR archive is signed using APK Signature Scheme v2, thus adding the so-called v2 signature - * to the output.
  6. - *
- * - *

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

To use the engine to sign an input APK (or a collection of JAR entries), follow these - * steps: - *

    - *
  1. Obtain a new instance of the engine -- engine instances are stateful and thus cannot be used - * for signing multiple APKs.
  2. - *
  3. Locate the input APK's APK Signing Block and provide it to - * {@link #inputApkSigningBlock(DataSource)}.
  4. - *
  5. 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.
  6. - *
  7. For each output JAR entry, invoke {@link #outputJarEntry(String)} which may request to - * inspect the entry.
  8. - *
  9. 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.
  10. - *
  11. 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.
  12. - *
  13. Invoke {@link #outputDone()} to signal that the APK was output in full. The engine will - * confirm that the output APK is signed.
  14. - *
  15. Invoke {@link #close()} to signal that the engine will no longer be used. This lets the - * engine free any resources it no longer needs. - *
- * - *

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

Incremental Operation

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

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

Output-only Operation

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

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

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

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

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

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

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

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

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

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

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

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(); - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java b/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java deleted file mode 100644 index f12b47f97..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java +++ /dev/null @@ -1,1233 +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.apk.ApkUtils; -import com.android.apksigner.core.internal.apk.v1.V1SchemeVerifier; -import com.android.apksigner.core.internal.apk.v2.ContentDigestAlgorithm; -import com.android.apksigner.core.internal.apk.v2.SignatureAlgorithm; -import com.android.apksigner.core.internal.apk.v2.V2SchemeVerifier; -import com.android.apksigner.core.internal.util.AndroidSdkVersion; -import com.android.apksigner.core.util.DataSource; -import com.android.apksigner.core.util.DataSources; -import com.android.apksigner.core.zip.ZipFormatException; - -import java.io.Closeable; -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * APK signature verifier which mimics the behavior of the Android platform. - * - *

The verifier is designed to closely mimic the behavior of Android platforms. This is to enable - * the verifier to be used for checking whether an APK's signatures will verify on Android. - * - *

Use {@link Builder} to obtain instances of this verifier. - */ -public class ApkVerifier { - - private static final int APK_SIGNATURE_SCHEME_V2_ID = 2; - private static final Map SUPPORTED_APK_SIG_SCHEME_NAMES = - Collections.singletonMap(APK_SIGNATURE_SCHEME_V2_ID, "APK Signature Scheme v2"); - - private final File mApkFile; - private final DataSource mApkDataSource; - - private final int mMinSdkVersion; - private final int mMaxSdkVersion; - - private ApkVerifier( - File apkFile, - DataSource apkDataSource, - int minSdkVersion, - int maxSdkVersion) { - mApkFile = apkFile; - mApkDataSource = apkDataSource; - mMinSdkVersion = minSdkVersion; - mMaxSdkVersion = maxSdkVersion; - } - - /** - * Verifies the APK's signatures and returns the result of verification. The APK can be - * considered verified iff the result's {@link Result#isVerified()} returns {@code true}. - * The verification result also includes errors, warnings, and information about signers. - * - * @throws IOException if an I/O error is encountered while reading the APK - * @throws ZipFormatException if the APK is malformed at ZIP format level - * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a - * required cryptographic algorithm implementation is missing - * @throws IllegalStateException if this verifier's configuration is missing required - * information. - */ - public Result verify() throws IOException, ZipFormatException, NoSuchAlgorithmException, - IllegalStateException { - Closeable in = null; - try { - DataSource apk; - if (mApkDataSource != null) { - apk = mApkDataSource; - } else if (mApkFile != null) { - RandomAccessFile f = new RandomAccessFile(mApkFile, "r"); - in = f; - apk = DataSources.asDataSource(f, 0, f.length()); - } else { - throw new IllegalStateException("APK not provided"); - } - return verify(apk, mMinSdkVersion, mMaxSdkVersion); - } finally { - if (in != null) { - in.close(); - } - } - } - - /** - * Verifies the APK's signatures and returns the result of verification. The APK can be - * considered verified iff the result's {@link Result#isVerified()} returns {@code true}. - * The verification result also includes errors, warnings, and information about signers. - * - * @param apk APK file contents - * @param minSdkVersion API Level of the oldest Android platform on which the APK's signatures - * may need to be verified - * @param maxSdkVersion API Level of the newest Android platform on which the APK's signatures - * may need to be verified - * - * @throws IOException if an I/O error is encountered while reading the APK - * @throws ZipFormatException if the APK is malformed at ZIP format level - * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a - * required cryptographic algorithm implementation is missing - */ - private static Result verify(DataSource apk, int minSdkVersion, int maxSdkVersion) - throws IOException, ZipFormatException, NoSuchAlgorithmException { - if (minSdkVersion < 0) { - throw new IllegalArgumentException( - "minSdkVersion must not be negative: " + minSdkVersion); - } - if (minSdkVersion > maxSdkVersion) { - throw new IllegalArgumentException( - "minSdkVersion (" + minSdkVersion + ") > maxSdkVersion (" + maxSdkVersion - + ")"); - } - ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk); - - Result result = new Result(); - - // Android N and newer attempts to verify APK Signature Scheme v2 signature of the APK. - // If the signature is not found, it falls back to JAR signature verification. If the - // signature is found but does not verify, the APK is rejected. - Set foundApkSigSchemeIds; - if (maxSdkVersion >= AndroidSdkVersion.N) { - foundApkSigSchemeIds = new HashSet<>(1); - try { - V2SchemeVerifier.Result v2Result = V2SchemeVerifier.verify(apk, zipSections); - foundApkSigSchemeIds.add(APK_SIGNATURE_SCHEME_V2_ID); - result.mergeFrom(v2Result); - } catch (V2SchemeVerifier.SignatureNotFoundException ignored) {} - if (result.containsErrors()) { - return result; - } - } else { - foundApkSigSchemeIds = Collections.emptySet(); - } - - // Attempt to verify the APK using JAR signing if necessary. Platforms prior to Android N - // ignore APK Signature Scheme v2 signatures and always attempt to verify JAR signatures. - // Android N onwards verifies JAR signatures only if no APK Signature Scheme v2 (or newer - // scheme) signatures were found. - if ((minSdkVersion < AndroidSdkVersion.N) || (foundApkSigSchemeIds.isEmpty())) { - V1SchemeVerifier.Result v1Result = - V1SchemeVerifier.verify( - apk, - zipSections, - SUPPORTED_APK_SIG_SCHEME_NAMES, - foundApkSigSchemeIds, - minSdkVersion, - maxSdkVersion); - result.mergeFrom(v1Result); - } - if (result.containsErrors()) { - return result; - } - - // Check whether v1 and v2 scheme signer identifies match, provided both v1 and v2 - // signatures verified. - if ((result.isVerifiedUsingV1Scheme()) && (result.isVerifiedUsingV2Scheme())) { - ArrayList v1Signers = - new ArrayList<>(result.getV1SchemeSigners()); - ArrayList v2Signers = - new ArrayList<>(result.getV2SchemeSigners()); - ArrayList v1SignerCerts = new ArrayList<>(); - ArrayList v2SignerCerts = new ArrayList<>(); - for (Result.V1SchemeSignerInfo signer : v1Signers) { - try { - v1SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded())); - } catch (CertificateEncodingException e) { - throw new RuntimeException( - "Failed to encode JAR signer " + signer.getName() + " certs", e); - } - } - for (Result.V2SchemeSignerInfo signer : v2Signers) { - try { - v2SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded())); - } catch (CertificateEncodingException e) { - throw new RuntimeException( - "Failed to encode APK Signature Scheme v2 signer (index: " - + signer.getIndex() + ") certs", - e); - } - } - - for (int i = 0; i < v1SignerCerts.size(); i++) { - ByteArray v1Cert = v1SignerCerts.get(i); - if (!v2SignerCerts.contains(v1Cert)) { - Result.V1SchemeSignerInfo v1Signer = v1Signers.get(i); - v1Signer.addError(Issue.V2_SIG_MISSING); - break; - } - } - for (int i = 0; i < v2SignerCerts.size(); i++) { - ByteArray v2Cert = v2SignerCerts.get(i); - if (!v1SignerCerts.contains(v2Cert)) { - Result.V2SchemeSignerInfo v2Signer = v2Signers.get(i); - v2Signer.addError(Issue.JAR_SIG_MISSING); - break; - } - } - } - if (result.containsErrors()) { - return result; - } - - // Verified - result.setVerified(); - if (result.isVerifiedUsingV2Scheme()) { - for (Result.V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) { - result.addSignerCertificate(signerInfo.getCertificate()); - } - } else if (result.isVerifiedUsingV1Scheme()) { - for (Result.V1SchemeSignerInfo signerInfo : result.getV1SchemeSigners()) { - result.addSignerCertificate(signerInfo.getCertificate()); - } - } else { - throw new RuntimeException( - "APK considered verified, but has not verified using either v1 or v2 schemes"); - } - - return result; - } - - /** - * Result of verifying an APKs signatures. The APK can be considered verified iff - * {@link #isVerified()} returns {@code true}. - */ - public static class Result { - private final List mErrors = new ArrayList<>(); - private final List mWarnings = new ArrayList<>(); - private final List mSignerCerts = new ArrayList<>(); - private final List mV1SchemeSigners = new ArrayList<>(); - private final List mV1SchemeIgnoredSigners = new ArrayList<>(); - private final List mV2SchemeSigners = new ArrayList<>(); - - private boolean mVerified; - private boolean mVerifiedUsingV1Scheme; - private boolean mVerifiedUsingV2Scheme; - - /** - * Returns {@code true} if the APK's signatures verified. - */ - public boolean isVerified() { - return mVerified; - } - - private void setVerified() { - mVerified = true; - } - - /** - * Returns {@code true} if the APK's JAR signatures verified. - */ - public boolean isVerifiedUsingV1Scheme() { - return mVerifiedUsingV1Scheme; - } - - /** - * Returns {@code true} if the APK's APK Signature Scheme v2 signatures verified. - */ - public boolean isVerifiedUsingV2Scheme() { - return mVerifiedUsingV2Scheme; - } - - /** - * Returns the verified signers' certificates, one per signer. - */ - public List getSignerCertificates() { - return mSignerCerts; - } - - private void addSignerCertificate(X509Certificate cert) { - mSignerCerts.add(cert); - } - - /** - * Returns information about JAR signers associated with the APK's signature. These are the - * signers used by Android. - * - * @see #getV1SchemeIgnoredSigners() - */ - public List getV1SchemeSigners() { - return mV1SchemeSigners; - } - - /** - * Returns information about JAR signers ignored by the APK's signature verification - * process. These signers are ignored by Android. However, each signer's errors or warnings - * will contain information about why they are ignored. - * - * @see #getV1SchemeSigners() - */ - public List getV1SchemeIgnoredSigners() { - return mV1SchemeIgnoredSigners; - } - - /** - * Returns information about APK Signature Scheme v2 signers associated with the APK's - * signature. - */ - public List getV2SchemeSigners() { - return mV2SchemeSigners; - } - - /** - * Returns errors encountered while verifying the APK's signatures. - */ - public List getErrors() { - return mErrors; - } - - /** - * Returns warnings encountered while verifying the APK's signatures. - */ - public List getWarnings() { - return mWarnings; - } - - private void mergeFrom(V1SchemeVerifier.Result source) { - mVerifiedUsingV1Scheme = source.verified; - mErrors.addAll(source.getErrors()); - mWarnings.addAll(source.getWarnings()); - for (V1SchemeVerifier.Result.SignerInfo signer : source.signers) { - mV1SchemeSigners.add(new V1SchemeSignerInfo(signer)); - } - for (V1SchemeVerifier.Result.SignerInfo signer : source.ignoredSigners) { - mV1SchemeIgnoredSigners.add(new V1SchemeSignerInfo(signer)); - } - } - - private void mergeFrom(V2SchemeVerifier.Result source) { - mVerifiedUsingV2Scheme = source.verified; - mErrors.addAll(source.getErrors()); - mWarnings.addAll(source.getWarnings()); - for (V2SchemeVerifier.Result.SignerInfo signer : source.signers) { - mV2SchemeSigners.add(new V2SchemeSignerInfo(signer)); - } - } - - /** - * Returns {@code true} if an error was encountered while verifying the APK. Any error - * prevents the APK from being considered verified. - */ - public boolean containsErrors() { - if (!mErrors.isEmpty()) { - return true; - } - if (!mV1SchemeSigners.isEmpty()) { - for (V1SchemeSignerInfo signer : mV1SchemeSigners) { - if (signer.containsErrors()) { - return true; - } - } - } - if (!mV2SchemeSigners.isEmpty()) { - for (V2SchemeSignerInfo signer : mV2SchemeSigners) { - if (signer.containsErrors()) { - return true; - } - } - } - - return false; - } - - /** - * Information about a JAR signer associated with the APK's signature. - */ - public static class V1SchemeSignerInfo { - private final String mName; - private final List mCertChain; - private final String mSignatureBlockFileName; - private final String mSignatureFileName; - - private final List mErrors; - private final List mWarnings; - - private V1SchemeSignerInfo(V1SchemeVerifier.Result.SignerInfo result) { - mName = result.name; - mCertChain = result.certChain; - mSignatureBlockFileName = result.signatureBlockFileName; - mSignatureFileName = result.signatureFileName; - mErrors = result.getErrors(); - mWarnings = result.getWarnings(); - } - - /** - * Returns a user-friendly name of the signer. - */ - public String getName() { - return mName; - } - - /** - * Returns the name of the JAR entry containing this signer's JAR signature block file. - */ - public String getSignatureBlockFileName() { - return mSignatureBlockFileName; - } - - /** - * Returns the name of the JAR entry containing this signer's JAR signature file. - */ - public String getSignatureFileName() { - return mSignatureFileName; - } - - /** - * Returns this signer's signing certificate or {@code null} if not available. The - * certificate is guaranteed to be available if no errors were encountered during - * verification (see {@link #containsErrors()}. - * - *

This certificate contains the signer's public key. - */ - public X509Certificate getCertificate() { - return mCertChain.isEmpty() ? null : mCertChain.get(0); - } - - /** - * Returns the certificate chain for the signer's public key. The certificate containing - * the public key is first, followed by the certificate (if any) which issued the - * signing certificate, and so forth. An empty list may be returned if an error was - * encountered during verification (see {@link #containsErrors()}). - */ - public List getCertificateChain() { - return mCertChain; - } - - /** - * Returns {@code true} if an error was encountered while verifying this signer's JAR - * signature. Any error prevents the signer's signature from being considered verified. - */ - public boolean containsErrors() { - return !mErrors.isEmpty(); - } - - /** - * Returns errors encountered while verifying this signer's JAR signature. Any error - * prevents the signer's signature from being considered verified. - */ - public List getErrors() { - return mErrors; - } - - /** - * Returns warnings encountered while verifying this signer's JAR signature. Warnings - * do not prevent the signer's signature from being considered verified. - */ - public List getWarnings() { - return mWarnings; - } - - private void addError(Issue msg, Object... parameters) { - mErrors.add(new IssueWithParams(msg, parameters)); - } - } - - /** - * Information about an APK Signature Scheme v2 signer associated with the APK's signature. - */ - public static class V2SchemeSignerInfo { - private final int mIndex; - private final List mCerts; - - private final List mErrors; - private final List mWarnings; - - private V2SchemeSignerInfo(V2SchemeVerifier.Result.SignerInfo result) { - mIndex = result.index; - mCerts = result.certs; - mErrors = result.getErrors(); - mWarnings = result.getWarnings(); - } - - /** - * Returns this signer's {@code 0}-based index in the list of signers contained in the - * APK's APK Signature Scheme v2 signature. - */ - public int getIndex() { - return mIndex; - } - - /** - * Returns this signer's signing certificate or {@code null} if not available. The - * certificate is guaranteed to be available if no errors were encountered during - * verification (see {@link #containsErrors()}. - * - *

This certificate contains the signer's public key. - */ - public X509Certificate getCertificate() { - return mCerts.isEmpty() ? null : mCerts.get(0); - } - - /** - * Returns this signer's certificates. The first certificate is for the signer's public - * key. An empty list may be returned if an error was encountered during verification - * (see {@link #containsErrors()}). - */ - public List getCertificates() { - return mCerts; - } - - private void addError(Issue msg, Object... parameters) { - mErrors.add(new IssueWithParams(msg, parameters)); - } - - public boolean containsErrors() { - return !mErrors.isEmpty(); - } - - public List getErrors() { - return mErrors; - } - - public List getWarnings() { - return mWarnings; - } - } - } - - /** - * Error or warning encountered while verifying an APK's signatures. - */ - public static enum Issue { - - /** - * APK is not JAR-signed. - */ - JAR_SIG_NO_SIGNATURES("No JAR signatures"), - - /** - * APK does not contain any entries covered by JAR signatures. - */ - JAR_SIG_NO_SIGNED_ZIP_ENTRIES("No JAR entries covered by JAR signatures"), - - /** - * APK contains multiple entries with the same name. - * - *

- */ - JAR_SIG_DUPLICATE_ZIP_ENTRY("Duplicate entry: %1$s"), - - /** - * JAR manifest contains a section with a duplicate name. - * - * - */ - JAR_SIG_DUPLICATE_MANIFEST_SECTION("Duplicate section in META-INF/MANIFEST.MF: %1$s"), - - /** - * JAR manifest contains a section without a name. - * - * - */ - JAR_SIG_UNNNAMED_MANIFEST_SECTION( - "Malformed META-INF/MANIFEST.MF: invidual section #%1$d does not have a name"), - - /** - * JAR signature file contains a section without a name. - * - * - */ - JAR_SIG_UNNNAMED_SIG_FILE_SECTION( - "Malformed %1$s: invidual section #%2$d does not have a name"), - - /** APK is missing the JAR manifest entry (META-INF/MANIFEST.MF). */ - JAR_SIG_NO_MANIFEST("Missing META-INF/MANIFEST.MF"), - - /** - * JAR manifest references an entry which is not there in the APK. - * - * - */ - JAR_SIG_MISSING_ZIP_ENTRY_REFERENCED_IN_MANIFEST( - "%1$s entry referenced by META-INF/MANIFEST.MF not found in the APK"), - - /** - * JAR manifest does not list a digest for the specified entry. - * - * - */ - JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST("No digest for %1$s in META-INF/MANIFEST.MF"), - - /** - * JAR signature does not list a digest for the specified entry. - * - * - */ - JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE("No digest for %1$s in %2$s"), - - /** - * The specified JAR entry is not covered by JAR signature. - * - * - */ - JAR_SIG_ZIP_ENTRY_NOT_SIGNED("%1$s entry not signed"), - - /** - * JAR signature uses different set of signers to protect the two specified ZIP entries. - * - * - */ - JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH( - "Entries %1$s and %3$s are signed with different sets of signers" - + " : <%2$s> vs <%4$s>"), - - /** - * Digest of the specified ZIP entry's data does not match the digest expected by the JAR - * signature. - * - * - */ - JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY( - "%2$s digest of %1$s does not match the digest specified in %3$s" - + ". Expected: <%5$s>, actual: <%4$s>"), - - /** - * Digest of the JAR manifest main section did not verify. - * - * - */ - JAR_SIG_MANIFEST_MAIN_SECTION_DIGEST_DID_NOT_VERIFY( - "%1$s digest of META-INF/MANIFEST.MF main section does not match the digest" - + " specified in %2$s. Expected: <%4$s>, actual: <%3$s>"), - - /** - * Digest of the specified JAR manifest section does not match the digest expected by the - * JAR signature. - * - * - */ - JAR_SIG_MANIFEST_SECTION_DIGEST_DID_NOT_VERIFY( - "%2$s digest of META-INF/MANIFEST.MF section for %1$s does not match the digest" - + " specified in %3$s. Expected: <%5$s>, actual: <%4$s>"), - - /** - * JAR signature file does not contain the whole-file digest of the JAR manifest file. The - * digest speeds up verification of JAR signature. - * - * - */ - JAR_SIG_NO_MANIFEST_DIGEST_IN_SIG_FILE( - "%1$s does not specify digest of META-INF/MANIFEST.MF" - + ". This slows down verification."), - - /** - * APK is signed using APK Signature Scheme v2 or newer, but JAR signature file does not - * contain protections against stripping of these newer scheme signatures. - * - * - */ - JAR_SIG_NO_APK_SIG_STRIP_PROTECTION( - "APK is signed using APK Signature Scheme v2 but these signatures may be stripped" - + " without being detected because %1$s does not contain anti-stripping" - + " protections."), - - /** - * JAR signature of the signer is missing a file/entry. - * - * - */ - JAR_SIG_MISSING_FILE("Partial JAR signature. Found: %1$s, missing: %2$s"), - - /** - * An exception was encountered while verifying JAR signature contained in a signature block - * against the signature file. - * - * - */ - JAR_SIG_VERIFY_EXCEPTION("Failed to verify JAR signature %1$s against %2$s: %3$s"), - - /** - * JAR signature contains unsupported digest algorithm. - * - * - */ - JAR_SIG_UNSUPPORTED_SIG_ALG( - "JAR signature %1$s uses digest algorithm %2$s and signature algorithm %3$s which" - + " is not supported on API Levels %4$s"), - - /** - * An exception was encountered while parsing JAR signature contained in a signature block. - * - * - */ - JAR_SIG_PARSE_EXCEPTION("Failed to parse JAR signature %1$s: %2$s"), - - /** - * An exception was encountered while parsing a certificate contained in the JAR signature - * block. - * - * - */ - JAR_SIG_MALFORMED_CERTIFICATE("Malformed certificate in JAR signature %1$s: %2$s"), - - /** - * JAR signature contained in a signature block file did not verify against the signature - * file. - * - * - */ - JAR_SIG_DID_NOT_VERIFY("JAR signature %1$s did not verify against %2$s"), - - /** - * JAR signature contains no verified signers. - * - * - */ - JAR_SIG_NO_SIGNERS("JAR signature %1$s contains no signers"), - - /** - * JAR signature file contains a section with a duplicate name. - * - * - */ - JAR_SIG_DUPLICATE_SIG_FILE_SECTION("Duplicate section in %1$s: %2$s"), - - /** - * JAR signature file's main section doesn't contain the mandatory Signature-Version - * attribute. - * - * - */ - JAR_SIG_MISSING_VERSION_ATTR_IN_SIG_FILE( - "Malformed %1$s: missing Signature-Version attribute"), - - /** - * JAR signature file references an unknown APK signature scheme ID. - * - * - */ - JAR_SIG_UNKNOWN_APK_SIG_SCHEME_ID( - "JAR signature %1$s references unknown APK signature scheme ID: %2$d"), - - /** - * JAR signature file indicates that the APK is supposed to be signed with a supported APK - * signature scheme (in addition to the JAR signature) but no such signature was found in - * the APK. - * - * - */ - JAR_SIG_MISSING_APK_SIG_REFERENCED( - "JAR signature %1$s indicates the APK is signed using %3$s but no such signature" - + " was found. Signature stripped?"), - - /** - * JAR entry is not covered by signature and thus unauthorized modifications to its contents - * will not be detected. - * - * - */ - JAR_SIG_UNPROTECTED_ZIP_ENTRY( - "%1$s not protected by signature. Unauthorized modifications to this JAR entry" - + " will not be detected. Delete or move the entry outside of META-INF/."), - - /** - * APK which is both JAR-signed and signed using APK Signature Scheme v2 contains an APK - * Signature Scheme v2 signature from this signer, but does not contain a JAR signature - * from this signer. - */ - JAR_SIG_MISSING( - "No APK Signature Scheme v2 signature from this signer despite APK being v2" - + " signed"), - - /** - * APK which is both JAR-signed and signed using APK Signature Scheme v2 contains a JAR - * signature from this signer, but does not contain an APK Signature Scheme v2 signature - * from this signer. - */ - V2_SIG_MISSING( - "No APK Signature Scheme v2 signature from this signer despite APK being v2" - + " signed"), - - /** - * Failed to parse the list of signers contained in the APK Signature Scheme v2 signature. - */ - V2_SIG_MALFORMED_SIGNERS("Malformed list of signers"), - - /** - * Failed to parse this signer's signer block contained in the APK Signature Scheme v2 - * signature. - */ - V2_SIG_MALFORMED_SIGNER("Malformed signer block"), - - /** - * Public key embedded in the APK Signature Scheme v2 signature of this signer could not be - * parsed. - * - * - */ - V2_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"), - - /** - * This APK Signature Scheme v2 signer's certificate could not be parsed. - * - * - */ - V2_SIG_MALFORMED_CERTIFICATE("Malformed certificate #%2$d: %3$s"), - - /** - * Failed to parse this signer's signature record contained in the APK Signature Scheme v2 - * signature. - * - * - */ - V2_SIG_MALFORMED_SIGNATURE("Malformed APK Signature Scheme v2 signature record #%1$d"), - - /** - * Failed to parse this signer's digest record contained in the APK Signature Scheme v2 - * signature. - * - * - */ - V2_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v2 digest record #%1$d"), - - /** - * This APK Signature Scheme v2 signer contains a malformed additional attribute. - * - * - */ - V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"), - - /** - * APK Signature Scheme v2 signature contains no signers. - */ - V2_SIG_NO_SIGNERS("No signers in APK Signature Scheme v2 signature"), - - /** - * This APK Signature Scheme v2 signer contains a signature produced using an unknown - * algorithm. - * - * - */ - V2_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"), - - /** - * This APK Signature Scheme v2 signer contains an unknown additional attribute. - * - * - */ - V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE("Unknown additional attribute: ID %1$#x"), - - /** - * An exception was encountered while verifying APK Signature Scheme v2 signature of this - * signer. - * - * - */ - V2_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"), - - /** - * APK Signature Scheme v2 signature over this signer's signed-data block did not verify. - * - * - */ - V2_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"), - - /** - * This APK Signature Scheme v2 signer offers no signatures. - */ - V2_SIG_NO_SIGNATURES("No signatures"), - - /** - * This APK Signature Scheme v2 signer offers signatures but none of them are supported. - */ - V2_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures"), - - /** - * This APK Signature Scheme v2 signer offers no certificates. - */ - V2_SIG_NO_CERTIFICATES("No certificates"), - - /** - * This APK Signature Scheme v2 signer's public key listed in the signer's certificate does - * not match the public key listed in the signatures record. - * - * - */ - V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD( - "Public key mismatch between certificate and signature record: <%1$s> vs <%2$s>"), - - /** - * This APK Signature Scheme v2 signer's signature algorithms listed in the signatures - * record do not match the signature algorithms listed in the signatures record. - * - * - */ - V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS( - "Signature algorithms mismatch between signatures and digests records" - + ": %1$s vs %2$s"), - - /** - * The APK's digest does not match the digest contained in the APK Signature Scheme v2 - * signature. - * - * - */ - V2_SIG_APK_DIGEST_DID_NOT_VERIFY( - "APK integrity check failed. %1$s digest mismatch." - + " Expected: <%2$s>, actual: <%3$s>"), - - /** - * APK Signing Block contains an unknown entry. - * - * - */ - APK_SIG_BLOCK_UNKNOWN_ENTRY_ID("APK Signing Block contains unknown entry: ID %1$#x"); - - private final String mFormat; - - private Issue(String format) { - mFormat = format; - } - - /** - * Returns the format string suitable for combining the parameters of this issue into a - * readable string. See {@link java.util.Formatter} for format. - */ - private String getFormat() { - return mFormat; - } - } - - /** - * {@link Issue} with associated parameters. {@link #toString()} produces a readable formatted - * form. - */ - public static class IssueWithParams { - private final Issue mIssue; - private final Object[] mParams; - - /** - * Constructs a new {@code IssueWithParams} of the specified type and with provided - * parameters. - */ - public IssueWithParams(Issue issue, Object[] params) { - mIssue = issue; - mParams = params; - } - - /** - * Returns the type of this issue. - */ - public Issue getIssue() { - return mIssue; - } - - /** - * Returns the parameters of this issue. - */ - public Object[] getParams() { - return mParams.clone(); - } - - /** - * Returns a readable form of this issue. - */ - @Override - public String toString() { - return String.format(mIssue.getFormat(), mParams); - } - } - - /** - * Wrapped around {@code byte[]} which ensures that {@code equals} and {@code hashCode} operate - * on the contents of the arrays rather than on references. - */ - private static class ByteArray { - private final byte[] mArray; - private final int mHashCode; - - private ByteArray(byte[] arr) { - mArray = arr; - mHashCode = Arrays.hashCode(mArray); - } - - @Override - public int hashCode() { - return mHashCode; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - ByteArray other = (ByteArray) obj; - if (hashCode() != other.hashCode()) { - return false; - } - if (!Arrays.equals(mArray, other.mArray)) { - return false; - } - return true; - } - } - - /** - * Builder of {@link ApkVerifier} instances. - * - *

Although not required, it is best to provide the SDK version (API Level) of the oldest - * Android platform on which the APK is supposed to be installed -- see - * {@link #setMinCheckedPlatformVersion(int)}. Without this information, APKs which use security - * features not supported on ancient Android platforms (e.g., SHA-256 digests or ECDSA - * signatures) will not verify. - */ - public static class Builder { - private final File mApkFile; - private final DataSource mApkDataSource; - - private int mMinSdkVersion = 1; - private int mMaxSdkVersion = Integer.MAX_VALUE; - - /** - * Constructs a new {@code Builder} for verifying the provided APK file. - */ - public Builder(File apk) { - if (apk == null) { - throw new NullPointerException("apk == null"); - } - mApkFile = apk; - mApkDataSource = null; - } - - /** - * Constructs a new {@code Builder} for verifying the provided APK. - */ - public Builder(DataSource apk) { - if (apk == null) { - throw new NullPointerException("apk == null"); - } - mApkDataSource = apk; - mApkFile = null; - } - - /** - * Sets the oldest Android platform version for which the APK is verified. APK verification - * will confirm that the APK is expected to install successfully on all known Android - * platforms starting from the platform version with the provided API Level. - * - *

By default, the APK is checked for all platform versions. Thus, APKs which use - * security features not supported on ancient Android platforms (e.g., SHA-256 digests or - * ECDSA signatures) will not verify by default. - * - * @param minSdkVersion API Level of the oldest platform for which to verify the APK - * - * @see #setCheckedPlatformVersions(int, int) - */ - public Builder setMinCheckedPlatformVersion(int minSdkVersion) { - mMinSdkVersion = minSdkVersion; - mMaxSdkVersion = Integer.MAX_VALUE; - return this; - } - - /** - * Sets the range of Android platform versions for which the APK is verified. APK - * verification will confirm that the APK is expected to install successfully on Android - * platforms whose API Levels fall into this inclusive range. - * - *

By default, the APK is checked for all platform versions. Thus, APKs which use - * security features not supported on ancient Android platforms (e.g., SHA-256 digests or - * ECDSA signatures) will not verify by default. - * - * @param minSdkVersion API Level of the oldest platform for which to verify the APK - * @param maxSdkVersion API Level of the newest platform for which to verify the APK - * - * @see #setMinCheckedPlatformVersion(int) - */ - public Builder setCheckedPlatformVersions(int minSdkVersion, int maxSdkVersion) { - mMinSdkVersion = minSdkVersion; - mMaxSdkVersion = maxSdkVersion; - return this; - } - - /** - * Returns an {@link ApkVerifier} initialized according to the configuration of this - * builder. - */ - public ApkVerifier build() { - return new ApkVerifier( - mApkFile, - mApkDataSource, - mMinSdkVersion, - mMaxSdkVersion); - } - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/DefaultApkSignerEngine.java b/tools/apksigner/core/src/com/android/apksigner/core/DefaultApkSignerEngine.java deleted file mode 100644 index 75b0b2042..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/DefaultApkSignerEngine.java +++ /dev/null @@ -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}. - * - *

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 mV1SignerConfigs; - private final DigestAlgorithm mV1ContentDigestAlgorithm; - private final List 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 mSignatureExpectedOutputJarEntryNames; - - /** Requests for digests of output JAR entries. */ - private final Map mOutputJarEntryDigestRequests = - new HashMap<>(); - - /** Digests of output JAR entries. */ - private final Map mOutputJarEntryDigests = new HashMap<>(); - - /** Data of JAR entries emitted by this engine as v1 signature. */ - private final Map mEmittedSignatureJarEntryData = new HashMap<>(); - - /** Requests for data of output JAR entries which comprise the v1 signature. */ - private final Map 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 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 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 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> 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 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 sigEntries = - new ArrayList<>(signatureZipEntries.size()); - for (Pair 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 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 mAdditionalJarEntries; - private volatile boolean mDone; - - private OutputJarSignatureRequestImpl(List additionalZipEntries) { - mAdditionalJarEntries = - Collections.unmodifiableList(new ArrayList<>(additionalZipEntries)); - } - - @Override - public List 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. - * - *

Use {@link Builder} to obtain configuration instances. - */ - public static class SignerConfig { - private final String mName; - private final PrivateKey mPrivateKey; - private final List mCertificates; - - private SignerConfig( - String name, - PrivateKey privateKey, - List 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 getCertificates() { - return mCertificates; - } - - /** - * Builder of {@link SignerConfig} instances. - */ - public static class Builder { - private final String mName; - private final PrivateKey mPrivateKey; - private final List 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 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 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 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). - * - *

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). - * - *

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

By default, signatures of other signers are omitted from the output APK. - */ - public Builder setOtherSignersSignaturesPreserved(boolean preserved) { - mOtherSignersSignaturesPreserved = preserved; - return this; - } - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/apk/ApkUtils.java b/tools/apksigner/core/src/com/android/apksigner/core/apk/ApkUtils.java deleted file mode 100644 index 8cc8c90d2..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/apk/ApkUtils.java +++ /dev/null @@ -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 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); - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/DigestAlgorithm.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/DigestAlgorithm.java deleted file mode 100644 index 71e698b99..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/DigestAlgorithm.java +++ /dev/null @@ -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; - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java deleted file mode 100644 index f124d1697..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java +++ /dev/null @@ -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 Signed JAR File - */ -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 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> sign( - List signerConfigs, - DigestAlgorithm jarEntryDigestAlgorithm, - Map jarEntryDigests, - List 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> signManifest( - List signerConfigs, - DigestAlgorithm digestAlgorithm, - List 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> 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 getOutputEntryNames(List signerConfigs) { - Set 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 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 sortedEntryNames = new ArrayList<>(jarEntryDigests.keySet()); - Collections.sort(sortedEntryNames); - SortedMap 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 individualSectionsContents; - public Attributes mainSectionAttributes; - } - - private static byte[] generateSignatureFile( - List 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 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 signerCerts = signerConfig.certificates; - X509Certificate signerCert = signerCerts.get(0); - PublicKey signerPublicKey = signerCert.getPublicKey(); - DigestAlgorithm digestAlgorithm = signerConfig.signatureDigestAlgorithm; - Pair 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 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); - } - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeVerifier.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeVerifier.java deleted file mode 100644 index 752ba7e02..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeVerifier.java +++ /dev/null @@ -1,1559 +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.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -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.Base64; -import java.util.Base64.Decoder; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.StringTokenizer; -import java.util.jar.Attributes; - -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.jar.ManifestParser; -import com.android.apksigner.core.internal.util.AndroidSdkVersion; -import com.android.apksigner.core.internal.util.InclusiveIntRange; -import com.android.apksigner.core.internal.util.MessageDigestSink; -import com.android.apksigner.core.internal.zip.CentralDirectoryRecord; -import com.android.apksigner.core.internal.zip.LocalFileRecord; -import com.android.apksigner.core.util.DataSource; -import com.android.apksigner.core.zip.ZipFormatException; - -import sun.security.pkcs.PKCS7; -import sun.security.pkcs.SignerInfo; - -/** - * APK verifier which uses JAR signing (aka v1 signing scheme). - * - * @see Signed JAR File - */ -public abstract class V1SchemeVerifier { - - private static final String MANIFEST_ENTRY_NAME = V1SchemeSigner.MANIFEST_ENTRY_NAME; - - private V1SchemeVerifier() {} - - /** - * Verifies the provided APK's JAR 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 ZipFormatException if the APK is malformed - * @throws IOException if an I/O error occurs when reading the APK - * @throws NoSuchAlgorithmException if the APK's JAR signatures cannot be verified because a - * required cryptographic algorithm implementation is missing - */ - public static Result verify( - DataSource apk, - ApkUtils.ZipSections apkSections, - Map supportedApkSigSchemeNames, - Set foundApkSigSchemeIds, - int minSdkVersion, - int maxSdkVersion) throws IOException, ZipFormatException, NoSuchAlgorithmException { - if (minSdkVersion > maxSdkVersion) { - throw new IllegalArgumentException( - "minSdkVersion (" + minSdkVersion + ") > maxSdkVersion (" + maxSdkVersion - + ")"); - } - - Result result = new Result(); - - // Parse the ZIP Central Directory and check that there are no entries with duplicate names. - List cdRecords = parseZipCentralDirectory(apk, apkSections); - Set cdEntryNames = checkForDuplicateEntries(cdRecords, result); - if (result.containsErrors()) { - return result; - } - - // Verify JAR signature(s). - Signers.verify( - apk, - apkSections.getZipCentralDirectoryOffset(), - cdRecords, - cdEntryNames, - supportedApkSigSchemeNames, - foundApkSigSchemeIds, - minSdkVersion, - maxSdkVersion, - result); - - return result; - } - - /** - * Returns the set of entry names and reports any duplicate entry names in the {@code result} - * as errors. - */ - private static Set checkForDuplicateEntries( - List cdRecords, Result result) { - Set cdEntryNames = new HashSet<>(cdRecords.size()); - Set duplicateCdEntryNames = null; - for (CentralDirectoryRecord cdRecord : cdRecords) { - String entryName = cdRecord.getName(); - if (!cdEntryNames.add(entryName)) { - // This is an error. Report this once per duplicate name. - if (duplicateCdEntryNames == null) { - duplicateCdEntryNames = new HashSet<>(); - } - if (duplicateCdEntryNames.add(entryName)) { - result.addError(Issue.JAR_SIG_DUPLICATE_ZIP_ENTRY, entryName); - } - } - } - return cdEntryNames; - } - - /** - * All JAR signers of an APK. - */ - private static class Signers { - - /** - * Verifies JAR signatures of the provided APK and populates the provided result container - * with errors, warnings, and information about signers. The APK is considered verified if - * the {@link Result#verified} is {@code true}. - */ - private static void verify( - DataSource apk, - long cdStartOffset, - List cdRecords, - Set cdEntryNames, - Map supportedApkSigSchemeNames, - Set foundApkSigSchemeIds, - int minSdkVersion, - int maxSdkVersion, - Result result) throws ZipFormatException, IOException, NoSuchAlgorithmException { - - // Find JAR manifest and signature block files. - CentralDirectoryRecord manifestEntry = null; - Map sigFileEntries = new HashMap<>(1); - List sigBlockEntries = new ArrayList<>(1); - for (CentralDirectoryRecord cdRecord : cdRecords) { - String entryName = cdRecord.getName(); - if (!entryName.startsWith("META-INF/")) { - continue; - } - if ((manifestEntry == null) && (MANIFEST_ENTRY_NAME.equals(entryName))) { - manifestEntry = cdRecord; - continue; - } - if (entryName.endsWith(".SF")) { - sigFileEntries.put(entryName, cdRecord); - continue; - } - if ((entryName.endsWith(".RSA")) - || (entryName.endsWith(".DSA")) - || (entryName.endsWith(".EC"))) { - sigBlockEntries.add(cdRecord); - continue; - } - } - if (manifestEntry == null) { - result.addError(Issue.JAR_SIG_NO_MANIFEST); - return; - } - - // Parse the JAR manifest and check that all JAR entries it references exist in the APK. - byte[] manifestBytes = - LocalFileRecord.getUncompressedData(apk, manifestEntry, cdStartOffset); - Map entryNameToManifestSection = null; - ManifestParser manifest = new ManifestParser(manifestBytes); - ManifestParser.Section manifestMainSection = manifest.readSection(); - List manifestIndividualSections = manifest.readAllSections(); - entryNameToManifestSection = new HashMap<>(manifestIndividualSections.size()); - int manifestSectionNumber = 0; - for (ManifestParser.Section manifestSection : manifestIndividualSections) { - manifestSectionNumber++; - String entryName = manifestSection.getName(); - if (entryName == null) { - result.addError(Issue.JAR_SIG_UNNNAMED_MANIFEST_SECTION, manifestSectionNumber); - continue; - } - if (entryNameToManifestSection.put(entryName, manifestSection) != null) { - result.addError(Issue.JAR_SIG_DUPLICATE_MANIFEST_SECTION, entryName); - continue; - } - if (!cdEntryNames.contains(entryName)) { - result.addError( - Issue.JAR_SIG_MISSING_ZIP_ENTRY_REFERENCED_IN_MANIFEST, entryName); - continue; - } - } - if (result.containsErrors()) { - return; - } - // STATE OF AFFAIRS: - // * All JAR entries listed in JAR manifest are present in the APK. - - // Identify signers - List signers = new ArrayList<>(sigBlockEntries.size()); - for (CentralDirectoryRecord sigBlockEntry : sigBlockEntries) { - String sigBlockEntryName = sigBlockEntry.getName(); - int extensionDelimiterIndex = sigBlockEntryName.lastIndexOf('.'); - if (extensionDelimiterIndex == -1) { - throw new RuntimeException( - "Signature block file name does not contain extension: " - + sigBlockEntryName); - } - String sigFileEntryName = - sigBlockEntryName.substring(0, extensionDelimiterIndex) + ".SF"; - CentralDirectoryRecord sigFileEntry = sigFileEntries.get(sigFileEntryName); - if (sigFileEntry == null) { - result.addWarning( - Issue.JAR_SIG_MISSING_FILE, sigBlockEntryName, sigFileEntryName); - continue; - } - String signerName = sigBlockEntryName.substring("META-INF/".length()); - Result.SignerInfo signerInfo = - new Result.SignerInfo( - signerName, sigBlockEntryName, sigFileEntry.getName()); - Signer signer = new Signer(signerName, sigBlockEntry, sigFileEntry, signerInfo); - signers.add(signer); - } - if (signers.isEmpty()) { - result.addError(Issue.JAR_SIG_NO_SIGNATURES); - return; - } - - // Verify each signer's signature block file .(RSA|DSA|EC) against the corresponding - // signature file .SF. Any error encountered for any signer terminates verification, to - // mimic Android's behavior. - for (Signer signer : signers) { - signer.verifySigBlockAgainstSigFile( - apk, cdStartOffset, minSdkVersion, maxSdkVersion); - if (signer.getResult().containsErrors()) { - result.signers.add(signer.getResult()); - } - } - if (result.containsErrors()) { - return; - } - // STATE OF AFFAIRS: - // * All JAR entries listed in JAR manifest are present in the APK. - // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC). - - // Verify each signer's signature file (.SF) against the JAR manifest. - List remainingSigners = new ArrayList<>(signers.size()); - for (Signer signer : signers) { - signer.verifySigFileAgainstManifest( - manifestBytes, - manifestMainSection, - entryNameToManifestSection, - supportedApkSigSchemeNames, - foundApkSigSchemeIds, - minSdkVersion, - maxSdkVersion); - if (signer.isIgnored()) { - result.ignoredSigners.add(signer.getResult()); - } else { - if (signer.getResult().containsErrors()) { - result.signers.add(signer.getResult()); - } else { - remainingSigners.add(signer); - } - } - } - if (result.containsErrors()) { - return; - } - signers = remainingSigners; - if (signers.isEmpty()) { - result.addError(Issue.JAR_SIG_NO_SIGNATURES); - return; - } - // STATE OF AFFAIRS: - // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC). - // * Contents of all JAR manifest sections listed in .SF files verify against .SF files. - // * All JAR entries listed in JAR manifest are present in the APK. - - // Verify data of JAR entries against JAR manifest and .SF files. On Android, an APK's - // JAR entry is considered signed by signers associated with an .SF file iff the entry - // is mentioned in the .SF file and the entry's digest(s) mentioned in the JAR manifest - // match theentry's uncompressed data. Android requires that all such JAR entries are - // signed by the same set of signers. This set may be smaller than the set of signers - // we've identified so far. - Set apkSigners = - verifyJarEntriesAgainstManifestAndSigners( - apk, - cdStartOffset, - cdRecords, - entryNameToManifestSection, - signers, - minSdkVersion, - maxSdkVersion, - result); - if (result.containsErrors()) { - return; - } - // STATE OF AFFAIRS: - // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC). - // * Contents of all JAR manifest sections listed in .SF files verify against .SF files. - // * All JAR entries listed in JAR manifest are present in the APK. - // * All JAR entries present in the APK and supposed to be covered by JAR signature - // (i.e., reside outside of META-INF/) are covered by signatures from the same set - // of signers. - - // Report any JAR entries which aren't covered by signature. - Set signatureEntryNames = new HashSet<>(1 + result.signers.size() * 2); - signatureEntryNames.add(manifestEntry.getName()); - for (Signer signer : apkSigners) { - signatureEntryNames.add(signer.getSignatureBlockEntryName()); - signatureEntryNames.add(signer.getSignatureFileEntryName()); - } - for (CentralDirectoryRecord cdRecord : cdRecords) { - String entryName = cdRecord.getName(); - if ((entryName.startsWith("META-INF/")) - && (!entryName.endsWith("/")) - && (!signatureEntryNames.contains(entryName))) { - result.addWarning(Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY, entryName); - } - } - - // Reflect the sets of used signers and ignored signers in the result. - for (Signer signer : signers) { - if (apkSigners.contains(signer)) { - result.signers.add(signer.getResult()); - } else { - result.ignoredSigners.add(signer.getResult()); - } - } - - result.verified = true; - } - } - - private static class Signer { - private final String mName; - private final Result.SignerInfo mResult; - private final CentralDirectoryRecord mSignatureFileEntry; - private final CentralDirectoryRecord mSignatureBlockEntry; - private boolean mIgnored; - - private byte[] mSigFileBytes; - private Set mSigFileEntryNames; - - private Signer( - String name, - CentralDirectoryRecord sigBlockEntry, - CentralDirectoryRecord sigFileEntry, - Result.SignerInfo result) { - mName = name; - mResult = result; - mSignatureBlockEntry = sigBlockEntry; - mSignatureFileEntry = sigFileEntry; - } - - public String getName() { - return mName; - } - - public String getSignatureFileEntryName() { - return mSignatureFileEntry.getName(); - } - - public String getSignatureBlockEntryName() { - return mSignatureBlockEntry.getName(); - } - - void setIgnored() { - mIgnored = true; - } - - public boolean isIgnored() { - return mIgnored; - } - - public Set getSigFileEntryNames() { - return mSigFileEntryNames; - } - - public Result.SignerInfo getResult() { - return mResult; - } - - @SuppressWarnings("restriction") - public void verifySigBlockAgainstSigFile( - DataSource apk, long cdStartOffset, int minSdkVersion, int maxSdkVersion) - throws IOException, ZipFormatException, NoSuchAlgorithmException { - byte[] sigBlockBytes = - LocalFileRecord.getUncompressedData(apk, mSignatureBlockEntry, cdStartOffset); - mSigFileBytes = - LocalFileRecord.getUncompressedData(apk, mSignatureFileEntry, cdStartOffset); - PKCS7 sigBlock; - try { - sigBlock = new PKCS7(sigBlockBytes); - } catch (IOException e) { - if (e.getCause() instanceof CertificateException) { - mResult.addError( - Issue.JAR_SIG_MALFORMED_CERTIFICATE, mSignatureBlockEntry.getName(), e); - } else { - mResult.addError( - Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e); - } - return; - } - SignerInfo[] unverifiedSignerInfos = sigBlock.getSignerInfos(); - if ((unverifiedSignerInfos == null) || (unverifiedSignerInfos.length == 0)) { - mResult.addError(Issue.JAR_SIG_NO_SIGNERS, mSignatureBlockEntry.getName()); - return; - } - - SignerInfo verifiedSignerInfo = null; - if ((unverifiedSignerInfos != null) && (unverifiedSignerInfos.length > 0)) { - for (int i = 0; i < unverifiedSignerInfos.length; i++) { - SignerInfo unverifiedSignerInfo = unverifiedSignerInfos[i]; - String digestAlgorithmOid = - unverifiedSignerInfo.getDigestAlgorithmId().getOID().toString(); - String signatureAlgorithmOid = - unverifiedSignerInfo - .getDigestEncryptionAlgorithmId().getOID().toString(); - InclusiveIntRange desiredApiLevels = - InclusiveIntRange.fromTo(minSdkVersion, maxSdkVersion); - List apiLevelsWhereDigestAndSigAlgorithmSupported = - getSigAlgSupportedApiLevels(digestAlgorithmOid, signatureAlgorithmOid); - List apiLevelsWhereDigestAlgorithmNotSupported = - desiredApiLevels.getValuesNotIn(apiLevelsWhereDigestAndSigAlgorithmSupported); - if (!apiLevelsWhereDigestAlgorithmNotSupported.isEmpty()) { - mResult.addError( - Issue.JAR_SIG_UNSUPPORTED_SIG_ALG, - mSignatureBlockEntry.getName(), - digestAlgorithmOid, - signatureAlgorithmOid, - String.valueOf(apiLevelsWhereDigestAlgorithmNotSupported)); - return; - } - try { - verifiedSignerInfo = sigBlock.verify(unverifiedSignerInfo, mSigFileBytes); - } catch (SignatureException e) { - mResult.addError( - Issue.JAR_SIG_VERIFY_EXCEPTION, - mSignatureBlockEntry.getName(), - mSignatureFileEntry.getName(), - e); - return; - } - if (verifiedSignerInfo != null) { - // Verified - break; - } - - // Did not verify - if (minSdkVersion < AndroidSdkVersion.N) { - // Prior to N, Android attempted to verify only the first SignerInfo. - mResult.addError( - Issue.JAR_SIG_DID_NOT_VERIFY, - mSignatureBlockEntry.getName(), - mSignatureFileEntry.getName()); - return; - } - } - } - if (verifiedSignerInfo == null) { - mResult.addError(Issue.JAR_SIG_NO_SIGNERS, mSignatureBlockEntry.getName()); - return; - } - - List certChain; - try { - certChain = verifiedSignerInfo.getCertificateChain(sigBlock); - } catch (IOException e) { - throw new RuntimeException( - "Failed to obtain cert chain from " + mSignatureBlockEntry.getName(), e); - } - if ((certChain == null) || (certChain.isEmpty())) { - throw new RuntimeException("Verified SignerInfo does not have a certificate chain"); - } - mResult.certChain.clear(); - mResult.certChain.addAll(certChain); - } - - private static final String OID_DIGEST_MD5 = "1.2.840.113549.2.5"; - private static final String OID_DIGEST_SHA1 = "1.3.14.3.2.26"; - private static final String OID_DIGEST_SHA224 = "2.16.840.1.101.3.4.2.4"; - private static final String OID_DIGEST_SHA256 = "2.16.840.1.101.3.4.2.1"; - private static final String OID_DIGEST_SHA384 = "2.16.840.1.101.3.4.2.2"; - private static final String OID_DIGEST_SHA512 = "2.16.840.1.101.3.4.2.3"; - - private static final String OID_SIG_RSA = "1.2.840.113549.1.1.1"; - private static final String OID_SIG_MD5_WITH_RSA = "1.2.840.113549.1.1.4"; - private static final String OID_SIG_SHA1_WITH_RSA = "1.2.840.113549.1.1.5"; - private static final String OID_SIG_SHA224_WITH_RSA = "1.2.840.113549.1.1.14"; - private static final String OID_SIG_SHA256_WITH_RSA = "1.2.840.113549.1.1.11"; - private static final String OID_SIG_SHA384_WITH_RSA = "1.2.840.113549.1.1.12"; - private static final String OID_SIG_SHA512_WITH_RSA = "1.2.840.113549.1.1.13"; - - private static final String OID_SIG_DSA = "1.2.840.10040.4.1"; - private static final String OID_SIG_SHA1_WITH_DSA = "1.2.840.10040.4.3"; - private static final String OID_SIG_SHA224_WITH_DSA = "2.16.840.1.101.3.4.3.1"; - private static final String OID_SIG_SHA256_WITH_DSA = "2.16.840.1.101.3.4.3.2"; - - private static final String OID_SIG_SHA1_WITH_ECDSA = "1.2.840.10045.4.1"; - private static final String OID_SIG_SHA224_WITH_ECDSA = "1.2.840.10045.4.3.1"; - private static final String OID_SIG_SHA256_WITH_ECDSA = "1.2.840.10045.4.3.2"; - private static final String OID_SIG_SHA384_WITH_ECDSA = "1.2.840.10045.4.3.3"; - private static final String OID_SIG_SHA512_WITH_ECDSA = "1.2.840.10045.4.3.4"; - - private static final Map> SUPPORTED_SIG_ALG_OIDS = - new HashMap<>(); - { - addSupportedSigAlg( - OID_DIGEST_MD5, OID_SIG_RSA, - InclusiveIntRange.from(0)); - addSupportedSigAlg( - OID_DIGEST_MD5, OID_SIG_MD5_WITH_RSA, - InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21)); - addSupportedSigAlg( - OID_DIGEST_MD5, OID_SIG_SHA1_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_MD5, OID_SIG_SHA224_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_MD5, OID_SIG_SHA256_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_MD5, OID_SIG_SHA384_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_MD5, OID_SIG_SHA512_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - - addSupportedSigAlg( - OID_DIGEST_SHA1, OID_SIG_RSA, - InclusiveIntRange.from(0)); - addSupportedSigAlg( - OID_DIGEST_SHA1, OID_SIG_MD5_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_RSA, - InclusiveIntRange.from(0)); - addSupportedSigAlg( - OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - - addSupportedSigAlg( - OID_DIGEST_SHA224, OID_SIG_RSA, - InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21)); - addSupportedSigAlg( - OID_DIGEST_SHA224, OID_SIG_MD5_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_RSA, - InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21)); - addSupportedSigAlg( - OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_RSA, - InclusiveIntRange.fromTo(21, 21)); - addSupportedSigAlg( - OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - - addSupportedSigAlg( - OID_DIGEST_SHA256, OID_SIG_RSA, - InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18)); - addSupportedSigAlg( - OID_DIGEST_SHA256, OID_SIG_MD5_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_RSA, - InclusiveIntRange.fromTo(21, 21)); - addSupportedSigAlg( - OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_RSA, - InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18)); - addSupportedSigAlg( - OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - - addSupportedSigAlg( - OID_DIGEST_SHA384, OID_SIG_RSA, - InclusiveIntRange.from(18)); - addSupportedSigAlg( - OID_DIGEST_SHA384, OID_SIG_MD5_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_RSA, - InclusiveIntRange.from(21)); - addSupportedSigAlg( - OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - - addSupportedSigAlg( - OID_DIGEST_SHA512, OID_SIG_RSA, - InclusiveIntRange.from(18)); - addSupportedSigAlg( - OID_DIGEST_SHA512, OID_SIG_MD5_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_RSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_RSA, - InclusiveIntRange.fromTo(21, 21)); - addSupportedSigAlg( - OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_RSA, - InclusiveIntRange.from(21)); - - addSupportedSigAlg( - OID_DIGEST_MD5, OID_SIG_SHA1_WITH_DSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_MD5, OID_SIG_SHA224_WITH_DSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_MD5, OID_SIG_SHA256_WITH_DSA, - InclusiveIntRange.fromTo(21, 23)); - - addSupportedSigAlg( - OID_DIGEST_SHA1, OID_SIG_DSA, - InclusiveIntRange.from(0)); - addSupportedSigAlg( - OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_DSA, - InclusiveIntRange.from(9)); - addSupportedSigAlg( - OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_DSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_DSA, - InclusiveIntRange.fromTo(21, 23)); - - addSupportedSigAlg( - OID_DIGEST_SHA224, OID_SIG_DSA, - InclusiveIntRange.from(22)); - addSupportedSigAlg( - OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_DSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_DSA, - InclusiveIntRange.from(21)); - addSupportedSigAlg( - OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_DSA, - InclusiveIntRange.fromTo(21, 23)); - - addSupportedSigAlg( - OID_DIGEST_SHA256, OID_SIG_DSA, - InclusiveIntRange.from(22)); - addSupportedSigAlg( - OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_DSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_DSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_DSA, - InclusiveIntRange.from(21)); - - addSupportedSigAlg( - OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_DSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_DSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_DSA, - InclusiveIntRange.fromTo(21, 23)); - - addSupportedSigAlg( - OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_DSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_DSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_DSA, - InclusiveIntRange.fromTo(21, 23)); - - - addSupportedSigAlg( - OID_DIGEST_MD5, OID_SIG_SHA1_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_MD5, OID_SIG_SHA224_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_MD5, OID_SIG_SHA256_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_MD5, OID_SIG_SHA384_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_MD5, OID_SIG_SHA512_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - - addSupportedSigAlg( - OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_ECDSA, - InclusiveIntRange.from(18)); - addSupportedSigAlg( - OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - - addSupportedSigAlg( - OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_ECDSA, - InclusiveIntRange.from(21)); - addSupportedSigAlg( - OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - - addSupportedSigAlg( - OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_ECDSA, - InclusiveIntRange.from(21)); - addSupportedSigAlg( - OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - - addSupportedSigAlg( - OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_ECDSA, - InclusiveIntRange.from(21)); - addSupportedSigAlg( - OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - - addSupportedSigAlg( - OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_ECDSA, - InclusiveIntRange.fromTo(21, 23)); - addSupportedSigAlg( - OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_ECDSA, - InclusiveIntRange.from(21)); - } - - private static void addSupportedSigAlg( - String digestAlgorithmOid, - String signatureAlgorithmOid, - InclusiveIntRange... supportedApiLevels) { - SUPPORTED_SIG_ALG_OIDS.put( - digestAlgorithmOid + "with" + signatureAlgorithmOid, - Arrays.asList(supportedApiLevels)); - } - - private List getSigAlgSupportedApiLevels( - String digestAlgorithmOid, - String signatureAlgorithmOid) { - List result = - SUPPORTED_SIG_ALG_OIDS.get(digestAlgorithmOid + "with" + signatureAlgorithmOid); - return (result != null) ? result : Collections.emptyList(); - } - - public void verifySigFileAgainstManifest( - byte[] manifestBytes, - ManifestParser.Section manifestMainSection, - Map entryNameToManifestSection, - Map supportedApkSigSchemeNames, - Set foundApkSigSchemeIds, - int minSdkVersion, - int maxSdkVersion) throws NoSuchAlgorithmException { - // Inspect the main section of the .SF file. - ManifestParser sf = new ManifestParser(mSigFileBytes); - ManifestParser.Section sfMainSection = sf.readSection(); - if (sfMainSection.getAttributeValue(Attributes.Name.SIGNATURE_VERSION) == null) { - mResult.addError( - Issue.JAR_SIG_MISSING_VERSION_ATTR_IN_SIG_FILE, - mSignatureFileEntry.getName()); - setIgnored(); - return; - } - - if (maxSdkVersion >= AndroidSdkVersion.N) { - // Android N and newer rejects APKs whose .SF file says they were supposed to be - // signed with APK Signature Scheme v2 (or newer) and yet no such signature was - // found. - checkForStrippedApkSignatures( - sfMainSection, supportedApkSigSchemeNames, foundApkSigSchemeIds); - if (mResult.containsErrors()) { - return; - } - } - - boolean createdBySigntool = false; - String createdBy = sfMainSection.getAttributeValue("Created-By"); - if (createdBy != null) { - createdBySigntool = createdBy.indexOf("signtool") != -1; - } - boolean manifestDigestVerified = - verifyManifestDigest( - sfMainSection, - createdBySigntool, - manifestBytes, - minSdkVersion, - maxSdkVersion); - if (!createdBySigntool) { - verifyManifestMainSectionDigest( - sfMainSection, - manifestMainSection, - manifestBytes, - minSdkVersion, - maxSdkVersion); - } - if (mResult.containsErrors()) { - return; - } - - // Inspect per-entry sections of .SF file. Technically, if the digest of JAR manifest - // verifies, per-entry sections should be ignored. However, most Android platform - // implementations require that such sections exist. - List sfSections = sf.readAllSections(); - Set sfEntryNames = new HashSet<>(sfSections.size()); - int sfSectionNumber = 0; - for (ManifestParser.Section sfSection : sfSections) { - sfSectionNumber++; - String entryName = sfSection.getName(); - if (entryName == null) { - mResult.addError( - Issue.JAR_SIG_UNNNAMED_SIG_FILE_SECTION, - mSignatureFileEntry.getName(), - sfSectionNumber); - setIgnored(); - return; - } - if (!sfEntryNames.add(entryName)) { - mResult.addError( - Issue.JAR_SIG_DUPLICATE_SIG_FILE_SECTION, - mSignatureFileEntry.getName(), - entryName); - setIgnored(); - return; - } - if (manifestDigestVerified) { - // No need to verify this entry's corresponding JAR manifest entry because the - // JAR manifest verifies in full. - continue; - } - // Whole-file digest of JAR manifest hasn't been verified. Thus, we need to verify - // the digest of the JAR manifest section corresponding to this .SF section. - ManifestParser.Section manifestSection = entryNameToManifestSection.get(entryName); - if (manifestSection == null) { - mResult.addError( - Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE, - entryName, - mSignatureFileEntry.getName()); - setIgnored(); - continue; - } - verifyManifestIndividualSectionDigest( - sfSection, - createdBySigntool, - manifestSection, - manifestBytes, - minSdkVersion, - maxSdkVersion); - } - mSigFileEntryNames = sfEntryNames; - } - - - /** - * Returns {@code true} if the whole-file digest of the manifest against the main section of - * the .SF file. - */ - private boolean verifyManifestDigest( - ManifestParser.Section sfMainSection, - boolean createdBySigntool, - byte[] manifestBytes, - int minSdkVersion, - int maxSdkVersion) throws NoSuchAlgorithmException { - Collection expectedDigests = - getDigestsToVerify( - sfMainSection, - ((createdBySigntool) ? "-Digest" : "-Digest-Manifest"), - minSdkVersion, - maxSdkVersion); - boolean digestFound = !expectedDigests.isEmpty(); - if (!digestFound) { - mResult.addWarning( - Issue.JAR_SIG_NO_MANIFEST_DIGEST_IN_SIG_FILE, - mSignatureFileEntry.getName()); - return false; - } - - boolean verified = true; - for (NamedDigest expectedDigest : expectedDigests) { - String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm; - byte[] actual = digest(jcaDigestAlgorithm, manifestBytes); - byte[] expected = expectedDigest.digest; - if (!Arrays.equals(expected, actual)) { - mResult.addWarning( - Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY, - V1SchemeSigner.MANIFEST_ENTRY_NAME, - jcaDigestAlgorithm, - mSignatureFileEntry.getName(), - Base64.getEncoder().encodeToString(actual), - Base64.getEncoder().encodeToString(expected)); - verified = false; - } - } - return verified; - } - - /** - * Verifies the digest of the manifest's main section against the main section of the .SF - * file. - */ - private void verifyManifestMainSectionDigest( - ManifestParser.Section sfMainSection, - ManifestParser.Section manifestMainSection, - byte[] manifestBytes, - int minSdkVersion, - int maxSdkVersion) throws NoSuchAlgorithmException { - Collection expectedDigests = - getDigestsToVerify( - sfMainSection, - "-Digest-Manifest-Main-Attributes", - minSdkVersion, - maxSdkVersion); - if (expectedDigests.isEmpty()) { - return; - } - - for (NamedDigest expectedDigest : expectedDigests) { - String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm; - byte[] actual = - digest( - jcaDigestAlgorithm, - manifestBytes, - manifestMainSection.getStartOffset(), - manifestMainSection.getSizeBytes()); - byte[] expected = expectedDigest.digest; - if (!Arrays.equals(expected, actual)) { - mResult.addError( - Issue.JAR_SIG_MANIFEST_MAIN_SECTION_DIGEST_DID_NOT_VERIFY, - jcaDigestAlgorithm, - mSignatureFileEntry.getName(), - Base64.getEncoder().encodeToString(actual), - Base64.getEncoder().encodeToString(expected)); - } - } - } - - /** - * Verifies the digest of the manifest's individual section against the corresponding - * individual section of the .SF file. - */ - private void verifyManifestIndividualSectionDigest( - ManifestParser.Section sfIndividualSection, - boolean createdBySigntool, - ManifestParser.Section manifestIndividualSection, - byte[] manifestBytes, - int minSdkVersion, - int maxSdkVersion) throws NoSuchAlgorithmException { - String entryName = sfIndividualSection.getName(); - Collection expectedDigests = - getDigestsToVerify( - sfIndividualSection, "-Digest", minSdkVersion, maxSdkVersion); - if (expectedDigests.isEmpty()) { - mResult.addError( - Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE, - entryName, - mSignatureFileEntry.getName()); - return; - } - - int sectionStartIndex = manifestIndividualSection.getStartOffset(); - int sectionSizeBytes = manifestIndividualSection.getSizeBytes(); - if (createdBySigntool) { - int sectionEndIndex = sectionStartIndex + sectionSizeBytes; - if ((manifestBytes[sectionEndIndex - 1] == '\n') - && (manifestBytes[sectionEndIndex - 2] == '\n')) { - sectionSizeBytes--; - } - } - for (NamedDigest expectedDigest : expectedDigests) { - String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm; - byte[] actual = - digest( - jcaDigestAlgorithm, - manifestBytes, - sectionStartIndex, - sectionSizeBytes); - byte[] expected = expectedDigest.digest; - if (!Arrays.equals(expected, actual)) { - mResult.addError( - Issue.JAR_SIG_MANIFEST_SECTION_DIGEST_DID_NOT_VERIFY, - entryName, - jcaDigestAlgorithm, - mSignatureFileEntry.getName(), - Base64.getEncoder().encodeToString(actual), - Base64.getEncoder().encodeToString(expected)); - } - } - } - - private void checkForStrippedApkSignatures( - ManifestParser.Section sfMainSection, - Map supportedApkSigSchemeNames, - Set foundApkSigSchemeIds) { - String signedWithApkSchemes = - sfMainSection.getAttributeValue( - V1SchemeSigner.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR); - // This field contains a comma-separated list of APK signature scheme IDs which were - // used to sign this APK. Android rejects APKs where an ID is known to the platform but - // the APK didn't verify using that scheme. - - if (signedWithApkSchemes == null) { - // APK signature (e.g., v2 scheme) stripping protections not enabled. - if (!foundApkSigSchemeIds.isEmpty()) { - // APK is signed with an APK signature scheme such as v2 scheme. - mResult.addWarning( - Issue.JAR_SIG_NO_APK_SIG_STRIP_PROTECTION, - mSignatureFileEntry.getName()); - } - return; - } - - if (supportedApkSigSchemeNames.isEmpty()) { - return; - } - - Set supportedApkSigSchemeIds = supportedApkSigSchemeNames.keySet(); - Set supportedExpectedApkSigSchemeIds = new HashSet<>(1); - StringTokenizer tokenizer = new StringTokenizer(signedWithApkSchemes, ","); - while (tokenizer.hasMoreTokens()) { - String idText = tokenizer.nextToken().trim(); - if (idText.isEmpty()) { - continue; - } - int id; - try { - id = Integer.parseInt(idText); - } catch (Exception ignored) { - continue; - } - // This APK was supposed to be signed with the APK signature scheme having - // this ID. - if (supportedApkSigSchemeIds.contains(id)) { - supportedExpectedApkSigSchemeIds.add(id); - } else { - mResult.addWarning( - Issue.JAR_SIG_UNKNOWN_APK_SIG_SCHEME_ID, - mSignatureFileEntry.getName(), - id); - } - } - - for (int id : supportedExpectedApkSigSchemeIds) { - if (!foundApkSigSchemeIds.contains(id)) { - String apkSigSchemeName = supportedApkSigSchemeNames.get(id); - mResult.addError( - Issue.JAR_SIG_MISSING_APK_SIG_REFERENCED, - mSignatureFileEntry.getName(), - id, - apkSigSchemeName); - } - } - } - } - - private static Collection getDigestsToVerify( - ManifestParser.Section section, - String digestAttrSuffix, - int minSdkVersion, - int maxSdkVersion) { - Decoder base64Decoder = Base64.getDecoder(); - List result = new ArrayList<>(1); - if (minSdkVersion < AndroidSdkVersion.JELLY_BEAN_MR2) { - // Prior to JB MR2, Android platform's logic for picking a digest algorithm to verify is - // to rely on the ancient Digest-Algorithms attribute which contains - // whitespace-separated list of digest algorithms (defaulting to SHA-1) to try. The - // first digest attribute (with supported digest algorithm) found using the list is - // used. - String algs = section.getAttributeValue("Digest-Algorithms"); - if (algs == null) { - algs = "SHA SHA1"; - } - StringTokenizer tokens = new StringTokenizer(algs); - while (tokens.hasMoreTokens()) { - String alg = tokens.nextToken(); - String attrName = alg + digestAttrSuffix; - String digestBase64 = section.getAttributeValue(attrName); - if (digestBase64 == null) { - // Attribute not found - continue; - } - alg = getCanonicalJcaMessageDigestAlgorithm(alg); - if ((alg == null) - || (getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile(alg) - > minSdkVersion)) { - // Unsupported digest algorithm - continue; - } - // Supported digest algorithm - result.add(new NamedDigest(alg, base64Decoder.decode(digestBase64))); - break; - } - // No supported digests found -- this will fail to verify on pre-JB MR2 Androids. - if (result.isEmpty()) { - return result; - } - } - - if (maxSdkVersion >= AndroidSdkVersion.JELLY_BEAN_MR2) { - // On JB MR2 and newer, Android platform picks the strongest algorithm out of: - // SHA-512, SHA-384, SHA-256, SHA-1. - for (String alg : JB_MR2_AND_NEWER_DIGEST_ALGS) { - String attrName = getJarDigestAttributeName(alg, digestAttrSuffix); - String digestBase64 = section.getAttributeValue(attrName); - if (digestBase64 == null) { - // Attribute not found - continue; - } - byte[] digest = base64Decoder.decode(digestBase64); - byte[] digestInResult = getDigest(result, alg); - if ((digestInResult == null) || (!Arrays.equals(digestInResult, digest))) { - result.add(new NamedDigest(alg, digest)); - } - break; - } - } - - return result; - } - - private static String[] JB_MR2_AND_NEWER_DIGEST_ALGS = { - "SHA-512", - "SHA-384", - "SHA-256", - "SHA-1", - }; - - private static String getCanonicalJcaMessageDigestAlgorithm(String algorithm) { - return UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.get(algorithm.toUpperCase(Locale.US)); - } - - public static int getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile( - String jcaAlgorithmName) { - Integer result = - MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.get( - jcaAlgorithmName.toUpperCase(Locale.US)); - return (result != null) ? result : Integer.MAX_VALUE; - } - - private static String getJarDigestAttributeName( - String jcaDigestAlgorithm, String attrNameSuffix) { - if ("SHA-1".equalsIgnoreCase(jcaDigestAlgorithm)) { - return "SHA1" + attrNameSuffix; - } else { - return jcaDigestAlgorithm + attrNameSuffix; - } - } - - private static Map UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL; - static { - UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL = new HashMap<>(8); - UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("MD5", "MD5"); - UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA", "SHA-1"); - UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA1", "SHA-1"); - UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-1", "SHA-1"); - UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-256", "SHA-256"); - UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-384", "SHA-384"); - UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-512", "SHA-512"); - } - - private static Map MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST; - static { - MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST = new HashMap<>(5); - MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("MD5", 0); - MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-1", 0); - MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-256", 0); - MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put( - "SHA-384", AndroidSdkVersion.GINGERBREAD); - MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put( - "SHA-512", AndroidSdkVersion.GINGERBREAD); - } - - private static byte[] getDigest(Collection digests, String jcaDigestAlgorithm) { - for (NamedDigest digest : digests) { - if (digest.jcaDigestAlgorithm.equalsIgnoreCase(jcaDigestAlgorithm)) { - return digest.digest; - } - } - return null; - } - - private static List parseZipCentralDirectory( - DataSource apk, - ApkUtils.ZipSections apkSections) - throws IOException, ZipFormatException { - // Read the ZIP Central Directory - 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); - - // Parse the ZIP Central Directory - int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount(); - List cdRecords = new ArrayList<>(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 Central Directory record #" + (i + 1) - + " at file offset " + (cdOffset + offsetInsideCd), - e); - } - String entryName = cdRecord.getName(); - if (entryName.endsWith("/")) { - // Ignore directory entries - continue; - } - cdRecords.add(cdRecord); - } - // There may be more data in Central Directory, but we don't warn or throw because Android - // ignores unused CD data. - - return cdRecords; - } - - /** - * Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's - * manifest for the APK to verify on Android. - */ - private static boolean isJarEntryDigestNeededInManifest(String entryName) { - // NOTE: This logic is different from what's required by the JAR signing scheme. This is - // because Android's APK verification logic differs from that spec. In particular, JAR - // signing spec includes into JAR manifest all files in subdirectories of META-INF and - // any files inside META-INF not related to signatures. - if (entryName.startsWith("META-INF/")) { - return false; - } - return !entryName.endsWith("/"); - } - - private static Set verifyJarEntriesAgainstManifestAndSigners( - DataSource apk, - long cdOffsetInApk, - Collection cdRecords, - Map entryNameToManifestSection, - List signers, - int minSdkVersion, - int maxSdkVersion, - Result result) throws ZipFormatException, IOException, NoSuchAlgorithmException { - // Iterate over APK contents as sequentially as possible to improve performance. - List cdRecordsSortedByLocalFileHeaderOffset = - new ArrayList<>(cdRecords); - Collections.sort( - cdRecordsSortedByLocalFileHeaderOffset, - CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR); - Set manifestEntryNamesMissingFromApk = - new HashSet<>(entryNameToManifestSection.keySet()); - List firstSignedEntrySigners = null; - String firstSignedEntryName = null; - for (CentralDirectoryRecord cdRecord : cdRecordsSortedByLocalFileHeaderOffset) { - String entryName = cdRecord.getName(); - manifestEntryNamesMissingFromApk.remove(entryName); - if (!isJarEntryDigestNeededInManifest(entryName)) { - continue; - } - - ManifestParser.Section manifestSection = entryNameToManifestSection.get(entryName); - if (manifestSection == null) { - result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName); - continue; - } - - List entrySigners = new ArrayList<>(signers.size()); - for (Signer signer : signers) { - if (signer.getSigFileEntryNames().contains(entryName)) { - entrySigners.add(signer); - } - } - if (entrySigners.isEmpty()) { - result.addError(Issue.JAR_SIG_ZIP_ENTRY_NOT_SIGNED, entryName); - continue; - } - if (firstSignedEntrySigners == null) { - firstSignedEntrySigners = entrySigners; - firstSignedEntryName = entryName; - } else if (!entrySigners.equals(firstSignedEntrySigners)) { - result.addError( - Issue.JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH, - firstSignedEntryName, - getSignerNames(firstSignedEntrySigners), - entryName, - getSignerNames(entrySigners)); - continue; - } - - Collection expectedDigests = - getDigestsToVerify(manifestSection, "-Digest", minSdkVersion, maxSdkVersion); - if (expectedDigests.isEmpty()) { - result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName); - continue; - } - - MessageDigest[] mds = new MessageDigest[expectedDigests.size()]; - int mdIndex = 0; - for (NamedDigest expectedDigest : expectedDigests) { - mds[mdIndex] = getMessageDigest(expectedDigest.jcaDigestAlgorithm); - mdIndex++; - } - - try { - LocalFileRecord.outputUncompressedData( - apk, - cdRecord, - cdOffsetInApk, - new MessageDigestSink(mds)); - } catch (ZipFormatException e) { - throw new ZipFormatException("Malformed entry: " + entryName, e); - } catch (IOException e) { - throw new IOException("Failed to read entry: " + entryName, e); - } - - mdIndex = 0; - for (NamedDigest expectedDigest : expectedDigests) { - byte[] actualDigest = mds[mdIndex].digest(); - if (!Arrays.equals(expectedDigest.digest, actualDigest)) { - result.addError( - Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY, - entryName, - expectedDigest.jcaDigestAlgorithm, - V1SchemeSigner.MANIFEST_ENTRY_NAME, - Base64.getEncoder().encodeToString(actualDigest), - Base64.getEncoder().encodeToString(expectedDigest.digest)); - } - } - } - - if (firstSignedEntrySigners == null) { - result.addError(Issue.JAR_SIG_NO_SIGNED_ZIP_ENTRIES); - return Collections.emptySet(); - } else { - return new HashSet<>(firstSignedEntrySigners); - } - } - - private static List getSignerNames(List signers) { - if (signers.isEmpty()) { - return Collections.emptyList(); - } - List result = new ArrayList<>(signers.size()); - for (Signer signer : signers) { - result.add(signer.getName()); - } - return result; - } - - private static MessageDigest getMessageDigest(String algorithm) - throws NoSuchAlgorithmException { - return MessageDigest.getInstance(algorithm); - } - - private static byte[] digest(String algorithm, byte[] data, int offset, int length) - throws NoSuchAlgorithmException { - MessageDigest md = getMessageDigest(algorithm); - md.update(data, offset, length); - return md.digest(); - } - - private static byte[] digest(String algorithm, byte[] data) throws NoSuchAlgorithmException { - return getMessageDigest(algorithm).digest(data); - } - - private static class NamedDigest { - private final String jcaDigestAlgorithm; - private final byte[] digest; - - private NamedDigest(String jcaDigestAlgorithm, byte[] digest) { - this.jcaDigestAlgorithm = jcaDigestAlgorithm; - this.digest = digest; - } - } - - public static class Result { - - /** Whether the APK's JAR signature verifies. */ - public boolean verified; - - /** List of APK's signers. These signers are used by Android. */ - public final List signers = new ArrayList<>(); - - /** - * Signers encountered in the APK but not included in the set of the APK's signers. These - * signers are ignored by Android. - */ - public final List ignoredSigners = new ArrayList<>(); - - private final List mWarnings = new ArrayList<>(); - private final List mErrors = new ArrayList<>(); - - private boolean containsErrors() { - if (!mErrors.isEmpty()) { - return true; - } - for (SignerInfo signer : signers) { - if (signer.containsErrors()) { - return true; - } - } - return false; - } - - private void addError(Issue msg, Object... parameters) { - mErrors.add(new IssueWithParams(msg, parameters)); - } - - private void addWarning(Issue msg, Object... parameters) { - mWarnings.add(new IssueWithParams(msg, parameters)); - } - - public List getErrors() { - return mErrors; - } - - public List getWarnings() { - return mWarnings; - } - - public static class SignerInfo { - public final String name; - public final String signatureFileName; - public final String signatureBlockFileName; - public final List certChain = new ArrayList<>(); - - private final List mWarnings = new ArrayList<>(); - private final List mErrors = new ArrayList<>(); - - private SignerInfo( - String name, String signatureBlockFileName, String signatureFileName) { - this.name = name; - this.signatureBlockFileName = signatureBlockFileName; - this.signatureFileName = signatureFileName; - } - - private boolean containsErrors() { - return !mErrors.isEmpty(); - } - - private void addError(Issue msg, Object... parameters) { - mErrors.add(new IssueWithParams(msg, parameters)); - } - - private void addWarning(Issue msg, Object... parameters) { - mWarnings.add(new IssueWithParams(msg, parameters)); - } - - public List getErrors() { - return mErrors; - } - - public List getWarnings() { - return mWarnings; - } - } - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java deleted file mode 100644 index 7c136bed3..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java deleted file mode 100644 index 20f890d46..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java +++ /dev/null @@ -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 mJcaSignatureAlgAndParams; - - private SignatureAlgorithm(int id, - ContentDigestAlgorithm contentDigestAlgorithm, - String jcaKeyAlgorithm, - Pair 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 getJcaSignatureAlgorithmAndParams() { - return mJcaSignatureAlgAndParams; - } - - static SignatureAlgorithm findById(int id) { - for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { - if (alg.getId() == id) { - return alg; - } - } - - return null; - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java deleted file mode 100644 index 06d31dd1f..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java +++ /dev/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. - * - *

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

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 certificates; - - /** - * List of signature algorithms with which to sign. - */ - public List 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 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 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 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 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 computeContentDigests( - Set 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 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 signerConfigs, - Map 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 signerConfigs, - Map contentDigests) - throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { - // FORMAT: - // * length-prefixed sequence of length-prefixed signer blocks. - - List 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 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> 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 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> signatures; - public byte[] publicKey; - } - - private static final class SignedData { - public List> digests; - public List 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 encodeCertificates(List certificates) - throws CertificateEncodingException { - List result = new ArrayList<>(certificates.size()); - for (X509Certificate certificate : certificates) { - result.add(certificate.getEncoded()); - } - return result; - } - - private static byte[] encodeAsSequenceOfLengthPrefixedElements(List 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> sequence) { - int resultSize = 0; - for (Pair element : sequence) { - resultSize += 12 + element.getSecond().length; - } - ByteBuffer result = ByteBuffer.allocate(resultSize); - result.order(ByteOrder.LITTLE_ENDIAN); - for (Pair 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(); - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java deleted file mode 100644 index 5e1e8fb74..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java +++ /dev/null @@ -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. - * - *

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

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 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}. - * - *

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 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}. - * - *

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 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 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 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 sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size()); - for (Result.SignerInfo.Signature signature : result.signatures) { - sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId()); - } - List 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 getSignaturesToVerify( - List 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 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 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 apkSigningBlockAndOffset = findApkSigningBlock(apk, zipSections); - DataSource apkSigningBlock = apkSigningBlockAndOffset.getFirst(); - long apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond(); - ByteBuffer apkSigningBlockBuf = - apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size()); - apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN); - - // Find the APK Signature Scheme v2 Block inside the APK Signing Block. - ByteBuffer apkSignatureSchemeV2Block = - findApkSignatureSchemeV2Block(apkSigningBlockBuf, result); - - return new SignatureInfo( - apkSignatureSchemeV2Block, - apkSigningBlockOffset, - zipSections.getZipCentralDirectoryOffset(), - zipSections.getZipEndOfCentralDirectoryOffset(), - eocd); - } - - /** - * Returns the APK Signing Block and its offset in the provided APK. - * - * @throws SignatureNotFoundException if the APK does not contain an APK Signing Block - */ - public static Pair findApkSigningBlock( - DataSource apk, ApkUtils.ZipSections zipSections) - throws IOException, SignatureNotFoundException { - // FORMAT: - // OFFSET DATA TYPE DESCRIPTION - // * @+0 bytes uint64: size in bytes (excluding this field) - // * @+8 bytes payload - // * @-24 bytes uint64: size in bytes (same as the one above) - // * @-16 bytes uint128: magic - - long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset(); - long centralDirEndOffset = - centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes(); - 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 get method for reading {@code size} number of bytes from the current - * position of this buffer. - * - *

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 signers = new ArrayList<>(); - private final List mWarnings = new ArrayList<>(); - private final List 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 getErrors() { - return mErrors; - } - - public List getWarnings() { - return mWarnings; - } - - public static class SignerInfo { - public int index; - public List certs = new ArrayList<>(); - public List contentDigests = new ArrayList<>(); - public Map verifiedContentDigests = new HashMap<>(); - public List signatures = new ArrayList<>(); - public Map verifiedSignatures = new HashMap<>(); - public List additionalAttributes = new ArrayList<>(); - public byte[] signedData; - - private final List mWarnings = new ArrayList<>(); - private final List 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 getErrors() { - return mErrors; - } - - public List 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(); - } - } - } - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestParser.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestParser.java deleted file mode 100644 index 793300c90..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestParser.java +++ /dev/null @@ -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. - * - *

These files consist of a main section followed by individual sections. Individual sections - * are named, their names referring to JAR entries. - * - * @see JAR Manifest format - */ -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

readAllSections() { - List
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 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 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 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 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; - } - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestWriter.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestWriter.java deleted file mode 100644 index 13b1aaf9e..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestWriter.java +++ /dev/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 JAR Manifest format - */ -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 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 getAttributesSortedByName(Attributes attributes) { - Set> attributesEntries = attributes.entrySet(); - SortedMap namedAttributes = new TreeMap(); - for (Map.Entry attribute : attributesEntries) { - String attrName = attribute.getKey().toString(); - String attrValue = attribute.getValue().toString(); - namedAttributes.put(attrName, attrValue); - } - return namedAttributes; - } - - static void writeAttributes( - OutputStream out, SortedMap attributesSortedByName) throws IOException { - for (Map.Entry attribute : attributesSortedByName.entrySet()) { - String attrName = attribute.getKey(); - String attrValue = attribute.getValue(); - writeAttribute(out, attrName, attrValue); - } - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/SignatureFileWriter.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/SignatureFileWriter.java deleted file mode 100644 index 94ae280ca..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/SignatureFileWriter.java +++ /dev/null @@ -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 JAR Manifest format - */ -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 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); - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/AndroidSdkVersion.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/AndroidSdkVersion.java deleted file mode 100644 index 3e9d0390f..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/AndroidSdkVersion.java +++ /dev/null @@ -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; -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferDataSource.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferDataSource.java deleted file mode 100644 index b2d9ca115..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferDataSource.java +++ /dev/null @@ -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 +")"); - } - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferSink.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferSink.java deleted file mode 100644 index 8c5790537..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferSink.java +++ /dev/null @@ -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); - } - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/DelegatingX509Certificate.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/DelegatingX509Certificate.java deleted file mode 100644 index 936cfa910..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/DelegatingX509Certificate.java +++ /dev/null @@ -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 getCriticalExtensionOIDs() { - return mDelegate.getCriticalExtensionOIDs(); - } - - @Override - public byte[] getExtensionValue(String oid) { - return mDelegate.getExtensionValue(oid); - } - - @Override - public Set 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(); - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/InclusiveIntRange.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/InclusiveIntRange.java deleted file mode 100644 index baf365594..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/InclusiveIntRange.java +++ /dev/null @@ -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 getValuesNotIn( - List sortedNonOverlappingRanges) { - if (sortedNonOverlappingRanges.isEmpty()) { - return Collections.singletonList(this); - } - - int testValue = min; - List 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)"); - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/MessageDigestSink.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/MessageDigestSink.java deleted file mode 100644 index 45bb30e02..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/MessageDigestSink.java +++ /dev/null @@ -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); - } - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/OutputStreamDataSink.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/OutputStreamDataSink.java deleted file mode 100644 index bf18ca0cb..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/OutputStreamDataSink.java +++ /dev/null @@ -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); - } - } - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/Pair.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/Pair.java deleted file mode 100644 index d59af410b..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/Pair.java +++ /dev/null @@ -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 { - private final A mFirst; - private final B mSecond; - - private Pair(A first, B second) { - mFirst = first; - mSecond = second; - } - - public static Pair of(A first, B second) { - return new Pair(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; - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSink.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSink.java deleted file mode 100644 index 219849228..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSink.java +++ /dev/null @@ -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; - } - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSource.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSource.java deleted file mode 100644 index 208033d6c..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSource.java +++ /dev/null @@ -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 +")"); - } - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/CentralDirectoryRecord.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/CentralDirectoryRecord.java deleted file mode 100644 index 141d01e1c..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/CentralDirectoryRecord.java +++ /dev/null @@ -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 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 { - @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; - } - } - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/EocdRecord.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/EocdRecord.java deleted file mode 100644 index 877759130..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/EocdRecord.java +++ /dev/null @@ -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; - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileRecord.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileRecord.java deleted file mode 100644 index 397a45008..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileRecord.java +++ /dev/null @@ -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. - * - *

The record consists of the Local File Header, file data, and (if present) Data Descriptor. - */ -public class LocalFileRecord { - private static final int RECORD_SIGNATURE = 0x04034b50; - private static final int HEADER_SIZE_BYTES = 30; - - private static final int GP_FLAGS_OFFSET = 6; - private static final int COMPRESSION_METHOD_OFFSET = 8; - private static final int CRC32_OFFSET = 14; - private static final int COMPRESSED_SIZE_OFFSET = 18; - private static final int UNCOMPRESSED_SIZE_OFFSET = 22; - private static final int NAME_LENGTH_OFFSET = 26; - private static final int EXTRA_LENGTH_OFFSET = 28; - private static final int NAME_OFFSET = HEADER_SIZE_BYTES; - - private static final int DATA_DESCRIPTOR_SIZE_BYTES_WITHOUT_SIGNATURE = 12; - private static final int DATA_DESCRIPTOR_SIGNATURE = 0x08074b50; - - private final String mName; - private final int mNameSizeBytes; - private final ByteBuffer mExtra; - - private final long mStartOffsetInArchive; - private final long mSize; - - private final int mDataStartOffset; - private final long mDataSize; - private final boolean mDataCompressed; - private final long mUncompressedDataSize; - - private LocalFileRecord( - String name, - int nameSizeBytes, - ByteBuffer extra, - long startOffsetInArchive, - long size, - int dataStartOffset, - long dataSize, - boolean dataCompressed, - long uncompressedDataSize) { - mName = name; - mNameSizeBytes = nameSizeBytes; - mExtra = extra; - mStartOffsetInArchive = startOffsetInArchive; - mSize = size; - mDataStartOffset = dataStartOffset; - mDataSize = dataSize; - mDataCompressed = dataCompressed; - mUncompressedDataSize = uncompressedDataSize; - } - - public String getName() { - return mName; - } - - public ByteBuffer getExtra() { - return (mExtra.capacity() > 0) ? mExtra.slice() : mExtra; - } - - public int getExtraFieldStartOffsetInsideRecord() { - return HEADER_SIZE_BYTES + mNameSizeBytes; - } - - public long getStartOffsetInArchive() { - return mStartOffsetInArchive; - } - - public int getDataStartOffsetInRecord() { - return mDataStartOffset; - } - - /** - * Returns the size (in bytes) of this record. - */ - public long getSize() { - return mSize; - } - - /** - * Returns {@code true} if this record's file data is stored in compressed form. - */ - public boolean isDataCompressed() { - return mDataCompressed; - } - - /** - * Returns the Local File record starting at the current position of the provided buffer - * and advances the buffer's position immediately past the end of the record. The record - * consists of the Local File Header, data, and (if present) Data Descriptor. - */ - public static LocalFileRecord getRecord( - DataSource apk, - CentralDirectoryRecord cdRecord, - long cdStartOffset) throws ZipFormatException, IOException { - return getRecord( - apk, - cdRecord, - cdStartOffset, - true, // obtain extra field contents - true // include Data Descriptor (if present) - ); - } - - /** - * Returns the Local File record starting at the current position of the provided buffer - * and advances the buffer's position immediately past the end of the record. The record - * consists of the Local File Header, data, and (if present) Data Descriptor. - */ - private static LocalFileRecord getRecord( - DataSource apk, - CentralDirectoryRecord cdRecord, - long cdStartOffset, - boolean extraFieldContentsNeeded, - boolean dataDescriptorIncluded) throws ZipFormatException, IOException { - // IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform - // exhibited when reading an APK for the purposes of verifying its signatures. - - String entryName = cdRecord.getName(); - int cdRecordEntryNameSizeBytes = cdRecord.getNameSizeBytes(); - int headerSizeWithName = HEADER_SIZE_BYTES + cdRecordEntryNameSizeBytes; - long headerStartOffset = cdRecord.getLocalFileHeaderOffset(); - long headerEndOffset = headerStartOffset + headerSizeWithName; - if (headerEndOffset >= cdStartOffset) { - throw new ZipFormatException( - "Local File Header of " + entryName + " extends beyond start of Central" - + " Directory. LFH end: " + headerEndOffset - + ", CD start: " + cdStartOffset); - } - ByteBuffer header; - try { - header = apk.getByteBuffer(headerStartOffset, headerSizeWithName); - } catch (IOException e) { - throw new IOException("Failed to read Local File Header of " + entryName, e); - } - header.order(ByteOrder.LITTLE_ENDIAN); - - int recordSignature = header.getInt(); - if (recordSignature != RECORD_SIGNATURE) { - throw new ZipFormatException( - "Not a Local File Header record for entry " + entryName + ". Signature: 0x" - + Long.toHexString(recordSignature & 0xffffffffL)); - } - short gpFlags = header.getShort(GP_FLAGS_OFFSET); - boolean dataDescriptorUsed = (gpFlags & ZipUtils.GP_FLAG_DATA_DESCRIPTOR_USED) != 0; - long uncompressedDataCrc32FromCdRecord = cdRecord.getCrc32(); - long compressedDataSizeFromCdRecord = cdRecord.getCompressedSize(); - long uncompressedDataSizeFromCdRecord = cdRecord.getUncompressedSize(); - if (!dataDescriptorUsed) { - long crc32 = ZipUtils.getUnsignedInt32(header, CRC32_OFFSET); - if (crc32 != uncompressedDataCrc32FromCdRecord) { - throw new ZipFormatException( - "CRC-32 mismatch between Local File Header and Central Directory for entry " - + entryName + ". LFH: " + crc32 - + ", CD: " + uncompressedDataCrc32FromCdRecord); - } - long compressedSize = ZipUtils.getUnsignedInt32(header, COMPRESSED_SIZE_OFFSET); - if (compressedSize != compressedDataSizeFromCdRecord) { - throw new ZipFormatException( - "Compressed size mismatch between Local File Header and Central Directory" - + " for entry " + entryName + ". LFH: " + compressedSize - + ", CD: " + compressedDataSizeFromCdRecord); - } - long uncompressedSize = ZipUtils.getUnsignedInt32(header, UNCOMPRESSED_SIZE_OFFSET); - if (uncompressedSize != uncompressedDataSizeFromCdRecord) { - throw new ZipFormatException( - "Uncompressed size mismatch between Local File Header and Central Directory" - + " for entry " + entryName + ". LFH: " + uncompressedSize - + ", CD: " + uncompressedDataSizeFromCdRecord); - } - } - int nameLength = ZipUtils.getUnsignedInt16(header, NAME_LENGTH_OFFSET); - if (nameLength > cdRecordEntryNameSizeBytes) { - throw new ZipFormatException( - "Name mismatch between Local File Header and Central Directory for entry" - + entryName + ". LFH: " + nameLength - + " bytes, CD: " + cdRecordEntryNameSizeBytes + " bytes"); - } - String name = CentralDirectoryRecord.getName(header, NAME_OFFSET, nameLength); - if (!entryName.equals(name)) { - throw new ZipFormatException( - "Name mismatch between Local File Header and Central Directory. LFH: \"" - + name + "\", CD: \"" + entryName + "\""); - } - int extraLength = ZipUtils.getUnsignedInt16(header, EXTRA_LENGTH_OFFSET); - - short compressionMethod = header.getShort(COMPRESSION_METHOD_OFFSET); - boolean compressed; - switch (compressionMethod) { - case ZipUtils.COMPRESSION_METHOD_STORED: - compressed = false; - break; - case ZipUtils.COMPRESSION_METHOD_DEFLATED: - compressed = true; - break; - default: - throw new ZipFormatException( - "Unsupported compression method of entry " + entryName - + ": " + (compressionMethod & 0xffff)); - } - - long dataStartOffset = headerStartOffset + HEADER_SIZE_BYTES + nameLength + extraLength; - long dataSize; - if (compressed) { - dataSize = compressedDataSizeFromCdRecord; - } else { - dataSize = uncompressedDataSizeFromCdRecord; - } - long dataEndOffset = dataStartOffset + dataSize; - if (dataEndOffset > cdStartOffset) { - throw new ZipFormatException( - "Local File Header data of " + entryName + " overlaps with Central Directory" - + ". LFH data start: " + dataStartOffset - + ", LFH data end: " + dataEndOffset + ", CD start: " + cdStartOffset); - } - - ByteBuffer extra = EMPTY_BYTE_BUFFER; - if ((extraFieldContentsNeeded) && (extraLength > 0)) { - extra = apk.getByteBuffer( - headerStartOffset + HEADER_SIZE_BYTES + nameLength, extraLength); - } - - long recordEndOffset = dataEndOffset; - // Include the Data Descriptor (if requested and present) into the record. - if ((dataDescriptorIncluded) && ((gpFlags & ZipUtils.GP_FLAG_DATA_DESCRIPTOR_USED) != 0)) { - // The record's data is supposed to be followed by the Data Descriptor. Unfortunately, - // the descriptor's size is not known in advance because the spec lets the signature - // field (the first four bytes) be omitted. Thus, there's no 100% reliable way to tell - // how long the Data Descriptor record is. Most parsers (including Android) check - // whether the first four bytes look like Data Descriptor record signature and, if so, - // assume that it is indeed the record's signature. However, this is the wrong - // conclusion if the record's CRC-32 (next field after the signature) has the same value - // as the signature. In any case, we're doing what Android is doing. - long dataDescriptorEndOffset = - dataEndOffset + DATA_DESCRIPTOR_SIZE_BYTES_WITHOUT_SIGNATURE; - if (dataDescriptorEndOffset > cdStartOffset) { - throw new ZipFormatException( - "Data Descriptor of " + entryName + " overlaps with Central Directory" - + ". Data Descriptor end: " + dataEndOffset - + ", CD start: " + cdStartOffset); - } - ByteBuffer dataDescriptorPotentialSig = apk.getByteBuffer(dataEndOffset, 4); - dataDescriptorPotentialSig.order(ByteOrder.LITTLE_ENDIAN); - if (dataDescriptorPotentialSig.getInt() == DATA_DESCRIPTOR_SIGNATURE) { - dataDescriptorEndOffset += 4; - if (dataDescriptorEndOffset > cdStartOffset) { - throw new ZipFormatException( - "Data Descriptor of " + entryName + " overlaps with Central Directory" - + ". Data Descriptor end: " + dataEndOffset - + ", CD start: " + cdStartOffset); - } - } - recordEndOffset = dataDescriptorEndOffset; - } - - long recordSize = recordEndOffset - headerStartOffset; - int dataStartOffsetInRecord = HEADER_SIZE_BYTES + nameLength + extraLength; - - return new LocalFileRecord( - entryName, - cdRecordEntryNameSizeBytes, - extra, - headerStartOffset, - recordSize, - dataStartOffsetInRecord, - dataSize, - compressed, - uncompressedDataSizeFromCdRecord); - } - - /** - * Outputs this record and returns returns the number of bytes output. - */ - public long outputRecord(DataSource sourceApk, DataSink output) throws IOException { - long size = getSize(); - sourceApk.feed(getStartOffsetInArchive(), size, output); - return size; - } - - /** - * Outputs this record, replacing its extra field with the provided one, and returns returns the - * number of bytes output. - */ - public long outputRecordWithModifiedExtra( - DataSource sourceApk, - ByteBuffer extra, - DataSink output) throws IOException { - long recordStartOffsetInSource = getStartOffsetInArchive(); - int extraStartOffsetInRecord = getExtraFieldStartOffsetInsideRecord(); - int extraSizeBytes = extra.remaining(); - int headerSize = extraStartOffsetInRecord + extraSizeBytes; - ByteBuffer header = ByteBuffer.allocate(headerSize); - header.order(ByteOrder.LITTLE_ENDIAN); - sourceApk.copyTo(recordStartOffsetInSource, extraStartOffsetInRecord, header); - header.put(extra.slice()); - header.flip(); - ZipUtils.setUnsignedInt16(header, EXTRA_LENGTH_OFFSET, extraSizeBytes); - - long outputByteCount = header.remaining(); - output.consume(header); - long remainingRecordSize = getSize() - mDataStartOffset; - sourceApk.feed(recordStartOffsetInSource + mDataStartOffset, remainingRecordSize, output); - outputByteCount += remainingRecordSize; - return outputByteCount; - } - - /** - * Outputs the specified Local File Header record with its data and returns the number of bytes - * output. - */ - public static long outputRecordWithDeflateCompressedData( - String name, - int lastModifiedTime, - int lastModifiedDate, - byte[] compressedData, - long crc32, - long uncompressedSize, - DataSink output) throws IOException { - byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8); - int recordSize = HEADER_SIZE_BYTES + nameBytes.length; - ByteBuffer result = ByteBuffer.allocate(recordSize); - result.order(ByteOrder.LITTLE_ENDIAN); - result.putInt(RECORD_SIGNATURE); - ZipUtils.putUnsignedInt16(result, 0x14); // Minimum version needed to extract - result.putShort(ZipUtils.GP_FLAG_EFS); // General purpose flag: UTF-8 encoded name - result.putShort(ZipUtils.COMPRESSION_METHOD_DEFLATED); - ZipUtils.putUnsignedInt16(result, lastModifiedTime); - ZipUtils.putUnsignedInt16(result, lastModifiedDate); - ZipUtils.putUnsignedInt32(result, crc32); - ZipUtils.putUnsignedInt32(result, compressedData.length); - ZipUtils.putUnsignedInt32(result, uncompressedSize); - ZipUtils.putUnsignedInt16(result, nameBytes.length); - ZipUtils.putUnsignedInt16(result, 0); // Extra field length - result.put(nameBytes); - if (result.hasRemaining()) { - throw new RuntimeException("pos: " + result.position() + ", limit: " + result.limit()); - } - result.flip(); - - long outputByteCount = result.remaining(); - output.consume(result); - outputByteCount += compressedData.length; - output.consume(compressedData, 0, compressedData.length); - return outputByteCount; - } - - private static final ByteBuffer EMPTY_BYTE_BUFFER = ByteBuffer.allocate(0); - - /** - * Sends uncompressed data of this record into the the provided data sink. - */ - public void outputUncompressedData( - DataSource lfhSection, - DataSink sink) throws IOException, ZipFormatException { - long dataStartOffsetInArchive = mStartOffsetInArchive + mDataStartOffset; - try { - if (mDataCompressed) { - try (InflateSinkAdapter inflateAdapter = new InflateSinkAdapter(sink)) { - lfhSection.feed(dataStartOffsetInArchive, mDataSize, inflateAdapter); - long actualUncompressedSize = inflateAdapter.getOutputByteCount(); - if (actualUncompressedSize != mUncompressedDataSize) { - throw new ZipFormatException( - "Unexpected size of uncompressed data of " + mName - + ". Expected: " + mUncompressedDataSize + " bytes" - + ", actual: " + actualUncompressedSize + " bytes"); - } - } - } else { - lfhSection.feed(dataStartOffsetInArchive, mDataSize, sink); - // No need to check whether output size is as expected because DataSource.feed is - // guaranteed to output exactly the number of bytes requested. - } - } catch (IOException e) { - throw new IOException( - "Failed to read data of " + ((mDataCompressed) ? "compressed" : "uncompressed") - + " entry " + mName, - e); - } - // Interestingly, Android doesn't check that uncompressed data's CRC-32 is as expected. We - // thus don't check either. - } - - /** - * Sends uncompressed data pointed to by the provided ZIP Central Directory (CD) record into the - * provided data sink. - */ - public static void outputUncompressedData( - DataSource source, - CentralDirectoryRecord cdRecord, - long cdStartOffsetInArchive, - DataSink sink) throws ZipFormatException, IOException { - // IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform - // exhibited when reading an APK for the purposes of verifying its signatures. - // When verifying an APK, Android doesn't care reading the extra field or the Data - // Descriptor. - LocalFileRecord lfhRecord = - getRecord( - source, - cdRecord, - cdStartOffsetInArchive, - false, // don't care about the extra field - false // don't read the Data Descriptor - ); - lfhRecord.outputUncompressedData(source, sink); - } - - /** - * Returns the uncompressed data pointed to by the provided ZIP Central Directory (CD) record. - */ - public static byte[] getUncompressedData( - DataSource source, - CentralDirectoryRecord cdRecord, - long cdStartOffsetInArchive) throws ZipFormatException, IOException { - if (cdRecord.getUncompressedSize() > Integer.MAX_VALUE) { - throw new IOException( - cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize()); - } - byte[] result = new byte[(int) cdRecord.getUncompressedSize()]; - ByteBuffer resultBuf = ByteBuffer.wrap(result); - ByteBufferSink resultSink = new ByteBufferSink(resultBuf); - outputUncompressedData( - source, - cdRecord, - cdStartOffsetInArchive, - resultSink); - return result; - } - - /** - * {@link DataSink} which inflates received data and outputs the deflated data into the provided - * delegate sink. - */ - private static class InflateSinkAdapter implements DataSink, Closeable { - private final DataSink mDelegate; - - private Inflater mInflater = new Inflater(true); - private byte[] mOutputBuffer; - private byte[] mInputBuffer; - private long mOutputByteCount; - private boolean mClosed; - - private InflateSinkAdapter(DataSink delegate) { - mDelegate = delegate; - } - - @Override - public void consume(byte[] buf, int offset, int length) throws IOException { - checkNotClosed(); - mInflater.setInput(buf, offset, length); - if (mOutputBuffer == null) { - mOutputBuffer = new byte[65536]; - } - while (!mInflater.finished()) { - int outputChunkSize; - try { - outputChunkSize = mInflater.inflate(mOutputBuffer); - } catch (DataFormatException e) { - throw new IOException("Failed to inflate data", e); - } - if (outputChunkSize == 0) { - return; - } - mDelegate.consume(mOutputBuffer, 0, outputChunkSize); - mOutputByteCount += outputChunkSize; - } - } - - @Override - public void consume(ByteBuffer buf) throws IOException { - checkNotClosed(); - if (buf.hasArray()) { - consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining()); - buf.position(buf.limit()); - } else { - if (mInputBuffer == null) { - mInputBuffer = new byte[65536]; - } - while (buf.hasRemaining()) { - int chunkSize = Math.min(buf.remaining(), mInputBuffer.length); - buf.get(mInputBuffer, 0, chunkSize); - consume(mInputBuffer, 0, chunkSize); - } - } - } - - public long getOutputByteCount() { - return mOutputByteCount; - } - - @Override - public void close() throws IOException { - mClosed = true; - mInputBuffer = null; - mOutputBuffer = null; - if (mInflater != null) { - mInflater.end(); - mInflater = null; - } - } - - private void checkNotClosed() { - if (mClosed) { - throw new IllegalStateException("Closed"); - } - } - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java deleted file mode 100644 index 6a0c501e9..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java +++ /dev/null @@ -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. - * - *

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

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

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

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

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

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; - } - } -} \ No newline at end of file diff --git a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSink.java b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSink.java deleted file mode 100644 index 35a61fcee..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSink.java +++ /dev/null @@ -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. - * - *

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

This data sink guarantees to not hold references to the provided buffer after this method - * terminates. - */ - void consume(ByteBuffer buf) throws IOException; -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSinks.java b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSinks.java deleted file mode 100644 index 4aefedb05..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSinks.java +++ /dev/null @@ -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); - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSource.java b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSource.java deleted file mode 100644 index e268dd29d..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSource.java +++ /dev/null @@ -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. - * - *

This abstraction serves three purposes: - *

    - *
  • Transparent handling of different types of sources, such as {@code byte[]}, - * {@link java.nio.ByteBuffer}, {@link java.io.RandomAccessFile}, memory-mapped file.
  • - *
  • Support sources larger than 2 GB. If all sources were smaller than 2 GB, {@code ByteBuffer} - * may have worked as the unifying abstraction.
  • - *
  • Support sources which do not fit into logical memory as a contiguous region.
  • - *
- * - *

There are following ways to obtain a chunk of data from the data source: - *

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

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); -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSources.java b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSources.java deleted file mode 100644 index 1cbb0af5f..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSources.java +++ /dev/null @@ -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); - } -} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/zip/ZipFormatException.java b/tools/apksigner/core/src/com/android/apksigner/core/zip/ZipFormatException.java deleted file mode 100644 index 7da57d9f1..000000000 --- a/tools/apksigner/core/src/com/android/apksigner/core/zip/ZipFormatException.java +++ /dev/null @@ -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); - } -} diff --git a/tools/signapk/Android.mk b/tools/signapk/Android.mk index eff066ccd..4506e2fc7 100644 --- a/tools/signapk/Android.mk +++ b/tools/signapk/Android.mk @@ -22,7 +22,7 @@ LOCAL_MODULE := signapk LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_JAR_MANIFEST := SignApk.mf LOCAL_STATIC_JAVA_LIBRARIES := \ - apksigner-core \ + apksig \ bouncycastle-host \ bouncycastle-bcpkix-host \ conscrypt-host diff --git a/tools/signapk/src/com/android/signapk/SignApk.java b/tools/signapk/src/com/android/signapk/SignApk.java index f1f340d34..84fddec2d 100644 --- a/tools/signapk/src/com/android/signapk/SignApk.java +++ b/tools/signapk/src/com/android/signapk/SignApk.java @@ -34,12 +34,12 @@ import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; import org.conscrypt.OpenSSLProvider; -import com.android.apksigner.core.ApkSignerEngine; -import com.android.apksigner.core.DefaultApkSignerEngine; -import com.android.apksigner.core.apk.ApkUtils; -import com.android.apksigner.core.util.DataSink; -import com.android.apksigner.core.util.DataSources; -import com.android.apksigner.core.zip.ZipFormatException; +import com.android.apksig.ApkSignerEngine; +import com.android.apksig.DefaultApkSignerEngine; +import com.android.apksig.apk.ApkUtils; +import com.android.apksig.util.DataSink; +import com.android.apksig.util.DataSources; +import com.android.apksig.zip.ZipFormatException; import java.io.Console; import java.io.BufferedReader;