/*
 * Copyright (C) 2017 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.server.backup.utils;

import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
import static com.android.server.backup.BackupManagerService.TAG;
import static com.android.server.backup.UserBackupManagerService.PACKAGE_MANAGER_SENTINEL;
import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;

import android.annotation.Nullable;
import android.app.backup.BackupTransport;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.Signature;
import android.content.pm.SigningInfo;
import android.os.UserHandle;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.backup.IBackupTransport;
import com.android.internal.util.ArrayUtils;
import com.android.server.LocalServices;
import com.android.server.backup.transport.TransportClient;

import com.google.android.collect.Sets;

import java.util.Set;

/**
 * Utility methods wrapping operations on ApplicationInfo and PackageInfo.
 */
public class AppBackupUtils {
    private static final boolean DEBUG = false;
    // Whitelist of system packages that are eligible for backup in non-system users.
    private static final Set<String> systemPackagesWhitelistedForAllUsers =
            Sets.newArraySet(PACKAGE_MANAGER_SENTINEL, PLATFORM_PACKAGE_NAME);

    /**
     * Returns whether app is eligible for backup.
     *
     * High level policy: apps are generally ineligible for backup if certain conditions apply. The
     * conditions are:
     *
     * <ol>
     *     <li>their manifest states android:allowBackup="false"
     *     <li>they run as a system-level uid but do not supply their own backup agent
     *     <li>it is the special shared-storage backup package used for 'adb backup'
     * </ol>
     */
    public static boolean appIsEligibleForBackup(ApplicationInfo app, int userId) {
        return appIsEligibleForBackup(
                app, LocalServices.getService(PackageManagerInternal.class), userId);
    }

    @VisibleForTesting
    static boolean appIsEligibleForBackup(
            ApplicationInfo app, PackageManagerInternal packageManager, int userId) {
        // 1. their manifest states android:allowBackup="false"
        if ((app.flags & ApplicationInfo.FLAG_ALLOW_BACKUP) == 0) {
            return false;
        }

        // 2. they run as a system-level uid
        if (UserHandle.isCore(app.uid)) {
            // and the backup is happening for non-system user on a non-whitelisted package.
            if (userId != UserHandle.USER_SYSTEM
                    && !systemPackagesWhitelistedForAllUsers.contains(app.packageName)) {
                return false;
            }

            // or do not supply their own backup agent
            if (app.backupAgentName == null) {
                return false;
            }
        }

        // 3. it is the special shared-storage backup package used for 'adb backup'
        if (app.packageName.equals(SHARED_BACKUP_AGENT_PACKAGE)) {
            return false;
        }

        // 4. it is an "instant" app
        if (app.isInstantApp()) {
            return false;
        }

        return !appIsDisabled(app, packageManager, userId);
    }

    /**
     * Returns whether an app is eligible for backup at runtime. That is, the app has to:
     * <ol>
     *     <li>Return true for {@link #appIsEligibleForBackup(ApplicationInfo, int)}
     *     <li>Return false for {@link #appIsStopped(ApplicationInfo)}
     *     <li>Return false for {@link #appIsDisabled(ApplicationInfo, int)}
     *     <li>Be eligible for the transport via
     *         {@link BackupTransport#isAppEligibleForBackup(PackageInfo, boolean)}
     * </ol>
     */
    public static boolean appIsRunningAndEligibleForBackupWithTransport(
            @Nullable TransportClient transportClient,
            String packageName,
            PackageManager pm,
            int userId) {
        try {
            PackageInfo packageInfo = pm.getPackageInfoAsUser(packageName,
                    PackageManager.GET_SIGNING_CERTIFICATES, userId);
            ApplicationInfo applicationInfo = packageInfo.applicationInfo;
            if (!appIsEligibleForBackup(applicationInfo, userId)
                    || appIsStopped(applicationInfo)
                    || appIsDisabled(applicationInfo, userId)) {
                return false;
            }
            if (transportClient != null) {
                try {
                    IBackupTransport transport =
                            transportClient.connectOrThrow(
                                    "AppBackupUtils.appIsRunningAndEligibleForBackupWithTransport");
                    return transport.isAppEligibleForBackup(
                            packageInfo, AppBackupUtils.appGetsFullBackup(packageInfo));
                } catch (Exception e) {
                    Slog.e(TAG, "Unable to ask about eligibility: " + e.getMessage());
                }
            }
            // If transport is not present we couldn't tell that the package is not eligible.
            return true;
        } catch (PackageManager.NameNotFoundException e) {
            return false;
        }
    }

    /** Avoid backups of 'disabled' apps. */
    static boolean appIsDisabled(ApplicationInfo app, int userId) {
        return appIsDisabled(app, LocalServices.getService(PackageManagerInternal.class), userId);
    }

