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.
pull/654/head
Alexander Bakker 4 years ago
parent 07c768893a
commit f080eaa8f9

@ -15,7 +15,10 @@
<application
android:name=".AegisApplication"
android:allowBackup="false"
android:allowBackup="true"
android:fullBackupOnly="true"
android:fullBackupContent="@xml/backup_rules"
android:backupAgent=".AegisBackupAgent"
android:icon="@mipmap/${iconName}"
android:label="Aegis"
android:supportsRtl="true"

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

@ -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);
}
}

@ -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);
}

@ -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);

@ -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();

@ -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);
}

@ -33,7 +33,9 @@
<string name="pref_slots_summary">Manage the list of keys that can decrypt the vault</string>
<string name="pref_import_file_title">Import from file</string>
<string name="pref_import_file_summary">Import tokens from a file</string>
<string name="pref_backups_title">Backup the vault</string>
<string name="pref_android_backups_title">Participate in Android\'s backup system</string>
<string name="pref_android_backups_summary">Allow Android\'s backup system to include Aegis\' vault in its backups. This is only supported for encrypted vaults.</string>
<string name="pref_backups_title">Automatically back up the vault</string>
<string name="pref_backups_summary">Automatically create backups of the vault on external storage when changes are made. This is only supported for encrypted vaults.</string>
<string name="pref_backups_location_title">Directory for backup files</string>
<string name="pref_backups_location_summary">Backups will be stored at</string>
@ -163,7 +165,7 @@
<string name="biometric_decrypt_error">An error occurred while trying to decrypt the vault with biometric authentication. This usually only happens if the security settings of your device were changed. Please unlock the vault with your password and reconfigure biometric authentication in the settings of Aegis.</string>
<string name="biometric_init_error">An error occurred while trying to prepare biometric authentication. This usually only happens if the security settings of your device were changed. Please unlock the vault with your password and reconfigure biometric authentication in the settings of Aegis.</string>
<string name="disable_encryption">Disable encryption</string>
<string name="disable_encryption_description">Are you sure you want to disable encryption? This will cause the vault to be stored in plain text.</string>
<string name="disable_encryption_description">Are you sure you want to disable encryption? This will cause the vault to be stored in plain text. <b>Automatic backups will also be disabled</b>.</string>
<string name="enable_encryption_error">An error occurred while enabling encryption</string>
<string name="disable_encryption_error">An error occurred while disabling encryption</string>
<string name="backup_successful">The backup was scheduled successfully</string>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<include domain="sharedpref" path="."/>
<include domain="file" path="backup/aegis.json"/>
</full-backup-content>

@ -2,6 +2,12 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:title="@string/pref_backups_group_title">
<androidx.preference.SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_android_backups"
android:title="@string/pref_android_backups_title"
android:summary="@string/pref_android_backups_summary"
app:iconSpaceReserved="false"/>
<androidx.preference.SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_backups"

Loading…
Cancel
Save