Update to APK Signature Scheme v2

This commit is contained in:
vvb2060 2020-08-08 05:00:49 -07:00 committed by John Wu
parent fe2388394d
commit 2e95d9f07e
7 changed files with 1215 additions and 543 deletions

View 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);
}
}
}

View File

@ -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;
}
} }
} }

View File

@ -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;
}
}
}

View File

@ -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) {

View 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);
}
}

View File

@ -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

View File

@ -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