1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.app.backup; 18 19 import static android.app.backup.BackupManager.OperationType; 20 21 import android.annotation.Nullable; 22 import android.annotation.StringDef; 23 import android.app.compat.CompatChanges; 24 import android.compat.annotation.ChangeId; 25 import android.compat.annotation.EnabledSince; 26 import android.compat.annotation.Overridable; 27 import android.compat.annotation.UnsupportedAppUsage; 28 import android.content.Context; 29 import android.content.pm.ApplicationInfo; 30 import android.content.pm.PackageManager; 31 import android.content.res.XmlResourceParser; 32 import android.os.Build; 33 import android.os.ParcelFileDescriptor; 34 import android.os.Process; 35 import android.os.storage.StorageManager; 36 import android.os.storage.StorageVolume; 37 import android.system.ErrnoException; 38 import android.system.Os; 39 import android.text.TextUtils; 40 import android.util.ArrayMap; 41 import android.util.ArraySet; 42 import android.util.Log; 43 import android.util.Slog; 44 45 import com.android.internal.annotations.VisibleForTesting; 46 47 import org.xmlpull.v1.XmlPullParser; 48 import org.xmlpull.v1.XmlPullParserException; 49 50 import java.io.File; 51 import java.io.FileInputStream; 52 import java.io.FileOutputStream; 53 import java.io.IOException; 54 import java.util.Map; 55 import java.util.Objects; 56 import java.util.Optional; 57 import java.util.Set; 58 59 /** 60 * Global constant definitions et cetera related to the full-backup-to-fd 61 * binary format. Nothing in this namespace is part of any API; it's all 62 * hidden details of the current implementation gathered into one location. 63 * 64 * @hide 65 */ 66 public class FullBackup { 67 static final String TAG = "FullBackup"; 68 /** Enable this log tag to get verbose information while parsing the client xml. */ 69 static final String TAG_XML_PARSER = "BackupXmlParserLogging"; 70 71 public static final String APK_TREE_TOKEN = "a"; 72 public static final String OBB_TREE_TOKEN = "obb"; 73 public static final String KEY_VALUE_DATA_TOKEN = "k"; 74 75 public static final String ROOT_TREE_TOKEN = "r"; 76 public static final String FILES_TREE_TOKEN = "f"; 77 public static final String NO_BACKUP_TREE_TOKEN = "nb"; 78 public static final String DATABASE_TREE_TOKEN = "db"; 79 public static final String SHAREDPREFS_TREE_TOKEN = "sp"; 80 public static final String CACHE_TREE_TOKEN = "c"; 81 82 public static final String DEVICE_ROOT_TREE_TOKEN = "d_r"; 83 public static final String DEVICE_FILES_TREE_TOKEN = "d_f"; 84 public static final String DEVICE_NO_BACKUP_TREE_TOKEN = "d_nb"; 85 public static final String DEVICE_DATABASE_TREE_TOKEN = "d_db"; 86 public static final String DEVICE_SHAREDPREFS_TREE_TOKEN = "d_sp"; 87 public static final String DEVICE_CACHE_TREE_TOKEN = "d_c"; 88 89 public static final String MANAGED_EXTERNAL_TREE_TOKEN = "ef"; 90 public static final String SHARED_STORAGE_TOKEN = "shared"; 91 92 public static final String APPS_PREFIX = "apps/"; 93 public static final String SHARED_PREFIX = SHARED_STORAGE_TOKEN + "/"; 94 95 public static final String FULL_BACKUP_INTENT_ACTION = "fullback"; 96 public static final String FULL_RESTORE_INTENT_ACTION = "fullrest"; 97 public static final String CONF_TOKEN_INTENT_EXTRA = "conftoken"; 98 99 public static final String FLAG_REQUIRED_CLIENT_SIDE_ENCRYPTION = "clientSideEncryption"; 100 public static final String FLAG_REQUIRED_DEVICE_TO_DEVICE_TRANSFER = "deviceToDeviceTransfer"; 101 public static final String FLAG_REQUIRED_FAKE_CLIENT_SIDE_ENCRYPTION = 102 "fakeClientSideEncryption"; 103 private static final String FLAG_DISABLE_IF_NO_ENCRYPTION_CAPABILITIES 104 = "disableIfNoEncryptionCapabilities"; 105 106 /** 107 * When this change is enabled, include / exclude rules specified via 108 * {@code android:fullBackupContent} are ignored during D2D transfers. 109 */ 110 @ChangeId 111 @Overridable 112 @EnabledSince(targetSdkVersion = Build.VERSION_CODES.S) 113 private static final long IGNORE_FULL_BACKUP_CONTENT_IN_D2D = 180523564L; 114 115 @StringDef({ 116 ConfigSection.CLOUD_BACKUP, 117 ConfigSection.DEVICE_TRANSFER 118 }) 119 @interface ConfigSection { 120 String CLOUD_BACKUP = "cloud-backup"; 121 String DEVICE_TRANSFER = "device-transfer"; 122 } 123 124 /** 125 * Identify {@link BackupScheme} object by package and operation type 126 * (see {@link OperationType}) it corresponds to. 127 */ 128 private static class BackupSchemeId { 129 final String mPackageName; 130 @OperationType final int mOperationType; 131 BackupSchemeId(String packageName, @OperationType int operationType)132 BackupSchemeId(String packageName, @OperationType int operationType) { 133 mPackageName = packageName; 134 mOperationType = operationType; 135 } 136 137 @Override hashCode()138 public int hashCode() { 139 return Objects.hash(mPackageName, mOperationType); 140 } 141 142 @Override equals(@ullable Object object)143 public boolean equals(@Nullable Object object) { 144 if (this == object) { 145 return true; 146 } 147 if (object == null || getClass() != object.getClass()) { 148 return false; 149 } 150 BackupSchemeId that = (BackupSchemeId) object; 151 return Objects.equals(mPackageName, that.mPackageName) && 152 Objects.equals(mOperationType, that.mOperationType); 153 } 154 } 155 156 /** 157 * @hide 158 */ 159 @UnsupportedAppUsage backupToTar(String packageName, String domain, String linkdomain, String rootpath, String path, FullBackupDataOutput output)160 static public native int backupToTar(String packageName, String domain, 161 String linkdomain, String rootpath, String path, FullBackupDataOutput output); 162 163 private static final Map<BackupSchemeId, BackupScheme> kPackageBackupSchemeMap = 164 new ArrayMap<>(); 165 getBackupScheme(Context context, @OperationType int operationType)166 static synchronized BackupScheme getBackupScheme(Context context, 167 @OperationType int operationType) { 168 BackupSchemeId backupSchemeId = new BackupSchemeId(context.getPackageName(), operationType); 169 BackupScheme backupSchemeForPackage = 170 kPackageBackupSchemeMap.get(backupSchemeId); 171 if (backupSchemeForPackage == null) { 172 backupSchemeForPackage = new BackupScheme(context, operationType); 173 kPackageBackupSchemeMap.put(backupSchemeId, backupSchemeForPackage); 174 } 175 return backupSchemeForPackage; 176 } 177 getBackupSchemeForTest(Context context)178 public static BackupScheme getBackupSchemeForTest(Context context) { 179 BackupScheme testing = new BackupScheme(context, OperationType.BACKUP); 180 testing.mExcludes = new ArraySet(); 181 testing.mIncludes = new ArrayMap(); 182 return testing; 183 } 184 185 186 /** 187 * Copy data from a socket to the given File location on permanent storage. The 188 * modification time and access mode of the resulting file will be set if desired, 189 * although group/all rwx modes will be stripped: the restored file will not be 190 * accessible from outside the target application even if the original file was. 191 * If the {@code type} parameter indicates that the result should be a directory, 192 * the socket parameter may be {@code null}; even if it is valid, no data will be 193 * read from it in this case. 194 * <p> 195 * If the {@code mode} argument is negative, then the resulting output file will not 196 * have its access mode or last modification time reset as part of this operation. 197 * 198 * @param data Socket supplying the data to be copied to the output file. If the 199 * output is a directory, this may be {@code null}. 200 * @param size Number of bytes of data to copy from the socket to the file. At least 201 * this much data must be available through the {@code data} parameter. 202 * @param type Must be either {@link BackupAgent#TYPE_FILE} for ordinary file data 203 * or {@link BackupAgent#TYPE_DIRECTORY} for a directory. 204 * @param mode Unix-style file mode (as used by the chmod(2) syscall) to be set on 205 * the output file or directory. group/all rwx modes are stripped even if set 206 * in this parameter. If this parameter is negative then neither 207 * the mode nor the mtime values will be applied to the restored file. 208 * @param mtime A timestamp in the standard Unix epoch that will be imposed as the 209 * last modification time of the output file. if the {@code mode} parameter is 210 * negative then this parameter will be ignored. 211 * @param outFile Location within the filesystem to place the data. This must point 212 * to a location that is writeable by the caller, preferably using an absolute path. 213 * @throws IOException 214 */ restoreFile(ParcelFileDescriptor data, long size, int type, long mode, long mtime, File outFile)215 static public void restoreFile(ParcelFileDescriptor data, 216 long size, int type, long mode, long mtime, File outFile) throws IOException { 217 if (type == BackupAgent.TYPE_DIRECTORY) { 218 // Canonically a directory has no associated content, so we don't need to read 219 // anything from the pipe in this case. Just create the directory here and 220 // drop down to the final metadata adjustment. 221 if (outFile != null) outFile.mkdirs(); 222 } else { 223 FileOutputStream out = null; 224 225 // Pull the data from the pipe, copying it to the output file, until we're done 226 try { 227 if (outFile != null) { 228 File parent = outFile.getParentFile(); 229 if (!parent.exists()) { 230 // in practice this will only be for the default semantic directories, 231 // and using the default mode for those is appropriate. 232 // This can also happen for the case where a parent directory has been 233 // excluded, but a file within that directory has been included. 234 parent.mkdirs(); 235 } 236 out = new FileOutputStream(outFile); 237 } 238 } catch (IOException e) { 239 Log.e(TAG, "Unable to create/open file " + outFile.getPath(), e); 240 } 241 242 byte[] buffer = new byte[32 * 1024]; 243 final long origSize = size; 244 FileInputStream in = new FileInputStream(data.getFileDescriptor()); 245 while (size > 0) { 246 int toRead = (size > buffer.length) ? buffer.length : (int)size; 247 int got = in.read(buffer, 0, toRead); 248 if (got <= 0) { 249 Log.w(TAG, "Incomplete read: expected " + size + " but got " 250 + (origSize - size)); 251 break; 252 } 253 if (out != null) { 254 try { 255 out.write(buffer, 0, got); 256 } catch (IOException e) { 257 // Problem writing to the file. Quit copying data and delete 258 // the file, but of course keep consuming the input stream. 259 Log.e(TAG, "Unable to write to file " + outFile.getPath(), e); 260 out.close(); 261 out = null; 262 outFile.delete(); 263 } 264 } 265 size -= got; 266 } 267 if (out != null) out.close(); 268 } 269 270 // Now twiddle the state to match the backup, assuming all went well 271 if (mode >= 0 && outFile != null) { 272 try { 273 // explicitly prevent emplacement of files accessible by outside apps 274 mode &= 0700; 275 Os.chmod(outFile.getPath(), (int)mode); 276 } catch (ErrnoException e) { 277 e.rethrowAsIOException(); 278 } 279 outFile.setLastModified(mtime); 280 } 281 } 282 283 @VisibleForTesting 284 public static class BackupScheme { 285 private final File FILES_DIR; 286 private final File DATABASE_DIR; 287 private final File ROOT_DIR; 288 private final File SHAREDPREF_DIR; 289 private final File CACHE_DIR; 290 private final File NOBACKUP_DIR; 291 292 private final File DEVICE_FILES_DIR; 293 private final File DEVICE_DATABASE_DIR; 294 private final File DEVICE_ROOT_DIR; 295 private final File DEVICE_SHAREDPREF_DIR; 296 private final File DEVICE_CACHE_DIR; 297 private final File DEVICE_NOBACKUP_DIR; 298 299 private final File EXTERNAL_DIR; 300 301 private final static String TAG_INCLUDE = "include"; 302 private final static String TAG_EXCLUDE = "exclude"; 303 304 final int mDataExtractionRules; 305 final int mFullBackupContent; 306 @OperationType final int mOperationType; 307 final PackageManager mPackageManager; 308 final StorageManager mStorageManager; 309 final String mPackageName; 310 311 // lazy initialized, only when needed 312 private StorageVolume[] mVolumes = null; 313 314 // Properties the transport must have (e.g. encryption) for the operation to go ahead. 315 @Nullable private Integer mRequiredTransportFlags; 316 @Nullable private Boolean mIsUsingNewScheme; 317 318 /** 319 * Parse out the semantic domains into the correct physical location. 320 */ tokenToDirectoryPath(String domainToken)321 String tokenToDirectoryPath(String domainToken) { 322 try { 323 if (domainToken.equals(FullBackup.FILES_TREE_TOKEN)) { 324 return FILES_DIR.getCanonicalPath(); 325 } else if (domainToken.equals(FullBackup.DATABASE_TREE_TOKEN)) { 326 return DATABASE_DIR.getCanonicalPath(); 327 } else if (domainToken.equals(FullBackup.ROOT_TREE_TOKEN)) { 328 return ROOT_DIR.getCanonicalPath(); 329 } else if (domainToken.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) { 330 return SHAREDPREF_DIR.getCanonicalPath(); 331 } else if (domainToken.equals(FullBackup.CACHE_TREE_TOKEN)) { 332 return CACHE_DIR.getCanonicalPath(); 333 } else if (domainToken.equals(FullBackup.NO_BACKUP_TREE_TOKEN)) { 334 return NOBACKUP_DIR.getCanonicalPath(); 335 } else if (domainToken.equals(FullBackup.DEVICE_FILES_TREE_TOKEN)) { 336 return DEVICE_FILES_DIR.getCanonicalPath(); 337 } else if (domainToken.equals(FullBackup.DEVICE_DATABASE_TREE_TOKEN)) { 338 return DEVICE_DATABASE_DIR.getCanonicalPath(); 339 } else if (domainToken.equals(FullBackup.DEVICE_ROOT_TREE_TOKEN)) { 340 return DEVICE_ROOT_DIR.getCanonicalPath(); 341 } else if (domainToken.equals(FullBackup.DEVICE_SHAREDPREFS_TREE_TOKEN)) { 342 return DEVICE_SHAREDPREF_DIR.getCanonicalPath(); 343 } else if (domainToken.equals(FullBackup.DEVICE_CACHE_TREE_TOKEN)) { 344 return DEVICE_CACHE_DIR.getCanonicalPath(); 345 } else if (domainToken.equals(FullBackup.DEVICE_NO_BACKUP_TREE_TOKEN)) { 346 return DEVICE_NOBACKUP_DIR.getCanonicalPath(); 347 } else if (domainToken.equals(FullBackup.MANAGED_EXTERNAL_TREE_TOKEN)) { 348 if (EXTERNAL_DIR != null) { 349 return EXTERNAL_DIR.getCanonicalPath(); 350 } else { 351 return null; 352 } 353 } else if (domainToken.startsWith(FullBackup.SHARED_PREFIX)) { 354 return sharedDomainToPath(domainToken); 355 } 356 // Not a supported location 357 Log.i(TAG, "Unrecognized domain " + domainToken); 358 return null; 359 } catch (Exception e) { 360 Log.i(TAG, "Error reading directory for domain: " + domainToken); 361 return null; 362 } 363 364 } 365 sharedDomainToPath(String domain)366 private String sharedDomainToPath(String domain) throws IOException { 367 // already known to start with SHARED_PREFIX, so we just look after that 368 final String volume = domain.substring(FullBackup.SHARED_PREFIX.length()); 369 final StorageVolume[] volumes = getVolumeList(); 370 final int volNum = Integer.parseInt(volume); 371 if (volNum < mVolumes.length) { 372 return volumes[volNum].getPathFile().getCanonicalPath(); 373 } 374 return null; 375 } 376 getVolumeList()377 private StorageVolume[] getVolumeList() { 378 if (mStorageManager != null) { 379 if (mVolumes == null) { 380 mVolumes = mStorageManager.getVolumeList(); 381 } 382 } else { 383 Log.e(TAG, "Unable to access Storage Manager"); 384 } 385 return mVolumes; 386 } 387 388 /** 389 * Represents a path attribute specified in an <include /> rule along with optional 390 * transport flags required from the transport to include file(s) under that path as 391 * specified by requiredFlags attribute. If optional requiredFlags attribute is not 392 * provided, default requiredFlags to 0. 393 * Note: since our parsing codepaths were the same for <include /> and <exclude /> tags, 394 * this structure is also used for <exclude /> tags to preserve that, however you can expect 395 * the getRequiredFlags() to always return 0 for exclude rules. 396 */ 397 public static class PathWithRequiredFlags { 398 private final String mPath; 399 private final int mRequiredFlags; 400 PathWithRequiredFlags(String path, int requiredFlags)401 public PathWithRequiredFlags(String path, int requiredFlags) { 402 mPath = path; 403 mRequiredFlags = requiredFlags; 404 } 405 getPath()406 public String getPath() { 407 return mPath; 408 } 409 getRequiredFlags()410 public int getRequiredFlags() { 411 return mRequiredFlags; 412 } 413 } 414 415 /** 416 * A map of domain -> set of pairs (canonical file; required transport flags) in that 417 * domain that are to be included if the transport has decared the required flags. 418 * We keep track of the domain so that we can go through the file system in order later on. 419 */ 420 Map<String, Set<PathWithRequiredFlags>> mIncludes; 421 422 /** 423 * Set that will be populated with pairs (canonical file; requiredFlags=0) for each file or 424 * directory that is to be excluded. Note that for excludes, the requiredFlags attribute is 425 * ignored and the value should be always set to 0. 426 */ 427 ArraySet<PathWithRequiredFlags> mExcludes; 428 BackupScheme(Context context, @OperationType int operationType)429 BackupScheme(Context context, @OperationType int operationType) { 430 ApplicationInfo applicationInfo = context.getApplicationInfo(); 431 432 mDataExtractionRules = applicationInfo.dataExtractionRulesRes; 433 mFullBackupContent = applicationInfo.fullBackupContent; 434 mOperationType = operationType; 435 mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); 436 mPackageManager = context.getPackageManager(); 437 mPackageName = context.getPackageName(); 438 439 // System apps have control over where their default storage context 440 // is pointed, so we're always explicit when building paths. 441 final Context ceContext = context.createCredentialProtectedStorageContext(); 442 FILES_DIR = ceContext.getFilesDir(); 443 DATABASE_DIR = ceContext.getDatabasePath("foo").getParentFile(); 444 ROOT_DIR = ceContext.getDataDir(); 445 SHAREDPREF_DIR = ceContext.getSharedPreferencesPath("foo").getParentFile(); 446 CACHE_DIR = ceContext.getCacheDir(); 447 NOBACKUP_DIR = ceContext.getNoBackupFilesDir(); 448 449 final Context deContext = context.createDeviceProtectedStorageContext(); 450 DEVICE_FILES_DIR = deContext.getFilesDir(); 451 DEVICE_DATABASE_DIR = deContext.getDatabasePath("foo").getParentFile(); 452 DEVICE_ROOT_DIR = deContext.getDataDir(); 453 DEVICE_SHAREDPREF_DIR = deContext.getSharedPreferencesPath("foo").getParentFile(); 454 DEVICE_CACHE_DIR = deContext.getCacheDir(); 455 DEVICE_NOBACKUP_DIR = deContext.getNoBackupFilesDir(); 456 457 if (android.os.Process.myUid() != Process.SYSTEM_UID) { 458 EXTERNAL_DIR = context.getExternalFilesDir(null); 459 } else { 460 EXTERNAL_DIR = null; 461 } 462 } 463 isFullBackupEnabled(int transportFlags)464 boolean isFullBackupEnabled(int transportFlags) { 465 try { 466 if (isUsingNewScheme()) { 467 int requiredTransportFlags = getRequiredTransportFlags(); 468 // All bits that are set in requiredTransportFlags must be set in 469 // transportFlags. 470 return (transportFlags & requiredTransportFlags) == requiredTransportFlags; 471 } 472 } catch (IOException | XmlPullParserException e) { 473 Slog.w(TAG, "Failed to interpret the backup scheme: " + e); 474 return false; 475 } 476 477 return isFullBackupContentEnabled(); 478 } 479 isFullRestoreEnabled()480 boolean isFullRestoreEnabled() { 481 try { 482 if (isUsingNewScheme()) { 483 return true; 484 } 485 } catch (IOException | XmlPullParserException e) { 486 Slog.w(TAG, "Failed to interpret the backup scheme: " + e); 487 return false; 488 } 489 490 return isFullBackupContentEnabled(); 491 } 492 isFullBackupContentEnabled()493 boolean isFullBackupContentEnabled() { 494 if (mFullBackupContent < 0) { 495 // android:fullBackupContent="false", bail. 496 if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { 497 Log.v(FullBackup.TAG_XML_PARSER, "android:fullBackupContent - \"false\""); 498 } 499 return false; 500 } 501 return true; 502 } 503 504 /** 505 * @return A mapping of domain -> set of pairs (canonical file; required transport flags) 506 * in that domain that are to be included if the transport has decared the required flags. 507 * Each of these paths specifies a file that the client has explicitly included in their 508 * backup set. If this map is empty we will back up the entire data directory (including 509 * managed external storage). 510 */ 511 public synchronized Map<String, Set<PathWithRequiredFlags>> maybeParseAndGetCanonicalIncludePaths()512 maybeParseAndGetCanonicalIncludePaths() throws IOException, XmlPullParserException { 513 if (mIncludes == null) { 514 maybeParseBackupSchemeLocked(); 515 } 516 return mIncludes; 517 } 518 519 /** 520 * @return A set of (canonical paths; requiredFlags=0) that are to be excluded from the 521 * backup/restore set. 522 */ maybeParseAndGetCanonicalExcludePaths()523 public synchronized ArraySet<PathWithRequiredFlags> maybeParseAndGetCanonicalExcludePaths() 524 throws IOException, XmlPullParserException { 525 if (mExcludes == null) { 526 maybeParseBackupSchemeLocked(); 527 } 528 return mExcludes; 529 } 530 531 @VisibleForTesting getRequiredTransportFlags()532 public synchronized int getRequiredTransportFlags() 533 throws IOException, XmlPullParserException { 534 if (mRequiredTransportFlags == null) { 535 maybeParseBackupSchemeLocked(); 536 } 537 538 return mRequiredTransportFlags; 539 } 540 isUsingNewScheme()541 private synchronized boolean isUsingNewScheme() 542 throws IOException, XmlPullParserException { 543 if (mIsUsingNewScheme == null) { 544 maybeParseBackupSchemeLocked(); 545 } 546 547 return mIsUsingNewScheme; 548 } 549 maybeParseBackupSchemeLocked()550 private void maybeParseBackupSchemeLocked() throws IOException, XmlPullParserException { 551 // This not being null is how we know that we've tried to parse the xml already. 552 mIncludes = new ArrayMap<String, Set<PathWithRequiredFlags>>(); 553 mExcludes = new ArraySet<PathWithRequiredFlags>(); 554 mRequiredTransportFlags = 0; 555 mIsUsingNewScheme = false; 556 557 if (mFullBackupContent == 0 && mDataExtractionRules == 0) { 558 // No scheme specified via either new or legacy config, will copy everything. 559 if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { 560 Log.v(FullBackup.TAG_XML_PARSER, "android:fullBackupContent - \"true\""); 561 } 562 } else { 563 // Scheme is present. 564 if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { 565 Log.v(FullBackup.TAG_XML_PARSER, "Found xml scheme: " 566 + "android:fullBackupContent=" + mFullBackupContent 567 + "; android:dataExtractionRules=" + mDataExtractionRules); 568 } 569 570 try { 571 parseSchemeForOperationType(mOperationType); 572 } catch (PackageManager.NameNotFoundException e) { 573 // Throw it as an IOException 574 throw new IOException(e); 575 } 576 } 577 } 578 parseSchemeForOperationType(@perationType int operationType)579 private void parseSchemeForOperationType(@OperationType int operationType) 580 throws PackageManager.NameNotFoundException, IOException, XmlPullParserException { 581 String configSection = getConfigSectionForOperationType(operationType); 582 if (configSection == null) { 583 Slog.w(TAG, "Given operation type isn't supported by backup scheme: " 584 + operationType); 585 return; 586 } 587 588 if (mDataExtractionRules != 0) { 589 // New config is present. Use it if it has configuration for this operation 590 // type. 591 boolean isSectionPresent; 592 try (XmlResourceParser parser = getParserForResource(mDataExtractionRules)) { 593 isSectionPresent = parseNewBackupSchemeFromXmlLocked(parser, configSection, 594 mExcludes, mIncludes); 595 } 596 if (isSectionPresent) { 597 // Found the relevant section in the new config, we will use it. 598 mIsUsingNewScheme = true; 599 return; 600 } 601 } 602 603 if (operationType == OperationType.MIGRATION 604 && CompatChanges.isChangeEnabled(IGNORE_FULL_BACKUP_CONTENT_IN_D2D)) { 605 mIsUsingNewScheme = true; 606 return; 607 } 608 609 if (mFullBackupContent != 0) { 610 // Fall back to the old config. 611 try (XmlResourceParser parser = getParserForResource(mFullBackupContent)) { 612 parseBackupSchemeFromXmlLocked(parser, mExcludes, mIncludes); 613 } 614 } 615 } 616 617 @Nullable getConfigSectionForOperationType(@perationType int operationType)618 private String getConfigSectionForOperationType(@OperationType int operationType) { 619 switch (operationType) { 620 case OperationType.BACKUP: 621 return ConfigSection.CLOUD_BACKUP; 622 case OperationType.MIGRATION: 623 return ConfigSection.DEVICE_TRANSFER; 624 default: 625 return null; 626 } 627 } 628 getParserForResource(int resourceId)629 private XmlResourceParser getParserForResource(int resourceId) 630 throws PackageManager.NameNotFoundException { 631 return mPackageManager 632 .getResourcesForApplication(mPackageName) 633 .getXml(resourceId); 634 } 635 636 @VisibleForTesting parseNewBackupSchemeFromXmlLocked(XmlPullParser parser, @ConfigSection String configSection, Set<PathWithRequiredFlags> excludes, Map<String, Set<PathWithRequiredFlags>> includes)637 public boolean parseNewBackupSchemeFromXmlLocked(XmlPullParser parser, 638 @ConfigSection String configSection, 639 Set<PathWithRequiredFlags> excludes, 640 Map<String, Set<PathWithRequiredFlags>> includes) 641 throws IOException, XmlPullParserException { 642 verifyTopLevelTag(parser, "data-extraction-rules"); 643 644 boolean isSectionPresent = false; 645 646 int event; 647 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { 648 if (event != XmlPullParser.START_TAG || !configSection.equals(parser.getName())) { 649 continue; 650 } 651 652 isSectionPresent = true; 653 654 parseRequiredTransportFlags(parser, configSection); 655 parseRules(parser, excludes, includes, Optional.of(0), configSection); 656 } 657 658 logParsingResults(excludes, includes); 659 660 return isSectionPresent; 661 } 662 parseRequiredTransportFlags(XmlPullParser parser, @ConfigSection String configSection)663 private void parseRequiredTransportFlags(XmlPullParser parser, 664 @ConfigSection String configSection) { 665 if (ConfigSection.CLOUD_BACKUP.equals(configSection)) { 666 String encryptionAttribute = parser.getAttributeValue(/* namespace */ null, 667 FLAG_DISABLE_IF_NO_ENCRYPTION_CAPABILITIES); 668 if ("true".equals(encryptionAttribute)) { 669 mRequiredTransportFlags = BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED; 670 } 671 } 672 } 673 674 @VisibleForTesting parseBackupSchemeFromXmlLocked(XmlPullParser parser, Set<PathWithRequiredFlags> excludes, Map<String, Set<PathWithRequiredFlags>> includes)675 public void parseBackupSchemeFromXmlLocked(XmlPullParser parser, 676 Set<PathWithRequiredFlags> excludes, 677 Map<String, Set<PathWithRequiredFlags>> includes) 678 throws IOException, XmlPullParserException { 679 verifyTopLevelTag(parser, "full-backup-content"); 680 681 parseRules(parser, excludes, includes, Optional.empty(), "full-backup-content"); 682 683 logParsingResults(excludes, includes); 684 } 685 verifyTopLevelTag(XmlPullParser parser, String tag)686 private void verifyTopLevelTag(XmlPullParser parser, String tag) 687 throws XmlPullParserException, IOException { 688 int event = parser.getEventType(); // START_DOCUMENT 689 while (event != XmlPullParser.START_TAG) { 690 event = parser.next(); 691 } 692 693 if (!tag.equals(parser.getName())) { 694 throw new XmlPullParserException("Xml file didn't start with correct tag" + 695 " (" + tag + " ). Found \"" + parser.getName() + "\""); 696 } 697 698 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 699 Log.v(TAG_XML_PARSER, "\n"); 700 Log.v(TAG_XML_PARSER, "===================================================="); 701 Log.v(TAG_XML_PARSER, "Found valid " + tag + "; parsing xml resource."); 702 Log.v(TAG_XML_PARSER, "===================================================="); 703 Log.v(TAG_XML_PARSER, ""); 704 } 705 } 706 parseRules(XmlPullParser parser, Set<PathWithRequiredFlags> excludes, Map<String, Set<PathWithRequiredFlags>> includes, Optional<Integer> maybeRequiredFlags, String endingTag)707 private void parseRules(XmlPullParser parser, 708 Set<PathWithRequiredFlags> excludes, 709 Map<String, Set<PathWithRequiredFlags>> includes, 710 Optional<Integer> maybeRequiredFlags, 711 String endingTag) 712 throws IOException, XmlPullParserException { 713 int event; 714 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT 715 && !parser.getName().equals(endingTag)) { 716 switch (event) { 717 case XmlPullParser.START_TAG: 718 validateInnerTagContents(parser); 719 final String domainFromXml = parser.getAttributeValue(null, "domain"); 720 final File domainDirectory = getDirectoryForCriteriaDomain(domainFromXml); 721 if (domainDirectory == null) { 722 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 723 Log.v(TAG_XML_PARSER, "...parsing \"" + parser.getName() + "\": " 724 + "domain=\"" + domainFromXml + "\" invalid; skipping"); 725 } 726 break; 727 } 728 final File canonicalFile = 729 extractCanonicalFile(domainDirectory, 730 parser.getAttributeValue(null, "path")); 731 if (canonicalFile == null) { 732 break; 733 } 734 735 int requiredFlags = getRequiredFlagsForRule(parser, maybeRequiredFlags); 736 737 // retrieve the include/exclude set we'll be adding this rule to 738 Set<PathWithRequiredFlags> activeSet = parseCurrentTagForDomain( 739 parser, excludes, includes, domainFromXml); 740 activeSet.add(new PathWithRequiredFlags(canonicalFile.getCanonicalPath(), 741 requiredFlags)); 742 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 743 Log.v(TAG_XML_PARSER, "...parsed " + canonicalFile.getCanonicalPath() 744 + " for domain \"" + domainFromXml + "\", requiredFlags + \"" 745 + requiredFlags + "\""); 746 } 747 748 // Special case journal files (not dirs) for sqlite database. frowny-face. 749 // Note that for a restore, the file is never a directory (b/c it doesn't 750 // exist). We have no way of knowing a priori whether or not to expect a 751 // dir, so we add the -journal anyway to be safe. 752 if ("database".equals(domainFromXml) && !canonicalFile.isDirectory()) { 753 final String canonicalJournalPath = 754 canonicalFile.getCanonicalPath() + "-journal"; 755 activeSet.add(new PathWithRequiredFlags(canonicalJournalPath, 756 requiredFlags)); 757 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 758 Log.v(TAG_XML_PARSER, "...automatically generated " 759 + canonicalJournalPath + ". Ignore if nonexistent."); 760 } 761 final String canonicalWalPath = 762 canonicalFile.getCanonicalPath() + "-wal"; 763 activeSet.add(new PathWithRequiredFlags(canonicalWalPath, 764 requiredFlags)); 765 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 766 Log.v(TAG_XML_PARSER, "...automatically generated " 767 + canonicalWalPath + ". Ignore if nonexistent."); 768 } 769 } 770 771 // Special case for sharedpref files (not dirs) also add ".xml" suffix file. 772 if ("sharedpref".equals(domainFromXml) && !canonicalFile.isDirectory() && 773 !canonicalFile.getCanonicalPath().endsWith(".xml")) { 774 final String canonicalXmlPath = 775 canonicalFile.getCanonicalPath() + ".xml"; 776 activeSet.add(new PathWithRequiredFlags(canonicalXmlPath, 777 requiredFlags)); 778 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 779 Log.v(TAG_XML_PARSER, "...automatically generated " 780 + canonicalXmlPath + ". Ignore if nonexistent."); 781 } 782 } 783 } 784 } 785 } 786 logParsingResults(Set<PathWithRequiredFlags> excludes, Map<String, Set<PathWithRequiredFlags>> includes)787 private void logParsingResults(Set<PathWithRequiredFlags> excludes, 788 Map<String, Set<PathWithRequiredFlags>> includes) { 789 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 790 Log.v(TAG_XML_PARSER, "\n"); 791 Log.v(TAG_XML_PARSER, "Xml resource parsing complete."); 792 Log.v(TAG_XML_PARSER, "Final tally."); 793 Log.v(TAG_XML_PARSER, "Includes:"); 794 if (includes.isEmpty()) { 795 Log.v(TAG_XML_PARSER, " ...nothing specified (This means the entirety of app" 796 + " data minus excludes)"); 797 } else { 798 for (Map.Entry<String, Set<PathWithRequiredFlags>> entry 799 : includes.entrySet()) { 800 Log.v(TAG_XML_PARSER, " domain=" + entry.getKey()); 801 for (PathWithRequiredFlags includeData : entry.getValue()) { 802 Log.v(TAG_XML_PARSER, " path: " + includeData.getPath() 803 + " requiredFlags: " + includeData.getRequiredFlags()); 804 } 805 } 806 } 807 808 Log.v(TAG_XML_PARSER, "Excludes:"); 809 if (excludes.isEmpty()) { 810 Log.v(TAG_XML_PARSER, " ...nothing to exclude."); 811 } else { 812 for (PathWithRequiredFlags excludeData : excludes) { 813 Log.v(TAG_XML_PARSER, " path: " + excludeData.getPath() 814 + " requiredFlags: " + excludeData.getRequiredFlags()); 815 } 816 } 817 818 Log.v(TAG_XML_PARSER, " "); 819 Log.v(TAG_XML_PARSER, "===================================================="); 820 Log.v(TAG_XML_PARSER, "\n"); 821 } 822 } 823 getRequiredFlagsFromString(String requiredFlags)824 private int getRequiredFlagsFromString(String requiredFlags) { 825 int flags = 0; 826 if (requiredFlags == null || requiredFlags.length() == 0) { 827 // requiredFlags attribute was missing or empty in <include /> tag 828 return flags; 829 } 830 String[] flagsStr = requiredFlags.split("\\|"); 831 for (String f : flagsStr) { 832 switch (f) { 833 case FLAG_REQUIRED_CLIENT_SIDE_ENCRYPTION: 834 flags |= BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED; 835 break; 836 case FLAG_REQUIRED_DEVICE_TO_DEVICE_TRANSFER: 837 flags |= BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER; 838 break; 839 case FLAG_REQUIRED_FAKE_CLIENT_SIDE_ENCRYPTION: 840 flags |= BackupAgent.FLAG_FAKE_CLIENT_SIDE_ENCRYPTION_ENABLED; 841 default: 842 Log.w(TAG, "Unrecognized requiredFlag provided, value: \"" + f + "\""); 843 } 844 } 845 return flags; 846 } 847 getRequiredFlagsForRule(XmlPullParser parser, Optional<Integer> maybeRequiredFlags)848 private int getRequiredFlagsForRule(XmlPullParser parser, 849 Optional<Integer> maybeRequiredFlags) { 850 if (maybeRequiredFlags.isPresent()) { 851 // This is the new config format where required flags are specified for the whole 852 // section, not per rule. 853 return maybeRequiredFlags.get(); 854 } 855 856 if (TAG_INCLUDE.equals(parser.getName())) { 857 // In the legacy config, requiredFlags are only supported for <include /> tag, 858 // for <exclude /> we should always leave them as the default = 0. 859 return getRequiredFlagsFromString( 860 parser.getAttributeValue(null, "requireFlags")); 861 } 862 863 return 0; 864 } 865 parseCurrentTagForDomain(XmlPullParser parser, Set<PathWithRequiredFlags> excludes, Map<String, Set<PathWithRequiredFlags>> includes, String domain)866 private Set<PathWithRequiredFlags> parseCurrentTagForDomain(XmlPullParser parser, 867 Set<PathWithRequiredFlags> excludes, 868 Map<String, Set<PathWithRequiredFlags>> includes, String domain) 869 throws XmlPullParserException { 870 if (TAG_INCLUDE.equals(parser.getName())) { 871 final String domainToken = getTokenForXmlDomain(domain); 872 Set<PathWithRequiredFlags> includeSet = includes.get(domainToken); 873 if (includeSet == null) { 874 includeSet = new ArraySet<PathWithRequiredFlags>(); 875 includes.put(domainToken, includeSet); 876 } 877 return includeSet; 878 } else if (TAG_EXCLUDE.equals(parser.getName())) { 879 return excludes; 880 } else { 881 // Unrecognised tag => hard failure. 882 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 883 Log.v(TAG_XML_PARSER, "Invalid tag found in xml \"" 884 + parser.getName() + "\"; aborting operation."); 885 } 886 throw new XmlPullParserException("Unrecognised tag in backup" + 887 " criteria xml (" + parser.getName() + ")"); 888 } 889 } 890 891 /** 892 * Map xml specified domain (human-readable, what clients put in their manifest's xml) to 893 * BackupAgent internal data token. 894 * @return null if the xml domain was invalid. 895 */ getTokenForXmlDomain(String xmlDomain)896 private String getTokenForXmlDomain(String xmlDomain) { 897 if ("root".equals(xmlDomain)) { 898 return FullBackup.ROOT_TREE_TOKEN; 899 } else if ("file".equals(xmlDomain)) { 900 return FullBackup.FILES_TREE_TOKEN; 901 } else if ("database".equals(xmlDomain)) { 902 return FullBackup.DATABASE_TREE_TOKEN; 903 } else if ("sharedpref".equals(xmlDomain)) { 904 return FullBackup.SHAREDPREFS_TREE_TOKEN; 905 } else if ("device_root".equals(xmlDomain)) { 906 return FullBackup.DEVICE_ROOT_TREE_TOKEN; 907 } else if ("device_file".equals(xmlDomain)) { 908 return FullBackup.DEVICE_FILES_TREE_TOKEN; 909 } else if ("device_database".equals(xmlDomain)) { 910 return FullBackup.DEVICE_DATABASE_TREE_TOKEN; 911 } else if ("device_sharedpref".equals(xmlDomain)) { 912 return FullBackup.DEVICE_SHAREDPREFS_TREE_TOKEN; 913 } else if ("external".equals(xmlDomain)) { 914 return FullBackup.MANAGED_EXTERNAL_TREE_TOKEN; 915 } else { 916 return null; 917 } 918 } 919 920 /** 921 * 922 * @param domain Directory where the specified file should exist. Not null. 923 * @param filePathFromXml parsed from xml. Not sanitised before calling this function so may 924 * be null. 925 * @return The canonical path of the file specified or null if no such file exists. 926 */ extractCanonicalFile(File domain, String filePathFromXml)927 private File extractCanonicalFile(File domain, String filePathFromXml) { 928 if (filePathFromXml == null) { 929 // Allow things like <include domain="sharedpref"/> 930 filePathFromXml = ""; 931 } 932 if (filePathFromXml.contains("..")) { 933 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 934 Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml 935 + "\", but the \"..\" path is not permitted; skipping."); 936 } 937 return null; 938 } 939 if (filePathFromXml.contains("//")) { 940 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 941 Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml 942 + "\", which contains the invalid \"//\" sequence; skipping."); 943 } 944 return null; 945 } 946 return new File(domain, filePathFromXml); 947 } 948 949 /** 950 * @param domain parsed from xml. Not sanitised before calling this function so may be null. 951 * @return The directory relevant to the domain specified. 952 */ getDirectoryForCriteriaDomain(String domain)953 private File getDirectoryForCriteriaDomain(String domain) { 954 if (TextUtils.isEmpty(domain)) { 955 return null; 956 } 957 if ("file".equals(domain)) { 958 return FILES_DIR; 959 } else if ("database".equals(domain)) { 960 return DATABASE_DIR; 961 } else if ("root".equals(domain)) { 962 return ROOT_DIR; 963 } else if ("sharedpref".equals(domain)) { 964 return SHAREDPREF_DIR; 965 } else if ("device_file".equals(domain)) { 966 return DEVICE_FILES_DIR; 967 } else if ("device_database".equals(domain)) { 968 return DEVICE_DATABASE_DIR; 969 } else if ("device_root".equals(domain)) { 970 return DEVICE_ROOT_DIR; 971 } else if ("device_sharedpref".equals(domain)) { 972 return DEVICE_SHAREDPREF_DIR; 973 } else if ("external".equals(domain)) { 974 return EXTERNAL_DIR; 975 } else { 976 return null; 977 } 978 } 979 980 /** 981 * Let's be strict about the type of xml the client can write. If we see anything untoward, 982 * throw an XmlPullParserException. 983 */ validateInnerTagContents(XmlPullParser parser)984 private void validateInnerTagContents(XmlPullParser parser) throws XmlPullParserException { 985 if (parser == null) { 986 return; 987 } 988 switch (parser.getName()) { 989 case TAG_INCLUDE: 990 if (parser.getAttributeCount() > 3) { 991 throw new XmlPullParserException("At most 3 tag attributes allowed for " 992 + "\"include\" tag (\"domain\" & \"path\"" 993 + " & optional \"requiredFlags\")."); 994 } 995 break; 996 case TAG_EXCLUDE: 997 if (parser.getAttributeCount() > 2) { 998 throw new XmlPullParserException("At most 2 tag attributes allowed for " 999 + "\"exclude\" tag (\"domain\" & \"path\"."); 1000 } 1001 break; 1002 default: 1003 throw new XmlPullParserException("A valid tag is one of \"<include/>\" or" + 1004 " \"<exclude/>. You provided \"" + parser.getName() + "\""); 1005 } 1006 } 1007 } 1008 } 1009