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

Commit 9a41c93f authored by Alex Klyubin's avatar Alex Klyubin
Browse files

Use Builder pattern for ApkVerifier parameters.

This should make it easier to add parameters/options without breaking
existing clients.

Bug: 27461702
Change-Id: Ia4577f78d703a6b91828dd08492c78d5e9afb110
parent 239f2b0f
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);
    }
}