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

View File

@ -64,4 +64,42 @@
android:text="@string/export_warning_accept"
android:checked="false"
android:visibility="gone" />
<CheckBox
android:id="@+id/export_selected_groups"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_marginTop="5dp"
android:text="@string/export_all_groups"
android:checked="true"/>
<TextView
android:id="@+id/select_groups_hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="30dp"
android:text="@string/export_choose_groups"
android:visibility="gone"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="20dp">
<LinearLayout
android:id="@+id/select_groups"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="20dp"
android:visibility="gone">
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_group"
android:checked="false"/>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -106,6 +106,9 @@
<string name="export_format_aegis">Aegis (.JSON)</string>
<string name="export_format_google_auth_uri">Text file (.TXT)</string>
<string name="export_format_hint">Export format</string>
<string name="export_all_groups">Export all groups</string>
<string name="export_choose_groups">Select which groups to export:</string>
<string name="export_no_groups_selected">No groups selected to export</string>
<string name="choose_authentication_method">Security</string>
<string name="authentication_method_explanation">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.</string>