Import of apksig using Copybara.
- 237975534 Sync apksig from AOSP to pick up parallel signing impleme... by kudasov <kudasov@google.com>
- 237284064 Automated g4 rollback of changelist 237191610. by kudasov <kudasov@google.com>
- 237191610 Sync apksig from AOSP to pick up parallel signing impleme... by kudasov <kudasov@google.com>
PiperOrigin-RevId: 237975534
Change-Id: I55a0afd5fbf47d7527c9bebb46e755843c628ffd
diff --git a/src/main/java/com/android/apksig/ApkSignerEngine.java b/src/main/java/com/android/apksig/ApkSignerEngine.java
index 9f77eb1..138bc38 100644
--- a/src/main/java/com/android/apksig/ApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/ApkSignerEngine.java
@@ -19,6 +19,7 @@
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSource;
+import com.android.apksig.util.RunnablesExecutor;
import java.io.Closeable;
import java.io.IOException;
import java.lang.UnsupportedOperationException;
@@ -116,6 +117,10 @@
*/
public interface ApkSignerEngine extends Closeable {
+ default void setExecutor(RunnablesExecutor executor) {
+ throw new UnsupportedOperationException("setExecutor method is not implemented");
+ }
+
/**
* Initializes the signer engine with the data already present in the apk (if any). There
* might already be data that can be reused if the entries has not been changed.
diff --git a/src/main/java/com/android/apksig/ApkVerifier.java b/src/main/java/com/android/apksig/ApkVerifier.java
index 6e0a520..3e1e7da 100644
--- a/src/main/java/com/android/apksig/ApkVerifier.java
+++ b/src/main/java/com/android/apksig/ApkVerifier.java
@@ -29,6 +29,7 @@
import com.android.apksig.internal.zip.CentralDirectoryRecord;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
+import com.android.apksig.util.RunnablesExecutor;
import com.android.apksig.zip.ZipFormatException;
import java.io.Closeable;
import java.io.File;
@@ -211,12 +212,13 @@
// verification. If the signature is found but does not verify, the APK is rejected.
Set<Integer> foundApkSigSchemeIds = new HashSet<>(2);
if (maxSdkVersion >= AndroidSdkVersion.N) {
-
+ RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED;
// Android P and newer attempts to verify APKs using APK Signature Scheme v3
if (maxSdkVersion >= AndroidSdkVersion.P) {
try {
ApkSigningBlockUtils.Result v3Result =
V3SchemeVerifier.verify(
+ executor,
apk,
zipSections,
Math.max(minSdkVersion, AndroidSdkVersion.P),
@@ -239,6 +241,7 @@
try {
ApkSigningBlockUtils.Result v2Result =
V2SchemeVerifier.verify(
+ executor,
apk,
zipSections,
supportedSchemeNames,
diff --git a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
index 778154e..c88239e 100644
--- a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
@@ -33,6 +33,7 @@
import com.android.apksig.util.DataSinks;
import com.android.apksig.util.DataSource;
+import com.android.apksig.util.RunnablesExecutor;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -141,6 +142,9 @@
*/
private OutputApkSigningBlockRequestImpl mAddSigningBlockRequest;
+
+ private RunnablesExecutor mExecutor = RunnablesExecutor.SINGLE_THREADED;
+
private DefaultApkSignerEngine(
List<SignerConfig> signerConfigs,
int minSdkVersion,
@@ -447,6 +451,11 @@
}
@Override
+ public void setExecutor(RunnablesExecutor executor) {
+ mExecutor = executor;
+ }
+
+ @Override
public void inputApkSigningBlock(DataSource apkSigningBlock) {
checkNotClosed();
@@ -774,16 +783,25 @@
List<ApkSigningBlockUtils.SignerConfig> v2SignerConfigs =
createV2SignerConfigs(apkSigningBlockPaddingSupported);
signingSchemeBlocks.add(
- V2SchemeSigner.generateApkSignatureSchemeV2Block(beforeCentralDir,
- zipCentralDirectory, eocd, v2SignerConfigs, mV3SigningEnabled));
+ V2SchemeSigner.generateApkSignatureSchemeV2Block(
+ mExecutor,
+ beforeCentralDir,
+ zipCentralDirectory,
+ eocd,
+ v2SignerConfigs,
+ mV3SigningEnabled));
}
if (mV3SigningEnabled) {
invalidateV3Signature();
List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs =
createV3SignerConfigs(apkSigningBlockPaddingSupported);
signingSchemeBlocks.add(
- V3SchemeSigner.generateApkSignatureSchemeV3Block(beforeCentralDir,
- zipCentralDirectory, eocd, v3SignerConfigs));
+ V3SchemeSigner.generateApkSignatureSchemeV3Block(
+ mExecutor,
+ beforeCentralDir,
+ zipCentralDirectory,
+ eocd,
+ v3SignerConfigs));
}
// create APK Signing Block with v2 and/or v3 blocks
diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
index 556c643..cc69af3 100644
--- a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
@@ -16,8 +16,8 @@
package com.android.apksig.internal.apk;
-import com.android.apksig.SigningCertificateLineage;
import com.android.apksig.ApkVerifier;
+import com.android.apksig.SigningCertificateLineage;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkSigningBlockNotFoundException;
import com.android.apksig.apk.ApkUtils;
@@ -31,6 +31,7 @@
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
+import com.android.apksig.util.RunnablesExecutor;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
@@ -57,12 +58,18 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
public class ApkSigningBlockUtils {
private static final char[] HEX_DIGITS = "01234567890abcdef".toCharArray();
- private static final int CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
+ private static final long CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
public static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096;
public static final byte[] APK_SIGNING_BLOCK_MAGIC =
new byte[] {
@@ -147,6 +154,7 @@
* exhibit the same behavior on all Android platform versions.
*/
public static void verifyIntegrity(
+ RunnablesExecutor executor,
DataSource beforeApkSigningBlock,
DataSource centralDir,
ByteBuffer eocd,
@@ -174,6 +182,7 @@
try {
actualContentDigests =
computeContentDigests(
+ executor,
contentDigestAlgorithms,
beforeApkSigningBlock,
centralDir,
@@ -400,6 +409,7 @@
}
public static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
+ RunnablesExecutor executor,
Set<ContentDigestAlgorithm> digestAlgorithms,
DataSource beforeCentralDir,
DataSource centralDir,
@@ -409,7 +419,9 @@
.filter(a -> a == ContentDigestAlgorithm.CHUNKED_SHA256 ||
a == ContentDigestAlgorithm.CHUNKED_SHA512)
.collect(Collectors.toSet());
- computeOneMbChunkContentDigests(oneMbChunkBasedAlgorithm,
+ computeOneMbChunkContentDigests(
+ executor,
+ oneMbChunkBasedAlgorithm,
new DataSource[] { beforeCentralDir, centralDir, eocd },
contentDigests);
@@ -419,7 +431,7 @@
return contentDigests;
}
- private static void computeOneMbChunkContentDigests(
+ static void computeOneMbChunkContentDigests(
Set<ContentDigestAlgorithm> digestAlgorithms,
DataSource[] contents,
Map<ContentDigestAlgorithm, byte[]> outputContentDigests)
@@ -522,6 +534,208 @@
}
}
+ static void computeOneMbChunkContentDigests(
+ RunnablesExecutor executor,
+ Set<ContentDigestAlgorithm> digestAlgorithms,
+ DataSource[] contents,
+ Map<ContentDigestAlgorithm, byte[]> outputContentDigests)
+ throws NoSuchAlgorithmException, DigestException {
+ long chunkCountLong = 0;
+ for (DataSource input : contents) {
+ chunkCountLong +=
+ getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
+ }
+ if (chunkCountLong > Integer.MAX_VALUE) {
+ throw new DigestException("Input too long: " + chunkCountLong + " chunks");
+ }
+ int chunkCount = (int) chunkCountLong;
+
+ List<ChunkDigests> chunkDigestsList = new ArrayList<>(digestAlgorithms.size());
+ for (ContentDigestAlgorithm algorithms : digestAlgorithms) {
+ chunkDigestsList.add(new ChunkDigests(algorithms, chunkCount));
+ }
+
+ ChunkSupplier chunkSupplier = new ChunkSupplier(contents);
+ executor.execute(() -> new ChunkDigester(chunkSupplier, chunkDigestsList));
+
+ // Compute and write out final digest for each algorithm.
+ for (ChunkDigests chunkDigests : chunkDigestsList) {
+ MessageDigest messageDigest = chunkDigests.createMessageDigest();
+ outputContentDigests.put(
+ chunkDigests.algorithm,
+ messageDigest.digest(chunkDigests.concatOfDigestsOfChunks));
+ }
+ }
+
+ private static class ChunkDigests {
+ private final ContentDigestAlgorithm algorithm;
+ private final int digestOutputSize;
+ private final byte[] concatOfDigestsOfChunks;
+
+ private ChunkDigests(ContentDigestAlgorithm algorithm, int chunkCount) {
+ this.algorithm = algorithm;
+ digestOutputSize = this.algorithm.getChunkDigestOutputSizeBytes();
+ concatOfDigestsOfChunks = new byte[1 + 4 + chunkCount * digestOutputSize];
+
+ // Fill the initial values of the concatenated digests of chunks, which is
+ // {0x5a, 4-bytes-of-little-endian-chunk-count, digests*...}.
+ concatOfDigestsOfChunks[0] = 0x5a;
+ setUnsignedInt32LittleEndian(chunkCount, concatOfDigestsOfChunks, 1);
+ }
+
+ private MessageDigest createMessageDigest() throws NoSuchAlgorithmException {
+ return MessageDigest.getInstance(algorithm.getJcaMessageDigestAlgorithm());
+ }
+
+ private int getOffset(int chunkIndex) {
+ return 1 + 4 + chunkIndex * digestOutputSize;
+ }
+ }
+
+ /**
+ * A per-thread digest worker.
+ */
+ private static class ChunkDigester implements Runnable {
+ private final ChunkSupplier dataSupplier;
+ private final List<ChunkDigests> chunkDigests;
+ private final List<MessageDigest> messageDigests;
+ private final DataSink mdSink;
+
+ private ChunkDigester(ChunkSupplier dataSupplier, List<ChunkDigests> chunkDigests) {
+ this.dataSupplier = dataSupplier;
+ this.chunkDigests = chunkDigests;
+ messageDigests = new ArrayList<>(chunkDigests.size());
+ for (ChunkDigests chunkDigest : chunkDigests) {
+ try {
+ messageDigests.add(chunkDigest.createMessageDigest());
+ } catch (NoSuchAlgorithmException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+ mdSink = DataSinks.asDataSink(messageDigests.toArray(new MessageDigest[0]));
+ }
+
+ @Override
+ public void run() {
+ byte[] chunkContentPrefix = new byte[5];
+ chunkContentPrefix[0] = (byte) 0xa5;
+
+ try {
+ for (ChunkSupplier.Chunk chunk = dataSupplier.get();
+ chunk != null;
+ chunk = dataSupplier.get()) {
+ long size = chunk.dataSource.size();
+ if (size > CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES) {
+ throw new RuntimeException("Chunk size greater than expected: " + size);
+ }
+
+ // First update with the chunk prefix.
+ setUnsignedInt32LittleEndian((int)size, chunkContentPrefix, 1);
+ mdSink.consume(chunkContentPrefix, 0, chunkContentPrefix.length);
+
+ // Then update with the chunk data.
+ chunk.dataSource.feed(0, size, mdSink);
+
+ // Now finalize chunk for all algorithms.
+ for (int i = 0; i < chunkDigests.size(); i++) {
+ ChunkDigests chunkDigest = chunkDigests.get(i);
+ int actualDigestSize = messageDigests.get(i).digest(
+ chunkDigest.concatOfDigestsOfChunks,
+ chunkDigest.getOffset(chunk.chunkIndex),
+ chunkDigest.digestOutputSize);
+ if (actualDigestSize != chunkDigest.digestOutputSize) {
+ throw new RuntimeException(
+ "Unexpected output size of " + chunkDigest.algorithm
+ + " digest: " + actualDigestSize);
+ }
+ }
+ }
+ } catch (IOException | DigestException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * Thread-safe 1MB DataSource chunk supplier. When bounds are met in a
+ * supplied {@link DataSource}, the data from the next {@link DataSource}
+ * are NOT concatenated. Only the next call to get() will fetch from the
+ * next {@link DataSource} in the input {@link DataSource} array.
+ */
+ private static class ChunkSupplier implements Supplier<ChunkSupplier.Chunk> {
+ private final DataSource[] dataSources;
+ private final int[] chunkCounts;
+ private final int totalChunkCount;
+ private final AtomicInteger nextIndex;
+
+ private ChunkSupplier(DataSource[] dataSources) {
+ this.dataSources = dataSources;
+ chunkCounts = new int[dataSources.length];
+ int totalChunkCount = 0;
+ for (int i = 0; i < dataSources.length; i++) {
+ long chunkCount = getChunkCount(dataSources[i].size(),
+ CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
+ if (chunkCount > Integer.MAX_VALUE) {
+ throw new RuntimeException(
+ String.format(
+ "Number of chunks in dataSource[%d] is greater than max int.",
+ i));
+ }
+ chunkCounts[i] = (int)chunkCount;
+ totalChunkCount += chunkCount;
+ }
+ this.totalChunkCount = totalChunkCount;
+ nextIndex = new AtomicInteger(0);
+ }
+
+ /**
+ * We map an integer index to the termination-adjusted dataSources 1MB chunks.
+ * Note that {@link Chunk}s could be less than 1MB, namely the last 1MB-aligned
+ * blocks in each input {@link DataSource} (unless the DataSource itself is
+ * 1MB-aligned).
+ */
+ @Override
+ public ChunkSupplier.Chunk get() {
+ int index = nextIndex.getAndIncrement();
+ if (index < 0 || index >= totalChunkCount) {
+ return null;
+ }
+
+ int dataSourceIndex = 0;
+ int dataSourceChunkOffset = index;
+ for (; dataSourceIndex < dataSources.length; dataSourceIndex++) {
+ if (dataSourceChunkOffset < chunkCounts[dataSourceIndex]) {
+ break;
+ }
+ dataSourceChunkOffset -= chunkCounts[dataSourceIndex];
+ }
+
+ long remainingSize = Math.min(
+ dataSources[dataSourceIndex].size() -
+ dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES,
+ CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
+ // Note that slicing may involve its own locking. We may wish to reimplement the
+ // underlying mechanism to get rid of that lock (e.g. ByteBufferDataSource should
+ // probably get reimplemented to a delegate model, such that grabbing a slice
+ // doesn't incur a lock).
+ return new Chunk(
+ dataSources[dataSourceIndex].slice(
+ dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES,
+ remainingSize),
+ index);
+ }
+
+ static class Chunk {
+ private final int chunkIndex;
+ private final DataSource dataSource;
+
+ private Chunk(DataSource parentSource, int chunkIndex) {
+ this.chunkIndex = chunkIndex;
+ dataSource = parentSource;
+ }
+ }
+ }
+
private static void computeApkVerityDigest(DataSource beforeCentralDir, DataSource centralDir,
DataSource eocd, Map<ContentDigestAlgorithm, byte[]> outputContentDigests)
throws IOException, NoSuchAlgorithmException {
@@ -545,7 +759,7 @@
outputContentDigests.put(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256, encoded.array());
}
- private static final long getChunkCount(long inputSize, int chunkSize) {
+ private static long getChunkCount(long inputSize, long chunkSize) {
return (inputSize + chunkSize - 1) / chunkSize;
}
@@ -795,6 +1009,7 @@
*/
public static Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>>
computeContentDigests(
+ RunnablesExecutor executor,
DataSource beforeCentralDir,
DataSource centralDir,
DataSource eocd,
@@ -818,6 +1033,7 @@
try {
contentDigests =
computeContentDigests(
+ executor,
contentDigestAlgorithms,
beforeCentralDir,
centralDir,
diff --git a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java
index e9f710b..d8e4723 100644
--- a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java
@@ -27,7 +27,7 @@
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
-
+import com.android.apksig.util.RunnablesExecutor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -139,6 +139,7 @@
}
public static Pair<byte[], Integer> generateApkSignatureSchemeV2Block(
+ RunnablesExecutor executor,
DataSource beforeCentralDir,
DataSource centralDir,
DataSource eocd,
@@ -148,8 +149,8 @@
SignatureException {
Pair<List<SignerConfig>,
Map<ContentDigestAlgorithm, byte[]>> digestInfo =
- ApkSigningBlockUtils.computeContentDigests(beforeCentralDir, centralDir, eocd,
- signerConfigs);
+ ApkSigningBlockUtils.computeContentDigests(
+ executor, beforeCentralDir, centralDir, eocd, signerConfigs);
return generateApkSignatureSchemeV2Block(
digestInfo.getFirst(), digestInfo.getSecond(),v3SigningEnabled);
}
diff --git a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
index bbef027..51c40bd 100644
--- a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
@@ -27,8 +27,7 @@
import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.util.DataSource;
-
-import java.io.ByteArrayInputStream;
+import com.android.apksig.util.RunnablesExecutor;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
@@ -89,6 +88,7 @@
* @throws IOException if an I/O error occurs when reading the APK
*/
public static ApkSigningBlockUtils.Result verify(
+ RunnablesExecutor executor,
DataSource apk,
ApkUtils.ZipSections zipSections,
Map<Integer, String> supportedApkSigSchemeNames,
@@ -110,7 +110,8 @@
signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
ByteBuffer eocd = signatureInfo.eocd;
- verify(beforeApkSigningBlock,
+ verify(executor,
+ beforeApkSigningBlock,
signatureInfo.signatureBlock,
centralDir,
eocd,
@@ -125,13 +126,14 @@
/**
* Verifies the provided APK's v2 signatures and outputs the results into the provided
* {@code result}. APK is considered verified only if there are no errors reported in the
- * {@code result}. See {@link #verify(DataSource, ApkUtils.ZipSections, Map, Set, int, int)} for
- * more information about the contract of this method.
+ * {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, Map,
+ * Set, int, int)} for more information about the contract of this method.
*
* @param result result populated by this method with interesting information about the APK,
* such as information about signers, and verification errors and warnings.
*/
private static void verify(
+ RunnablesExecutor executor,
DataSource beforeApkSigningBlock,
ByteBuffer apkSignatureSchemeV2Block,
DataSource centralDir,
@@ -155,7 +157,7 @@
return;
}
ApkSigningBlockUtils.verifyIntegrity(
- beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
+ executor, beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
if (!result.containsErrors()) {
result.verified = true;
}
diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java
index fc70a0a..722b304 100644
--- a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java
@@ -29,7 +29,7 @@
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
-
+import com.android.apksig.util.RunnablesExecutor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -128,6 +128,7 @@
}
public static Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
+ RunnablesExecutor executor,
DataSource beforeCentralDir,
DataSource centralDir,
DataSource eocd,
@@ -136,8 +137,8 @@
SignatureException {
Pair<List<SignerConfig>,
Map<ContentDigestAlgorithm, byte[]>> digestInfo =
- ApkSigningBlockUtils.computeContentDigests(beforeCentralDir, centralDir, eocd,
- signerConfigs);
+ ApkSigningBlockUtils.computeContentDigests(
+ executor, beforeCentralDir, centralDir, eocd, signerConfigs);
return generateApkSignatureSchemeV3Block(digestInfo.getFirst(), digestInfo.getSecond());
}
diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
index 9a2932b..16a6408 100644
--- a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
@@ -33,7 +33,7 @@
import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.util.DataSource;
-
+import com.android.apksig.util.RunnablesExecutor;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
@@ -94,6 +94,7 @@
* @throws IOException if an I/O error occurs when reading the APK
*/
public static ApkSigningBlockUtils.Result verify(
+ RunnablesExecutor executor,
DataSource apk,
ApkUtils.ZipSections zipSections,
int minSdkVersion,
@@ -118,7 +119,8 @@
minSdkVersion = AndroidSdkVersion.P;
}
- verify(beforeApkSigningBlock,
+ verify(executor,
+ beforeApkSigningBlock,
signatureInfo.signatureBlock,
centralDir,
eocd,
@@ -131,13 +133,14 @@
/**
* Verifies the provided APK's v3 signatures and outputs the results into the provided
* {@code result}. APK is considered verified only if there are no errors reported in the
- * {@code result}. See {@link #verify(DataSource, ApkUtils.ZipSections, int, int)} for more
- * information about the contract of this method.
+ * {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, int,
+ * int)} for more information about the contract of this method.
*
* @param result result populated by this method with interesting information about the APK,
* such as information about signers, and verification errors and warnings.
*/
private static void verify(
+ RunnablesExecutor executor,
DataSource beforeApkSigningBlock,
ByteBuffer apkSignatureSchemeV3Block,
DataSource centralDir,
@@ -153,7 +156,7 @@
return;
}
ApkSigningBlockUtils.verifyIntegrity(
- beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
+ executor, beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
// make sure that the v3 signers cover the entire targeted sdk version ranges and that the
// longest SigningCertificateHistory, if present, corresponds to the newest platform
diff --git a/src/main/java/com/android/apksig/util/RunnablesExecutor.java b/src/main/java/com/android/apksig/util/RunnablesExecutor.java
new file mode 100644
index 0000000..04ec1d8
--- /dev/null
+++ b/src/main/java/com/android/apksig/util/RunnablesExecutor.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.util;
+
+public interface RunnablesExecutor {
+ RunnablesExecutor SINGLE_THREADED = p -> p.getRunnable().run();
+
+ void execute(RunnablesProvider provider);
+}
diff --git a/src/main/java/com/android/apksig/util/RunnablesProvider.java b/src/main/java/com/android/apksig/util/RunnablesProvider.java
new file mode 100644
index 0000000..5b7bad2
--- /dev/null
+++ b/src/main/java/com/android/apksig/util/RunnablesProvider.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.util;
+
+public interface RunnablesProvider {
+ Runnable getRunnable();
+}
diff --git a/src/test/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsTest.java b/src/test/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsTest.java
new file mode 100644
index 0000000..7eb7c9b
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsTest.java
@@ -0,0 +1,132 @@
+package com.android.apksig.internal.apk;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apksig.util.RunnablesExecutor;
+import com.android.apksig.util.RunnablesProvider;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.Future;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ApkSigningBlockUtilsTest {
+ @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private static final int BASE = 255; // Intentionally not power of 2 to test properly
+
+ DataSource[] dataSource;
+
+ final Set<ContentDigestAlgorithm> algos = EnumSet.of(ContentDigestAlgorithm.CHUNKED_SHA512);
+
+ @Before
+ public void setUp() throws Exception {
+ byte[] part1 = new byte[80 * 1024 * 1024 + 12345];
+ for (int i = 0; i < part1.length; ++i) {
+ part1[i] = (byte)(i % BASE);
+ }
+
+ File dataFile = temporaryFolder.newFile("fake.apk");
+
+ try (FileOutputStream fos = new FileOutputStream(dataFile)) {
+ fos.write(part1);
+ }
+ RandomAccessFile raf = new RandomAccessFile(dataFile, "r");
+
+ byte[] part2 = new byte[1_500_000];
+ for (int i = 0; i < part2.length; ++i) {
+ part2[i] = (byte)(i % BASE);
+ }
+ byte[] part3 = new byte[30_000];
+ for (int i = 0; i < part3.length; ++i) {
+ part3[i] = (byte)(i % BASE);
+ }
+ dataSource = new DataSource[] {
+ DataSources.asDataSource(raf),
+ DataSources.asDataSource(ByteBuffer.wrap(part2)),
+ DataSources.asDataSource(ByteBuffer.wrap(part3)),
+ };
+ }
+
+ @Test
+ public void testNewVersionMatchesOld() throws Exception {
+ Map<ContentDigestAlgorithm, byte[]> outputContentDigestsOld =
+ new EnumMap<>(ContentDigestAlgorithm.class);
+ Map<ContentDigestAlgorithm, byte[]> outputContentDigestsNew =
+ new EnumMap<>(ContentDigestAlgorithm.class);
+
+ ApkSigningBlockUtils.computeOneMbChunkContentDigests(
+ algos, dataSource, outputContentDigestsOld);
+
+ ApkSigningBlockUtils.computeOneMbChunkContentDigests(
+ RunnablesExecutor.SINGLE_THREADED,
+ algos, dataSource, outputContentDigestsNew);
+
+ assertEqualDigests(outputContentDigestsOld, outputContentDigestsNew);
+ }
+
+ @Test
+ public void testMultithreadedVersionMatchesSinglethreaded() throws Exception {
+ Map<ContentDigestAlgorithm, byte[]> outputContentDigests =
+ new EnumMap<>(ContentDigestAlgorithm.class);
+ Map<ContentDigestAlgorithm, byte[]> outputContentDigestsMultithreaded =
+ new EnumMap<>(ContentDigestAlgorithm.class);
+
+ ApkSigningBlockUtils.computeOneMbChunkContentDigests(
+ RunnablesExecutor.SINGLE_THREADED,
+ algos, dataSource, outputContentDigests);
+
+ ApkSigningBlockUtils.computeOneMbChunkContentDigests(
+ (RunnablesProvider provider) -> {
+ ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
+ int jobCount = forkJoinPool.getParallelism();
+ List<Future<?>> jobs = new ArrayList<>(jobCount);
+
+ for (int i = 0; i < jobCount; i++) {
+ jobs.add(forkJoinPool.submit(provider.getRunnable()));
+ }
+
+ try {
+ for (Future<?> future : jobs) {
+ future.get();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(e);
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ },
+ algos, dataSource, outputContentDigestsMultithreaded);
+
+ assertEqualDigests(outputContentDigestsMultithreaded, outputContentDigests);
+ }
+
+ private void assertEqualDigests(
+ Map<ContentDigestAlgorithm, byte[]> d1, Map<ContentDigestAlgorithm, byte[]> d2) {
+ assertEquals(d1.keySet(), d2.keySet());
+ for (ContentDigestAlgorithm algo : d1.keySet()) {
+ byte[] digest1 = d1.get(algo);
+ byte[] digest2 = d2.get(algo);
+ assertArrayEquals(digest1, digest2);
+ }
+ }
+}