mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-04-16 08:21:25 +00:00
Update to APK Signature Scheme v2
This commit is contained in:
parent
fe2388394d
commit
2e95d9f07e
772
app/signing/src/main/java/com/topjohnwu/signing/ApkSignerV2.java
Normal file
772
app/signing/src/main/java/com/topjohnwu/signing/ApkSignerV2.java
Normal file
@ -0,0 +1,772 @@
|
|||||||
|
package com.topjohnwu.signing;
|
||||||
|
|
||||||
|
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;
|
||||||
|
public static final int SIGNATURE_DSA_WITH_SHA512 = 0x0302;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@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 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);
|
||||||
|
case SIGNATURE_DSA_WITH_SHA512:
|
||||||
|
return Pair.create("SHA512withDSA", 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:
|
||||||
|
case SIGNATURE_DSA_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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pair of two elements.
|
||||||
|
*/
|
||||||
|
private static 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<>(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) {
|
||||||
|
return other.mSecond == null;
|
||||||
|
} else return mSecond.equals(other.mSecond);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,9 +2,7 @@ package com.topjohnwu.signing;
|
|||||||
|
|
||||||
import org.bouncycastle.asn1.ASN1Encoding;
|
import org.bouncycastle.asn1.ASN1Encoding;
|
||||||
import org.bouncycastle.asn1.ASN1InputStream;
|
import org.bouncycastle.asn1.ASN1InputStream;
|
||||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
|
||||||
import org.bouncycastle.asn1.ASN1OutputStream;
|
import org.bouncycastle.asn1.ASN1OutputStream;
|
||||||
import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
|
|
||||||
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
||||||
import org.bouncycastle.cms.CMSException;
|
import org.bouncycastle.cms.CMSException;
|
||||||
import org.bouncycastle.cms.CMSProcessableByteArray;
|
import org.bouncycastle.cms.CMSProcessableByteArray;
|
||||||
@ -12,34 +10,38 @@ import org.bouncycastle.cms.CMSSignedData;
|
|||||||
import org.bouncycastle.cms.CMSSignedDataGenerator;
|
import org.bouncycastle.cms.CMSSignedDataGenerator;
|
||||||
import org.bouncycastle.cms.CMSTypedData;
|
import org.bouncycastle.cms.CMSTypedData;
|
||||||
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
|
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
import org.bouncycastle.operator.ContentSigner;
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
import org.bouncycastle.operator.OperatorCreationException;
|
import org.bouncycastle.operator.OperatorCreationException;
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
||||||
import org.bouncycastle.util.encoders.Base64;
|
import org.bouncycastle.util.encoders.Base64;
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.FilterOutputStream;
|
import java.io.FilterOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
import java.io.RandomAccessFile;
|
import java.nio.ByteBuffer;
|
||||||
import java.security.DigestOutputStream;
|
import java.security.DigestOutputStream;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Security;
|
||||||
import java.security.cert.CertificateEncodingException;
|
import java.security.cert.CertificateEncodingException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.TimeZone;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
import java.util.jar.Attributes;
|
import java.util.jar.Attributes;
|
||||||
import java.util.jar.JarEntry;
|
import java.util.jar.JarEntry;
|
||||||
@ -49,72 +51,28 @@ import java.util.jar.Manifest;
|
|||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Modified from from AOSP
|
* Modified from from AOSP
|
||||||
* https://android.googlesource.com/platform/build/+/refs/heads/marshmallow-release/tools/signapk/SignApk.java
|
* https://android.googlesource.com/platform/build/+/refs/tags/android-7.1.2_r39/tools/signapk/src/com/android/signapk/SignApk.java
|
||||||
* */
|
* */
|
||||||
|
|
||||||
public class SignAPK {
|
|
||||||
|
|
||||||
|
public class SignApk {
|
||||||
private static final String CERT_SF_NAME = "META-INF/CERT.SF";
|
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_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";
|
||||||
|
|
||||||
// bitmasks for which hash algorithms we need the manifest to include.
|
// bitmasks for which hash algorithms we need the manifest to include.
|
||||||
private static final int USE_SHA1 = 1;
|
private static final int USE_SHA1 = 1;
|
||||||
private static final int USE_SHA256 = 2;
|
private static final int USE_SHA256 = 2;
|
||||||
|
|
||||||
public static void signAndAdjust(X509Certificate cert, PrivateKey key,
|
/**
|
||||||
JarMap input, OutputStream output) throws Exception {
|
* Digest algorithm used when signing the APK using APK Signature Scheme v2.
|
||||||
File temp1 = File.createTempFile("signAPK", null);
|
*/
|
||||||
File temp2 = File.createTempFile("signAPK", null);
|
private static final String APK_SIG_SCHEME_V2_DIGEST_ALGORITHM = "SHA-256";
|
||||||
|
// Files matching this pattern are not copied to the output.
|
||||||
try {
|
private static final Pattern stripPattern =
|
||||||
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(temp1))) {
|
Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
|
||||||
sign(cert, key, input, out, false);
|
Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
|
||||||
}
|
|
||||||
|
|
||||||
ZipAdjust.adjust(temp1, temp2);
|
|
||||||
|
|
||||||
try (JarMap map = JarMap.open(temp2, false)) {
|
|
||||||
sign(cert, key, map, output, true);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
temp1.delete();
|
|
||||||
temp2.delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void sign(X509Certificate cert, PrivateKey key,
|
|
||||||
JarMap input, OutputStream output) throws Exception {
|
|
||||||
sign(cert, key, input, output, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sign(X509Certificate cert, PrivateKey key, JarMap input,
|
|
||||||
OutputStream output, boolean wholeFile) throws Exception {
|
|
||||||
int hashes = 0;
|
|
||||||
hashes |= getDigestAlgorithm(cert);
|
|
||||||
|
|
||||||
// Set the ZIP file timestamp to the starting valid time
|
|
||||||
// of the 0th certificate plus one hour (to match what
|
|
||||||
// we've historically done).
|
|
||||||
long timestamp = cert.getNotBefore().getTime() + 3600L * 1000;
|
|
||||||
|
|
||||||
if (wholeFile) {
|
|
||||||
signWholeFile(input.getFile(), cert, key, output);
|
|
||||||
} else {
|
|
||||||
JarOutputStream outputJar = new JarOutputStream(output);
|
|
||||||
// For signing .apks, use the maximum compression to make
|
|
||||||
// them as small as possible (since they live forever on
|
|
||||||
// the system partition). For OTA packages, use the
|
|
||||||
// default compression level, which is much much faster
|
|
||||||
// and produces output that is only a tiny bit larger
|
|
||||||
// (~0.1% on full OTA packages I tested).
|
|
||||||
outputJar.setLevel(9);
|
|
||||||
Manifest manifest = addDigestsToManifest(input, hashes);
|
|
||||||
copyFiles(manifest, input, outputJar, timestamp, 4);
|
|
||||||
signFile(manifest, input, cert, key, outputJar);
|
|
||||||
outputJar.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return one of USE_SHA1 or USE_SHA256 according to the signature
|
* Return one of USE_SHA1 or USE_SHA256 according to the signature
|
||||||
@ -122,7 +80,7 @@ public class SignAPK {
|
|||||||
*/
|
*/
|
||||||
private static int getDigestAlgorithm(X509Certificate cert) {
|
private static int getDigestAlgorithm(X509Certificate cert) {
|
||||||
String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
|
String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
|
||||||
if (sigAlg.startsWith("SHA1WITHRSA") || sigAlg.startsWith("MD5WITHRSA")) {
|
if ("SHA1WITHRSA".equals(sigAlg) || "MD5WITHRSA".equals(sigAlg)) {
|
||||||
return USE_SHA1;
|
return USE_SHA1;
|
||||||
} else if (sigAlg.startsWith("SHA256WITH")) {
|
} else if (sigAlg.startsWith("SHA256WITH")) {
|
||||||
return USE_SHA256;
|
return USE_SHA256;
|
||||||
@ -132,9 +90,10 @@ public class SignAPK {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the expected signature algorithm for this key type. */
|
/**
|
||||||
|
* Returns the expected signature algorithm for this key type.
|
||||||
|
*/
|
||||||
private static String getSignatureAlgorithm(X509Certificate cert) {
|
private static String getSignatureAlgorithm(X509Certificate cert) {
|
||||||
String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
|
|
||||||
String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
|
String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
|
||||||
if ("RSA".equalsIgnoreCase(keyType)) {
|
if ("RSA".equalsIgnoreCase(keyType)) {
|
||||||
if (getDigestAlgorithm(cert) == USE_SHA256) {
|
if (getDigestAlgorithm(cert) == USE_SHA256) {
|
||||||
@ -149,11 +108,6 @@ public class SignAPK {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Files matching this pattern are not copied to the output.
|
|
||||||
private static Pattern stripPattern =
|
|
||||||
Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
|
|
||||||
Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the hash(es) of every file to the manifest, creating it if
|
* Add the hash(es) of every file to the manifest, creating it if
|
||||||
* necessary.
|
* necessary.
|
||||||
@ -169,6 +123,7 @@ public class SignAPK {
|
|||||||
main.putValue("Manifest-Version", "1.0");
|
main.putValue("Manifest-Version", "1.0");
|
||||||
main.putValue("Created-By", "1.0 (Android SignApk)");
|
main.putValue("Created-By", "1.0 (Android SignApk)");
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDigest md_sha1 = null;
|
MessageDigest md_sha1 = null;
|
||||||
MessageDigest md_sha256 = null;
|
MessageDigest md_sha256 = null;
|
||||||
if ((hashes & USE_SHA1) != 0) {
|
if ((hashes & USE_SHA1) != 0) {
|
||||||
@ -177,30 +132,51 @@ public class SignAPK {
|
|||||||
if ((hashes & USE_SHA256) != 0) {
|
if ((hashes & USE_SHA256) != 0) {
|
||||||
md_sha256 = MessageDigest.getInstance("SHA256");
|
md_sha256 = MessageDigest.getInstance("SHA256");
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] buffer = new byte[4096];
|
byte[] buffer = new byte[4096];
|
||||||
int num;
|
int num;
|
||||||
|
|
||||||
// We sort the input entries by name, and add them to the
|
// We sort the input entries by name, and add them to the
|
||||||
// output manifest in sorted order. We expect that the output
|
// output manifest in sorted order. We expect that the output
|
||||||
// map will be deterministic.
|
// map will be deterministic.
|
||||||
|
|
||||||
TreeMap<String, JarEntry> byName = new TreeMap<>();
|
TreeMap<String, JarEntry> byName = new TreeMap<>();
|
||||||
|
|
||||||
for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
|
for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
|
||||||
JarEntry entry = e.nextElement();
|
JarEntry entry = e.nextElement();
|
||||||
byName.put(entry.getName(), entry);
|
byName.put(entry.getName(), entry);
|
||||||
}
|
}
|
||||||
for (JarEntry entry: byName.values()) {
|
|
||||||
|
for (JarEntry entry : byName.values()) {
|
||||||
String name = entry.getName();
|
String name = entry.getName();
|
||||||
if (!entry.isDirectory() &&
|
if (!entry.isDirectory() && !stripPattern.matcher(name).matches()) {
|
||||||
(stripPattern == null || !stripPattern.matcher(name).matches())) {
|
|
||||||
InputStream data = jar.getInputStream(entry);
|
InputStream data = jar.getInputStream(entry);
|
||||||
while ((num = data.read(buffer)) > 0) {
|
while ((num = data.read(buffer)) > 0) {
|
||||||
if (md_sha1 != null) md_sha1.update(buffer, 0, num);
|
if (md_sha1 != null) md_sha1.update(buffer, 0, num);
|
||||||
if (md_sha256 != null) md_sha256.update(buffer, 0, num);
|
if (md_sha256 != null) md_sha256.update(buffer, 0, num);
|
||||||
}
|
}
|
||||||
Attributes attr = new Attributes();
|
|
||||||
|
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 =
|
||||||
|
key.toString().toLowerCase(Locale.US);
|
||||||
|
if (attributeNameLowerCase.endsWith("-digest")) {
|
||||||
|
i.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add SHA-1 digest if requested
|
||||||
if (md_sha1 != null) {
|
if (md_sha1 != null) {
|
||||||
attr.putValue("SHA1-Digest",
|
attr.putValue("SHA1-Digest",
|
||||||
new String(Base64.encode(md_sha1.digest()), "ASCII"));
|
new String(Base64.encode(md_sha1.digest()), "ASCII"));
|
||||||
}
|
}
|
||||||
|
// Add SHA-256 digest if requested
|
||||||
if (md_sha256 != null) {
|
if (md_sha256 != null) {
|
||||||
attr.putValue("SHA-256-Digest",
|
attr.putValue("SHA-256-Digest",
|
||||||
new String(Base64.encode(md_sha256.digest()), "ASCII"));
|
new String(Base64.encode(md_sha256.digest()), "ASCII"));
|
||||||
@ -208,62 +184,39 @@ public class SignAPK {
|
|||||||
output.getEntries().put(name, attr);
|
output.getEntries().put(name, attr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write to another stream and track how many bytes have been
|
* Write a .SF file with a digest of the specified manifest.
|
||||||
* written.
|
|
||||||
*/
|
*/
|
||||||
private static class CountOutputStream extends FilterOutputStream {
|
private static void writeSignatureFile(Manifest manifest, OutputStream out,
|
||||||
private int mCount;
|
int hash)
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An OutputStream that does literally nothing
|
|
||||||
*/
|
|
||||||
private static OutputStream stubStream = new OutputStream() {
|
|
||||||
@Override
|
|
||||||
public void write(int b) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(byte[] b, int off, int len) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Write a .SF file with a digest of the specified manifest. */
|
|
||||||
private static void writeSignatureFile(Manifest manifest, OutputStream out, int hash)
|
|
||||||
throws IOException, GeneralSecurityException {
|
throws IOException, GeneralSecurityException {
|
||||||
Manifest sf = new Manifest();
|
Manifest sf = new Manifest();
|
||||||
Attributes main = sf.getMainAttributes();
|
Attributes main = sf.getMainAttributes();
|
||||||
main.putValue("Signature-Version", "1.0");
|
main.putValue("Signature-Version", "1.0");
|
||||||
main.putValue("Created-By", "1.0 (Android SignApk)");
|
main.putValue("Created-By", "1.0 (Android SignApk)");
|
||||||
MessageDigest md = MessageDigest.getInstance(
|
// Add APK Signature Scheme v2 signature stripping protection.
|
||||||
hash == USE_SHA256 ? "SHA256" : "SHA1");
|
// This attribute indicates that this APK is supposed to have been signed using one or
|
||||||
PrintStream print = new PrintStream(
|
// more APK-specific signature schemes in addition to the standard JAR signature scheme
|
||||||
new DigestOutputStream(stubStream, md),
|
// 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");
|
true, "UTF-8");
|
||||||
|
|
||||||
// Digest of the entire manifest
|
// Digest of the entire manifest
|
||||||
manifest.write(print);
|
manifest.write(print);
|
||||||
print.flush();
|
print.flush();
|
||||||
main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
|
main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
|
||||||
new String(Base64.encode(md.digest()), "ASCII"));
|
new String(Base64.encode(md.digest()), "ASCII"));
|
||||||
|
|
||||||
Map<String, Attributes> entries = manifest.getEntries();
|
Map<String, Attributes> entries = manifest.getEntries();
|
||||||
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
|
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
|
||||||
// Digest of the manifest stanza for this entry.
|
// Digest of the manifest stanza for this entry.
|
||||||
@ -273,13 +226,16 @@ public class SignAPK {
|
|||||||
}
|
}
|
||||||
print.print("\r\n");
|
print.print("\r\n");
|
||||||
print.flush();
|
print.flush();
|
||||||
|
|
||||||
Attributes sfAttr = new Attributes();
|
Attributes sfAttr = new Attributes();
|
||||||
sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest",
|
sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest",
|
||||||
new String(Base64.encode(md.digest()), "ASCII"));
|
new String(Base64.encode(md.digest()), "ASCII"));
|
||||||
sf.getEntries().put(entry.getKey(), sfAttr);
|
sf.getEntries().put(entry.getKey(), sfAttr);
|
||||||
}
|
}
|
||||||
|
|
||||||
CountOutputStream cout = new CountOutputStream(out);
|
CountOutputStream cout = new CountOutputStream(out);
|
||||||
sf.write(cout);
|
sf.write(cout);
|
||||||
|
|
||||||
// A bug in the java.util.jar implementation of Android platforms
|
// A bug in the java.util.jar implementation of Android platforms
|
||||||
// up to version 1.6 will cause a spurious IOException to be thrown
|
// 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.
|
// if the length of the signature file is a multiple of 1024 bytes.
|
||||||
@ -290,10 +246,11 @@ public class SignAPK {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sign data and write the digital signature to 'out'. */
|
/**
|
||||||
|
* Sign data and write the digital signature to 'out'.
|
||||||
|
*/
|
||||||
private static void writeSignatureBlock(
|
private static void writeSignatureBlock(
|
||||||
CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
|
CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, OutputStream out)
|
||||||
OutputStream out)
|
|
||||||
throws IOException,
|
throws IOException,
|
||||||
CertificateEncodingException,
|
CertificateEncodingException,
|
||||||
OperatorCreationException,
|
OperatorCreationException,
|
||||||
@ -301,19 +258,22 @@ public class SignAPK {
|
|||||||
ArrayList<X509Certificate> certList = new ArrayList<>(1);
|
ArrayList<X509Certificate> certList = new ArrayList<>(1);
|
||||||
certList.add(publicKey);
|
certList.add(publicKey);
|
||||||
JcaCertStore certs = new JcaCertStore(certList);
|
JcaCertStore certs = new JcaCertStore(certList);
|
||||||
|
|
||||||
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
||||||
ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey))
|
ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey))
|
||||||
.build(privateKey);
|
.build(privateKey);
|
||||||
gen.addSignerInfoGenerator(
|
gen.addSignerInfoGenerator(
|
||||||
new JcaSignerInfoGeneratorBuilder(
|
new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build())
|
||||||
new JcaDigestCalculatorProviderBuilder().build())
|
|
||||||
.setDirectSignature(true)
|
.setDirectSignature(true)
|
||||||
.build(signer, publicKey));
|
.build(signer, publicKey)
|
||||||
|
);
|
||||||
gen.addCertificates(certs);
|
gen.addCertificates(certs);
|
||||||
CMSSignedData sigData = gen.generate(data, false);
|
CMSSignedData sigData = gen.generate(data, false);
|
||||||
ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
|
|
||||||
ASN1OutputStream dos = ASN1OutputStream.create(out, ASN1Encoding.DER);
|
try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) {
|
||||||
dos.writeObject(asn1.readObject());
|
ASN1OutputStream dos = ASN1OutputStream.create(out, ASN1Encoding.DER);
|
||||||
|
dos.writeObject(asn1.readObject());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -323,27 +283,37 @@ public class SignAPK {
|
|||||||
* more efficient.
|
* more efficient.
|
||||||
*/
|
*/
|
||||||
private static void copyFiles(Manifest manifest, JarMap in, JarOutputStream out,
|
private static void copyFiles(Manifest manifest, JarMap in, JarOutputStream out,
|
||||||
long timestamp, int alignment) throws IOException {
|
long timestamp, int defaultAlignment) throws IOException {
|
||||||
byte[] buffer = new byte[4096];
|
byte[] buffer = new byte[4096];
|
||||||
int num;
|
int num;
|
||||||
|
|
||||||
Map<String, Attributes> entries = manifest.getEntries();
|
Map<String, Attributes> entries = manifest.getEntries();
|
||||||
ArrayList<String> names = new ArrayList<>(entries.keySet());
|
ArrayList<String> names = new ArrayList<>(entries.keySet());
|
||||||
Collections.sort(names);
|
Collections.sort(names);
|
||||||
|
|
||||||
boolean firstEntry = true;
|
boolean firstEntry = true;
|
||||||
long offset = 0L;
|
long offset = 0L;
|
||||||
|
|
||||||
// We do the copy in two passes -- first copying all the
|
// We do the copy in two passes -- first copying all the
|
||||||
// entries that are STORED, then copying all the entries that
|
// entries that are STORED, then copying all the entries that
|
||||||
// have any other compression flag (which in practice means
|
// have any other compression flag (which in practice means
|
||||||
// DEFLATED). This groups all the stored entries together at
|
// DEFLATED). This groups all the stored entries together at
|
||||||
// the start of the file and makes it easier to do alignment
|
// the start of the file and makes it easier to do alignment
|
||||||
// on them (since only stored entries are aligned).
|
// on them (since only stored entries are aligned).
|
||||||
|
|
||||||
for (String name : names) {
|
for (String name : names) {
|
||||||
JarEntry inEntry = in.getJarEntry(name);
|
JarEntry inEntry = in.getJarEntry(name);
|
||||||
JarEntry outEntry = null;
|
JarEntry outEntry;
|
||||||
if (inEntry.getMethod() != JarEntry.STORED) continue;
|
if (inEntry.getMethod() != JarEntry.STORED) continue;
|
||||||
// Preserve the STORED method of the input entry.
|
// Preserve the STORED method of the input entry.
|
||||||
outEntry = new JarEntry(inEntry);
|
outEntry = new JarEntry(inEntry);
|
||||||
outEntry.setTime(timestamp);
|
outEntry.setTime(timestamp);
|
||||||
|
// Discard comment and extra fields of this entry to
|
||||||
|
// simplify alignment logic below and for consistency with
|
||||||
|
// how compressed entries are handled later.
|
||||||
|
outEntry.setComment(null);
|
||||||
|
outEntry.setExtra(null);
|
||||||
|
|
||||||
// 'offset' is the offset into the file at which we expect
|
// 'offset' is the offset into the file at which we expect
|
||||||
// the file data to begin. This is the value we need to
|
// the file data to begin. This is the value we need to
|
||||||
// make a multiple of 'alignement'.
|
// make a multiple of 'alignement'.
|
||||||
@ -357,15 +327,18 @@ public class SignAPK {
|
|||||||
offset += 4;
|
offset += 4;
|
||||||
firstEntry = false;
|
firstEntry = false;
|
||||||
}
|
}
|
||||||
|
int alignment = getStoredEntryDataAlignment(name, defaultAlignment);
|
||||||
if (alignment > 0 && (offset % alignment != 0)) {
|
if (alignment > 0 && (offset % alignment != 0)) {
|
||||||
// Set the "extra data" of the entry to between 1 and
|
// Set the "extra data" of the entry to between 1 and
|
||||||
// alignment-1 bytes, to make the file data begin at
|
// alignment-1 bytes, to make the file data begin at
|
||||||
// an aligned offset.
|
// an aligned offset.
|
||||||
int needed = alignment - (int)(offset % alignment);
|
int needed = alignment - (int) (offset % alignment);
|
||||||
outEntry.setExtra(new byte[needed]);
|
outEntry.setExtra(new byte[needed]);
|
||||||
offset += needed;
|
offset += needed;
|
||||||
}
|
}
|
||||||
|
|
||||||
out.putNextEntry(outEntry);
|
out.putNextEntry(outEntry);
|
||||||
|
|
||||||
InputStream data = in.getInputStream(inEntry);
|
InputStream data = in.getInputStream(inEntry);
|
||||||
while ((num = data.read(buffer)) > 0) {
|
while ((num = data.read(buffer)) > 0) {
|
||||||
out.write(buffer, 0, num);
|
out.write(buffer, 0, num);
|
||||||
@ -373,17 +346,20 @@ public class SignAPK {
|
|||||||
}
|
}
|
||||||
out.flush();
|
out.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy all the non-STORED entries. We don't attempt to
|
// Copy all the non-STORED entries. We don't attempt to
|
||||||
// maintain the 'offset' variable past this point; we don't do
|
// maintain the 'offset' variable past this point; we don't do
|
||||||
// alignment on these entries.
|
// alignment on these entries.
|
||||||
|
|
||||||
for (String name : names) {
|
for (String name : names) {
|
||||||
JarEntry inEntry = in.getJarEntry(name);
|
JarEntry inEntry = in.getJarEntry(name);
|
||||||
JarEntry outEntry = null;
|
JarEntry outEntry;
|
||||||
if (inEntry.getMethod() == JarEntry.STORED) continue;
|
if (inEntry.getMethod() == JarEntry.STORED) continue;
|
||||||
// Create a new entry so that the compressed len is recomputed.
|
// Create a new entry so that the compressed len is recomputed.
|
||||||
outEntry = new JarEntry(name);
|
outEntry = new JarEntry(name);
|
||||||
outEntry.setTime(timestamp);
|
outEntry.setTime(timestamp);
|
||||||
out.putNextEntry(outEntry);
|
out.putNextEntry(outEntry);
|
||||||
|
|
||||||
InputStream data = in.getInputStream(inEntry);
|
InputStream data = in.getInputStream(inEntry);
|
||||||
while ((num = data.read(buffer)) > 0) {
|
while ((num = data.read(buffer)) > 0) {
|
||||||
out.write(buffer, 0, num);
|
out.write(buffer, 0, num);
|
||||||
@ -392,135 +368,203 @@ public class SignAPK {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This class is to provide a file's content, but trimming out the last two bytes
|
/**
|
||||||
// Used for signWholeFile
|
* Returns the multiple (in bytes) at which the provided {@code STORED} entry's data must start
|
||||||
private static class CMSProcessableFile implements CMSTypedData {
|
* relative to start of file or {@code 0} if alignment of this entry's data is not important.
|
||||||
|
*/
|
||||||
private ASN1ObjectIdentifier type;
|
private static int getStoredEntryDataAlignment(String entryName, int defaultAlignment) {
|
||||||
private RandomAccessFile file;
|
if (defaultAlignment <= 0) {
|
||||||
|
return 0;
|
||||||
CMSProcessableFile(File file) throws FileNotFoundException {
|
|
||||||
this.file = new RandomAccessFile(file, "r");
|
|
||||||
type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
if (entryName.endsWith(".so")) {
|
||||||
public ASN1ObjectIdentifier getContentType() {
|
// Align .so contents to memory page boundary to enable memory-mapped
|
||||||
return type;
|
// execution.
|
||||||
}
|
return 4096;
|
||||||
|
} else {
|
||||||
@Override
|
return defaultAlignment;
|
||||||
public void write(OutputStream out) throws IOException, CMSException {
|
|
||||||
file.seek(0);
|
|
||||||
int read;
|
|
||||||
byte buffer[] = new byte[4096];
|
|
||||||
int len = (int) file.length() - 2;
|
|
||||||
while ((read = file.read(buffer, 0, len < buffer.length ? len : buffer.length)) > 0) {
|
|
||||||
out.write(buffer, 0, read);
|
|
||||||
len -= read;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getContent() {
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] getTail() throws IOException {
|
|
||||||
byte tail[] = new byte[22];
|
|
||||||
file.seek(file.length() - 22);
|
|
||||||
file.readFully(tail);
|
|
||||||
return tail;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void signWholeFile(File input, X509Certificate publicKey,
|
private static void signFile(Manifest manifest,
|
||||||
PrivateKey privateKey, OutputStream outputStream)
|
X509Certificate[] publicKey, PrivateKey[] privateKey,
|
||||||
throws Exception {
|
long timestamp, JarOutputStream outputJar) throws Exception {
|
||||||
ByteArrayOutputStream temp = new ByteArrayOutputStream();
|
|
||||||
// put a readable message and a null char at the start of the
|
|
||||||
// archive comment, so that tools that display the comment
|
|
||||||
// (hopefully) show something sensible.
|
|
||||||
// TODO: anything more useful we can put in this message?
|
|
||||||
byte[] message = "signed by SignApk".getBytes("UTF-8");
|
|
||||||
temp.write(message);
|
|
||||||
temp.write(0);
|
|
||||||
|
|
||||||
CMSProcessableFile cmsFile = new CMSProcessableFile(input);
|
|
||||||
writeSignatureBlock(cmsFile, publicKey, privateKey, temp);
|
|
||||||
|
|
||||||
// For a zip with no archive comment, the
|
|
||||||
// end-of-central-directory record will be 22 bytes long, so
|
|
||||||
// we expect to find the EOCD marker 22 bytes from the end.
|
|
||||||
byte[] zipData = cmsFile.getTail();
|
|
||||||
if (zipData[zipData.length-22] != 0x50 ||
|
|
||||||
zipData[zipData.length-21] != 0x4b ||
|
|
||||||
zipData[zipData.length-20] != 0x05 ||
|
|
||||||
zipData[zipData.length-19] != 0x06) {
|
|
||||||
throw new IllegalArgumentException("zip data already has an archive comment");
|
|
||||||
}
|
|
||||||
int total_size = temp.size() + 6;
|
|
||||||
if (total_size > 0xffff) {
|
|
||||||
throw new IllegalArgumentException("signature is too big for ZIP file comment");
|
|
||||||
}
|
|
||||||
// signature starts this many bytes from the end of the file
|
|
||||||
int signature_start = total_size - message.length - 1;
|
|
||||||
temp.write(signature_start & 0xff);
|
|
||||||
temp.write((signature_start >> 8) & 0xff);
|
|
||||||
// Why the 0xff bytes? In a zip file with no archive comment,
|
|
||||||
// bytes [-6:-2] of the file are the little-endian offset from
|
|
||||||
// the start of the file to the central directory. So for the
|
|
||||||
// two high bytes to be 0xff 0xff, the archive would have to
|
|
||||||
// be nearly 4GB in size. So it's unlikely that a real
|
|
||||||
// commentless archive would have 0xffs here, and lets us tell
|
|
||||||
// an old signed archive from a new one.
|
|
||||||
temp.write(0xff);
|
|
||||||
temp.write(0xff);
|
|
||||||
temp.write(total_size & 0xff);
|
|
||||||
temp.write((total_size >> 8) & 0xff);
|
|
||||||
temp.flush();
|
|
||||||
// Signature verification checks that the EOCD header is the
|
|
||||||
// last such sequence in the file (to avoid minzip finding a
|
|
||||||
// fake EOCD appended after the signature in its scan). The
|
|
||||||
// odds of producing this sequence by chance are very low, but
|
|
||||||
// let's catch it here if it does.
|
|
||||||
byte[] b = temp.toByteArray();
|
|
||||||
for (int i = 0; i < b.length-3; ++i) {
|
|
||||||
if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
|
|
||||||
throw new IllegalArgumentException("found spurious EOCD header at " + i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cmsFile.write(outputStream);
|
|
||||||
outputStream.write(total_size & 0xff);
|
|
||||||
outputStream.write((total_size >> 8) & 0xff);
|
|
||||||
temp.writeTo(outputStream);
|
|
||||||
outputStream.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void signFile(Manifest manifest, JarMap inputJar,
|
|
||||||
X509Certificate cert, PrivateKey privateKey,
|
|
||||||
JarOutputStream outputJar)
|
|
||||||
throws Exception {
|
|
||||||
// Assume the certificate is valid for at least an hour.
|
|
||||||
long timestamp = cert.getNotBefore().getTime() + 3600L * 1000;
|
|
||||||
// MANIFEST.MF
|
// MANIFEST.MF
|
||||||
JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
|
JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
|
||||||
je.setTime(timestamp);
|
je.setTime(timestamp);
|
||||||
outputJar.putNextEntry(je);
|
outputJar.putNextEntry(je);
|
||||||
manifest.write(outputJar);
|
manifest.write(outputJar);
|
||||||
je = new JarEntry(CERT_SF_NAME);
|
|
||||||
je.setTime(timestamp);
|
int numKeys = publicKey.length;
|
||||||
outputJar.putNextEntry(je);
|
for (int k = 0; k < numKeys; ++k) {
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
// CERT.SF / CERT#.SF
|
||||||
writeSignatureFile(manifest, baos, getDigestAlgorithm(cert));
|
je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
|
||||||
byte[] signedData = baos.toByteArray();
|
(String.format(Locale.US, CERT_SF_MULTI_NAME, k)));
|
||||||
outputJar.write(signedData);
|
je.setTime(timestamp);
|
||||||
// CERT.{EC,RSA} / CERT#.{EC,RSA}
|
outputJar.putNextEntry(je);
|
||||||
final String keyType = cert.getPublicKey().getAlgorithm();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
je = new JarEntry(String.format(CERT_SIG_NAME, keyType));
|
writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k]));
|
||||||
je.setTime(timestamp);
|
byte[] signedData = baos.toByteArray();
|
||||||
outputJar.putNextEntry(je);
|
outputJar.write(signedData);
|
||||||
writeSignatureBlock(new CMSProcessableByteArray(signedData),
|
|
||||||
cert, privateKey, outputJar);
|
// 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(Locale.US, CERT_SIG_MULTI_NAME, k, keyType)));
|
||||||
|
je.setTime(timestamp);
|
||||||
|
outputJar.putNextEntry(je);
|
||||||
|
writeSignatureBlock(new CMSProcessableByteArray(signedData),
|
||||||
|
publicKey[k], privateKey[k], outputJar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
private static List<ApkSignerV2.SignerConfig> createV2SignerConfigs(
|
||||||
|
PrivateKey[] privateKeys, X509Certificate[] certificates, String[] digestAlgorithms)
|
||||||
|
throws InvalidKeyException {
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
return ApkSignerV2.SIGNATURE_DSA_WITH_SHA512;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void sign(X509Certificate cert, PrivateKey key,
|
||||||
|
JarMap inputJar, FileOutputStream outputFile) throws Exception {
|
||||||
|
int alignment = 4;
|
||||||
|
int hashes = 0;
|
||||||
|
|
||||||
|
X509Certificate[] publicKey = new X509Certificate[1];
|
||||||
|
publicKey[0] = cert;
|
||||||
|
hashes |= getDigestAlgorithm(publicKey[0]);
|
||||||
|
|
||||||
|
// Set all ZIP file timestamps to Jan 1 2009 00:00:00.
|
||||||
|
long timestamp = 1230768000000L;
|
||||||
|
// The Java ZipEntry API we're using converts milliseconds since epoch into MS-DOS
|
||||||
|
// timestamp using the current timezone. We thus adjust the milliseconds since epoch
|
||||||
|
// value to end up with MS-DOS timestamp of Jan 1 2009 00:00:00.
|
||||||
|
timestamp -= TimeZone.getDefault().getOffset(timestamp);
|
||||||
|
|
||||||
|
PrivateKey[] privateKey = new PrivateKey[1];
|
||||||
|
privateKey[0] = key;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
Manifest manifest = addDigestsToManifest(inputJar, hashes);
|
||||||
|
copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
|
||||||
|
signFile(manifest, publicKey, privateKey, timestamp, outputJar);
|
||||||
|
outputJar.close();
|
||||||
|
ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
|
||||||
|
v1SignedApkBuf.reset();
|
||||||
|
|
||||||
|
ByteBuffer[] outputChunks;
|
||||||
|
List<ApkSignerV2.SignerConfig> signerConfigs = createV2SignerConfigs(privateKey, publicKey,
|
||||||
|
new String[]{APK_SIG_SCHEME_V2_DIGEST_ALGORITHM});
|
||||||
|
outputChunks = ApkSignerV2.sign(v1SignedApk, signerConfigs);
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,277 +0,0 @@
|
|||||||
package com.topjohnwu.signing;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
|
|
||||||
public class ZipAdjust {
|
|
||||||
|
|
||||||
public static void adjust(File input, File output) throws IOException {
|
|
||||||
try (
|
|
||||||
RandomAccessFile in = new RandomAccessFile(input, "r");
|
|
||||||
FileOutputStream out = new FileOutputStream(output)
|
|
||||||
) {
|
|
||||||
adjust(in, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void adjust(RandomAccessFile in, OutputStream out) throws IOException {
|
|
||||||
|
|
||||||
CentralFooter footer = new CentralFooter(in);
|
|
||||||
int outOff = 0;
|
|
||||||
long centralOff = unsigned(footer.centralDirectoryOffset);
|
|
||||||
CentralHeader[] centralHeaders = new CentralHeader[unsigned(footer.numEntries)];
|
|
||||||
|
|
||||||
// Loop through central directory entries
|
|
||||||
for (int i = 0; i < centralHeaders.length; ++i) {
|
|
||||||
// Read central header
|
|
||||||
in.seek(centralOff);
|
|
||||||
centralHeaders[i] = new CentralHeader(in);
|
|
||||||
centralOff = in.getFilePointer();
|
|
||||||
|
|
||||||
// Read local header
|
|
||||||
in.seek(unsigned(centralHeaders[i].localHeaderOffset));
|
|
||||||
LocalHeader localHeader = new LocalHeader(in);
|
|
||||||
|
|
||||||
// Make sure local and central headers matches, and strip out data descriptor flag
|
|
||||||
centralHeaders[i].localHeaderOffset = outOff;
|
|
||||||
centralHeaders[i].flags &= ~(1 << 3);
|
|
||||||
localHeader.flags = centralHeaders[i].flags;
|
|
||||||
localHeader.crc32 = centralHeaders[i].crc32;
|
|
||||||
localHeader.compressedSize = centralHeaders[i].compressedSize;
|
|
||||||
localHeader.uncompressedSize = centralHeaders[i].uncompressedSize;
|
|
||||||
localHeader.fileNameLength = centralHeaders[i].fileNameLength;
|
|
||||||
localHeader.filename = centralHeaders[i].filename;
|
|
||||||
|
|
||||||
// Write local header
|
|
||||||
outOff += localHeader.write(out);
|
|
||||||
|
|
||||||
// Copy data
|
|
||||||
int read;
|
|
||||||
long len = unsigned(localHeader.compressedSize);
|
|
||||||
outOff += len;
|
|
||||||
byte data[] = new byte[4096];
|
|
||||||
while ((read = in.read(data, 0,
|
|
||||||
len < data.length ? (int) len : data.length)) > 0) {
|
|
||||||
out.write(data, 0, read);
|
|
||||||
len -= read;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
footer.centralDirectoryOffset = outOff;
|
|
||||||
|
|
||||||
// Write central directory
|
|
||||||
outOff = 0;
|
|
||||||
for (CentralHeader header : centralHeaders)
|
|
||||||
outOff += header.write(out);
|
|
||||||
|
|
||||||
// Write central directory record
|
|
||||||
footer.centralDirectorySize = outOff;
|
|
||||||
footer.write(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static short unsigned(byte n) {
|
|
||||||
return (short)(n & 0xff);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int unsigned(short n) {
|
|
||||||
return n & 0xffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static long unsigned(int n) {
|
|
||||||
return n & 0xffffffffL;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class CentralFooter {
|
|
||||||
|
|
||||||
static final int MAGIC = 0x06054b50;
|
|
||||||
|
|
||||||
int signature;
|
|
||||||
short diskNumber;
|
|
||||||
short centralDirectoryDiskNumber;
|
|
||||||
short numEntriesThisDisk;
|
|
||||||
short numEntries;
|
|
||||||
int centralDirectorySize;
|
|
||||||
int centralDirectoryOffset;
|
|
||||||
short zipCommentLength;
|
|
||||||
// byte[] comments;
|
|
||||||
|
|
||||||
CentralFooter(RandomAccessFile file) throws IOException {
|
|
||||||
byte[] buffer = new byte[22];
|
|
||||||
for (long i = file.length() - 4; i >= 0; --i) {
|
|
||||||
file.seek(i);
|
|
||||||
file.read(buffer, 0 ,4);
|
|
||||||
ByteBuffer buf = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
signature = buf.getInt();
|
|
||||||
if (signature != MAGIC) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
file.read(buffer, 4, buffer.length - 4);
|
|
||||||
diskNumber = buf.getShort();
|
|
||||||
centralDirectoryDiskNumber = buf.getShort();
|
|
||||||
numEntriesThisDisk = buf.getShort();
|
|
||||||
numEntries = buf.getShort();
|
|
||||||
centralDirectorySize = buf.getInt();
|
|
||||||
centralDirectoryOffset = buf.getInt();
|
|
||||||
zipCommentLength = buf.getShort();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int write(OutputStream out) throws IOException {
|
|
||||||
byte[] buffer = new byte[22];
|
|
||||||
ByteBuffer buf = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
buf.putInt(signature);
|
|
||||||
buf.putShort(diskNumber);
|
|
||||||
buf.putShort(centralDirectoryDiskNumber);
|
|
||||||
buf.putShort(numEntriesThisDisk);
|
|
||||||
buf.putShort(numEntries);
|
|
||||||
buf.putInt(centralDirectorySize);
|
|
||||||
buf.putInt(centralDirectoryOffset);
|
|
||||||
buf.putShort((short) 0); // zipCommentLength
|
|
||||||
out.write(buffer);
|
|
||||||
return buffer.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class CentralHeader {
|
|
||||||
|
|
||||||
static final int MAGIC = 0x02014b50;
|
|
||||||
|
|
||||||
int signature;
|
|
||||||
short versionMadeBy;
|
|
||||||
short versionNeededToExtract;
|
|
||||||
short flags;
|
|
||||||
short compressionMethod;
|
|
||||||
short lastModFileTime;
|
|
||||||
short lastModFileDate;
|
|
||||||
int crc32;
|
|
||||||
int compressedSize;
|
|
||||||
int uncompressedSize;
|
|
||||||
short fileNameLength;
|
|
||||||
short extraFieldLength;
|
|
||||||
short fileCommentLength;
|
|
||||||
short diskNumberStart;
|
|
||||||
short internalFileAttributes;
|
|
||||||
int externalFileAttributes;
|
|
||||||
int localHeaderOffset;
|
|
||||||
byte[] filename;
|
|
||||||
// byte[] extra;
|
|
||||||
// byte[] comment;
|
|
||||||
|
|
||||||
CentralHeader(RandomAccessFile file) throws IOException {
|
|
||||||
byte[] buffer = new byte[46];
|
|
||||||
file.read(buffer);
|
|
||||||
ByteBuffer buf = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
signature = buf.getInt();
|
|
||||||
if (signature != MAGIC)
|
|
||||||
throw new IOException();
|
|
||||||
versionMadeBy = buf.getShort();
|
|
||||||
versionNeededToExtract = buf.getShort();
|
|
||||||
flags = buf.getShort();
|
|
||||||
compressionMethod = buf.getShort();
|
|
||||||
lastModFileTime = buf.getShort();
|
|
||||||
lastModFileDate = buf.getShort();
|
|
||||||
crc32 = buf.getInt();
|
|
||||||
compressedSize = buf.getInt();
|
|
||||||
uncompressedSize = buf.getInt();
|
|
||||||
fileNameLength = buf.getShort();
|
|
||||||
extraFieldLength = buf.getShort();
|
|
||||||
fileCommentLength = buf.getShort();
|
|
||||||
diskNumberStart = buf.getShort();
|
|
||||||
internalFileAttributes = buf.getShort();
|
|
||||||
externalFileAttributes = buf.getInt();
|
|
||||||
localHeaderOffset = buf.getInt();
|
|
||||||
filename = new byte[unsigned(fileNameLength)];
|
|
||||||
file.read(filename);
|
|
||||||
file.skipBytes(unsigned(extraFieldLength) + unsigned(fileCommentLength));
|
|
||||||
}
|
|
||||||
|
|
||||||
int write(OutputStream out) throws IOException {
|
|
||||||
byte[] buffer = new byte[46];
|
|
||||||
ByteBuffer buf = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
buf.putInt(signature);
|
|
||||||
buf.putShort(versionMadeBy);
|
|
||||||
buf.putShort(versionNeededToExtract);
|
|
||||||
buf.putShort(flags);
|
|
||||||
buf.putShort(compressionMethod);
|
|
||||||
buf.putShort(lastModFileTime);
|
|
||||||
buf.putShort(lastModFileDate);
|
|
||||||
buf.putInt(crc32);
|
|
||||||
buf.putInt(compressedSize);
|
|
||||||
buf.putInt(uncompressedSize);
|
|
||||||
buf.putShort(fileNameLength);
|
|
||||||
buf.putShort((short) 0); // extraFieldLength
|
|
||||||
buf.putShort((short) 0); // fileCommentLength
|
|
||||||
buf.putShort(diskNumberStart);
|
|
||||||
buf.putShort(internalFileAttributes);
|
|
||||||
buf.putInt(externalFileAttributes);
|
|
||||||
buf.putInt(localHeaderOffset);
|
|
||||||
out.write(buffer);
|
|
||||||
out.write(filename);
|
|
||||||
return buffer.length + filename.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class LocalHeader {
|
|
||||||
|
|
||||||
static final int MAGIC = 0x04034b50;
|
|
||||||
|
|
||||||
int signature;
|
|
||||||
short versionNeededToExtract;
|
|
||||||
short flags;
|
|
||||||
short compressionMethod;
|
|
||||||
short lastModFileTime;
|
|
||||||
short lastModFileDate;
|
|
||||||
int crc32;
|
|
||||||
int compressedSize;
|
|
||||||
int uncompressedSize;
|
|
||||||
short fileNameLength;
|
|
||||||
short extraFieldLength;
|
|
||||||
byte[] filename;
|
|
||||||
// byte[] extra;
|
|
||||||
|
|
||||||
LocalHeader(RandomAccessFile file) throws IOException {
|
|
||||||
byte[] buffer = new byte[30];
|
|
||||||
file.read(buffer);
|
|
||||||
ByteBuffer buf = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
signature = buf.getInt();
|
|
||||||
if (signature != MAGIC)
|
|
||||||
throw new IOException();
|
|
||||||
versionNeededToExtract = buf.getShort();
|
|
||||||
flags = buf.getShort();
|
|
||||||
compressionMethod = buf.getShort();
|
|
||||||
lastModFileTime = buf.getShort();
|
|
||||||
lastModFileDate = buf.getShort();
|
|
||||||
crc32 = buf.getInt();
|
|
||||||
compressedSize = buf.getInt();
|
|
||||||
uncompressedSize = buf.getInt();
|
|
||||||
fileNameLength = buf.getShort();
|
|
||||||
extraFieldLength = buf.getShort();
|
|
||||||
file.skipBytes(unsigned(fileNameLength) + unsigned(extraFieldLength));
|
|
||||||
}
|
|
||||||
|
|
||||||
int write(OutputStream out) throws IOException {
|
|
||||||
byte[] buffer = new byte[30];
|
|
||||||
ByteBuffer buf = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
buf.putInt(signature);
|
|
||||||
buf.putShort(versionNeededToExtract);
|
|
||||||
buf.putShort(flags);
|
|
||||||
buf.putShort(compressionMethod);
|
|
||||||
buf.putShort(lastModFileTime);
|
|
||||||
buf.putShort(lastModFileDate);
|
|
||||||
buf.putInt(crc32);
|
|
||||||
buf.putInt(compressedSize);
|
|
||||||
buf.putInt(uncompressedSize);
|
|
||||||
buf.putShort(fileNameLength);
|
|
||||||
buf.putShort((short) 0); // extraFieldLength
|
|
||||||
out.write(buffer);
|
|
||||||
out.write(filename);
|
|
||||||
return buffer.length + filename.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,7 +6,6 @@ import java.io.FileInputStream;
|
|||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.security.KeyStore;
|
import java.security.KeyStore;
|
||||||
import java.security.KeyStoreException;
|
import java.security.KeyStoreException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
@ -28,20 +27,20 @@ public class ZipSigner {
|
|||||||
System.exit(2);
|
System.exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void sign(JarMap input, OutputStream output) throws Exception {
|
private static void sign(JarMap input, FileOutputStream output) throws Exception {
|
||||||
sign(SignAPK.class.getResourceAsStream("/keys/testkey.x509.pem"),
|
sign(SignApk.class.getResourceAsStream("/keys/testkey.x509.pem"),
|
||||||
SignAPK.class.getResourceAsStream("/keys/testkey.pk8"), input, output);
|
SignApk.class.getResourceAsStream("/keys/testkey.pk8"), input, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void sign(InputStream certIs, InputStream keyIs,
|
private static void sign(InputStream certIs, InputStream keyIs,
|
||||||
JarMap input, OutputStream output) throws Exception {
|
JarMap input, FileOutputStream output) throws Exception {
|
||||||
X509Certificate cert = CryptoUtils.readCertificate(certIs);
|
X509Certificate cert = CryptoUtils.readCertificate(certIs);
|
||||||
PrivateKey key = CryptoUtils.readPrivateKey(keyIs);
|
PrivateKey key = CryptoUtils.readPrivateKey(keyIs);
|
||||||
SignAPK.signAndAdjust(cert, key, input, output);
|
SignApk.sign(cert, key, input, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void sign(String keyStore, String keyStorePass, String alias, String keyPass,
|
private static void sign(String keyStore, String keyStorePass, String alias, String keyPass,
|
||||||
JarMap in, OutputStream out) throws Exception {
|
JarMap in, FileOutputStream out) throws Exception {
|
||||||
KeyStore ks;
|
KeyStore ks;
|
||||||
try {
|
try {
|
||||||
ks = KeyStore.getInstance("JKS");
|
ks = KeyStore.getInstance("JKS");
|
||||||
@ -56,7 +55,7 @@ public class ZipSigner {
|
|||||||
}
|
}
|
||||||
X509Certificate cert = (X509Certificate) ks.getCertificate(alias);
|
X509Certificate cert = (X509Certificate) ks.getCertificate(alias);
|
||||||
PrivateKey key = (PrivateKey) ks.getKey(alias, keyPass.toCharArray());
|
PrivateKey key = (PrivateKey) ks.getKey(alias, keyPass.toCharArray());
|
||||||
SignAPK.signAndAdjust(cert, key, in, out);
|
SignApk.sign(cert, key, in, out);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
@ -66,7 +65,7 @@ public class ZipSigner {
|
|||||||
Security.insertProviderAt(new BouncyCastleProvider(), 1);
|
Security.insertProviderAt(new BouncyCastleProvider(), 1);
|
||||||
|
|
||||||
try (JarMap in = JarMap.open(args[args.length - 2], false);
|
try (JarMap in = JarMap.open(args[args.length - 2], false);
|
||||||
OutputStream out = new FileOutputStream(args[args.length - 1])) {
|
FileOutputStream out = new FileOutputStream(args[args.length - 1])) {
|
||||||
if (args.length == 2) {
|
if (args.length == 2) {
|
||||||
sign(in, out);
|
sign(in, out);
|
||||||
} else if (args.length == 4) {
|
} else if (args.length == 4) {
|
||||||
|
136
app/signing/src/main/java/com/topjohnwu/signing/ZipUtils.java
Normal file
136
app/signing/src/main/java/com/topjohnwu/signing/ZipUtils.java
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
package com.topjohnwu.signing;
|
||||||
|
|
||||||
|
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 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;
|
||||||
|
|
||||||
|
private ZipUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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);
|
||||||
|
}
|
||||||
|
}
|
@ -39,7 +39,7 @@ class Keygen(context: Context) : CertKeyProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val start = Calendar.getInstance().apply { add(Calendar.MONTH, -3) }
|
private val start = Calendar.getInstance().apply { add(Calendar.MONTH, -3) }
|
||||||
private val end = Calendar.getInstance().apply { add(Calendar.YEAR, 30) }
|
private val end = start.apply { add(Calendar.YEAR, 30) }
|
||||||
|
|
||||||
override val cert get() = provider.cert
|
override val cert get() = provider.cert
|
||||||
override val key get() = provider.key
|
override val key get() = provider.key
|
||||||
|
@ -13,7 +13,7 @@ import com.topjohnwu.magisk.data.network.GithubRawServices
|
|||||||
import com.topjohnwu.magisk.ktx.get
|
import com.topjohnwu.magisk.ktx.get
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.ktx.writeTo
|
||||||
import com.topjohnwu.signing.JarMap
|
import com.topjohnwu.signing.JarMap
|
||||||
import com.topjohnwu.signing.SignAPK
|
import com.topjohnwu.signing.SignApk
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@ -29,10 +29,8 @@ import java.security.SecureRandom
|
|||||||
|
|
||||||
object PatchAPK {
|
object PatchAPK {
|
||||||
|
|
||||||
private const val ALPHA = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
|
||||||
private const val DIGITS = "0123456789"
|
private const val ALPHADOTS = "$ALPHA....."
|
||||||
const val ALPHANUM = ALPHA + DIGITS
|
|
||||||
private const val ALPHANUMDOTS = "$ALPHANUM............"
|
|
||||||
|
|
||||||
private const val APP_ID = "com.topjohnwu.magisk"
|
private const val APP_ID = "com.topjohnwu.magisk"
|
||||||
private const val APP_NAME = "Magisk Manager"
|
private const val APP_NAME = "Magisk Manager"
|
||||||
@ -48,7 +46,7 @@ object PatchAPK {
|
|||||||
next = if (prev == '.' || i == len - 1) {
|
next = if (prev == '.' || i == len - 1) {
|
||||||
ALPHA[random.nextInt(ALPHA.length)]
|
ALPHA[random.nextInt(ALPHA.length)]
|
||||||
} else {
|
} else {
|
||||||
ALPHANUMDOTS[random.nextInt(ALPHANUMDOTS.length)]
|
ALPHADOTS[random.nextInt(ALPHADOTS.length)]
|
||||||
}
|
}
|
||||||
builder.append(next)
|
builder.append(next)
|
||||||
prev = next
|
prev = next
|
||||||
@ -96,7 +94,7 @@ object PatchAPK {
|
|||||||
// Write apk changes
|
// Write apk changes
|
||||||
jar.getOutputStream(je).write(xml)
|
jar.getOutputStream(je).write(xml)
|
||||||
val keys = Keygen(get())
|
val keys = Keygen(get())
|
||||||
SignAPK.sign(keys.cert, keys.key, jar, FileOutputStream(out).buffered())
|
SignApk.sign(keys.cert, keys.key, jar, FileOutputStream(out))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
|
Loading…
x
Reference in New Issue
Block a user