summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/app/backup/BackupAgent.java69
-rw-r--r--core/java/android/app/backup/IBackupManager.aidl24
-rw-r--r--packages/BackupRestoreConfirmation/res/layout/confirm_backup.xml16
-rw-r--r--packages/BackupRestoreConfirmation/res/layout/confirm_restore.xml16
-rw-r--r--packages/BackupRestoreConfirmation/res/values/strings.xml7
-rw-r--r--packages/BackupRestoreConfirmation/src/com/android/backupconfirm/BackupRestoreConfirmation.java21
-rwxr-xr-xpackages/DefaultContainerService/AndroidManifest.xml3
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java16
-rw-r--r--packages/SystemUI/AndroidManifest.xml1
-rw-r--r--packages/VpnDialogs/AndroidManifest.xml3
-rw-r--r--services/java/com/android/server/BackupManagerService.java746
-rw-r--r--services/java/com/android/server/SystemBackupAgent.java20
12 files changed, 737 insertions, 205 deletions
diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java
index 436fdf841409..58ea0c34e422 100644
--- a/core/java/android/app/backup/BackupAgent.java
+++ b/core/java/android/app/backup/BackupAgent.java
@@ -213,13 +213,13 @@ public abstract class BackupAgent extends ContextWrapper {
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 @@ public abstract class BackupAgent extends ContextWrapper {
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();
-
- // 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();
+ 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.
+ 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 @@ public abstract class BackupAgent extends ContextWrapper {
while (scanQueue.size() > 0) {
File file = scanQueue.remove(0);
- String filePath = file.getAbsolutePath();
+ String filePath;
+ try {
+ filePath = file.getCanonicalPath();
- // prune this subtree?
- if (excludes != null && excludes.contains(filePath)) {
- continue;
- }
+ // prune this subtree?
+ if (excludes != null && excludes.contains(filePath)) {
+ continue;
+ }
- // If it's a directory, enqueue its contents for scanning.
- try {
+ // 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 @@ public abstract class BackupAgent extends ContextWrapper {
}
}
}
+ } 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 @@ public abstract class BackupAgent extends ContextWrapper {
// 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 bac874e56b7a..d645ac334694 100644
--- a/core/java/android/app/backup/IBackupManager.aidl
+++ b/core/java/android/app/backup/IBackupManager.aidl
@@ -111,6 +111,23 @@ interface IBackupManager {
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 @@ interface IBackupManager {
* 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 a4564e6cc72f..08dcfae3b331 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 ca99ae18a1e0..8b12ed4adb4b 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 3d85e86c956b..48a8df664d39 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 ed413e693701..fad58b9b3df5 100644
--- a/packages/BackupRestoreConfirmation/src/com/android/backupconfirm/BackupRestoreConfirmation.java
+++ b/packages/BackupRestoreConfirmation/src/com/android/backupconfirm/BackupRestoreConfirmation.java
@@ -126,7 +126,7 @@ public class BackupRestoreConfirmation extends Activity {
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 @@ public class BackupRestoreConfirmation extends Activity {
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 @@ public class BackupRestoreConfirmation extends Activity {
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 b0597c4e6ee4..0f47482e7e22 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 3a7a6e153803..4f39e6980f5b 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 @@ public class SettingsBackupAgent extends BackupAgentHelper {
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 @@ public class SettingsBackupAgent extends BackupAgentHelper {
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 @@ public class SettingsBackupAgent extends BackupAgentHelper {
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 ba9b5b0ae8d5..cf0bdd3b8d5e 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 8554b77b5a4f..bd5f739945b7 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 e9e66cb9d164..3ec2a9675bb4 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.IBackupTransport;
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.FileNotFoundException;
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.Set;
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 @@ class BackupManagerService extends IBackupManager.Stub {
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 @@ class BackupManagerService extends IBackupManager.Stub {
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 @@ class BackupManagerService extends IBackupManager.Stub {
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 @@ class BackupManagerService extends IBackupManager.Stub {
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 @@ class BackupManagerService extends IBackupManager.Stub {
{
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 @@ class BackupManagerService extends IBackupManager.Stub {
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 @@ class BackupManagerService extends IBackupManager.Stub {
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 @@ class BackupManagerService extends IBackupManager.Stub {
}
}
+ 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 @@ class BackupManagerService extends IBackupManager.Stub {
boolean mIncludeShared;
boolean mAllApps;
String[] mPackages;
+ String mUserPassword;
AtomicBoolean mLatchObject;
File mFilesDir;
File mManifestFile;
@@ -1748,7 +1963,7 @@ class BackupManagerService extends IBackupManager.Stub {
}
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 @@ class BackupManagerService extends IBackupManager.Stub {
mIncludeShared = includeShared;
mAllApps = doAllApps;
mPackages = packages;
+ mUserPassword = password;
mLatchObject = latch;
mFilesDir = new File("/data/system");
@@ -1796,16 +2012,13 @@ class BackupManagerService extends IBackupManager.Stub {
}
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 @@ class BackupManagerService extends IBackupManager.Stub {
// 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 @@ class BackupManagerService extends IBackupManager.Stub {
} 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 @@ class BackupManagerService extends IBackupManager.Stub {
}
}
- 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 @@ class BackupManagerService extends IBackupManager.Stub {
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 @@ class BackupManagerService extends IBackupManager.Stub {
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 BackupManagerService extends IBackupManager.Stub {
class PerformFullRestoreTask implements Runnable {
ParcelFileDescriptor mInputFile;
+ String mUserPassword;
IFullBackupRestoreObserver mObserver;
AtomicBoolean mLatchObject;
IBackupAgent mAgent;
@@ -2144,9 +2469,10 @@ class BackupManagerService extends IBackupManager.Stub {
// 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 @@ class BackupManagerService extends IBackupManager.Stub {
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 @@ class BackupManagerService extends IBackupManager.Stub {
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 @@ class BackupManagerService extends IBackupManager.Stub {
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 @@ class BackupManagerService extends IBackupManager.Stub {
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 @@ class BackupManagerService extends IBackupManager.Stub {
}
}
} 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 @@ class BackupManagerService extends IBackupManager.Stub {
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);
-
- 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);
+ 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;
}
- 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;
- }
- 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);
+ if (!gotHeader) throw new IOException("Bad or missing pax header");
+
+ typeChar = block[156];
}
- }
- // 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;
}
+ 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);
+ }
+ }
- info.path = info.path.substring(slash + 1);
+ // 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;
}
+ 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;
+ }
+ }
+
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);
+ 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 (nRead > 0);
+ return (totalRead == 512);
}
// overwrites 'info' fields based on the pax extended header
@@ -3102,7 +3542,7 @@ class BackupManagerService extends IBackupManager.Stub {
// 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 @@ class BackupManagerService extends IBackupManager.Stub {
}
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 @@ class BackupManagerService extends IBackupManager.Stub {
// 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 @@ class BackupManagerService extends IBackupManager.Stub {
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 @@ class BackupManagerService extends IBackupManager.Stub {
// 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 @@ class BackupManagerService extends IBackupManager.Stub {
// 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 @@ class BackupManagerService extends IBackupManager.Stub {
// 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 950f3b68f6d5..da97089b72d2 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.BackupDataInput;
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 @@ public class SystemBackupAgent extends BackupAgentHelper {
@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 @@ public class SystemBackupAgent extends BackupAgentHelper {
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