Fix transport encryption

Prior to this commit, some of the application data was not included during encryption. This is a breaking change, any backups made prior to this commit can no longer be restored.

1. Encrypt 'full' backup data.
2. Increase number of key generation iterations to 32767.
3. Change cipher to 'AES/CBC/PKCS5Padding'.
This commit is contained in:
Steve Soltys 2019-03-14 20:09:06 -04:00
parent b16fcf5d87
commit 04543a1014
7 changed files with 254 additions and 168 deletions

View file

@ -16,9 +16,10 @@ public class CipherUtil {
/**
* The cipher algorithm.
*/
public static final String CIPHER_ALGORITHM = "AES/CFB/PKCS5Padding";
public static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
/**.
/**
* .
* Encrypts the given payload using the provided secret key.
*
* @param payload The payload.
@ -29,9 +30,22 @@ public class CipherUtil {
NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException,
InvalidAlgorithmParameterException, InvalidKeyException {
return startEncrypt(secretKey, iv).doFinal(payload);
}
/**
* Initializes a cipher in {@link Cipher#ENCRYPT_MODE}.
*
* @param secretKey The secret key.
* @param iv The initialization vector.
* @return The initialized cipher.
*/
public static Cipher startEncrypt(SecretKey secretKey, byte[] iv) throws NoSuchPaddingException,
NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
return cipher.doFinal(payload);
return cipher;
}
/**
@ -45,8 +59,21 @@ public class CipherUtil {
NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException,
InvalidAlgorithmParameterException, InvalidKeyException {
return startDecrypt(secretKey, iv).doFinal(payload);
}
/**
* Initializes a cipher in {@link Cipher#DECRYPT_MODE}.
*
* @param secretKey The secret key.
* @param iv The initialization vector.
* @return The initialized cipher.
*/
public static Cipher startDecrypt(SecretKey secretKey, byte[] iv) throws NoSuchPaddingException,
NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
return cipher.doFinal(payload);
return cipher;
}
}

View file

