From c45564d852258370618632349abf1fd95a36945b Mon Sep 17 00:00:00 2001 From: elena Date: Tue, 1 Nov 2022 21:49:39 +0000 Subject: [PATCH] Allow exporting specific groups --- .../ImportExportPreferencesFragment.java | 91 +++++++++++++++++-- .../beemdevelopment/aegis/vault/Vault.java | 14 ++- .../aegis/vault/VaultRepository.java | 33 +++++-- app/src/main/res/layout/dialog_export.xml | 38 ++++++++ app/src/main/res/values/strings.xml | 3 + 5 files changed, 165 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java index 103b6061..24cd1bd5 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java @@ -15,6 +15,7 @@ import android.widget.Toast; import androidx.annotation.ArrayRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.content.FileProvider; import androidx.preference.Preference; @@ -32,6 +33,7 @@ import com.beemdevelopment.aegis.ui.TransferEntriesActivity; import com.beemdevelopment.aegis.ui.dialogs.Dialogs; import com.beemdevelopment.aegis.ui.tasks.ExportTask; import com.beemdevelopment.aegis.ui.tasks.ImportFileTask; +import com.beemdevelopment.aegis.vault.Vault; import com.beemdevelopment.aegis.vault.VaultBackupManager; import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.vault.VaultFileCredentials; @@ -45,15 +47,18 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Random; +import java.util.Set; import javax.crypto.Cipher; public class ImportExportPreferencesFragment extends PreferencesFragment { // keep a reference to the type of database converter that was selected private DatabaseImporter.Definition _importerDef; + private Vault.EntryFilter _exportFilter; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @@ -153,6 +158,9 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { TextView warningText = view.findViewById(R.id.text_export_warning); CheckBox checkBoxEncrypt = view.findViewById(R.id.checkbox_export_encrypt); CheckBox checkBoxAccept = view.findViewById(R.id.checkbox_accept); + CheckBox checkBoxExportAllGroups = view.findViewById(R.id.export_selected_groups); + LinearLayout groupsSelection = view.findViewById(R.id.select_groups); + TextView groupsSelectionDescriptor = view.findViewById(R.id.select_groups_hint); TextView passwordInfoText = view.findViewById(R.id.text_separate_password); passwordInfoText.setVisibility(checkBoxEncrypt.isChecked() && isBackupPasswordSet ? View.VISIBLE : View.GONE); AutoCompleteTextView dropdown = view.findViewById(R.id.dropdown_export_format); @@ -165,6 +173,13 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { passwordInfoText.setVisibility(checkBoxEncrypt.isChecked() && isBackupPasswordSet ? View.VISIBLE : View.GONE); }); + for (String group: _vaultManager.getVault().getGroups()) { + CheckBox box = new CheckBox(requireContext()); + box.setText(group); + box.setChecked(false); + groupsSelection.addView(box); + } + AlertDialog dialog = new AlertDialog.Builder(requireContext()) .setTitle(R.string.pref_export_summary) .setView(view) @@ -191,6 +206,12 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { btnNeutral.setEnabled(isChecked); }); + checkBoxExportAllGroups.setOnCheckedChangeListener((button, isChecked) -> { + int visibility = isChecked ? View.GONE : View.VISIBLE; + groupsSelection.setVisibility(visibility); + groupsSelectionDescriptor.setVisibility(visibility); + }); + btnPos.setOnClickListener(v -> { dialog.dismiss(); @@ -198,6 +219,16 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { return; } + if (!checkBoxExportAllGroups.isChecked()) { + _exportFilter = getVaultEntryFilter(groupsSelection); + if (_exportFilter == null) { + Toast noGroupsSelected = new Toast(requireContext()); + noGroupsSelected.setText(R.string.export_no_groups_selected); + noGroupsSelected.show(); + return; + } + } + int pos = getStringResourceIndex(R.array.export_formats, dropdown.getText().toString()); int requestCode = getExportRequestCode(pos, checkBoxEncrypt.isChecked()); VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, checkBoxEncrypt.isChecked()); @@ -205,6 +236,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { .addCategory(Intent.CATEGORY_OPENABLE) .setType(getExportMimeType(requestCode)) .putExtra(Intent.EXTRA_TITLE, fileInfo.toString()); + _vaultManager.startActivityForResult(this, intent, requestCode); }); @@ -216,6 +248,16 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { return; } + if (!checkBoxExportAllGroups.isChecked()) { + _exportFilter = getVaultEntryFilter(groupsSelection); + if (_exportFilter == null) { + Toast noGroupsSelected = new Toast(requireContext()); + noGroupsSelected.setText(R.string.export_no_groups_selected); + noGroupsSelected.show(); + return; + } + } + File file; try { VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, checkBoxEncrypt.isChecked()); @@ -246,13 +288,28 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { .putExtra(Intent.EXTRA_STREAM, uri); Intent chooser = Intent.createChooser(intent, getString(R.string.pref_export_summary)); _vaultManager.startActivity(this, chooser); - }); + }, _exportFilter); + _exportFilter = null; }); }); Dialogs.showSecureDialog(dialog); } + private Vault.EntryFilter getVaultEntryFilter(LinearLayout view) { + Set groups = new HashSet<>(); + for (int i=0; i groups.contains(entry.getGroup()); + } + private void startGoogleAuthenticatorStyleExport() { ArrayList toExport = new ArrayList<>(); for (VaultEntry entry : _vaultManager.getVault().getEntries()) { @@ -329,11 +386,17 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { return dir; } - private void startExportVault(int requestCode, StartExportCallback cb) { + private void startExportVault(int requestCode, StartExportCallback cb, @Nullable Vault.EntryFilter filter) { switch (requestCode) { case CODE_EXPORT: if (_vaultManager.getVault().isEncryptionEnabled()) { - cb.exportVault(stream -> _vaultManager.getVault().export(stream)); + cb.exportVault(stream -> { + if (filter != null) { + _vaultManager.getVault().exportFiltered(stream, filter); + } else { + _vaultManager.getVault().export(stream); + } + }); } else { Dialogs.showSetPasswordDialog(requireActivity(), new Dialogs.PasswordSlotListener() { @Override @@ -348,7 +411,13 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { return; } - cb.exportVault(stream -> _vaultManager.getVault().export(stream, creds)); + cb.exportVault(stream -> { + if (filter != null) { + _vaultManager.getVault().exportFiltered(stream, creds, filter); + } else { + _vaultManager.getVault().export(stream, creds); + } + }); } @Override @@ -359,11 +428,18 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { } break; case CODE_EXPORT_PLAIN: - cb.exportVault((stream) -> _vaultManager.getVault().export(stream, null)); + cb.exportVault(stream -> { + if (filter != null) { + _vaultManager.getVault().exportFiltered(stream, null, filter); + } else { + _vaultManager.getVault().export(stream, null); + } + }); + _prefs.setIsPlaintextBackupWarningNeeded(true); break; case CODE_EXPORT_GOOGLE_URI: - cb.exportVault((stream) -> _vaultManager.getVault().exportGoogleUris(stream)); + cb.exportVault((stream) -> _vaultManager.getVault().exportGoogleUris(stream, filter)); _prefs.setIsPlaintextBackupWarningNeeded(true); break; } @@ -396,7 +472,8 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { e.printStackTrace(); } } - }); + }, _exportFilter); + _exportFilter = null; } private int getStringResourceIndex(@ArrayRes int id, String string) { diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/Vault.java b/app/src/main/java/com/beemdevelopment/aegis/vault/Vault.java index 1800dbb9..74ea038c 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/Vault.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/Vault.java @@ -1,5 +1,7 @@ package com.beemdevelopment.aegis.vault; +import androidx.annotation.Nullable; + import com.beemdevelopment.aegis.util.UUIDMap; import org.json.JSONArray; @@ -11,10 +13,16 @@ public class Vault { private UUIDMap _entries = new UUIDMap<>(); public JSONObject toJson() { + return toJson(null); + } + + public JSONObject toJson(@Nullable EntryFilter filter) { try { JSONArray array = new JSONArray(); for (VaultEntry e : _entries) { - array.put(e.toJson()); + if (filter == null || filter.includeEntry(e)) { + array.put(e.toJson()); + } } JSONObject obj = new JSONObject(); @@ -51,4 +59,8 @@ public class Vault { public UUIDMap getEntries() { return _entries; } + + public interface EntryFilter { + boolean includeEntry(VaultEntry entry); + } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java index bd80b490..4fff6440 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java @@ -141,17 +141,36 @@ public class VaultRepository { * Exports the vault by serializing it and writing it to the given OutputStream. If creds is * not null, it will be used to encrypt the vault first. */ - public void export(OutputStream stream, VaultFileCredentials creds) throws VaultRepositoryException { + public void export(OutputStream stream, @Nullable VaultFileCredentials creds) throws VaultRepositoryException { + exportFiltered(stream, creds, null); + } + + /** + * Exports the vault by serializing it and writing it to the given OutputStream. If encryption + * is enabled, the vault will be encrypted automatically. If filter is not null only specified + * entries will be exported + */ + public void exportFiltered(OutputStream stream, @Nullable Vault.EntryFilter filter) throws VaultRepositoryException { + exportFiltered(stream, getCredentials(), filter); + } + + /** + * Exports the vault by serializing it and writing it to the given OutputStream. If creds is + * not null, it will be used to encrypt the vault first. If filter is not null only specified + * entries will be exported + */ + public void exportFiltered(OutputStream stream, @Nullable VaultFileCredentials creds, @Nullable Vault.EntryFilter filter) throws VaultRepositoryException { if (creds != null) { creds = creds.exportable(); } try { VaultFile vaultFile = new VaultFile(); + if (creds != null) { - vaultFile.setContent(_vault.toJson(), creds); + vaultFile.setContent(_vault.toJson(filter), creds); } else { - vaultFile.setContent(_vault.toJson()); + vaultFile.setContent(_vault.toJson(filter)); } byte[] bytes = vaultFile.toBytes(); @@ -165,11 +184,13 @@ public class VaultRepository { * Exports the vault by serializing the list of entries to a newline-separated list of * Google Authenticator URI's and writing it to the given OutputStream. */ - public void exportGoogleUris(OutputStream outStream) throws VaultRepositoryException { + public void exportGoogleUris(OutputStream outStream, @Nullable Vault.EntryFilter filter) throws VaultRepositoryException { try (PrintStream stream = new PrintStream(outStream, false, StandardCharsets.UTF_8.name())) { for (VaultEntry entry : getEntries()) { - GoogleAuthInfo info = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer()); - stream.println(info.getUri().toString()); + if (filter == null || filter.includeEntry(entry)) { + GoogleAuthInfo info = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer()); + stream.println(info.getUri().toString()); + } } } catch (IOException e) { throw new VaultRepositoryException(e); diff --git a/app/src/main/res/layout/dialog_export.xml b/app/src/main/res/layout/dialog_export.xml index bad96118..d04ca043 100644 --- a/app/src/main/res/layout/dialog_export.xml +++ b/app/src/main/res/layout/dialog_export.xml @@ -64,4 +64,42 @@ android:text="@string/export_warning_accept" android:checked="false" android:visibility="gone" /> + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c1be0058..d2ba7030 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -104,6 +104,9 @@ Aegis (.JSON) Text file (.TXT) Export format + Export all groups + Select which groups to export: + No groups selected to export Security Aegis is a security-focused 2FA app. Tokens are stored in a vault, that can optionally be encrypted with a password of your choosing. If an attacker obtains your encrypted vault file, they will not be able to access the contents without knowing the password.\n\nWe\'ve preselected the option that we think would fit best for your device.