Switch signapk to apksigner-core.
This switches signapk's APK signing from its own signing logic to that offered by apksigner-core library. OTA update package signing logic remains inside signapk codebase. Bug: 27461702 Change-Id: Ibf8435c555fe3f2b621d5189e7ae44f79082c810
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksigner.core.apk;
|
||||
|
||||
import com.android.apksigner.core.internal.util.Pair;
|
||||
import com.android.apksigner.core.internal.zip.ZipUtils;
|
||||
import com.android.apksigner.core.util.DataSource;
|
||||
import com.android.apksigner.core.zip.ZipFormatException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* APK utilities.
|
||||
*/
|
||||
public class ApkUtils {
|
||||
|
||||
private ApkUtils() {}
|
||||
|
||||
/**
|
||||
* Finds the main ZIP sections of the provided APK.
|
||||
*
|
||||
* @throws IOException if an I/O error occurred while reading the APK
|
||||
* @throws ZipFormatException if the APK is malformed
|
||||
*/
|
||||
public static ZipSections findZipSections(DataSource apk)
|
||||
throws IOException, ZipFormatException {
|
||||
Pair<ByteBuffer, Long> eocdAndOffsetInFile =
|
||||
ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
|
||||
if (eocdAndOffsetInFile == null) {
|
||||
throw new ZipFormatException("ZIP End of Central Directory record not found");
|
||||
}
|
||||
|
||||
ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
|
||||
long eocdOffset = eocdAndOffsetInFile.getSecond();
|
||||
if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
|
||||
throw new ZipFormatException("ZIP64 APK not supported");
|
||||
}
|
||||
eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
|
||||
if (cdStartOffset >= eocdOffset) {
|
||||
throw new ZipFormatException(
|
||||
"ZIP Central Directory start offset out of range: " + cdStartOffset
|
||||
+ ". ZIP End of Central Directory offset: " + eocdOffset);
|
||||
}
|
||||
|
||||
long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
|
||||
long cdEndOffset = cdStartOffset + cdSizeBytes;
|
||||
if (cdEndOffset > eocdOffset) {
|
||||
throw new ZipFormatException(
|
||||
"ZIP Central Directory overlaps with End of Central Directory"
|
||||
+ ". CD end: " + cdEndOffset
|
||||
+ ", EoCD start: " + eocdOffset);
|
||||
}
|
||||
|
||||
int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
|
||||
|
||||
return new ZipSections(
|
||||
cdStartOffset,
|
||||
cdSizeBytes,
|
||||
cdRecordCount,
|
||||
eocdOffset,
|
||||
eocdBuf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the ZIP sections of an APK.
|
||||
*/
|
||||
public static class ZipSections {
|
||||
private final long mCentralDirectoryOffset;
|
||||
private final long mCentralDirectorySizeBytes;
|
||||
private final int mCentralDirectoryRecordCount;
|
||||
private final long mEocdOffset;
|
||||
private final ByteBuffer mEocd;
|
||||
|
||||
public ZipSections(
|
||||
long centralDirectoryOffset,
|
||||
long centralDirectorySizeBytes,
|
||||
int centralDirectoryRecordCount,
|
||||
long eocdOffset,
|
||||
ByteBuffer eocd) {
|
||||
mCentralDirectoryOffset = centralDirectoryOffset;
|
||||
mCentralDirectorySizeBytes = centralDirectorySizeBytes;
|
||||
mCentralDirectoryRecordCount = centralDirectoryRecordCount;
|
||||
mEocdOffset = eocdOffset;
|
||||
mEocd = eocd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the start offset of the ZIP Central Directory. This value is taken from the
|
||||
* ZIP End of Central Directory record.
|
||||
*/
|
||||
public long getZipCentralDirectoryOffset() {
|
||||
return mCentralDirectoryOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the
|
||||
* ZIP End of Central Directory record.
|
||||
*/
|
||||
public long getZipCentralDirectorySizeBytes() {
|
||||
return mCentralDirectorySizeBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of records in the ZIP Central Directory. This value is taken from the
|
||||
* ZIP End of Central Directory record.
|
||||
*/
|
||||
public int getZipCentralDirectoryRecordCount() {
|
||||
return mCentralDirectoryRecordCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the start offset of the ZIP End of Central Directory record. The record extends
|
||||
* until the very end of the APK.
|
||||
*/
|
||||
public long getZipEndOfCentralDirectoryOffset() {
|
||||
return mEocdOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contents of the ZIP End of Central Directory.
|
||||
*/
|
||||
public ByteBuffer getZipEndOfCentralDirectory() {
|
||||
return mEocd;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the offset of the start of the ZIP Central Directory in the APK's ZIP End of Central
|
||||
* Directory record.
|
||||
*
|
||||
* @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
|
||||
* @param offset offset of the ZIP Central Directory relative to the start of the archive. Must
|
||||
* be between {@code 0} and {@code 2^32 - 1} inclusive.
|
||||
*/
|
||||
public static void setZipEocdCentralDirectoryOffset(
|
||||
ByteBuffer zipEndOfCentralDirectory, long offset) {
|
||||
ByteBuffer eocd = zipEndOfCentralDirectory.slice();
|
||||
eocd.order(ByteOrder.LITTLE_ENDIAN);
|
||||
ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset);
|
||||
}
|
||||
}
|
@@ -16,9 +16,14 @@
|
||||
|
||||
package com.android.apksigner.core.internal.zip;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import com.android.apksigner.core.internal.util.ByteBufferSink;
|
||||
import com.android.apksigner.core.internal.util.Pair;
|
||||
import com.android.apksigner.core.util.DataSource;
|
||||
|
||||
/**
|
||||
* Assorted ZIP format helpers.
|
||||
*
|
||||
@@ -28,7 +33,20 @@ import java.nio.ByteOrder;
|
||||
public abstract class ZipUtils {
|
||||
private ZipUtils() {}
|
||||
|
||||
public static final short COMPRESSION_METHOD_STORED = 0;
|
||||
public static final short COMPRESSION_METHOD_DEFLATED = 8;
|
||||
|
||||
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.
|
||||
@@ -44,16 +62,221 @@ public abstract class ZipUtils {
|
||||
offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset of the start of the ZIP Central Directory in the archive.
|
||||
*
|
||||
* <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
|
||||
*/
|
||||
public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) {
|
||||
assertByteOrderLittleEndian(zipEndOfCentralDirectory);
|
||||
return getUnsignedInt32(
|
||||
zipEndOfCentralDirectory,
|
||||
zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size (in bytes) of the ZIP Central Directory.
|
||||
*
|
||||
* <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
|
||||
*/
|
||||
public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) {
|
||||
assertByteOrderLittleEndian(zipEndOfCentralDirectory);
|
||||
return getUnsignedInt32(
|
||||
zipEndOfCentralDirectory,
|
||||
zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of records in ZIP Central Directory.
|
||||
*
|
||||
* <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
|
||||
*/
|
||||
public static int getZipEocdCentralDirectoryTotalRecordCount(
|
||||
ByteBuffer zipEndOfCentralDirectory) {
|
||||
assertByteOrderLittleEndian(zipEndOfCentralDirectory);
|
||||
return getUnsignedInt16(
|
||||
zipEndOfCentralDirectory,
|
||||
zipEndOfCentralDirectory.position()
|
||||
+ ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ZIP End of Central Directory record of the provided ZIP file.
|
||||
*
|
||||
* @return contents of the ZIP End of Central Directory record and the record's offset in the
|
||||
* file or {@code null} if the file does not contain the record.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs while reading the file.
|
||||
*/
|
||||
public static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(DataSource zip)
|
||||
throws IOException {
|
||||
// ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
|
||||
// The record can be identified by its 4-byte signature/magic which is located at the very
|
||||
// beginning of the record. A complication is that the record is variable-length because of
|
||||
// the comment field.
|
||||
// The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
|
||||
// end of the buffer for the EOCD record signature. Whenever we find a signature, we check
|
||||
// the candidate record's comment length is such that the remainder of the record takes up
|
||||
// exactly the remaining bytes in the buffer. The search is bounded because the maximum
|
||||
// size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
|
||||
|
||||
long fileSize = zip.size();
|
||||
if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus
|
||||
// the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily
|
||||
// reading more data.
|
||||
Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// EoCD does not start where we expected it to. Perhaps it contains a non-empty comment
|
||||
// field. Expand the search. The maximum size of the comment field in EoCD is 65535 because
|
||||
// the comment length field is an unsigned 16-bit number.
|
||||
return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ZIP End of Central Directory record of the provided ZIP file.
|
||||
*
|
||||
* @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted
|
||||
* value is from 0 to 65535 inclusive. The smaller the value, the faster this method
|
||||
* locates the record, provided its comment field is no longer than this value.
|
||||
*
|
||||
* @return contents of the ZIP End of Central Directory record and the record's offset in the
|
||||
* file or {@code null} if the file does not contain the record.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs while reading the file.
|
||||
*/
|
||||
private static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(
|
||||
DataSource zip, int maxCommentSize) throws IOException {
|
||||
// ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
|
||||
// The record can be identified by its 4-byte signature/magic which is located at the very
|
||||
// beginning of the record. A complication is that the record is variable-length because of
|
||||
// the comment field.
|
||||
// The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
|
||||
// end of the buffer for the EOCD record signature. Whenever we find a signature, we check
|
||||
// the candidate record's comment length is such that the remainder of the record takes up
|
||||
// exactly the remaining bytes in the buffer. The search is bounded because the maximum
|
||||
// size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
|
||||
|
||||
if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) {
|
||||
throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize);
|
||||
}
|
||||
|
||||
long fileSize = zip.size();
|
||||
if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
|
||||
// No space for EoCD record in the file.
|
||||
return null;
|
||||
}
|
||||
// Lower maxCommentSize if the file is too small.
|
||||
maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE);
|
||||
|
||||
ByteBuffer buf = ByteBuffer.allocate(ZIP_EOCD_REC_MIN_SIZE + maxCommentSize);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
long bufOffsetInFile = fileSize - buf.capacity();
|
||||
zip.feed(bufOffsetInFile, buf.remaining(), new ByteBufferSink(buf));
|
||||
buf.flip();
|
||||
int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf);
|
||||
if (eocdOffsetInBuf == -1) {
|
||||
// No EoCD record found in the buffer
|
||||
return null;
|
||||
}
|
||||
// EoCD found
|
||||
buf.position(eocdOffsetInBuf);
|
||||
ByteBuffer eocd = buf.slice();
|
||||
eocd.order(ByteOrder.LITTLE_ENDIAN);
|
||||
return Pair.of(eocd, bufOffsetInFile + eocdOffsetInBuf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position at which ZIP End of Central Directory record starts in the provided
|
||||
* buffer or {@code -1} if the record is not present.
|
||||
*
|
||||
* <p>NOTE: Byte order of {@code zipContents} must be little-endian.
|
||||
*/
|
||||
private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
|
||||
assertByteOrderLittleEndian(zipContents);
|
||||
|
||||
// ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
|
||||
// The record can be identified by its 4-byte signature/magic which is located at the very
|
||||
// beginning of the record. A complication is that the record is variable-length because of
|
||||
// the comment field.
|
||||
// The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
|
||||
// end of the buffer for the EOCD record signature. Whenever we find a signature, we check
|
||||
// the candidate record's comment length is such that the remainder of the record takes up
|
||||
// exactly the remaining bytes in the buffer. The search is bounded because the maximum
|
||||
// size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
|
||||
|
||||
int archiveSize = zipContents.capacity();
|
||||
if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) {
|
||||
return -1;
|
||||
}
|
||||
int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE);
|
||||
int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
|
||||
for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength;
|
||||
expectedCommentLength++) {
|
||||
int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength;
|
||||
if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) {
|
||||
int actualCommentLength =
|
||||
getUnsignedInt16(
|
||||
zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET);
|
||||
if (actualCommentLength == expectedCommentLength) {
|
||||
return eocdStartPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the provided file contains a ZIP64 End of Central Directory
|
||||
* Locator.
|
||||
*
|
||||
* @param zipEndOfCentralDirectoryPosition offset of the ZIP End of Central Directory record
|
||||
* in the file.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs while reading the data source
|
||||
*/
|
||||
public static final boolean isZip64EndOfCentralDirectoryLocatorPresent(
|
||||
DataSource zip, long zipEndOfCentralDirectoryPosition) throws IOException {
|
||||
|
||||
// ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central
|
||||
// Directory Record.
|
||||
long locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE;
|
||||
if (locatorPosition < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ByteBuffer sig = ByteBuffer.allocate(4);
|
||||
sig.order(ByteOrder.LITTLE_ENDIAN);
|
||||
zip.feed(locatorPosition, sig.remaining(), new ByteBufferSink(sig));
|
||||
sig.flip();
|
||||
return sig.getInt(0) == ZIP64_EOCD_LOCATOR_SIG;
|
||||
}
|
||||
|
||||
private static void assertByteOrderLittleEndian(ByteBuffer buffer) {
|
||||
if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
|
||||
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
|
||||
}
|
||||
}
|
||||
|
||||
private static int getUnsignedInt16(ByteBuffer buffer, int offset) {
|
||||
return buffer.getShort(offset) & 0xffff;
|
||||
}
|
||||
|
||||
private 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(buffer.position() + offset, (int) value);
|
||||
buffer.putInt(offset, (int) value);
|
||||
}
|
||||
|
||||
private static long getUnsignedInt32(ByteBuffer buffer, int offset) {
|
||||
return buffer.getInt(offset) & 0xffffffffL;
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
@@ -21,7 +21,11 @@ include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := signapk
|
||||
LOCAL_SRC_FILES := $(call all-java-files-under, src)
|
||||
LOCAL_JAR_MANIFEST := SignApk.mf
|
||||
LOCAL_STATIC_JAVA_LIBRARIES := bouncycastle-host bouncycastle-bcpkix-host conscrypt-host
|
||||
LOCAL_STATIC_JAVA_LIBRARIES := \
|
||||
apksigner-core \
|
||||
bouncycastle-host \
|
||||
bouncycastle-bcpkix-host \
|
||||
conscrypt-host
|
||||
LOCAL_REQUIRED_MODULES := libconscrypt_openjdk_jni
|
||||
include $(BUILD_HOST_JAVA_LIBRARY)
|
||||
|
||||
|
@@ -1,725 +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.signapk;
|
||||
|
||||
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.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.spec.AlgorithmParameterSpec;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.MGF1ParameterSpec;
|
||||
import java.security.spec.PSSParameterSpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* APK Signature Scheme v2 signer.
|
||||
*
|
||||
* <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
|
||||
* bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
|
||||
* uncompressed contents of ZIP entries.
|
||||
*/
|
||||
public abstract class ApkSignerV2 {
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
public static final int SIGNATURE_RSA_PSS_WITH_SHA256 = 0x0101;
|
||||
public static final int SIGNATURE_RSA_PSS_WITH_SHA512 = 0x0102;
|
||||
public static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0103;
|
||||
public static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512 = 0x0104;
|
||||
public static final int SIGNATURE_ECDSA_WITH_SHA256 = 0x0201;
|
||||
public static final int SIGNATURE_ECDSA_WITH_SHA512 = 0x0202;
|
||||
public static final int SIGNATURE_DSA_WITH_SHA256 = 0x0301;
|
||||
|
||||
/**
|
||||
* {@code .SF} file header section attribute indicating that the APK is signed not just with
|
||||
* JAR signature scheme but also with APK Signature Scheme v2 or newer. This attribute
|
||||
* facilitates v2 signature stripping detection.
|
||||
*
|
||||
* <p>The attribute contains a comma-separated set of signature scheme IDs.
|
||||
*/
|
||||
public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed";
|
||||
public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE = "2";
|
||||
|
||||
private static final int CONTENT_DIGEST_CHUNKED_SHA256 = 0;
|
||||
private static final int CONTENT_DIGEST_CHUNKED_SHA512 = 1;
|
||||
|
||||
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;
|
||||
|
||||
private ApkSignerV2() {}
|
||||
|
||||
/**
|
||||
* Signer configuration.
|
||||
*/
|
||||
public static final class SignerConfig {
|
||||
/** Private key. */
|
||||
public PrivateKey privateKey;
|
||||
|
||||
/**
|
||||
* Certificates, with the first certificate containing the public key corresponding to
|
||||
* {@link #privateKey}.
|
||||
*/
|
||||
public List<X509Certificate> certificates;
|
||||
|
||||
/**
|
||||
* List of signature algorithms with which to sign (see {@code SIGNATURE_...} constants).
|
||||
*/
|
||||
public List<Integer> signatureAlgorithms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the provided APK using APK Signature Scheme v2 and returns the signed APK as a list of
|
||||
* consecutive chunks.
|
||||
*
|
||||
* <p>NOTE: To enable APK signature verifier to detect v2 signature stripping, header sections
|
||||
* of META-INF/*.SF files of APK being signed must contain the
|
||||
* {@code X-Android-APK-Signed: true} attribute.
|
||||
*
|
||||
* @param inputApk contents of the APK to be signed. The APK starts at the current position
|
||||
* of the buffer and ends at the limit of the buffer.
|
||||
* @param signerConfigs signer configurations, one for each signer.
|
||||
*
|
||||
* @throws ApkParseException if the APK cannot be parsed.
|
||||
* @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 ByteBuffer[] sign(
|
||||
ByteBuffer inputApk,
|
||||
List<SignerConfig> signerConfigs)
|
||||
throws ApkParseException, InvalidKeyException, SignatureException {
|
||||
// Slice/create a view in the inputApk to make sure that:
|
||||
// 1. inputApk is what's between position and limit of the original inputApk, and
|
||||
// 2. changes to position, limit, and byte order are not reflected in the original.
|
||||
ByteBuffer originalInputApk = inputApk;
|
||||
inputApk = originalInputApk.slice();
|
||||
inputApk.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
// Locate ZIP End of Central Directory (EoCD), Central Directory, and check that Central
|
||||
// Directory is immediately followed by the ZIP End of Central Directory.
|
||||
int eocdOffset = ZipUtils.findZipEndOfCentralDirectoryRecord(inputApk);
|
||||
if (eocdOffset == -1) {
|
||||
throw new ApkParseException("Failed to locate ZIP End of Central Directory");
|
||||
}
|
||||
if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(inputApk, eocdOffset)) {
|
||||
throw new ApkParseException("ZIP64 format not supported");
|
||||
}
|
||||
inputApk.position(eocdOffset);
|
||||
long centralDirSizeLong = ZipUtils.getZipEocdCentralDirectorySizeBytes(inputApk);
|
||||
if (centralDirSizeLong > Integer.MAX_VALUE) {
|
||||
throw new ApkParseException(
|
||||
"ZIP Central Directory size out of range: " + centralDirSizeLong);
|
||||
}
|
||||
int centralDirSize = (int) centralDirSizeLong;
|
||||
long centralDirOffsetLong = ZipUtils.getZipEocdCentralDirectoryOffset(inputApk);
|
||||
if (centralDirOffsetLong > Integer.MAX_VALUE) {
|
||||
throw new ApkParseException(
|
||||
"ZIP Central Directory offset in file out of range: " + centralDirOffsetLong);
|
||||
}
|
||||
int centralDirOffset = (int) centralDirOffsetLong;
|
||||
int expectedEocdOffset = centralDirOffset + centralDirSize;
|
||||
if (expectedEocdOffset < centralDirOffset) {
|
||||
throw new ApkParseException(
|
||||
"ZIP Central Directory extent too large. Offset: " + centralDirOffset
|
||||
+ ", size: " + centralDirSize);
|
||||
}
|
||||
if (eocdOffset != expectedEocdOffset) {
|
||||
throw new ApkParseException(
|
||||
"ZIP Central Directory not immeiately followed by ZIP End of"
|
||||
+ " Central Directory. CD end: " + expectedEocdOffset
|
||||
+ ", EoCD start: " + eocdOffset);
|
||||
}
|
||||
|
||||
// Create ByteBuffers holding the contents of everything before ZIP Central Directory,
|
||||
// ZIP Central Directory, and ZIP End of Central Directory.
|
||||
inputApk.clear();
|
||||
ByteBuffer beforeCentralDir = getByteBuffer(inputApk, centralDirOffset);
|
||||
ByteBuffer centralDir = getByteBuffer(inputApk, eocdOffset - centralDirOffset);
|
||||
// Create a copy of End of Central Directory because we'll need modify its contents later.
|
||||
byte[] eocdBytes = new byte[inputApk.remaining()];
|
||||
inputApk.get(eocdBytes);
|
||||
ByteBuffer eocd = ByteBuffer.wrap(eocdBytes);
|
||||
eocd.order(inputApk.order());
|
||||
|
||||
// Figure which which digests to use for APK contents.
|
||||
Set<Integer> contentDigestAlgorithms = new HashSet<>();
|
||||
for (SignerConfig signerConfig : signerConfigs) {
|
||||
for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
|
||||
contentDigestAlgorithms.add(
|
||||
getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm));
|
||||
}
|
||||
}
|
||||
|
||||
// Compute digests of APK contents.
|
||||
Map<Integer, byte[]> contentDigests; // digest algorithm ID -> digest
|
||||
try {
|
||||
contentDigests =
|
||||
computeContentDigests(
|
||||
contentDigestAlgorithms,
|
||||
new ByteBuffer[] {beforeCentralDir, centralDir, eocd});
|
||||
} 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.
|
||||
ByteBuffer apkSigningBlock =
|
||||
ByteBuffer.wrap(generateApkSigningBlock(signerConfigs, contentDigests));
|
||||
|
||||
// Update Central Directory Offset in End of Central Directory Record. Central Directory
|
||||
// follows the APK Signing Block and thus is shifted by the size of the APK Signing Block.
|
||||
centralDirOffset += apkSigningBlock.remaining();
|
||||
eocd.clear();
|
||||
ZipUtils.setZipEocdCentralDirectoryOffset(eocd, centralDirOffset);
|
||||
|
||||
// Follow the Java NIO pattern for ByteBuffer whose contents have been consumed.
|
||||
originalInputApk.position(originalInputApk.limit());
|
||||
|
||||
// Reset positions (to 0) and limits (to capacity) in the ByteBuffers below to follow the
|
||||
// Java NIO pattern for ByteBuffers which are ready for their contents to be read by caller.
|
||||
// Contrary to the name, this does not clear the contents of these ByteBuffer.
|
||||
beforeCentralDir.clear();
|
||||
centralDir.clear();
|
||||
eocd.clear();
|
||||
|
||||
// Insert APK Signing Block immediately before the ZIP Central Directory.
|
||||
return new ByteBuffer[] {
|
||||
beforeCentralDir,
|
||||
apkSigningBlock,
|
||||
centralDir,
|
||||
eocd,
|
||||
};
|
||||
}
|
||||
|
||||
private static Map<Integer, byte[]> computeContentDigests(
|
||||
Set<Integer> digestAlgorithms,
|
||||
ByteBuffer[] contents) throws 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.
|
||||
|
||||
int chunkCount = 0;
|
||||
for (ByteBuffer input : contents) {
|
||||
chunkCount += getChunkCount(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
|
||||
}
|
||||
|
||||
final Map<Integer, byte[]> digestsOfChunks = new HashMap<>(digestAlgorithms.size());
|
||||
for (int digestAlgorithm : digestAlgorithms) {
|
||||
int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
|
||||
byte[] concatenationOfChunkCountAndChunkDigests =
|
||||
new byte[5 + chunkCount * digestOutputSizeBytes];
|
||||
concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
|
||||
setUnsignedInt32LittleEngian(
|
||||
chunkCount, concatenationOfChunkCountAndChunkDigests, 1);
|
||||
digestsOfChunks.put(digestAlgorithm, concatenationOfChunkCountAndChunkDigests);
|
||||
}
|
||||
|
||||
int chunkIndex = 0;
|
||||
byte[] chunkContentPrefix = new byte[5];
|
||||
chunkContentPrefix[0] = (byte) 0xa5;
|
||||
// Optimization opportunity: digests of chunks can be computed in parallel.
|
||||
for (ByteBuffer input : contents) {
|
||||
while (input.hasRemaining()) {
|
||||
int chunkSize =
|
||||
Math.min(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
|
||||
final ByteBuffer chunk = getByteBuffer(input, chunkSize);
|
||||
for (int digestAlgorithm : digestAlgorithms) {
|
||||
String jcaAlgorithmName =
|
||||
getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
|
||||
MessageDigest md;
|
||||
try {
|
||||
md = MessageDigest.getInstance(jcaAlgorithmName);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new DigestException(
|
||||
jcaAlgorithmName + " MessageDigest not supported", e);
|
||||
}
|
||||
// Reset position to 0 and limit to capacity. Position would've been modified
|
||||
// by the preceding iteration of this loop. NOTE: Contrary to the method name,
|
||||
// this does not modify the contents of the chunk.
|
||||
chunk.clear();
|
||||
setUnsignedInt32LittleEngian(chunk.remaining(), chunkContentPrefix, 1);
|
||||
md.update(chunkContentPrefix);
|
||||
md.update(chunk);
|
||||
byte[] concatenationOfChunkCountAndChunkDigests =
|
||||
digestsOfChunks.get(digestAlgorithm);
|
||||
int expectedDigestSizeBytes =
|
||||
getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
|
||||
int actualDigestSizeBytes =
|
||||
md.digest(
|
||||
concatenationOfChunkCountAndChunkDigests,
|
||||
5 + chunkIndex * expectedDigestSizeBytes,
|
||||
expectedDigestSizeBytes);
|
||||
if (actualDigestSizeBytes != expectedDigestSizeBytes) {
|
||||
throw new DigestException(
|
||||
"Unexpected output size of " + md.getAlgorithm()
|
||||
+ " digest: " + actualDigestSizeBytes);
|
||||
}
|
||||
}
|
||||
chunkIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
Map<Integer, byte[]> result = new HashMap<>(digestAlgorithms.size());
|
||||
for (Map.Entry<Integer, byte[]> entry : digestsOfChunks.entrySet()) {
|
||||
int digestAlgorithm = entry.getKey();
|
||||
byte[] concatenationOfChunkCountAndChunkDigests = entry.getValue();
|
||||
String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
|
||||
MessageDigest md;
|
||||
try {
|
||||
md = MessageDigest.getInstance(jcaAlgorithmName);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new DigestException(jcaAlgorithmName + " MessageDigest not supported", e);
|
||||
}
|
||||
result.put(digestAlgorithm, md.digest(concatenationOfChunkCountAndChunkDigests));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final int getChunkCount(int inputSize, int chunkSize) {
|
||||
return (inputSize + chunkSize - 1) / chunkSize;
|
||||
}
|
||||
|
||||
private static void setUnsignedInt32LittleEngian(int value, byte[] result, int offset) {
|
||||
result[offset] = (byte) (value & 0xff);
|
||||
result[offset + 1] = (byte) ((value >> 8) & 0xff);
|
||||
result[offset + 2] = (byte) ((value >> 16) & 0xff);
|
||||
result[offset + 3] = (byte) ((value >> 24) & 0xff);
|
||||
}
|
||||
|
||||
private static byte[] generateApkSigningBlock(
|
||||
List<SignerConfig> signerConfigs,
|
||||
Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
|
||||
byte[] apkSignatureSchemeV2Block =
|
||||
generateApkSignatureSchemeV2Block(signerConfigs, contentDigests);
|
||||
return generateApkSigningBlock(apkSignatureSchemeV2Block);
|
||||
}
|
||||
|
||||
private static byte[] generateApkSigningBlock(byte[] apkSignatureSchemeV2Block) {
|
||||
// FORMAT:
|
||||
// uint64: size (excluding this field)
|
||||
// repeated ID-value pairs:
|
||||
// uint64: size (excluding this field)
|
||||
// uint32: ID
|
||||
// (size - 4) bytes: value
|
||||
// uint64: size (same as the one above)
|
||||
// uint128: magic
|
||||
|
||||
int resultSize =
|
||||
8 // size
|
||||
+ 8 + 4 + apkSignatureSchemeV2Block.length // v2Block as ID-value pair
|
||||
+ 8 // size
|
||||
+ 16 // magic
|
||||
;
|
||||
ByteBuffer result = ByteBuffer.allocate(resultSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
long blockSizeFieldValue = resultSize - 8;
|
||||
result.putLong(blockSizeFieldValue);
|
||||
|
||||
long pairSizeFieldValue = 4 + apkSignatureSchemeV2Block.length;
|
||||
result.putLong(pairSizeFieldValue);
|
||||
result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
|
||||
result.put(apkSignatureSchemeV2Block);
|
||||
|
||||
result.putLong(blockSizeFieldValue);
|
||||
result.put(APK_SIGNING_BLOCK_MAGIC);
|
||||
|
||||
return result.array();
|
||||
}
|
||||
|
||||
private static byte[] generateApkSignatureSchemeV2Block(
|
||||
List<SignerConfig> signerConfigs,
|
||||
Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
|
||||
// FORMAT:
|
||||
// * length-prefixed sequence of length-prefixed signer blocks.
|
||||
|
||||
List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
|
||||
int signerNumber = 0;
|
||||
for (SignerConfig signerConfig : signerConfigs) {
|
||||
signerNumber++;
|
||||
byte[] signerBlock;
|
||||
try {
|
||||
signerBlock = generateSignerBlock(signerConfig, contentDigests);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
|
||||
} catch (SignatureException e) {
|
||||
throw new SignatureException("Signer #" + signerNumber + " failed", e);
|
||||
}
|
||||
signerBlocks.add(signerBlock);
|
||||
}
|
||||
|
||||
return encodeAsSequenceOfLengthPrefixedElements(
|
||||
new byte[][] {
|
||||
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
|
||||
});
|
||||
}
|
||||
|
||||
private static byte[] generateSignerBlock(
|
||||
SignerConfig signerConfig,
|
||||
Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
|
||||
if (signerConfig.certificates.isEmpty()) {
|
||||
throw new SignatureException("No certificates configured for signer");
|
||||
}
|
||||
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
||||
|
||||
byte[] encodedPublicKey = encodePublicKey(publicKey);
|
||||
|
||||
V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData();
|
||||
try {
|
||||
signedData.certificates = encodeCertificates(signerConfig.certificates);
|
||||
} catch (CertificateEncodingException e) {
|
||||
throw new SignatureException("Failed to encode certificates", e);
|
||||
}
|
||||
|
||||
List<Pair<Integer, byte[]>> digests =
|
||||
new ArrayList<>(signerConfig.signatureAlgorithms.size());
|
||||
for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
|
||||
int contentDigestAlgorithm =
|
||||
getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm);
|
||||
byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
|
||||
if (contentDigest == null) {
|
||||
throw new RuntimeException(
|
||||
getContentDigestAlgorithmJcaDigestAlgorithm(contentDigestAlgorithm)
|
||||
+ " content digest for "
|
||||
+ getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithm)
|
||||
+ " not computed");
|
||||
}
|
||||
digests.add(Pair.create(signatureAlgorithm, 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<>();
|
||||
for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
|
||||
Pair<String, ? extends AlgorithmParameterSpec> signatureParams =
|
||||
getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithm);
|
||||
String jcaSignatureAlgorithm = signatureParams.getFirst();
|
||||
AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureParams.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 sign using " + jcaSignatureAlgorithm, e);
|
||||
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
|
||||
| SignatureException e) {
|
||||
throw new SignatureException("Failed 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 (NoSuchAlgorithmException | InvalidAlgorithmParameterException
|
||||
| SignatureException e) {
|
||||
throw new SignatureException("Failed to verify generated " + jcaSignatureAlgorithm
|
||||
+ " signature using public key from certificate", e);
|
||||
}
|
||||
|
||||
signer.signatures.add(Pair.create(signatureAlgorithm, signatureBytes));
|
||||
}
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed signed data
|
||||
// * length-prefixed sequence of length-prefixed signatures:
|
||||
// * uint32: signature algorithm ID
|
||||
// * length-prefixed bytes: signature of signed data
|
||||
// * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
|
||||
return encodeAsSequenceOfLengthPrefixedElements(
|
||||
new byte[][] {
|
||||
signer.signedData,
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
signer.signatures),
|
||||
signer.publicKey,
|
||||
});
|
||||
}
|
||||
|
||||
private static final class V2SignatureSchemeBlock {
|
||||
private static final class Signer {
|
||||
public byte[] signedData;
|
||||
public List<Pair<Integer, byte[]>> signatures;
|
||||
public byte[] publicKey;
|
||||
}
|
||||
|
||||
private static final class SignedData {
|
||||
public List<Pair<Integer, byte[]>> digests;
|
||||
public List<byte[]> certificates;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] encodePublicKey(PublicKey publicKey) throws InvalidKeyException {
|
||||
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 (NoSuchAlgorithmException | 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;
|
||||
}
|
||||
|
||||
public static List<byte[]> encodeCertificates(List<X509Certificate> certificates)
|
||||
throws CertificateEncodingException {
|
||||
List<byte[]> result = new ArrayList<>();
|
||||
for (X509Certificate certificate : certificates) {
|
||||
result.add(certificate.getEncoded());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] encodeAsSequenceOfLengthPrefixedElements(List<byte[]> sequence) {
|
||||
return encodeAsSequenceOfLengthPrefixedElements(
|
||||
sequence.toArray(new byte[sequence.size()][]));
|
||||
}
|
||||
|
||||
private static byte[] encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence) {
|
||||
int payloadSize = 0;
|
||||
for (byte[] element : sequence) {
|
||||
payloadSize += 4 + element.length;
|
||||
}
|
||||
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
for (byte[] element : sequence) {
|
||||
result.putInt(element.length);
|
||||
result.put(element);
|
||||
}
|
||||
return result.array();
|
||||
}
|
||||
|
||||
private static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
List<Pair<Integer, byte[]>> sequence) {
|
||||
int resultSize = 0;
|
||||
for (Pair<Integer, byte[]> element : sequence) {
|
||||
resultSize += 12 + element.getSecond().length;
|
||||
}
|
||||
ByteBuffer result = ByteBuffer.allocate(resultSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
for (Pair<Integer, byte[]> element : sequence) {
|
||||
byte[] second = element.getSecond();
|
||||
result.putInt(8 + second.length);
|
||||
result.putInt(element.getFirst());
|
||||
result.putInt(second.length);
|
||||
result.put(second);
|
||||
}
|
||||
return result.array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Relative <em>get</em> method for reading {@code size} number of bytes from the current
|
||||
* position of this buffer.
|
||||
*
|
||||
* <p>This method reads the next {@code size} bytes at this buffer's current position,
|
||||
* returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
|
||||
* {@code size}, byte order set to this buffer's byte order; and then increments the position by
|
||||
* {@code size}.
|
||||
*/
|
||||
private static ByteBuffer getByteBuffer(ByteBuffer source, int size) {
|
||||
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 Pair<String, ? extends AlgorithmParameterSpec>
|
||||
getSignatureAlgorithmJcaSignatureAlgorithm(int sigAlgorithm) {
|
||||
switch (sigAlgorithm) {
|
||||
case SIGNATURE_RSA_PSS_WITH_SHA256:
|
||||
return Pair.create(
|
||||
"SHA256withRSA/PSS",
|
||||
new PSSParameterSpec(
|
||||
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1));
|
||||
case SIGNATURE_RSA_PSS_WITH_SHA512:
|
||||
return Pair.create(
|
||||
"SHA512withRSA/PSS",
|
||||
new PSSParameterSpec(
|
||||
"SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1));
|
||||
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
|
||||
return Pair.create("SHA256withRSA", null);
|
||||
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
|
||||
return Pair.create("SHA512withRSA", null);
|
||||
case SIGNATURE_ECDSA_WITH_SHA256:
|
||||
return Pair.create("SHA256withECDSA", null);
|
||||
case SIGNATURE_ECDSA_WITH_SHA512:
|
||||
return Pair.create("SHA512withECDSA", null);
|
||||
case SIGNATURE_DSA_WITH_SHA256:
|
||||
return Pair.create("SHA256withDSA", null);
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown signature algorithm: 0x"
|
||||
+ Long.toHexString(sigAlgorithm & 0xffffffff));
|
||||
}
|
||||
}
|
||||
|
||||
private static int getSignatureAlgorithmContentDigestAlgorithm(int sigAlgorithm) {
|
||||
switch (sigAlgorithm) {
|
||||
case SIGNATURE_RSA_PSS_WITH_SHA256:
|
||||
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
|
||||
case SIGNATURE_ECDSA_WITH_SHA256:
|
||||
case SIGNATURE_DSA_WITH_SHA256:
|
||||
return CONTENT_DIGEST_CHUNKED_SHA256;
|
||||
case SIGNATURE_RSA_PSS_WITH_SHA512:
|
||||
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
|
||||
case SIGNATURE_ECDSA_WITH_SHA512:
|
||||
return CONTENT_DIGEST_CHUNKED_SHA512;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown signature algorithm: 0x"
|
||||
+ Long.toHexString(sigAlgorithm & 0xffffffff));
|
||||
}
|
||||
}
|
||||
|
||||
private static String getContentDigestAlgorithmJcaDigestAlgorithm(int digestAlgorithm) {
|
||||
switch (digestAlgorithm) {
|
||||
case CONTENT_DIGEST_CHUNKED_SHA256:
|
||||
return "SHA-256";
|
||||
case CONTENT_DIGEST_CHUNKED_SHA512:
|
||||
return "SHA-512";
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown content digest algorthm: " + digestAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getContentDigestAlgorithmOutputSizeBytes(int digestAlgorithm) {
|
||||
switch (digestAlgorithm) {
|
||||
case CONTENT_DIGEST_CHUNKED_SHA256:
|
||||
return 256 / 8;
|
||||
case CONTENT_DIGEST_CHUNKED_SHA512:
|
||||
return 512 / 8;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown content digest algorthm: " + digestAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that APK file could not be parsed.
|
||||
*/
|
||||
public static class ApkParseException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ApkParseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ApkParseException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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.signapk;
|
||||
|
||||
/**
|
||||
* Pair of two elements.
|
||||
*/
|
||||
public final class Pair<A, B> {
|
||||
private final A mFirst;
|
||||
private final B mSecond;
|
||||
|
||||
private Pair(A first, B second) {
|
||||
mFirst = first;
|
||||
mSecond = second;
|
||||
}
|
||||
|
||||
public static <A, B> Pair<A, B> create(A first, B second) {
|
||||
return new Pair<A, B>(first, second);
|
||||
}
|
||||
|
||||
public A getFirst() {
|
||||
return mFirst;
|
||||
}
|
||||
|
||||
public B getSecond() {
|
||||
return mSecond;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode());
|
||||
result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
@SuppressWarnings("rawtypes")
|
||||
Pair other = (Pair) obj;
|
||||
if (mFirst == null) {
|
||||
if (other.mFirst != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!mFirst.equals(other.mFirst)) {
|
||||
return false;
|
||||
}
|
||||
if (mSecond == null) {
|
||||
if (other.mSecond != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!mSecond.equals(other.mSecond)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -23,7 +23,6 @@ import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
||||
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
||||
import org.bouncycastle.cms.CMSException;
|
||||
import org.bouncycastle.cms.CMSProcessableByteArray;
|
||||
import org.bouncycastle.cms.CMSSignedData;
|
||||
import org.bouncycastle.cms.CMSSignedDataGenerator;
|
||||
import org.bouncycastle.cms.CMSTypedData;
|
||||
@@ -33,9 +32,15 @@ import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.OperatorCreationException;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
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 java.io.Console;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -49,19 +54,14 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.Key;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Provider;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Security;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
@@ -71,17 +71,12 @@ import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
import java.util.TreeMap;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
@@ -113,11 +108,6 @@ import javax.crypto.spec.PBEKeySpec;
|
||||
* APK Signature Scheme v2.
|
||||
*/
|
||||
class SignApk {
|
||||
private static final String CERT_SF_NAME = "META-INF/CERT.SF";
|
||||
private static final String CERT_SIG_NAME = "META-INF/CERT.%s";
|
||||
private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
|
||||
private static final String CERT_SIG_MULTI_NAME = "META-INF/CERT%d.%s";
|
||||
|
||||
private static final String OTACERT_NAME = "META-INF/com/android/otacert";
|
||||
|
||||
/**
|
||||
@@ -137,36 +127,6 @@ class SignApk {
|
||||
private static final int USE_SHA1 = 1;
|
||||
private static final int USE_SHA256 = 2;
|
||||
|
||||
/** Digest algorithm used when signing the APK using APK Signature Scheme v2. */
|
||||
private static final String APK_SIG_SCHEME_V2_DIGEST_ALGORITHM = "SHA-256";
|
||||
|
||||
/**
|
||||
* Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used
|
||||
* for v1 signing (JAR signing) an APK using the private key corresponding to the provided
|
||||
* certificate.
|
||||
*
|
||||
* @param minSdkVersion minimum Android platform API Level supported by the APK (see
|
||||
* minSdkVersion attribute in AndroidManifest.xml). The higher the minSdkVersion, the
|
||||
* stronger hash may be used for signing the APK.
|
||||
*/
|
||||
private static int getV1DigestAlgorithmForApk(X509Certificate cert, int minSdkVersion) {
|
||||
String keyAlgorithm = cert.getPublicKey().getAlgorithm();
|
||||
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||
// RSA can be used only with SHA-1 prior to API Level 18.
|
||||
return (minSdkVersion < 18) ? USE_SHA1 : USE_SHA256;
|
||||
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||
// ECDSA cannot be used prior to API Level 18 at all. It can only be used with SHA-1
|
||||
// on API Levels 18, 19, and 20.
|
||||
if (minSdkVersion < 18) {
|
||||
throw new IllegalArgumentException(
|
||||
"ECDSA signatures only supported for minSdkVersion 18 and higher");
|
||||
}
|
||||
return (minSdkVersion < 21) ? USE_SHA1 : USE_SHA256;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used
|
||||
* for signing an OTA update package using the private key corresponding to the provided
|
||||
@@ -187,10 +147,10 @@ class SignApk {
|
||||
|
||||
/**
|
||||
* Returns the JCA {@link java.security.Signature} algorithm to be used for signing and OTA
|
||||
* or v1 signing an APK using the private key corresponding to the provided certificate and the
|
||||
* update package using the private key corresponding to the provided certificate and the
|
||||
* provided digest algorithm (see {@code USE_SHA1} and {@code USE_SHA256} constants).
|
||||
*/
|
||||
private static String getJcaSignatureAlgorithmForV1SigningOrOta(
|
||||
private static String getJcaSignatureAlgorithmForOta(
|
||||
X509Certificate cert, int hash) {
|
||||
String sigAlgDigestPrefix;
|
||||
switch (hash) {
|
||||
@@ -214,11 +174,6 @@ class SignApk {
|
||||
}
|
||||
}
|
||||
|
||||
/* Files matching this pattern are not copied to the output. */
|
||||
private static final Pattern STRIP_PATTERN =
|
||||
Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|("
|
||||
+ Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
|
||||
|
||||
private static X509Certificate readPublicKey(File file)
|
||||
throws IOException, GeneralSecurityException {
|
||||
FileInputStream input = new FileInputStream(file);
|
||||
@@ -323,89 +278,6 @@ class SignApk {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the hash(es) of every file to the manifest, creating it if
|
||||
* necessary.
|
||||
*/
|
||||
private static Manifest addDigestsToManifest(
|
||||
JarFile jar, Pattern ignoredFilenamePattern, int hashes)
|
||||
throws IOException, GeneralSecurityException {
|
||||
Manifest input = jar.getManifest();
|
||||
Manifest output = new Manifest();
|
||||
Attributes main = output.getMainAttributes();
|
||||
if (input != null) {
|
||||
main.putAll(input.getMainAttributes());
|
||||
} else {
|
||||
main.putValue("Manifest-Version", "1.0");
|
||||
main.putValue("Created-By", "1.0 (Android SignApk)");
|
||||
}
|
||||
|
||||
MessageDigest md_sha1 = null;
|
||||
MessageDigest md_sha256 = null;
|
||||
if ((hashes & USE_SHA1) != 0) {
|
||||
md_sha1 = MessageDigest.getInstance("SHA1");
|
||||
}
|
||||
if ((hashes & USE_SHA256) != 0) {
|
||||
md_sha256 = MessageDigest.getInstance("SHA256");
|
||||
}
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int num;
|
||||
|
||||
// We sort the input entries by name, and add them to the
|
||||
// output manifest in sorted order. We expect that the output
|
||||
// map will be deterministic.
|
||||
|
||||
TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
|
||||
|
||||
for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
|
||||
JarEntry entry = e.nextElement();
|
||||
byName.put(entry.getName(), entry);
|
||||
}
|
||||
|
||||
for (JarEntry entry: byName.values()) {
|
||||
String name = entry.getName();
|
||||
if (!entry.isDirectory()
|
||||
&& (ignoredFilenamePattern == null
|
||||
|| !ignoredFilenamePattern.matcher(name).matches())) {
|
||||
InputStream data = jar.getInputStream(entry);
|
||||
while ((num = data.read(buffer)) > 0) {
|
||||
if (md_sha1 != null) md_sha1.update(buffer, 0, num);
|
||||
if (md_sha256 != null) md_sha256.update(buffer, 0, num);
|
||||
}
|
||||
|
||||
Attributes attr = null;
|
||||
if (input != null) attr = input.getAttributes(name);
|
||||
attr = attr != null ? new Attributes(attr) : new Attributes();
|
||||
// Remove any previously computed digests from this entry's attributes.
|
||||
for (Iterator<Object> i = attr.keySet().iterator(); i.hasNext();) {
|
||||
Object key = i.next();
|
||||
if (!(key instanceof Attributes.Name)) {
|
||||
continue;
|
||||
}
|
||||
String attributeNameLowerCase =
|
||||
((Attributes.Name) key).toString().toLowerCase(Locale.US);
|
||||
if (attributeNameLowerCase.endsWith("-digest")) {
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
// Add SHA-1 digest if requested
|
||||
if (md_sha1 != null) {
|
||||
attr.putValue("SHA1-Digest",
|
||||
new String(Base64.encode(md_sha1.digest()), "ASCII"));
|
||||
}
|
||||
// Add SHA-256 digest if requested
|
||||
if (md_sha256 != null) {
|
||||
attr.putValue("SHA-256-Digest",
|
||||
new String(Base64.encode(md_sha256.digest()), "ASCII"));
|
||||
}
|
||||
output.getEntries().put(name, attr);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a copy of the public key to the archive; this should
|
||||
* exactly match one of the files in
|
||||
@@ -416,7 +288,7 @@ class SignApk {
|
||||
private static void addOtacert(JarOutputStream outputJar,
|
||||
File publicKeyFile,
|
||||
long timestamp)
|
||||
throws IOException, GeneralSecurityException {
|
||||
throws IOException {
|
||||
|
||||
JarEntry je = new JarEntry(OTACERT_NAME);
|
||||
je.setTime(timestamp);
|
||||
@@ -431,94 +303,6 @@ class SignApk {
|
||||
}
|
||||
|
||||
|
||||
/** Write to another stream and track how many bytes have been
|
||||
* written.
|
||||
*/
|
||||
private static class CountOutputStream extends FilterOutputStream {
|
||||
private int mCount;
|
||||
|
||||
public CountOutputStream(OutputStream out) {
|
||||
super(out);
|
||||
mCount = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
super.write(b);
|
||||
mCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
super.write(b, off, len);
|
||||
mCount += len;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return mCount;
|
||||
}
|
||||
}
|
||||
|
||||
/** Write a .SF file with a digest of the specified manifest. */
|
||||
private static void writeSignatureFile(Manifest manifest, OutputStream out,
|
||||
int hash, boolean additionallySignedUsingAnApkSignatureScheme)
|
||||
throws IOException, GeneralSecurityException {
|
||||
Manifest sf = new Manifest();
|
||||
Attributes main = sf.getMainAttributes();
|
||||
main.putValue("Signature-Version", "1.0");
|
||||
main.putValue("Created-By", "1.0 (Android SignApk)");
|
||||
if (additionallySignedUsingAnApkSignatureScheme) {
|
||||
// Add APK Signature Scheme v2 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.
|
||||
main.putValue(
|
||||
ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME,
|
||||
ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE);
|
||||
}
|
||||
|
||||
MessageDigest md = MessageDigest.getInstance(
|
||||
hash == USE_SHA256 ? "SHA256" : "SHA1");
|
||||
PrintStream print = new PrintStream(
|
||||
new DigestOutputStream(new ByteArrayOutputStream(), md),
|
||||
true, "UTF-8");
|
||||
|
||||
// Digest of the entire manifest
|
||||
manifest.write(print);
|
||||
print.flush();
|
||||
main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
|
||||
new String(Base64.encode(md.digest()), "ASCII"));
|
||||
|
||||
Map<String, Attributes> entries = manifest.getEntries();
|
||||
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
|
||||
// Digest of the manifest stanza for this entry.
|
||||
print.print("Name: " + entry.getKey() + "\r\n");
|
||||
for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
|
||||
print.print(att.getKey() + ": " + att.getValue() + "\r\n");
|
||||
}
|
||||
print.print("\r\n");
|
||||
print.flush();
|
||||
|
||||
Attributes sfAttr = new Attributes();
|
||||
sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest",
|
||||
new String(Base64.encode(md.digest()), "ASCII"));
|
||||
sf.getEntries().put(entry.getKey(), sfAttr);
|
||||
}
|
||||
|
||||
CountOutputStream cout = new CountOutputStream(out);
|
||||
sf.write(cout);
|
||||
|
||||
// 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 ((cout.size() % 1024) == 0) {
|
||||
cout.write('\r');
|
||||
cout.write('\n');
|
||||
}
|
||||
}
|
||||
|
||||
/** Sign data and write the digital signature to 'out'. */
|
||||
private static void writeSignatureBlock(
|
||||
CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash,
|
||||
@@ -534,7 +318,7 @@ class SignApk {
|
||||
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
||||
ContentSigner signer =
|
||||
new JcaContentSignerBuilder(
|
||||
getJcaSignatureAlgorithmForV1SigningOrOta(publicKey, hash))
|
||||
getJcaSignatureAlgorithmForOta(publicKey, hash))
|
||||
.build(privateKey);
|
||||
gen.addSignerInfoGenerator(
|
||||
new JcaSignerInfoGeneratorBuilder(
|
||||
@@ -551,13 +335,40 @@ class SignApk {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds ZIP entries which represent the v1 signature (JAR signature scheme).
|
||||
*/
|
||||
private static void addV1Signature(
|
||||
ApkSignerEngine apkSigner,
|
||||
ApkSignerEngine.OutputJarSignatureRequest v1Signature,
|
||||
JarOutputStream out,
|
||||
long timestamp) throws IOException {
|
||||
for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry
|
||||
: v1Signature.getAdditionalJarEntries()) {
|
||||
String entryName = entry.getName();
|
||||
JarEntry outEntry = new JarEntry(entryName);
|
||||
outEntry.setTime(timestamp);
|
||||
out.putNextEntry(outEntry);
|
||||
byte[] entryData = entry.getData();
|
||||
out.write(entryData);
|
||||
ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
|
||||
apkSigner.outputJarEntry(entryName);
|
||||
if (inspectEntryRequest != null) {
|
||||
inspectEntryRequest.getDataSink().consume(entryData, 0, entryData.length);
|
||||
inspectEntryRequest.done();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy all JAR entries from input to output. We set the modification times in the output to a
|
||||
* fixed time, so as to reduce variation in the output file and make incremental OTAs more
|
||||
* efficient.
|
||||
*/
|
||||
private static void copyFiles(JarFile in,
|
||||
private static void copyFiles(
|
||||
JarFile in,
|
||||
Pattern ignoredFilenamePattern,
|
||||
ApkSignerEngine apkSigner,
|
||||
JarOutputStream out,
|
||||
long timestamp,
|
||||
int defaultAlignment) throws IOException {
|
||||
@@ -589,12 +400,21 @@ class SignApk {
|
||||
// the start of the file and makes it easier to do alignment
|
||||
// on them (since only stored entries are aligned).
|
||||
|
||||
List<String> remainingNames = new ArrayList<>(names.size());
|
||||
for (String name : names) {
|
||||
JarEntry inEntry = in.getJarEntry(name);
|
||||
JarEntry outEntry = null;
|
||||
if (inEntry.getMethod() != JarEntry.STORED) continue;
|
||||
if (inEntry.getMethod() != JarEntry.STORED) {
|
||||
// Defer outputting this entry until we're ready to output compressed entries.
|
||||
remainingNames.add(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Preserve the STORED method of the input entry.
|
||||
outEntry = new JarEntry(inEntry);
|
||||
JarEntry outEntry = new JarEntry(inEntry);
|
||||
outEntry.setTime(timestamp);
|
||||
// Discard comment and extra fields of this entry to
|
||||
// simplify alignment logic below and for consistency with
|
||||
@@ -638,33 +458,97 @@ class SignApk {
|
||||
offset += extra.length;
|
||||
|
||||
out.putNextEntry(outEntry);
|
||||
ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
|
||||
(apkSigner != null) ? apkSigner.outputJarEntry(name) : null;
|
||||
DataSink entryDataSink =
|
||||
(inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null;
|
||||
|
||||
InputStream data = in.getInputStream(inEntry);
|
||||
while ((num = data.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, num);
|
||||
offset += num;
|
||||
try (InputStream data = in.getInputStream(inEntry)) {
|
||||
while ((num = data.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, num);
|
||||
if (entryDataSink != null) {
|
||||
entryDataSink.consume(buffer, 0, num);
|
||||
}
|
||||
offset += num;
|
||||
}
|
||||
}
|
||||
out.flush();
|
||||
if (inspectEntryRequest != null) {
|
||||
inspectEntryRequest.done();
|
||||
}
|
||||
}
|
||||
|
||||
// Copy all the non-STORED entries. We don't attempt to
|
||||
// maintain the 'offset' variable past this point; we don't do
|
||||
// alignment on these entries.
|
||||
|
||||
for (String name : names) {
|
||||
for (String name : remainingNames) {
|
||||
JarEntry inEntry = in.getJarEntry(name);
|
||||
JarEntry outEntry = null;
|
||||
if (inEntry.getMethod() == JarEntry.STORED) continue;
|
||||
if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a new entry so that the compressed len is recomputed.
|
||||
outEntry = new JarEntry(name);
|
||||
JarEntry outEntry = new JarEntry(name);
|
||||
outEntry.setTime(timestamp);
|
||||
out.putNextEntry(outEntry);
|
||||
ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
|
||||
(apkSigner != null) ? apkSigner.outputJarEntry(name) : null;
|
||||
DataSink entryDataSink =
|
||||
(inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null;
|
||||
|
||||
InputStream data = in.getInputStream(inEntry);
|
||||
while ((num = data.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, num);
|
||||
if (entryDataSink != null) {
|
||||
entryDataSink.consume(buffer, 0, num);
|
||||
}
|
||||
}
|
||||
out.flush();
|
||||
if (inspectEntryRequest != null) {
|
||||
inspectEntryRequest.done();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldOutputApkEntry(
|
||||
ApkSignerEngine apkSigner, JarFile inFile, JarEntry inEntry, byte[] tmpbuf)
|
||||
throws IOException {
|
||||
if (apkSigner == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ApkSignerEngine.InputJarEntryInstructions instructions =
|
||||
apkSigner.inputJarEntry(inEntry.getName());
|
||||
ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
|
||||
instructions.getInspectJarEntryRequest();
|
||||
if (inspectEntryRequest != null) {
|
||||
provideJarEntry(inFile, inEntry, inspectEntryRequest, tmpbuf);
|
||||
}
|
||||
switch (instructions.getOutputPolicy()) {
|
||||
case OUTPUT:
|
||||
return true;
|
||||
case SKIP:
|
||||
case OUTPUT_BY_ENGINE:
|
||||
return false;
|
||||
default:
|
||||
throw new RuntimeException(
|
||||
"Unsupported output policy: " + instructions.getOutputPolicy());
|
||||
}
|
||||
}
|
||||
|
||||
private static void provideJarEntry(
|
||||
JarFile jarFile,
|
||||
JarEntry jarEntry,
|
||||
ApkSignerEngine.InspectJarEntryRequest request,
|
||||
byte[] tmpbuf) throws IOException {
|
||||
DataSink dataSink = request.getDataSink();
|
||||
try (InputStream in = jarFile.getInputStream(jarEntry)) {
|
||||
int chunkSize;
|
||||
while ((chunkSize = in.read(tmpbuf)) > 0) {
|
||||
dataSink.consume(tmpbuf, 0, chunkSize);
|
||||
}
|
||||
request.done();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,6 +640,11 @@ class SignApk {
|
||||
private final ASN1ObjectIdentifier type;
|
||||
private WholeFileSignerOutputStream signer;
|
||||
|
||||
// Files matching this pattern are not copied to the output.
|
||||
private static final Pattern STRIP_PATTERN =
|
||||
Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|("
|
||||
+ Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
|
||||
|
||||
public CMSSigner(JarFile inputJar, File publicKeyFile,
|
||||
X509Certificate publicKey, PrivateKey privateKey, int hash,
|
||||
long timestamp, OutputStream outputStream) {
|
||||
@@ -789,7 +678,7 @@ class SignApk {
|
||||
signer = new WholeFileSignerOutputStream(out, outputStream);
|
||||
JarOutputStream outputJar = new JarOutputStream(signer);
|
||||
|
||||
copyFiles(inputJar, STRIP_PATTERN, outputJar, timestamp, 0);
|
||||
copyFiles(inputJar, STRIP_PATTERN, null, outputJar, timestamp, 0);
|
||||
addOtacert(outputJar, publicKeyFile, timestamp);
|
||||
|
||||
signer.notifyClosing();
|
||||
@@ -883,47 +772,6 @@ class SignApk {
|
||||
temp.writeTo(outputStream);
|
||||
}
|
||||
|
||||
private static void signFile(Manifest manifest,
|
||||
X509Certificate[] publicKey, PrivateKey[] privateKey, int[] hash,
|
||||
long timestamp,
|
||||
boolean additionallySignedUsingAnApkSignatureScheme,
|
||||
JarOutputStream outputJar)
|
||||
throws Exception {
|
||||
|
||||
// MANIFEST.MF
|
||||
JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
|
||||
je.setTime(timestamp);
|
||||
outputJar.putNextEntry(je);
|
||||
manifest.write(outputJar);
|
||||
|
||||
int numKeys = publicKey.length;
|
||||
for (int k = 0; k < numKeys; ++k) {
|
||||
// CERT.SF / CERT#.SF
|
||||
je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
|
||||
(String.format(CERT_SF_MULTI_NAME, k)));
|
||||
je.setTime(timestamp);
|
||||
outputJar.putNextEntry(je);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
writeSignatureFile(
|
||||
manifest,
|
||||
baos,
|
||||
hash[k],
|
||||
additionallySignedUsingAnApkSignatureScheme);
|
||||
byte[] signedData = baos.toByteArray();
|
||||
outputJar.write(signedData);
|
||||
|
||||
// CERT.{EC,RSA} / CERT#.{EC,RSA}
|
||||
final String keyType = publicKey[k].getPublicKey().getAlgorithm();
|
||||
je = new JarEntry(numKeys == 1 ?
|
||||
(String.format(CERT_SIG_NAME, keyType)) :
|
||||
(String.format(CERT_SIG_MULTI_NAME, k, keyType)));
|
||||
je.setTime(timestamp);
|
||||
outputJar.putNextEntry(je);
|
||||
writeSignatureBlock(new CMSProcessableByteArray(signedData),
|
||||
publicKey[k], privateKey[k], hash[k], outputJar);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to load a JSE Provider by class name. This is for custom PrivateKey
|
||||
* types that might be stored in PKCS#11-like storage.
|
||||
@@ -976,81 +824,68 @@ class SignApk {
|
||||
Security.insertProviderAt((Provider) o, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided lists of private keys, their X.509 certificates, and digest algorithms
|
||||
* into a list of APK Signature Scheme v2 {@code SignerConfig} instances.
|
||||
*/
|
||||
public static List<ApkSignerV2.SignerConfig> createV2SignerConfigs(
|
||||
PrivateKey[] privateKeys, X509Certificate[] certificates, String[] digestAlgorithms)
|
||||
throws InvalidKeyException {
|
||||
private static List<DefaultApkSignerEngine.SignerConfig> createSignerConfigs(
|
||||
PrivateKey[] privateKeys, X509Certificate[] certificates) {
|
||||
if (privateKeys.length != certificates.length) {
|
||||
throw new IllegalArgumentException(
|
||||
"The number of private keys must match the number of certificates: "
|
||||
+ privateKeys.length + " vs" + certificates.length);
|
||||
}
|
||||
List<ApkSignerV2.SignerConfig> result = new ArrayList<>(privateKeys.length);
|
||||
List<DefaultApkSignerEngine.SignerConfig> signerConfigs = new ArrayList<>();
|
||||
String signerNameFormat = (privateKeys.length == 1) ? "CERT" : "CERT%s";
|
||||
for (int i = 0; i < privateKeys.length; i++) {
|
||||
PrivateKey privateKey = privateKeys[i];
|
||||
X509Certificate certificate = certificates[i];
|
||||
PublicKey publicKey = certificate.getPublicKey();
|
||||
String keyAlgorithm = privateKey.getAlgorithm();
|
||||
if (!keyAlgorithm.equalsIgnoreCase(publicKey.getAlgorithm())) {
|
||||
throw new InvalidKeyException(
|
||||
"Key algorithm of private key #" + (i + 1) + " does not match key"
|
||||
+ " algorithm of public key #" + (i + 1) + ": " + keyAlgorithm
|
||||
+ " vs " + publicKey.getAlgorithm());
|
||||
}
|
||||
ApkSignerV2.SignerConfig signerConfig = new ApkSignerV2.SignerConfig();
|
||||
signerConfig.privateKey = privateKey;
|
||||
signerConfig.certificates = Collections.singletonList(certificate);
|
||||
List<Integer> signatureAlgorithms = new ArrayList<>(digestAlgorithms.length);
|
||||
for (String digestAlgorithm : digestAlgorithms) {
|
||||
try {
|
||||
signatureAlgorithms.add(
|
||||
getV2SignatureAlgorithm(keyAlgorithm, digestAlgorithm));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new InvalidKeyException(
|
||||
"Unsupported key and digest algorithm combination for signer #"
|
||||
+ (i + 1),
|
||||
e);
|
||||
}
|
||||
}
|
||||
signerConfig.signatureAlgorithms = signatureAlgorithms;
|
||||
result.add(signerConfig);
|
||||
String signerName = String.format(Locale.US, signerNameFormat, (i + 1));
|
||||
DefaultApkSignerEngine.SignerConfig signerConfig =
|
||||
new DefaultApkSignerEngine.SignerConfig.Builder(
|
||||
signerName,
|
||||
privateKeys[i],
|
||||
Collections.singletonList(certificates[i]))
|
||||
.build();
|
||||
signerConfigs.add(signerConfig);
|
||||
}
|
||||
return result;
|
||||
return signerConfigs;
|
||||
}
|
||||
|
||||
private static int getV2SignatureAlgorithm(String keyAlgorithm, String digestAlgorithm) {
|
||||
if ("SHA-256".equalsIgnoreCase(digestAlgorithm)) {
|
||||
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).
|
||||
return ApkSignerV2.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256;
|
||||
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||
return ApkSignerV2.SIGNATURE_ECDSA_WITH_SHA256;
|
||||
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||
return ApkSignerV2.SIGNATURE_DSA_WITH_SHA256;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
|
||||
}
|
||||
} else if ("SHA-512".equalsIgnoreCase(digestAlgorithm)) {
|
||||
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).
|
||||
return ApkSignerV2.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512;
|
||||
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||
return ApkSignerV2.SIGNATURE_ECDSA_WITH_SHA512;
|
||||
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||
throw new IllegalArgumentException("SHA-512 is not supported with DSA");
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm);
|
||||
private static class ZipSections {
|
||||
ByteBuffer beforeCentralDir;
|
||||
ByteBuffer centralDir;
|
||||
ByteBuffer eocd;
|
||||
}
|
||||
|
||||
private static ZipSections findMainZipSections(ByteBuffer apk)
|
||||
throws IOException, ZipFormatException {
|
||||
apk.slice();
|
||||
ApkUtils.ZipSections sections = ApkUtils.findZipSections(DataSources.asDataSource(apk));
|
||||
long centralDirStartOffset = sections.getZipCentralDirectoryOffset();
|
||||
long centralDirSizeBytes = sections.getZipCentralDirectorySizeBytes();
|
||||
long centralDirEndOffset = centralDirStartOffset + centralDirSizeBytes;
|
||||
long eocdStartOffset = sections.getZipEndOfCentralDirectoryOffset();
|
||||
if (centralDirEndOffset != eocdStartOffset) {
|
||||
throw new ZipFormatException(
|
||||
"ZIP Central Directory is not immediately followed by End of Central Directory"
|
||||
+ ". CD end: " + centralDirEndOffset
|
||||
+ ", EoCD start: " + eocdStartOffset);
|
||||
}
|
||||
apk.position(0);
|
||||
apk.limit((int) centralDirStartOffset);
|
||||
ByteBuffer beforeCentralDir = apk.slice();
|
||||
|
||||
apk.position((int) centralDirStartOffset);
|
||||
apk.limit((int) centralDirEndOffset);
|
||||
ByteBuffer centralDir = apk.slice();
|
||||
|
||||
apk.position((int) eocdStartOffset);
|
||||
apk.limit(apk.capacity());
|
||||
ByteBuffer eocd = apk.slice();
|
||||
|
||||
apk.position(0);
|
||||
apk.limit(apk.capacity());
|
||||
|
||||
ZipSections result = new ZipSections();
|
||||
result.beforeCentralDir = beforeCentralDir;
|
||||
result.centralDir = centralDir;
|
||||
result.eocd = eocd;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void usage() {
|
||||
@@ -1167,57 +1002,80 @@ class SignApk {
|
||||
timestamp,
|
||||
outputFile);
|
||||
} else {
|
||||
// Generate, in memory, an APK signed using standard JAR Signature Scheme.
|
||||
ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
|
||||
JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf);
|
||||
// Use maximum compression for compressed entries because the APK lives forever on
|
||||
// the system partition.
|
||||
outputJar.setLevel(9);
|
||||
int v1DigestAlgorithmBitSet = 0;
|
||||
int[] v1DigestAlgorithm = new int[numKeys];
|
||||
for (int i = 0; i < numKeys; ++i) {
|
||||
v1DigestAlgorithm[i] = getV1DigestAlgorithmForApk(publicKey[i], minSdkVersion);
|
||||
v1DigestAlgorithmBitSet |= v1DigestAlgorithm[i];
|
||||
}
|
||||
Manifest manifest =
|
||||
addDigestsToManifest(inputJar, STRIP_PATTERN, v1DigestAlgorithmBitSet);
|
||||
copyFiles(inputJar, STRIP_PATTERN, outputJar, timestamp, alignment);
|
||||
signFile(
|
||||
manifest,
|
||||
publicKey, privateKey, v1DigestAlgorithm,
|
||||
timestamp, signUsingApkSignatureSchemeV2,
|
||||
outputJar);
|
||||
outputJar.close();
|
||||
ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
|
||||
v1SignedApkBuf.reset();
|
||||
try (ApkSignerEngine apkSigner =
|
||||
new DefaultApkSignerEngine.Builder(
|
||||
createSignerConfigs(privateKey, publicKey), minSdkVersion)
|
||||
.setV1SigningEnabled(true)
|
||||
.setV2SigningEnabled(signUsingApkSignatureSchemeV2)
|
||||
.setOtherSignersSignaturesPreserved(false)
|
||||
.build()) {
|
||||
// We don't preserve the input APK's APK Signing Block (which contains v2
|
||||
// signatures)
|
||||
apkSigner.inputApkSigningBlock(null);
|
||||
|
||||
ByteBuffer[] outputChunks;
|
||||
if (signUsingApkSignatureSchemeV2) {
|
||||
// Additionally sign the APK using the APK Signature Scheme v2.
|
||||
ByteBuffer apkContents = v1SignedApk;
|
||||
List<ApkSignerV2.SignerConfig> signerConfigs =
|
||||
createV2SignerConfigs(
|
||||
privateKey,
|
||||
publicKey,
|
||||
new String[] {APK_SIG_SCHEME_V2_DIGEST_ALGORITHM});
|
||||
outputChunks = ApkSignerV2.sign(apkContents, signerConfigs);
|
||||
} else {
|
||||
// Output the JAR-signed APK as is.
|
||||
outputChunks = new ByteBuffer[] {v1SignedApk};
|
||||
// Build the output APK in memory, by copying input APK's ZIP entries across
|
||||
// and then signing the output APK.
|
||||
ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
|
||||
JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf);
|
||||
// Use maximum compression for compressed entries because the APK lives forever
|
||||
// on the system partition.
|
||||
outputJar.setLevel(9);
|
||||
copyFiles(inputJar, null, apkSigner, outputJar, timestamp, alignment);
|
||||
ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest =
|
||||
apkSigner.outputJarEntries();
|
||||
if (addV1SignatureRequest != null) {
|
||||
addV1Signature(apkSigner, addV1SignatureRequest, outputJar, timestamp);
|
||||
addV1SignatureRequest.done();
|
||||
}
|
||||
outputJar.close();
|
||||
ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
|
||||
v1SignedApkBuf.reset();
|
||||
ByteBuffer[] outputChunks = new ByteBuffer[] {v1SignedApk};
|
||||
|
||||
ZipSections zipSections = findMainZipSections(v1SignedApk);
|
||||
ApkSignerEngine.OutputApkSigningBlockRequest addV2SignatureRequest =
|
||||
apkSigner.outputZipSections(
|
||||
DataSources.asDataSource(zipSections.beforeCentralDir),
|
||||
DataSources.asDataSource(zipSections.centralDir),
|
||||
DataSources.asDataSource(zipSections.eocd));
|
||||
if (addV2SignatureRequest != null) {
|
||||
// Need to insert the returned APK Signing Block before ZIP Central
|
||||
// Directory.
|
||||
byte[] apkSigningBlock = addV2SignatureRequest.getApkSigningBlock();
|
||||
// Because the APK Signing Block is inserted before the Central Directory,
|
||||
// we need to adjust accordingly the offset of Central Directory inside the
|
||||
// ZIP End of Central Directory (EoCD) record.
|
||||
ByteBuffer modifiedEocd = ByteBuffer.allocate(zipSections.eocd.remaining());
|
||||
modifiedEocd.put(zipSections.eocd);
|
||||
modifiedEocd.flip();
|
||||
modifiedEocd.order(ByteOrder.LITTLE_ENDIAN);
|
||||
ApkUtils.setZipEocdCentralDirectoryOffset(
|
||||
modifiedEocd,
|
||||
zipSections.beforeCentralDir.remaining() + apkSigningBlock.length);
|
||||
outputChunks =
|
||||
new ByteBuffer[] {
|
||||
zipSections.beforeCentralDir,
|
||||
ByteBuffer.wrap(apkSigningBlock),
|
||||
zipSections.centralDir,
|
||||
modifiedEocd};
|
||||
addV2SignatureRequest.done();
|
||||
}
|
||||
|
||||
// This assumes outputChunks are array-backed. To avoid this assumption, the
|
||||
// code could be rewritten to use FileChannel.
|
||||
for (ByteBuffer outputChunk : outputChunks) {
|
||||
outputFile.write(
|
||||
outputChunk.array(),
|
||||
outputChunk.arrayOffset() + outputChunk.position(),
|
||||
outputChunk.remaining());
|
||||
outputChunk.position(outputChunk.limit());
|
||||
}
|
||||
|
||||
outputFile.close();
|
||||
outputFile = null;
|
||||
apkSigner.outputDone();
|
||||
}
|
||||
|
||||
// This assumes outputChunks are array-backed. To avoid this assumption, the
|
||||
// code could be rewritten to use FileChannel.
|
||||
for (ByteBuffer outputChunk : outputChunks) {
|
||||
outputFile.write(
|
||||
outputChunk.array(),
|
||||
outputChunk.arrayOffset() + outputChunk.position(),
|
||||
outputChunk.remaining());
|
||||
outputChunk.position(outputChunk.limit());
|
||||
}
|
||||
|
||||
outputFile.close();
|
||||
outputFile = null;
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
@@ -1,162 +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.signapk;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* Assorted ZIP format helpers.
|
||||
*
|
||||
* <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte
|
||||
* order of these buffers is little-endian.
|
||||
*/
|
||||
public abstract class ZipUtils {
|
||||
private ZipUtils() {}
|
||||
|
||||
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_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;
|
||||
|
||||
/**
|
||||
* Returns the position at which ZIP End of Central Directory record starts in the provided
|
||||
* buffer or {@code -1} if the record is not present.
|
||||
*
|
||||
* <p>NOTE: Byte order of {@code zipContents} must be little-endian.
|
||||
*/
|
||||
public 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 buffer contains a ZIP64 End of Central Directory
|
||||
* Locator.
|
||||
*
|
||||
* <p>NOTE: Byte order of {@code zipContents} must be little-endian.
|
||||
*/
|
||||
public static final boolean isZip64EndOfCentralDirectoryLocatorPresent(
|
||||
ByteBuffer zipContents, int zipEndOfCentralDirectoryPosition) {
|
||||
assertByteOrderLittleEndian(zipContents);
|
||||
|
||||
// ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central
|
||||
// Directory Record.
|
||||
|
||||
int locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE;
|
||||
if (locatorPosition < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return zipContents.getInt(locatorPosition) == ZIP64_EOCD_LOCATOR_SIG;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset of the start of the ZIP Central Directory in the archive.
|
||||
*
|
||||
* <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
|
||||
*/
|
||||
public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) {
|
||||
assertByteOrderLittleEndian(zipEndOfCentralDirectory);
|
||||
return getUnsignedInt32(
|
||||
zipEndOfCentralDirectory,
|
||||
zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the offset of the start of the ZIP Central Directory in the archive.
|
||||
*
|
||||
* <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
|
||||
*/
|
||||
public static void setZipEocdCentralDirectoryOffset(
|
||||
ByteBuffer zipEndOfCentralDirectory, long offset) {
|
||||
assertByteOrderLittleEndian(zipEndOfCentralDirectory);
|
||||
setUnsignedInt32(
|
||||
zipEndOfCentralDirectory,
|
||||
zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET,
|
||||
offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size (in bytes) of the ZIP Central Directory.
|
||||
*
|
||||
* <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
|
||||
*/
|
||||
public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) {
|
||||
assertByteOrderLittleEndian(zipEndOfCentralDirectory);
|
||||
return getUnsignedInt32(
|
||||
zipEndOfCentralDirectory,
|
||||
zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET);
|
||||
}
|
||||
|
||||
private static void assertByteOrderLittleEndian(ByteBuffer buffer) {
|
||||
if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
|
||||
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
|
||||
}
|
||||
}
|
||||
|
||||
private static int getUnsignedInt16(ByteBuffer buffer, int offset) {
|
||||
return buffer.getShort(offset) & 0xffff;
|
||||
}
|
||||
|
||||
private static long getUnsignedInt32(ByteBuffer buffer, int offset) {
|
||||
return buffer.getInt(offset) & 0xffffffffL;
|
||||
}
|
||||
|
||||
private 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(buffer.position() + offset, (int) value);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user