diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 08a48bb5..c151aadd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,10 @@ = 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); + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/Preferences.java b/app/src/main/java/com/beemdevelopment/aegis/Preferences.java index eefef812..b2fd5e66 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/Preferences.java +++ b/app/src/main/java/com/beemdevelopment/aegis/Preferences.java @@ -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); } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/BackupsPreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/BackupsPreferencesFragment.java index e99aa502..a1875f10 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/BackupsPreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/BackupsPreferencesFragment.java @@ -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); diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/SecurityPreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/SecurityPreferencesFragment.java index 9764645d..c0bcfd84 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/SecurityPreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/SecurityPreferencesFragment.java @@ -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(); diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java index d688b096..1b68a432 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java @@ -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); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 584180ba..b9113277 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -33,7 +33,9 @@ Manage the list of keys that can decrypt the vault Import from file Import tokens from a file - Backup the vault + Participate in Android\'s backup system + Allow Android\'s backup system to include Aegis\' vault in its backups. This is only supported for encrypted vaults. + Automatically back up the vault Automatically create backups of the vault on external storage when changes are made. This is only supported for encrypted vaults. Directory for backup files Backups will be stored at @@ -163,7 +165,7 @@ 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. 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. Disable encryption - Are you sure you want to disable encryption? This will cause the vault to be stored in plain text. + Are you sure you want to disable encryption? This will cause the vault to be stored in plain text. Automatic backups will also be disabled. An error occurred while enabling encryption An error occurred while disabling encryption The backup was scheduled successfully diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 00000000..617fec3d --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/xml/preferences_backups.xml b/app/src/main/res/xml/preferences_backups.xml index 2f418d0c..364966a0 100644 --- a/app/src/main/res/xml/preferences_backups.xml +++ b/app/src/main/res/xml/preferences_backups.xml @@ -2,6 +2,12 @@ +