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,12 +16,13 @@ public class CipherUtil {
/** /**
* The cipher algorithm. * 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. * Encrypts the given payload using the provided secret key.
* *
* @param payload The payload. * @param payload The payload.
* @param secretKey The secret key. * @param secretKey The secret key.
* @param iv The initialization vector. * @param iv The initialization vector.
*/ */
@ -29,9 +30,22 @@ public class CipherUtil {
NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException,
InvalidAlgorithmParameterException, InvalidKeyException { 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 cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv)); 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, NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException,
InvalidAlgorithmParameterException, InvalidKeyException { 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 cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv)); 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. * The number of iterations for key generation.
*/ */
private static final int ITERATIONS = 25; private static final int ITERATIONS = 32767;
/** /**
* The generated key length. * The generated key length.

View file

@ -12,6 +12,7 @@ import com.stevesoltys.backup.transport.component.BackupComponent;
import libcore.io.IoUtils; import libcore.io.IoUtils;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import javax.crypto.Cipher;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -44,6 +45,30 @@ public class ContentProviderBackupComponent implements BackupComponent {
this.configuration = configuration; 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 @Override
public String currentDestinationString() { public String currentDestinationString() {
return DESTINATION_DESCRIPTION; return DESTINATION_DESCRIPTION;
@ -54,85 +79,19 @@ public class ContentProviderBackupComponent implements BackupComponent {
return TRANSPORT_DATA_MANAGEMENT_LABEL; return TRANSPORT_DATA_MANAGEMENT_LABEL;
} }
@Override
public int initializeDevice() {
return TRANSPORT_OK;
}
@Override
public int clearBackupData(PackageInfo packageInfo) {
return TRANSPORT_OK;
}
@Override @Override
public int finishBackup() { public int finishBackup() {
return clearBackupState(false); 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 @Override
public long getBackupQuota(String packageName, boolean fullBackup) { public long getBackupQuota(String packageName, boolean fullBackup) {
return configuration.getBackupSizeQuota(); return configuration.getBackupSizeQuota();
} }
@Override @Override
public long requestBackupTime() { public int initializeDevice() {
return 0; return TRANSPORT_OK;
}
@Override
public long requestFullBackupTime() {
return 0;
} }
@Override @Override
@ -152,6 +111,9 @@ public class ContentProviderBackupComponent implements BackupComponent {
backupState.setInputStream(new FileInputStream(fileDescriptor.getFileDescriptor())); backupState.setInputStream(new FileInputStream(fileDescriptor.getFileDescriptor()));
backupState.setBytesTransferred(0); backupState.setBytesTransferred(0);
Cipher cipher = CipherUtil.startEncrypt(backupState.getSecretKey(), backupState.getSalt());
backupState.setCipher(cipher);
ZipEntry zipEntry = new ZipEntry(configuration.getFullBackupDirectory() + backupState.getPackageName()); ZipEntry zipEntry = new ZipEntry(configuration.getFullBackupDirectory() + backupState.getPackageName());
backupState.getOutputStream().putNextEntry(zipEntry); backupState.getOutputStream().putNextEntry(zipEntry);
@ -165,17 +127,30 @@ public class ContentProviderBackupComponent implements BackupComponent {
} }
@Override @Override
public int checkFullBackupSize(long size) { public int performIncrementalBackup(PackageInfo packageInfo, ParcelFileDescriptor data) {
int result = TRANSPORT_OK; BackupDataInput backupDataInput = new BackupDataInput(data.getFileDescriptor());
if (size <= 0) { try {
result = TRANSPORT_PACKAGE_REJECTED; initializeBackupState();
backupState.setPackageIndex(backupState.getPackageIndex() + 1);
backupState.setPackageName(packageInfo.packageName);
} else if (size > configuration.getBackupSizeQuota()) { return transferIncrementalBackupData(backupDataInput);
result = TRANSPORT_QUOTA_EXCEEDED;
} 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 @Override
@ -196,51 +171,22 @@ public class ContentProviderBackupComponent implements BackupComponent {
ZipOutputStream outputStream = backupState.getOutputStream(); ZipOutputStream outputStream = backupState.getOutputStream();
try { 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); backupState.setBytesTransferred(bytesTransferred);
} catch (IOException ex) { } catch (Exception ex) {
Log.e(TAG, "Error handling backup data for " + backupState.getPackageName() + ": ", ex); Log.e(TAG, "Error handling backup data for " + backupState.getPackageName() + ": ", ex);
return TRANSPORT_ERROR; return TRANSPORT_ERROR;
} }
return TRANSPORT_OK; 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 { private int transferIncrementalBackupData(BackupDataInput backupDataInput) throws IOException {
ZipOutputStream outputStream = backupState.getOutputStream(); ZipOutputStream outputStream = backupState.getOutputStream();
@ -285,4 +231,73 @@ public class ContentProviderBackupComponent implements BackupComponent {
return TRANSPORT_OK; 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 android.os.ParcelFileDescriptor;
import javax.crypto.Cipher;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import java.io.InputStream; import java.io.InputStream;
import java.security.SecureRandom; import java.security.SecureRandom;
@ -22,6 +23,8 @@ class ContentProviderBackupState {
private ZipOutputStream outputStream; private ZipOutputStream outputStream;
private Cipher cipher;
private long bytesTransferred; private long bytesTransferred;
private String packageName; private String packageName;
@ -45,6 +48,14 @@ class ContentProviderBackupState {
this.bytesTransferred = bytesTransferred; this.bytesTransferred = bytesTransferred;
} }
Cipher getCipher() {
return cipher;
}
void setCipher(Cipher cipher) {
this.cipher = cipher;
}
ParcelFileDescriptor getInputFileDescriptor() { ParcelFileDescriptor getInputFileDescriptor() {
return inputFileDescriptor; return inputFileDescriptor;
} }

View file

@ -17,6 +17,7 @@ import libcore.io.Streams;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import java.io.*; import java.io.*;
import java.util.Arrays;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -166,29 +167,6 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
return TRANSPORT_OK; 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 { private byte[] readBackupData(ZipInputStream inputStream) throws Exception {
byte[] backupData = Streams.readFullyNoClose(inputStream); byte[] backupData = Streams.readFullyNoClose(inputStream);
SecretKey secretKey = restoreState.getSecretKey(); SecretKey secretKey = restoreState.getSecretKey();
@ -248,7 +226,28 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
if (bytesRead <= 0) { if (bytesRead <= 0) {
bytesRead = NO_MORE_DATA; bytesRead = NO_MORE_DATA;
if (restoreState.getCipher() != null) {
buffer = restoreState.getCipher().doFinal();
bytesRead = buffer.length;
outputStream.write(buffer, 0, bytesRead);
restoreState.setCipher(null);
}
} else { } 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); outputStream.write(buffer, 0, bytesRead);
} }
@ -305,4 +304,27 @@ public class ContentProviderRestoreComponent implements RestoreComponent {
IoUtils.closeQuietly(restoreState.getInputFileDescriptor()); IoUtils.closeQuietly(restoreState.getInputFileDescriptor());
restoreState = null; 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.content.pm.PackageInfo;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import javax.crypto.Cipher;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import java.util.List; import java.util.List;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
@ -23,16 +24,26 @@ class ContentProviderRestoreState {
private ZipInputStream inputStream; private ZipInputStream inputStream;
private Cipher cipher;
private byte[] salt; private byte[] salt;
private SecretKey secretKey; private SecretKey secretKey;
private List<ZipEntry> zipEntries; private List<ZipEntry> zipEntries;
Cipher getCipher() {
return cipher;
}
ParcelFileDescriptor getInputFileDescriptor() { ParcelFileDescriptor getInputFileDescriptor() {
return inputFileDescriptor; return inputFileDescriptor;
} }
void setCipher(Cipher cipher) {
this.cipher = cipher;
}
void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) { void setInputFileDescriptor(ParcelFileDescriptor inputFileDescriptor) {
this.inputFileDescriptor = inputFileDescriptor; this.inputFileDescriptor = inputFileDescriptor;
} }

View file

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