Merge pull request #17 from stevesoltys/feature/encryption

Add support for encrypted backups
This commit is contained in:
Steve Soltys 2019-03-01 23:39:11 -05:00 committed by GitHub
commit 93f878f574
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 895 additions and 525 deletions

View file

@ -0,0 +1,50 @@
package com.stevesoltys.backup.activity;
import android.app.Activity;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import com.stevesoltys.backup.R;
import java.util.Set;
import java.util.stream.IntStream;
/**
* @author Steve Soltys
*/
public abstract class PackageListActivity extends Activity implements AdapterView.OnItemClickListener {
protected ListView packageListView;
protected Set<String> selectedPackageList;
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
String clickedPackage = (String) packageListView.getItemAtPosition(position);
if (!selectedPackageList.remove(clickedPackage)) {
selectedPackageList.add(clickedPackage);
packageListView.setItemChecked(position, true);
} else {
packageListView.setItemChecked(position, false);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_select_all) {
IntStream.range(0, packageListView.getCount())
.forEach(position -> {
selectedPackageList.add((String) packageListView.getItemAtPosition(position));
packageListView.setItemChecked(position, true);
});
return true;
}
return super.onOptionsItemSelected(item);
}
}

View file

@ -0,0 +1,32 @@
package com.stevesoltys.backup.activity;
import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupWindow;
import com.stevesoltys.backup.R;
/**
* @author Steve Soltys
*/
public class PopupWindowUtil {
public static PopupWindow showLoadingPopupWindow(Activity parent) {
LayoutInflater inflater = (LayoutInflater) parent.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
ViewGroup popupViewGroup = parent.findViewById(R.id.popup_layout);
View popupView = inflater.inflate(R.layout.progress_popup_window, popupViewGroup);
PopupWindow popupWindow = new PopupWindow(popupView, 750, 350, true);
popupWindow.setBackgroundDrawable(new ColorDrawable(Color.WHITE));
popupWindow.setElevation(10);
popupWindow.setFocusable(false);
popupWindow.showAtLocation(popupView, Gravity.CENTER, 0, 0);
popupWindow.setOutsideTouchable(false);
return popupWindow;
}
}

View file

