pkgStates = new ArrayList<>();
skipToTag(parser, TAG_PERMISSION_BACKUP);
int backupPlatformVersion;
try {
backupPlatformVersion = Integer.parseInt(
parser.getAttributeValue(null, ATTR_PLATFORM_VERSION));
} catch (NumberFormatException ignored) {
// Platforms P and before did not store the platform version
backupPlatformVersion = Build.VERSION_CODES.P;
}
skipToTag(parser, TAG_ALL_GRANTS);
if (parser.getEventType() != START_TAG && !parser.getName().equals(TAG_ALL_GRANTS)) {
throw new XmlPullParserException("Could not find " + TAG_PERMISSION_BACKUP + " > "
+ TAG_ALL_GRANTS);
}
// Read packages to restore from xml
int type;
do {
type = parser.next();
switch (type) {
case START_TAG:
switch (parser.getName()) {
case TAG_GRANT:
try {
pkgStates.add(BackupPackageState.parseFromXml(parser, mContext,
backupPlatformVersion));
} catch (XmlPullParserException e) {
Log.e(LOG_TAG, "Could not parse permissions ", e);
skipToEndOfTag(parser);
}
break;
default:
// ignore tag
Log.w(LOG_TAG, "Found unexpected tag " + parser.getName()
+ " during restore");
skipToEndOfTag(parser);
}
}
} while (type != END_DOCUMENT);
return pkgStates;
}
/**
* Try to restore the permission state from XML.
*
* If some apps could not be restored, the leftover apps are written to
* {@link Constants#DELAYED_RESTORE_PERMISSIONS_FILE}.
*
* @param parser The xml to read
*/
void restoreState(@NonNull XmlPullParser parser) throws IOException, XmlPullParserException {
ArrayList pkgStates = parseFromXml(parser);
ArrayList packagesToRestoreLater = new ArrayList<>();
int numPkgStates = pkgStates.size();
if (numPkgStates > 0) {
// Try to restore packages
for (int i = 0; i < numPkgStates; i++) {
BackupPackageState pkgState = pkgStates.get(i);
PackageInfo pkgInfo;
try {
pkgInfo = mContext.getPackageManager().getPackageInfo(pkgState.mPackageName,
GET_PERMISSIONS | GET_SIGNING_CERTIFICATES);
} catch (PackageManager.NameNotFoundException ignored) {
packagesToRestoreLater.add(pkgState);
continue;
}
if (!checkCertificateDigestsMatch(pkgInfo, pkgState)) {
continue;
}
pkgState.restore(mContext, pkgInfo);
}
}
synchronized (sLock) {
writeDelayedStorePkgsLocked(packagesToRestoreLater);
}
}
/**
* Returns whether the backed up package and the package being restored have compatible signing
* certificate digests.
*
* Permissions should only be restored if the backed up package has the same signing
* certificate(s) or an ancestor (in the case of certification rotation).
*
*
If no certificates are found stored for the backed up package, we return true anyway as
* certificate storage does not exist before {@link Build.VERSION_CODES.TIRAMISU}.
*/
private boolean checkCertificateDigestsMatch(
@NonNull PackageInfo packageToRestoreInfo,
@NonNull BackupPackageState backupPackageState) {
// No signing information was stored for the backed up app.
if (backupPackageState.mBackupSigningInfoState == null) {
return true;
}
// The backed up app was unsigned.
if (backupPackageState.mBackupSigningInfoState.mCurrentCertDigests.isEmpty()) {
return false;
}
// We don't have signing information for the restored app, but the backed up app was signed.
if (packageToRestoreInfo.signingInfo == null) {
return false;
}
// The restored app is unsigned.
if (packageToRestoreInfo.signingInfo.getApkContentsSigners() == null
|| packageToRestoreInfo.signingInfo.getApkContentsSigners().length == 0) {
return false;
}
// If the restored app is a system app, we allow permissions to be restored without any
// certificate checks.
// System apps are signed with the device's platform certificate, so on
// different phones the same system app can have different certificates.
// We perform this check to be consistent with the Backup and Restore feature logic in
// frameworks/base/services/core/java/com/android/server/backup/BackupUtils.java
if ((packageToRestoreInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
return true;
}
// Both backed up app and restored app have signing information, so we check that these are
// compatible for the purpose of restoring permissions to the restored app.
return hasCompatibleSignaturesForRestore(packageToRestoreInfo.signingInfo,
backupPackageState.mBackupSigningInfoState);
}
/**
* Write a xml file for the given packages.
*
* @param serializer The file to write to
* @param pkgs The packages to write
*/
private static void writePkgsAsXml(@NonNull XmlSerializer serializer,
@NonNull ArrayList pkgs) throws IOException {
serializer.startDocument(null, true);
serializer.startTag(null, TAG_PERMISSION_BACKUP);
serializer.attribute(null, ATTR_PLATFORM_VERSION,
Integer.valueOf(Build.VERSION.SDK_INT).toString());
serializer.startTag(null, TAG_ALL_GRANTS);
int numPkgs = pkgs.size();
for (int i = 0; i < numPkgs; i++) {
BackupPackageState packageState = pkgs.get(i);
if (packageState != null) {
packageState.writeAsXml(serializer);
}
}
serializer.endTag(null, TAG_ALL_GRANTS);
serializer.endTag(null, TAG_PERMISSION_BACKUP);
serializer.endDocument();
}
/**
* Update the {@link Constants#DELAYED_RESTORE_PERMISSIONS_FILE} to contain the
* {@code packagesToRestoreLater}.
*
* @param packagesToRestoreLater The new pkgs in the delayed restore file
*/
private void writeDelayedStorePkgsLocked(
@NonNull ArrayList packagesToRestoreLater) {
if (packagesToRestoreLater.size() == 0) {
mContext.deleteFile(DELAYED_RESTORE_PERMISSIONS_FILE);
return;
}
try (OutputStream delayedRestoreData = mContext.openFileOutput(
DELAYED_RESTORE_PERMISSIONS_FILE, MODE_PRIVATE)) {
XmlSerializer serializer = newSerializer();
serializer.setOutput(delayedRestoreData, UTF_8.name());
writePkgsAsXml(serializer, packagesToRestoreLater);
serializer.flush();
} catch (IOException e) {
Log.e(LOG_TAG, "Could not remember which packages still need to be restored", e);
}
}
/**
* Write the state of all packages as XML.
*
* @param serializer The xml to write to
*/
void writeState(@NonNull XmlSerializer serializer) throws IOException {
List pkgs = mContext.getPackageManager().getInstalledPackages(
GET_PERMISSIONS | GET_SIGNING_CERTIFICATES);
ArrayList backupPkgs = new ArrayList<>();
int numPkgs = pkgs.size();
for (int i = 0; i < numPkgs; i++) {
BackupPackageState packageState = BackupPackageState.fromAppPermissions(mContext,
pkgs.get(i));
if (packageState != null) {
backupPkgs.add(packageState);
}
}
writePkgsAsXml(serializer, backupPkgs);
}
/**
* Restore delayed permission state for a package (if delayed during {@link #restoreState}).
*
* @param packageName The package to be restored
*
* @return {@code true} if there is still delayed backup left
*/
boolean restoreDelayedState(@NonNull String packageName) {
synchronized (sLock) {
ArrayList packagesToRestoreLater;
try (FileInputStream delayedRestoreData =
mContext.openFileInput(DELAYED_RESTORE_PERMISSIONS_FILE)) {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(delayedRestoreData, UTF_8.name());
packagesToRestoreLater = parseFromXml(parser);
} catch (IOException | XmlPullParserException e) {
Log.e(LOG_TAG, "Could not parse delayed permissions", e);
return false;
}
PackageInfo pkgInfo = null;
try {
pkgInfo = mContext.getPackageManager().getPackageInfo(
packageName, GET_PERMISSIONS | GET_SIGNING_CERTIFICATES);
} catch (PackageManager.NameNotFoundException e) {
Log.e(LOG_TAG, "Could not restore delayed permissions for " + packageName, e);
}
if (pkgInfo != null) {
int numPkgs = packagesToRestoreLater.size();
for (int i = 0; i < numPkgs; i++) {
BackupPackageState pkgState = packagesToRestoreLater.get(i);
if (pkgState.mPackageName.equals(packageName) && checkCertificateDigestsMatch(
pkgInfo, pkgState)) {
pkgState.restore(mContext, pkgInfo);
packagesToRestoreLater.remove(i);
writeDelayedStorePkgsLocked(packagesToRestoreLater);
break;
}
}
}
return packagesToRestoreLater.size() > 0;
}
}
/**
* State that needs to be backed up for a permission.
*/
private static class BackupPermissionState {
@NonNull
private final String mPermissionName;
private final boolean mIsGranted;
private final boolean mIsUserSet;
private final boolean mIsUserFixed;
private final boolean mWasReviewed;
// Not persisted, used during parsing so explicitly defined state takes precedence
private final boolean mIsAddedFromSplit;
private BackupPermissionState(@NonNull String permissionName, boolean isGranted,
boolean isUserSet, boolean isUserFixed, boolean wasReviewed,
boolean isAddedFromSplit) {
mPermissionName = permissionName;
mIsGranted = isGranted;
mIsUserSet = isUserSet;
mIsUserFixed = isUserFixed;
mWasReviewed = wasReviewed;
mIsAddedFromSplit = isAddedFromSplit;
}
/**
* Parse a package state from XML.
*
* @param parser The data to read
* @param context a context to use
* @param backupPlatformVersion The platform version the backup was created on
*
* @return The state
*/
@NonNull
static List parseFromXml(@NonNull XmlPullParser parser,
@NonNull Context context, int backupPlatformVersion)
throws XmlPullParserException {
String permName = parser.getAttributeValue(null, ATTR_PERMISSION_NAME);
if (permName == null) {
throw new XmlPullParserException("Found " + TAG_PERMISSION + " without "
+ ATTR_PERMISSION_NAME);
}
ArrayList expandedPermissions = new ArrayList<>();
expandedPermissions.add(permName);
List splitPerms = context.getSystemService(
PermissionManager.class).getSplitPermissions();
// Expand the properties to permissions that were split between the platform version the
// backup was taken and the current version.
int numSplitPerms = splitPerms.size();
for (int i = 0; i < numSplitPerms; i++) {
SplitPermissionInfo splitPerm = splitPerms.get(i);
if (backupPlatformVersion < splitPerm.getTargetSdk()
&& permName.equals(splitPerm.getSplitPermission())) {
expandedPermissions.addAll(splitPerm.getNewPermissions());
}
}
ArrayList parsedPermissions = new ArrayList<>(
expandedPermissions.size());
int numExpandedPerms = expandedPermissions.size();
for (int i = 0; i < numExpandedPerms; i++) {
parsedPermissions.add(new BackupPermissionState(expandedPermissions.get(i),
"true".equals(parser.getAttributeValue(null, ATTR_IS_GRANTED)),
"true".equals(parser.getAttributeValue(null, ATTR_USER_SET)),
"true".equals(parser.getAttributeValue(null, ATTR_USER_FIXED)),
"true".equals(parser.getAttributeValue(null, ATTR_WAS_REVIEWED)),
/* isAddedFromSplit */ i > 0));
}
return parsedPermissions;
}
/**
* Is the permission granted, also considering the app-op.
*
* This does not consider the review-required state of the permission.
*
* @param perm The permission that might be granted
*
* @return {@code true} iff the permission and app-op is granted
*/
private static boolean isPermGrantedIncludingAppOp(@NonNull Permission perm) {
return perm.isGranted() && (!perm.affectsAppOp() || perm.isAppOpAllowed());
}
/**
* Get the state of a permission to back up.
*
* @param perm The permission to back up
* @param appSupportsRuntimePermissions If the app supports runtimePermissions
*
* @return The state to back up or {@code null} if the permission does not need to be
* backed up.
*/
@Nullable
private static BackupPermissionState fromPermission(@NonNull Permission perm,
boolean appSupportsRuntimePermissions) {
int grantFlags = perm.getFlags();
if ((grantFlags & SYSTEM_RUNTIME_GRANT_MASK) != 0) {
return null;
}
if (!perm.isUserSet() && perm.isGrantedByDefault()) {
return null;
}
boolean permissionWasReviewed;
boolean isNotInDefaultGrantState;
if (appSupportsRuntimePermissions) {
isNotInDefaultGrantState = isPermGrantedIncludingAppOp(perm);
permissionWasReviewed = false;
} else {
isNotInDefaultGrantState = !isPermGrantedIncludingAppOp(perm);
permissionWasReviewed = !perm.isReviewRequired();
}
if (isNotInDefaultGrantState || perm.isUserSet() || perm.isUserFixed()
|| permissionWasReviewed) {
return new BackupPermissionState(perm.getName(), isPermGrantedIncludingAppOp(perm),
perm.isUserSet(), perm.isUserFixed(), permissionWasReviewed,
/* isAddedFromSplit */ false);
} else {
return null;
}
}
/**
* Get the states of all permissions of a group to back up.
*
* @param group The group of the permissions to back up
*
* @return The state to back up. Empty list if no permissions in the group need to be backed
* up
*/
@NonNull
static ArrayList fromPermissionGroup(
@NonNull AppPermissionGroup group) {
ArrayList permissionsToRestore = new ArrayList<>();
List perms = group.getPermissions();
boolean appSupportsRuntimePermissions =
group.getApp().applicationInfo.targetSdkVersion >= Build.VERSION_CODES.M;
int numPerms = perms.size();
for (int i = 0; i < numPerms; i++) {
BackupPermissionState permState = fromPermission(perms.get(i),
appSupportsRuntimePermissions);
if (permState != null) {
permissionsToRestore.add(permState);
}
}
return permissionsToRestore;
}
/**
* Write this state as XML.
*
* @param serializer The file to write to
*/
void writeAsXml(@NonNull XmlSerializer serializer) throws IOException {
serializer.startTag(null, TAG_PERMISSION);
serializer.attribute(null, ATTR_PERMISSION_NAME, mPermissionName);
if (mIsGranted) {
serializer.attribute(null, ATTR_IS_GRANTED, "true");
}
if (mIsUserSet) {
serializer.attribute(null, ATTR_USER_SET, "true");
}
if (mIsUserFixed) {
serializer.attribute(null, ATTR_USER_FIXED, "true");
}
if (mWasReviewed) {
serializer.attribute(null, ATTR_WAS_REVIEWED, "true");
}
serializer.endTag(null, TAG_PERMISSION);
}
/**
* Restore this permission state.
*
* @param appPerms The {@link AppPermissions} to restore the state to
* @param restoreBackgroundPerms if {@code true} only restore background permissions,
* if {@code false} do not restore background permissions
*/
void restore(@NonNull AppPermissions appPerms, boolean restoreBackgroundPerms) {
AppPermissionGroup group = appPerms.getGroupForPermission(mPermissionName);
if (group == null) {
Log.w(LOG_TAG, "Could not find group for " + mPermissionName + " in "
+ appPerms.getPackageInfo().packageName);
return;
}
if (restoreBackgroundPerms != group.isBackgroundGroup()) {
return;
}
Permission perm = group.getPermission(mPermissionName);
if (mWasReviewed) {
perm.unsetReviewRequired();
}
// Don't grant or revoke fixed permission groups
if (group.isSystemFixed() || group.isPolicyFixed()) {
return;
}
if (!perm.isUserSet()) {
if (mIsGranted) {
group.grantRuntimePermissions(false, mIsUserFixed,
new String[]{mPermissionName});
} else {
group.revokeRuntimePermissions(mIsUserFixed,
new String[]{mPermissionName});
}
perm.setUserSet(mIsUserSet);
}
}
}
/** Signing certificate information for a backed up package. */
private static class BackupSigningInfoState {
@NonNull
private final Set mCurrentCertDigests;
@NonNull
private final Set mPastCertDigests;
private BackupSigningInfoState(@NonNull Set currentCertDigests,
@NonNull Set pastCertDigests) {
mCurrentCertDigests = currentCertDigests;
mPastCertDigests = pastCertDigests;
}
/**
* Write this state as XML.
*
* @param serializer the file to write to
*/
void writeAsXml(@NonNull XmlSerializer serializer) throws IOException {
serializer.startTag(null, TAG_SIGNING_INFO);
for (byte[] digest : mCurrentCertDigests) {
serializer.startTag(null, TAG_CURRENT_CERTIFICATE);
serializer.attribute(
null, ATTR_CERTIFICATE_DIGEST,
Base64.encodeToString(digest, Base64.NO_WRAP));
serializer.endTag(null, TAG_CURRENT_CERTIFICATE);
}
for (byte[] digest : mPastCertDigests) {
serializer.startTag(null, TAG_PAST_CERTIFICATE);
serializer.attribute(
null, ATTR_CERTIFICATE_DIGEST,
Base64.encodeToString(digest, Base64.NO_WRAP));
serializer.endTag(null, TAG_PAST_CERTIFICATE);
}
serializer.endTag(null, TAG_SIGNING_INFO);
}
/**
* Parse the signing information state from XML.
*
* @param parser the data to read
*
* @return the signing information state
*/
@NonNull
static BackupSigningInfoState parseFromXml(@NonNull XmlPullParser parser)
throws IOException, XmlPullParserException {
Set currentCertDigests = new HashSet<>();
Set pastCertDigests = new HashSet<>();
while (true) {
switch (parser.next()) {
case START_TAG:
switch (parser.getName()) {
case TAG_CURRENT_CERTIFICATE:
String currentCertDigest =
parser.getAttributeValue(
null, ATTR_CERTIFICATE_DIGEST);
if (currentCertDigest == null) {
throw new XmlPullParserException(
"Found " + TAG_CURRENT_CERTIFICATE + " without "
+ ATTR_CERTIFICATE_DIGEST);
}
currentCertDigests.add(
Base64.decode(currentCertDigest, Base64.NO_WRAP));
skipToEndOfTag(parser);
break;
case TAG_PAST_CERTIFICATE:
String pastCertDigest =
parser.getAttributeValue(
null, ATTR_CERTIFICATE_DIGEST);
if (pastCertDigest == null) {
throw new XmlPullParserException(
"Found " + TAG_PAST_CERTIFICATE + " without "
+ ATTR_CERTIFICATE_DIGEST);
}
pastCertDigests.add(
Base64.decode(pastCertDigest, Base64.NO_WRAP));
skipToEndOfTag(parser);
break;
default:
Log.w(LOG_TAG, "Found unexpected tag " + parser.getName());
skipToEndOfTag(parser);
}
break;
case END_TAG:
return new BackupSigningInfoState(
currentCertDigests,
pastCertDigests);
}
}
}
/**
* Construct the signing information state from a {@link SigningInfo} instance.
*
* @param signingInfo the {@link SigningInfo} instance
*
* @return the state
*/
@NonNull
static BackupSigningInfoState fromSigningInfo(@NonNull SigningInfo signingInfo) {
Set currentCertDigests = new HashSet<>();
Set pastCertDigests = new HashSet<>();
Signature[] apkContentsSigners = signingInfo.getApkContentsSigners();
for (int i = 0; i < apkContentsSigners.length; i++) {
currentCertDigests.add(
computeSha256DigestBytes(apkContentsSigners[i].toByteArray()));
}
if (signingInfo.hasPastSigningCertificates()) {
Signature[] signingCertificateHistory = signingInfo.getSigningCertificateHistory();
for (int i = 0; i < signingCertificateHistory.length; i++) {
pastCertDigests.add(
computeSha256DigestBytes(signingCertificateHistory[i].toByteArray()));
}
}
return new BackupSigningInfoState(currentCertDigests, pastCertDigests);
}
}
/**
* State that needs to be backed up for a package.
*/
private static class BackupPackageState {
@NonNull
final String mPackageName;
@NonNull
private final ArrayList mPermissionsToRestore;
@Nullable
private final BackupSigningInfoState mBackupSigningInfoState;
private BackupPackageState(
@NonNull String packageName,
@NonNull ArrayList permissionsToRestore,
@Nullable BackupSigningInfoState backupSigningInfoState) {
mPackageName = packageName;
mPermissionsToRestore = permissionsToRestore;
mBackupSigningInfoState = backupSigningInfoState;
}
/**
* Parse a package state from XML.
*
* @param parser The data to read
* @param context a context to use
* @param backupPlatformVersion The platform version the backup was created on
*
* @return The state
*/
@NonNull
static BackupPackageState parseFromXml(@NonNull XmlPullParser parser,
@NonNull Context context, int backupPlatformVersion)
throws IOException, XmlPullParserException {
String packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME);
if (packageName == null) {
throw new XmlPullParserException("Found " + TAG_GRANT + " without "
+ ATTR_PACKAGE_NAME);
}
ArrayMap permissionsToRestore = new ArrayMap<>();
BackupSigningInfoState signingInfo = null;
while (true) {
switch (parser.next()) {
case START_TAG:
switch (parser.getName()) {
case TAG_PERMISSION:
try {
addNewPermissions(permissionsToRestore,
BackupPermissionState.parseFromXml(parser, context,
backupPlatformVersion));
} catch (XmlPullParserException e) {
Log.e(LOG_TAG, "Could not parse permission for "
+ packageName, e);
}
skipToEndOfTag(parser);
break;
case TAG_SIGNING_INFO:
try {
signingInfo = BackupSigningInfoState.parseFromXml(parser);
} catch (XmlPullParserException e) {
Log.e(LOG_TAG, "Could not parse signing info for "
+ packageName, e);
skipToEndOfTag(parser);
}
break;
default:
// ignore tag
Log.w(LOG_TAG, "Found unexpected tag " + parser.getName()
+ " while restoring " + packageName);
skipToEndOfTag(parser);
}
break;
case END_TAG:
ArrayList permissionsToRestoreList =
new ArrayList<>();
int numPerms = permissionsToRestore.size();
for (int i = 0; i < numPerms; i++) {
permissionsToRestoreList.add(permissionsToRestore.valueAt(i));
}
return new BackupPackageState(
packageName,
permissionsToRestoreList,
signingInfo);
case END_DOCUMENT:
throw new XmlPullParserException("Could not parse state for "
+ packageName);
}
}
}
private static void addNewPermissions(
@NonNull ArrayMap permissionsToRestore,
@NonNull List newPermissionsToRestore) {
int numPerms = newPermissionsToRestore.size();
for (int i = 0; i < numPerms; i++) {
BackupPermissionState newPermission = newPermissionsToRestore.get(i);
boolean shouldOverwrite = true;
if (permissionsToRestore.containsKey(newPermission.mPermissionName)) {
// If it already exists only overwrite if newly added state was explicitly
// saved while existing state was implicit by permission split.
shouldOverwrite = !newPermission.mIsAddedFromSplit
&& permissionsToRestore.get(newPermission.mPermissionName)
.mIsAddedFromSplit;
}
if (shouldOverwrite) {
permissionsToRestore.put(newPermission.mPermissionName, newPermission);
}
}
}
/**
* Get the state of a package to back up.
*
* @param context A context to use
* @param pkgInfo The package to back up.
*
* @return The state to back up or {@code null} if no permission of the package need to be
* backed up.
*/
@Nullable
static BackupPackageState fromAppPermissions(@NonNull Context context,
@NonNull PackageInfo pkgInfo) {
AppPermissions appPerms = new AppPermissions(context, pkgInfo, false, null);
ArrayList permissionsToRestore = new ArrayList<>();
List groups = appPerms.getPermissionGroups();
int numGroups = groups.size();
for (int groupNum = 0; groupNum < numGroups; groupNum++) {
AppPermissionGroup group = groups.get(groupNum);
permissionsToRestore.addAll(BackupPermissionState.fromPermissionGroup(group));
// Background permissions are in a subgroup that is not part of
// {@link AppPermission#getPermissionGroups}. Hence add it explicitly here.
if (group.getBackgroundPermissions() != null) {
permissionsToRestore.addAll(BackupPermissionState.fromPermissionGroup(
group.getBackgroundPermissions()));
}
}
if (permissionsToRestore.size() == 0) {
return null;
}
BackupSigningInfoState signingInfoState = null;
if (pkgInfo.signingInfo != null) {
signingInfoState = BackupSigningInfoState.fromSigningInfo(pkgInfo.signingInfo);
}
return new BackupPackageState(
pkgInfo.packageName, permissionsToRestore, signingInfoState);
}
/**
* Write this state as XML.
*
* @param serializer The file to write to
*/
void writeAsXml(@NonNull XmlSerializer serializer) throws IOException {
if (mPermissionsToRestore.size() == 0) {
return;
}
serializer.startTag(null, TAG_GRANT);
serializer.attribute(null, ATTR_PACKAGE_NAME, mPackageName);
int numPerms = mPermissionsToRestore.size();
for (int i = 0; i < numPerms; i++) {
mPermissionsToRestore.get(i).writeAsXml(serializer);
}
if (mBackupSigningInfoState != null) {
mBackupSigningInfoState.writeAsXml(serializer);
}
serializer.endTag(null, TAG_GRANT);
}
/**
* Restore this package state.
*
* @param context A context to use
* @param pkgInfo The package to restore.
*/
void restore(@NonNull Context context, @NonNull PackageInfo pkgInfo) {
AppPermissions appPerms = new AppPermissions(context, pkgInfo, false, true, null);
ArraySet affectedPermissions = new ArraySet<>();
// Restore background permissions after foreground permissions as for pre-M apps bg
// granted and fg revoked cannot be expressed.
int numPerms = mPermissionsToRestore.size();
for (int i = 0; i < numPerms; i++) {
mPermissionsToRestore.get(i).restore(appPerms, false);
affectedPermissions.add(mPermissionsToRestore.get(i).mPermissionName);
}
for (int i = 0; i < numPerms; i++) {
mPermissionsToRestore.get(i).restore(appPerms, true);
}
int numGroups = appPerms.getPermissionGroups().size();
for (int i = 0; i < numGroups; i++) {
AppPermissionGroup group = appPerms.getPermissionGroups().get(i);
// Only denied groups can be user fixed
if (group.areRuntimePermissionsGranted()) {
group.setUserFixed(false);
}
AppPermissionGroup bgGroup = group.getBackgroundPermissions();
if (bgGroup != null) {
// Only denied groups can be user fixed
if (bgGroup.areRuntimePermissionsGranted()) {
bgGroup.setUserFixed(false);
}
}
}
appPerms.persistChanges(true, affectedPermissions);
}
}
/**
* Returns whether the signing certificates of the restored app and backed up app are
* compatible for the restored app to be granted the backed up app's permissions.
*
* This returns true when any one of the following is true:
*
*
* - the backed up app has multiple signing certificates and the restored app
* has identical multiple signing certificates
*
- the backed up app has a single signing certificate and it is the current
* single signing certificate of the restored app
*
- the backed up app has a single signing certificate and it is present in the
* signing certificate history of the restored app
*
- the backed up app has a single signing certificate and signing certificate
* history, and the signing certificate of the restored app is present in that history
*
*
*/
private boolean hasCompatibleSignaturesForRestore(@NonNull SigningInfo restoredSigningInfo,
@NonNull BackupSigningInfoState backupSigningInfoState) {
Set backupCertDigests = backupSigningInfoState.mCurrentCertDigests;
Set backupPastCertDigests = backupSigningInfoState.mPastCertDigests;
Signature[] restoredSignatures = restoredSigningInfo.getApkContentsSigners();
// Check that both apps have the same number of signing certificates. This will be a
// required check for both the single and multiple certificate cases.
if (backupCertDigests.size() != restoredSignatures.length) {
return false;
}
Set restoredCertDigests = new HashSet<>();
for (Signature signature: restoredSignatures) {
restoredCertDigests.add(computeSha256DigestBytes(signature.toByteArray()));
}
// If the backed up app has multiple signing certificates, the restored app should be
// signed by that exact set of multiple signing certificates.
if (backupCertDigests.size() > 1) {
// Check that the restored certificates are a subset of the backed up certificates.
if (!CollectionUtils.containsSubset(backupCertDigests, restoredCertDigests)) {
return false;
}
// Check that the backed up certificates are a subset of the restored certificates.
if (!CollectionUtils.containsSubset(restoredCertDigests, backupCertDigests)) {
return false;
}
return true;
}
// If both apps have a single signing certificate, we check if they are equal or if one
// app's certificate is in the signing certificate history of the other.
byte[] backupCertDigest = backupCertDigests.iterator().next();
byte[] restoredPastCertDigest = restoredCertDigests.iterator().next();
// Check if the backed up app and restored app have the same signing certificate.
if (Arrays.equals(backupCertDigest, restoredPastCertDigest)) {
return true;
}
// Check if the restored app's certificate is in the backed up app's signing certificate
// history.
if (CollectionUtils.contains(backupPastCertDigests, restoredPastCertDigest)) {
return true;
}
// Check if the backed up app's certificate is in the restored app's signing certificate
// history.
if (restoredSigningInfo.hasPastSigningCertificates()) {
// The last element in the pastSigningCertificates array is the current signer;
// since that was verified above, just check all the signers in the lineage.
for (int i = 0; i < restoredSigningInfo.getSigningCertificateHistory().length - 1;
i++) {
restoredPastCertDigest = computeSha256DigestBytes(
restoredSigningInfo.getSigningCertificateHistory()[i].toByteArray());
if (Arrays.equals(backupCertDigest, restoredPastCertDigest)) {
return true;
}
}
}
return false;
}
/** Computes the SHA256 digest of the provided {@code byte} array. */
@Nullable
private static byte[] computeSha256DigestBytes(@NonNull byte[] data) {
MessageDigest messageDigest;
try {
messageDigest = MessageDigest.getInstance("SHA256");
} catch (NoSuchAlgorithmException e) {
return null;
}
messageDigest.update(data);
return messageDigest.digest();
}
}