Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 744e7746 authored by Treehugger Robot's avatar Treehugger Robot Committed by Gerrit Code Review
Browse files

Merge "Use Builder pattern for ApkVerifier parameters."

parents 239f2b0f 9a41c93f
Loading
Loading
Loading
Loading
+154 −5
Original line number Diff line number Diff line
@@ -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);
        }
    }
}
+165 −0
Original line number Diff line number Diff line
/*
 * 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  +")");
        }
    }
}
+24 −0
Original line number Diff line number Diff line
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);
    }
}