Add support for encrypted backups

1. Add prompt for entering password during backup and restore.
2. Use PBKDF2 to generate a secret key that is used to encrypt backups.
3. Store salt in backup zip file.
4. Fetch salt from backup zip file during restore and use it to decrypt restoration data.
This commit is contained in:
Steve Soltys 2019-02-15 02:46:24 -05:00
parent 8ec018442a
commit b182e743e8
12 changed files with 420 additions and 197 deletions

View file

@ -32,7 +32,7 @@ public class CreateBackupActivity extends Activity implements View.OnClickListen
switch (viewId) {
case R.id.create_confirm_button:
controller.backupPackages(selectedPackageList, contentUri, this);
controller.showEnterPasswordAlert(selectedPackageList, contentUri, this);
break;
}
}

View file

@ -1,20 +1,19 @@
package com.stevesoltys.backup.activity.backup;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
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.stevesoltys.backup.R;
import com.stevesoltys.backup.session.BackupManagerController;
import com.stevesoltys.backup.session.backup.BackupSession;
@ -27,7 +26,10 @@ import com.stevesoltys.backup.transport.component.provider.ContentProviderBackup
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConfigurationBuilder;
import com.stevesoltys.backup.transport.component.provider.ContentProviderRestoreComponent;
import java.util.*;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* @author Steve Soltys
@ -61,12 +63,64 @@ class CreateBackupActivityController {
packageListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
}
void backupPackages(Set<String> selectedPackages, Uri contentUri, Activity parent) {
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);
new AlertDialog.Builder(parent)
.setMessage("Please enter a password.\n" +
"You'll need this to restore your backup, so write it down!")
.setView(passwordTextView)
.setPositiveButton("Set password", (dialog, button) ->
showConfirmPasswordAlert(selectedPackages, contentUri, parent,
passwordTextView.getText().toString()))
.setNegativeButton("Cancel", (dialog, button) -> dialog.cancel())
.show();
}
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);
new AlertDialog.Builder(parent)
.setMessage("Please confirm your password.")
.setView(passwordTextView)
.setPositiveButton("Confirm", (dialog, button) -> {
String password = passwordTextView.getText().toString();
if (originalPassword.equals(password)) {
backupPackages(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();
}
private void backupPackages(Set<String> selectedPackages, Uri contentUri, Activity parent,
String selectedPassword) {
try {
selectedPackages.add("@pm@");
ContentProviderBackupConfiguration backupConfiguration = new ContentProviderBackupConfigurationBuilder()
.setContext(parent).setOutputUri(contentUri).setPackages(selectedPackages).build();
.setContext(parent)
.setOutputUri(contentUri)
.setPackages(selectedPackages)
.setPassword(selectedPassword)
.build();
boolean success = initializeBackupTransport(backupConfiguration);
if (!success) {
@ -74,7 +128,7 @@ class CreateBackupActivityController {
return;
}
PopupWindow popupWindow = buildPopupWindow(parent);
PopupWindow popupWindow = buildStatusPopupWindow(parent);
BackupObserver backupObserver = new BackupObserver(parent, popupWindow);
BackupSession backupSession = backupManager.backup(backupObserver, selectedPackages);
@ -102,7 +156,7 @@ class CreateBackupActivityController {
return true;
}
private PopupWindow buildPopupWindow(Activity parent) {
private PopupWindow buildStatusPopupWindow(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);

View file

@ -32,7 +32,7 @@ public class RestoreBackupActivity extends Activity implements View.OnClickListe
switch (viewId) {
case R.id.restore_confirm_button:
controller.restorePackages(selectedPackageList, contentUri, this);
controller.showEnterPasswordAlert(selectedPackageList, contentUri, this);
break;
}
}

View file

@ -1,21 +1,20 @@
package com.stevesoltys.backup.activity.restore;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
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;
@ -89,10 +88,31 @@ class RestoreBackupActivityController {
return results;
}
void restorePackages(Set<String> selectedPackages, Uri contentUri, Activity parent) {
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);
new AlertDialog.Builder(parent)
.setMessage("Please enter a password.\n" +
"If you didn't enter one while creating the backup, you can leave this blank.")
.setView(passwordTextView)
.setPositiveButton("Confirm", (dialog, button) ->
restorePackages(selectedPackages, contentUri, parent, passwordTextView.getText().toString()))
.setNegativeButton("Cancel", (dialog, button) -> dialog.cancel())
.show();
}
private void restorePackages(Set<String> selectedPackages, Uri contentUri, Activity parent, String password) {
try {
ContentProviderBackupConfiguration backupConfiguration = new ContentProviderBackupConfigurationBuilder().
setContext(parent).setOutputUri(contentUri).setPackages(selectedPackages).build();
setContext(parent)
.setOutputUri(contentUri)
.setPackages(selectedPackages)
.setPassword(password)
.build();
boolean success = initializeBackupTransport(backupConfiguration);
if(!success) {

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 = 1000;
/**
* 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

@ -6,15 +6,20 @@ import android.content.pm.PackageInfo;
import android.os.ParcelFileDescriptor;
import android.util.Base64;
import android.util.Log;
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.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@ -41,11 +46,6 @@ public class ContentProviderBackupComponent implements BackupComponent {
this.configuration = configuration;
}
@Override
public long requestBackupTime() {
return 0;
}
@Override
public String currentDestinationString() {
return DESTINATION_DESCRIPTION;
@ -67,33 +67,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 +88,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 +213,74 @@ public class ContentProviderBackupComponent implements BackupComponent {
clearBackupState(false);
}
@Override
public int finishBackup() {
return clearBackupState(false);
}
private int clearBackupState(boolean closeFile) {
private void initializeBackupState() throws IOException {
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();
private void initializeOutputStream() throws IOException {
ContentResolver contentResolver = configuration.getContext().getContentResolver();
ParcelFileDescriptor outputFileDescriptor = contentResolver.openFileDescriptor(configuration.getUri(), "w");
backupState.setOutputFileDescriptor(outputFileDescriptor);
if (outputStream != null) {
outputStream.closeEntry();
}
FileOutputStream fileOutputStream = new FileOutputStream(outputFileDescriptor.getFileDescriptor());
ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream);
backupState.setOutputStream(zipOutputStream);
if (backupState.getPackageIndex() == configuration.getPackageCount() || closeFile) {
if (outputStream != null) {
outputStream.finish();
outputStream.close();
ZipEntry saltZipEntry = new ZipEntry(ContentProviderBackupConstants.SALT_FILE_PATH);
zipOutputStream.putNextEntry(saltZipEntry);
zipOutputStream.write(backupState.getSalt());
zipOutputStream.closeEntry();
}
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 (configuration.getPassword() != null && !configuration.getPassword().isEmpty()) {
SecretKey secretKey = KeyGenerator.generate(configuration.getPassword(), backupState.getSalt());
Cipher cipher = Cipher.getInstance(ContentProviderBackupConstants.CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(backupState.getSalt()));
byte[] payload = Arrays.copyOfRange(buffer, 0, dataSize);
byte[] encryptedBuffer = cipher.doFinal(payload);
outputStream.write(encryptedBuffer);
} 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,11 @@
package com.stevesoltys.backup.transport.component.provider;
/**
* @author Steve Soltys
*/
public class ContentProviderBackupConstants {
static final String CIPHER_ALGORITHM = "AES/CFB/PKCS5Padding";
static final String SALT_FILE_PATH = "salt";
}

View file

@ -3,6 +3,7 @@ package com.stevesoltys.backup.transport.component.provider;
import android.os.ParcelFileDescriptor;
import java.io.InputStream;
import java.security.SecureRandom;
import java.util.zip.ZipOutputStream;
/**
@ -10,6 +11,8 @@ import java.util.zip.ZipOutputStream;
*/
class ContentProviderBackupState {
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private ParcelFileDescriptor inputFileDescriptor;
private ParcelFileDescriptor outputFileDescriptor;
@ -24,36 +27,11 @@ class ContentProviderBackupState {
private int packageIndex;
ParcelFileDescriptor getInputFileDescriptor() {
return inputFileDescriptor;
}
private byte[] salt;
void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) {
this.inputFileDescriptor = inputFileDescriptor;
}
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 +42,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 +81,16 @@ class ContentProviderBackupState {
void setPackageIndex(int packageIndex) {
this.packageIndex = packageIndex;
}
String getPackageName() {
return packageName;
}
void setPackageName(String packageName) {
this.packageName = packageName;
}
byte[] getSalt() {
return salt;
}
}

View file

@ -9,13 +9,15 @@ import android.os.ParcelFileDescriptor;
import android.util.Base64;
import android.util.Log;
import com.android.internal.util.Preconditions;
import com.stevesoltys.backup.security.KeyGenerator;
import com.stevesoltys.backup.transport.component.RestoreComponent;
import libcore.io.IoUtils;
import libcore.io.Streams;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import java.io.*;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.util.Optional;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@ -48,6 +50,23 @@ 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));
IoUtils.closeQuietly(inputFileDescriptor);
IoUtils.closeQuietly(inputStream);
} catch (IOException ex) {
Log.e(TAG, "Salt not found", ex);
}
}
return TRANSPORT_OK;
}
@ -72,15 +91,14 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
return new RestoreDescription(name, restoreState.getRestoreType());
}
} catch (IOException | InvalidKeyException | InvalidAlgorithmParameterException ex) {
} catch (IOException ex) {
Log.e(TAG, "Error choosing package " + name + " at index " + packageIndex + "failed selection:", ex);
}
}
return RestoreDescription.NO_MORE_PACKAGES;
}
private boolean containsPackageFile(String fileName) throws IOException, InvalidKeyException,
InvalidAlgorithmParameterException {
private boolean containsPackageFile(String fileName) throws IOException {
ParcelFileDescriptor inputFileDescriptor = buildInputFileDescriptor();
ZipInputStream inputStream = buildInputStream(inputFileDescriptor);
@ -132,7 +150,7 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
}
private int transferIncrementalRestoreData(String packageName, ParcelFileDescriptor outputFileDescriptor)
throws IOException, InvalidAlgorithmParameterException, InvalidKeyException {
throws Exception {
ParcelFileDescriptor inputFileDescriptor = buildInputFileDescriptor();
ZipInputStream inputStream = buildInputStream(inputFileDescriptor);
@ -140,11 +158,12 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
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);
byte[] backupData = readBackupData(inputStream);
backupDataOutput.writeEntityHeader(blobKey, backupData.length);
backupDataOutput.writeEntityData(backupData, backupData.length);
inputStream.closeEntry();
@ -157,6 +176,22 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
return TRANSPORT_OK;
}
private byte[] readBackupData(ZipInputStream inputStream) throws Exception {
byte[] backupData = Streams.readFullyNoClose(inputStream);
if (configuration.getPassword() != null && !configuration.getPassword().isEmpty() &&
restoreState.getSalt() != null) {
SecretKey secretKey = KeyGenerator.generate(configuration.getPassword(), restoreState.getSalt());
Cipher cipher = Cipher.getInstance(ContentProviderBackupConstants.CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(restoreState.getSalt()));
backupData = cipher.doFinal(backupData);
}
return backupData;
}
@Override
public int getNextFullRestoreDataChunk(ParcelFileDescriptor outputFileDescriptor) {
Preconditions.checkState(restoreState.getRestoreType() == TYPE_FULL_STREAM,

View file

@ -20,28 +20,14 @@ class ContentProviderRestoreState {
private ZipInputStream inputStream;
PackageInfo[] getPackages() {
return packages;
private byte[] salt;
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 +38,35 @@ 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;
}
}