Merge \"Use Builder pattern for ApkVerifier parameters.\"

am: 744e7746c4

Change-Id: I36d18044ea627ff9f5f83ef8463476de07ce45ff
This commit is contained in:
Alex Klyubin
2016-06-23 00:37:28 +00:00
committed by android-build-merger
3 changed files with 343 additions and 5 deletions

View File

@@ -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;
*
* <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 @@ public class ApkVerifier {
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 @@ 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.
*
* <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);
}
}
}

View File

@@ -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 +")");
}
}
}

View File

@@ -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);
}
}