summaryrefslogtreecommitdiffstats
path: root/services/java/com/android/server/BackupManagerService.java
diff options
context:
space:
mode:
Diffstat (limited to 'services/java/com/android/server/BackupManagerService.java')
-rw-r--r--services/java/com/android/server/BackupManagerService.java775
1 files changed, 620 insertions, 155 deletions
diff --git a/services/java/com/android/server/BackupManagerService.java b/services/java/com/android/server/BackupManagerService.java
index e9e66cb..bdd8938 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,8 @@ class BackupManagerService extends IBackupManager.Stub {
public ParcelFileDescriptor fd;
public final AtomicBoolean latch;
public IFullBackupRestoreObserver observer;
+ public String curPassword; // filled in by the confirmation step
+ public String encryptPassword;
FullParams() {
latch = new AtomicBoolean(false);
@@ -329,6 +355,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,8 +459,8 @@ 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.latch)).run();
+ params.includeShared, params.curPassword, params.encryptPassword,
+ params.allApps, params.packages, params.latch)).run();
break;
}
@@ -434,7 +477,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.curPassword, params.encryptPassword,
+ params.observer, params.latch)).run();
break;
}
@@ -584,6 +628,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 +913,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 +1909,8 @@ class BackupManagerService extends IBackupManager.Stub {
boolean mIncludeShared;
boolean mAllApps;
String[] mPackages;
+ String mCurrentPassword;
+ String mEncryptPassword;
AtomicBoolean mLatchObject;
File mFilesDir;
File mManifestFile;
@@ -1748,14 +1965,25 @@ class BackupManagerService extends IBackupManager.Stub {
}
PerformFullBackupTask(ParcelFileDescriptor fd, IFullBackupRestoreObserver observer,
- boolean includeApks, boolean includeShared,
- boolean doAllApps, String[] packages, AtomicBoolean latch) {
+ boolean includeApks, boolean includeShared, String curPassword,
+ String encryptPassword, boolean doAllApps, String[] packages,
+ AtomicBoolean latch) {
mOutputFile = fd;
mObserver = observer;
mIncludeApks = includeApks;
mIncludeShared = includeShared;
mAllApps = doAllApps;
mPackages = packages;
+ mCurrentPassword = curPassword;
+ // when backing up, if there is a current backup password, we require that
+ // the user use a nonempty encryption password as well. if one is supplied
+ // in the UI we use that, but if the UI was left empty we fall back to the
+ // current backup password (which was supplied by the user as well).
+ if (encryptPassword == null || "".equals(encryptPassword)) {
+ mEncryptPassword = curPassword;
+ } else {
+ mEncryptPassword = encryptPassword;
+ }
mLatchObject = latch;
mFilesDir = new File("/data/system");
@@ -1796,16 +2024,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 = (mEncryptPassword != null && mEncryptPassword.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 +2039,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(mCurrentPassword, 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 +2112,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 +2130,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(mEncryptPassword, 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 +2258,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 +2372,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 +2457,8 @@ class BackupManagerService extends IBackupManager.Stub {
class PerformFullRestoreTask implements Runnable {
ParcelFileDescriptor mInputFile;
+ String mCurrentPassword;
+ String mDecryptPassword;
IFullBackupRestoreObserver mObserver;
AtomicBoolean mLatchObject;
IBackupAgent mAgent;
@@ -2144,9 +2482,11 @@ 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 curPassword, String decryptPassword,
+ IFullBackupRestoreObserver observer, AtomicBoolean latch) {
mInputFile = fd;
+ mCurrentPassword = curPassword;
+ mDecryptPassword = decryptPassword;
mObserver = observer;
mLatchObject = latch;
mAgent = null;
@@ -2202,50 +2542,58 @@ class BackupManagerService extends IBackupManager.Stub {
mPackagePolicies.put("com.android.sharedstoragebackup", RestorePolicy.ACCEPT);
}
+ FileInputStream rawInStream = null;
+ DataInputStream rawDataIn = null;
try {
+ if (hasBackupPassword()) {
+ if (!passwordMatchesSaved(mCurrentPassword, PBKDF2_HASH_ROUNDS)) {
+ if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting");
+ return;
+ }
+ }
+
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 (mDecryptPassword != null && mDecryptPassword.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 +2608,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 +2630,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 +2638,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(mDecryptPassword, 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 +2969,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 +3358,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);
}
+ }
+
+ // 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);
+ 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 +3563,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 +4391,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 +4472,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 curPassword, String encPpassword, 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 {
@@ -4031,11 +4493,14 @@ class BackupManagerService extends IBackupManager.Stub {
mFullConfirmations.delete(token);
if (allow) {
- params.observer = observer;
final int verb = params instanceof FullBackupParams
? MSG_RUN_FULL_BACKUP
: MSG_RUN_FULL_RESTORE;
+ params.observer = observer;
+ params.curPassword = curPassword;
+ params.encryptPassword = encPpassword;
+
if (DEBUG) Slog.d(TAG, "Sending conf message with verb " + verb);
mWakelock.acquire();
Message msg = mBackupHandler.obtainMessage(verb, params);
@@ -4057,7 +4522,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 +4567,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 +4701,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);