From: Alex Klyubin Date: Tue, 21 Jun 2016 17:46:56 +0000 (-0700) Subject: Use Builder pattern for ApkVerifier parameters. X-Git-Url: http://git.osdn.net/view?a=commitdiff_plain;h=9a41c93f410bad11091561381c34a1ff2e80abf3;p=android-x86%2Fbuild.git Use Builder pattern for ApkVerifier parameters. This should make it easier to add parameters/options without breaking existing clients. Bug: 27461702 Change-Id: Ia4577f78d703a6b91828dd08492c78d5e9afb110 --- 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 d509a48dc..f12b47f97 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 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 @@ public class ApkVerifier { * @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 @@ public class ApkVerifier { */ 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 @@ public class ApkVerifier { 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. + * + *

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 000000000..208033d6c --- /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 6ce0ac899..1cbb0af5f 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); + } }