blob: 7c47f1e477b610c57df9fa7f5d999ab20383170f [file] [log] [blame]
/*
* 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.SETTINGS_PACKAGE;
import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
import static com.android.server.backup.UserBackupManagerService.WALLPAPER_PACKAGE;
import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
import android.annotation.Nullable;
import android.app.backup.BackupAnnotations.BackupDestination;
import android.app.backup.BackupTransport;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.Overridable;
import android.content.Context;
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.Build;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.server.backup.SetUtils;
import com.android.server.backup.transport.BackupTransportClient;
import com.android.server.backup.transport.TransportConnection;
import com.google.android.collect.Sets;
import java.util.Arrays;
import java.util.Set;
/**
* Utility methods wrapping operations on ApplicationInfo and PackageInfo.
*/
public class BackupEligibilityRules {
private static final boolean DEBUG = false;
/**
* List of system packages that are eligible for backup in "profile" users (such as work
* profile). See {@link UserManager#isProfile()}. This is a subset of {@link
* #systemPackagesAllowedForNonSystemUsers}
*/
private static final Set<String> systemPackagesAllowedForProfileUser =
Sets.newArraySet(PACKAGE_MANAGER_SENTINEL, PLATFORM_PACKAGE_NAME);
/**
* List of system packages that are eligible for backup in non-system users.
*/
private static final Set<String> systemPackagesAllowedForNonSystemUsers = SetUtils.union(
systemPackagesAllowedForProfileUser,
Sets.newArraySet(WALLPAPER_PACKAGE, SETTINGS_PACKAGE));
private final PackageManager mPackageManager;
private final PackageManagerInternal mPackageManagerInternal;
private final int mUserId;
private boolean mIsProfileUser = false;
@BackupDestination private final int mBackupDestination;
/**
* When this change is enabled, {@code adb backup} is automatically turned on for apps
* running as debuggable ({@code android:debuggable} set to {@code true}) and unavailable to
* any other apps.
*/
@ChangeId
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.S)
static final long RESTRICT_ADB_BACKUP = 171032338L;
/**
* When this change is enabled, {@code android:allowBackup} is ignored for apps during D2D
* (device-to-device) migrations.
*/
@ChangeId
@Overridable
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.S)
static final long IGNORE_ALLOW_BACKUP_IN_D2D = 183147249L;
public static BackupEligibilityRules forBackup(PackageManager packageManager,
PackageManagerInternal packageManagerInternal,
int userId,
Context context) {
return new BackupEligibilityRules(packageManager, packageManagerInternal, userId, context,
BackupDestination.CLOUD);
}
public BackupEligibilityRules(PackageManager packageManager,
PackageManagerInternal packageManagerInternal,
int userId,
Context context,
@BackupDestination int backupDestination) {
mPackageManager = packageManager;
mPackageManagerInternal = packageManagerInternal;
mUserId = userId;
mBackupDestination = backupDestination;
UserManager userManager = context.getSystemService(UserManager.class);
mIsProfileUser = userManager.isProfile();
}
/**
* 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>
*
* However, the above eligibility rules are ignored for non-system apps in in case of
* device-to-device migration, see {@link BackupDestination}.
*/
@VisibleForTesting
public boolean appIsEligibleForBackup(ApplicationInfo app) {
// 1. their manifest states android:allowBackup="false" and this is not a device-to-device
// migration
if (!isAppBackupAllowed(app)) {
return false;
}
// 2. they run as a system-level uid
if (UserHandle.isCore(app.uid)) {
// and the backup is happening for a non-system user or profile on a package that is
// not explicitly allowed.
if (mUserId != UserHandle.USER_SYSTEM) {
if (mIsProfileUser && !systemPackagesAllowedForProfileUser.contains(
app.packageName)) {
return false;
}
if (!mIsProfileUser && !systemPackagesAllowedForNonSystemUsers.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);
}
/**
* Check if this app allows backup. Apps can opt out of backup by stating
* android:allowBackup="false" in their manifest. However, this flag is ignored for non-system
* apps during device-to-device migrations, see {@link BackupDestination}.
*
* @param app The app under check.
* @return boolean indicating whether backup is allowed.
*/
public boolean isAppBackupAllowed(ApplicationInfo app) {
boolean allowBackup = (app.flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0;
switch (mBackupDestination) {
case BackupDestination.DEVICE_TRANSFER:
// Backup / restore of all non-system apps is force allowed during
// device-to-device migration.
boolean isSystemApp = (app.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
boolean ignoreAllowBackup = !isSystemApp && CompatChanges.isChangeEnabled(
IGNORE_ALLOW_BACKUP_IN_D2D, app.packageName, UserHandle.of(mUserId));
return ignoreAllowBackup || allowBackup;
case BackupDestination.ADB_BACKUP:
String packageName = app.packageName;
if (packageName == null) {
Slog.w(TAG, "Invalid ApplicationInfo object");
return false;
}
if (!CompatChanges.isChangeEnabled(RESTRICT_ADB_BACKUP, packageName,
UserHandle.of(mUserId))) {
return allowBackup;
}
if (PLATFORM_PACKAGE_NAME.equals(packageName)) {
// Always enable adb backup for SystemBackupAgent in "android" package (this is
// done to avoid breaking existing integration tests and might change in the
// future).
return true;
}
boolean isPrivileged = (app.flags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0;
boolean isDebuggable = (app.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
if (UserHandle.isCore(app.uid) || isPrivileged) {
try {
return mPackageManager.getPropertyAsUser(
PackageManager.PROPERTY_ALLOW_ADB_BACKUP, packageName,
null /* className */, mUserId).getBoolean();
} catch (PackageManager.NameNotFoundException e) {
Slog.w(TAG, "Failed to read allowAdbBackup property for + "
+ packageName);
// This temporarily falls back to the legacy allowBackup flag to
// avoid breaking existing users of adb backup. Once they're able to use
// the new ALLOW_ADB_BACKUP property, we'll return false here.
// TODO(b/176088499): Return false here.
return allowBackup;
}
} else {
// All other apps can use adb backup only when running in debuggable mode.
return isDebuggable;
}
case BackupDestination.CLOUD:
return allowBackup;
default:
Slog.w(TAG, "Unknown operation type:" + mBackupDestination);
return false;
}
}
/**
* 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 boolean appIsRunningAndEligibleForBackupWithTransport(
@Nullable TransportConnection transportConnection,
String packageName) {
try {
PackageInfo packageInfo = mPackageManager.getPackageInfoAsUser(packageName,
PackageManager.GET_SIGNING_CERTIFICATES, mUserId);
ApplicationInfo applicationInfo = packageInfo.applicationInfo;
if (!appIsEligibleForBackup(applicationInfo)
|| appIsStopped(applicationInfo)
|| appIsDisabled(applicationInfo)) {
return false;
}
if (transportConnection != null) {
try {
BackupTransportClient transport =
transportConnection.connectOrThrow(
"AppBackupUtils.appIsRunningAndEligibleForBackupWithTransport");
return transport.isAppEligibleForBackup(
packageInfo, 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. */
@VisibleForTesting
boolean appIsDisabled(
ApplicationInfo app) {
int enabledSetting = mPackageManagerInternal.getApplicationEnabledState(app.packageName,
mUserId);
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 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!
*/
@VisibleForTesting
public 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 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 boolean signaturesMatch(Signature[] storedSigs, PackageInfo target) {
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=" + Arrays.toString(storedSigs)
+ " device=" + Arrays.toString(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 mPackageManagerInternal.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;
}
}
public int getBackupDestination() {
return mBackupDestination;
}
}