Add support for participating in Android's backup system

This adds an option for participating in Android's backup system. Two items are
backed up: the ``files/aegis.json`` file and the entire ``shared_prefs``
directory. The option is disabled by default and can only be enabled if
encryption is enabled as well.

I tested this with Local Transport and Seedvault. To test with Local Transport,
see: https://developer.android.com/guide/topics/data/testingbackup.
This commit is contained in:
Alexander Bakker 2021-01-03 18:19:16 +01:00
parent 07c768893a
commit f080eaa8f9
10 changed files with 199 additions and 23 deletions

View file

@ -96,7 +96,7 @@ public class AegisApplication extends Application {
}
if (_vaultFile == null) {
_vaultFile = VaultManager.readFile(this);
_vaultFile = VaultManager.readVaultFile(this);
}
return _vaultFile;

View file

@ -0,0 +1,118 @@
package com.beemdevelopment.aegis;
import android.app.backup.BackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.app.backup.FullBackupDataOutput;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultManager;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class AegisBackupAgent extends BackupAgent {
private static final String TAG = BackupAgent.class.getSimpleName();
private Preferences _prefs;
@Override
public void onCreate() {
super.onCreate();
_prefs = new Preferences(this);
}
@Override
public synchronized void onFullBackup(FullBackupDataOutput data) throws IOException {
Log.i(TAG, String.format("onFullBackup() called: flags=%d, quota=%d",
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? data.getTransportFlags() : -1,
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? data.getQuota() : -1));
if (!_prefs.isAndroidBackupsEnabled()) {
Log.i(TAG, "onFullBackup() skipped: Android backups disabled in preferences");
return;
}
// first copy the vault to the files/backup directory
createBackupDir();
File vaultBackupFile = getVaultBackupFile();
try (FileInputStream inStream = VaultManager.getAtomicFile(this).openRead();
FileOutputStream outStream = new FileOutputStream(vaultBackupFile)) {
IOUtils.copy(inStream, outStream);
} catch (IOException e) {
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
deleteBackupDir();
throw e;
}
// then call the original implementation so that fullBackupContent specified in AndroidManifest is read
try {
super.onFullBackup(data);
} catch (IOException e) {
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
throw e;
} finally {
deleteBackupDir();
}
Log.i(TAG, "onFullBackup() finished");
}
@Override
public synchronized void onRestoreFile(ParcelFileDescriptor data, long size, File destination, int type, long mode, long mtime) throws IOException {
Log.i(TAG, String.format("onRestoreFile() called: dest=%s", destination));
super.onRestoreFile(data, size, destination, type, mode, mtime);
File vaultBackupFile = getVaultBackupFile();
if (destination.getCanonicalFile().equals(vaultBackupFile.getCanonicalFile())) {
try (InputStream inStream = new FileInputStream(vaultBackupFile)) {
VaultManager.writeToFile(this, inStream);
} catch (IOException e) {
Log.e(TAG, String.format("onRestoreFile() failed: dest=%s, error=%s", destination, e));
throw e;
} finally {
deleteBackupDir();
}
}
Log.i(TAG, String.format("onRestoreFile() finished: dest=%s", destination));
}
@Override
public synchronized void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
super.onQuotaExceeded(backupDataBytes, quotaBytes);
Log.e(TAG, String.format("onQuotaExceeded() called: backupDataBytes=%d, quotaBytes=%d", backupDataBytes, quotaBytes));
}
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) throws IOException {
}
@Override
public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException {
}
private void createBackupDir() throws IOException {
File dir = getVaultBackupFile().getParentFile();
if (!dir.exists() && !dir.mkdir()) {
throw new IOException(String.format("Unable to create backup directory: %s", dir.toString()));
}
}
private void deleteBackupDir() {
File dir = getVaultBackupFile().getParentFile();
IOUtils.clearDirectory(dir, true);
}
private File getVaultBackupFile() {
return new File(new File(getFilesDir(), "backup"), VaultManager.FILENAME);
}
}

View file

@ -164,6 +164,14 @@ public class Preferences {
return new Locale(parts[0], parts[1]);
}
public boolean isAndroidBackupsEnabled() {
return _prefs.getBoolean("pref_android_backups", false);
}
public void setIsAndroidBackupsEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_android_backups", enabled).apply();
}
public boolean isBackupsEnabled() {
return _prefs.getBoolean("pref_backups", false);
}

View file

