mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-04-21 22:39:12 +00:00
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:
parent
866466d158
commit
4a69e9efb4
13 changed files with 475 additions and 46 deletions
|
@ -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"
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
Loading…
Add table
Reference in a new issue