@ -18,7 +18,7 @@ public class KeyGenerator {
/**
* The number of iterations for key generation.
*/
private static final int ITERATIONS = 25;
private static final int ITERATIONS = 32767;
/**
* The generated key length.

View file

@ -12,6 +12,7 @@ import com.stevesoltys.backup.transport.component.BackupComponent;
import libcore.io.IoUtils;
import org.apache.commons.io.IOUtils;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@ -44,6 +45,30 @@ public class ContentProviderBackupComponent implements BackupComponent {
this.configuration = configuration;
}
@Override
public void cancelFullBackup() {
clearBackupState(false);
}
@Override
public int checkFullBackupSize(long size) {
int result = TRANSPORT_OK;
if (size <= 0) {
result = TRANSPORT_PACKAGE_REJECTED;
} else if (size > configuration.getBackupSizeQuota()) {
result = TRANSPORT_QUOTA_EXCEEDED;
}
return result;
}
@Override
public int clearBackupData(PackageInfo packageInfo) {
return TRANSPORT_OK;
}
@Override
public String currentDestinationString() {
return DESTINATION_DESCRIPTION;
@ -54,85 +79,19 @@ public class ContentProviderBackupComponent implements BackupComponent {
return TRANSPORT_DATA_MANAGEMENT_LABEL;
}
@Override
public int initializeDevice() {
return TRANSPORT_OK;
}
@Override
public int clearBackupData(PackageInfo packageInfo) {
return TRANSPORT_OK;
}
@Override
public int finishBackup() {
return clearBackupState(false);
}
@Override
public int performIncrementalBackup(PackageInfo packageInfo, ParcelFileDescriptor data) {
BackupDataInput backupDataInput = new BackupDataInput(data.getFileDescriptor());
try {
initializeBackupState();
backupState.setPackageIndex(backupState.getPackageIndex() + 1);
backupState.setPackageName(packageInfo.packageName);
return transferIncrementalBackupData(backupDataInput);
} catch (Exception ex) {
Log.e(TAG, "Error reading backup input: ", ex);
return TRANSPORT_ERROR;
}
}
private int clearBackupState(boolean closeFile) {
if (backupState == null) {
return TRANSPORT_OK;
}
try {
IoUtils.closeQuietly(backupState.getInputFileDescriptor());
backupState.setInputFileDescriptor(null);
ZipOutputStream outputStream = backupState.getOutputStream();
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;
public int initializeDevice() {
return TRANSPORT_OK;
}
@Override
@ -152,6 +111,9 @@ public class ContentProviderBackupComponent implements BackupComponent {
backupState.setInputStream(new FileInputStream(fileDescriptor.getFileDescriptor()));
backupState.setBytesTransferred(0);
Cipher cipher = CipherUtil.startEncrypt(backupState.getSecretKey(), backupState.getSalt());
backupState.setCipher(cipher);
ZipEntry zipEntry = new ZipEntry(configuration.getFullBackupDirectory() + backupState.getPackageName());
backupState.getOutputStream().putNextEntry(zipEntry);
@ -165,17 +127,30 @@ public class ContentProviderBackupComponent implements BackupComponent {
}
@Override
public int checkFullBackupSize(long size) {
int result = TRANSPORT_OK;
public int performIncrementalBackup(PackageInfo packageInfo, ParcelFileDescriptor data) {
BackupDataInput backupDataInput = new BackupDataInput(data.getFileDescriptor());
if (size <= 0) {
result = TRANSPORT_PACKAGE_REJECTED;
try {
initializeBackupState();
backupState.setPackageIndex(backupState.getPackageIndex() + 1);
backupState.setPackageName(packageInfo.packageName);
} else if (size > configuration.getBackupSizeQuota()) {
result = TRANSPORT_QUOTA_EXCEEDED;
return transferIncrementalBackupData(backupDataInput);
} catch (Exception ex) {
Log.e(TAG, "Error reading backup input: ", ex);
return TRANSPORT_ERROR;
}
}
return result;
@Override
public long requestBackupTime() {
return 0;
}
@Override
public long requestFullBackupTime() {
return 0;
}
@Override
@ -196,51 +171,22 @@ public class ContentProviderBackupComponent implements BackupComponent {
ZipOutputStream outputStream = backupState.getOutputStream();
try {
outputStream.write(IOUtils.readFully(inputStream, numBytes));
byte[] payload = IOUtils.readFully(inputStream, numBytes);
if (backupState.getCipher() != null) {
payload = backupState.getCipher().update(payload);
}
outputStream.write(payload, 0, numBytes);
backupState.setBytesTransferred(bytesTransferred);
} catch (IOException ex) {
} catch (Exception ex) {
Log.e(TAG, "Error handling backup data for " + backupState.getPackageName() + ": ", ex);
return TRANSPORT_ERROR;
}
return TRANSPORT_OK;
}
@Override
public void cancelFullBackup() {
clearBackupState(false);
}
private void initializeBackupState() throws Exception {
if (backupState == null) {
backupState = new ContentProviderBackupState();
}
if (backupState.getOutputStream() == null) {
initializeOutputStream();
ZipEntry saltZipEntry = new ZipEntry(ContentProviderBackupConstants.SALT_FILE_PATH);
backupState.getOutputStream().putNextEntry(saltZipEntry);
backupState.getOutputStream().write(backupState.getSalt());
backupState.getOutputStream().closeEntry();
if (configuration.getPassword() != null && !configuration.getPassword().isEmpty()) {
backupState.setSecretKey(KeyGenerator.generate(configuration.getPassword(), backupState.getSalt()));
}
}
}
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();
@ -285,4 +231,73 @@ public class ContentProviderBackupComponent implements BackupComponent {
return TRANSPORT_OK;
}
private void initializeBackupState() throws Exception {
if (backupState == null) {
backupState = new ContentProviderBackupState();
}
if (backupState.getOutputStream() == null) {
initializeOutputStream();
ZipEntry saltZipEntry = new ZipEntry(ContentProviderBackupConstants.SALT_FILE_PATH);
backupState.getOutputStream().putNextEntry(saltZipEntry);
backupState.getOutputStream().write(backupState.getSalt());
backupState.getOutputStream().closeEntry();
if (configuration.getPassword() != null && !configuration.getPassword().isEmpty()) {
backupState.setSecretKey(KeyGenerator.generate(configuration.getPassword(), backupState.getSalt()));
}
}
}
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 clearBackupState(boolean closeFile) {
if (backupState == null) {
return TRANSPORT_OK;
}
try {
IoUtils.closeQuietly(backupState.getInputFileDescriptor());
backupState.setInputFileDescriptor(null);
ZipOutputStream outputStream = backupState.getOutputStream();
if (outputStream != null) {
if (backupState.getCipher() != null) {
outputStream.write(backupState.getCipher().doFinal());
backupState.setCipher(null);
}
outputStream.closeEntry();
}
if (backupState.getPackageIndex() == configuration.getPackageCount() || closeFile) {
if (outputStream != null) {
outputStream.finish();
outputStream.close();
}
IoUtils.closeQuietly(backupState.getOutputFileDescriptor());
backupState = null;
}
} catch (Exception ex) {
Log.e(TAG, "Error cancelling full backup: ", ex);
return TRANSPORT_ERROR;
}
return TRANSPORT_OK;
}
}

View file

@ -2,6 +2,7 @@ package com.stevesoltys.backup.transport.component.provider;
import android.os.ParcelFileDescriptor;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import java.io.InputStream;
import java.security.SecureRandom;
@ -22,6 +23,8 @@ class ContentProviderBackupState {
private ZipOutputStream outputStream;
private Cipher cipher;
private long bytesTransferred;
private String packageName;
@ -45,6 +48,14 @@ class ContentProviderBackupState {
this.bytesTransferred = bytesTransferred;
}
Cipher getCipher() {
return cipher;
}
void setCipher(Cipher cipher) {
this.cipher = cipher;
}
ParcelFileDescriptor getInputFileDescriptor() {
return inputFileDescriptor;
}

View file

@ -17,6 +17,7 @@ import libcore.io.Streams;
import javax.crypto.SecretKey;
import java.io.*;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
@ -166,29 +167,6 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
return TRANSPORT_OK;
}
private ParcelFileDescriptor buildInputFileDescriptor() throws FileNotFoundException {
ContentResolver contentResolver = configuration.getContext().getContentResolver();
return contentResolver.openFileDescriptor(configuration.getUri(), "r");
}
private ZipInputStream buildInputStream(ParcelFileDescriptor inputFileDescriptor) throws FileNotFoundException {
FileInputStream fileInputStream = new FileInputStream(inputFileDescriptor.getFileDescriptor());
return new ZipInputStream(fileInputStream);
}
private Optional<ZipEntry> seekToEntry(ZipInputStream inputStream, String entryPath) throws IOException {
ZipEntry zipEntry;
while ((zipEntry = inputStream.getNextEntry()) != null) {
if (zipEntry.getName().startsWith(entryPath)) {
return Optional.of(zipEntry);
}
inputStream.closeEntry();
}
return Optional.empty();
}
private byte[] readBackupData(ZipInputStream inputStream) throws Exception {
byte[] backupData = Streams.readFullyNoClose(inputStream);
SecretKey secretKey = restoreState.getSecretKey();
@ -248,7 +226,28 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
if (bytesRead <= 0) {
bytesRead = NO_MORE_DATA;
if (restoreState.getCipher() != null) {
buffer = restoreState.getCipher().doFinal();
bytesRead = buffer.length;
outputStream.write(buffer, 0, bytesRead);
restoreState.setCipher(null);
}
} else {
if (restoreState.getSecretKey() != null) {
SecretKey secretKey = restoreState.getSecretKey();
byte[] salt = restoreState.getSalt();
if (restoreState.getCipher() == null) {
restoreState.setCipher(CipherUtil.startDecrypt(secretKey, salt));
}
buffer = restoreState.getCipher().update(Arrays.copyOfRange(buffer, 0, bytesRead));
bytesRead = buffer.length;
}
outputStream.write(buffer, 0, bytesRead);
}
@ -305,4 +304,27 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
IoUtils.closeQuietly(restoreState.getInputFileDescriptor());
restoreState = null;
}
private ParcelFileDescriptor buildInputFileDescriptor() throws FileNotFoundException {
ContentResolver contentResolver = configuration.getContext().getContentResolver();
return contentResolver.openFileDescriptor(configuration.getUri(), "r");
}
private ZipInputStream buildInputStream(ParcelFileDescriptor inputFileDescriptor) throws FileNotFoundException {
FileInputStream fileInputStream = new FileInputStream(inputFileDescriptor.getFileDescriptor());
return new ZipInputStream(fileInputStream);
}
private Optional<ZipEntry> seekToEntry(ZipInputStream inputStream, String entryPath) throws IOException {
ZipEntry zipEntry;
while ((zipEntry = inputStream.getNextEntry()) != null) {
if (zipEntry.getName().startsWith(entryPath)) {
return Optional.of(zipEntry);
}
inputStream.closeEntry();
}
return Optional.empty();
}
}

View file

@ -3,6 +3,7 @@ package com.stevesoltys.backup.transport.component.provider;
import android.content.pm.PackageInfo;
import android.os.ParcelFileDescriptor;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import java.util.List;
import java.util.zip.ZipEntry;
@ -23,16 +24,26 @@ class ContentProviderRestoreState {
private ZipInputStream inputStream;
private Cipher cipher;
private byte[] salt;
private SecretKey secretKey;
private List<ZipEntry> zipEntries;
Cipher getCipher() {
return cipher;
}
ParcelFileDescriptor getInputFileDescriptor() {
return inputFileDescriptor;
}
void setCipher(Cipher cipher) {
this.cipher = cipher;
}
void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) {
this.inputFileDescriptor = inputFileDescriptor;
}

View file

@ -9,6 +9,21 @@ import com.stevesoltys.backup.transport.component.BackupComponent;
*/
public class StubBackupComponent implements BackupComponent {
@Override
public void cancelFullBackup() {
}
@Override
public int checkFullBackupSize(long size) {
return 0;
}
@Override
public int clearBackupData(PackageInfo packageInfo) {
return 0;
}
@Override
public String currentDestinationString() {
return null;
@ -19,23 +34,18 @@ public class StubBackupComponent implements BackupComponent {
return null;
}
@Override
public int initializeDevice() {
return 0;
}
@Override
public int clearBackupData(PackageInfo packageInfo) {
return 0;
}
@Override
public int finishBackup() {
return 0;
}
@Override
public int performIncrementalBackup(PackageInfo targetPackage, ParcelFileDescriptor data) {
public long getBackupQuota(String packageName, boolean fullBackup) {
return 0;
}
@Override
public int initializeDevice() {
return 0;
}
@ -45,22 +55,7 @@ public class StubBackupComponent implements BackupComponent {
}
@Override
public int checkFullBackupSize(long size) {
return 0;
}
@Override
public int sendBackupData(int numBytes) {
return 0;
}
@Override
public void cancelFullBackup() {
}
@Override
public long getBackupQuota(String packageName, boolean fullBackup) {
public int performIncrementalBackup(PackageInfo targetPackage, ParcelFileDescriptor data) {
return 0;
}
@ -73,4 +68,9 @@ public class StubBackupComponent implements BackupComponent {
public long requestFullBackupTime() {
return 0;
}
@Override
public int sendBackupData(int numBytes) {
return 0;
}
}