    @VisibleForTesting
    static boolean appIsDisabled(
            ApplicationInfo app, PackageManagerInternal packageManager, int userId) {
        int enabledSetting = packageManager.getApplicationEnabledState(app.packageName, userId);

        switch (enabledSetting) {
            case PackageManager.COMPONENT_ENABLED_STATE_DISABLED:
            case PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER:
            case PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED:
                return true;
            case PackageManager.COMPONENT_ENABLED_STATE_DEFAULT:
                return !app.enabled;
            default:
                return false;
        }
    }

    /**
     * Checks if the app is in a stopped state.  This is not part of the general "eligible for
     * backup?" check because we *do* still need to restore data to apps in this state (e.g.
     * newly-installing ones).
     *
     * <p>Reasons for such state:
     * <ul>
     *     <li>The app has been force-stopped.
     *     <li>The app has been cleared.
     *     <li>The app has just been installed.
     * </ul>
     */
    public static boolean appIsStopped(ApplicationInfo app) {
        return ((app.flags & ApplicationInfo.FLAG_STOPPED) != 0);
    }

    /**
     * Returns whether the app can get full backup. Does *not* check overall backup eligibility
     * policy!
     */
    public static boolean appGetsFullBackup(PackageInfo pkg) {
        if (pkg.applicationInfo.backupAgentName != null) {
            // If it has an agent, it gets full backups only if it says so
            return (pkg.applicationInfo.flags & ApplicationInfo.FLAG_FULL_BACKUP_ONLY) != 0;
        }

        // No agent or fullBackupOnly="true" means we do indeed perform full-data backups for it
        return true;
    }

    /**
     * Returns whether the app is only capable of doing key/value. We say it's not if it allows full
     * backup, and it is otherwise.
     */
    public static boolean appIsKeyValueOnly(PackageInfo pkg) {
        return !appGetsFullBackup(pkg);
    }

    /**
     * Returns whether the signatures stored {@param storedSigs}, coming from the source apk, match
     * the signatures of the apk installed on the device, the target apk. If the target resides in
     * the system partition we return true. Otherwise it's considered a match if both conditions
     * hold:
     *
     * <ul>
     *   <li>Source and target have at least one signature each
     *   <li>Target contains all signatures in source, and nothing more
     * </ul>
     *
     * or if both source and target have exactly one signature, and they don't match, we check
     * if the app was ever signed with source signature (i.e. app has rotated key)
     * Note: key rotation is only supported for apps ever signed with one key, and those apps will
     * not be allowed to be signed by more certificates in the future
     *
     * Note that if {@param target} is null we return false.
     */
    public static boolean signaturesMatch(Signature[] storedSigs, PackageInfo target,
            PackageManagerInternal pmi) {
        if (target == null || target.packageName == null) {
            return false;
        }

        // If the target resides on the system partition, we allow it to restore
        // data from the like-named package in a restore set even if the signatures
        // do not match.  (Unlike general applications, those flashed to the system
        // partition will be signed with the device's platform certificate, so on
        // different phones the same system app will have different signatures.)
        if ((target.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
            if (MORE_DEBUG) {
                Slog.v(TAG, "System app " + target.packageName + " - skipping sig check");
            }
            return true;
        }

        // Don't allow unsigned apps on either end
        if (ArrayUtils.isEmpty(storedSigs)) {
            return false;
        }

        SigningInfo signingInfo = target.signingInfo;
        if (signingInfo == null) {
            Slog.w(TAG, "signingInfo is empty, app was either unsigned or the flag" +
                    " PackageManager#GET_SIGNING_CERTIFICATES was not specified");
            return false;
        }

        if (DEBUG) {
            Slog.v(TAG, "signaturesMatch(): stored=" + storedSigs + " device="
                    + signingInfo.getApkContentsSigners());
        }

        final int nStored = storedSigs.length;
        if (nStored == 1) {
            // if the app is only signed with one sig, it's possible it has rotated its key
            // (the checks with signing history are delegated to PackageManager)
            // TODO(b/73988180): address the case that app has declared restoreAnyVersion and is
            // restoring from higher version to lower after having rotated the key (i.e. higher
            // version has different sig than lower version that we want to restore to)
            return pmi.isDataRestoreSafe(storedSigs[0], target.packageName);
        } else {
            // the app couldn't have rotated keys, since it was signed with multiple sigs - do
            // a check to see if we find a match for all stored sigs
            // since app hasn't rotated key, we only need to check with its current signers
            Signature[] deviceSigs = signingInfo.getApkContentsSigners();
            int nDevice = deviceSigs.length;

            // ensure that each stored sig matches an on-device sig
            for (int i = 0; i < nStored; i++) {
                boolean match = false;
                for (int j = 0; j < nDevice; j++) {
                    if (storedSigs[i].equals(deviceSigs[j])) {
                        match = true;
                        break;
                    }
                }
                if (!match) {
                    return false;
                }
            }
            // we have found a match for all stored sigs
            return true;
        }
    }
}
