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.content.Context;
20 import android.content.pm.PackageManager;
21 import android.content.res.XmlResourceParser;
22 import android.os.*;
23 import android.os.Process;
24 import android.system.ErrnoException;
25 import android.system.Os;
26 import android.text.TextUtils;
27 import android.util.ArrayMap;
28 import android.util.ArraySet;
29 import android.util.Log;
30 
31 import com.android.internal.annotations.VisibleForTesting;
32 
33 import org.xmlpull.v1.XmlPullParser;
34 
35 import java.io.File;
36 import java.io.FileInputStream;
37 import java.io.FileOutputStream;
38 import java.io.IOException;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Set;
42 
43 import org.xmlpull.v1.XmlPullParserException;
44 /**
45  * Global constant definitions et cetera related to the full-backup-to-fd
46  * binary format.  Nothing in this namespace is part of any API; it's all
47  * hidden details of the current implementation gathered into one location.
48  *
49  * @hide
50  */
51 public class FullBackup {
52     static final String TAG = "FullBackup";
53     /** Enable this log tag to get verbose information while parsing the client xml. */
54     static final String TAG_XML_PARSER = "BackupXmlParserLogging";
55 
56     public static final String APK_TREE_TOKEN = "a";
57     public static final String OBB_TREE_TOKEN = "obb";
58     public static final String ROOT_TREE_TOKEN = "r";
59     public static final String DATA_TREE_TOKEN = "f";
60     public static final String NO_BACKUP_TREE_TOKEN = "nb";
61     public static final String DATABASE_TREE_TOKEN = "db";
62     public static final String SHAREDPREFS_TREE_TOKEN = "sp";
63     public static final String MANAGED_EXTERNAL_TREE_TOKEN = "ef";
64     public static final String CACHE_TREE_TOKEN = "c";
65     public static final String SHARED_STORAGE_TOKEN = "shared";
66 
67     public static final String APPS_PREFIX = "apps/";
68     public static final String SHARED_PREFIX = SHARED_STORAGE_TOKEN + "/";
69 
70     public static final String FULL_BACKUP_INTENT_ACTION = "fullback";
71     public static final String FULL_RESTORE_INTENT_ACTION = "fullrest";
72     public static final String CONF_TOKEN_INTENT_EXTRA = "conftoken";
73 
74     /**
75      * @hide
76      */
backupToTar(String packageName, String domain, String linkdomain, String rootpath, String path, FullBackupDataOutput output)77     static public native int backupToTar(String packageName, String domain,
78             String linkdomain, String rootpath, String path, FullBackupDataOutput output);
79 
80     private static final Map<String, BackupScheme> kPackageBackupSchemeMap =
81             new ArrayMap<String, BackupScheme>();
82 
getBackupScheme(Context context)83     static synchronized BackupScheme getBackupScheme(Context context) {
84         BackupScheme backupSchemeForPackage =
85                 kPackageBackupSchemeMap.get(context.getPackageName());
86         if (backupSchemeForPackage == null) {
87             backupSchemeForPackage = new BackupScheme(context);
88             kPackageBackupSchemeMap.put(context.getPackageName(), backupSchemeForPackage);
89         }
90         return backupSchemeForPackage;
91     }
92 
getBackupSchemeForTest(Context context)93     public static BackupScheme getBackupSchemeForTest(Context context) {
94         BackupScheme testing = new BackupScheme(context);
95         testing.mExcludes = new ArraySet();
96         testing.mIncludes = new ArrayMap();
97         return testing;
98     }
99 
100 
101     /**
102      * Copy data from a socket to the given File location on permanent storage.  The
103      * modification time and access mode of the resulting file will be set if desired,
104      * although group/all rwx modes will be stripped: the restored file will not be
105      * accessible from outside the target application even if the original file was.
106      * If the {@code type} parameter indicates that the result should be a directory,
107      * the socket parameter may be {@code null}; even if it is valid, no data will be
108      * read from it in this case.
109      * <p>
110      * If the {@code mode} argument is negative, then the resulting output file will not
111      * have its access mode or last modification time reset as part of this operation.
112      *
113      * @param data Socket supplying the data to be copied to the output file.  If the
114      *    output is a directory, this may be {@code null}.
115      * @param size Number of bytes of data to copy from the socket to the file.  At least
116      *    this much data must be available through the {@code data} parameter.
117      * @param type Must be either {@link BackupAgent#TYPE_FILE} for ordinary file data
118      *    or {@link BackupAgent#TYPE_DIRECTORY} for a directory.
119      * @param mode Unix-style file mode (as used by the chmod(2) syscall) to be set on
120      *    the output file or directory.  group/all rwx modes are stripped even if set
121      *    in this parameter.  If this parameter is negative then neither
122      *    the mode nor the mtime values will be applied to the restored file.
123      * @param mtime A timestamp in the standard Unix epoch that will be imposed as the
124      *    last modification time of the output file.  if the {@code mode} parameter is
125      *    negative then this parameter will be ignored.
126      * @param outFile Location within the filesystem to place the data.  This must point
127      *    to a location that is writeable by the caller, preferably using an absolute path.
128      * @throws IOException
129      */
restoreFile(ParcelFileDescriptor data, long size, int type, long mode, long mtime, File outFile)130     static public void restoreFile(ParcelFileDescriptor data,
131             long size, int type, long mode, long mtime, File outFile) throws IOException {
132         if (type == BackupAgent.TYPE_DIRECTORY) {
133             // Canonically a directory has no associated content, so we don't need to read
134             // anything from the pipe in this case.  Just create the directory here and
135             // drop down to the final metadata adjustment.
136             if (outFile != null) outFile.mkdirs();
137         } else {
138             FileOutputStream out = null;
139 
140             // Pull the data from the pipe, copying it to the output file, until we're done
141             try {
142                 if (outFile != null) {
143                     File parent = outFile.getParentFile();
144                     if (!parent.exists()) {
145                         // in practice this will only be for the default semantic directories,
146                         // and using the default mode for those is appropriate.
147                         // This can also happen for the case where a parent directory has been
148                         // excluded, but a file within that directory has been included.
149                         parent.mkdirs();
150                     }
151                     out = new FileOutputStream(outFile);
152                 }
153             } catch (IOException e) {
154                 Log.e(TAG, "Unable to create/open file " + outFile.getPath(), e);
155             }
156 
157             byte[] buffer = new byte[32 * 1024];
158             final long origSize = size;
159             FileInputStream in = new FileInputStream(data.getFileDescriptor());
160             while (size > 0) {
161                 int toRead = (size > buffer.length) ? buffer.length : (int)size;
162                 int got = in.read(buffer, 0, toRead);
163                 if (got <= 0) {
164                     Log.w(TAG, "Incomplete read: expected " + size + " but got "
165                             + (origSize - size));
166                     break;
167                 }
168                 if (out != null) {
169                     try {
170                         out.write(buffer, 0, got);
171                     } catch (IOException e) {
172                         // Problem writing to the file.  Quit copying data and delete
173                         // the file, but of course keep consuming the input stream.
174                         Log.e(TAG, "Unable to write to file " + outFile.getPath(), e);
175                         out.close();
176                         out = null;
177                         outFile.delete();
178                     }
179                 }
180                 size -= got;
181             }
182             if (out != null) out.close();
183         }
184 
185         // Now twiddle the state to match the backup, assuming all went well
186         if (mode >= 0 && outFile != null) {
187             try {
188                 // explicitly prevent emplacement of files accessible by outside apps
189                 mode &= 0700;
190                 Os.chmod(outFile.getPath(), (int)mode);
191             } catch (ErrnoException e) {
192                 e.rethrowAsIOException();
193             }
194             outFile.setLastModified(mtime);
195         }
196     }
197 
198     @VisibleForTesting
199     public static class BackupScheme {
200         private final File FILES_DIR;
201         private final File DATABASE_DIR;
202         private final File ROOT_DIR;
203         private final File SHAREDPREF_DIR;
204         private final File EXTERNAL_DIR;
205         private final File CACHE_DIR;
206         private final File NOBACKUP_DIR;
207 
208         final int mFullBackupContent;
209         final PackageManager mPackageManager;
210         final String mPackageName;
211 
212         /**
213          * Parse out the semantic domains into the correct physical location.
214          */
tokenToDirectoryPath(String domainToken)215         String tokenToDirectoryPath(String domainToken) {
216             try {
217                 if (domainToken.equals(FullBackup.DATA_TREE_TOKEN)) {
218                     return FILES_DIR.getCanonicalPath();
219                 } else if (domainToken.equals(FullBackup.DATABASE_TREE_TOKEN)) {
220                     return DATABASE_DIR.getCanonicalPath();
221                 } else if (domainToken.equals(FullBackup.ROOT_TREE_TOKEN)) {
222                     return ROOT_DIR.getCanonicalPath();
223                 } else if (domainToken.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) {
224                     return SHAREDPREF_DIR.getCanonicalPath();
225                 } else if (domainToken.equals(FullBackup.CACHE_TREE_TOKEN)) {
226                     return CACHE_DIR.getCanonicalPath();
227                 } else if (domainToken.equals(FullBackup.MANAGED_EXTERNAL_TREE_TOKEN)) {
228                     if (EXTERNAL_DIR != null) {
229                         return EXTERNAL_DIR.getCanonicalPath();
230                     } else {
231                         return null;
232                     }
233                 } else if (domainToken.equals(FullBackup.NO_BACKUP_TREE_TOKEN)) {
234                     return NOBACKUP_DIR.getCanonicalPath();
235                 }
236                 // Not a supported location
237                 Log.i(TAG, "Unrecognized domain " + domainToken);
238                 return null;
239             } catch (IOException e) {
240                 Log.i(TAG, "Error reading directory for domain: " + domainToken);
241                 return null;
242             }
243 
244         }
245         /**
246         * A map of domain -> list of canonical file names in that domain that are to be included.
247         * We keep track of the domain so that we can go through the file system in order later on.
248         */
249         Map<String, Set<String>> mIncludes;
250         /**e
251          * List that will be populated with the canonical names of each file or directory that is
252          * to be excluded.
253          */
254         ArraySet<String> mExcludes;
255 
BackupScheme(Context context)256         BackupScheme(Context context) {
257             mFullBackupContent = context.getApplicationInfo().fullBackupContent;
258             mPackageManager = context.getPackageManager();
259             mPackageName = context.getPackageName();
260             FILES_DIR = context.getFilesDir();
261             DATABASE_DIR = context.getDatabasePath("foo").getParentFile();
262             ROOT_DIR = new File(context.getApplicationInfo().dataDir);
263             SHAREDPREF_DIR = context.getSharedPrefsFile("foo").getParentFile();
264             CACHE_DIR = context.getCacheDir();
265             NOBACKUP_DIR = context.getNoBackupFilesDir();
266             if (android.os.Process.myUid() != Process.SYSTEM_UID) {
267                 EXTERNAL_DIR = context.getExternalFilesDir(null);
268             } else {
269                 EXTERNAL_DIR = null;
270             }
271         }
272 
isFullBackupContentEnabled()273         boolean isFullBackupContentEnabled() {
274             if (mFullBackupContent < 0) {
275                 // android:fullBackupContent="false", bail.
276                 if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
277                     Log.v(FullBackup.TAG_XML_PARSER, "android:fullBackupContent - \"false\"");
278                 }
279                 return false;
280             }
281             return true;
282         }
283 
284         /**
285          * @return A mapping of domain -> canonical paths within that domain. Each of these paths
286          * specifies a file that the client has explicitly included in their backup set. If this
287          * map is empty we will back up the entire data directory (including managed external
288          * storage).
289          */
maybeParseAndGetCanonicalIncludePaths()290         public synchronized Map<String, Set<String>> maybeParseAndGetCanonicalIncludePaths()
291                 throws IOException, XmlPullParserException {
292             if (mIncludes == null) {
293                 maybeParseBackupSchemeLocked();
294             }
295             return mIncludes;
296         }
297 
298         /**
299          * @return A set of canonical paths that are to be excluded from the backup/restore set.
300          */
maybeParseAndGetCanonicalExcludePaths()301         public synchronized ArraySet<String> maybeParseAndGetCanonicalExcludePaths()
302                 throws IOException, XmlPullParserException {
303             if (mExcludes == null) {
304                 maybeParseBackupSchemeLocked();
305             }
306             return mExcludes;
307         }
308 
maybeParseBackupSchemeLocked()309         private void maybeParseBackupSchemeLocked() throws IOException, XmlPullParserException {
310             // This not being null is how we know that we've tried to parse the xml already.
311             mIncludes = new ArrayMap<String, Set<String>>();
312             mExcludes = new ArraySet<String>();
313 
314             if (mFullBackupContent == 0) {
315                 // android:fullBackupContent="true" which means that we'll do everything.
316                 if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
317                     Log.v(FullBackup.TAG_XML_PARSER, "android:fullBackupContent - \"true\"");
318                 }
319             } else {
320                 // android:fullBackupContent="@xml/some_resource".
321                 if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
322                     Log.v(FullBackup.TAG_XML_PARSER,
323                             "android:fullBackupContent - found xml resource");
324                 }
325                 XmlResourceParser parser = null;
326                 try {
327                     parser = mPackageManager
328                             .getResourcesForApplication(mPackageName)
329                             .getXml(mFullBackupContent);
330                     parseBackupSchemeFromXmlLocked(parser, mExcludes, mIncludes);
331                 } catch (PackageManager.NameNotFoundException e) {
332                     // Throw it as an IOException
333                     throw new IOException(e);
334                 } finally {
335                     if (parser != null) {
336                         parser.close();
337                     }
338                 }
339             }
340         }
341 
342         @VisibleForTesting
parseBackupSchemeFromXmlLocked(XmlPullParser parser, Set<String> excludes, Map<String, Set<String>> includes)343         public void parseBackupSchemeFromXmlLocked(XmlPullParser parser,
344                                                    Set<String> excludes,
345                                                    Map<String, Set<String>> includes)
346                 throws IOException, XmlPullParserException {
347             int event = parser.getEventType(); // START_DOCUMENT
348             while (event != XmlPullParser.START_TAG) {
349                 event = parser.next();
350             }
351 
352             if (!"full-backup-content".equals(parser.getName())) {
353                 throw new XmlPullParserException("Xml file didn't start with correct tag" +
354                         " (<full-backup-content>). Found \"" + parser.getName() + "\"");
355             }
356 
357             if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
358                 Log.v(TAG_XML_PARSER, "\n");
359                 Log.v(TAG_XML_PARSER, "====================================================");
360                 Log.v(TAG_XML_PARSER, "Found valid fullBackupContent; parsing xml resource.");
361                 Log.v(TAG_XML_PARSER, "====================================================");
362                 Log.v(TAG_XML_PARSER, "");
363             }
364 
365             while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
366                 switch (event) {
367                     case XmlPullParser.START_TAG:
368                         validateInnerTagContents(parser);
369                         final String domainFromXml = parser.getAttributeValue(null, "domain");
370                         final File domainDirectory =
371                                 getDirectoryForCriteriaDomain(domainFromXml);
372                         if (domainDirectory == null) {
373                             if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
374                                 Log.v(TAG_XML_PARSER, "...parsing \"" + parser.getName() + "\": "
375                                         + "domain=\"" + domainFromXml + "\" invalid; skipping");
376                             }
377                             break;
378                         }
379                         final File canonicalFile =
380                                 extractCanonicalFile(domainDirectory,
381                                         parser.getAttributeValue(null, "path"));
382                         if (canonicalFile == null) {
383                             break;
384                         }
385 
386                         Set<String> activeSet = parseCurrentTagForDomain(
387                                 parser, excludes, includes, domainFromXml);
388                         activeSet.add(canonicalFile.getCanonicalPath());
389                         if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
390                             Log.v(TAG_XML_PARSER, "...parsed " + canonicalFile.getCanonicalPath()
391                                     + " for domain \"" + domainFromXml + "\"");
392                         }
393 
394                         // Special case journal files (not dirs) for sqlite database. frowny-face.
395                         // Note that for a restore, the file is never a directory (b/c it doesn't
396                         // exist). We have no way of knowing a priori whether or not to expect a
397                         // dir, so we add the -journal anyway to be safe.
398                         if ("database".equals(domainFromXml) && !canonicalFile.isDirectory()) {
399                             final String canonicalJournalPath =
400                                     canonicalFile.getCanonicalPath() + "-journal";
401                             activeSet.add(canonicalJournalPath);
402                             if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
403                                 Log.v(TAG_XML_PARSER, "...automatically generated "
404                                         + canonicalJournalPath + ". Ignore if nonexistant.");
405                             }
406                         }
407                 }
408             }
409             if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
410                 Log.v(TAG_XML_PARSER, "\n");
411                 Log.v(TAG_XML_PARSER, "Xml resource parsing complete.");
412                 Log.v(TAG_XML_PARSER, "Final tally.");
413                 Log.v(TAG_XML_PARSER, "Includes:");
414                 if (includes.isEmpty()) {
415                     Log.v(TAG_XML_PARSER, "  ...nothing specified (This means the entirety of app"
416                             + " data minus excludes)");
417                 } else {
418                     for (Map.Entry<String, Set<String>> entry : includes.entrySet()) {
419                         Log.v(TAG_XML_PARSER, "  domain=" + entry.getKey());
420                         for (String includeData : entry.getValue()) {
421                             Log.v(TAG_XML_PARSER, "  " + includeData);
422                         }
423                     }
424                 }
425 
426                 Log.v(TAG_XML_PARSER, "Excludes:");
427                 if (excludes.isEmpty()) {
428                     Log.v(TAG_XML_PARSER, "  ...nothing to exclude.");
429                 } else {
430                     for (String excludeData : excludes) {
431                         Log.v(TAG_XML_PARSER, "  " + excludeData);
432                     }
433                 }
434 
435                 Log.v(TAG_XML_PARSER, "  ");
436                 Log.v(TAG_XML_PARSER, "====================================================");
437                 Log.v(TAG_XML_PARSER, "\n");
438             }
439         }
440 
parseCurrentTagForDomain(XmlPullParser parser, Set<String> excludes, Map<String, Set<String>> includes, String domain)441         private Set<String> parseCurrentTagForDomain(XmlPullParser parser,
442                                                      Set<String> excludes,
443                                                      Map<String, Set<String>> includes,
444                                                      String domain)
445                 throws XmlPullParserException {
446             if ("include".equals(parser.getName())) {
447                 final String domainToken = getTokenForXmlDomain(domain);
448                 Set<String> includeSet = includes.get(domainToken);
449                 if (includeSet == null) {
450                     includeSet = new ArraySet<String>();
451                     includes.put(domainToken, includeSet);
452                 }
453                 return includeSet;
454             } else if ("exclude".equals(parser.getName())) {
455                 return excludes;
456             } else {
457                 // Unrecognised tag => hard failure.
458                 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
459                     Log.v(TAG_XML_PARSER, "Invalid tag found in xml \""
460                             + parser.getName() + "\"; aborting operation.");
461                 }
462                 throw new XmlPullParserException("Unrecognised tag in backup" +
463                         " criteria xml (" + parser.getName() + ")");
464             }
465         }
466 
467         /**
468          * Map xml specified domain (human-readable, what clients put in their manifest's xml) to
469          * BackupAgent internal data token.
470          * @return null if the xml domain was invalid.
471          */
getTokenForXmlDomain(String xmlDomain)472         private String getTokenForXmlDomain(String xmlDomain) {
473             if ("root".equals(xmlDomain)) {
474                 return FullBackup.ROOT_TREE_TOKEN;
475             } else if ("file".equals(xmlDomain)) {
476                 return FullBackup.DATA_TREE_TOKEN;
477             } else if ("database".equals(xmlDomain)) {
478                 return FullBackup.DATABASE_TREE_TOKEN;
479             } else if ("sharedpref".equals(xmlDomain)) {
480                 return FullBackup.SHAREDPREFS_TREE_TOKEN;
481             } else if ("external".equals(xmlDomain)) {
482                 return FullBackup.MANAGED_EXTERNAL_TREE_TOKEN;
483             } else {
484                 return null;
485             }
486         }
487 
488         /**
489          *
490          * @param domain Directory where the specified file should exist. Not null.
491          * @param filePathFromXml parsed from xml. Not sanitised before calling this function so may be
492          *                        null.
493          * @return The canonical path of the file specified or null if no such file exists.
494          */
extractCanonicalFile(File domain, String filePathFromXml)495         private File extractCanonicalFile(File domain, String filePathFromXml) {
496             if (filePathFromXml == null) {
497                 // Allow things like <include domain="sharedpref"/>
498                 filePathFromXml = "";
499             }
500             if (filePathFromXml.contains("..")) {
501                 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
502                     Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml
503                             + "\", but the \"..\" path is not permitted; skipping.");
504                 }
505                 return null;
506             }
507             if (filePathFromXml.contains("//")) {
508                 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
509                     Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml
510                             + "\", which contains the invalid \"//\" sequence; skipping.");
511                 }
512                 return null;
513             }
514             return new File(domain, filePathFromXml);
515         }
516 
517         /**
518          * @param domain parsed from xml. Not sanitised before calling this function so may be null.
519          * @return The directory relevant to the domain specified.
520          */
getDirectoryForCriteriaDomain(String domain)521         private File getDirectoryForCriteriaDomain(String domain) {
522             if (TextUtils.isEmpty(domain)) {
523                 return null;
524             }
525             if ("file".equals(domain)) {
526                 return FILES_DIR;
527             } else if ("database".equals(domain)) {
528                 return DATABASE_DIR;
529             } else if ("root".equals(domain)) {
530                 return ROOT_DIR;
531             } else if ("sharedpref".equals(domain)) {
532                 return SHAREDPREF_DIR;
533             } else if ("external".equals(domain)) {
534                 return EXTERNAL_DIR;
535             } else {
536                 return null;
537             }
538         }
539 
540         /**
541          * Let's be strict about the type of xml the client can write. If we see anything untoward,
542          * throw an XmlPullParserException.
543          */
validateInnerTagContents(XmlPullParser parser)544         private void validateInnerTagContents(XmlPullParser parser)
545                 throws XmlPullParserException {
546             if (parser.getAttributeCount() > 2) {
547                 throw new XmlPullParserException("At most 2 tag attributes allowed for \""
548                         + parser.getName() + "\" tag (\"domain\" & \"path\".");
549             }
550             if (!"include".equals(parser.getName()) && !"exclude".equals(parser.getName())) {
551                 throw new XmlPullParserException("A valid tag is one of \"<include/>\" or" +
552                         " \"<exclude/>. You provided \"" + parser.getName() + "\"");
553             }
554         }
555     }
556 }
557