Refer to groups by UUID

- Also lays the foundations for adding entries to multiple groups and changing group names

Co-authored-by: Alexander Bakker <ab@alexbakker.me>
This commit is contained in:
elena 2023-04-30 15:31:38 +01:00 committed by Alexander Bakker
parent b84ecf15da
commit 5c86e5c099
24 changed files with 782 additions and 197 deletions

View File

@ -20,9 +20,11 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
public class Preferences {
@ -478,26 +480,26 @@ public class Preferences {
return _prefs.getBoolean("pref_minimize_on_copy", false);
}
public void setGroupFilter(List<String> groupFilter) {
public void setGroupFilter(Set<UUID> groupFilter) {
JSONArray json = new JSONArray(groupFilter);
_prefs.edit().putString("pref_group_filter", json.toString()).apply();
_prefs.edit().putString("pref_group_filter_uuids", json.toString()).apply();
}
public List<String> getGroupFilter() {
String raw = _prefs.getString("pref_group_filter", null);
public Set<UUID> getGroupFilter() {
String raw = _prefs.getString("pref_group_filter_uuids", null);
if (raw == null || raw.isEmpty()) {
return Collections.emptyList();
return Collections.emptySet();
}
try {
JSONArray json = new JSONArray(raw);
List<String> filter = new ArrayList<>();
Set<UUID> filter = new HashSet<>();
for (int i = 0; i < json.length(); i++) {
filter.add(json.isNull(i) ? null : json.optString(i));
filter.add(json.isNull(i) ? null : UUID.fromString(json.getString(i)));
}
return filter;
} catch (JSONException e) {
return Collections.emptyList();
return Collections.emptySet();
}
}

View File

@ -16,6 +16,7 @@ import com.beemdevelopment.aegis.vault.VaultEntryException;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.topjohnwu.superuser.io.SuFile;
@ -27,6 +28,7 @@ import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;
public class AegisImporter extends DatabaseImporter {
@ -132,11 +134,31 @@ public class AegisImporter extends DatabaseImporter {
Result result = new Result();
try {
JSONArray array = _obj.getJSONArray("entries");
for (int i = 0; i < array.length(); i++) {
JSONObject entryObj = array.getJSONObject(i);
if (_obj.has("groups")) {
JSONArray groupArray = _obj.getJSONArray("groups");
for (int i = 0; i < groupArray.length(); i++) {
JSONObject groupObj = groupArray.getJSONObject(i);
try {
VaultGroup group = convertGroup(groupObj);
if (!result.getGroups().has(group)) {
result.addGroup(group);
}
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
}
JSONArray entryArray = _obj.getJSONArray("entries");
for (int i = 0; i < entryArray.length(); i++) {
JSONObject entryObj = entryArray.getJSONObject(i);
try {
VaultEntry entry = convertEntry(entryObj);
for (UUID groupUuid : entry.getGroups()) {
if (!result.getGroups().has(groupUuid)) {
entry.getGroups().remove(groupUuid);
}
}
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
@ -156,5 +178,13 @@ public class AegisImporter extends DatabaseImporter {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
private static VaultGroup convertGroup(JSONObject obj) throws DatabaseImporterEntryException {
try {
return VaultGroup.fromJson(obj);
} catch (VaultEntryException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}

View File

@ -8,6 +8,7 @@ import androidx.annotation.StringRes;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
@ -168,12 +169,17 @@ public abstract class DatabaseImporter {
public static class Result {
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
private UUIDMap<VaultGroup> _groups = new UUIDMap<>();
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
public void addEntry(VaultEntry entry) {
_entries.add(entry);
}
public void addGroup(VaultGroup group) {
_groups.add(group);
}
public void addError(DatabaseImporterEntryException error) {
_errors.add(error);
}
@ -182,6 +188,10 @@ public abstract class DatabaseImporter {
return _entries;
}
public UUIDMap<VaultGroup> getGroups() {
return _groups;
}
public List<DatabaseImporterEntryException> getErrors() {
return _errors;
}

View File

@ -1,7 +1,7 @@
package com.beemdevelopment.aegis.ui;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
@ -15,7 +15,6 @@ import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.AdapterView;
import android.widget.AutoCompleteTextView;
import android.widget.ImageView;
import android.widget.LinearLayout;
@ -54,11 +53,13 @@ import com.beemdevelopment.aegis.otp.YandexInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
import com.beemdevelopment.aegis.ui.glide.IconLoader;
import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
import com.beemdevelopment.aegis.ui.tasks.ImportFileTask;
import com.beemdevelopment.aegis.ui.views.IconAdapter;
import com.beemdevelopment.aegis.util.Cloner;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
@ -73,11 +74,14 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.TreeSet;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
@ -90,7 +94,7 @@ public class EditEntryActivity extends AegisActivity {
private boolean _isNew = false;
private boolean _isManual = false;
private VaultEntry _origEntry;
private TreeSet<String> _groups;
private Collection<VaultGroup> _groups;
private boolean _hasCustomIcon = false;
// keep track of icon changes separately as the generated jpeg's are not deterministic
private boolean _hasChangedIcon = false;
@ -114,7 +118,7 @@ public class EditEntryActivity extends AegisActivity {
private AutoCompleteTextView _dropdownAlgo;
private TextInputLayout _dropdownAlgoLayout;
private AutoCompleteTextView _dropdownGroup;
private List<String> _dropdownGroupList = new ArrayList<>();
private List<VaultGroupModel> _dropdownGroupList = new ArrayList<>();
private KropView _kropView;
@ -262,8 +266,13 @@ public class EditEntryActivity extends AegisActivity {
updateAdvancedFieldStatus(_origEntry.getInfo().getTypeId());
updatePinFieldVisibility(_origEntry.getInfo().getTypeId());
String group = _origEntry.getGroup();
setGroup(group);
Set<UUID> groups = _origEntry.getGroups();
if (groups.isEmpty()) {
setGroup(new VaultGroupModel(getString(R.string.no_group)));
} else {
VaultGroup group = _vaultManager.getVault().getGroupByUUID(groups.iterator().next());
setGroup(new VaultGroupModel(group));
}
// Update the icon if the issuer or name has changed
_textIssuer.addTextChangedListener(_nameChangeListener);
@ -327,24 +336,31 @@ public class EditEntryActivity extends AegisActivity {
startIconSelection();
});
_dropdownGroup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
private int prevPosition = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString());
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (position == _dropdownGroupList.size() - 1) {
Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, text -> {
String groupName = new String(text);
if (!groupName.isEmpty()) {
_groups.add(groupName);
updateGroupDropdownList();
_dropdownGroup.setText(groupName, false);
_dropdownGroup.setOnItemClickListener((parent, view, position, id) -> {
VaultGroupModel selectedGroup = _dropdownGroupList.get(position);
if (selectedGroup.isPlaceholder() && Objects.equals(selectedGroup.getName(), getString(R.string.new_group))) {
Dialogs.TextInputListener onAddGroup = text -> {
String groupName = new String(text).trim();
if (!groupName.isEmpty()) {
VaultGroup group = _vaultManager.getVault().findGroupByName(groupName);
if (group == null) {
group = new VaultGroup(groupName);
_vaultManager.getVault().addGroup(group);
}
});
_dropdownGroup.setText(_dropdownGroupList.get(prevPosition), false);
} else {
prevPosition = position;
}
updateGroupDropdownList();
setGroup(new VaultGroupModel(group));
}
};
DialogInterface.OnCancelListener onCancel = dialogInterface -> {
VaultGroupModel previous = (VaultGroupModel) _dropdownGroup.getTag();
_dropdownGroup.setText(previous.getName(), false);
};
Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, onAddGroup, onCancel);
} else {
setGroup(_dropdownGroupList.get(position));
}
});
@ -365,13 +381,9 @@ public class EditEntryActivity extends AegisActivity {
_textPin.setHint(otpType.equals(MotpInfo.ID) ? R.string.motp_pin : R.string.yandex_pin);
}
private void setGroup(String groupName) {
int pos = 0;
if (groupName != null) {
pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() + 1 : 0;
}
_dropdownGroup.setText(_dropdownGroupList.get(pos), false);
private void setGroup(VaultGroupModel group) {
_dropdownGroup.setText(group.getName(), false);
_dropdownGroup.setTag(group);
}
private void openAdvancedSettings() {
@ -395,11 +407,10 @@ public class EditEntryActivity extends AegisActivity {
}
private void updateGroupDropdownList() {
Resources res = getResources();
_dropdownGroupList.clear();
_dropdownGroupList.add(res.getString(R.string.no_group));
_dropdownGroupList.addAll(_groups);
_dropdownGroupList.add(res.getString(R.string.new_group));
_dropdownGroupList.add(new VaultGroupModel(getString(R.string.new_group)));
_dropdownGroupList.addAll(_groups.stream().map(VaultGroupModel::new).collect(Collectors.toList()));
_dropdownGroupList.add(new VaultGroupModel(getString(R.string.no_group)));
}
private boolean hasUnsavedChanges(VaultEntry newEntry) {
@ -726,12 +737,13 @@ public class EditEntryActivity extends AegisActivity {
entry.setName(_textName.getText().toString());
entry.setNote(_textNote.getText().toString());
int groupPos = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString());
if (groupPos != 0) {
String group = _dropdownGroupList.get(groupPos);
entry.setGroup(group);
VaultGroupModel group = (VaultGroupModel) _dropdownGroup.getTag();
if (group.isPlaceholder()) {
entry.setGroups(new HashSet<>());
} else {
entry.setGroup(null);
Set<UUID> groups = new HashSet<>();
groups.add(group.getUUID());
entry.setGroups(groups);
}
if (_hasChangedIcon) {

View File

@ -14,16 +14,17 @@ import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.views.GroupAdapter;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultGroup;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener {
private GroupAdapter _adapter;
private HashSet<String> _removedGroups;
private HashSet<UUID> _removedGroups;
private RecyclerView _groupsView;
private View _emptyStateView;
private BackPressHandler _backPressHandler;
@ -43,11 +44,14 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
_backPressHandler = new BackPressHandler();
getOnBackPressedDispatcher().addCallback(this, _backPressHandler);
_removedGroups = new HashSet<>();
if (savedInstanceState != null) {
List<String> groups = savedInstanceState.getStringArrayList("removedGroups");
_removedGroups = new HashSet<>(Objects.requireNonNull(groups));
} else {
_removedGroups = new HashSet<>();
if (groups != null) {
for (String uuid : groups) {
_removedGroups.add(UUID.fromString(uuid));
}
}
}
_adapter = new GroupAdapter(this);
@ -57,8 +61,10 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
_groupsView.setAdapter(_adapter);
_groupsView.setNestedScrollingEnabled(false);
for (String group : _vaultManager.getVault().getGroups()) {
_adapter.addGroup(group);
for (VaultGroup group : _vaultManager.getVault().getGroups()) {
if (!_removedGroups.contains(group.getUUID())) {
_adapter.addGroup(group);
}
}
_emptyStateView = findViewById(R.id.vEmptyList);
@ -68,16 +74,21 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putStringArrayList("removedGroups", new ArrayList<>(_removedGroups));
ArrayList<String> removed = new ArrayList<>();
for (UUID uuid : _removedGroups) {
removed.add(uuid.toString());
}
outState.putStringArrayList("removedGroups", removed);
}
@Override
public void onRemoveGroup(String group) {
public void onRemoveGroup(VaultGroup group) {
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(R.string.remove_group)
.setMessage(R.string.remove_group_description)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
_removedGroups.add(group);
_removedGroups.add(group.getUUID());
_adapter.removeGroup(group);
_backPressHandler.setEnabled(true);
updateEmptyState();
@ -86,12 +97,29 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
.create());
}
public void onRemoveUnusedGroups() {
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(R.string.remove_unused_groups)
.setMessage(R.string.remove_unused_groups_description)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
Set<VaultGroup> unusedGroups = new HashSet<>(_vaultManager.getVault().getGroups());
unusedGroups.removeAll(_vaultManager.getVault().getUsedGroups());
for (VaultGroup group : unusedGroups) {
_removedGroups.add(group.getUUID());
_adapter.removeGroup(group);
}
_backPressHandler.setEnabled(true);
updateEmptyState();
})
.setNegativeButton(android.R.string.no, null)
.create());
}
private void saveAndFinish() {
if (!_removedGroups.isEmpty()) {
for (VaultEntry entry : _vaultManager.getVault().getEntries()) {
if (_removedGroups.contains(entry.getGroup())) {
entry.setGroup(null);
}
for (UUID uuid : _removedGroups) {
_vaultManager.getVault().removeGroup(uuid);
}
saveAndBackupVault();
@ -126,6 +154,9 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
case R.id.action_save:
saveAndFinish();
break;
case R.id.action_delete_unused_groups:
onRemoveUnusedGroups();
break;
default:
return super.onOptionsItemSelected(item);
}

View File

@ -25,6 +25,7 @@ import com.beemdevelopment.aegis.ui.tasks.RootShellTask;
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
@ -37,6 +38,7 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public class ImportEntriesActivity extends AegisActivity {
@ -45,6 +47,8 @@ public class ImportEntriesActivity extends AegisActivity {
private ImportEntriesAdapter _adapter;
private FabScrollHelper _fabScrollHelper;
private UUIDMap<VaultGroup> _importedGroups;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -205,6 +209,8 @@ public class ImportEntriesActivity extends AegisActivity {
importEntries.add(importEntry);
}
_importedGroups = result.getGroups();
List<DatabaseImporterEntryException> errors = result.getErrors();
if (errors.size() > 0) {
String message = getResources().getQuantityString(R.plurals.import_error_dialog, errors.size(), errors.size());
@ -225,10 +231,43 @@ public class ImportEntriesActivity extends AegisActivity {
private void saveAndFinish(boolean wipeEntries) {
VaultRepository vault = _vaultManager.getVault();
if (wipeEntries) {
vault.wipeEntries();
vault.wipeContents();
}
// Given the list of selected entries, collect the UUID's of all groups
// that we're actually going to import
List<ImportEntry> selectedEntries = _adapter.getCheckedEntries();
List<UUID> selectedGroupUuids = new ArrayList<>();
for (ImportEntry entry : selectedEntries) {
selectedGroupUuids.addAll(entry.getEntry().getGroups());
}
// Add all of the new groups to the vault. If a group with the same name already
// exists in the vault, rewrite all entries in that group to reference the existing group.
for (VaultGroup importedGroup : _importedGroups) {
if (!selectedGroupUuids.contains(importedGroup.getUUID())) {
continue;
}
VaultGroup existingGroup = vault.findGroupByUUID(importedGroup.getUUID());
if (existingGroup != null) {
continue;
}
existingGroup = vault.findGroupByName(importedGroup.getName());
if (existingGroup == null) {
vault.addGroup(importedGroup);
} else {
for (ImportEntry entry : selectedEntries) {
Set<UUID> entryGroups = entry.getEntry().getGroups();
if (entryGroups.contains(importedGroup.getUUID())) {
entryGroups.remove(importedGroup.getUUID());
entryGroups.add(existingGroup.getUUID());
}
}
}
}
for (ImportEntry selectedEntry : selectedEntries) {
VaultEntry entry = selectedEntry.getEntry();

View File

@ -63,7 +63,7 @@ import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeSet;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@ -632,7 +632,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
startAuthActivity(false);
} else if (_loaded) {
// update the list of groups in the entry list view so that the chip gets updated
_entryListView.setGroups(_vaultManager.getVault().getGroups());
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
// update the usage counts in case they are edited outside of the EntryListView
_entryListView.setUsageCounts(_prefs.getUsageCounts());
@ -670,7 +670,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
updateLockIcon();
if (_loaded) {
_entryListView.setGroups(_vaultManager.getVault().getGroups());
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
updateSortCategoryMenu();
}
@ -980,7 +980,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
public void onListChange() { _fabScrollHelper.setVisible(true); }
@Override
public void onSaveGroupFilter(List<String> groupFilter) {
public void onSaveGroupFilter(Set<UUID> groupFilter) {
_prefs.setGroupFilter(groupFilter);
}
@ -1128,17 +1128,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
case R.id.action_delete:
Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> {
deleteEntries(_selectedEntries);
for (VaultEntry entry : _selectedEntries) {
if (entry.getGroup() != null) {
TreeSet<String> groups = _vaultManager.getVault().getGroups();
if (!groups.contains(entry.getGroup())) {
_entryListView.setGroups(groups);
break;
}
}
}
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
mode.finish();
});
return true;

View File

@ -18,19 +18,19 @@ import androidx.appcompat.widget.AppCompatAutoCompleteTextView;
import com.beemdevelopment.aegis.R;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
public class DropdownCheckBoxes<T> extends AppCompatAutoCompleteTextView {
private @PluralsRes int _selectedCountPlural = R.plurals.dropdown_checkboxes_default_count;
private boolean _allowFiltering = false;
private final List<String> _items = new ArrayList<>();
private List<String> _visibleItems = new ArrayList<>();
private final Set<String> _checkedItems = new TreeSet<>();
private final List<T> _items = new ArrayList<>();
private List<T> _visibleItems = new ArrayList<>();
private final Set<T> _checkedItems = new HashSet<>();
private CheckboxAdapter _adapter;
@ -70,7 +70,15 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
}
}
public void addItems(List<String> items, boolean startChecked) {
/**
* Add parameterized items to be displayed as a checkbox in the dropdown view
* the label for the checkbox is determined by the toString() method of the items
* you add.
*
* @param items a list of the items you want to show in the dropdown
* @param startChecked whether the checkbox should be checked initially
*/
public void addItems(List<T> items, boolean startChecked) {
_items.addAll(items);
_visibleItems.addAll(items);
@ -97,7 +105,7 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
_selectedCountPlural = resId;
}
public Set<String> getCheckedItems() {
public Set<T> getCheckedItems() {
return _checkedItems;
}
@ -109,7 +117,7 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
}
@Override
public String getItem(int i) {
public T getItem(int i) {
return _visibleItems.get(i);
}
@ -124,19 +132,18 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.dropdown_checkbox, viewGroup, false);
}
String item = _visibleItems.get(i);
T item = _visibleItems.get(i);
CheckBox checkBox = convertView.findViewById(R.id.checkbox_in_dropdown);
checkBox.setText(item);
checkBox.setText(item.toString());
checkBox.setTag(item);
checkBox.setChecked(_checkedItems.contains(item));
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
String label = buttonView.getText().toString();
if (isChecked) {
_checkedItems.add(label);
_checkedItems.add((T) buttonView.getTag());
} else {
_checkedItems.remove(label);
_checkedItems.remove((T) buttonView.getTag());
}
updateCheckedItemsCountText();
@ -153,9 +160,9 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
FilterResults results = new FilterResults();
results.values = (query == null || query.toString().isEmpty())
? _items
: _items.stream().filter(str -> {
: _items.stream().filter(item -> {
String q = query.toString().toLowerCase();
String strLower = str.toLowerCase();
String strLower = item.toString().toLowerCase();
return strLower.contains(q);
})
@ -166,7 +173,7 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
@Override
protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
_visibleItems = (List<String>) filterResults.values;
_visibleItems = (List<T>) filterResults.values;
notifyDataSetChanged();
}
};

View File

@ -25,6 +25,7 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.ComponentActivity;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
@ -242,8 +243,8 @@ public class Dialogs {
showTextInputDialog(context, titleId, 0, hintId, listener, null, isSecret);
}
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) {
showTextInputDialog(context, titleId, hintId, listener, false);
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, @Nullable DialogInterface.OnCancelListener onCancel) {
showTextInputDialog(context, titleId, 0, hintId, listener, onCancel, false);
}
public static void showPasswordInputDialog(Context context, TextInputListener listener) {

View File

@ -31,12 +31,14 @@ import com.beemdevelopment.aegis.ui.ImportEntriesActivity;
import com.beemdevelopment.aegis.ui.TransferEntriesActivity;
import com.beemdevelopment.aegis.ui.components.DropdownCheckBoxes;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
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;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
@ -48,12 +50,14 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.crypto.Cipher;
@ -164,7 +168,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
CheckBox checkBoxAccept = view.findViewById(R.id.checkbox_accept);
CheckBox checkBoxExportAllGroups = view.findViewById(R.id.export_selected_groups);
TextInputLayout groupsSelectionLayout = view.findViewById(R.id.group_selection_layout);
DropdownCheckBoxes groupsSelection = view.findViewById(R.id.group_selection_dropdown);
DropdownCheckBoxes<VaultGroupModel> groupsSelection = view.findViewById(R.id.group_selection_dropdown);
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);
@ -177,13 +181,13 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
passwordInfoText.setVisibility(checkBoxEncrypt.isChecked() && isBackupPasswordSet ? View.VISIBLE : View.GONE);
});
TreeSet<String> groups = _vaultManager.getVault().getGroups();
Collection<VaultGroup> groups = _vaultManager.getVault().getUsedGroups();
if (groups.size() > 0) {
checkBoxExportAllGroups.setVisibility(View.VISIBLE);
ArrayList<String> groupsArray = new ArrayList<>();
groupsArray.add(getString(R.string.no_group));
groupsArray.addAll(groups);
ArrayList<VaultGroupModel> groupsArray = new ArrayList<>();
groupsArray.add(new VaultGroupModel(getString(R.string.no_group)));
groupsArray.addAll(groups.stream().map(VaultGroupModel::new).collect(Collectors.toList()));
groupsSelection.setCheckedItemsCountTextRes(R.plurals.export_groups_selected_count);
groupsSelection.addItems(groupsArray, false);
@ -319,17 +323,19 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
Dialogs.showSecureDialog(dialog);
}
private Vault.EntryFilter getVaultEntryFilter(DropdownCheckBoxes dropdownCheckBoxes) {
Set<String> groups = new HashSet<>();
for (String group: dropdownCheckBoxes.getCheckedItems()) {
if (group.equals(getString(R.string.no_group))) {
groups.add(null);
} else {
groups.add(group);
}
private Vault.EntryFilter getVaultEntryFilter(DropdownCheckBoxes<VaultGroupModel> dropdownCheckBoxes) {
Set<UUID> groups = new HashSet<>();
for (VaultGroupModel group : dropdownCheckBoxes.getCheckedItems()) {
groups.add(group.getUUID());
}
return groups.isEmpty() ? null : entry -> groups.contains(entry.getGroup());
return groups.isEmpty() ? null : entry -> {
if (entry.getGroups().isEmpty()) {
return groups.contains(null);
} else {
return entry.getGroups().stream().anyMatch(groups::contains);
}
};
}
private void startGoogleAuthenticatorStyleExport() {

View File

@ -0,0 +1,45 @@
package com.beemdevelopment.aegis.ui.models;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.vault.VaultGroup;
import java.io.Serializable;
import java.util.UUID;
public class VaultGroupModel implements Serializable {
private final VaultGroup _group;
private final String _placeholderName;
public VaultGroupModel(VaultGroup group) {
_group = group;
_placeholderName = null;
}
public VaultGroupModel(String placeholderName) {
_group = null;
_placeholderName = placeholderName;
}
public VaultGroup getGroup() {
return _group;
}
public String getName() {
return _group != null ? _group.getName() : _placeholderName;
}
public boolean isPlaceholder() {
return _group == null;
}
@Nullable
public UUID getUUID() {
return _group == null ? null : _group.getUUID();
}
@NonNull
@Override
public String toString() {
return getName();
}
}

View File

@ -39,6 +39,8 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
@ -59,7 +61,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
private boolean _tapToReveal;
private int _tapToRevealTime;
private CopyBehavior _copyBehavior;
private List<String> _groupFilter;
private Set<UUID> _groupFilter;
private SortCategory _sortCategory;
private ViewMode _viewMode;
private String _searchFilter;
@ -76,7 +78,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
_entries = new ArrayList<>();
_shownEntries = new ArrayList<>();
_selectedEntries = new ArrayList<>();
_groupFilter = new ArrayList<>();
_groupFilter = new TreeSet<>();
_holders = new ArrayList<>();
_dimHandler = new Handler();
_doubleTapHandler = new Handler();
@ -246,12 +248,15 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
}
private boolean isEntryFiltered(VaultEntry entry) {
String group = entry.getGroup();
Set<UUID> groups = entry.getGroups();
String issuer = entry.getIssuer().toLowerCase();
String name = entry.getName().toLowerCase();
if (!_groupFilter.isEmpty()) {
if (!_groupFilter.contains(group)) {
if (groups.isEmpty() && !_groupFilter.contains(null)) {
return true;
}
if (!groups.isEmpty() && _groupFilter.stream().filter(Objects::nonNull).noneMatch(groups::contains)) {
return true;
}
}
@ -274,7 +279,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
}
}
public void setGroupFilter(@NonNull List<String> groups) {
public void setGroupFilter(@NonNull Set<UUID> groups) {
if (_groupFilter.equals(groups)) {
return;
}
@ -352,10 +357,6 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
return (int) _shownEntries.stream().filter(VaultEntry::isFavorite).count();
}
public void setGroups(TreeSet<String> groups) {
_view.setGroups(groups);
}
@Override
public void onItemDismiss(int position) {

View File

@ -37,7 +37,10 @@ import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.glide.IconLoader;
import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.bumptech.glide.Glide;
import com.bumptech.glide.ListPreloader;
import com.bumptech.glide.RequestBuilder;
@ -54,7 +57,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@ -71,11 +74,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
private TotpProgressBar _progressBar;
private boolean _showProgress;
private ViewMode _viewMode;
private TreeSet<String> _groups;
private Collection<VaultGroup> _groups;
private LinearLayout _emptyStateView;
private Chip _groupChip;
private List<String> _groupFilter;
private List<String> _prefGroupFilter;
private Set<UUID> _groupFilter;
private Set<UUID> _prefGroupFilter;
private UiRefresher _refresher;
@ -168,7 +171,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
super.onDestroyView();
}
public void setGroupFilter(List<String> groups, boolean animate) {
public void setGroupFilter(Set<UUID> groups, boolean animate) {
_groupFilter = groups;
_adapter.setGroupFilter(groups);
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
@ -334,7 +337,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
}
public void setPrefGroupFilter(List<String> groupFilter) {
public void setPrefGroupFilter(Set<UUID> groupFilter) {
_prefGroupFilter = groupFilter;
}
@ -465,17 +468,17 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_recyclerView.scheduleLayoutAnimation();
}
private void addChipTo(ChipGroup chipGroup, String group) {
private void addChipTo(ChipGroup chipGroup, VaultGroupModel group) {
Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_material, null, false);
chip.setText(group == null ? getString(R.string.no_group) : group);
chip.setText(group.getName());
chip.setCheckable(true);
chip.setChecked(_groupFilter != null && _groupFilter.contains(group));
chip.setChecked(_groupFilter != null && _groupFilter.contains(group.getUUID()));
chip.setCheckedIconVisible(true);
chip.setOnCheckedChangeListener((group1, checkedId) -> {
List<String> groupFilter = getGroupFilter(chipGroup);
Set<UUID> groupFilter = getGroupFilter(chipGroup);
setGroupFilter(groupFilter, true);
});
chip.setTag(group == null ? new Object() : null);
chip.setTag(group);
chipGroup.addView(chip);
}
@ -489,7 +492,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
Button saveButton = view.findViewById(R.id.btnSave);
clearButton.setOnClickListener(v -> {
chipGroup.clearCheck();
List<String> groupFilter = Collections.emptyList();
Set<UUID> groupFilter = Collections.emptySet();
if (_listener != null) {
_listener.onSaveGroupFilter(groupFilter);
}
@ -498,7 +501,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
});
saveButton.setOnClickListener(v -> {
List<String> groupFilter = getGroupFilter(chipGroup);
Set<UUID> groupFilter = getGroupFilter(chipGroup);
if (_listener != null) {
_listener.onSaveGroupFilter(groupFilter);
}
@ -509,25 +512,23 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_groupChip.setOnClickListener(v -> {
chipGroup.removeAllViews();
for (String group : _groups) {
addChipTo(chipGroup, group);
for (VaultGroup group : _groups) {
addChipTo(chipGroup, new VaultGroupModel(group));
}
addChipTo(chipGroup, null);
addChipTo(chipGroup, new VaultGroupModel(getString(R.string.no_group)));
Dialogs.showSecureDialog(dialog);
});
}
private static List<String> getGroupFilter(ChipGroup chipGroup) {
private static Set<UUID> getGroupFilter(ChipGroup chipGroup) {
return chipGroup.getCheckedChipIds().stream()
.map(i -> {
Chip chip = chipGroup.findViewById(i);
if (chip.getTag() != null) {
return null;
}
return chip. getText().toString();
VaultGroupModel group = (VaultGroupModel) chip.getTag();
return group.getUUID();
})
.collect(Collectors.toList());
.collect(Collectors.toSet());
}
private void updateGroupChip() {
@ -543,29 +544,31 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
updateDividerDecoration();
}
public void setGroups(TreeSet<String> groups) {
public void setGroups(Collection<VaultGroup> groups) {
_groups = groups;
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
updateDividerDecoration();
if (_prefGroupFilter != null) {
List<String> groupFilter = cleanGroupFilter(_prefGroupFilter);
Set<UUID> groupFilter = cleanGroupFilter(_prefGroupFilter);
_prefGroupFilter = null;
if (!groupFilter.isEmpty()) {
setGroupFilter(groupFilter, false);
}
} else if (_groupFilter != null) {
List<String> groupFilter = cleanGroupFilter(_groupFilter);
Set<UUID> groupFilter = cleanGroupFilter(_groupFilter);
if (!_groupFilter.equals(groupFilter)) {
setGroupFilter(groupFilter, true);
}
}
}
private List<String> cleanGroupFilter(List<String> groupFilter) {
return groupFilter.stream()
.filter(g -> g == null || _groups.contains(g))
.collect(Collectors.toList());
private Set<UUID> cleanGroupFilter(Set<UUID> groupFilter) {
Set<UUID> groupUuids = _groups.stream().map(UUIDMap.Value::getUUID).collect(Collectors.toSet());
return groupFilter.stream()
.filter(g -> g == null || groupUuids.contains(g))
.collect(Collectors.toSet());
}
private void updateDividerDecoration() {
@ -616,7 +619,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
void onSelect(VaultEntry entry);
void onDeselect(VaultEntry entry);
void onListChange();
void onSaveGroupFilter(List<String> groupFilter);
void onSaveGroupFilter(Set<UUID> groupFilter);
void onEntryListTouch();
}

View File

@ -7,19 +7,20 @@ import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.vault.VaultGroup;
import java.util.ArrayList;
public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
private GroupAdapter.Listener _listener;
private ArrayList<String> _groups;
private ArrayList<VaultGroup> _groups;
public GroupAdapter(GroupAdapter.Listener listener) {
_listener = listener;
_groups = new ArrayList<>();
}
public void addGroup(String group) {
public void addGroup(VaultGroup group) {
_groups.add(group);
int position = getItemCount() - 1;
@ -30,7 +31,7 @@ public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
}
}
public void removeGroup(String group) {
public void removeGroup(VaultGroup group) {
int position = _groups.indexOf(group);
_groups.remove(position);
notifyItemRemoved(position);
@ -57,6 +58,6 @@ public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
}
public interface Listener {
void onRemoveGroup(String group);
void onRemoveGroup(VaultGroup group);
}
}

View File

@ -7,6 +7,7 @@ import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.vault.VaultGroup;
public class GroupHolder extends RecyclerView.ViewHolder {
private TextView _slotName;
@ -18,8 +19,8 @@ public class GroupHolder extends RecyclerView.ViewHolder {
_buttonDelete = view.findViewById(R.id.button_delete);
}
public void setData(String groupName) {
_slotName.setText(groupName);
public void setData(VaultGroup group) {
_slotName.setText(group.getName());
}
public void setOnDeleteClickListener(View.OnClickListener listener) {

View File

@ -9,6 +9,7 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
/**
@ -99,7 +100,14 @@ public class UUIDMap <T extends UUIDMap.Value> implements Iterable<T>, Serializa
* Reports whether the internal map contains a value with the UUID of the given value.
*/
public boolean has(T value) {
return _map.containsKey(value.getUUID());
return has(value.getUUID());
}
/**
* Reports whether the internal map contains a value with the given UUID.
*/
public boolean has(UUID uuid) {
return _map.containsKey(uuid);
}
/**
@ -161,7 +169,7 @@ public class UUIDMap <T extends UUIDMap.Value> implements Iterable<T>, Serializa
return false;
}
return getUUID().equals(((Value) o).getUUID());
return Objects.equals(getUUID(), ((Value) o).getUUID());
}
}
}

View File

@ -8,9 +8,13 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Optional;
import java.util.UUID;
public class Vault {
private static final int VERSION = 2;
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
private static final int VERSION = 3;
private final UUIDMap<VaultEntry> _entries = new UUIDMap<>();
private final UUIDMap<VaultGroup> _groups = new UUIDMap<>();
public JSONObject toJson() {
return toJson(null);
@ -18,16 +22,24 @@ public class Vault {
public JSONObject toJson(@Nullable EntryFilter filter) {
try {
JSONArray array = new JSONArray();
JSONArray entriesArray = new JSONArray();
for (VaultEntry e : _entries) {
if (filter == null || filter.includeEntry(e)) {
array.put(e.toJson());
entriesArray.put(e.toJson());
}
}
// Always include all groups, even if they're not assigned to any entry (before or after the entry filter)
JSONArray groupsArray = new JSONArray();
for (VaultGroup group : _groups) {
groupsArray.put(group.toJson());
}
JSONObject obj = new JSONObject();
obj.put("version", VERSION);
obj.put("entries", array);
obj.put("entries", entriesArray);
obj.put("groups", groupsArray);
return obj;
} catch (JSONException e) {
throw new RuntimeException(e);
@ -37,6 +49,7 @@ public class Vault {
public static Vault fromJson(JSONObject obj) throws VaultException {
Vault vault = new Vault();
UUIDMap<VaultEntry> entries = vault.getEntries();
UUIDMap<VaultGroup> groups = vault.getGroups();
try {
int ver = obj.getInt("version");
@ -44,9 +57,28 @@ public class Vault {
throw new VaultException("Unsupported version");
}
if (obj.has("groups")) {
JSONArray groupsArray = obj.getJSONArray("groups");
for (int i = 0; i < groupsArray.length(); i++) {
VaultGroup group = VaultGroup.fromJson(groupsArray.getJSONObject(i));
if (!groups.has(group)) {
groups.add(group);
}
}
}
JSONArray array = obj.getJSONArray("entries");
for (int i = 0; i < array.length(); i++) {
VaultEntry entry = VaultEntry.fromJson(array.getJSONObject(i));
vault.migrateOldGroup(entry);
// check the vault has a group corresponding to each one the entry claims to be in
for (UUID groupUuid: entry.getGroups()) {
if (!groups.has(groupUuid)) {
entry.removeGroup(groupUuid);
}
}
entries.add(entry);
}
} catch (VaultEntryException | JSONException e) {
@ -56,10 +88,33 @@ public class Vault {
return vault;
}
public void migrateOldGroup(VaultEntry entry) {
if (entry.getOldGroup() != null) {
Optional<VaultGroup> optGroup = getGroups().getValues()
.stream()
.filter(g -> g.getName().equals(entry.getOldGroup()))
.findFirst();
if (optGroup.isPresent()) {
entry.addGroup(optGroup.get().getUUID());
} else {
VaultGroup group = new VaultGroup(entry.getOldGroup());
getGroups().add(group);
entry.addGroup(group.getUUID());
}
entry.setOldGroup(null);
}
}
public UUIDMap<VaultEntry> getEntries() {
return _entries;
}
public UUIDMap<VaultGroup> getGroups() {
return _groups;
}
public interface EntryFilter {
boolean includeEntry(VaultEntry entry);
}

View File

@ -10,23 +10,26 @@ import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.util.UUIDMap;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Arrays;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
public class VaultEntry extends UUIDMap.Value {
private String _name = "";
private String _issuer = "";
private String _group;
private OtpInfo _info;
private byte[] _icon;
private IconType _iconType = IconType.INVALID;
private boolean _isFavorite;
private int _usageCount;
private String _note = "";
private String _oldGroup;
private Set<UUID> _groups = new TreeSet<>();
private VaultEntry(UUID uuid, OtpInfo info) {
super(uuid);
@ -44,13 +47,6 @@ public class VaultEntry extends UUIDMap.Value {
setIssuer(issuer);
}
public VaultEntry(OtpInfo info, String name, String issuer, String group) {
this(info);
setName(name);
setIssuer(issuer);
setGroup(group);
}
public VaultEntry(GoogleAuthInfo info) {
this(info.getOtpInfo(), info.getAccountName(), info.getIssuer());
}
@ -63,12 +59,18 @@ public class VaultEntry extends UUIDMap.Value {
obj.put("uuid", getUUID().toString());
obj.put("name", _name);
obj.put("issuer", _issuer);
obj.put("group", _group);
obj.put("note", _note);
obj.put("favorite", _isFavorite);
obj.put("icon", _icon == null ? JSONObject.NULL : Base64.encode(_icon));
obj.put("icon_mime", _icon == null ? null : _iconType.toMimeType());
obj.put("info", _info.toJson());
JSONArray groupUuids = new JSONArray();
for (UUID uuid : _groups) {
groupUuids.put(uuid.toString());
}
obj.put("groups", groupUuids);
} catch (JSONException e) {
throw new RuntimeException(e);
}
@ -90,10 +92,21 @@ public class VaultEntry extends UUIDMap.Value {
VaultEntry entry = new VaultEntry(uuid, info);
entry.setName(obj.getString("name"));
entry.setIssuer(obj.getString("issuer"));
entry.setGroup(obj.optString("group", null));
entry.setNote(obj.optString("note", ""));
entry.setIsFavorite(obj.optBoolean("favorite", false));
// If the entry contains a list of group UUID's, assume conversion from the
// old group system has already taken place and ignore the old group field.
if (obj.has("groups")) {
JSONArray groups = obj.getJSONArray("groups");
for (int i = 0; i < groups.length(); i++) {
String groupUuid = groups.getString(i);
entry.addGroup(UUID.fromString(groupUuid));
}
} else if (obj.has("group")) {
entry.setOldGroup(JsonUtils.optString(obj, "group"));
}
Object icon = obj.get("icon");
if (icon != JSONObject.NULL) {
String mime = JsonUtils.optString(obj, "icon_mime");
@ -121,8 +134,8 @@ public class VaultEntry extends UUIDMap.Value {
return _issuer;
}
public String getGroup() {
return _group;
public Set<UUID> getGroups() {
return _groups;
}
public byte[] getIcon() {
@ -153,8 +166,22 @@ public class VaultEntry extends UUIDMap.Value {
_issuer = issuer;
}
public void setGroup(String group) {
_group = group;
public void addGroup(UUID group) {
if (group == null) {
throw new AssertionError("Attempt to add null group to entry's group list");
}
_groups.add(group);
}
public void removeGroup(UUID group) {
_groups.remove(group);
}
public void setGroups(Set<UUID> groups) {
if (groups.contains(null)) {
throw new AssertionError("Attempt to add null group to entry's group list");
}
_groups = groups;
}
public void setInfo(OtpInfo info) {
@ -176,6 +203,14 @@ public class VaultEntry extends UUIDMap.Value {
public void setIsFavorite(boolean isFavorite) { _isFavorite = isFavorite; }
void setOldGroup(String oldGroup) {
_oldGroup = oldGroup;
}
String getOldGroup() {
return _oldGroup;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof VaultEntry)) {
@ -194,12 +229,12 @@ public class VaultEntry extends UUIDMap.Value {
public boolean equivalates(VaultEntry entry) {
return getName().equals(entry.getName())
&& getIssuer().equals(entry.getIssuer())
&& Objects.equals(getGroup(), entry.getGroup())
&& getInfo().equals(entry.getInfo())
&& Arrays.equals(getIcon(), entry.getIcon())
&& getIconType().equals(entry.getIconType())
&& getNote().equals(entry.getNote())
&& isFavorite() == entry.isFavorite();
&& isFavorite() == entry.isFavorite()
&& getGroups().equals(entry.getGroups());
}
/**

View File

@ -0,0 +1,69 @@
package com.beemdevelopment.aegis.vault;
import com.beemdevelopment.aegis.util.UUIDMap;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.UUID;
public class VaultGroup extends UUIDMap.Value {
private String _name;
private VaultGroup(UUID uuid, String name) {
super(uuid);
_name = name;
}
public VaultGroup(String name) {
super();
_name = name;
}
public JSONObject toJson() {
JSONObject obj = new JSONObject();
try {
obj.put("uuid", getUUID().toString());
obj.put("name", _name);
} catch (JSONException e) {
throw new RuntimeException(e);
}
return obj;
}
public static VaultGroup fromJson(JSONObject obj) throws VaultEntryException {
try {
UUID uuid = UUID.fromString(obj.getString("uuid"));
String groupName = obj.getString("name");
return new VaultGroup(uuid, groupName);
} catch (JSONException e) {
throw new VaultEntryException(e);
}
}
@Override
public boolean equals(Object o) {
if (!(o instanceof VaultGroup)) {
return false;
}
VaultGroup entry = (VaultGroup) o;
return super.equals(entry) && getName().equals(entry.getName());
}
public String getName() {
return _name;
}
public void setName(String name) {
_name = name;
}
@Override
public String toString() {
return _name;
}
}

View File

@ -20,9 +20,9 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.text.Collator;
import java.util.Collection;
import java.util.TreeSet;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@ -220,6 +220,8 @@ public class VaultRepository {
}
public void addEntry(VaultEntry entry) {
// Entries added by importing a file may contain an old group that needs to be migrated
_vault.migrateOldGroup(entry);
_vault.getEntries().add(entry);
}
@ -231,8 +233,12 @@ public class VaultRepository {
return _vault.getEntries().remove(entry);
}
public void wipeEntries() {
/**
* Wipes all entries and groups from the vault.
*/
public void wipeContents() {
_vault.getEntries().wipe();
_vault.getGroups().wipe();
}
public VaultEntry replaceEntry(VaultEntry entry) {
@ -240,8 +246,8 @@ public class VaultRepository {
}
/**
* Moves entry1 to the position of entry2.
*/
* Moves entry1 to the position of entry2.
*/
public void moveEntry(VaultEntry entry1, VaultEntry entry2) {
_vault.getEntries().move(entry1, entry2);
}
@ -254,15 +260,54 @@ public class VaultRepository {
return _vault.getEntries().getValues();
}
public TreeSet<String> getGroups() {
TreeSet<String> groups = new TreeSet<>(Collator.getInstance());
public void addGroup(VaultGroup group) {
_vault.getGroups().add(group);
}
public VaultGroup getGroupByUUID(UUID uuid) {
return _vault.getGroups().getByUUID(uuid);
}
@Nullable
public VaultGroup findGroupByUUID(UUID uuid) {
return _vault.getGroups().has(uuid) ? _vault.getGroups().getByUUID(uuid) : null;
}
@Nullable
public VaultGroup findGroupByName(String name) {
return _vault.getGroups().getValues()
.stream()
.filter(g -> g.getName().equals(name))
.findFirst()
.orElse(null);
}
public void removeGroup(UUID groupUuid) {
VaultGroup group = _vault.getGroups().getByUUID(groupUuid);
removeGroup(group);
}
public void removeGroup(VaultGroup group) {
for (VaultEntry entry : getEntries()) {
String group = entry.getGroup();
if (group != null) {
groups.add(group);
}
entry.removeGroup(group.getUUID());
}
return groups;
_vault.getGroups().remove(group);
}
public Collection<VaultGroup> getGroups() {
return _vault.getGroups().getValues();
}
public Collection<VaultGroup> getUsedGroups() {
Set<UUID> usedGroups = new HashSet<>();
for (VaultEntry entry : getEntries()) {
usedGroups.addAll(entry.getGroups());
}
return getGroups().stream()
.filter(vg -> usedGroups.contains(vg.getUUID()))
.collect(Collectors.toList());
}
public VaultFileCredentials getCredentials() {

View File

@ -3,6 +3,10 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.beemdevelopment.aegis.ui.GroupManagerActivity">
<item
android:id="@+id/action_delete_unused_groups"
app:showAsAction="collapseActionView"
android:title="@string/remove_unused_groups"/>
<item
android:id="@+id/action_save"
app:showAsAction="ifRoom"

View File

@ -283,6 +283,8 @@
<string name="unlocking_vault">Unlocking the vault</string>
<string name="remove_group">Remove group</string>
<string name="remove_group_description">Are you sure you want to remove this group? Entries in this group will automatically switch to \'No group\'.</string>
<string name="remove_unused_groups">Delete unused groups</string>
<string name="remove_unused_groups_description">Are you sure you want to delete all groups that are not assigned to an entry?</string>
<string name="remove_icon_pack">Remove icon pack</string>
<string name="remove_icon_pack_description">Are you sure you want to remove this icon pack? Entries that use icons from this pack will not be affected.</string>
<string name="details">Details</string>

View File

@ -0,0 +1,71 @@
package com.beemdevelopment.aegis.vault;
import static org.junit.Assert.assertEquals;
import com.beemdevelopment.aegis.util.IOUtils;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.stream.Collectors;
public class VaultTest {
@Test
public void testGroupConversion()
throws IOException, VaultFileException, VaultException {
Vault vault;
try (InputStream inStream = getClass().getResourceAsStream("aegis_plain_grouped_v2.json")) {
byte[] bytes = IOUtils.readAll(inStream);
VaultFile file = VaultFile.fromBytes(bytes);
vault = Vault.fromJson(file.getContent());
}
checkGroups(vault);
// After saving to and loading from the new format, the same checks should still pass
vault = Vault.fromJson(vault.toJson());
checkGroups(vault);
}
private static void checkGroups(Vault vault) {
// No old groups should be present anymore
assertEquals(0, vault.getEntries().getValues().stream()
.filter(e -> e.getOldGroup() != null)
.count());
// New groups should have been created, and groups with the same name
// should have been merged into a single group
assertEquals(2, vault.getGroups().getValues().size());
// Only one group with name group1
List<VaultGroup> foundGroups = vault.getGroups().getValues().stream()
.filter(g -> g.getName().equals("group1"))
.collect(Collectors.toList());
assertEquals(1, foundGroups.size());
VaultGroup group1 = foundGroups.get(0);
// Only one group with name group2
foundGroups = vault.getGroups().getValues().stream()
.filter(g -> g.getName().equals("group2"))
.collect(Collectors.toList());
assertEquals(1, foundGroups.size());
VaultGroup group2 = foundGroups.get(0);
// Two entries in group1
assertEquals(2, vault.getEntries().getValues().stream()
.filter(e -> e.getGroups().contains(group1.getUUID()))
.count());
// One entry in group2
assertEquals(1, vault.getEntries().getValues().stream()
.filter(e -> e.getGroups().contains(group2.getUUID()))
.count());
// Rest of the entries in no groups
assertEquals(vault.getEntries().getValues().size() - 3, vault.getEntries().getValues().stream()
.filter(e -> e.getGroups().isEmpty())
.count());
}
}

View File

@ -0,0 +1,117 @@
{
"version": 1,
"header": {
"slots": null,
"params": null
},
"db": {
"version": 2,
"entries": [
{
"type": "totp",
"uuid": "3ae6f1ad-2e65-4ed2-a953-1ec0dff2386d",
"name": "Mason",
"issuer": "Deno",
"icon": null,
"icon_mime": null,
"group": "group1",
"info": {
"secret": "4SJHB4GSD43FZBAI7C2HLRJGPQ",
"algo": "SHA1",
"digits": 6,
"period": 30
}
},
{
"type": "totp",
"uuid": "84b55971-a3d2-4173-a5bb-0aea113dbc17",
"name": "James",
"issuer": "SPDX",
"icon": null,
"icon_mime": null,
"group": null,
"info": {
"secret": "5OM4WOOGPLQEF6UGN3CPEOOLWU",
"algo": "SHA256",
"digits": 7,
"period": 20
}
},
{
"type": "totp",
"uuid": "3deaff2e-f181-4837-80e1-fdf0c54e9363",
"name": "Elijah",
"issuer": "Airbnb",
"icon": null,
"icon_mime": null,
"group": "group1",
"info": {
"secret": "7ELGJSGXNCCTV3O6LKJWYFV2RA",
"algo": "SHA512",
"digits": 8,
"period": 50
}
},
{
"type": "hotp",
"uuid": "0a8c0571-ff6f-4b02-aa4b-50553b4fb4fe",
"name": "James",
"issuer": "Issuu",
"icon": null,
"icon_mime": null,
"group": "group2",
"info": {
"secret": "YOOMIXWS5GN6RTBPUFFWKTW5M4",
"algo": "SHA1",
"digits": 6,
"counter": 1
}
},
{
"type": "hotp",
"uuid": "03e572f2-8ebd-44b0-a57e-e958af74815d",
"name": "Benjamin",
"issuer": "Air Canada",
"icon": null,
"icon_mime": null,
"group": null,
"info": {
"secret": "KUVJJOM753IHTNDSZVCNKL7GII",
"algo": "SHA256",
"digits": 7,
"counter": 50
}
},
{
"type": "hotp",
"uuid": "b25f8815-007f-40f7-a700-ce058ac05435",
"name": "Mason",
"issuer": "WWE",
"icon": null,
"icon_mime": null,
"group": null,
"info": {
"secret": "5VAML3X35THCEBVRLV24CGBKOY",
"algo": "SHA512",
"digits": 8,
"counter": 10300
}
},
{
"type": "steam",
"uuid": "5b11ae3b-6fc3-4d46-8ca7-cf0aea7de920",
"name": "Sophia",
"issuer": "Boeing",
"icon": null,
"icon_mime": null,
"group": null,
"info": {
"secret": "JRZCL47CMXVOQMNPZR2F7J4RGI",
"algo": "SHA1",
"digits": 5,
"period": 30
}
}
]
}
}