Merge pull request #1014 from orange-elephant/export-selected-groups

Allow exporting specific groups
This commit is contained in:
Alexander Bakker 2022-11-20 18:37:54 +01:00 committed by GitHub
commit fd5a0390f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 165 additions and 14 deletions

View file

@ -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<String> groups = new HashSet<>();
for (int i=0; i<view.getChildCount(); i++) {
CheckBox group = (CheckBox) view.getChildAt(i);
if (group.isChecked() && group.getText().toString().equals(getString(R.string.no_group))) {
groups.add(null);
} else if (group.isChecked()) {
groups.add(group.getText().toString());
}
}
return groups.isEmpty() ? null : entry -> groups.contains(entry.getGroup());
}
private void startGoogleAuthenticatorStyleExport() {
ArrayList<GoogleAuthInfo> toExport = new ArrayList<>();
for (VaultEntry entry : _vaultManager.getVault().getEntries()) {
@ -326,11 +383,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
@ -345,7 +408,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
@ -356,11 +425,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;
}
@ -393,7 +469,8 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
e.printStackTrace();
}
}
});
}, _exportFilter);
_exportFilter = null;
}
private int getStringResourceIndex(@ArrayRes int id, String string) {

View file

@ -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<VaultEntry> _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<VaultEntry> getEntries() {
return _entries;
}
public interface EntryFilter {
boolean includeEntry(VaultEntry entry);
}
}

View file

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