diff --git a/app/build.gradle b/app/build.gradle index 652c77e5..2bcf2a14 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,7 +17,7 @@ android { defaultConfig { applicationId "com.beemdevelopment.aegis" - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 29 versionCode 29 versionName "1.1.4" diff --git a/app/src/main/java/com/beemdevelopment/aegis/Preferences.java b/app/src/main/java/com/beemdevelopment/aegis/Preferences.java index 5ff095d1..79826273 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/Preferences.java +++ b/app/src/main/java/com/beemdevelopment/aegis/Preferences.java @@ -3,6 +3,7 @@ package com.beemdevelopment.aegis; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; +import android.net.Uri; import android.os.Build; import android.preference.PreferenceManager; @@ -121,4 +122,33 @@ public class Preferences { return new Locale(lang); } + + public boolean isBackupsEnabled() { + return _prefs.getBoolean("pref_backups", false); + } + + public void setIsBackupsEnabled(boolean enabled) { + _prefs.edit().putBoolean("pref_backups", enabled).apply(); + } + + public Uri getBackupsLocation() { + String str = _prefs.getString("pref_backups_location", null); + if (str != null) { + return Uri.parse(str); + } + + return null; + } + + public void setBackupsLocation(Uri location) { + _prefs.edit().putString("pref_backups_location", location == null ? null : location.toString()).apply(); + } + + public int getBackupsVersionCount() { + return _prefs.getInt("pref_backups_versions", 5); + } + + public void setBackupsVersionCount(int versions) { + _prefs.edit().putInt("pref_backups_versions", versions).apply(); + } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java index dffd4619..e6b0e3b6 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java @@ -4,7 +4,7 @@ import android.content.Context; import android.content.pm.PackageManager; import com.beemdevelopment.aegis.vault.VaultEntry; -import com.beemdevelopment.aegis.util.ByteInputStream; +import com.beemdevelopment.aegis.util.IOUtils; import com.beemdevelopment.aegis.util.UUIDMap; import com.topjohnwu.superuser.io.SuFile; @@ -151,9 +151,7 @@ public abstract class DatabaseImporter { } public byte[] readAll() throws IOException { - try (ByteInputStream stream = ByteInputStream.create(_stream)) { - return stream.getBytes(); - } + return IOUtils.readAll(_stream); } public InputStream getStream() { diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/SteamImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/SteamImporter.java index 36a91ebf..28e1ff45 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/SteamImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/SteamImporter.java @@ -3,12 +3,11 @@ package com.beemdevelopment.aegis.importers; import android.content.Context; import android.content.pm.PackageManager; -import com.beemdevelopment.aegis.encoding.EncodingException; -import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.encoding.Base64; +import com.beemdevelopment.aegis.encoding.EncodingException; import com.beemdevelopment.aegis.otp.OtpInfoException; import com.beemdevelopment.aegis.otp.SteamInfo; -import com.beemdevelopment.aegis.util.ByteInputStream; +import com.beemdevelopment.aegis.vault.VaultEntry; import com.topjohnwu.superuser.io.SuFile; import org.json.JSONException; @@ -45,8 +44,9 @@ public class SteamImporter extends DatabaseImporter { @Override public State read(FileReader reader) throws DatabaseImporterException { - try (ByteInputStream stream = ByteInputStream.create(reader.getStream())) { - JSONObject obj = new JSONObject(new String(stream.getBytes(), StandardCharsets.UTF_8)); + try { + byte[] bytes = reader.readAll(); + JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8)); return new State(obj); } catch (IOException | JSONException e) { throw new DatabaseImporterException(e); diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java b/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java index 528b5a15..19d36526 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java @@ -214,6 +214,31 @@ public class Dialogs { showSecureDialog(dialog); } + public static void showBackupVersionsPickerDialog(Activity activity, NumberInputListener listener) { + final int max = 30; + String[] numbers = new String[max / 5]; + for (int i = 0; i < numbers.length; i++) { + numbers[i] = Integer.toString(i * 5 + 5); + } + + View view = activity.getLayoutInflater().inflate(R.layout.dialog_number_picker, null); + NumberPicker numberPicker = view.findViewById(R.id.numberPicker); + numberPicker.setDisplayedValues(numbers); + numberPicker.setMaxValue(numbers.length - 1); + numberPicker.setMinValue(0); + numberPicker.setValue(new Preferences(activity.getApplicationContext()).getBackupsVersionCount() / 5 - 1); + numberPicker.setWrapSelectorWheel(false); + + AlertDialog dialog = new AlertDialog.Builder(activity) + .setTitle(R.string.set_number) + .setView(view) + .setPositiveButton(android.R.string.ok, (dialog1, which) -> + listener.onNumberInputResult(numberPicker.getValue())) + .create(); + + showSecureDialog(dialog); + } + public interface NumberInputListener { void onNumberInputResult(int number); } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java index 9a56548d..55513e2b 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java @@ -16,10 +16,12 @@ import androidx.appcompat.app.AlertDialog; import androidx.biometric.BiometricPrompt; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.SwitchPreferenceCompat; import com.beemdevelopment.aegis.AegisApplication; import com.beemdevelopment.aegis.BuildConfig; import com.beemdevelopment.aegis.CancelAction; +import com.beemdevelopment.aegis.Preferences; import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.Theme; import com.beemdevelopment.aegis.ViewMode; @@ -71,8 +73,10 @@ public class PreferencesFragment extends PreferenceFragmentCompat { private static final int CODE_SELECT_ENTRIES = 4; private static final int CODE_EXPORT = 5; private static final int CODE_EXPORT_ENCRYPT = 6; + private static final int CODE_BACKUPS = 7; private Intent _result; + private Preferences _prefs; private VaultManager _vault; // keep a reference to the type of database converter the user selected @@ -87,30 +91,35 @@ public class PreferencesFragment extends PreferenceFragmentCompat { private Preference _slotsPreference; private Preference _groupsPreference; private Preference _passwordReminderPreference; + private SwitchPreferenceCompat _backupsPreference; + private Preference _backupsLocationPreference; + private Preference _backupsTriggerPreference; + private Preference _backupsVersionsPreference; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { addPreferencesFromResource(R.xml.preferences); AegisApplication app = (AegisApplication) getActivity().getApplication(); + _prefs = app.getPreferences(); _vault = app.getVaultManager(); // set the result intent in advance setResult(new Intent()); - int currentTheme = app.getPreferences().getCurrentTheme().ordinal(); + int currentTheme = _prefs.getCurrentTheme().ordinal(); Preference darkModePreference = findPreference("pref_dark_mode"); darkModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.theme_titles)[currentTheme])); darkModePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { - int currentTheme = app.getPreferences().getCurrentTheme().ordinal(); + int currentTheme = _prefs.getCurrentTheme().ordinal(); Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) .setTitle(R.string.choose_theme) .setSingleChoiceItems(R.array.theme_titles, currentTheme, (dialog, which) -> { int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); - app.getPreferences().setCurrentTheme(Theme.fromInteger(i)); + _prefs.setCurrentTheme(Theme.fromInteger(i)); dialog.dismiss(); @@ -136,19 +145,19 @@ public class PreferencesFragment extends PreferenceFragmentCompat { langPreference.setVisible(false); } - int currentViewMode = app.getPreferences().getCurrentViewMode().ordinal(); + int currentViewMode = _prefs.getCurrentViewMode().ordinal(); Preference viewModePreference = findPreference("pref_view_mode"); viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[currentViewMode])); viewModePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { - int currentViewMode = app.getPreferences().getCurrentViewMode().ordinal(); + int currentViewMode = _prefs.getCurrentViewMode().ordinal(); Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) .setTitle(R.string.choose_view_mode) .setSingleChoiceItems(R.array.view_mode_titles, currentViewMode, (dialog, which) -> { int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); - app.getPreferences().setCurrentViewMode(ViewMode.fromInteger(i)); + _prefs.setCurrentViewMode(ViewMode.fromInteger(i)); viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[i])); _result.putExtra("needsRefresh", true); dialog.dismiss(); @@ -360,6 +369,51 @@ public class PreferencesFragment extends PreferenceFragmentCompat { } }); + _backupsPreference = findPreference("pref_backups"); + _backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> { + if ((boolean) newValue) { + selectBackupsLocation(); + } else { + _prefs.setIsBackupsEnabled(false); + updateBackupPreference(); + } + + return false; + }); + + Uri backupLocation = _prefs.getBackupsLocation(); + _backupsLocationPreference = findPreference("pref_backups_location"); + if (backupLocation != null) { + _backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(backupLocation.toString()))); + } + _backupsLocationPreference.setOnPreferenceClickListener(preference -> { + selectBackupsLocation(); + return false; + }); + + _backupsTriggerPreference = findPreference("pref_backups_trigger"); + _backupsTriggerPreference.setOnPreferenceClickListener(preference -> { + if (_prefs.isBackupsEnabled()) { + try { + _vault.backup(); + } catch (VaultManagerException e) { + Toast.makeText(getContext(), String.format("Error creating backup: %s", e.getMessage()), Toast.LENGTH_LONG).show(); + } + } + return true; + }); + + _backupsVersionsPreference = findPreference("pref_backups_versions"); + _backupsVersionsPreference.setSummary(String.format(getString(R.string.pref_backups_versions_summary), _prefs.getBackupsVersionCount())); + _backupsVersionsPreference.setOnPreferenceClickListener(preference -> { + Dialogs.showBackupVersionsPickerDialog(getActivity(), number -> { + number = number * 5 + 5; + _prefs.setBackupsVersionCount(number); + _backupsVersionsPreference.setSummary(String.format(getString(R.string.pref_backups_versions_summary), number)); + }); + return false; + }); + _autoLockPreference = findPreference("pref_auto_lock"); _passwordReminderPreference = findPreference("pref_password_reminder"); } @@ -368,6 +422,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat { public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); updateEncryptionPreferences(); + updateBackupPreference(); } @Override @@ -396,6 +451,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat { // intentional fallthrough case CODE_EXPORT_ENCRYPT: onExportResult(resultCode, data, requestCode == CODE_EXPORT_ENCRYPT); + case CODE_BACKUPS: + onSelectBackupsLocationResult(resultCode, data); break; } } @@ -662,6 +719,21 @@ public class PreferencesFragment extends PreferenceFragmentCompat { Toast.makeText(getActivity(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show(); } + private void onSelectBackupsLocationResult(int resultCode, Intent data) { + Uri uri = data.getData(); + if (resultCode != Activity.RESULT_OK || uri == null) { + return; + } + + int flags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + getContext().getContentResolver().takePersistableUriPermission(data.getData(), flags); + + _prefs.setBackupsLocation(uri); + _prefs.setIsBackupsEnabled(true); + _backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString()))); + updateBackupPreference(); + } + private boolean saveVault() { try { _vault.save(); @@ -699,6 +771,28 @@ public class PreferencesFragment extends PreferenceFragmentCompat { _slotsPreference.setVisible(false); _passwordReminderPreference.setVisible(false); } + + updateBackupPreference(); + } + + private void updateBackupPreference() { + boolean encrypted = _vault.isEncryptionEnabled(); + boolean backupEnabled = _prefs.isBackupsEnabled() && encrypted; + _backupsPreference.setChecked(backupEnabled); + _backupsPreference.setEnabled(encrypted); + _backupsLocationPreference.setVisible(backupEnabled); + _backupsTriggerPreference.setVisible(backupEnabled); + _backupsVersionsPreference.setVisible(backupEnabled); + } + + private void selectBackupsLocation() { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); + + startActivityForResult(intent, CODE_BACKUPS); } private class SetPasswordListener implements Dialogs.SlotListener { diff --git a/app/src/main/java/com/beemdevelopment/aegis/util/ByteInputStream.java b/app/src/main/java/com/beemdevelopment/aegis/util/ByteInputStream.java deleted file mode 100644 index 9547f7dd..00000000 --- a/app/src/main/java/com/beemdevelopment/aegis/util/ByteInputStream.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.beemdevelopment.aegis.util; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; - -public class ByteInputStream extends ByteArrayInputStream { - private ByteInputStream(byte[] buf) { - super(buf); - } - - public static ByteInputStream create(InputStream fileStream) throws IOException { - int read; - byte[] buf = new byte[4096]; - ByteArrayOutputStream outStream = new ByteArrayOutputStream(); - while ((read = fileStream.read(buf, 0, buf.length)) != -1) { - outStream.write(buf, 0, read); - } - - return new ByteInputStream(outStream.toByteArray()); - } - - public byte[] getBytes() { - return this.buf; - } -} diff --git a/app/src/main/java/com/beemdevelopment/aegis/util/IOUtils.java b/app/src/main/java/com/beemdevelopment/aegis/util/IOUtils.java new file mode 100644 index 00000000..abf0a2c2 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/util/IOUtils.java @@ -0,0 +1,37 @@ +package com.beemdevelopment.aegis.util; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class IOUtils { + private IOUtils() { + + } + + public static byte[] readFile(FileInputStream inStream) throws IOException { + try (DataInputStream outStream = new DataInputStream(inStream)) { + byte[] fileBytes = new byte[(int) inStream.getChannel().size()]; + outStream.readFully(fileBytes); + return fileBytes; + } + } + + public static byte[] readAll(InputStream inStream) throws IOException { + try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) { + copy(inStream, outStream); + return outStream.toByteArray(); + } + } + + public static void copy(InputStream inStream, OutputStream outStream) throws IOException { + int read; + byte[] buf = new byte[4096]; + while ((read = inStream.read(buf, 0, buf.length)) != -1) { + outStream.write(buf, 0, read); + } + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultBackupManager.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultBackupManager.java new file mode 100644 index 00000000..9ae6c3a6 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultBackupManager.java @@ -0,0 +1,219 @@ +package com.beemdevelopment.aegis.vault; + +import android.content.Context; +import android.content.UriPermission; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.documentfile.provider.DocumentFile; + +import com.beemdevelopment.aegis.util.IOUtils; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.text.ParseException; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +public class VaultBackupManager { + private static final String TAG = VaultBackupManager.class.getSimpleName(); + + private static final StrictDateFormat _dateFormat = + new StrictDateFormat("yyyyMMdd-HHmmss", Locale.ENGLISH); + + public static final String FILENAME_PREFIX = "aegis-backup"; + + private Context _context; + + public VaultBackupManager(Context context) { + _context = context; + } + + public void create(Uri dirUri, int versionsToKeep) throws VaultManagerException { + FileInfo fileInfo = new FileInfo(FILENAME_PREFIX, Calendar.getInstance().getTime()); + DocumentFile dir = DocumentFile.fromTreeUri(_context, dirUri); + + Log.i(TAG, String.format("Creating backup at %s: %s", Uri.decode(dir.getUri().toString()), fileInfo.toString())); + + if (!hasPermissionsAt(dirUri)) { + Log.e(TAG, "Unable to create file for backup, no persisted URI permissions"); + throw new VaultManagerException("No persisted URI permissions"); + } + + // If we create a file with a name that already exists, SAF will append a number + // to the filename and write to that instead. We can't overwrite existing files, so + // just avoid that altogether by checking beforehand. + if (dir.findFile(fileInfo.toString()) != null) { + throw new VaultManagerException("Backup file already exists"); + } + + DocumentFile file = dir.createFile("application/json", fileInfo.toString()); + if (file == null) { + Log.e(TAG, "Unable to create file for backup, createFile returned null"); + throw new VaultManagerException("createFile returned null"); + } + + try (FileInputStream inStream = _context.openFileInput(VaultManager.FILENAME); + OutputStream outStream = _context.getContentResolver().openOutputStream(file.getUri())) { + IOUtils.copy(inStream, outStream); + } catch (IOException e) { + Log.e(TAG, "Unable to create backup", e); + throw new VaultManagerException(e); + } + + enforceVersioning(dir, versionsToKeep); + } + + public boolean hasPermissionsAt(Uri uri) { + for (UriPermission perm : _context.getContentResolver().getPersistedUriPermissions()) { + if (perm.getUri().equals(uri)) { + return perm.isReadPermission() && perm.isWritePermission(); + } + } + + return false; + } + + private void enforceVersioning(DocumentFile dir, int versionsToKeep) { + Log.i(TAG, String.format("Scanning directory %s for backup files", Uri.decode(dir.getUri().toString()))); + + List files = new ArrayList<>(); + for (DocumentFile docFile : dir.listFiles()) { + if (docFile.isFile() && !docFile.isVirtual()) { + try { + files.add(new File(docFile)); + } catch (ParseException ignored) { } + } + } + + Collections.sort(files, new FileComparator()); + for (File file : files) { + Log.i(TAG, file.getFile().getName()); + } + + if (files.size() > versionsToKeep) { + for (File file : files.subList(0, files.size() - versionsToKeep)) { + Log.i(TAG, String.format("Deleting %s", file.getFile().getName())); + if (!file.getFile().delete()) { + Log.e(TAG, String.format("Unable to delete %s", file.getFile().getName())); + } + } + } + } + + private static class FileInfo { + private String _filename; + private Date _date; + + public FileInfo(String filename, Date date) { + _filename = filename; + _date = date; + } + + public static FileInfo parseFilename(String filename) throws ParseException { + if (filename == null) { + throw new ParseException("The filename must not be null", 0); + } + + final String ext = ".json"; + if (!filename.endsWith(ext)) { + throwBadFormat(filename); + } + filename = filename.substring(0, filename.length() - ext.length()); + + final String delim = "-"; + String[] parts = filename.split(delim); + if (parts.length < 3) { + throwBadFormat(filename); + } + + filename = TextUtils.join(delim, Arrays.copyOf(parts, parts.length - 2)); + if (!filename.equals(FILENAME_PREFIX)) { + throwBadFormat(filename); + } + + Date date = _dateFormat.parse(parts[parts.length - 2] + delim + parts[parts.length - 1]); + if (date == null) { + throwBadFormat(filename); + } + + return new FileInfo(filename, date); + } + + private static void throwBadFormat(String filename) throws ParseException { + throw new ParseException(String.format("Bad backup filename format: %s", filename), 0); + } + + public String getFilename() { + return _filename; + } + + public Date getDate() { + return _date; + } + + @NonNull + @Override + public String toString() { + return String.format("%s-%s.json", _filename, _dateFormat.format(_date)); + } + } + + private static class File { + private DocumentFile _file; + private FileInfo _info; + + public File(DocumentFile file) throws ParseException { + _file = file; + _info = FileInfo.parseFilename(file.getName()); + } + + public DocumentFile getFile() { + return _file; + } + + public FileInfo getInfo() { + return _info; + } + } + + private static class FileComparator implements Comparator { + @Override + public int compare(File o1, File o2) { + return o1.getInfo().getDate().compareTo(o2.getInfo().getDate()); + } + } + + // https://stackoverflow.com/a/19503019 + private static class StrictDateFormat extends SimpleDateFormat { + public StrictDateFormat(String pattern, Locale locale) { + super(pattern, locale); + setLenient(false); + } + + @Override + public Date parse(@NonNull String text, ParsePosition pos) { + int posIndex = pos.getIndex(); + Date d = super.parse(text, pos); + if (!isLenient() && d != null) { + String format = this.format(d); + if (posIndex + format.length() != text.length() || + !text.endsWith(format)) { + d = null; // Not exact match + } + } + return d; + } + } +} 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 b378ad6c..2157dfe7 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java @@ -5,6 +5,7 @@ import android.content.Intent; import androidx.core.util.AtomicFile; +import com.beemdevelopment.aegis.Preferences; import com.beemdevelopment.aegis.services.NotificationService; import org.json.JSONObject; @@ -18,9 +19,9 @@ import java.util.Collection; import java.util.TreeSet; public class VaultManager { - private static final String FILENAME = "aegis.json"; - public static final String FILENAME_EXPORT = "aegis_export.json"; - public static final String FILENAME_EXPORT_PLAIN = "aegis_export_plain.json"; + public static final String FILENAME = "aegis.json"; + public static final String FILENAME_EXPORT = "aegis-export.json"; + public static final String FILENAME_EXPORT_PLAIN = "aegis-export-plain.json"; private Vault _vault; private VaultFile _file; @@ -28,9 +29,13 @@ public class VaultManager { private boolean _encrypt; private Context _context; + private Preferences _prefs; + private VaultBackupManager _backups; public VaultManager(Context context) { _context = context; + _prefs = new Preferences(context); + _backups = new VaultBackupManager(context); } public boolean fileExists() { @@ -102,6 +107,10 @@ public class VaultManager { _file.setContent(obj); } save(_context, _file); + + if (_prefs.isBackupsEnabled()) { + backup(); + } } catch (VaultFileException e) { throw new VaultManagerException(e); } @@ -125,6 +134,11 @@ public class VaultManager { } } + public void backup() throws VaultManagerException { + assertState(false, true); + _backups.create(_prefs.getBackupsLocation(), _prefs.getBackupsVersionCount()); + } + public void addEntry(VaultEntry entry) { assertState(false, true); _vault.getEntries().add(entry); diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManagerException.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManagerException.java index 586aed0a..3ac44b50 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManagerException.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManagerException.java @@ -4,4 +4,8 @@ public class VaultManagerException extends Exception { public VaultManagerException(Throwable cause) { super(cause); } + + public VaultManagerException(String message) { + super(message); + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5499c21f..0b4ae6b2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,6 +17,7 @@ General Security Tools + Backups Theme View mode Language @@ -28,6 +29,14 @@ Manage the list of keys that can decrypt the vault Import from file Import tokens from a file + Backup the vault + Create backups of the vault on external storage. This is only supported for encrypted vaults. + Directory for backup files + Backups will be stored at + Trigger backup + Manually trigger a backup + Amount of versions to keep + Keep %1$d versions of the backup Import from app Import tokens from an app (requires root access) Export diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index cc6d0f02..f0b9c00c 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -132,6 +132,32 @@ app:iconSpaceReserved="false"/> + + + + + + +