Add support for backups

Allow users to select a folder where automatic backups will be created. This
also bumps minSdkVersion to 21.
This commit is contained in:
Alexander Bakker 2020-01-04 22:06:59 +01:00
parent 866466d158
commit 4a69e9efb4
13 changed files with 475 additions and 46 deletions

View file

@ -17,7 +17,7 @@ android {
defaultConfig { defaultConfig {
applicationId "com.beemdevelopment.aegis" applicationId "com.beemdevelopment.aegis"
minSdkVersion 19 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode 29 versionCode 29
versionName "1.1.4" versionName "1.1.4"

View file

@ -3,6 +3,7 @@ package com.beemdevelopment.aegis;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Resources; import android.content.res.Resources;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
@ -121,4 +122,33 @@ public class Preferences {
return new Locale(lang); 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();
}
} }

View file

@ -4,7 +4,7 @@ import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import com.beemdevelopment.aegis.vault.VaultEntry; 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.beemdevelopment.aegis.util.UUIDMap;
import com.topjohnwu.superuser.io.SuFile; import com.topjohnwu.superuser.io.SuFile;
@ -151,9 +151,7 @@ public abstract class DatabaseImporter {
} }
public byte[] readAll() throws IOException { public byte[] readAll() throws IOException {
try (ByteInputStream stream = ByteInputStream.create(_stream)) { return IOUtils.readAll(_stream);
return stream.getBytes();
}
} }
public InputStream getStream() { public InputStream getStream() {

View file

@ -3,12 +3,11 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; 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.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfoException; import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo; 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 com.topjohnwu.superuser.io.SuFile;
import org.json.JSONException; import org.json.JSONException;
@ -45,8 +44,9 @@ public class SteamImporter extends DatabaseImporter {
@Override @Override
public State read(FileReader reader) throws DatabaseImporterException { public State read(FileReader reader) throws DatabaseImporterException {
try (ByteInputStream stream = ByteInputStream.create(reader.getStream())) { try {
JSONObject obj = new JSONObject(new String(stream.getBytes(), StandardCharsets.UTF_8)); byte[] bytes = reader.readAll();
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
return new State(obj); return new State(obj);
} catch (IOException | JSONException e) { } catch (IOException | JSONException e) {
throw new DatabaseImporterException(e); throw new DatabaseImporterException(e);

View file

@ -214,6 +214,31 @@ public class Dialogs {
showSecureDialog(dialog); 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 { public interface NumberInputListener {
void onNumberInputResult(int number); void onNumberInputResult(int number);
} }

View file

@ -16,10 +16,12 @@ import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt; import androidx.biometric.BiometricPrompt;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreferenceCompat;
import com.beemdevelopment.aegis.AegisApplication; import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.BuildConfig; import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.CancelAction; import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme; import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.ViewMode; 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_SELECT_ENTRIES = 4;
private static final int CODE_EXPORT = 5; private static final int CODE_EXPORT = 5;
private static final int CODE_EXPORT_ENCRYPT = 6; private static final int CODE_EXPORT_ENCRYPT = 6;
private static final int CODE_BACKUPS = 7;
private Intent _result; private Intent _result;
private Preferences _prefs;
private VaultManager _vault; private VaultManager _vault;
// keep a reference to the type of database converter the user selected // 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 _slotsPreference;
private Preference _groupsPreference; private Preference _groupsPreference;
private Preference _passwordReminderPreference; private Preference _passwordReminderPreference;
private SwitchPreferenceCompat _backupsPreference;
private Preference _backupsLocationPreference;
private Preference _backupsTriggerPreference;
private Preference _backupsVersionsPreference;
@Override @Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences); addPreferencesFromResource(R.xml.preferences);
AegisApplication app = (AegisApplication) getActivity().getApplication(); AegisApplication app = (AegisApplication) getActivity().getApplication();
_prefs = app.getPreferences();
_vault = app.getVaultManager(); _vault = app.getVaultManager();
// set the result intent in advance // set the result intent in advance
setResult(new Intent()); setResult(new Intent());
int currentTheme = app.getPreferences().getCurrentTheme().ordinal(); int currentTheme = _prefs.getCurrentTheme().ordinal();
Preference darkModePreference = findPreference("pref_dark_mode"); Preference darkModePreference = findPreference("pref_dark_mode");
darkModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.theme_titles)[currentTheme])); darkModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.theme_titles)[currentTheme]));
darkModePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { darkModePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override @Override
public boolean onPreferenceClick(Preference preference) { public boolean onPreferenceClick(Preference preference) {
int currentTheme = app.getPreferences().getCurrentTheme().ordinal(); int currentTheme = _prefs.getCurrentTheme().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.choose_theme) .setTitle(R.string.choose_theme)
.setSingleChoiceItems(R.array.theme_titles, currentTheme, (dialog, which) -> { .setSingleChoiceItems(R.array.theme_titles, currentTheme, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
app.getPreferences().setCurrentTheme(Theme.fromInteger(i)); _prefs.setCurrentTheme(Theme.fromInteger(i));
dialog.dismiss(); dialog.dismiss();
@ -136,19 +145,19 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
langPreference.setVisible(false); langPreference.setVisible(false);
} }
int currentViewMode = app.getPreferences().getCurrentViewMode().ordinal(); int currentViewMode = _prefs.getCurrentViewMode().ordinal();
Preference viewModePreference = findPreference("pref_view_mode"); 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.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[currentViewMode]));
viewModePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { viewModePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override @Override
public boolean onPreferenceClick(Preference preference) { public boolean onPreferenceClick(Preference preference) {
int currentViewMode = app.getPreferences().getCurrentViewMode().ordinal(); int currentViewMode = _prefs.getCurrentViewMode().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.choose_view_mode) .setTitle(R.string.choose_view_mode)
.setSingleChoiceItems(R.array.view_mode_titles, currentViewMode, (dialog, which) -> { .setSingleChoiceItems(R.array.view_mode_titles, currentViewMode, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); 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])); viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[i]));
_result.putExtra("needsRefresh", true); _result.putExtra("needsRefresh", true);
dialog.dismiss(); 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"); _autoLockPreference = findPreference("pref_auto_lock");
_passwordReminderPreference = findPreference("pref_password_reminder"); _passwordReminderPreference = findPreference("pref_password_reminder");
} }
@ -368,6 +422,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
public void onActivityCreated(Bundle savedInstanceState) { public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
updateEncryptionPreferences(); updateEncryptionPreferences();
updateBackupPreference();
} }
@Override @Override
@ -396,6 +451,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
// intentional fallthrough // intentional fallthrough
case CODE_EXPORT_ENCRYPT: case CODE_EXPORT_ENCRYPT:
onExportResult(resultCode, data, requestCode == CODE_EXPORT_ENCRYPT); onExportResult(resultCode, data, requestCode == CODE_EXPORT_ENCRYPT);
case CODE_BACKUPS:
onSelectBackupsLocationResult(resultCode, data);
break; break;
} }
} }
@ -662,6 +719,21 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
Toast.makeText(getActivity(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show(); 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() { private boolean saveVault() {
try { try {
_vault.save(); _vault.save();
@ -699,6 +771,28 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
_slotsPreference.setVisible(false); _slotsPreference.setVisible(false);
_passwordReminderPreference.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 { private class SetPasswordListener implements Dialogs.SlotListener {

View file

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

View file

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

View file

@ -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<File> 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<File> {
@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;
}
}
}

View file

@ -5,6 +5,7 @@ import android.content.Intent;
import androidx.core.util.AtomicFile; import androidx.core.util.AtomicFile;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.services.NotificationService; import com.beemdevelopment.aegis.services.NotificationService;
import org.json.JSONObject; import org.json.JSONObject;
@ -18,9 +19,9 @@ import java.util.Collection;
import java.util.TreeSet; import java.util.TreeSet;
public class VaultManager { public class VaultManager {
private static final String FILENAME = "aegis.json"; public static final String FILENAME = "aegis.json";
public static final String FILENAME_EXPORT = "aegis_export.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_EXPORT_PLAIN = "aegis-export-plain.json";
private Vault _vault; private Vault _vault;
private VaultFile _file; private VaultFile _file;
@ -28,9 +29,13 @@ public class VaultManager {
private boolean _encrypt; private boolean _encrypt;
private Context _context; private Context _context;
private Preferences _prefs;
private VaultBackupManager _backups;
public VaultManager(Context context) { public VaultManager(Context context) {
_context = context; _context = context;
_prefs = new Preferences(context);
_backups = new VaultBackupManager(context);
} }
public boolean fileExists() { public boolean fileExists() {
@ -102,6 +107,10 @@ public class VaultManager {
_file.setContent(obj); _file.setContent(obj);
} }
save(_context, _file); save(_context, _file);
if (_prefs.isBackupsEnabled()) {
backup();
}
} catch (VaultFileException e) { } catch (VaultFileException e) {
throw new VaultManagerException(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) { public void addEntry(VaultEntry entry) {
assertState(false, true); assertState(false, true);
_vault.getEntries().add(entry); _vault.getEntries().add(entry);

View file

@ -4,4 +4,8 @@ public class VaultManagerException extends Exception {
public VaultManagerException(Throwable cause) { public VaultManagerException(Throwable cause) {
super(cause); super(cause);
} }
public VaultManagerException(String message) {
super(message);
}
} }

View file

@ -17,6 +17,7 @@
<string name="pref_general_group_title">General</string> <string name="pref_general_group_title">General</string>
<string name="pref_security_group_title">Security</string> <string name="pref_security_group_title">Security</string>
<string name="pref_tools_group_title">Tools</string> <string name="pref_tools_group_title">Tools</string>
<string name="pref_backups_group_title">Backups</string>
<string name="pref_select_theme_title">Theme</string> <string name="pref_select_theme_title">Theme</string>
<string name="pref_view_mode_title">View mode</string> <string name="pref_view_mode_title">View mode</string>
<string name="pref_lang_title">Language</string> <string name="pref_lang_title">Language</string>
@ -28,6 +29,14 @@
<string name="pref_slots_summary">Manage the list of keys that can decrypt the vault</string> <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_title">Import from file</string>
<string name="pref_import_file_summary">Import tokens from a 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_backups_summary">Create backups of the vault on external storage. 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>
<string name="pref_backups_trigger_title">Trigger backup</string>
<string name="pref_backups_trigger_summary">Manually trigger a backup</string>
<string name="pref_backups_versions_title">Amount of versions to keep</string>
<string name="pref_backups_versions_summary">Keep %1$d versions of the backup</string>
<string name="pref_import_app_title">Import from app</string> <string name="pref_import_app_title">Import from app</string>
<string name="pref_import_app_summary">Import tokens from an app (requires root access)</string> <string name="pref_import_app_summary">Import tokens from an app (requires root access)</string>
<string name="pref_export_title">Export</string> <string name="pref_export_title">Export</string>

View file

@ -132,6 +132,32 @@
app:iconSpaceReserved="false"/> app:iconSpaceReserved="false"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory
android:title="@string/pref_backups_group_title"
app:iconSpaceReserved="false">
<androidx.preference.SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_backups"
android:title="@string/pref_backups_title"
android:summary="@string/pref_backups_summary"
app:iconSpaceReserved="false"/>
<Preference
android:key="pref_backups_location"
android:title="@string/pref_backups_location_title"
android:summary="@string/pref_backups_location_summary"
app:iconSpaceReserved="false"/>
<Preference
android:key="pref_backups_trigger"
android:title="@string/pref_backups_trigger_title"
android:summary="@string/pref_backups_trigger_summary"
app:iconSpaceReserved="false"/>
<Preference
android:key="pref_backups_versions"
android:title="@string/pref_backups_versions_title"
android:summary="@string/pref_backups_versions_summary"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:title="@string/pref_tools_group_title" android:title="@string/pref_tools_group_title"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">