Merge "Use Builder pattern for ApkVerifier parameters."
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java b/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java
index d509a48..f12b47f 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java
@@ -23,9 +23,13 @@
import com.android.apksigner.core.internal.apk.v2.V2SchemeVerifier;
import com.android.apksigner.core.internal.util.AndroidSdkVersion;
import com.android.apksigner.core.util.DataSource;
+import com.android.apksigner.core.util.DataSources;
import com.android.apksigner.core.zip.ZipFormatException;
+import java.io.Closeable;
+import java.io.File;
import java.io.IOException;
+import java.io.RandomAccessFile;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
@@ -42,6 +46,8 @@
*
* <p>The verifier is designed to closely mimic the behavior of Android platforms. This is to enable
* the verifier to be used for checking whether an APK's signatures will verify on Android.
+ *
+ * <p>Use {@link Builder} to obtain instances of this verifier.
*/
public class ApkVerifier {
@@ -49,6 +55,57 @@
private static final Map<Integer, String> SUPPORTED_APK_SIG_SCHEME_NAMES =
Collections.singletonMap(APK_SIGNATURE_SCHEME_V2_ID, "APK Signature Scheme v2");
+ private final File mApkFile;
+ private final DataSource mApkDataSource;
+
+ private final int mMinSdkVersion;
+ private final int mMaxSdkVersion;
+
+ private ApkVerifier(
+ File apkFile,
+ DataSource apkDataSource,
+ int minSdkVersion,
+ int maxSdkVersion) {
+ mApkFile = apkFile;
+ mApkDataSource = apkDataSource;
+ mMinSdkVersion = minSdkVersion;
+ mMaxSdkVersion = maxSdkVersion;
+ }
+
+ /**
+ * Verifies the APK's signatures and returns the result of verification. The APK can be
+ * considered verified iff the result's {@link Result#isVerified()} returns {@code true}.
+ * The verification result also includes errors, warnings, and information about signers.
+ *
+ * @throws IOException if an I/O error is encountered while reading the APK
+ * @throws ZipFormatException if the APK is malformed at ZIP format level
+ * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+ * required cryptographic algorithm implementation is missing
+ * @throws IllegalStateException if this verifier's configuration is missing required
+ * information.
+ */
+ public Result verify() throws IOException, ZipFormatException, NoSuchAlgorithmException,
+ IllegalStateException {
+ Closeable in = null;
+ try {
+ DataSource apk;
+ if (mApkDataSource != null) {
+ apk = mApkDataSource;
+ } else if (mApkFile != null) {
+ RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
+ in = f;
+ apk = DataSources.asDataSource(f, 0, f.length());
+ } else {
+ throw new IllegalStateException("APK not provided");
+ }
+ return verify(apk, mMinSdkVersion, mMaxSdkVersion);
+ } finally {
+ if (in != null) {
+ in.close();
+ }
+ }
+ }
+
/**
* Verifies the APK's signatures and returns the result of verification. The APK can be
* considered verified iff the result's {@link Result#isVerified()} returns {@code true}.
@@ -65,7 +122,7 @@
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
* required cryptographic algorithm implementation is missing
*/
- public Result verify(DataSource apk, int minSdkVersion, int maxSdkVersion)
+ private static Result verify(DataSource apk, int minSdkVersion, int maxSdkVersion)
throws IOException, ZipFormatException, NoSuchAlgorithmException {
if (minSdkVersion < 0) {
throw new IllegalArgumentException(
@@ -1050,17 +1107,16 @@
*/
private static class ByteArray {
private final byte[] mArray;
+ private final int mHashCode;
private ByteArray(byte[] arr) {
mArray = arr;
+ mHashCode = Arrays.hashCode(mArray);
}
@Override
public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + Arrays.hashCode(mArray);
- return result;
+ return mHashCode;
}
@Override
@@ -1075,10 +1131,103 @@
return false;
}
ByteArray other = (ByteArray) obj;
+ if (hashCode() != other.hashCode()) {
+ return false;
+ }
if (!Arrays.equals(mArray, other.mArray)) {
return false;
}
return true;
}
}
+
+ /**
+ * Builder of {@link ApkVerifier} instances.
+ *
+ * <p>Although not required, it is best to provide the SDK version (API Level) of the oldest
+ * Android platform on which the APK is supposed to be installed -- see
+ * {@link #setMinCheckedPlatformVersion(int)}. Without this information, APKs which use security
+ * features not supported on ancient Android platforms (e.g., SHA-256 digests or ECDSA
+ * signatures) will not verify.
+ */
+ public static class Builder {
+ private final File mApkFile;
+ private final DataSource mApkDataSource;
+
+ private int mMinSdkVersion = 1;
+ private int mMaxSdkVersion = Integer.MAX_VALUE;
+
+ /**
+ * Constructs a new {@code Builder} for verifying the provided APK file.
+ */
+ public Builder(File apk) {
+ if (apk == null) {
+ throw new NullPointerException("apk == null");
+ }
+ mApkFile = apk;
+ mApkDataSource = null;
+ }
+
+ /**
+ * Constructs a new {@code Builder} for verifying the provided APK.
+ */
+ public Builder(DataSource apk) {
+ if (apk == null) {
+ throw new NullPointerException("apk == null");
+ }
+ mApkDataSource = apk;
+ mApkFile = null;
+ }
+
+ /**
+ * Sets the oldest Android platform version for which the APK is verified. APK verification
+ * will confirm that the APK is expected to install successfully on all known Android
+ * platforms starting from the platform version with the provided API Level.
+ *
+ * <p>By default, the APK is checked for all platform versions. Thus, APKs which use
+ * security features not supported on ancient Android platforms (e.g., SHA-256 digests or
+ * ECDSA signatures) will not verify by default.
+ *
+ * @param minSdkVersion API Level of the oldest platform for which to verify the APK
+ *
+ * @see #setCheckedPlatformVersions(int, int)
+ */
+ public Builder setMinCheckedPlatformVersion(int minSdkVersion) {
+ mMinSdkVersion = minSdkVersion;
+ mMaxSdkVersion = Integer.MAX_VALUE;
+ return this;
+ }
+
+ /**
+ * Sets the range of Android platform versions for which the APK is verified. APK
+ * verification will confirm that the APK is expected to install successfully on Android
+ * platforms whose API Levels fall into this inclusive range.
+ *
+ * <p>By default, the APK is checked for all platform versions. Thus, APKs which use
+ * security features not supported on ancient Android platforms (e.g., SHA-256 digests or
+ * ECDSA signatures) will not verify by default.
+ *
+ * @param minSdkVersion API Level of the oldest platform for which to verify the APK
+ * @param maxSdkVersion API Level of the newest platform for which to verify the APK
+ *
+ * @see #setMinCheckedPlatformVersion(int)
+ */
+ public Builder setCheckedPlatformVersions(int minSdkVersion, int maxSdkVersion) {
+ mMinSdkVersion = minSdkVersion;
+ mMaxSdkVersion = maxSdkVersion;
+ return this;
+ }
+
+ /**
+ * Returns an {@link ApkVerifier} initialized according to the configuration of this
+ * builder.
+ */
+ public ApkVerifier build() {
+ return new ApkVerifier(
+ mApkFile,
+ mApkDataSource,
+ mMinSdkVersion,
+ mMaxSdkVersion);
+ }
+ }
}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSource.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSource.java
new file mode 100644
index 0000000..208033d
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSource.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksigner.core.internal.util;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+
+import com.android.apksigner.core.util.DataSink;
+import com.android.apksigner.core.util.DataSource;
+
+/**
+ * {@link DataSource} backed by a {@link RandomAccessFile}.
+ */
+public class RandomAccessFileDataSource implements DataSource {
+
+ private static final int MAX_READ_CHUNK_SIZE = 65536;
+
+ private final RandomAccessFile mFile;
+ private final long mOffset;
+ private final long mSize;
+
+ /**
+ * Constructs a new {@code RandomAccessFileDataSource} based on the data contained in the
+ * specified the whole file. Changes to the contents of the file, including the size of the
+ * file, will be visible in this data source.
+ */
+ public RandomAccessFileDataSource(RandomAccessFile file) {
+ mFile = file;
+ mOffset = 0;
+ mSize = -1;
+ }
+
+ /**
+ * Constructs a new {@code RandomAccessFileDataSource} based on the data contained in the
+ * specified region of the provided file. Changes to the contents of the file will be visible in
+ * this data source.
+ */
+ public RandomAccessFileDataSource(RandomAccessFile file, long offset, long size) {
+ if (offset < 0) {
+ throw new IllegalArgumentException("offset: " + size);
+ }
+ if (size < 0) {
+ throw new IllegalArgumentException("size: " + size);
+ }
+ mFile = file;
+ mOffset = offset;
+ mSize = size;
+ }
+
+ @Override
+ public long size() {
+ if (mSize == -1) {
+ try {
+ return mFile.length();
+ } catch (IOException e) {
+ return 0;
+ }
+ } else {
+ return mSize;
+ }
+ }
+
+ @Override
+ public RandomAccessFileDataSource slice(long offset, long size) {
+ long sourceSize = size();
+ checkChunkValid(offset, size, sourceSize);
+ if ((offset == 0) && (size == sourceSize)) {
+ return this;
+ }
+
+ return new RandomAccessFileDataSource(mFile, mOffset + offset, size);
+ }
+
+ @Override
+ public void feed(long offset, long size, DataSink sink) throws IOException {
+ long sourceSize = size();
+ checkChunkValid(offset, size, sourceSize);
+ if (size == 0) {
+ return;
+ }
+
+ long chunkOffsetInFile = mOffset + offset;
+ long remaining = size;
+ byte[] buf = new byte[(int) Math.min(remaining, MAX_READ_CHUNK_SIZE)];
+ while (remaining > 0) {
+ int chunkSize = (int) Math.min(remaining, buf.length);
+ synchronized (mFile) {
+ mFile.seek(chunkOffsetInFile);
+ mFile.readFully(buf, 0, chunkSize);
+ }
+ sink.consume(buf, 0, chunkSize);
+ chunkOffsetInFile += chunkSize;
+ remaining -= chunkSize;
+ }
+ }
+
+ @Override
+ public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
+ long sourceSize = size();
+ checkChunkValid(offset, size, sourceSize);
+ if (size == 0) {
+ return;
+ }
+
+ long offsetInFile = mOffset + offset;
+ int remaining = size;
+ FileChannel fileChannel = mFile.getChannel();
+ while (remaining > 0) {
+ int chunkSize;
+ synchronized (mFile) {
+ fileChannel.position(offsetInFile);
+ chunkSize = fileChannel.read(dest);
+ }
+ offsetInFile += chunkSize;
+ remaining -= chunkSize;
+ }
+ }
+
+ @Override
+ public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
+ ByteBuffer result = ByteBuffer.allocate(size);
+ copyTo(offset, size, result);
+ result.flip();
+ return result;
+ }
+
+ private static void checkChunkValid(long offset, long size, long sourceSize) {
+ if (offset < 0) {
+ throw new IllegalArgumentException("offset: " + offset);
+ }
+ if (size < 0) {
+ throw new IllegalArgumentException("size: " + size);
+ }
+ if (offset > sourceSize) {
+ throw new IllegalArgumentException(
+ "offset (" + offset + ") > source size (" + sourceSize + ")");
+ }
+ long endOffset = offset + size;
+ if (endOffset < offset) {
+ throw new IllegalArgumentException(
+ "offset (" + offset + ") + size (" + size + ") overflow");
+ }
+ if (endOffset > sourceSize) {
+ throw new IllegalArgumentException(
+ "offset (" + offset + ") + size (" + size
+ + ") > source size (" + sourceSize +")");
+ }
+ }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSources.java b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSources.java
index 6ce0ac8..1cbb0af 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSources.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSources.java
@@ -1,7 +1,9 @@
package com.android.apksigner.core.util;
import com.android.apksigner.core.internal.util.ByteBufferDataSource;
+import com.android.apksigner.core.internal.util.RandomAccessFileDataSource;
+import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
/**
@@ -21,4 +23,26 @@
}
return new ByteBufferDataSource(buffer);
}
+
+ /**
+ * Returns a {@link DataSource} backed by the provided {@link RandomAccessFile}. Changes to the
+ * file, including changes to size of file, will be visible in the data source.
+ */
+ public static DataSource asDataSource(RandomAccessFile file) {
+ if (file == null) {
+ throw new NullPointerException();
+ }
+ return new RandomAccessFileDataSource(file);
+ }
+
+ /**
+ * Returns a {@link DataSource} backed by the provided region of the {@link RandomAccessFile}.
+ * Changes to the file will be visible in the data source.
+ */
+ public static DataSource asDataSource(RandomAccessFile file, long offset, long size) {
+ if (file == null) {
+ throw new NullPointerException();
+ }
+ return new RandomAccessFileDataSource(file, offset, size);
+ }
}