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 android.annotation.Nullable; 20 import android.annotation.StringDef; 21 import android.app.backup.BackupAnnotations.BackupDestination; 22 import android.app.compat.CompatChanges; 23 import android.compat.annotation.ChangeId; 24 import android.compat.annotation.EnabledSince; 25 import android.compat.annotation.Overridable; 26 import android.compat.annotation.UnsupportedAppUsage; 27 import android.content.Context; 28 import android.content.pm.ApplicationInfo; 29 import android.content.pm.PackageManager; 30 import android.content.res.XmlResourceParser; 31 import android.os.Build; 32 import android.os.ParcelFileDescriptor; 33 import android.os.Process; 34 import android.os.storage.StorageManager; 35 import android.os.storage.StorageVolume; 36 import android.system.ErrnoException; 37 import android.system.Os; 38 import android.text.TextUtils; 39 import android.util.ArrayMap; 40 import android.util.ArraySet; 41 import android.util.Log; 42 import android.util.Slog; 43 44 import com.android.internal.annotations.VisibleForTesting; 45 46 import org.xmlpull.v1.XmlPullParser; 47 import org.xmlpull.v1.XmlPullParserException; 48 49 import java.io.File; 50 import java.io.FileInputStream; 51 import java.io.FileOutputStream; 52 import java.io.IOException; 53 import java.util.Map; 54 import java.util.Objects; 55 import java.util.Optional; 56 import java.util.Set; 57 58 /** 59 * Global constant definitions et cetera related to the full-backup-to-fd 60 * binary format. Nothing in this namespace is part of any API; it's all 61 * hidden details of the current implementation gathered into one location. 62 * 63 * @hide 64 */ 65 public class FullBackup { 66 static final String TAG = "FullBackup"; 67 /** Enable this log tag to get verbose information while parsing the client xml. */ 68 static final String TAG_XML_PARSER = "BackupXmlParserLogging"; 69 70 public static final String APK_TREE_TOKEN = "a"; 71 public static final String OBB_TREE_TOKEN = "obb"; 72 public static final String KEY_VALUE_DATA_TOKEN = "k"; 73 74 public static final String ROOT_TREE_TOKEN = "r"; 75 public static final String FILES_TREE_TOKEN = "f"; 76 public static final String NO_BACKUP_TREE_TOKEN = "nb"; 77 public static final String DATABASE_TREE_TOKEN = "db"; 78 public static final String SHAREDPREFS_TREE_TOKEN = "sp"; 79 public static final String CACHE_TREE_TOKEN = "c"; 80 81 public static final String DEVICE_ROOT_TREE_TOKEN = "d_r"; 82 public static final String DEVICE_FILES_TREE_TOKEN = "d_f"; 83 public static final String DEVICE_NO_BACKUP_TREE_TOKEN = "d_nb"; 84 public static final String DEVICE_DATABASE_TREE_TOKEN = "d_db"; 85 public static final String DEVICE_SHAREDPREFS_TREE_TOKEN = "d_sp"; 86 public static final String DEVICE_CACHE_TREE_TOKEN = "d_c"; 87 88 public static final String MANAGED_EXTERNAL_TREE_TOKEN = "ef"; 89 public static final String SHARED_STORAGE_TOKEN = "shared"; 90 91 public static final String APPS_PREFIX = "apps/"; 92 public static final String SHARED_PREFIX = SHARED_STORAGE_TOKEN + "/"; 93 94 public static final String FULL_BACKUP_INTENT_ACTION = "fullback"; 95 public static final String FULL_RESTORE_INTENT_ACTION = "fullrest"; 96 public static final String CONF_TOKEN_INTENT_EXTRA = "conftoken"; 97 98 public static final String FLAG_REQUIRED_CLIENT_SIDE_ENCRYPTION = "clientSideEncryption"; 99 public static final String FLAG_REQUIRED_DEVICE_TO_DEVICE_TRANSFER = "deviceToDeviceTransfer"; 100 public static final String FLAG_REQUIRED_FAKE_CLIENT_SIDE_ENCRYPTION = 101 "fakeClientSideEncryption"; 102 private static final String FLAG_DISABLE_IF_NO_ENCRYPTION_CAPABILITIES 103 = "disableIfNoEncryptionCapabilities"; 104 105 /** 106 * When this change is enabled, include / exclude rules specified via 107 * {@code android:fullBackupContent} are ignored during D2D transfers. 108 */ 109 @ChangeId 110 @Overridable 111 @EnabledSince(targetSdkVersion = Build.VERSION_CODES.S) 112 private static final long IGNORE_FULL_BACKUP_CONTENT_IN_D2D = 180523564L; 113 114 @StringDef({ 115 ConfigSection.CLOUD_BACKUP, 116 ConfigSection.DEVICE_TRANSFER 117 }) 118 @interface ConfigSection { 119 String CLOUD_BACKUP = "cloud-backup"; 120 String DEVICE_TRANSFER = "device-transfer"; 121 } 122 123 /** 124 * Identify {@link BackupScheme} object by package and operation type 125 * (see {@link BackupDestination}) it corresponds to. 126 */ 127 private static class BackupSchemeId { 128 final String mPackageName; 129 @BackupDestination final int mBackupDestination; 130 BackupSchemeId(String packageName, @BackupDestination int backupDestination)131 BackupSchemeId(String packageName, @BackupDestination int backupDestination) { 132 mPackageName = packageName; 133 mBackupDestination = backupDestination; 134 } 135 136 @Override hashCode()137 public int hashCode() { 138 return Objects.hash(mPackageName, mBackupDestination); 139 } 140 141 @Override equals(@ullable Object object)142 public boolean equals(@Nullable Object object) { 143 if (this == object) { 144 return true; 145 } 146 if (object == null || getClass() != object.getClass()) { 147 return false; 148 } 149 BackupSchemeId that = (BackupSchemeId) object; 150 return Objects.equals(mPackageName, that.mPackageName) && 151 Objects.equals(mBackupDestination, that.mBackupDestination); 152 } 153 } 154 155 /** 156 * @hide 157 */ 158 @UnsupportedAppUsage backupToTar(String packageName, String domain, String linkdomain, String rootpath, String path, FullBackupDataOutput output)159 static public native int backupToTar(String packageName, String domain, 160 String linkdomain, String rootpath, String path, FullBackupDataOutput output); 161 162 private static final Map<BackupSchemeId, BackupScheme> kPackageBackupSchemeMap = 163 new ArrayMap<>(); 164 getBackupScheme(Context context, @BackupDestination int backupDestination)165 static synchronized BackupScheme getBackupScheme(Context context, 166 @BackupDestination int backupDestination) { 167 BackupSchemeId backupSchemeId = new BackupSchemeId(context.getPackageName(), 168 backupDestination); 169 BackupScheme backupSchemeForPackage = 170 kPackageBackupSchemeMap.get(backupSchemeId); 171 if (backupSchemeForPackage == null) { 172 backupSchemeForPackage = new BackupScheme(context, backupDestination); 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, BackupDestination.CLOUD); 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[64 * 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 @BackupDestination final int mBackupDestination; 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, @BackupDestination int backupDestination)429 BackupScheme(Context context, @BackupDestination int backupDestination) { 430 ApplicationInfo applicationInfo = context.getApplicationInfo(); 431 432 mDataExtractionRules = applicationInfo.dataExtractionRulesRes; 433 mFullBackupContent = applicationInfo.fullBackupContent; 434 mBackupDestination = backupDestination; 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 parseSchemeForBackupDestination(mBackupDestination); 572 } catch (PackageManager.NameNotFoundException e) { 573 // Throw it as an IOException 574 throw new IOException(e); 575 } 576 } 577 } 578 parseSchemeForBackupDestination(@ackupDestination int backupDestination)579 private void parseSchemeForBackupDestination(@BackupDestination int backupDestination) 580 throws PackageManager.NameNotFoundException, IOException, XmlPullParserException { 581 String configSection = getConfigSectionForBackupDestination(backupDestination); 582 if (configSection == null) { 583 Slog.w(TAG, "Given backup destination isn't supported by backup scheme: " 584 + backupDestination); 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 (backupDestination == BackupDestination.DEVICE_TRANSFER 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 getConfigSectionForBackupDestination( @ackupDestination int backupDestination)618 private String getConfigSectionForBackupDestination( 619 @BackupDestination int backupDestination) { 620 switch (backupDestination) { 621 case BackupDestination.CLOUD: 622 return ConfigSection.CLOUD_BACKUP; 623 case BackupDestination.DEVICE_TRANSFER: 624 return ConfigSection.DEVICE_TRANSFER; 625 default: 626 return null; 627 } 628 } 629 getParserForResource(int resourceId)630 private XmlResourceParser getParserForResource(int resourceId) 631 throws PackageManager.NameNotFoundException { 632 return mPackageManager 633 .getResourcesForApplication(mPackageName) 634 .getXml(resourceId); 635 } 636 637 @VisibleForTesting parseNewBackupSchemeFromXmlLocked(XmlPullParser parser, @ConfigSection String configSection, Set<PathWithRequiredFlags> excludes, Map<String, Set<PathWithRequiredFlags>> includes)638 public boolean parseNewBackupSchemeFromXmlLocked(XmlPullParser parser, 639 @ConfigSection String configSection, 640 Set<PathWithRequiredFlags> excludes, 641 Map<String, Set<PathWithRequiredFlags>> includes) 642 throws IOException, XmlPullParserException { 643 verifyTopLevelTag(parser, "data-extraction-rules"); 644 645 boolean isSectionPresent = false; 646 647 int event; 648 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { 649 if (event != XmlPullParser.START_TAG || !configSection.equals(parser.getName())) { 650 continue; 651 } 652 653 isSectionPresent = true; 654 655 parseRequiredTransportFlags(parser, configSection); 656 parseRules(parser, excludes, includes, Optional.of(0), configSection); 657 } 658 659 logParsingResults(excludes, includes); 660 661 return isSectionPresent; 662 } 663 parseRequiredTransportFlags(XmlPullParser parser, @ConfigSection String configSection)664 private void parseRequiredTransportFlags(XmlPullParser parser, 665 @ConfigSection String configSection) { 666 if (ConfigSection.CLOUD_BACKUP.equals(configSection)) { 667 String encryptionAttribute = parser.getAttributeValue(/* namespace */ null, 668 FLAG_DISABLE_IF_NO_ENCRYPTION_CAPABILITIES); 669 if ("true".equals(encryptionAttribute)) { 670 mRequiredTransportFlags = BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED; 671 } 672 } 673 } 674 675 @VisibleForTesting parseBackupSchemeFromXmlLocked(XmlPullParser parser, Set<PathWithRequiredFlags> excludes, Map<String, Set<PathWithRequiredFlags>> includes)676 public void parseBackupSchemeFromXmlLocked(XmlPullParser parser, 677 Set<PathWithRequiredFlags> excludes, 678 Map<String, Set<PathWithRequiredFlags>> includes) 679 throws IOException, XmlPullParserException { 680 verifyTopLevelTag(parser, "full-backup-content"); 681 682 parseRules(parser, excludes, includes, Optional.empty(), "full-backup-content"); 683 684 logParsingResults(excludes, includes); 685 } 686 verifyTopLevelTag(XmlPullParser parser, String tag)687 private void verifyTopLevelTag(XmlPullParser parser, String tag) 688 throws XmlPullParserException, IOException { 689 int event = parser.getEventType(); // START_DOCUMENT 690 while (event != XmlPullParser.START_TAG) { 691 event = parser.next(); 692 } 693 694 if (!tag.equals(parser.getName())) { 695 throw new XmlPullParserException("Xml file didn't start with correct tag" + 696 " (" + tag + " ). Found \"" + parser.getName() + "\""); 697 } 698 699 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 700 Log.v(TAG_XML_PARSER, "\n"); 701 Log.v(TAG_XML_PARSER, "===================================================="); 702 Log.v(TAG_XML_PARSER, "Found valid " + tag + "; parsing xml resource."); 703 Log.v(TAG_XML_PARSER, "===================================================="); 704 Log.v(TAG_XML_PARSER, ""); 705 } 706 } 707 parseRules(XmlPullParser parser, Set<PathWithRequiredFlags> excludes, Map<String, Set<PathWithRequiredFlags>> includes, Optional<Integer> maybeRequiredFlags, String endingTag)708 private void parseRules(XmlPullParser parser, 709 Set<PathWithRequiredFlags> excludes, 710 Map<String, Set<PathWithRequiredFlags>> includes, 711 Optional<Integer> maybeRequiredFlags, 712 String endingTag) 713 throws IOException, XmlPullParserException { 714 int event; 715 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT 716 && !parser.getName().equals(endingTag)) { 717 switch (event) { 718 case XmlPullParser.START_TAG: 719 validateInnerTagContents(parser); 720 final String domainFromXml = parser.getAttributeValue(null, "domain"); 721 final File domainDirectory = getDirectoryForCriteriaDomain(domainFromXml); 722 if (domainDirectory == null) { 723 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 724 Log.v(TAG_XML_PARSER, "...parsing \"" + parser.getName() + "\": " 725 + "domain=\"" + domainFromXml + "\" invalid; skipping"); 726 } 727 break; 728 } 729 final File canonicalFile = 730 extractCanonicalFile(domainDirectory, 731 parser.getAttributeValue(null, "path")); 732 if (canonicalFile == null) { 733 break; 734 } 735 736 int requiredFlags = getRequiredFlagsForRule(parser, maybeRequiredFlags); 737 738 // retrieve the include/exclude set we'll be adding this rule to 739 Set<PathWithRequiredFlags> activeSet = parseCurrentTagForDomain( 740 parser, excludes, includes, domainFromXml); 741 activeSet.add(new PathWithRequiredFlags(canonicalFile.getCanonicalPath(), 742 requiredFlags)); 743 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 744 Log.v(TAG_XML_PARSER, "...parsed " + canonicalFile.getCanonicalPath() 745 + " for domain \"" + domainFromXml + "\", requiredFlags + \"" 746 + requiredFlags + "\""); 747 } 748 749 // Special case journal files (not dirs) for sqlite database. frowny-face. 750 // Note that for a restore, the file is never a directory (b/c it doesn't 751 // exist). We have no way of knowing a priori whether or not to expect a 752 // dir, so we add the -journal anyway to be safe. 753 if ("database".equals(domainFromXml) && !canonicalFile.isDirectory()) { 754 final String canonicalJournalPath = 755 canonicalFile.getCanonicalPath() + "-journal"; 756 activeSet.add(new PathWithRequiredFlags(canonicalJournalPath, 757 requiredFlags)); 758 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 759 Log.v(TAG_XML_PARSER, "...automatically generated " 760 + canonicalJournalPath + ". Ignore if nonexistent."); 761 } 762 final String canonicalWalPath = 763 canonicalFile.getCanonicalPath() + "-wal"; 764 activeSet.add(new PathWithRequiredFlags(canonicalWalPath, 765 requiredFlags)); 766 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 767 Log.v(TAG_XML_PARSER, "...automatically generated " 768 + canonicalWalPath + ". Ignore if nonexistent."); 769 } 770 } 771 772 // Special case for sharedpref files (not dirs) also add ".xml" suffix file. 773 if ("sharedpref".equals(domainFromXml) && !canonicalFile.isDirectory() && 774 !canonicalFile.getCanonicalPath().endsWith(".xml")) { 775 final String canonicalXmlPath = 776 canonicalFile.getCanonicalPath() + ".xml"; 777 activeSet.add(new PathWithRequiredFlags(canonicalXmlPath, 778 requiredFlags)); 779 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 780 Log.v(TAG_XML_PARSER, "...automatically generated " 781 + canonicalXmlPath + ". Ignore if nonexistent."); 782 } 783 } 784 } 785 } 786 } 787 logParsingResults(Set<PathWithRequiredFlags> excludes, Map<String, Set<PathWithRequiredFlags>> includes)788 private void logParsingResults(Set<PathWithRequiredFlags> excludes, 789 Map<String, Set<PathWithRequiredFlags>> includes) { 790 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 791 Log.v(TAG_XML_PARSER, "\n"); 792 Log.v(TAG_XML_PARSER, "Xml resource parsing complete."); 793 Log.v(TAG_XML_PARSER, "Final tally."); 794 Log.v(TAG_XML_PARSER, "Includes:"); 795 if (includes.isEmpty()) { 796 Log.v(TAG_XML_PARSER, " ...nothing specified (This means the entirety of app" 797 + " data minus excludes)"); 798 } else { 799 for (Map.Entry<String, Set<PathWithRequiredFlags>> entry 800 : includes.entrySet()) { 801 Log.v(TAG_XML_PARSER, " domain=" + entry.getKey()); 802 for (PathWithRequiredFlags includeData : entry.getValue()) { 803 Log.v(TAG_XML_PARSER, " path: " + includeData.getPath() 804 + " requiredFlags: " + includeData.getRequiredFlags()); 805 } 806 } 807 } 808 809 Log.v(TAG_XML_PARSER, "Excludes:"); 810 if (excludes.isEmpty()) { 811 Log.v(TAG_XML_PARSER, " ...nothing to exclude."); 812 } else { 813 for (PathWithRequiredFlags excludeData : excludes) { 814 Log.v(TAG_XML_PARSER, " path: " + excludeData.getPath() 815 + " requiredFlags: " + excludeData.getRequiredFlags()); 816 } 817 } 818 819 Log.v(TAG_XML_PARSER, " "); 820 Log.v(TAG_XML_PARSER, "===================================================="); 821 Log.v(TAG_XML_PARSER, "\n"); 822 } 823 } 824 getRequiredFlagsFromString(String requiredFlags)825 private int getRequiredFlagsFromString(String requiredFlags) { 826 int flags = 0; 827 if (requiredFlags == null || requiredFlags.length() == 0) { 828 // requiredFlags attribute was missing or empty in <include /> tag 829 return flags; 830 } 831 String[] flagsStr = requiredFlags.split("\\|"); 832 for (String f : flagsStr) { 833 switch (f) { 834 case FLAG_REQUIRED_CLIENT_SIDE_ENCRYPTION: 835 flags |= BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED; 836 break; 837 case FLAG_REQUIRED_DEVICE_TO_DEVICE_TRANSFER: 838 flags |= BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER; 839 break; 840 case FLAG_REQUIRED_FAKE_CLIENT_SIDE_ENCRYPTION: 841 flags |= BackupAgent.FLAG_FAKE_CLIENT_SIDE_ENCRYPTION_ENABLED; 842 default: 843 Log.w(TAG, "Unrecognized requiredFlag provided, value: \"" + f + "\""); 844 } 845 } 846 return flags; 847 } 848 getRequiredFlagsForRule(XmlPullParser parser, Optional<Integer> maybeRequiredFlags)849 private int getRequiredFlagsForRule(XmlPullParser parser, 850 Optional<Integer> maybeRequiredFlags) { 851 if (maybeRequiredFlags.isPresent()) { 852 // This is the new config format where required flags are specified for the whole 853 // section, not per rule. 854 return maybeRequiredFlags.get(); 855 } 856 857 if (TAG_INCLUDE.equals(parser.getName())) { 858 // In the legacy config, requiredFlags are only supported for <include /> tag, 859 // for <exclude /> we should always leave them as the default = 0. 860 return getRequiredFlagsFromString( 861 parser.getAttributeValue(null, "requireFlags")); 862 } 863 864 return 0; 865 } 866 parseCurrentTagForDomain(XmlPullParser parser, Set<PathWithRequiredFlags> excludes, Map<String, Set<PathWithRequiredFlags>> includes, String domain)867 private Set<PathWithRequiredFlags> parseCurrentTagForDomain(XmlPullParser parser, 868 Set<PathWithRequiredFlags> excludes, 869 Map<String, Set<PathWithRequiredFlags>> includes, String domain) 870 throws XmlPullParserException { 871 if (TAG_INCLUDE.equals(parser.getName())) { 872 final String domainToken = getTokenForXmlDomain(domain); 873 Set<PathWithRequiredFlags> includeSet = includes.get(domainToken); 874 if (includeSet == null) { 875 includeSet = new ArraySet<PathWithRequiredFlags>(); 876 includes.put(domainToken, includeSet); 877 } 878 return includeSet; 879 } else if (TAG_EXCLUDE.equals(parser.getName())) { 880 return excludes; 881 } else { 882 // Unrecognised tag => hard failure. 883 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 884 Log.v(TAG_XML_PARSER, "Invalid tag found in xml \"" 885 + parser.getName() + "\"; aborting operation."); 886 } 887 throw new XmlPullParserException("Unrecognised tag in backup" + 888 " criteria xml (" + parser.getName() + ")"); 889 } 890 } 891 892 /** 893 * Map xml specified domain (human-readable, what clients put in their manifest's xml) to 894 * BackupAgent internal data token. 895 * @return null if the xml domain was invalid. 896 */ getTokenForXmlDomain(String xmlDomain)897 private String getTokenForXmlDomain(String xmlDomain) { 898 if ("root".equals(xmlDomain)) { 899 return FullBackup.ROOT_TREE_TOKEN; 900 } else if ("file".equals(xmlDomain)) { 901 return FullBackup.FILES_TREE_TOKEN; 902 } else if ("database".equals(xmlDomain)) { 903 return FullBackup.DATABASE_TREE_TOKEN; 904 } else if ("sharedpref".equals(xmlDomain)) { 905 return FullBackup.SHAREDPREFS_TREE_TOKEN; 906 } else if ("device_root".equals(xmlDomain)) { 907 return FullBackup.DEVICE_ROOT_TREE_TOKEN; 908 } else if ("device_file".equals(xmlDomain)) { 909 return FullBackup.DEVICE_FILES_TREE_TOKEN; 910 } else if ("device_database".equals(xmlDomain)) { 911 return FullBackup.DEVICE_DATABASE_TREE_TOKEN; 912 } else if ("device_sharedpref".equals(xmlDomain)) { 913 return FullBackup.DEVICE_SHAREDPREFS_TREE_TOKEN; 914 } else if ("external".equals(xmlDomain)) { 915 return FullBackup.MANAGED_EXTERNAL_TREE_TOKEN; 916 } else { 917 return null; 918 } 919 } 920 921 /** 922 * 923 * @param domain Directory where the specified file should exist. Not null. 924 * @param filePathFromXml parsed from xml. Not sanitised before calling this function so may 925 * be null. 926 * @return The canonical path of the file specified or null if no such file exists. 927 */ extractCanonicalFile(File domain, String filePathFromXml)928 private File extractCanonicalFile(File domain, String filePathFromXml) { 929 if (filePathFromXml == null) { 930 // Allow things like <include domain="sharedpref"/> 931 filePathFromXml = ""; 932 } 933 if (filePathFromXml.contains("..")) { 934 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 935 Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml 936 + "\", but the \"..\" path is not permitted; skipping."); 937 } 938 return null; 939 } 940 if (filePathFromXml.contains("//")) { 941 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { 942 Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml 943 + "\", which contains the invalid \"//\" sequence; skipping."); 944 } 945 return null; 946 } 947 return new File(domain, filePathFromXml); 948 } 949 950 /** 951 * @param domain parsed from xml. Not sanitised before calling this function so may be null. 952 * @return The directory relevant to the domain specified. 953 */ getDirectoryForCriteriaDomain(String domain)954 private File getDirectoryForCriteriaDomain(String domain) { 955 if (TextUtils.isEmpty(domain)) { 956 return null; 957 } 958 if ("file".equals(domain)) { 959 return FILES_DIR; 960 } else if ("database".equals(domain)) { 961 return DATABASE_DIR; 962 } else if ("root".equals(domain)) { 963 return ROOT_DIR; 964 } else if ("sharedpref".equals(domain)) { 965 return SHAREDPREF_DIR; 966 } else if ("device_file".equals(domain)) { 967 return DEVICE_FILES_DIR; 968 } else if ("device_database".equals(domain)) { 969 return DEVICE_DATABASE_DIR; 970 } else if ("device_root".equals(domain)) { 971 return DEVICE_ROOT_DIR; 972 } else if ("device_sharedpref".equals(domain)) { 973 return DEVICE_SHAREDPREF_DIR; 974 } else if ("external".equals(domain)) { 975 return EXTERNAL_DIR; 976 } else { 977 return null; 978 } 979 } 980 981 /** 982 * Let's be strict about the type of xml the client can write. If we see anything untoward, 983 * throw an XmlPullParserException. 984 */ validateInnerTagContents(XmlPullParser parser)985 private void validateInnerTagContents(XmlPullParser parser) throws XmlPullParserException { 986 if (parser == null) { 987 return; 988 } 989 switch (parser.getName()) { 990 case TAG_INCLUDE: 991 if (parser.getAttributeCount() > 3) { 992 throw new XmlPullParserException("At most 3 tag attributes allowed for " 993 + "\"include\" tag (\"domain\" & \"path\"" 994 + " & optional \"requiredFlags\")."); 995 } 996 break; 997 case TAG_EXCLUDE: 998 if (parser.getAttributeCount() > 2) { 999 throw new XmlPullParserException("At most 2 tag attributes allowed for " 1000 + "\"exclude\" tag (\"domain\" & \"path\"."); 1001 } 1002 break; 1003 default: 1004 throw new XmlPullParserException("A valid tag is one of \"<include/>\" or" + 1005 " \"<exclude/>. You provided \"" + parser.getName() + "\""); 1006 } 1007 } 1008 } 1009 } 1010