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
This commit is contained in:
@@ -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();
|
||||
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 @@ public abstract class BackupAgent extends ContextWrapper {
|
||||
|
||||
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 @@ 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");
|
||||
|
||||
@@ -110,6 +110,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
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user