/*
 * Copyright 2018 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 androidx.core.content.pm;

import android.annotation.SuppressLint;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.content.pm.SigningInfo;
import android.os.Build;

import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.Size;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

/** Helper for accessing features in {@link PackageInfo}. */
public final class PackageInfoCompat {
    /**
     * Return {@link android.R.attr#versionCode} and {@link android.R.attr#versionCodeMajor}
     * combined together as a single long value. The {@code versionCodeMajor} is placed in the
     * upper 32 bits on Android P or newer, otherwise these bits are all set to 0.
     *
     * @see PackageInfo#getLongVersionCode()
     */
    @SuppressWarnings("deprecation")
    public static long getLongVersionCode(@NonNull PackageInfo info) {
        if (Build.VERSION.SDK_INT >= 28) {
            return Api28Impl.getLongVersionCode(info);
        }
        return info.versionCode;
    }

    /**
     * Retrieve the {@link Signature} array for the given package. This returns some of
     * certificates, depending on whether the package in question is multi-signed or has signing
     * history.
     *
     * <note>
     * <p>
     * Security/identity verification should <b>not</b> be done with this method. This is only
     * intended to return some array of certificates that correspond to a package.
     * </p>
     * <p>
     * If verification if required, either use
     * {@link #hasSignatures(PackageManager, String, Map, boolean)} or manually verify the set of
     * certificates using {@link PackageManager#GET_SIGNING_CERTIFICATES} or
     * {@link PackageManager#GET_SIGNATURES}.
     * </p>
     * </note>
     *
     * @param packageManager The {@link PackageManager} instance to query against.
     * @param packageName    The package to query the {@param packageManager} for. Query by app
     *                       UID is only supported by manually choosing a package name
     *                       returned in {@link PackageManager#getPackagesForUid(int)}.
     * @return an array of certificates the app is signed with
     * @throws PackageManager.NameNotFoundException if the package cannot be found through the
     *                                              provided {@param packageManager}
     */
    @NonNull
    @SuppressWarnings("deprecation")
    public static List<Signature> getSignatures(@NonNull PackageManager packageManager,
            @NonNull String packageName) throws PackageManager.NameNotFoundException {
        Signature[] array;
        if (Build.VERSION.SDK_INT >= 28) {
            PackageInfo pkgInfo = packageManager.getPackageInfo(packageName,
                    PackageManager.GET_SIGNING_CERTIFICATES);
            SigningInfo signingInfo = pkgInfo.signingInfo;
            if (Api28Impl.hasMultipleSigners(signingInfo)) {
                array = Api28Impl.getApkContentsSigners(signingInfo);
            } else {
                array = Api28Impl.getSigningCertificateHistory(signingInfo);
            }
        } else {
            // Lint warning's vulnerability is explicitly not handled for this method.
            @SuppressLint("PackageManagerGetSignatures")
            PackageInfo pkgInfo = packageManager.getPackageInfo(packageName,
                    PackageManager.GET_SIGNATURES);
            array = pkgInfo.signatures;
        }

        // Framework code implies nullable/empty, although it may be impossible in practice.
        if (array == null) {
            return Collections.emptyList();
        } else {
            return Arrays.asList(array);
        }
    }

