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

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

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 certificates; /** * List of signature algorithms with which to sign (see {@code SIGNATURE_...} constants). */ public List signatureAlgorithms; } /** * Signs the provided APK using APK Signature Scheme v2 and returns the signed APK as a list of * consecutive chunks. * *

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 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 contentDigestAlgorithms = new HashSet<>(); for (SignerConfig signerConfig : signerConfigs) { for (int signatureAlgorithm : signerConfig.signatureAlgorithms) { contentDigestAlgorithms.add( getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm)); } } // Compute digests of APK contents. Map 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 computeContentDigests( Set 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 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 result = new HashMap<>(digestAlgorithms.size()); for (Map.Entry 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 signerConfigs, Map 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 signerConfigs, Map contentDigests) throws InvalidKeyException, SignatureException { // FORMAT: // * length-prefixed sequence of length-prefixed signer blocks. List signerBlocks = new ArrayList<>(signerConfigs.size()); int signerNumber = 0; for (SignerConfig signerConfig : signerConfigs) { signerNumber++; byte[] signerBlock; try { signerBlock = generateSignerBlock(signerConfig, contentDigests); } catch (InvalidKeyException e) { throw new InvalidKeyException("Signer #" + signerNumber + " failed", e); } catch (SignatureException e) { throw new SignatureException("Signer #" + signerNumber + " failed", e); } signerBlocks.add(signerBlock); } return encodeAsSequenceOfLengthPrefixedElements( new byte[][] { encodeAsSequenceOfLengthPrefixedElements(signerBlocks), }); } private static byte[] generateSignerBlock( SignerConfig signerConfig, Map contentDigests) throws InvalidKeyException, SignatureException { if (signerConfig.certificates.isEmpty()) { throw new SignatureException("No certificates configured for signer"); } PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); byte[] encodedPublicKey = encodePublicKey(publicKey); V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData(); try { signedData.certificates = encodeCertificates(signerConfig.certificates); } catch (CertificateEncodingException e) { throw new SignatureException("Failed to encode certificates", e); } List> digests = new ArrayList<>(signerConfig.signatureAlgorithms.size()); for (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 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> signatures; public byte[] publicKey; } private static final class SignedData { public List> digests; public List 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 encodeCertificates(List certificates) throws CertificateEncodingException { List result = new ArrayList<>(); for (X509Certificate certificate : certificates) { result.add(certificate.getEncoded()); } return result; } private static byte[] encodeAsSequenceOfLengthPrefixedElements(List sequence) { return encodeAsSequenceOfLengthPrefixedElements( sequence.toArray(new byte[sequence.size()][])); } private static byte[] encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence) { int payloadSize = 0; for (byte[] element : sequence) { payloadSize += 4 + element.length; } ByteBuffer result = ByteBuffer.allocate(payloadSize); result.order(ByteOrder.LITTLE_ENDIAN); for (byte[] element : sequence) { result.putInt(element.length); result.put(element); } return result.array(); } private static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( List> sequence) { int resultSize = 0; for (Pair element : sequence) { resultSize += 12 + element.getSecond().length; } ByteBuffer result = ByteBuffer.allocate(resultSize); result.order(ByteOrder.LITTLE_ENDIAN); for (Pair element : sequence) { byte[] second = element.getSecond(); result.putInt(8 + second.length); result.putInt(element.getFirst()); result.putInt(second.length); result.put(second); } return result.array(); } /** * Relative get method for reading {@code size} number of bytes from the current * position of this buffer. * *

This method reads the next {@code size} bytes at this buffer's current position, * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to * {@code size}, byte order set to this buffer's byte order; and then increments the position by * {@code size}. */ private static ByteBuffer getByteBuffer(ByteBuffer source, int size) { 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 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 { private final A mFirst; private final B mSecond; private Pair(A first, B second) { mFirst = first; mSecond = second; } public static Pair 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); } } }