blob: 0a7159bfe1b78170898113070cff0674e37bc04e [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.server.backup.fullbackup;
import static com.android.server.backup.BackupManagerService.DEBUG;
import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
import static com.android.server.backup.BackupManagerService.TAG;
import static com.android.server.backup.BackupPasswordManager.PBKDF_CURRENT;
import static com.android.server.backup.UserBackupManagerService.BACKUP_FILE_HEADER_MAGIC;
import static com.android.server.backup.UserBackupManagerService.BACKUP_FILE_VERSION;
import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
import android.app.backup.IFullBackupRestoreObserver;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Slog;
import com.android.server.AppWidgetBackupBridge;
import com.android.server.backup.BackupRestoreTask;
import com.android.server.backup.KeyValueAdbBackupEngine;
import com.android.server.backup.UserBackupManagerService;
import com.android.server.backup.utils.AppBackupUtils;
import com.android.server.backup.utils.PasswordUtils;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
/**
* Full backup task variant used for adb backup.
*/
public class PerformAdbBackupTask extends FullBackupTask implements BackupRestoreTask {
private final UserBackupManagerService mUserBackupManagerService;
private final AtomicBoolean mLatch;
private final ParcelFileDescriptor mOutputFile;
private final boolean mIncludeApks;
private final boolean mIncludeObbs;
private final boolean mIncludeShared;
private final boolean mDoWidgets;
private final boolean mAllApps;
private final boolean mIncludeSystem;
private final boolean mCompress;
private final boolean mKeyValue;
private final ArrayList<String> mPackages;
private PackageInfo mCurrentTarget;
private final String mCurrentPassword;
private final String mEncryptPassword;
private final int mCurrentOpToken;
public PerformAdbBackupTask(UserBackupManagerService backupManagerService,
ParcelFileDescriptor fd, IFullBackupRestoreObserver observer,
boolean includeApks, boolean includeObbs, boolean includeShared, boolean doWidgets,
String curPassword, String encryptPassword, boolean doAllApps, boolean doSystem,
boolean doCompress, boolean doKeyValue, String[] packages, AtomicBoolean latch) {
super(observer);
mUserBackupManagerService = backupManagerService;
mCurrentOpToken = backupManagerService.generateRandomIntegerToken();
mLatch = latch;
mOutputFile = fd;
mIncludeApks = includeApks;
mIncludeObbs = includeObbs;
mIncludeShared = includeShared;
mDoWidgets = doWidgets;
mAllApps = doAllApps;
mIncludeSystem = doSystem;
mPackages = (packages == null)
? new ArrayList<>()
: new ArrayList<>(Arrays.asList(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;
}
if (MORE_DEBUG) {
Slog.w(TAG, "Encrypting backup with passphrase=" + mEncryptPassword);
}
mCompress = doCompress;
mKeyValue = doKeyValue;
}
private void addPackagesToSet(TreeMap<String, PackageInfo> set, List<String> pkgNames) {
for (String pkgName : pkgNames) {
if (!set.containsKey(pkgName)) {
try {
PackageInfo info = mUserBackupManagerService.getPackageManager().getPackageInfo(
pkgName,
PackageManager.GET_SIGNING_CERTIFICATES);
set.put(pkgName, info);
} catch (NameNotFoundException e) {
Slog.w(TAG, "Unknown package " + pkgName + ", skipping");
}
}
}
}
private OutputStream emitAesBackupHeader(StringBuilder headerbuf,
OutputStream ofstream) throws Exception {
// User key will be used to encrypt the master key.
byte[] newUserSalt = mUserBackupManagerService
.randomBytes(PasswordUtils.PBKDF2_SALT_SIZE);
SecretKey userKey = PasswordUtils
.buildPasswordKey(PBKDF_CURRENT, mEncryptPassword,
newUserSalt,
PasswordUtils.PBKDF2_HASH_ROUNDS);
// the master key is random for each backup
byte[] masterPw = new byte[256 / 8];
mUserBackupManagerService.getRng().nextBytes(masterPw);
byte[] checksumSalt = mUserBackupManagerService
.randomBytes(PasswordUtils.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(PasswordUtils.ENCRYPTION_ALGORITHM_NAME);
headerbuf.append('\n');
// line 5: user password salt [hex]
headerbuf.append(PasswordUtils.byteArrayToHex(newUserSalt));
headerbuf.append('\n');
// line 6: master key checksum salt [hex]
headerbuf.append(PasswordUtils.byteArrayToHex(checksumSalt));
headerbuf.append('\n');
// line 7: number of PBKDF2 rounds used [decimal]
headerbuf.append(PasswordUtils.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(PasswordUtils.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 = PasswordUtils
.makeKeyChecksum(PBKDF_CURRENT,
masterKeySpec.getEncoded(),
checksumSalt, PasswordUtils.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(PasswordUtils.byteArrayToHex(encryptedMk));
headerbuf.append('\n');
return finalOutput;
}
private void finalizeBackup(OutputStream out) {
try {
// A standard 'tar' EOF sequence: two 512-byte blocks of all zeroes.
byte[] eof = new byte[512 * 2]; // newly allocated == zero filled
out.write(eof);
} catch (IOException e) {
Slog.w(TAG, "Error attempting to finalize backup stream");
}
}
@Override
public void run() {
String includeKeyValue = mKeyValue ? ", including key-value backups" : "";
Slog.i(TAG, "--- Performing adb backup" + includeKeyValue + " ---");
TreeMap<String, PackageInfo> packagesToBackup = new TreeMap<>();
FullBackupObbConnection obbConnection = new FullBackupObbConnection(
mUserBackupManagerService);
obbConnection.establish(); // we'll want this later
sendStartBackup();
PackageManager pm = mUserBackupManagerService.getPackageManager();
// doAllApps supersedes the package set if any
if (mAllApps) {
List<PackageInfo> allPackages = pm.getInstalledPackages(
PackageManager.GET_SIGNING_CERTIFICATES);
for (int i = 0; i < allPackages.size(); i++) {
PackageInfo pkg = allPackages.get(i);
// Exclude system apps if we've been asked to do so
if (mIncludeSystem
|| ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0)) {
packagesToBackup.put(pkg.packageName, pkg);
}
}
}
// If we're doing widget state as well, ensure that we have all the involved
// host & provider packages in the set
if (mDoWidgets) {
// TODO: http://b/22388012
List<String> pkgs =
AppWidgetBackupBridge.getWidgetParticipants(UserHandle.USER_SYSTEM);
if (pkgs != null) {
if (MORE_DEBUG) {
Slog.i(TAG, "Adding widget participants to backup set:");
StringBuilder sb = new StringBuilder(128);
sb.append(" ");
for (String s : pkgs) {
sb.append(' ');
sb.append(s);
}
Slog.i(TAG, sb.toString());
}
addPackagesToSet(packagesToBackup, pkgs);
}
}
// Now process the command line argument packages, if any. Note that explicitly-
// named system-partition packages will be included even if includeSystem was
// set to false.
if (mPackages != null) {
addPackagesToSet(packagesToBackup, mPackages);
}
// Now we cull any inapplicable / inappropriate packages from the set. This
// includes the special shared-storage agent package; we handle that one
// explicitly at the end of the backup pass. Packages supporting key-value backup are
// added to their own queue, and handled after packages supporting fullbackup.
ArrayList<PackageInfo> keyValueBackupQueue = new ArrayList<>();
Iterator<Entry<String, PackageInfo>> iter = packagesToBackup.entrySet().iterator();
while (iter.hasNext()) {
PackageInfo pkg = iter.next().getValue();
if (!AppBackupUtils.appIsEligibleForBackup(pkg.applicationInfo,
mUserBackupManagerService.getUserId())
|| AppBackupUtils.appIsStopped(pkg.applicationInfo)) {
iter.remove();
if (DEBUG) {
Slog.i(TAG, "Package " + pkg.packageName
+ " is not eligible for backup, removing.");
}
} else if (AppBackupUtils.appIsKeyValueOnly(pkg)) {
iter.remove();
if (DEBUG) {
Slog.i(TAG, "Package " + pkg.packageName
+ " is key-value.");
}
keyValueBackupQueue.add(pkg);
}
}
// flatten the set of packages now so we can explicitly control the ordering
ArrayList<PackageInfo> backupQueue =
new ArrayList<>(packagesToBackup.values());
FileOutputStream ofstream = new FileOutputStream(mOutputFile.getFileDescriptor());
OutputStream out = null;
PackageInfo pkg = null;
try {
boolean encrypting = (mEncryptPassword != null && mEncryptPassword.length() > 0);
// Only allow encrypted backups of encrypted devices
if (mUserBackupManagerService.deviceIsEncrypted() && !encrypting) {
Slog.e(TAG, "Unencrypted backup of encrypted device; aborting");
return;
}
OutputStream finalOutput = ofstream;
// Verify that the given password matches the currently-active
// backup password, if any
if (!mUserBackupManagerService.backupPasswordMatches(mCurrentPassword)) {
if (DEBUG) {
Slog.w(TAG, "Backup password mismatch; aborting");
}
return;
}
// Write the global file header. All strings are UTF-8 encoded; lines end
// with a '\n' byte. Actual backup data begins immediately following the
// final '\n'.
//
// line 1: "ANDROID BACKUP"
// line 2: backup file format version, currently "5"
// line 3: compressed? "0" if not compressed, "1" if compressed.
// 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(BACKUP_FILE_VERSION); // integer, no trailing \n
headerbuf.append(mCompress ? "\n1\n" : "\n0\n");
try {
// Set up the encryption stage if appropriate, and emit the correct header
if (encrypting) {
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 (mCompress) {
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);
return;
}
// Shared storage if requested
if (mIncludeShared) {
try {
pkg = mUserBackupManagerService.getPackageManager().getPackageInfo(
SHARED_BACKUP_AGENT_PACKAGE, 0);
backupQueue.add(pkg);
} catch (NameNotFoundException e) {
Slog.e(TAG, "Unable to find shared-storage backup handler");
}
}
// Now actually run the constructed backup sequence for full backup
int N = backupQueue.size();
for (int i = 0; i < N; i++) {
pkg = backupQueue.get(i);
if (DEBUG) {
Slog.i(TAG, "--- Performing full backup for package " + pkg.packageName
+ " ---");
}
final boolean isSharedStorage =
pkg.packageName.equals(
SHARED_BACKUP_AGENT_PACKAGE);
FullBackupEngine mBackupEngine =
new FullBackupEngine(
mUserBackupManagerService,
out,
null,
pkg,
mIncludeApks,
this,
Long.MAX_VALUE,
mCurrentOpToken,
/*transportFlags=*/ 0);
sendOnBackupPackage(isSharedStorage ? "Shared storage" : pkg.packageName);
// Don't need to check preflight result as there is no preflight hook.
mCurrentTarget = pkg;
mBackupEngine.backupOnePackage();
// after the app's agent runs to handle its private filesystem
// contents, back up any OBB content it has on its behalf.
if (mIncludeObbs && !isSharedStorage) {
boolean obbOkay = obbConnection.backupObbs(pkg, out);
if (!obbOkay) {
throw new RuntimeException("Failure writing OBB stack for " + pkg);
}
}
}
// And for key-value backup if enabled
if (mKeyValue) {
for (PackageInfo keyValuePackage : keyValueBackupQueue) {
if (DEBUG) {
Slog.i(TAG, "--- Performing key-value backup for package "
+ keyValuePackage.packageName + " ---");
}
KeyValueAdbBackupEngine kvBackupEngine =
new KeyValueAdbBackupEngine(out, keyValuePackage,
mUserBackupManagerService,
mUserBackupManagerService.getPackageManager(),
mUserBackupManagerService.getBaseStateDir(),
mUserBackupManagerService.getDataDir());
sendOnBackupPackage(keyValuePackage.packageName);
kvBackupEngine.backupOnePackage();
}
}
// Done!
finalizeBackup(out);
} catch (RemoteException e) {
Slog.e(TAG, "App died during full backup");
} catch (Exception e) {
Slog.e(TAG, "Internal exception during full backup", e);
} finally {
try {
if (out != null) {
out.flush();
out.close();
}
mOutputFile.close();
} catch (IOException e) {
Slog.e(TAG, "IO error closing adb backup file: " + e.getMessage());
}
synchronized (mLatch) {
mLatch.set(true);
mLatch.notifyAll();
}
sendEndBackup();
obbConnection.tearDown();
if (DEBUG) {
Slog.d(TAG, "Full backup pass complete.");
}
mUserBackupManagerService.getWakelock().release();
}
}
// BackupRestoreTask methods, used for timeout handling
@Override
public void execute() {
// Unused
}
@Override
public void operationComplete(long result) {
// Unused
}
@Override
public void handleCancel(boolean cancelAll) {
final PackageInfo target = mCurrentTarget;
if (DEBUG) {
Slog.w(TAG, "adb backup cancel of " + target);
}
if (target != null) {
mUserBackupManagerService.tearDownAgentAndKill(mCurrentTarget.applicationInfo);
}
mUserBackupManagerService.removeOperation(mCurrentOpToken);
}
}