    /**
     * Check if a package on device contains set of a certificates. Supported types are raw X509 or
     * SHA-256 bytes.
     *
     * @param packageManager      The {@link PackageManager} instance to query against.
     * @param packageName         The package to query the {@param packageManager} for. Query by
     *                            app UID is only supported by manually choosing a package name
     *                            returned in {@link PackageManager#getPackagesForUid(int)}.
     * @param certificatesAndType The bytes of the certificate mapped to the type, either
     *                            {@link PackageManager#CERT_INPUT_RAW_X509} or
     *                            {@link PackageManager#CERT_INPUT_SHA256}. A single or multiple
     *                            certificates may be included.
     * @param matchExact          Whether or not to check for presence of all signatures exactly.
     *                            If false, then the check will succeed if the query contains a
     *                            subset of the package certificates. Matching exactly is strongly
     *                            recommended when running on devices below
     *                            {@link Build.VERSION_CODES#LOLLIPOP} due to the fake ID
     *                            vulnerability that allows a package to be modified to include
     *                            an unverified signature.
     * @return true if the package is considered signed by the given certificate set, or false
     * otherwise
     * @throws PackageManager.NameNotFoundException if the package cannot be found through the
     *                                              provided {@param packageManager}
     */
    public static boolean hasSignatures(@NonNull PackageManager packageManager,
            @NonNull String packageName,
            @Size(min = 1) @NonNull Map<byte[], Integer> certificatesAndType, boolean matchExact)
            throws PackageManager.NameNotFoundException {
        // If empty is passed in, return false to prevent accidentally succeeding
        if (certificatesAndType.isEmpty()) {
            return false;
        }

        Set<byte[]> expectedCertBytes = certificatesAndType.keySet();

        // The type has to be checked before any API level branching. If a new type is ever added,
        // this code should fail and will have to be updated manually. To do otherwise would
        // introduce a behavioral difference between the API level that added the new type and
        // devices on prior API levels, which may not be caught by a developer calling this
        // method if they do not test on an old API level.
        for (byte[] bytes : expectedCertBytes) {
            if (bytes == null) {
                throw new IllegalArgumentException("Cert byte array cannot be null when verifying "
                        + packageName);
            }
            Integer type = certificatesAndType.get(bytes);
            if (type == null) {
                throw new IllegalArgumentException("Type must be specified for cert when verifying "
                        + packageName);
            }

            switch (type) {
                case PackageManager.CERT_INPUT_RAW_X509:
                case PackageManager.CERT_INPUT_SHA256:
                    break;
                default:
                    throw new IllegalArgumentException("Unsupported certificate type " + type
                            + " when verifying " + packageName);
            }
        }

        // getSignatures is called first to throw NameNotFoundException if necessary
        final List<Signature> signers = getSignatures(packageManager, packageName);

        // The vulnerability requiring matchExact is not necessary on P, but the signatures
        // must still be checked manually in order to match the behavior described by the
        // method. Otherwise matchExact == true will allow additional certificates if run
        // on a device >= P.
        if (!matchExact && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            // If not matching exact, delegate to the API 28 PackageManager API for checking
            // individual certificates. This is less performant, but goes through a formally
            // supported API.
            for (byte[] bytes : expectedCertBytes) {
                Integer type = certificatesAndType.get(bytes);
                //noinspection ConstantConditions type cannot be null
                if (!Api28Impl.hasSigningCertificate(packageManager, packageName, bytes, type)) {
                    return false;
                }
            }

            return true;
        }

        // Fail if the query is larger than the actual set, or the size doesn't match and it should.
        if (signers.size() == 0
                || certificatesAndType.size() > signers.size()
                || (matchExact && certificatesAndType.size() != signers.size())) {
            return false;
        }

        @SuppressLint("InlinedApi")
        boolean hasSha256 = certificatesAndType.containsValue(PackageManager.CERT_INPUT_SHA256);
        byte[][] sha256Digests = null;
        if (hasSha256) {
            // Since the search does several array contains checks, cache the SHA256 digests here.
            sha256Digests = new byte[signers.size()][];
            for (int index = 0; index < signers.size(); index++) {
                sha256Digests[index] = computeSHA256Digest(signers.get(index).toByteArray());
            }
        }

        for (byte[] bytes : expectedCertBytes) {
            Integer type = certificatesAndType.get(bytes);
            //noinspection ConstantConditions type cannot be null
            switch (type) {
                case PackageManager.CERT_INPUT_RAW_X509:
                    // RAW_X509 is the type that Signatures are and always have been stored as,
                    // so defer to the Signature equals method for the platform.
                    Signature expectedSignature = new Signature(bytes);
                    if (!signers.contains(expectedSignature)) {
                        return false;
                    }
                    break;
                case PackageManager.CERT_INPUT_SHA256:
                    // sha256Digests cannot be null due to pre-checked containsValue for its type
                    //noinspection ConstantConditions
                    if (!byteArrayContains(sha256Digests, bytes)) {
                        return false;
                    }
                    break;
                default:
                    // Impossible to reach this point due to check at beginning of method.
                    throw new IllegalArgumentException("Unsupported certificate type " + type);
            }

            // If this point is reached, all searches have succeeded
            return true;
        }

        return false;
    }

    private static boolean byteArrayContains(@NonNull byte[][] array, @NonNull byte[] expected) {
        for (byte[] item : array) {
            if (Arrays.equals(expected, item)) {
                return true;
            }
        }
        return false;
    }

    private static byte[] computeSHA256Digest(byte[] bytes) {
        try {
            return MessageDigest.getInstance("SHA256").digest(bytes);
        } catch (NoSuchAlgorithmException e) {
            // Can't happen, SHA256 required since API level 1
            throw new RuntimeException("Device doesn't support SHA256 cert checking", e);
        }
    }

    private PackageInfoCompat() {
    }

    @RequiresApi(28)
    private static class Api28Impl {
        private Api28Impl() {
            // This class is not instantiable.
        }

        @DoNotInline
        static boolean hasSigningCertificate(@NonNull PackageManager packageManager,
                @NonNull String packageName, @NonNull byte[] bytes, int type) {
            return packageManager.hasSigningCertificate(packageName, bytes, type);
        }

        @DoNotInline
        static boolean hasMultipleSigners(@NonNull SigningInfo signingInfo) {
            return signingInfo.hasMultipleSigners();
        }

        @DoNotInline
        @Nullable
        static Signature[] getApkContentsSigners(@NonNull SigningInfo signingInfo) {
            return signingInfo.getApkContentsSigners();
        }

        @DoNotInline
        @Nullable
        static Signature[] getSigningCertificateHistory(@NonNull SigningInfo signingInfo) {
            return signingInfo.getSigningCertificateHistory();
        }

        @DoNotInline
        static long getLongVersionCode(PackageInfo packageInfo) {
            return packageInfo.getLongVersionCode();
        }
    }
}
