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