Support full-backup encryption and global backup password

If the user has supplied a backup password in Settings, that password
is validated during the full backup process and is used as an encryption
key for encoding the backed-up data itself.  This is the fundamental
mechanism whereby users can secure their data even against malicious
parties getting physical unlocked access to their device.

Technically the user-supplied password is not used as the encryption
key for the backed-up data itself.  What is actually done is that a
random key is generated to use as the raw encryption key.  THAT key,
in turn, is encrypted with the user-supplied password (after random
salting and key expansion with PBKDF2).  The encrypted master key
and a checksum are stored in the backup header.  At restore time,
the user supplies their password, which allows the system to decrypt
the master key, which in turn allows the decryption of the backup
data itself.

The checksum is part of the archive in order to permit validation
of the user-supplied password.  The checksum is the result of running
the user-supplied password through PBKDF2 with a randomly selected
salt.  At restore time, the proposed password is run through PBKDF2
with the salt described by the archive header.  If the result does
not match the archive's stated checksum, then the user has supplied
the wrong decryption password.

Also, suppress backup consideration for a few packages whose
data is either nonexistent or inapplicable across devices or
factory reset operations.

Bug 4901637

Change-Id: Id0cc9d0fdfc046602b129f273d48e23b7a14df36
diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java
index 436fdf8..58ea0c3 100644
--- a/core/java/android/app/backup/BackupAgent.java
+++ b/core/java/android/app/backup/BackupAgent.java
@@ -213,13 +213,13 @@
     public void onFullBackup(FullBackupDataOutput data) throws IOException {
         ApplicationInfo appInfo = getApplicationInfo();
 
-        String rootDir = new File(appInfo.dataDir).getAbsolutePath();
-        String filesDir = getFilesDir().getAbsolutePath();
-        String databaseDir = getDatabasePath("foo").getParentFile().getAbsolutePath();
-        String sharedPrefsDir = getSharedPrefsFile("foo").getParentFile().getAbsolutePath();
-        String cacheDir = getCacheDir().getAbsolutePath();
+        String rootDir = new File(appInfo.dataDir).getCanonicalPath();
+        String filesDir = getFilesDir().getCanonicalPath();
+        String databaseDir = getDatabasePath("foo").getParentFile().getCanonicalPath();
+        String sharedPrefsDir = getSharedPrefsFile("foo").getParentFile().getCanonicalPath();
+        String cacheDir = getCacheDir().getCanonicalPath();
         String libDir = (appInfo.nativeLibraryDir != null)
-                ? new File(appInfo.nativeLibraryDir).getAbsolutePath()
+                ? new File(appInfo.nativeLibraryDir).getCanonicalPath()
                 : null;
 
         // Filters, the scan queue, and the set of resulting entities
@@ -271,20 +271,27 @@
         String spDir;
         String cacheDir;
         String libDir;
+        String filePath;
 
         ApplicationInfo appInfo = getApplicationInfo();
 
-        mainDir = new File(appInfo.dataDir).getAbsolutePath();
-        filesDir = getFilesDir().getAbsolutePath();
-        dbDir = getDatabasePath("foo").getParentFile().getAbsolutePath();
-        spDir = getSharedPrefsFile("foo").getParentFile().getAbsolutePath();
-        cacheDir = getCacheDir().getAbsolutePath();
-        libDir = (appInfo.nativeLibraryDir == null) ? null
-                : new File(appInfo.nativeLibraryDir).getAbsolutePath();
+        try {
+            mainDir = new File(appInfo.dataDir).getCanonicalPath();
+            filesDir = getFilesDir().getCanonicalPath();
+            dbDir = getDatabasePath("foo").getParentFile().getCanonicalPath();
+            spDir = getSharedPrefsFile("foo").getParentFile().getCanonicalPath();
+            cacheDir = getCacheDir().getCanonicalPath();
+            libDir = (appInfo.nativeLibraryDir == null)
+                    ? null
+                    : new File(appInfo.nativeLibraryDir).getCanonicalPath();
 
-        // Now figure out which well-defined tree the file is placed in, working from
-        // most to least specific.  We also specifically exclude the lib and cache dirs.
-        String filePath = file.getAbsolutePath();
+            // Now figure out which well-defined tree the file is placed in, working from
+            // most to least specific.  We also specifically exclude the lib and cache dirs.
+            filePath = file.getCanonicalPath();
+        } catch (IOException e) {
+            Log.w(TAG, "Unable to obtain canonical paths");
+            return;
+        }
 
         if (filePath.startsWith(cacheDir) || filePath.startsWith(libDir)) {
             Log.w(TAG, "lib and cache files are not backed up");
@@ -334,15 +341,16 @@
 
             while (scanQueue.size() > 0) {
                 File file = scanQueue.remove(0);
-                String filePath = file.getAbsolutePath();
-
-                // prune this subtree?
-                if (excludes != null && excludes.contains(filePath)) {
-                    continue;
-                }
-
-                // If it's a directory, enqueue its contents for scanning.
+                String filePath;
                 try {
+                    filePath = file.getCanonicalPath();
+
+                    // prune this subtree?
+                    if (excludes != null && excludes.contains(filePath)) {
+                        continue;
+                    }
+
+                    // If it's a directory, enqueue its contents for scanning.
                     StructStat stat = Libcore.os.lstat(filePath);
                     if (OsConstants.S_ISLNK(stat.st_mode)) {
                         if (DEBUG) Log.i(TAG, "Symlink (skipping)!: " + file);
@@ -355,6 +363,9 @@
                             }
                         }
                     }
+                } catch (IOException e) {
+                    if (DEBUG) Log.w(TAG, "Error canonicalizing path of " + file);
+                    continue;
                 } catch (ErrnoException e) {
                     if (DEBUG) Log.w(TAG, "Error scanning file " + file + " : " + e);
                     continue;
@@ -415,15 +426,15 @@
 
         // Parse out the semantic domains into the correct physical location
         if (domain.equals(FullBackup.DATA_TREE_TOKEN)) {
-            basePath = getFilesDir().getAbsolutePath();
+            basePath = getFilesDir().getCanonicalPath();
         } else if (domain.equals(FullBackup.DATABASE_TREE_TOKEN)) {
-            basePath = getDatabasePath("foo").getParentFile().getAbsolutePath();
+            basePath = getDatabasePath("foo").getParentFile().getCanonicalPath();
         } else if (domain.equals(FullBackup.ROOT_TREE_TOKEN)) {
-            basePath = new File(getApplicationInfo().dataDir).getAbsolutePath();
+            basePath = new File(getApplicationInfo().dataDir).getCanonicalPath();
         } else if (domain.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) {
-            basePath = getSharedPrefsFile("foo").getParentFile().getAbsolutePath();
+            basePath = getSharedPrefsFile("foo").getParentFile().getCanonicalPath();
         } else if (domain.equals(FullBackup.CACHE_TREE_TOKEN)) {
-            basePath = getCacheDir().getAbsolutePath();
+            basePath = getCacheDir().getCanonicalPath();
         } else {
             // Not a supported location
             Log.i(TAG, "Data restored from non-app domain " + domain + ", ignoring");
diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl
index bac874e..d645ac3 100644
--- a/core/java/android/app/backup/IBackupManager.aidl
+++ b/core/java/android/app/backup/IBackupManager.aidl
@@ -111,6 +111,23 @@
     boolean isBackupEnabled();
 
     /**
+     * Set the device's backup password.  Returns {@code true} if the password was set
+     * successfully, {@code false} otherwise.  Typically a failure means that an incorrect
+     * current password was supplied.
+     *
+     * <p>Callers must hold the android.permission.BACKUP permission to use this method.
+     */
+    boolean setBackupPassword(in String currentPw, in String newPw);
+
+    /**
+     * Reports whether a backup password is currently set.  If not, then a null or empty
+     * "current password" argument should be passed to setBackupPassword().
+     *
+     * <p>Callers must hold the android.permission.BACKUP permission to use this method.
+     */
+    boolean hasBackupPassword();
+
+    /**
      * Schedule an immediate backup attempt for all pending updates.  This is
      * primarily intended for transports to use when they detect a suitable
      * opportunity for doing a backup pass.  If there are no pending updates to
@@ -161,9 +178,14 @@
      * the same time, the UI supplies a callback Binder for progress notifications during
      * the operation.
      *
+     * <p>The password passed by the confirming entity must match the saved backup or
+     * full-device encryption password in order to perform a backup.  If a password is
+     * supplied for restore, it must match the password used when creating the full
+     * backup dataset being used for restore.
+     *
      * <p>Callers must hold the android.permission.BACKUP permission to use this method.
      */
-    void acknowledgeFullBackupOrRestore(int token, boolean allow,
+    void acknowledgeFullBackupOrRestore(int token, boolean allow, in String password,
             IFullBackupRestoreObserver observer);
 
     /**
diff --git a/packages/BackupRestoreConfirmation/res/layout/confirm_backup.xml b/packages/BackupRestoreConfirmation/res/layout/confirm_backup.xml
index a4564e6..08dcfae 100644
--- a/packages/BackupRestoreConfirmation/res/layout/confirm_backup.xml
+++ b/packages/BackupRestoreConfirmation/res/layout/confirm_backup.xml
@@ -29,11 +29,25 @@
               android:layout_marginBottom="30dp"
               android:text="@string/backup_confirm_text" />
 
+    <TextView android:id="@+id/password_desc"
+              android:layout_below="@id/confirm_text"
+              android:layout_width="wrap_content"
+              android:layout_height="wrap_content"
+              android:layout_marginBottom="10dp"
+              android:text="@string/backup_password_text" />
+
+    <EditText android:id="@+id/password"
+              android:layout_below="@id/password_desc"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:layout_marginBottom="30dp"
+              android:password="true" />
+
     <TextView android:id="@+id/package_name"
               android:layout_width="match_parent"
               android:layout_height="20dp"
               android:layout_marginLeft="30dp"
-              android:layout_below="@id/confirm_text"
+              android:layout_below="@id/password"
               android:layout_marginBottom="30dp" />
 
     <Button android:id="@+id/button_allow"
diff --git a/packages/BackupRestoreConfirmation/res/layout/confirm_restore.xml b/packages/BackupRestoreConfirmation/res/layout/confirm_restore.xml
index ca99ae1..8b12ed4 100644
--- a/packages/BackupRestoreConfirmation/res/layout/confirm_restore.xml
+++ b/packages/BackupRestoreConfirmation/res/layout/confirm_restore.xml
@@ -29,11 +29,25 @@
               android:layout_marginBottom="30dp"
               android:text="@string/restore_confirm_text" />
 
+    <TextView android:id="@+id/password_desc"
+              android:layout_below="@id/confirm_text"
+              android:layout_width="wrap_content"
+              android:layout_height="wrap_content"
+              android:layout_marginBottom="10dp"
+              android:text="@string/restore_password_text" />
+
+    <EditText android:id="@+id/password"
+              android:layout_below="@id/password_desc"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:layout_marginBottom="30dp"
+              android:password="true" />
+
     <TextView android:id="@+id/package_name"
               android:layout_width="match_parent"
               android:layout_height="20dp"
               android:layout_marginLeft="30dp"
-              android:layout_below="@id/confirm_text"
+              android:layout_below="@id/password"
               android:layout_marginBottom="30dp" />
 
     <Button android:id="@+id/button_allow"
diff --git a/packages/BackupRestoreConfirmation/res/values/strings.xml b/packages/BackupRestoreConfirmation/res/values/strings.xml
index 3d85e86..48a8df6 100644
--- a/packages/BackupRestoreConfirmation/res/values/strings.xml
+++ b/packages/BackupRestoreConfirmation/res/values/strings.xml
@@ -29,4 +29,11 @@
     <!-- Button to refuse to allow the requested full restore -->
     <string name="deny_restore_button_label">Do not restore</string>
 
+    <!-- Text for message to user that they must enter their predefined backup password in order to perform this operation. -->
+    <string name="backup_password_text">Please enter your predefined backup password below. The full backup will also be encrypted using this password:</string>
+    <!-- Text for message to user that they may optionally supply an encryption password to use for a full backup operation. -->
+    <string name="backup_password_optional">If you wish to encrypt the full backup data, enter a password below:</string>
+
+    <!-- Text for message to user when performing a full restore operation, explaining that they must enter the password originally used to encrypt the full backup data. -->
+    <string name="restore_password_text">If the backup data is encrypted, please enter the password below:</string>
 </resources>
diff --git a/packages/BackupRestoreConfirmation/src/com/android/backupconfirm/BackupRestoreConfirmation.java b/packages/BackupRestoreConfirmation/src/com/android/backupconfirm/BackupRestoreConfirmation.java
index ed413e6..fad58b9 100644
--- a/packages/BackupRestoreConfirmation/src/com/android/backupconfirm/BackupRestoreConfirmation.java
+++ b/packages/BackupRestoreConfirmation/src/com/android/backupconfirm/BackupRestoreConfirmation.java
@@ -126,7 +126,7 @@
         final Intent intent = getIntent();
         final String action = intent.getAction();
 
-        int layoutId;
+        final int layoutId;
         if (action.equals(FullBackup.FULL_BACKUP_INTENT_ACTION)) {
             layoutId = R.layout.confirm_backup;
         } else if (action.equals(FullBackup.FULL_RESTORE_INTENT_ACTION)) {
@@ -156,6 +156,20 @@
         mAllowButton = (Button) findViewById(R.id.button_allow);
         mDenyButton = (Button) findViewById(R.id.button_deny);
 
+        // For full backup, we vary the password prompt text depending on whether one is predefined
+        if (layoutId == R.layout.confirm_backup) {
+            TextView pwDesc = (TextView) findViewById(R.id.password_desc); 
+            try {
+                if (mBackupManager.hasBackupPassword()) {
+                    pwDesc.setText(R.string.backup_password_text);
+                } else {
+                    pwDesc.setText(R.string.backup_password_optional);
+                }
+            } catch (RemoteException e) {
+                // TODO: bail gracefully
+            }
+        }
+
         mAllowButton.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
@@ -188,8 +202,11 @@
     void sendAcknowledgement(int token, boolean allow, IFullBackupRestoreObserver observer) {
         if (!mDidAcknowledge) {
             mDidAcknowledge = true;
+
             try {
-                mBackupManager.acknowledgeFullBackupOrRestore(mToken, true, mObserver);
+                TextView pwView = (TextView) findViewById(R.id.password);
+                mBackupManager.acknowledgeFullBackupOrRestore(mToken, allow,
+                        String.valueOf(pwView.getText()), mObserver);
             } catch (RemoteException e) {
                 // TODO: bail gracefully if we can't contact the backup manager
             }
diff --git a/packages/DefaultContainerService/AndroidManifest.xml b/packages/DefaultContainerService/AndroidManifest.xml
index b0597c4e..0f47482 100755
--- a/packages/DefaultContainerService/AndroidManifest.xml
+++ b/packages/DefaultContainerService/AndroidManifest.xml
@@ -9,7 +9,8 @@
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.ACCESS_CACHE_FILESYSTEM" />
 
-    <application android:label="@string/service_name">
+    <application android:label="@string/service_name"
+                 android:allowBackup="false">
 
         <service android:name=".DefaultContainerService"
                  android:enabled="true"
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
index 3a7a6e1..4f39e69 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
@@ -201,16 +201,22 @@
             BufferedOutputStream bufstream = new BufferedOutputStream(filestream);
             DataOutputStream out = new DataOutputStream(bufstream);
 
+            if (DEBUG_BACKUP) Log.d(TAG, "Writing flattened data version " + FULL_BACKUP_VERSION);
             out.writeInt(FULL_BACKUP_VERSION);
 
+            if (DEBUG_BACKUP) Log.d(TAG, systemSettingsData.length + " bytes of settings data");
             out.writeInt(systemSettingsData.length);
             out.write(systemSettingsData);
+            if (DEBUG_BACKUP) Log.d(TAG, secureSettingsData.length + " bytes of secure settings data");
             out.writeInt(secureSettingsData.length);
             out.write(secureSettingsData);
+            if (DEBUG_BACKUP) Log.d(TAG, locale.length + " bytes of locale data");
             out.writeInt(locale.length);
             out.write(locale);
+            if (DEBUG_BACKUP) Log.d(TAG, wifiSupplicantData.length + " bytes of wifi supplicant data");
             out.writeInt(wifiSupplicantData.length);
             out.write(wifiSupplicantData);
+            if (DEBUG_BACKUP) Log.d(TAG, wifiConfigData.length + " bytes of wifi config data");
             out.writeInt(wifiConfigData.length);
             out.write(wifiConfigData);
 
@@ -241,28 +247,28 @@
             int nBytes = in.readInt();
             if (DEBUG_BACKUP) Log.d(TAG, nBytes + " bytes of settings data");
             byte[] buffer = new byte[nBytes];
-            in.read(buffer, 0, nBytes);
+            in.readFully(buffer, 0, nBytes);
             restoreSettings(buffer, nBytes, Settings.System.CONTENT_URI);
 
             // secure settings
             nBytes = in.readInt();
             if (DEBUG_BACKUP) Log.d(TAG, nBytes + " bytes of secure settings data");
             if (nBytes > buffer.length) buffer = new byte[nBytes];
-            in.read(buffer, 0, nBytes);
+            in.readFully(buffer, 0, nBytes);
             restoreSettings(buffer, nBytes, Settings.Secure.CONTENT_URI);
 
             // locale
             nBytes = in.readInt();
             if (DEBUG_BACKUP) Log.d(TAG, nBytes + " bytes of locale data");
             if (nBytes > buffer.length) buffer = new byte[nBytes];
-            in.read(buffer, 0, nBytes);
+            in.readFully(buffer, 0, nBytes);
             mSettingsHelper.setLocaleData(buffer, nBytes);
 
             // wifi supplicant
             nBytes = in.readInt();
             if (DEBUG_BACKUP) Log.d(TAG, nBytes + " bytes of wifi supplicant data");
             if (nBytes > buffer.length) buffer = new byte[nBytes];
-            in.read(buffer, 0, nBytes);
+            in.readFully(buffer, 0, nBytes);
             int retainedWifiState = enableWifi(false);
             restoreWifiSupplicant(FILE_WIFI_SUPPLICANT, buffer, nBytes);
             FileUtils.setPermissions(FILE_WIFI_SUPPLICANT,
@@ -277,7 +283,7 @@
             nBytes = in.readInt();
             if (DEBUG_BACKUP) Log.d(TAG, nBytes + " bytes of wifi config data");
             if (nBytes > buffer.length) buffer = new byte[nBytes];
-            in.read(buffer, 0, nBytes);
+            in.readFully(buffer, 0, nBytes);
             restoreFileData(mWifiConfigFile, buffer, nBytes);
 
             if (DEBUG_BACKUP) Log.d(TAG, "Full restore complete.");
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index ba9b5b0..cf0bdd3 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -13,6 +13,7 @@
     <application
         android:persistent="true"
         android:allowClearUserData="false"
+        android:allowBackup="false"
         android:hardwareAccelerated="true"
         android:label="@string/app_label"
         android:icon="@drawable/ic_launcher_settings">
diff --git a/packages/VpnDialogs/AndroidManifest.xml b/packages/VpnDialogs/AndroidManifest.xml
index 8554b77..bd5f739 100644
--- a/packages/VpnDialogs/AndroidManifest.xml
+++ b/packages/VpnDialogs/AndroidManifest.xml
@@ -2,7 +2,8 @@
         package="com.android.vpndialogs"
         android:sharedUserId="android.uid.system">
 
-    <application android:label="VpnDialogs">
+    <application android:label="VpnDialogs"
+            android:allowBackup="false" >
         <activity android:name=".ConfirmDialog"
                 android:permission="android.permission.VPN"
                 android:theme="@style/transparent">
diff --git a/services/java/com/android/server/BackupManagerService.java b/services/java/com/android/server/BackupManagerService.java
index e9e66cb..3ec2a96 100644
--- a/services/java/com/android/server/BackupManagerService.java
+++ b/services/java/com/android/server/BackupManagerService.java
@@ -76,7 +76,11 @@
 import com.android.internal.backup.LocalTransport;
 import com.android.server.PackageManagerBackupAgent.Metadata;
 
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.DataInputStream;
+import java.io.DataOutputStream;
 import java.io.EOFException;
 import java.io.File;
 import java.io.FileDescriptor;
@@ -85,9 +89,16 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.io.RandomAccessFile;
-import java.io.UnsupportedEncodingException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -101,9 +112,20 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.zip.Deflater;
 import java.util.zip.DeflaterOutputStream;
-import java.util.zip.Inflater;
 import java.util.zip.InflaterInputStream;
 
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
 class BackupManagerService extends IBackupManager.Stub {
     private static final String TAG = "BackupManagerService";
     private static final boolean DEBUG = true;
@@ -113,6 +135,7 @@
     static final int BACKUP_MANIFEST_VERSION = 1;
     static final String BACKUP_FILE_HEADER_MAGIC = "ANDROID BACKUP\n";
     static final int BACKUP_FILE_VERSION = 1;
+    static final boolean COMPRESS_FULL_BACKUPS = true; // should be true in production
 
     // How often we perform a backup pass.  Privileged external callers can
     // trigger an immediate pass.
@@ -148,8 +171,9 @@
     static final long TIMEOUT_SHARED_BACKUP_INTERVAL = 30 * 60 * 1000;
     static final long TIMEOUT_RESTORE_INTERVAL = 60 * 1000;
 
-    // User confirmation timeout for a full backup/restore operation
-    static final long TIMEOUT_FULL_CONFIRMATION = 30 * 1000;
+    // User confirmation timeout for a full backup/restore operation.  It's this long in
+    // order to give them time to enter the backup password.
+    static final long TIMEOUT_FULL_CONFIRMATION = 60 * 1000;
 
     private Context mContext;
     private PackageManager mPackageManager;
@@ -283,6 +307,7 @@
         public ParcelFileDescriptor fd;
         public final AtomicBoolean latch;
         public IFullBackupRestoreObserver observer;
+        public String password;     // filled in by the confirmation step
 
         FullParams() {
             latch = new AtomicBoolean(false);
@@ -329,6 +354,23 @@
     File mJournalDir;
     File mJournal;
 
+    // Backup password, if any, and the file where it's saved.  What is stored is not the
+    // password text itself; it's the result of a PBKDF2 hash with a randomly chosen (but
+    // persisted) salt.  Validation is performed by running the challenge text through the
+    // same PBKDF2 cycle with the persisted salt; if the resulting derived key string matches
+    // the saved hash string, then the challenge text matches the originally supplied
+    // password text.
+    private final SecureRandom mRng = new SecureRandom();
+    private String mPasswordHash;
+    private File mPasswordHashFile;
+    private byte[] mPasswordSalt;
+
+    // Configuration of PBKDF2 that we use for generating pw hashes and intermediate keys
+    static final int PBKDF2_HASH_ROUNDS = 10000;
+    static final int PBKDF2_KEY_SIZE = 256;     // bits
+    static final int PBKDF2_SALT_SIZE = 512;    // bits
+    static final String ENCRYPTION_ALGORITHM_NAME = "AES-256";
+
     // Keep a log of all the apps we've ever backed up, and what the
     // dataset tokens are for both the current backup dataset and
     // the ancestral dataset.
@@ -416,7 +458,7 @@
             {
                 FullBackupParams params = (FullBackupParams)msg.obj;
                 (new PerformFullBackupTask(params.fd, params.observer, params.includeApks,
-                        params.includeShared, params.allApps, params.packages,
+                        params.includeShared, params.password, params.allApps, params.packages,
                         params.latch)).run();
                 break;
             }
@@ -434,7 +476,8 @@
             case MSG_RUN_FULL_RESTORE:
             {
                 FullRestoreParams params = (FullRestoreParams)msg.obj;
-                (new PerformFullRestoreTask(params.fd, params.observer, params.latch)).run();
+                (new PerformFullRestoreTask(params.fd, params.password,
+                        params.observer, params.latch)).run();
                 break;
             }
 
@@ -584,6 +627,32 @@
         mBaseStateDir.mkdirs();
         mDataDir = Environment.getDownloadCacheDirectory();
 
+        mPasswordHashFile = new File(mBaseStateDir, "pwhash");
+        if (mPasswordHashFile.exists()) {
+            FileInputStream fin = null;
+            DataInputStream in = null;
+            try {
+                fin = new FileInputStream(mPasswordHashFile);
+                in = new DataInputStream(new BufferedInputStream(fin));
+                // integer length of the salt array, followed by the salt,
+                // then the hex pw hash string
+                int saltLen = in.readInt();
+                byte[] salt = new byte[saltLen];
+                in.readFully(salt);
+                mPasswordHash = in.readUTF();
+                mPasswordSalt = salt;
+            } catch (IOException e) {
+                Slog.e(TAG, "Unable to read saved backup pw hash");
+            } finally {
+                try {
+                    if (in != null) in.close();
+                    if (fin != null) fin.close();
+                } catch (IOException e) {
+                    Slog.w(TAG, "Unable to close streams");
+                }
+            }
+        }
+
         // Alarm receivers for scheduled backups & initialization operations
         mRunBackupReceiver = new RunBackupReceiver();
         IntentFilter filter = new IntentFilter();
@@ -843,6 +912,151 @@
         }
     }
 
+    private SecretKey buildPasswordKey(String pw, byte[] salt, int rounds) {
+        return buildCharArrayKey(pw.toCharArray(), salt, rounds);
+    }
+
+    private SecretKey buildCharArrayKey(char[] pwArray, byte[] salt, int rounds) {
+        try {
+            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
+            KeySpec ks = new PBEKeySpec(pwArray, salt, rounds, PBKDF2_KEY_SIZE);
+            return keyFactory.generateSecret(ks);
+        } catch (InvalidKeySpecException e) {
+            Slog.e(TAG, "Invalid key spec for PBKDF2!");
+        } catch (NoSuchAlgorithmException e) {
+            Slog.e(TAG, "PBKDF2 unavailable!");
+        }
+        return null;
+    }
+
+    private String buildPasswordHash(String pw, byte[] salt, int rounds) {
+        SecretKey key = buildPasswordKey(pw, salt, rounds);
+        if (key != null) {
+            return byteArrayToHex(key.getEncoded());
+        }
+        return null;
+    }
+
+    private String byteArrayToHex(byte[] data) {
+        StringBuilder buf = new StringBuilder(data.length * 2);
+        for (int i = 0; i < data.length; i++) {
+            buf.append(Byte.toHexString(data[i], true));
+        }
+        return buf.toString();
+    }
+
+    private byte[] hexToByteArray(String digits) {
+        final int bytes = digits.length() / 2;
+        if (2*bytes != digits.length()) {
+            throw new IllegalArgumentException("Hex string must have an even number of digits");
+        }
+
+        byte[] result = new byte[bytes];
+        for (int i = 0; i < digits.length(); i += 2) {
+            result[i/2] = (byte) Integer.parseInt(digits.substring(i, i+2), 16);
+        }
+        return result;
+    }
+
+    private byte[] makeKeyChecksum(byte[] pwBytes, byte[] salt, int rounds) {
+        char[] mkAsChar = new char[pwBytes.length];
+        for (int i = 0; i < pwBytes.length; i++) {
+            mkAsChar[i] = (char) pwBytes[i];
+        }
+
+        Key checksum = buildCharArrayKey(mkAsChar, salt, rounds);
+        return checksum.getEncoded();
+    }
+
+    // Used for generating random salts or passwords
+    private byte[] randomBytes(int bits) {
+        byte[] array = new byte[bits / 8];
+        mRng.nextBytes(array);
+        return array;
+    }
+
+    // Backup password management
+    boolean passwordMatchesSaved(String candidatePw, int rounds) {
+        if (mPasswordHash == null) {
+            // no current password case -- require that 'currentPw' be null or empty
+            if (candidatePw == null || "".equals(candidatePw)) {
+                return true;
+            } // else the non-empty candidate does not match the empty stored pw
+        } else {
+            // hash the stated current pw and compare to the stored one
+            if (candidatePw != null && candidatePw.length() > 0) {
+                String currentPwHash = buildPasswordHash(candidatePw, mPasswordSalt, rounds);
+                if (mPasswordHash.equalsIgnoreCase(currentPwHash)) {
+                    // candidate hash matches the stored hash -- the password matches
+                    return true;
+                }
+            } // else the stored pw is nonempty but the candidate is empty; no match
+        }
+        return false;
+    }
+
+    @Override
+    public boolean setBackupPassword(String currentPw, String newPw) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "setBackupPassword");
+
+        // If the supplied pw doesn't hash to the the saved one, fail
+        if (!passwordMatchesSaved(currentPw, PBKDF2_HASH_ROUNDS)) {
+            return false;
+        }
+
+        // Clearing the password is okay
+        if (newPw == null || newPw.isEmpty()) {
+            if (mPasswordHashFile.exists()) {
+                if (!mPasswordHashFile.delete()) {
+                    // Unable to delete the old pw file, so fail
+                    Slog.e(TAG, "Unable to clear backup password");
+                    return false;
+                }
+            }
+            mPasswordHash = null;
+            mPasswordSalt = null;
+            return true;
+        }
+
+        try {
+            // Okay, build the hash of the new backup password
+            byte[] salt = randomBytes(PBKDF2_SALT_SIZE);
+            String newPwHash = buildPasswordHash(newPw, salt, PBKDF2_HASH_ROUNDS);
+
+            OutputStream pwf = null, buffer = null;
+            DataOutputStream out = null;
+            try {
+                pwf = new FileOutputStream(mPasswordHashFile);
+                buffer = new BufferedOutputStream(pwf);
+                out = new DataOutputStream(buffer);
+                // integer length of the salt array, followed by the salt,
+                // then the hex pw hash string
+                out.writeInt(salt.length);
+                out.write(salt);
+                out.writeUTF(newPwHash);
+                out.flush();
+                mPasswordHash = newPwHash;
+                mPasswordSalt = salt;
+                return true;
+            } finally {
+                if (out != null) out.close();
+                if (buffer != null) buffer.close();
+                if (pwf != null) pwf.close();
+            }
+        } catch (IOException e) {
+            Slog.e(TAG, "Unable to set backup password");
+        }
+        return false;
+    }
+
+    @Override
+    public boolean hasBackupPassword() {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "hasBackupPassword");
+        return (mPasswordHash != null && mPasswordHash.length() > 0);
+    }
+
     // Maintain persistent state around whether need to do an initialize operation.
     // Must be called with the queue lock held.
     void recordInitPendingLocked(boolean isPending, String transportName) {
@@ -1694,6 +1908,7 @@
         boolean mIncludeShared;
         boolean mAllApps;
         String[] mPackages;
+        String mUserPassword;
         AtomicBoolean mLatchObject;
         File mFilesDir;
         File mManifestFile;
@@ -1748,7 +1963,7 @@
         }
 
         PerformFullBackupTask(ParcelFileDescriptor fd, IFullBackupRestoreObserver observer, 
-                boolean includeApks, boolean includeShared,
+                boolean includeApks, boolean includeShared, String password,
                 boolean doAllApps, String[] packages, AtomicBoolean latch) {
             mOutputFile = fd;
             mObserver = observer;
@@ -1756,6 +1971,7 @@
             mIncludeShared = includeShared;
             mAllApps = doAllApps;
             mPackages = packages;
+            mUserPassword = password;
             mLatchObject = latch;
 
             mFilesDir = new File("/data/system");
@@ -1796,16 +2012,13 @@
             }
 
             FileOutputStream ofstream = new FileOutputStream(mOutputFile.getFileDescriptor());
-
-            // Set up the compression stage
-            Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
-            DeflaterOutputStream out = new DeflaterOutputStream(ofstream, deflater, true);
+            OutputStream out = null;
 
             PackageInfo pkg = null;
             try {
-
-                // !!! TODO: if using encryption, set up the encryption stage
-                // and emit the tar header stating the password salt.
+                boolean encrypting = (mUserPassword != null && mUserPassword.length() > 0);
+                boolean compressing = COMPRESS_FULL_BACKUPS;
+                OutputStream finalOutput = ofstream;
 
                 // Write the global file header.  All strings are UTF-8 encoded; lines end
                 // with a '\n' byte.  Actual backup data begins immediately following the
@@ -1814,17 +2027,57 @@
                 // line 1: "ANDROID BACKUP"
                 // line 2: backup file format version, currently "1"
                 // line 3: compressed?  "0" if not compressed, "1" if compressed.
-                // line 4: encryption salt?  "-" if not encrypted, otherwise this
-                //         line contains the encryption salt with which the user-
-                //         supplied password is to be expanded, in hexadecimal.
-                StringBuffer headerbuf = new StringBuffer(256);
-                // !!! TODO: programmatically build the compressed / encryption salt fields
+                // line 4: name of encryption algorithm [currently only "none" or "AES-256"]
+                //
+                // When line 4 is not "none", then additional header data follows:
+                //
+                // line 5: user password salt [hex]
+                // line 6: master key checksum salt [hex]
+                // line 7: number of PBKDF2 rounds to use (same for user & master) [decimal]
+                // line 8: IV of the user key [hex]
+                // line 9: master key blob [hex]
+                //     IV of the master key, master key itself, master key checksum hash
+                //
+                // The master key checksum is the master key plus its checksum salt, run through
+                // 10k rounds of PBKDF2.  This is used to verify that the user has supplied the
+                // correct password for decrypting the archive:  the master key decrypted from
+                // the archive using the user-supplied password is also run through PBKDF2 in
+                // this way, and if the result does not match the checksum as stored in the
+                // archive, then we know that the user-supplied password does not match the
+                // archive's.
+                StringBuilder headerbuf = new StringBuilder(1024);
+
                 headerbuf.append(BACKUP_FILE_HEADER_MAGIC);
-                headerbuf.append("1\n1\n-\n");
+                headerbuf.append(BACKUP_FILE_VERSION); // integer, no trailing \n
+                headerbuf.append(compressing ? "\n1\n" : "\n0\n");
 
                 try {
+                    // Set up the encryption stage if appropriate, and emit the correct header
+                    if (encrypting) {
+                        // Verify that the given password matches the currently-active
+                        // backup password, if any
+                        if (hasBackupPassword()) {
+                            if (!passwordMatchesSaved(mUserPassword, PBKDF2_HASH_ROUNDS)) {
+                                if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting");
+                                return;
+                            }
+                        }
+
+                        finalOutput = emitAesBackupHeader(headerbuf, finalOutput);
+                    } else {
+                        headerbuf.append("none\n");
+                    }
+
                     byte[] header = headerbuf.toString().getBytes("UTF-8");
                     ofstream.write(header);
+
+                    // Set up the compression stage feeding into the encryption stage (if any)
+                    if (compressing) {
+                        Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
+                        finalOutput = new DeflaterOutputStream(finalOutput, deflater, true);
+                    }
+
+                    out = finalOutput;
                 } catch (Exception e) {
                     // Should never happen!
                     Slog.e(TAG, "Unable to emit archive header", e);
@@ -1847,7 +2100,7 @@
             } finally {
                 tearDown(pkg);
                 try {
-                    out.close();
+                    if (out != null) out.close();
                     mOutputFile.close();
                 } catch (IOException e) {
                     /* nothing we can do about this */
@@ -1865,7 +2118,78 @@
             }
         }
 
-        private void backupOnePackage(PackageInfo pkg, DeflaterOutputStream out)
+        private OutputStream emitAesBackupHeader(StringBuilder headerbuf,
+                OutputStream ofstream) throws Exception {
+            // User key will be used to encrypt the master key.
+            byte[] newUserSalt = randomBytes(PBKDF2_SALT_SIZE);
+            SecretKey userKey = buildPasswordKey(mUserPassword, newUserSalt,
+                    PBKDF2_HASH_ROUNDS);
+
+            // the master key is random for each backup
+            byte[] masterPw = new byte[256 / 8];
+            mRng.nextBytes(masterPw);
+            byte[] checksumSalt = randomBytes(PBKDF2_SALT_SIZE);
+
+            // primary encryption of the datastream with the random key
+            Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
+            SecretKeySpec masterKeySpec = new SecretKeySpec(masterPw, "AES");
+            c.init(Cipher.ENCRYPT_MODE, masterKeySpec);
+            OutputStream finalOutput = new CipherOutputStream(ofstream, c);
+
+            // line 4: name of encryption algorithm
+            headerbuf.append(ENCRYPTION_ALGORITHM_NAME);
+            headerbuf.append('\n');
+            // line 5: user password salt [hex]
+            headerbuf.append(byteArrayToHex(newUserSalt));
+            headerbuf.append('\n');
+            // line 6: master key checksum salt [hex]
+            headerbuf.append(byteArrayToHex(checksumSalt));
+            headerbuf.append('\n');
+            // line 7: number of PBKDF2 rounds used [decimal]
+            headerbuf.append(PBKDF2_HASH_ROUNDS);
+            headerbuf.append('\n');
+
+            // line 8: IV of the user key [hex]
+            Cipher mkC = Cipher.getInstance("AES/CBC/PKCS5Padding");
+            mkC.init(Cipher.ENCRYPT_MODE, userKey);
+
+            byte[] IV = mkC.getIV();
+            headerbuf.append(byteArrayToHex(IV));
+            headerbuf.append('\n');
+
+            // line 9: master IV + key blob, encrypted by the user key [hex].  Blob format:
+            //    [byte] IV length = Niv
+            //    [array of Niv bytes] IV itself
+            //    [byte] master key length = Nmk
+            //    [array of Nmk bytes] master key itself
+            //    [byte] MK checksum hash length = Nck
+            //    [array of Nck bytes] master key checksum hash
+            //
+            // The checksum is the (master key + checksum salt), run through the
+            // stated number of PBKDF2 rounds
+            IV = c.getIV();
+            byte[] mk = masterKeySpec.getEncoded();
+            byte[] checksum = makeKeyChecksum(masterKeySpec.getEncoded(),
+                    checksumSalt, PBKDF2_HASH_ROUNDS);
+
+            ByteArrayOutputStream blob = new ByteArrayOutputStream(IV.length + mk.length
+                    + checksum.length + 3);
+            DataOutputStream mkOut = new DataOutputStream(blob);
+            mkOut.writeByte(IV.length);
+            mkOut.write(IV);
+            mkOut.writeByte(mk.length);
+            mkOut.write(mk);
+            mkOut.writeByte(checksum.length);
+            mkOut.write(checksum);
+            mkOut.flush();
+            byte[] encryptedMk = mkC.doFinal(blob.toByteArray());
+            headerbuf.append(byteArrayToHex(encryptedMk));
+            headerbuf.append('\n');
+
+            return finalOutput;
+        }
+
+        private void backupOnePackage(PackageInfo pkg, OutputStream out)
                 throws RemoteException {
             Slog.d(TAG, "Binding to full backup agent : " + pkg.packageName);
 
@@ -1922,13 +2246,12 @@
                     Slog.e(TAG, "Error backing up " + pkg.packageName, e);
                 } finally {
                     try {
+                        // flush after every package
+                        out.flush();
                         if (pipes != null) {
                             if (pipes[0] != null) pipes[0].close();
                             if (pipes[1] != null) pipes[1].close();
                         }
-
-                        // Apply a full sync/flush after each application's data
-                        out.flush();
                     } catch (IOException e) {
                         Slog.w(TAG, "Error bringing down backup stack");
                     }
@@ -2037,7 +2360,8 @@
                         mActivityManager.unbindBackupAgent(app);
 
                         // The agent was running with a stub Application object, so shut it down.
-                        if (app.uid != Process.SYSTEM_UID) {
+                        if (app.uid != Process.SYSTEM_UID
+                                && app.uid != Process.PHONE_UID) {
                             if (DEBUG) Slog.d(TAG, "Backup complete, killing host process");
                             mActivityManager.killApplicationProcess(app.processName, app.uid);
                         } else {
@@ -2121,6 +2445,7 @@
 
     class PerformFullRestoreTask implements Runnable {
         ParcelFileDescriptor mInputFile;
+        String mUserPassword;
         IFullBackupRestoreObserver mObserver;
         AtomicBoolean mLatchObject;
         IBackupAgent mAgent;
@@ -2144,9 +2469,10 @@
         // Packages we've already wiped data on when restoring their first file
         final HashSet<String> mClearedPackages = new HashSet<String>();
 
-        PerformFullRestoreTask(ParcelFileDescriptor fd, IFullBackupRestoreObserver observer,
-                AtomicBoolean latch) {
+        PerformFullRestoreTask(ParcelFileDescriptor fd, String password,
+                IFullBackupRestoreObserver observer, AtomicBoolean latch) {
             mInputFile = fd;
+            mUserPassword = password;
             mObserver = observer;
             mLatchObject = latch;
             mAgent = null;
@@ -2202,50 +2528,51 @@
                 mPackagePolicies.put("com.android.sharedstoragebackup", RestorePolicy.ACCEPT);
             }
 
+            FileInputStream rawInStream = null;
+            DataInputStream rawDataIn = null;
             try {
                 mBytes = 0;
                 byte[] buffer = new byte[32 * 1024];
-                FileInputStream rawInStream = new FileInputStream(mInputFile.getFileDescriptor());
+                rawInStream = new FileInputStream(mInputFile.getFileDescriptor());
+                rawDataIn = new DataInputStream(rawInStream);
 
                 // First, parse out the unencrypted/uncompressed header
                 boolean compressed = false;
-                boolean encrypted = false;
+                InputStream preCompressStream = rawInStream;
                 final InputStream in;
 
                 boolean okay = false;
                 final int headerLen = BACKUP_FILE_HEADER_MAGIC.length();
                 byte[] streamHeader = new byte[headerLen];
-                try {
-                    int got;
-                    if ((got = rawInStream.read(streamHeader, 0, headerLen)) == headerLen) {
-                        byte[] magicBytes = BACKUP_FILE_HEADER_MAGIC.getBytes("UTF-8");
-                        if (Arrays.equals(magicBytes, streamHeader)) {
-                            // okay, header looks good.  now parse out the rest of the fields.
-                            String s = readHeaderLine(rawInStream);
-                            if (Integer.parseInt(s) == BACKUP_FILE_VERSION) {
-                                // okay, it's a version we recognize
-                                s = readHeaderLine(rawInStream);
-                                compressed = (Integer.parseInt(s) != 0);
-                                s = readHeaderLine(rawInStream);
-                                if (!s.startsWith("-")) {
-                                    encrypted = true;
-                                    // TODO: parse out the salt here and process with the user pw
-                                }
+                rawDataIn.readFully(streamHeader);
+                byte[] magicBytes = BACKUP_FILE_HEADER_MAGIC.getBytes("UTF-8");
+                if (Arrays.equals(magicBytes, streamHeader)) {
+                    // okay, header looks good.  now parse out the rest of the fields.
+                    String s = readHeaderLine(rawInStream);
+                    if (Integer.parseInt(s) == BACKUP_FILE_VERSION) {
+                        // okay, it's a version we recognize
+                        s = readHeaderLine(rawInStream);
+                        compressed = (Integer.parseInt(s) != 0);
+                        s = readHeaderLine(rawInStream);
+                        if (s.equals("none")) {
+                            // no more header to parse; we're good to go
+                            okay = true;
+                        } else if (mUserPassword != null && mUserPassword.length() > 0) {
+                            preCompressStream = decodeAesHeaderAndInitialize(s, rawInStream);
+                            if (preCompressStream != null) {
                                 okay = true;
-                            } else Slog.e(TAG, "Wrong header version: " + s);
-                        } else Slog.e(TAG, "Didn't read the right header magic");
-                    } else Slog.e(TAG, "Only read " + got + " bytes of header");
-                } catch (NumberFormatException e) {
-                    Slog.e(TAG, "Can't parse restore data header");
-                }
+                            }
+                        } else Slog.w(TAG, "Archive is encrypted but no password given");
+                    } else Slog.w(TAG, "Wrong header version: " + s);
+                } else Slog.w(TAG, "Didn't read the right header magic");
 
                 if (!okay) {
-                    Slog.e(TAG, "Invalid restore data; aborting.");
+                    Slog.w(TAG, "Invalid restore data; aborting.");
                     return;
                 }
 
                 // okay, use the right stream layer based on compression
-                in = (compressed) ? new InflaterInputStream(rawInStream) : rawInStream;
+                in = (compressed) ? new InflaterInputStream(preCompressStream) : preCompressStream;
 
                 boolean didRestore;
                 do {
@@ -2260,6 +2587,8 @@
                 tearDownAgent(mTargetApp);
 
                 try {
+                    if (rawDataIn != null) rawDataIn.close();
+                    if (rawInStream != null) rawInStream.close();
                     mInputFile.close();
                 } catch (IOException e) {
                     Slog.w(TAG, "Close of restore data pipe threw", e);
@@ -2280,7 +2609,7 @@
 
         String readHeaderLine(InputStream in) throws IOException {
             int c;
-            StringBuffer buffer = new StringBuffer(80);
+            StringBuilder buffer = new StringBuilder(80);
             while ((c = in.read()) >= 0) {
                 if (c == '\n') break;   // consume and discard the newlines
                 buffer.append((char)c);
@@ -2288,6 +2617,85 @@
             return buffer.toString();
         }
 
+        InputStream decodeAesHeaderAndInitialize(String encryptionName, InputStream rawInStream) {
+            InputStream result = null;
+            try {
+                if (encryptionName.equals(ENCRYPTION_ALGORITHM_NAME)) {
+
+                    String userSaltHex = readHeaderLine(rawInStream); // 5
+                    byte[] userSalt = hexToByteArray(userSaltHex);
+
+                    String ckSaltHex = readHeaderLine(rawInStream); // 6
+                    byte[] ckSalt = hexToByteArray(ckSaltHex);
+
+                    int rounds = Integer.parseInt(readHeaderLine(rawInStream)); // 7
+                    String userIvHex = readHeaderLine(rawInStream); // 8
+
+                    String masterKeyBlobHex = readHeaderLine(rawInStream); // 9
+
+                    // decrypt the master key blob
+                    Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
+                    SecretKey userKey = buildPasswordKey(mUserPassword, userSalt,
+                            rounds);
+                    byte[] IV = hexToByteArray(userIvHex);
+                    IvParameterSpec ivSpec = new IvParameterSpec(IV);
+                    c.init(Cipher.DECRYPT_MODE,
+                            new SecretKeySpec(userKey.getEncoded(), "AES"),
+                            ivSpec);
+                    byte[] mkCipher = hexToByteArray(masterKeyBlobHex);
+                    byte[] mkBlob = c.doFinal(mkCipher);
+
+                    // first, the master key IV
+                    int offset = 0;
+                    int len = mkBlob[offset++];
+                    IV = Arrays.copyOfRange(mkBlob, offset, offset + len);
+                    offset += len;
+                    // then the master key itself
+                    len = mkBlob[offset++];
+                    byte[] mk = Arrays.copyOfRange(mkBlob,
+                            offset, offset + len);
+                    offset += len;
+                    // and finally the master key checksum hash
+                    len = mkBlob[offset++];
+                    byte[] mkChecksum = Arrays.copyOfRange(mkBlob,
+                            offset, offset + len);
+
+                    // now validate the decrypted master key against the checksum
+                    byte[] calculatedCk = makeKeyChecksum(mk, ckSalt, rounds);
+                    if (Arrays.equals(calculatedCk, mkChecksum)) {
+                        ivSpec = new IvParameterSpec(IV);
+                        c.init(Cipher.DECRYPT_MODE,
+                                new SecretKeySpec(mk, "AES"),
+                                ivSpec);
+                        // Only if all of the above worked properly will 'result' be assigned
+                        result = new CipherInputStream(rawInStream, c);
+                    } else Slog.w(TAG, "Incorrect password");
+                } else Slog.w(TAG, "Unsupported encryption method: " + encryptionName);
+            } catch (InvalidAlgorithmParameterException e) {
+                Slog.e(TAG, "Needed parameter spec unavailable!", e);
+            } catch (BadPaddingException e) {
+                // This case frequently occurs when the wrong password is used to decrypt
+                // the master key.  Use the identical "incorrect password" log text as is
+                // used in the checksum failure log in order to avoid providing additional
+                // information to an attacker.
+                Slog.w(TAG, "Incorrect password");
+            } catch (IllegalBlockSizeException e) {
+                Slog.w(TAG, "Invalid block size in master key");
+            } catch (NoSuchAlgorithmException e) {
+                Slog.e(TAG, "Needed decryption algorithm unavailable!");
+            } catch (NoSuchPaddingException e) {
+                Slog.e(TAG, "Needed padding mechanism unavailable!");
+            } catch (InvalidKeyException e) {
+                Slog.w(TAG, "Illegal password; aborting");
+            } catch (NumberFormatException e) {
+                Slog.w(TAG, "Can't parse restore data header");
+            } catch (IOException e) {
+                Slog.w(TAG, "Can't read input header");
+            }
+
+            return result;
+        }
+
         boolean restoreOneFile(InputStream instream, byte[] buffer) {
             FileMetadata info;
             try {
@@ -2540,7 +2948,7 @@
                     }
                 }
             } catch (IOException e) {
-                Slog.w(TAG, "io exception on restore socket read", e);
+                if (DEBUG) Slog.w(TAG, "io exception on restore socket read", e);
                 // treat as EOF
                 info = null;
             }
@@ -2929,110 +3337,142 @@
 
             boolean gotHeader = readTarHeader(instream, block);
             if (gotHeader) {
-                // okay, presume we're okay, and extract the various metadata
-                info = new FileMetadata();
-                info.size = extractRadix(block, 124, 12, 8);
-                info.mtime = extractRadix(block, 136, 12, 8);
-                info.mode = extractRadix(block, 100, 8, 8);
+                try {
+                    // okay, presume we're okay, and extract the various metadata
+                    info = new FileMetadata();
+                    info.size = extractRadix(block, 124, 12, 8);
+                    info.mtime = extractRadix(block, 136, 12, 8);
+                    info.mode = extractRadix(block, 100, 8, 8);
 
-                info.path = extractString(block, 345, 155); // prefix
-                String path = extractString(block, 0, 100);
-                if (path.length() > 0) {
-                    if (info.path.length() > 0) info.path += '/';
-                    info.path += path;
-                }
-
-                // tar link indicator field: 1 byte at offset 156 in the header.
-                int typeChar = block[156];
-                if (typeChar == 'x') {
-                    // pax extended header, so we need to read that
-                    gotHeader = readPaxExtendedHeader(instream, info);
-                    if (gotHeader) {
-                        // and after a pax extended header comes another real header -- read
-                        // that to find the real file type
-                        gotHeader = readTarHeader(instream, block);
+                    info.path = extractString(block, 345, 155); // prefix
+                    String path = extractString(block, 0, 100);
+                    if (path.length() > 0) {
+                        if (info.path.length() > 0) info.path += '/';
+                        info.path += path;
                     }
-                    if (!gotHeader) throw new IOException("Bad or missing pax header");
 
-                    typeChar = block[156];
-                }
-
-                switch (typeChar) {
-                    case '0': info.type = BackupAgent.TYPE_FILE; break;
-                    case '5': {
-                        info.type = BackupAgent.TYPE_DIRECTORY;
-                        if (info.size != 0) {
-                            Slog.w(TAG, "Directory entry with nonzero size in header");
-                            info.size = 0;
+                    // tar link indicator field: 1 byte at offset 156 in the header.
+                    int typeChar = block[156];
+                    if (typeChar == 'x') {
+                        // pax extended header, so we need to read that
+                        gotHeader = readPaxExtendedHeader(instream, info);
+                        if (gotHeader) {
+                            // and after a pax extended header comes another real header -- read
+                            // that to find the real file type
+                            gotHeader = readTarHeader(instream, block);
                         }
-                        break;
+                        if (!gotHeader) throw new IOException("Bad or missing pax header");
+
+                        typeChar = block[156];
                     }
-                    case 0: {
-                        // presume EOF
-                        if (DEBUG) Slog.w(TAG, "Saw type=0 in tar header block, info=" + info);
-                        return null;
-                    }
-                    default: {
-                        Slog.e(TAG, "Unknown tar entity type: " + typeChar);
-                        throw new IOException("Unknown entity type " + typeChar);
-                    }
-                }
 
-                // Parse out the path
-                //
-                // first: apps/shared/unrecognized
-                if (FullBackup.SHARED_PREFIX.regionMatches(0,
-                        info.path, 0, FullBackup.SHARED_PREFIX.length())) {
-                    // File in shared storage.  !!! TODO: implement this.
-                    info.path = info.path.substring(FullBackup.SHARED_PREFIX.length());
-                    info.packageName = "com.android.sharedstoragebackup";
-                    info.domain = FullBackup.SHARED_STORAGE_TOKEN;
-                    if (DEBUG) Slog.i(TAG, "File in shared storage: " + info.path);
-                } else if (FullBackup.APPS_PREFIX.regionMatches(0,
-                        info.path, 0, FullBackup.APPS_PREFIX.length())) {
-                    // App content!  Parse out the package name and domain
-
-                    // strip the apps/ prefix
-                    info.path = info.path.substring(FullBackup.APPS_PREFIX.length());
-
-                    // extract the package name
-                    int slash = info.path.indexOf('/');
-                    if (slash < 0) throw new IOException("Illegal semantic path in " + info.path);
-                    info.packageName = info.path.substring(0, slash);
-                    info.path = info.path.substring(slash+1);
-
-                    // if it's a manifest we're done, otherwise parse out the domains
-                    if (!info.path.equals(BACKUP_MANIFEST_FILENAME)) {
-                        slash = info.path.indexOf('/');
-                        if (slash < 0) throw new IOException("Illegal semantic path in non-manifest " + info.path);
-                        info.domain = info.path.substring(0, slash);
-                        // validate that it's one of the domains we understand
-                        if (!info.domain.equals(FullBackup.APK_TREE_TOKEN)
-                                && !info.domain.equals(FullBackup.DATA_TREE_TOKEN)
-                                && !info.domain.equals(FullBackup.DATABASE_TREE_TOKEN)
-                                && !info.domain.equals(FullBackup.ROOT_TREE_TOKEN)
-                                && !info.domain.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)
-                                && !info.domain.equals(FullBackup.OBB_TREE_TOKEN)
-                                && !info.domain.equals(FullBackup.CACHE_TREE_TOKEN)) {
-                            throw new IOException("Unrecognized domain " + info.domain);
+                    switch (typeChar) {
+                        case '0': info.type = BackupAgent.TYPE_FILE; break;
+                        case '5': {
+                            info.type = BackupAgent.TYPE_DIRECTORY;
+                            if (info.size != 0) {
+                                Slog.w(TAG, "Directory entry with nonzero size in header");
+                                info.size = 0;
+                            }
+                            break;
                         }
-
-                        info.path = info.path.substring(slash + 1);
+                        case 0: {
+                            // presume EOF
+                            if (DEBUG) Slog.w(TAG, "Saw type=0 in tar header block, info=" + info);
+                            return null;
+                        }
+                        default: {
+                            Slog.e(TAG, "Unknown tar entity type: " + typeChar);
+                            throw new IOException("Unknown entity type " + typeChar);
+                        }
                     }
+
+                    // Parse out the path
+                    //
+                    // first: apps/shared/unrecognized
+                    if (FullBackup.SHARED_PREFIX.regionMatches(0,
+                            info.path, 0, FullBackup.SHARED_PREFIX.length())) {
+                        // File in shared storage.  !!! TODO: implement this.
+                        info.path = info.path.substring(FullBackup.SHARED_PREFIX.length());
+                        info.packageName = "com.android.sharedstoragebackup";
+                        info.domain = FullBackup.SHARED_STORAGE_TOKEN;
+                        if (DEBUG) Slog.i(TAG, "File in shared storage: " + info.path);
+                    } else if (FullBackup.APPS_PREFIX.regionMatches(0,
+                            info.path, 0, FullBackup.APPS_PREFIX.length())) {
+                        // App content!  Parse out the package name and domain
+
+                        // strip the apps/ prefix
+                        info.path = info.path.substring(FullBackup.APPS_PREFIX.length());
+
+                        // extract the package name
+                        int slash = info.path.indexOf('/');
+                        if (slash < 0) throw new IOException("Illegal semantic path in " + info.path);
+                        info.packageName = info.path.substring(0, slash);
+                        info.path = info.path.substring(slash+1);
+
+                        // if it's a manifest we're done, otherwise parse out the domains
+                        if (!info.path.equals(BACKUP_MANIFEST_FILENAME)) {
+                            slash = info.path.indexOf('/');
+                            if (slash < 0) throw new IOException("Illegal semantic path in non-manifest " + info.path);
+                            info.domain = info.path.substring(0, slash);
+                            // validate that it's one of the domains we understand
+                            if (!info.domain.equals(FullBackup.APK_TREE_TOKEN)
+                                    && !info.domain.equals(FullBackup.DATA_TREE_TOKEN)
+                                    && !info.domain.equals(FullBackup.DATABASE_TREE_TOKEN)
+                                    && !info.domain.equals(FullBackup.ROOT_TREE_TOKEN)
+                                    && !info.domain.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)
+                                    && !info.domain.equals(FullBackup.OBB_TREE_TOKEN)
+                                    && !info.domain.equals(FullBackup.CACHE_TREE_TOKEN)) {
+                                throw new IOException("Unrecognized domain " + info.domain);
+                            }
+
+                            info.path = info.path.substring(slash + 1);
+                        }
+                    }
+                } catch (IOException e) {
+                    if (DEBUG) {
+                        Slog.e(TAG, "Parse error in header.  Hexdump:");
+                        HEXLOG(block);
+                    }
+                    throw e;
                 }
             }
             return info;
         }
 
-        boolean readTarHeader(InputStream instream, byte[] block) throws IOException {
-            int nRead = instream.read(block, 0, 512);
-            if (nRead >= 0) mBytes += nRead;
-            if (nRead > 0 && nRead != 512) {
-                // if we read only a partial block, then things are
-                // clearly screwed up.  terminate the restore.
-                throw new IOException("Partial header block: " + nRead);
+        private void HEXLOG(byte[] block) {
+            int offset = 0;
+            int todo = block.length;
+            StringBuilder buf = new StringBuilder(64);
+            while (todo > 0) {
+                buf.append(String.format("%04x   ", offset));
+                int numThisLine = (todo > 16) ? 16 : todo;
+                for (int i = 0; i < numThisLine; i++) {
+                    buf.append(String.format("%02x ", block[offset+i]));
+                }
+                Slog.i("hexdump", buf.toString());
+                buf.setLength(0);
+                todo -= numThisLine;
+                offset += numThisLine;
             }
-            return (nRead > 0);
+        }
+
+        boolean readTarHeader(InputStream instream, byte[] block) throws IOException {
+            int totalRead = 0;
+            while (totalRead < 512) {
+                int nRead = instream.read(block, totalRead, 512 - totalRead);
+                if (nRead >= 0) {
+                    mBytes += nRead;
+                    totalRead += nRead;
+                } else {
+                    if (totalRead == 0) {
+                        // EOF instead of a new header; we're done
+                        break;
+                    }
+                    throw new IOException("Unable to read full block header, t=" + totalRead);
+                }
+            }
+            return (totalRead == 512);
         }
 
         // overwrites 'info' fields based on the pax extended header
@@ -3102,7 +3542,7 @@
                 // Numeric fields in tar can terminate with either NUL or SPC
                 if (b == 0 || b == ' ') break;
                 if (b < '0' || b > ('0' + radix - 1)) {
-                    throw new IOException("Invalid number in header");
+                    throw new IOException("Invalid number in header: '" + (char)b + "' for radix " + radix);
                 }
                 value = radix * value + (b - '0');
             }
@@ -3930,7 +4370,7 @@
     }
 
     public void fullRestore(ParcelFileDescriptor fd) {
-        mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "fullBackup");
+        mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "fullRestore");
         Slog.i(TAG, "Beginning full restore...");
 
         long oldId = Binder.clearCallingIdentity();
@@ -4011,14 +4451,15 @@
 
     // Confirm that the previously-requested full backup/restore operation can proceed.  This
     // is used to require a user-facing disclosure about the operation.
+    @Override
     public void acknowledgeFullBackupOrRestore(int token, boolean allow,
-            IFullBackupRestoreObserver observer) {
+            String password, IFullBackupRestoreObserver observer) {
         if (DEBUG) Slog.d(TAG, "acknowledgeFullBackupOrRestore : token=" + token
                 + " allow=" + allow);
 
         // TODO: possibly require not just this signature-only permission, but even
         // require that the specific designated confirmation-UI app uid is the caller?
-        mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "fullBackup");
+        mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "acknowledgeFullBackupOrRestore");
 
         long oldId = Binder.clearCallingIdentity();
         try {
@@ -4032,6 +4473,7 @@
 
                     if (allow) {
                         params.observer = observer;
+                        params.password = password;
                         final int verb = params instanceof FullBackupParams
                                 ? MSG_RUN_FULL_BACKUP
                                 : MSG_RUN_FULL_RESTORE;
@@ -4057,7 +4499,7 @@
     // Enable/disable the backup service
     public void setBackupEnabled(boolean enable) {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
-        "setBackupEnabled");
+                "setBackupEnabled");
 
         Slog.i(TAG, "Backup enabled => " + enable);
 
@@ -4102,7 +4544,7 @@
     // Enable/disable automatic restore of app data at install time
     public void setAutoRestore(boolean doAutoRestore) {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
-        "setBackupEnabled");
+                "setAutoRestore");
 
         Slog.i(TAG, "Auto restore => " + doAutoRestore);
 
@@ -4236,7 +4678,7 @@
     // This string is used VERBATIM as the summary text of the relevant Settings item!
     public String getDestinationString(String transportName) {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
-                "getConfigurationIntent");
+                "getDestinationString");
 
         synchronized (mTransports) {
             final IBackupTransport transport = mTransports.get(transportName);
diff --git a/services/java/com/android/server/SystemBackupAgent.java b/services/java/com/android/server/SystemBackupAgent.java
index 950f3b6..da97089 100644
--- a/services/java/com/android/server/SystemBackupAgent.java
+++ b/services/java/com/android/server/SystemBackupAgent.java
@@ -21,6 +21,7 @@
 import android.app.backup.BackupDataOutput;
 import android.app.backup.BackupAgentHelper;
 import android.app.backup.FullBackup;
+import android.app.backup.FullBackupDataOutput;
 import android.app.backup.WallpaperBackupHelper;
 import android.content.Context;
 import android.os.ParcelFileDescriptor;
@@ -53,13 +54,6 @@
     @Override
     public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
             ParcelFileDescriptor newState) throws IOException {
-        if (oldState == null) {
-            // Ah, it's a full backup dataset, being restored piecemeal.  Just
-            // pop over to the full restore handling and we're done.
-            runFullBackup(data);
-            return;
-        }
-
         // We only back up the data under the current "wallpaper" schema with metadata
         WallpaperManagerService wallpaper = (WallpaperManagerService)ServiceManager.getService(
                 Context.WALLPAPER_SERVICE);
@@ -74,19 +68,21 @@
         super.onBackup(oldState, data, newState);
     }
 
-    private void runFullBackup(BackupDataOutput output) {
-        fullWallpaperBackup(output);
+    @Override
+    public void onFullBackup(FullBackupDataOutput data) throws IOException {
+        // At present we back up only the wallpaper
+        fullWallpaperBackup(data);
     }
 
-    private void fullWallpaperBackup(BackupDataOutput output) {
+    private void fullWallpaperBackup(FullBackupDataOutput output) {
         // Back up the data files directly.  We do them in this specific order --
         // info file followed by image -- because then we need take no special
         // steps during restore; the restore will happen properly when the individual
         // files are restored piecemeal.
         FullBackup.backupToTar(getPackageName(), FullBackup.ROOT_TREE_TOKEN, null,
-                WALLPAPER_INFO_DIR, WALLPAPER_INFO, output);
+                WALLPAPER_INFO_DIR, WALLPAPER_INFO, output.getData());
         FullBackup.backupToTar(getPackageName(), FullBackup.ROOT_TREE_TOKEN, null,
-                WALLPAPER_IMAGE_DIR, WALLPAPER_IMAGE, output);
+                WALLPAPER_IMAGE_DIR, WALLPAPER_IMAGE, output.getData());
     }
 
     @Override