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 d509a48dc7..f12b47f977 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.SignatureAlgorithm; 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 @@ import java.util.Set; * *
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. + * + *
Use {@link Builder} to obtain instances of this verifier.
*/
public class ApkVerifier {
@@ -49,6 +55,57 @@ public class ApkVerifier {
private static final Map 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.
+ *
+ * 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.
+ *
+ * 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 0000000000..208033d6c8
--- /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 6ce0ac899d..1cbb0af5fa 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 @@ public abstract class DataSources {
}
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);
+ }
}