chromium/base/android/java/src/org/chromium/base/PackageUtils.java

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.base;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;

/** This class provides package checking related methods. */
public class PackageUtils {
    private static final String TAG = "PackageUtils";
    private static final char[] HEX_CHAR_LOOKUP = "0123456789ABCDEF".toCharArray();

    /** Retrieves the PackageInfo for the given package, or null if it is not installed. */
    public static @Nullable PackageInfo getPackageInfo(String packageName, int flags) {
        PackageManager pm = ContextUtils.getApplicationContext().getPackageManager();
        try {
            return pm.getPackageInfo(packageName, flags);
        } catch (PackageManager.NameNotFoundException e) {
            return null;
        }
    }

    /**
     * Retrieves the version of the given package installed on the device.
     *
     * @param packageName Name of the package to find.
     * @return The package's version code if found, -1 otherwise.
     */
    public static int getPackageVersion(String packageName) {
        // TODO(agrieve): Return a long and move BuildInfo.packageVersionCode() to this class.
        PackageInfo packageInfo = getPackageInfo(packageName, 0);
        if (packageInfo != null) return packageInfo.versionCode;
        return -1;
    }

    // TODO(agrieve): Delete downstream references.
    public static int getPackageVersion(Context unused, String packageName) {
        return getPackageVersion(packageName);
    }

    /**
     * Checks if the app has been installed on the system.
     * @return true if the PackageManager reports that the app is installed, false otherwise.
     * @param packageName Name of the package to check.
     */
    public static boolean isPackageInstalled(String packageName) {
        return getPackageInfo(packageName, 0) != null;
    }

    /** Returns the PackageInfo for the current app, as retrieve by PackageManager. */
    public static PackageInfo getApplicationPackageInfo(int flags) {
        PackageInfo ret = getPackageInfo(BuildInfo.getInstance().packageName, flags);
        assert ret != null;
        return ret;
    }

    /**
     * Computes the SHA256 certificates for the given package name. The app with the given package
     * name has to be installed on device. The output will be a list of 30 long HEX strings with :
     * between each value. There will be one string for each signature the app is signed with.
     * @param packageName The package name to query the signature for.
     * @return The SHA256 certificate for the package name.
     */
    @SuppressLint("PackageManagerGetSignatures")
    // https://stackoverflow.com/questions/39192844/android-studio-warning-when-using-packagemanager-get-signatures
    public static List<String> getCertificateSHA256FingerprintForPackage(String packageName) {
        PackageInfo packageInfo = getPackageInfo(packageName, PackageManager.GET_SIGNATURES);

        if (packageInfo == null) return null;

        ArrayList<String> fingerprints = new ArrayList<>(packageInfo.signatures.length);

        for (Signature signature : packageInfo.signatures) {
            InputStream input = new ByteArrayInputStream(signature.toByteArray());
            String hexString = null;
            try {
                X509Certificate certificate =
                        (X509Certificate)
                                CertificateFactory.getInstance("X509").generateCertificate(input);
                hexString =
                        byteArrayToHexString(
                                MessageDigest.getInstance("SHA256")
                                        .digest(certificate.getEncoded()));
            } catch (CertificateException | NoSuchAlgorithmException e) {
                Log.w(TAG, "Exception", e);
                return null;
            }

            fingerprints.add(hexString);
        }

        return fingerprints;
    }

    /**
     * Converts a byte array to hex string with : inserted between each element.
     * @param byteArray The array to be converted.
     * @return A string with two letters representing each byte and : in between.
     */
    @VisibleForTesting
    static String byteArrayToHexString(byte[] byteArray) {
        StringBuilder hexString = new StringBuilder(byteArray.length * 3 - 1);
        for (int i = 0; i < byteArray.length; ++i) {
            hexString.append(HEX_CHAR_LOOKUP[(byteArray[i] & 0xf0) >>> 4]);
            hexString.append(HEX_CHAR_LOOKUP[byteArray[i] & 0xf]);
            if (i < (byteArray.length - 1)) hexString.append(':');
        }
        return hexString.toString();
    }

    private PackageUtils() {
        // Hide constructor
    }
}