@ -4,7 +4,6 @@ import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import com.stevesoltys.backup.R;
import com.stevesoltys.backup.session.backup.BackupResult;
import com.stevesoltys.backup.session.backup.BackupSession;
@ -12,7 +11,7 @@ import com.stevesoltys.backup.session.backup.BackupSession;
/**
* @author Steve Soltys
*/
class BackupPopupWindowListener implements Button.OnClickListener {
public class BackupPopupWindowListener implements Button.OnClickListener {
private static final String TAG = BackupPopupWindowListener.class.getName();
@ -26,16 +25,13 @@ class BackupPopupWindowListener implements Button.OnClickListener {
public void onClick(View view) {
int viewId = view.getId();
switch (viewId) {
if (viewId == R.id.popup_cancel_button) {
try {
backupSession.stop(BackupResult.CANCELLED);
case R.id.popup_cancel_button:
try {
backupSession.stop(BackupResult.CANCELLED);
} catch (RemoteException e) {
Log.e(TAG, "Error cancelling backup session: ", e);
}
break;
} catch (RemoteException e) {
Log.e(TAG, "Error cancelling backup session: ", e);
}
}
}
}

View file

@ -1,39 +1,28 @@
package com.stevesoltys.backup.activity.backup;
import android.app.Activity;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import com.stevesoltys.backup.R;
import com.stevesoltys.backup.activity.PackageListActivity;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.IntStream;
public class CreateBackupActivity extends Activity implements View.OnClickListener, AdapterView.OnItemClickListener {
public class CreateBackupActivity extends PackageListActivity implements View.OnClickListener {
private CreateBackupActivityController controller;
private ListView packageListView;
private Set<String> selectedPackageList;
private Uri contentUri;
@Override
public void onClick(View view) {
int viewId = view.getId();
switch (viewId) {
case R.id.create_confirm_button:
controller.backupPackages(selectedPackageList, contentUri, this);
break;
if (viewId == R.id.create_confirm_button) {
controller.showEnterPasswordAlert(selectedPackageList, contentUri, this);
}
}
@ -49,7 +38,7 @@ public class CreateBackupActivity extends Activity implements View.OnClickListen
contentUri = getIntent().getData();
controller = new CreateBackupActivityController();
controller.populatePackageList(packageListView, this);
AsyncTask.execute(() -> controller.populatePackageList(packageListView, CreateBackupActivity.this));
}
@Override
@ -58,38 +47,4 @@ public class CreateBackupActivity extends Activity implements View.OnClickListen
inflater.inflate(R.menu.backup_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_select_all:
IntStream.range(0, packageListView.getCount())
.forEach(position -> {
selectedPackageList.add((String) packageListView.getItemAtPosition(position));
packageListView.setItemChecked(position, true);
});
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
String clickedPackage = (String) packageListView.getItemAtPosition(position);
if (!selectedPackageList.remove(clickedPackage)) {
selectedPackageList.add(clickedPackage);
packageListView.setItemChecked(position, true);
} else {
packageListView.setItemChecked(position, false);
}
}
}

View file

@ -1,33 +1,23 @@
package com.stevesoltys.backup.activity.backup;
import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.app.AlertDialog;
import android.net.Uri;
import android.os.RemoteException;
import android.text.InputType;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.PopupWindow;
import android.widget.Toast;
import android.widget.*;
import com.google.android.collect.Sets;
import com.stevesoltys.backup.R;
import com.stevesoltys.backup.session.BackupManagerController;
import com.stevesoltys.backup.session.backup.BackupSession;
import com.stevesoltys.backup.transport.ConfigurableBackupTransport;
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
import com.stevesoltys.backup.transport.component.BackupComponent;
import com.stevesoltys.backup.transport.component.RestoreComponent;
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupComponent;
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfiguration;
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfigurationBuilder;
import com.stevesoltys.backup.transport.component.provider.ContentProviderRestoreComponent;
import com.stevesoltys.backup.activity.PopupWindowUtil;
import com.stevesoltys.backup.service.backup.BackupService;
import com.stevesoltys.backup.service.PackageService;
import java.util.*;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author Steve Soltys
@ -36,82 +26,91 @@ class CreateBackupActivityController {
private static final String TAG = CreateBackupActivityController.class.getName();
private static final Set<String> IGNORED_PACKAGES = Collections.singleton("com.android.providers.downloads.ui");
private static final Set<String> IGNORED_PACKAGES = Sets.newArraySet(
"com.android.providers.downloads.ui", "com.android.providers.downloads", "com.android.providers.media",
"com.stevesoltys.backup"
);
private final BackupManagerController backupManager;
private final BackupService backupService = new BackupService();
CreateBackupActivityController() {
backupManager = new BackupManagerController();
}
private final PackageService packageService = new PackageService();
void populatePackageList(ListView packageListView, CreateBackupActivity parent) {
AtomicReference<PopupWindow> popupWindow = new AtomicReference<>();
parent.runOnUiThread(() -> {
popupWindow.set(PopupWindowUtil.showLoadingPopupWindow(parent));
TextView textView = popupWindow.get().getContentView().findViewById(R.id.popup_text_view);
textView.setText(R.string.loading_packages);
View popupWindowButton = popupWindow.get().getContentView().findViewById(R.id.popup_cancel_button);
popupWindowButton.setOnClickListener(view -> parent.finish());
});
List<String> eligiblePackageList = new LinkedList<>();
try {
eligiblePackageList.addAll(backupManager.getEligiblePackages());
eligiblePackageList.addAll(packageService.getEligiblePackages());
eligiblePackageList.removeAll(IGNORED_PACKAGES);
} catch (RemoteException e) {
Log.e(TAG, "Error while obtaining package list: ", e);
}
packageListView.setOnItemClickListener(parent);
packageListView.setAdapter(new ArrayAdapter<>(parent, R.layout.checked_list_item, eligiblePackageList));
packageListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
}
void backupPackages(Set<String> selectedPackages, Uri contentUri, Activity parent) {
try {
selectedPackages.add("@pm@");
ContentProviderBackupConfiguration backupConfiguration = new ContentProviderBackupConfigurationBuilder()
.setContext(parent).setOutputUri(contentUri).setPackages(selectedPackages).build();
boolean success = initializeBackupTransport(backupConfiguration);
if (!success) {
Toast.makeText(parent, R.string.backup_in_progress, Toast.LENGTH_LONG).show();
return;
parent.runOnUiThread(() -> {
if (popupWindow.get() != null) {
popupWindow.get().dismiss();
}
PopupWindow popupWindow = buildPopupWindow(parent);
BackupObserver backupObserver = new BackupObserver(parent, popupWindow);
BackupSession backupSession = backupManager.backup(backupObserver, selectedPackages);
View popupWindowButton = popupWindow.getContentView().findViewById(R.id.popup_cancel_button);
if (popupWindowButton != null) {
popupWindowButton.setOnClickListener(new BackupPopupWindowListener(backupSession));
}
} catch (Exception e) {
Log.e(TAG, "Error while running backup: ", e);
}
packageListView.setOnItemClickListener(parent);
packageListView.setAdapter(new ArrayAdapter<>(parent, R.layout.checked_list_item, eligiblePackageList));
packageListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
});
}
private boolean initializeBackupTransport(ContentProviderBackupConfiguration configuration) {
ConfigurableBackupTransport backupTransport = ConfigurableBackupTransportService.getBackupTransport();
void showEnterPasswordAlert(Set<String> selectedPackages, Uri contentUri, Activity parent) {
final EditText passwordTextView = new EditText(parent);
passwordTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
if (backupTransport.isActive()) {
return false;
}
new AlertDialog.Builder(parent)
.setTitle("Enter a password")
.setMessage("You'll need this to restore your backup, so write it down!")
.setView(passwordTextView)
BackupComponent backupComponent = new ContentProviderBackupComponent(configuration);
RestoreComponent restoreComponent = new ContentProviderRestoreComponent(configuration);
backupTransport.initialize(backupComponent, restoreComponent);
return true;
.setPositiveButton("Set password", (dialog, button) ->
showConfirmPasswordAlert(selectedPackages, contentUri, parent,
passwordTextView.getText().toString()))
.setNegativeButton("Cancel", (dialog, button) -> dialog.cancel())
.show();
}
private PopupWindow buildPopupWindow(Activity parent) {
LayoutInflater inflater = (LayoutInflater) parent.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
ViewGroup popupViewGroup = parent.findViewById(R.id.popup_layout);
View popupView = inflater.inflate(R.layout.progress_popup_window, popupViewGroup);
private void showConfirmPasswordAlert(Set<String> selectedPackages, Uri contentUri, Activity parent,
String originalPassword) {
final EditText passwordTextView = new EditText(parent);
passwordTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
PopupWindow popupWindow = new PopupWindow(popupView, 750, 350, true);
popupWindow.setBackgroundDrawable(new ColorDrawable(Color.WHITE));
popupWindow.setElevation(10);
popupWindow.setFocusable(false);
popupWindow.showAtLocation(popupView, Gravity.CENTER, 0, 0);
return popupWindow;
new AlertDialog.Builder(parent)
.setTitle("Confirm password")
.setView(passwordTextView)
.setPositiveButton("Confirm", (dialog, button) -> {
String password = passwordTextView.getText().toString();
if (originalPassword.equals(password)) {
backupService.backupPackageData(selectedPackages, contentUri, parent, password);
} else {
new AlertDialog.Builder(parent)
.setMessage("Passwords do not match, please try again.")
.setPositiveButton("Ok", (dialog2, button2) -> dialog2.dismiss())
.show();
dialog.cancel();
}
})
.setNegativeButton("Cancel", (dialog, button) -> dialog.cancel())
.show();
}
}

View file

@ -1,39 +1,28 @@
package com.stevesoltys.backup.activity.restore;
import android.app.Activity;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import com.stevesoltys.backup.R;
import com.stevesoltys.backup.activity.PackageListActivity;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.IntStream;
public class RestoreBackupActivity extends Activity implements View.OnClickListener, AdapterView.OnItemClickListener {
public class RestoreBackupActivity extends PackageListActivity implements View.OnClickListener {
private RestoreBackupActivityController controller;
private ListView packageListView;
private Set<String> selectedPackageList;
private Uri contentUri;
@Override
public void onClick(View view) {
int viewId = view.getId();
switch (viewId) {
case R.id.restore_confirm_button:
controller.restorePackages(selectedPackageList, contentUri, this);
break;
if (viewId == R.id.restore_confirm_button) {
controller.showEnterPasswordAlert(selectedPackageList, contentUri, this);
}
}
@ -49,7 +38,7 @@ public class RestoreBackupActivity extends Activity implements View.OnClickListe
contentUri = getIntent().getData();
controller = new RestoreBackupActivityController();
controller.populatePackageList(packageListView, contentUri, this);
AsyncTask.execute(() -> controller.populatePackageList(packageListView, contentUri, this));
}
@Override
@ -58,38 +47,4 @@ public class RestoreBackupActivity extends Activity implements View.OnClickListe
inflater.inflate(R.menu.backup_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_select_all:
IntStream.range(0, packageListView.getCount())
.forEach(position -> {
selectedPackageList.add((String) packageListView.getItemAtPosition(position));
packageListView.setItemChecked(position, true);
});
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
String clickedPackage = (String) packageListView.getItemAtPosition(position);
if (!selectedPackageList.remove(clickedPackage)) {
selectedPackageList.add(clickedPackage);
packageListView.setItemChecked(position, true);
} else {
packageListView.setItemChecked(position, false);
}
}
}

View file

@ -1,32 +1,17 @@
package com.stevesoltys.backup.activity.restore;
import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.app.AlertDialog;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.text.InputType;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.PopupWindow;
import android.widget.Toast;
import android.widget.*;
import com.stevesoltys.backup.R;
import com.stevesoltys.backup.session.BackupManagerController;
import com.stevesoltys.backup.session.restore.RestoreSession;
import com.stevesoltys.backup.transport.ConfigurableBackupTransport;
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
import com.stevesoltys.backup.transport.component.BackupComponent;
import com.stevesoltys.backup.transport.component.RestoreComponent;
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfiguration;
import com.stevesoltys.backup.activity.PopupWindowUtil;
import com.stevesoltys.backup.service.restore.RestoreService;
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfigurationBuilder;
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupComponent;
import com.stevesoltys.backup.transport.component.provider.ContentProviderRestoreComponent;
import libcore.io.IoUtils;
import java.io.File;
@ -35,6 +20,7 @@ import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@ -45,14 +31,22 @@ class RestoreBackupActivityController {
private static final String TAG = RestoreBackupActivityController.class.getName();
private final BackupManagerController backupManager;
RestoreBackupActivityController() {
backupManager = new BackupManagerController();
}
private final RestoreService restoreService = new RestoreService();
void populatePackageList(ListView packageListView, Uri contentUri, RestoreBackupActivity parent) {
AtomicReference<PopupWindow> popupWindow = new AtomicReference<>();
parent.runOnUiThread(() -> {
popupWindow.set(PopupWindowUtil.showLoadingPopupWindow(parent));
TextView textView = popupWindow.get().getContentView().findViewById(R.id.popup_text_view);
textView.setText(R.string.loading_backup);
View popupWindowButton = popupWindow.get().getContentView().findViewById(R.id.popup_cancel_button);
popupWindowButton.setOnClickListener(view -> parent.finish());
});
List<String> eligiblePackageList = new LinkedList<>();
try {
eligiblePackageList.addAll(getEligiblePackages(contentUri, parent));
@ -60,9 +54,15 @@ class RestoreBackupActivityController {
Log.e(TAG, "Error while obtaining package list: ", e);
}
packageListView.setOnItemClickListener(parent);
packageListView.setAdapter(new ArrayAdapter<>(parent, R.layout.checked_list_item, eligiblePackageList));
packageListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
parent.runOnUiThread(() -> {
if (popupWindow.get() != null) {
popupWindow.get().dismiss();
}
packageListView.setOnItemClickListener(parent);
packageListView.setAdapter(new ArrayAdapter<>(parent, R.layout.checked_list_item, eligiblePackageList));
packageListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
});
}
private List<String> getEligiblePackages(Uri contentUri, Activity context) throws IOException {
@ -89,53 +89,20 @@ class RestoreBackupActivityController {
return results;
}
void restorePackages(Set<String> selectedPackages, Uri contentUri, Activity parent) {
try {
ContentProviderBackupConfiguration backupConfiguration = new ContentProviderBackupConfigurationBuilder().
setContext(parent).setOutputUri(contentUri).setPackages(selectedPackages).build();
boolean success = initializeBackupTransport(backupConfiguration);
void showEnterPasswordAlert(Set<String> selectedPackages, Uri contentUri, Activity parent) {
final EditText passwordTextView = new EditText(parent);
passwordTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
if(!success) {
Toast.makeText(parent, R.string.restore_in_progress, Toast.LENGTH_LONG).show();
return;
}
new AlertDialog.Builder(parent)
.setTitle("Enter a password")
.setMessage("If you didn't enter one while creating the backup, you can leave this blank.")
.setView(passwordTextView)
PopupWindow popupWindow = buildPopupWindow(parent);
RestoreObserver restoreObserver = new RestoreObserver(parent, popupWindow, selectedPackages.size());
RestoreSession restoreSession = backupManager.restore(restoreObserver, selectedPackages);
.setPositiveButton("Confirm", (dialog, button) ->
restoreService.restorePackages(selectedPackages, contentUri, parent,
passwordTextView.getText().toString()))
View popupWindowButton = popupWindow.getContentView().findViewById(R.id.popup_cancel_button);
if (popupWindowButton != null) {
popupWindowButton.setOnClickListener(new RestorePopupWindowListener(restoreSession));
}
} catch (RemoteException e) {
Log.e(TAG, "Error while running restore: ", e);
}
}
private boolean initializeBackupTransport(ContentProviderBackupConfiguration configuration) {
ConfigurableBackupTransport backupTransport = ConfigurableBackupTransportService.getBackupTransport();
if(backupTransport.isActive()) {
return false;
}
BackupComponent backupComponent = new ContentProviderBackupComponent(configuration);
RestoreComponent restoreComponent = new ContentProviderRestoreComponent(configuration);
backupTransport.initialize(backupComponent, restoreComponent);
return true;
}
private PopupWindow buildPopupWindow(Activity parent) {
LayoutInflater inflater = (LayoutInflater) parent.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
ViewGroup popupViewGroup = parent.findViewById(R.id.popup_layout);
View popupView = inflater.inflate(R.layout.progress_popup_window, popupViewGroup);
PopupWindow popupWindow = new PopupWindow(popupView, 750, 350, true);
popupWindow.setBackgroundDrawable(new ColorDrawable(Color.WHITE));
popupWindow.setElevation(10);
popupWindow.setFocusable(false);
popupWindow.showAtLocation(popupView, Gravity.CENTER, 0, 0);
return popupWindow;
.setNegativeButton("Cancel", (dialog, button) -> dialog.cancel())
.show();
}
}

View file

@ -12,13 +12,13 @@ import com.stevesoltys.backup.session.restore.RestoreSession;
/**
* @author Steve Soltys
*/
class RestorePopupWindowListener implements Button.OnClickListener {
public class RestorePopupWindowListener implements Button.OnClickListener {
private static final String TAG = RestorePopupWindowListener.class.getName();
private final RestoreSession restoreSession;
RestorePopupWindowListener(RestoreSession restoreSession) {
public RestorePopupWindowListener(RestoreSession restoreSession) {
this.restoreSession = restoreSession;
}

View file

@ -0,0 +1,52 @@
package com.stevesoltys.backup.security;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* A utility class for encrypting and decrypting data using a {@link Cipher}.
*
* @author Steve Soltys
*/
public class CipherUtil {
/**
* The cipher algorithm.
*/
public static final String CIPHER_ALGORITHM = "AES/CFB/PKCS5Padding";
/**.
* Encrypts the given payload using the provided secret key.
*
* @param payload The payload.
* @param secretKey The secret key.
* @param iv The initialization vector.
*/
public static byte[] encrypt(byte[] payload, SecretKey secretKey, byte[] iv) throws NoSuchPaddingException,
NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException,
InvalidAlgorithmParameterException, InvalidKeyException {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
return cipher.doFinal(payload);
}
/**
* Decrypts the given payload using the provided secret key.
*
* @param payload The payload.
* @param secretKey The secret key.
* @param iv The initialization vector.
*/
public static byte[] decrypt(byte[] payload, SecretKey secretKey, byte[] iv) throws NoSuchPaddingException,
NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException,
InvalidAlgorithmParameterException, InvalidKeyException {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
return cipher.doFinal(payload);
}
}

View file

@ -0,0 +1,44 @@
package com.stevesoltys.backup.security;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
/**
* A utility class which can be used for generating an AES secret key using PBKDF2.
*
* @author Steve Soltys
*/
public class KeyGenerator {
/**
* The number of iterations for key generation.
*/
private static final int ITERATIONS = 25;
/**
* The generated key length.
*/
private static final int KEY_LENGTH = 256;
/**
* Generates an AES secret key using PBKDF2.
*
* @param password The password.
* @param salt The salt.
* @return The generated key.
*/
public static SecretKey generate(String password, byte[] salt)
throws NoSuchAlgorithmException, InvalidKeySpecException {
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LENGTH);
SecretKey secretKey = secretKeyFactory.generateSecret(keySpec);
return new SecretKeySpec(secretKey.getEncoded(), "AES");
}
}

View file

@ -0,0 +1,42 @@
package com.stevesoltys.backup.service;
import android.app.backup.IBackupManager;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import java.util.ArrayList;
import java.util.List;
/**
* @author Steve Soltys
*/
public class PackageService {
private final IBackupManager backupManager;
private final IPackageManager packageManager;
public PackageService() {
backupManager = IBackupManager.Stub.asInterface(ServiceManager.getService("backup"));
packageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
}
public List<String> getEligiblePackages() throws RemoteException {
List<String> results = new ArrayList<>();
List<PackageInfo> packages = packageManager.getInstalledPackages(0, UserHandle.USER_SYSTEM).getList();
if (packages != null) {
for (PackageInfo packageInfo : packages) {
if (backupManager.isAppEligibleForBackup(packageInfo.packageName)) {
results.add(packageInfo.packageName);
}
}
}
return results;
}
}

View file

@ -1,35 +1,46 @@
package com.stevesoltys.backup.session;
package com.stevesoltys.backup.service;
import android.app.backup.IBackupManager;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.os.RemoteException;
import android.os.ServiceManager;
import com.stevesoltys.backup.session.backup.BackupSession;
import com.stevesoltys.backup.session.backup.BackupSessionObserver;
import com.stevesoltys.backup.session.restore.RestoreSession;
import com.stevesoltys.backup.session.restore.RestoreSessionObserver;
import com.stevesoltys.backup.transport.ConfigurableBackupTransport;
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
import com.stevesoltys.backup.transport.component.BackupComponent;
import com.stevesoltys.backup.transport.component.RestoreComponent;
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupComponent;
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfiguration;
import com.stevesoltys.backup.transport.component.provider.ContentProviderRestoreComponent;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static android.os.UserHandle.USER_SYSTEM;
/**
* @author Steve Soltys
*/
public class BackupManagerController {
public class TransportService {
private static final String BACKUP_TRANSPORT = "com.stevesoltys.backup.transport.ConfigurableBackupTransport";
private final IBackupManager backupManager;
private final IPackageManager packageManager;
public BackupManagerController() {
public TransportService() {
backupManager = IBackupManager.Stub.asInterface(ServiceManager.getService("backup"));
packageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
}
public boolean initializeBackupTransport(ContentProviderBackupConfiguration configuration) {
ConfigurableBackupTransport backupTransport = ConfigurableBackupTransportService.getBackupTransport();
if (backupTransport.isActive()) {
return false;
}
BackupComponent backupComponent = new ContentProviderBackupComponent(configuration);
RestoreComponent restoreComponent = new ContentProviderRestoreComponent(configuration);
backupTransport.initialize(backupComponent, restoreComponent);
return true;
}
public BackupSession backup(BackupSessionObserver observer, Set<String> packages) throws RemoteException {
@ -61,20 +72,4 @@ public class BackupManagerController {
restoreSession.start();
return restoreSession;
}
public List<String> getEligiblePackages() throws RemoteException {
List<String> results = new ArrayList<>();
List<PackageInfo> packages = packageManager.getInstalledPackages(0, USER_SYSTEM).getList();
if (packages != null) {
for (PackageInfo packageInfo : packages) {
if (backupManager.isAppEligibleForBackup(packageInfo.packageName)) {
results.add(packageInfo.packageName);
}
}
}
return results;
}
}

View file

@ -1,4 +1,4 @@
package com.stevesoltys.backup.activity.backup;
package com.stevesoltys.backup.service.backup;
import android.app.Activity;
import android.app.backup.BackupProgress;
@ -13,6 +13,8 @@ import com.stevesoltys.backup.session.backup.BackupSessionObserver;
import com.stevesoltys.backup.transport.ConfigurableBackupTransport;
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
import java.net.URI;
/**
* @author Steve Soltys
*/
@ -22,9 +24,12 @@ class BackupObserver implements BackupSessionObserver {
private final PopupWindow popupWindow;
BackupObserver(Activity context, PopupWindow popupWindow) {
private final URI contentUri;
BackupObserver(Activity context, PopupWindow popupWindow, URI contentUri) {
this.context = context;
this.popupWindow = popupWindow;
this.contentUri = contentUri;
}
@Override

View file

@ -0,0 +1,67 @@
package com.stevesoltys.backup.service.backup;
import android.app.Activity;
import android.net.Uri;
import android.util.Log;
import android.view.View;
import android.widget.PopupWindow;
import android.widget.TextView;
import android.widget.Toast;
import com.stevesoltys.backup.R;
import com.stevesoltys.backup.activity.backup.BackupPopupWindowListener;
import com.stevesoltys.backup.activity.PopupWindowUtil;
import com.stevesoltys.backup.service.TransportService;
import com.stevesoltys.backup.session.backup.BackupSession;
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfiguration;
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfigurationBuilder;
import java.net.URI;
import java.util.Set;
/**
* @author Steve Soltys
*/
public class BackupService {
private static final String TAG = BackupService.class.getName();
private final TransportService transportService = new TransportService();
public void backupPackageData(Set<String> selectedPackages, Uri contentUri, Activity parent,
String selectedPassword) {
try {
selectedPackages.add("@pm@");
ContentProviderBackupConfiguration backupConfiguration = new ContentProviderBackupConfigurationBuilder()
.setContext(parent)
.setOutputUri(contentUri)
.setPackages(selectedPackages)
.setPassword(selectedPassword)
.build();
boolean success = transportService.initializeBackupTransport(backupConfiguration);
if (!success) {
Toast.makeText(parent, R.string.backup_in_progress, Toast.LENGTH_LONG).show();
return;
}
PopupWindow popupWindow = PopupWindowUtil.showLoadingPopupWindow(parent);
BackupObserver backupObserver = new BackupObserver(parent, popupWindow, new URI(contentUri.getPath()));
BackupSession backupSession = transportService.backup(backupObserver, selectedPackages);
View popupWindowButton = popupWindow.getContentView().findViewById(R.id.popup_cancel_button);
popupWindowButton.setOnClickListener(new BackupPopupWindowListener(backupSession));
TextView textView = popupWindow.getContentView().findViewById(R.id.popup_text_view);
textView.setText(R.string.initializing);
} catch (Exception e) {
Log.e(TAG, "Error while running backup: ", e);
}
}
public void backupPackages() {
}
}

View file

@ -1,11 +1,10 @@
package com.stevesoltys.backup.activity.restore;
package com.stevesoltys.backup.service.restore;
import android.app.Activity;
import android.widget.PopupWindow;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.stevesoltys.backup.R;
import com.stevesoltys.backup.session.restore.RestoreResult;
import com.stevesoltys.backup.session.restore.RestoreSessionObserver;
@ -31,6 +30,10 @@ class RestoreObserver implements RestoreSessionObserver {
@Override
public void restoreSessionStarted(int packageCount) {
context.runOnUiThread(() -> {
TextView textView = popupWindow.getContentView().findViewById(R.id.popup_text_view);
textView.setText(R.string.initializing);
});
}
@Override
@ -55,7 +58,7 @@ class RestoreObserver implements RestoreSessionObserver {
public void restoreSessionCompleted(RestoreResult restoreResult) {
ConfigurableBackupTransport backupTransport = ConfigurableBackupTransportService.getBackupTransport();
if(!backupTransport.isActive()) {
if (!backupTransport.isActive()) {
return;
}

View file

@ -0,0 +1,59 @@
package com.stevesoltys.backup.service.restore;
import android.app.Activity;
import android.net.Uri;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.widget.PopupWindow;
import android.widget.Toast;
import com.stevesoltys.backup.R;
import com.stevesoltys.backup.activity.PopupWindowUtil;
import com.stevesoltys.backup.activity.restore.RestorePopupWindowListener;
import com.stevesoltys.backup.service.TransportService;
import com.stevesoltys.backup.session.restore.RestoreSession;
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfiguration;
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfigurationBuilder;
import java.util.Set;
/**
* @author Steve Soltys
*/
public class RestoreService {
private static final String TAG = RestoreService.class.getName();
private final TransportService transportService = new TransportService();
public void restorePackages(Set<String> selectedPackages, Uri contentUri, Activity parent, String password) {
try {
ContentProviderBackupConfiguration backupConfiguration = new ContentProviderBackupConfigurationBuilder().
setContext(parent)
.setOutputUri(contentUri)
.setPackages(selectedPackages)
.setPassword(password)
.build();
boolean success = transportService.initializeBackupTransport(backupConfiguration);
if (!success) {
Toast.makeText(parent, R.string.restore_in_progress, Toast.LENGTH_LONG).show();
return;
}
PopupWindow popupWindow = PopupWindowUtil.showLoadingPopupWindow(parent);
RestoreObserver restoreObserver = new RestoreObserver(parent, popupWindow, selectedPackages.size());
RestoreSession restoreSession = transportService.restore(restoreObserver, selectedPackages);
View popupWindowButton = popupWindow.getContentView().findViewById(R.id.popup_cancel_button);
if (popupWindowButton != null) {
popupWindowButton.setOnClickListener(new RestorePopupWindowListener(restoreSession));
}
} catch (RemoteException e) {
Log.e(TAG, "Error while running restore: ", e);
}
}
}

View file

@ -6,15 +6,18 @@ import android.content.pm.PackageInfo;
import android.os.ParcelFileDescriptor;
import android.util.Base64;
import android.util.Log;
import com.stevesoltys.backup.security.CipherUtil;
import com.stevesoltys.backup.security.KeyGenerator;
import com.stevesoltys.backup.transport.component.BackupComponent;
import libcore.io.IoUtils;
import org.apache.commons.io.IOUtils;
import libcore.io.IoUtils;
import java.io.*;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import javax.crypto.SecretKey;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@ -41,11 +44,6 @@ public class ContentProviderBackupComponent implements BackupComponent {
this.configuration = configuration;
}
@Override
public long requestBackupTime() {
return 0;
}
@Override
public String currentDestinationString() {
return DESTINATION_DESCRIPTION;
@ -67,33 +65,8 @@ public class ContentProviderBackupComponent implements BackupComponent {
}
@Override
public long getBackupQuota(String packageName, boolean fullBackup) {
return configuration.getBackupSizeQuota();
}
@Override
public long requestFullBackupTime() {
return 0;
}
private void initializeBackupState() throws IOException {
if (backupState == null) {
backupState = new ContentProviderBackupState();
}
if (backupState.getOutputStream() == null) {
initializeOutputStream();
}
}
private void initializeOutputStream() throws FileNotFoundException {
ContentResolver contentResolver = configuration.getContext().getContentResolver();
ParcelFileDescriptor outputFileDescriptor = contentResolver.openFileDescriptor(configuration.getUri(), "w");
backupState.setOutputFileDescriptor(outputFileDescriptor);
FileOutputStream fileOutputStream = new FileOutputStream(outputFileDescriptor.getFileDescriptor());
ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream);
backupState.setOutputStream(zipOutputStream);
public int finishBackup() {
return clearBackupState(false);
}
@Override
@ -113,44 +86,55 @@ public class ContentProviderBackupComponent implements BackupComponent {
}
}
private int transferIncrementalBackupData(BackupDataInput backupDataInput)
throws IOException, InvalidAlgorithmParameterException, InvalidKeyException {
private int clearBackupState(boolean closeFile) {
ZipOutputStream outputStream = backupState.getOutputStream();
if (backupState == null) {
return TRANSPORT_OK;
}
int bufferSize = INITIAL_BUFFER_SIZE;
byte[] buffer = new byte[bufferSize];
try {
IoUtils.closeQuietly(backupState.getInputFileDescriptor());
backupState.setInputFileDescriptor(null);
while (backupDataInput.readNextHeader()) {
String chunkFileName = Base64.encodeToString(backupDataInput.getKey().getBytes(), Base64.DEFAULT);
int dataSize = backupDataInput.getDataSize();
ZipOutputStream outputStream = backupState.getOutputStream();
if (dataSize >= 0) {
ZipEntry zipEntry = new ZipEntry(configuration.getIncrementalBackupDirectory() +
backupState.getPackageName() + "/" + chunkFileName);
outputStream.putNextEntry(zipEntry);
if (dataSize > bufferSize) {
bufferSize = dataSize;
buffer = new byte[bufferSize];
}
backupDataInput.readEntityData(buffer, 0, dataSize);
try {
outputStream.write(buffer, 0, dataSize);
} catch (Exception ex) {
Log.e(TAG, "Error performing incremental backup for " + backupState.getPackageName() + ": ", ex);
clearBackupState(true);
return TRANSPORT_ERROR;
}
if (outputStream != null) {
outputStream.closeEntry();
}
if (backupState.getPackageIndex() == configuration.getPackageCount() || closeFile) {
if (outputStream != null) {
outputStream.finish();
outputStream.close();
}
IoUtils.closeQuietly(backupState.getOutputFileDescriptor());
backupState = null;
}
} catch (IOException ex) {
Log.e(TAG, "Error cancelling full backup: ", ex);
return TRANSPORT_ERROR;
}
return TRANSPORT_OK;
}
@Override
public long getBackupQuota(String packageName, boolean fullBackup) {
return configuration.getBackupSizeQuota();
}
@Override
public long requestBackupTime() {
return 0;
}
@Override
public long requestFullBackupTime() {
return 0;
}
@Override
public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor fileDescriptor) {
@ -227,40 +211,76 @@ public class ContentProviderBackupComponent implements BackupComponent {
clearBackupState(false);
}
@Override
public int finishBackup() {
return clearBackupState(false);
}
private int clearBackupState(boolean closeFile) {
private void initializeBackupState() throws Exception {
if (backupState == null) {
return TRANSPORT_OK;
backupState = new ContentProviderBackupState();
}
try {
IoUtils.closeQuietly(backupState.getInputFileDescriptor());
backupState.setInputFileDescriptor(null);
if (backupState.getOutputStream() == null) {
initializeOutputStream();
ZipOutputStream outputStream = backupState.getOutputStream();
ZipEntry saltZipEntry = new ZipEntry(ContentProviderBackupConstants.SALT_FILE_PATH);
backupState.getOutputStream().putNextEntry(saltZipEntry);
backupState.getOutputStream().write(backupState.getSalt());
backupState.getOutputStream().closeEntry();
if (outputStream != null) {
outputStream.closeEntry();
if (configuration.getPassword() != null && !configuration.getPassword().isEmpty()) {
backupState.setSecretKey(KeyGenerator.generate(configuration.getPassword(), backupState.getSalt()));
}
}
}
if (backupState.getPackageIndex() == configuration.getPackageCount() || closeFile) {
if (outputStream != null) {
outputStream.finish();
outputStream.close();
private void initializeOutputStream() throws IOException {
ContentResolver contentResolver = configuration.getContext().getContentResolver();
ParcelFileDescriptor outputFileDescriptor = contentResolver.openFileDescriptor(configuration.getUri(), "w");
backupState.setOutputFileDescriptor(outputFileDescriptor);
FileOutputStream fileOutputStream = new FileOutputStream(outputFileDescriptor.getFileDescriptor());
ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream);
backupState.setOutputStream(zipOutputStream);
}
private int transferIncrementalBackupData(BackupDataInput backupDataInput) throws IOException {
ZipOutputStream outputStream = backupState.getOutputStream();
int bufferSize = INITIAL_BUFFER_SIZE;
byte[] buffer = new byte[bufferSize];
while (backupDataInput.readNextHeader()) {
String chunkFileName = Base64.encodeToString(backupDataInput.getKey().getBytes(), Base64.DEFAULT);
int dataSize = backupDataInput.getDataSize();
if (dataSize >= 0) {
ZipEntry zipEntry = new ZipEntry(configuration.getIncrementalBackupDirectory() +
backupState.getPackageName() + "/" + chunkFileName);
outputStream.putNextEntry(zipEntry);
if (dataSize > bufferSize) {
bufferSize = dataSize;
buffer = new byte[bufferSize];
}
IoUtils.closeQuietly(backupState.getOutputFileDescriptor());
backupState = null;
}
backupDataInput.readEntityData(buffer, 0, dataSize);
} catch (IOException ex) {
Log.e(TAG, "Error cancelling full backup: ", ex);
return TRANSPORT_ERROR;
try {
if (backupState.getSecretKey() != null) {
byte[] payload = Arrays.copyOfRange(buffer, 0, dataSize);
SecretKey secretKey = backupState.getSecretKey();
byte[] salt = backupState.getSalt();
outputStream.write(CipherUtil.encrypt(payload, secretKey, salt));
} else {
outputStream.write(buffer, 0, dataSize);
}
} catch (Exception ex) {
Log.e(TAG, "Error performing incremental backup for " + backupState.getPackageName() + ": ", ex);
clearBackupState(true);
return TRANSPORT_ERROR;
}
}
}
return TRANSPORT_OK;

View file

@ -14,6 +14,8 @@ public class ContentProviderBackupConfiguration {
private final Uri uri;
private final String password;
private final long backupSizeQuota;
private final Set<String> packages;
@ -22,30 +24,24 @@ public class ContentProviderBackupConfiguration {
private final String incrementalBackupDirectory;
ContentProviderBackupConfiguration(Context context, Uri uri, Set<String> packages, long backupSizeQuota,
String fullBackupDirectory, String incrementalBackupDirectory) {
ContentProviderBackupConfiguration(Context context, Uri uri, Set<String> packages, String password,
long backupSizeQuota, String fullBackupDirectory,
String incrementalBackupDirectory) {
this.context = context;
this.uri = uri;
this.packages = packages;
this.password = password;
this.backupSizeQuota = backupSizeQuota;
this.fullBackupDirectory = fullBackupDirectory;
this.incrementalBackupDirectory = incrementalBackupDirectory;
}
public Context getContext() {
return context;
}
public Uri getUri() {
return uri;
}
public long getBackupSizeQuota() {
return backupSizeQuota;
}
public int getPackageCount() {
return packages.size();
public Context getContext() {
return context;
}
public String getFullBackupDirectory() {
@ -55,4 +51,16 @@ public class ContentProviderBackupConfiguration {
public String getIncrementalBackupDirectory() {
return incrementalBackupDirectory;
}
public int getPackageCount() {
return packages.size();
}
public String getPassword() {
return password;
}
public Uri getUri() {
return uri;
}
}

View file

@ -21,6 +21,8 @@ public class ContentProviderBackupConfigurationBuilder {
private Set<String> packages;
private String password;
private long backupSizeQuota = Long.MAX_VALUE;
private String incrementalBackupDirectory = DEFAULT_INCREMENTAL_BACKUP_DIRECTORY;
@ -34,15 +36,30 @@ public class ContentProviderBackupConfigurationBuilder {
Preconditions.checkState(incrementalBackupDirectory != null, "Incremental backup directory must be set.");
Preconditions.checkState(fullBackupDirectory != null, "Full backup directory must be set.");
return new ContentProviderBackupConfiguration(context, outputUri, packages, backupSizeQuota,
return new ContentProviderBackupConfiguration(context, outputUri, packages, password, backupSizeQuota,
fullBackupDirectory, incrementalBackupDirectory);
}
public ContentProviderBackupConfigurationBuilder setBackupSizeQuota(long backupSizeQuota) {
this.backupSizeQuota = backupSizeQuota;
return this;
}
public ContentProviderBackupConfigurationBuilder setContext(Context context) {
this.context = context;
return this;
}
public ContentProviderBackupConfigurationBuilder setFullBackupDirectory(String fullBackupDirectory) {
this.fullBackupDirectory = fullBackupDirectory;
return this;
}
public ContentProviderBackupConfigurationBuilder setIncrementalBackupDirectory(String incrementalBackupDirectory) {
this.incrementalBackupDirectory = incrementalBackupDirectory;
return this;
}
public ContentProviderBackupConfigurationBuilder setOutputUri(Uri outputUri) {
this.outputUri = outputUri;
return this;
@ -53,18 +70,8 @@ public class ContentProviderBackupConfigurationBuilder {
return this;
}
public ContentProviderBackupConfigurationBuilder setBackupSizeQuota(long backupSizeQuota) {
this.backupSizeQuota = backupSizeQuota;
return this;
}
public ContentProviderBackupConfigurationBuilder setIncrementalBackupDirectory(String incrementalBackupDirectory) {
this.incrementalBackupDirectory = incrementalBackupDirectory;
return this;
}
public ContentProviderBackupConfigurationBuilder setFullBackupDirectory(String fullBackupDirectory) {
this.fullBackupDirectory = fullBackupDirectory;
public ContentProviderBackupConfigurationBuilder setPassword(String password) {
this.password = password;
return this;
}
}

View file

@ -0,0 +1,9 @@
package com.stevesoltys.backup.transport.component.provider;
/**
* @author Steve Soltys
*/
class ContentProviderBackupConstants {
static final String SALT_FILE_PATH = "salt";
}

View file

@ -2,7 +2,9 @@ package com.stevesoltys.backup.transport.component.provider;
import android.os.ParcelFileDescriptor;
import javax.crypto.SecretKey;
import java.io.InputStream;
import java.security.SecureRandom;
import java.util.zip.ZipOutputStream;
/**
@ -10,6 +12,8 @@ import java.util.zip.ZipOutputStream;
*/
class ContentProviderBackupState {
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private ParcelFileDescriptor inputFileDescriptor;
private ParcelFileDescriptor outputFileDescriptor;
@ -24,36 +28,13 @@ class ContentProviderBackupState {
private int packageIndex;
ParcelFileDescriptor getInputFileDescriptor() {
return inputFileDescriptor;
}
private byte[] salt;
void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) {
this.inputFileDescriptor = inputFileDescriptor;
}
private SecretKey secretKey;
ParcelFileDescriptor getOutputFileDescriptor() {
return outputFileDescriptor;
}
void setOutputFileDescriptor(ParcelFileDescriptor outputFileDescriptor) {
this.outputFileDescriptor = outputFileDescriptor;
}
InputStream getInputStream() {
return inputStream;
}
void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
ZipOutputStream getOutputStream() {
return outputStream;
}
void setOutputStream(ZipOutputStream outputStream) {
this.outputStream = outputStream;
public ContentProviderBackupState() {
salt = new byte[16];
SECURE_RANDOM.nextBytes(salt);
}
long getBytesTransferred() {
@ -64,12 +45,36 @@ class ContentProviderBackupState {
this.bytesTransferred = bytesTransferred;
}
String getPackageName() {
return packageName;
ParcelFileDescriptor getInputFileDescriptor() {
return inputFileDescriptor;
}
void setPackageName(String packageName) {
this.packageName = packageName;
void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) {
this.inputFileDescriptor = inputFileDescriptor;
}
InputStream getInputStream() {
return inputStream;
}
void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
ParcelFileDescriptor getOutputFileDescriptor() {
return outputFileDescriptor;
}
void setOutputFileDescriptor(ParcelFileDescriptor outputFileDescriptor) {
this.outputFileDescriptor = outputFileDescriptor;
}
ZipOutputStream getOutputStream() {
return outputStream;
}
void setOutputStream(ZipOutputStream outputStream) {
this.outputStream = outputStream;
}
int getPackageIndex() {
@ -79,4 +84,24 @@ class ContentProviderBackupState {
void setPackageIndex(int packageIndex) {
this.packageIndex = packageIndex;
}
String getPackageName() {
return packageName;
}
void setPackageName(String packageName) {
this.packageName = packageName;
}
byte[] getSalt() {
return salt;
}
public SecretKey getSecretKey() {
return secretKey;
}
public void setSecretKey(SecretKey secretKey) {
this.secretKey = secretKey;
}
}

View file

@ -9,13 +9,16 @@ import android.os.ParcelFileDescriptor;
import android.util.Base64;
import android.util.Log;
import com.android.internal.util.Preconditions;
import com.stevesoltys.backup.security.CipherUtil;
import com.stevesoltys.backup.security.KeyGenerator;
import com.stevesoltys.backup.transport.component.RestoreComponent;
import libcore.io.IoUtils;
import libcore.io.Streams;
import javax.crypto.SecretKey;
import java.io.*;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@ -48,6 +51,45 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
restoreState = new ContentProviderRestoreState();
restoreState.setPackages(packages);
restoreState.setPackageIndex(-1);
if (configuration.getPassword() != null && !configuration.getPassword().isEmpty()) {
try {
ParcelFileDescriptor inputFileDescriptor = buildInputFileDescriptor();
ZipInputStream inputStream = buildInputStream(inputFileDescriptor);
seekToEntry(inputStream, ContentProviderBackupConstants.SALT_FILE_PATH);
restoreState.setSalt(Streams.readFullyNoClose(inputStream));
restoreState.setSecretKey(KeyGenerator.generate(configuration.getPassword(), restoreState.getSalt()));
IoUtils.closeQuietly(inputFileDescriptor);
IoUtils.closeQuietly(inputStream);
} catch (Exception ex) {
Log.e(TAG, "Salt not found", ex);
}
}
try {
List<ZipEntry> zipEntries = new LinkedList<>();
ParcelFileDescriptor inputFileDescriptor = buildInputFileDescriptor();
ZipInputStream inputStream = buildInputStream(inputFileDescriptor);
ZipEntry zipEntry;
while ((zipEntry = inputStream.getNextEntry()) != null) {
zipEntries.add(zipEntry);
inputStream.closeEntry();
}
IoUtils.closeQuietly(inputFileDescriptor);
IoUtils.closeQuietly(inputStream);
restoreState.setZipEntries(zipEntries);
} catch (Exception ex) {
Log.e(TAG, "Error while caching zip entries", ex);
}
return TRANSPORT_OK;
}
@ -62,32 +104,66 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
restoreState.setPackageIndex(packageIndex);
String name = packages[packageIndex].packageName;
try {
if (containsPackageFile(configuration.getIncrementalBackupDirectory() + name)) {
restoreState.setRestoreType(TYPE_KEY_VALUE);
return new RestoreDescription(name, restoreState.getRestoreType());
if (containsPackageFile(configuration.getIncrementalBackupDirectory() + name)) {
restoreState.setRestoreType(TYPE_KEY_VALUE);
return new RestoreDescription(name, restoreState.getRestoreType());
} else if (containsPackageFile(configuration.getFullBackupDirectory() + name)) {
restoreState.setRestoreType(TYPE_FULL_STREAM);
return new RestoreDescription(name, restoreState.getRestoreType());
}
} catch (IOException | InvalidKeyException | InvalidAlgorithmParameterException ex) {
Log.e(TAG, "Error choosing package " + name + " at index " + packageIndex + "failed selection:", ex);
} else if (containsPackageFile(configuration.getFullBackupDirectory() + name)) {
restoreState.setRestoreType(TYPE_FULL_STREAM);
return new RestoreDescription(name, restoreState.getRestoreType());
}
}
return RestoreDescription.NO_MORE_PACKAGES;
}
private boolean containsPackageFile(String fileName) throws IOException, InvalidKeyException,
InvalidAlgorithmParameterException {
private boolean containsPackageFile(String fileName) {
return restoreState.getZipEntries().stream()
.anyMatch(zipEntry -> zipEntry.getName().startsWith(fileName));
}
@Override
public int getRestoreData(ParcelFileDescriptor outputFileDescriptor) {
Preconditions.checkState(restoreState != null, "startRestore() not called");
Preconditions.checkState(restoreState.getPackageIndex() >= 0, "nextRestorePackage() not called");
Preconditions.checkState(restoreState.getRestoreType() == TYPE_KEY_VALUE,
"getRestoreData() for non-key/value dataset");
PackageInfo packageInfo = restoreState.getPackages()[restoreState.getPackageIndex()];
try {
return transferIncrementalRestoreData(packageInfo.packageName, outputFileDescriptor);
} catch (Exception ex) {
Log.e(TAG, "Unable to read backup records: ", ex);
return TRANSPORT_ERROR;
}
}
private int transferIncrementalRestoreData(String packageName, ParcelFileDescriptor outputFileDescriptor)
throws Exception {
ParcelFileDescriptor inputFileDescriptor = buildInputFileDescriptor();
ZipInputStream inputStream = buildInputStream(inputFileDescriptor);
BackupDataOutput backupDataOutput = new BackupDataOutput(outputFileDescriptor.getFileDescriptor());
Optional<ZipEntry> zipEntryOptional = seekToEntry(inputStream,
configuration.getIncrementalBackupDirectory() + packageName);
while (zipEntryOptional.isPresent()) {
String fileName = new File(zipEntryOptional.get().getName()).getName();
String blobKey = new String(Base64.decode(fileName, Base64.DEFAULT));
byte[] backupData = readBackupData(inputStream);
backupDataOutput.writeEntityHeader(blobKey, backupData.length);
backupDataOutput.writeEntityData(backupData, backupData.length);
inputStream.closeEntry();
zipEntryOptional = seekToEntry(inputStream, configuration.getIncrementalBackupDirectory() + packageName);
}
Optional<ZipEntry> zipEntry = seekToEntry(inputStream, fileName);
IoUtils.closeQuietly(inputFileDescriptor);
IoUtils.closeQuietly(inputStream);
return zipEntry.isPresent();
IoUtils.closeQuietly(outputFileDescriptor);
return TRANSPORT_OK;
}
private ParcelFileDescriptor buildInputFileDescriptor() throws FileNotFoundException {
@ -113,48 +189,16 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
return Optional.empty();
}
@Override
public int getRestoreData(ParcelFileDescriptor outputFileDescriptor) {
Preconditions.checkState(restoreState != null, "startRestore() not called");
Preconditions.checkState(restoreState.getPackageIndex() >= 0, "nextRestorePackage() not called");
Preconditions.checkState(restoreState.getRestoreType() == TYPE_KEY_VALUE,
"getRestoreData() for non-key/value dataset");
private byte[] readBackupData(ZipInputStream inputStream) throws Exception {
byte[] backupData = Streams.readFullyNoClose(inputStream);
SecretKey secretKey = restoreState.getSecretKey();
byte[] initializationVector = restoreState.getSalt();
PackageInfo packageInfo = restoreState.getPackages()[restoreState.getPackageIndex()];
try {
return transferIncrementalRestoreData(packageInfo.packageName, outputFileDescriptor);
} catch (Exception ex) {
Log.e(TAG, "Unable to read backup records: ", ex);
return TRANSPORT_ERROR;
}
}
private int transferIncrementalRestoreData(String packageName, ParcelFileDescriptor outputFileDescriptor)
throws IOException, InvalidAlgorithmParameterException, InvalidKeyException {
ParcelFileDescriptor inputFileDescriptor = buildInputFileDescriptor();
ZipInputStream inputStream = buildInputStream(inputFileDescriptor);
BackupDataOutput backupDataOutput = new BackupDataOutput(outputFileDescriptor.getFileDescriptor());
Optional<ZipEntry> zipEntryOptional = seekToEntry(inputStream,
configuration.getIncrementalBackupDirectory() + packageName);
while (zipEntryOptional.isPresent()) {
String fileName = new File(zipEntryOptional.get().getName()).getName();
String blobKey = new String(Base64.decode(fileName, Base64.DEFAULT));
byte[] backupData = Streams.readFullyNoClose(inputStream);
backupDataOutput.writeEntityHeader(blobKey, backupData.length);
backupDataOutput.writeEntityData(backupData, backupData.length);
inputStream.closeEntry();
zipEntryOptional = seekToEntry(inputStream, configuration.getIncrementalBackupDirectory() + packageName);
if (secretKey != null) {
backupData = CipherUtil.decrypt(backupData, secretKey, initializationVector);
}
IoUtils.closeQuietly(inputFileDescriptor);
IoUtils.closeQuietly(outputFileDescriptor);
return TRANSPORT_OK;
return backupData;
}
@Override

View file

@ -3,6 +3,9 @@ package com.stevesoltys.backup.transport.component.provider;
import android.content.pm.PackageInfo;
import android.os.ParcelFileDescriptor;
import javax.crypto.SecretKey;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
@ -20,28 +23,18 @@ class ContentProviderRestoreState {
private ZipInputStream inputStream;
PackageInfo[] getPackages() {
return packages;
private byte[] salt;
private SecretKey secretKey;
private List<ZipEntry> zipEntries;
ParcelFileDescriptor getInputFileDescriptor() {
return inputFileDescriptor;
}
void setPackages(PackageInfo[] packages) {
this.packages = packages;
}
int getPackageIndex() {
return packageIndex;
}
void setPackageIndex(int packageIndex) {
this.packageIndex = packageIndex;
}
int getRestoreType() {
return restoreType;
}
void setRestoreType(int restoreType) {
this.restoreType = restoreType;
void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) {
this.inputFileDescriptor = inputFileDescriptor;
}
ZipInputStream getInputStream() {
@ -52,11 +45,51 @@ class ContentProviderRestoreState {
this.inputStream = inputStream;
}
ParcelFileDescriptor getInputFileDescriptor() {
return inputFileDescriptor;
int getPackageIndex() {
return packageIndex;
}
void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) {
this.inputFileDescriptor = inputFileDescriptor;
void setPackageIndex(int packageIndex) {
this.packageIndex = packageIndex;
}
PackageInfo[] getPackages() {
return packages;
}
void setPackages(PackageInfo[] packages) {
this.packages = packages;
}
int getRestoreType() {
return restoreType;
}
void setRestoreType(int restoreType) {
this.restoreType = restoreType;
}
byte[] getSalt() {
return salt;
}
void setSalt(byte[] salt) {
this.salt = salt;
}
public SecretKey getSecretKey() {
return secretKey;
}
public void setSecretKey(SecretKey secretKey) {
this.secretKey = secretKey;
}
public List<ZipEntry> getZipEntries() {
return zipEntries;
}
public void setZipEntries(List<ZipEntry> zipEntries) {
this.zipEntries = zipEntries;
}
}

View file

@ -19,5 +19,8 @@
<string name="popup_cancel">Cancel</string>
<string name="select_all">Select all</string>
<string name="loading_backup">Loading backup…</string>
<string name="loading_packages">Loading packages…</string>
<string name="initializing">Initializing…</string>
</resources>