@ -15,6 +15,7 @@ import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.vault.VaultManagerException;
public class BackupsPreferencesFragment extends PreferencesFragment {
private SwitchPreferenceCompat _androidBackupsPreference;
private SwitchPreferenceCompat _backupsPreference;
private Preference _backupsLocationPreference;
private Preference _backupsTriggerPreference;
@ -44,6 +45,14 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
return false;
});
_androidBackupsPreference = findPreference("pref_android_backups");
_androidBackupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
prefs.setIsAndroidBackupsEnabled((boolean) newValue);
updateBackupPreference();
getVault().androidBackupDataChanged();
return false;
});
Uri backupLocation = prefs.getBackupsLocation();
_backupsLocationPreference = findPreference("pref_backups_location");
if (backupLocation != null) {
@ -106,7 +115,10 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
private void updateBackupPreference() {
boolean encrypted = getVault().isEncryptionEnabled();
boolean androidBackupEnabled = getPreferences().isAndroidBackupsEnabled() && encrypted;
boolean backupEnabled = getPreferences().isBackupsEnabled() && encrypted;
_androidBackupsPreference.setChecked(androidBackupEnabled);
_androidBackupsPreference.setEnabled(encrypted);
_backupsPreference.setChecked(backupEnabled);
_backupsPreference.setEnabled(encrypted);
_backupsLocationPreference.setVisible(backupEnabled);

View file

@ -95,7 +95,7 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
} else {
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.disable_encryption)
.setMessage(getString(R.string.disable_encryption_description))
.setMessage(getText(R.string.disable_encryption_description))
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
try {
getVault().disableEncryption();

View file

@ -1,5 +1,6 @@
package com.beemdevelopment.aegis.vault;
import android.app.backup.BackupManager;
import android.content.Context;
import androidx.core.util.AtomicFile;
@ -10,6 +11,7 @@ import com.beemdevelopment.aegis.util.IOUtils;
import org.json.JSONObject;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@ -34,11 +36,13 @@ public class VaultManager {
private Context _context;
private Preferences _prefs;
private VaultBackupManager _backups;
private BackupManager _androidBackups;
public VaultManager(Context context, Vault vault, VaultFileCredentials creds) {
_context = context;
_prefs = new Preferences(context);
_backups = new VaultBackupManager(context);
_androidBackups = new BackupManager(context);
_vault = vault;
_creds = creds;
}
@ -47,7 +51,7 @@ public class VaultManager {
this(context, vault, null);
}
private static AtomicFile getAtomicFile(Context context) {
public static AtomicFile getAtomicFile(Context context) {
return new AtomicFile(new File(context.getFilesDir(), FILENAME));
}
@ -60,7 +64,7 @@ public class VaultManager {
getAtomicFile(context).delete();
}
public static VaultFile readFile(Context context) throws VaultManagerException {
public static VaultFile readVaultFile(Context context) throws VaultManagerException {
AtomicFile file = getAtomicFile(context);
try {
@ -71,6 +75,22 @@ public class VaultManager {
}
}
public static void writeToFile(Context context, InputStream inStream) throws IOException {
AtomicFile file = VaultManager.getAtomicFile(context);
FileOutputStream outStream = null;
try {
outStream = file.startWrite();
IOUtils.copy(inStream, outStream);
file.finishWrite(outStream);
} catch (IOException e) {
if (outStream != null) {
file.failWrite(outStream);
}
throw e;
}
}
public static VaultManager init(Context context, VaultFile file, VaultFileCredentials creds) throws VaultManagerException {
if (file.isEncrypted() && creds == null) {
throw new IllegalArgumentException("The VaultFile is encrypted but the given VaultFileCredentials is null");
@ -94,18 +114,10 @@ public class VaultManager {
}
public static void save(Context context, VaultFile vaultFile) throws VaultManagerException {
byte[] bytes = vaultFile.toBytes();
AtomicFile file = getAtomicFile(context);
FileOutputStream stream = null;
try {
stream = file.startWrite();
stream.write(bytes);
file.finishWrite(stream);
byte[] bytes = vaultFile.toBytes();
writeToFile(context, new ByteArrayInputStream(bytes));
} catch (IOException e) {
if (stream != null) {
file.failWrite(stream);
}
throw new VaultManagerException(e);
}
}
@ -130,12 +142,18 @@ public class VaultManager {
throw new VaultManagerException(e);
}
if (backup && _prefs.isBackupsEnabled()) {
try {
backup();
_prefs.setBackupsError(null);
} catch (VaultManagerException e) {
_prefs.setBackupsError(e);
if (backup) {
if (_prefs.isBackupsEnabled()) {
try {
backup();
_prefs.setBackupsError(null);
} catch (VaultManagerException e) {
_prefs.setBackupsError(e);
}
}
if (_prefs.isAndroidBackupsEnabled()) {
androidBackupDataChanged();
}
}
}
@ -202,6 +220,10 @@ public class VaultManager {
}
}
public void androidBackupDataChanged() {
_androidBackups.dataChanged();
}
public void addEntry(VaultEntry entry) {
_vault.getEntries().add(entry);
}