From 2323d8993846dc66890da7975734ffc3bae81e90 Mon Sep 17 00:00:00 2001 From: Alexander Bakker Date: Mon, 10 Jun 2019 18:25:44 +0200 Subject: [PATCH] Introduce UUIDMap for storing objects that are keyed by a UUID This patch introduces the new ``UUIDMap`` type, reducing code duplication and making UUID lookups faster. We currently already use UUIDs as the identifier for the ``DatabaseEntry`` and ``Slot`` types, but the way lookups by UUID work are kind of ugly, as we simply iterate over the list until we find a match. As we're probably going to have more types like this soon (groups and icons, for example), I figured it'd be good to abstract this away into a separate type and make it a map instead of a list. The only thing that has gotten slower is the ``swap`` method. The internal ``LinkedHashMap`` retains insertion order with a linked list, but does not know about the position of the values, so we basically have to copy the entire map to simply swap two values. I don't think it's too big of a deal, because swap operations still take less than a millisecond even with large vaults, but suggestions for improving this are welcome. I had to update gradle and JUnit to be able to use the new ``assertThrows`` assertion method, so this patch includes that as well. --- app/build.gradle | 11 +- .../beemdevelopment/aegis/db/Database.java | 33 +--- .../aegis/db/DatabaseEntry.java | 25 +-- .../aegis/db/DatabaseEntryList.java | 62 ------- .../aegis/db/DatabaseManager.java | 22 +-- .../beemdevelopment/aegis/db/slots/Slot.java | 26 +-- .../aegis/db/slots/SlotList.java | 53 +----- .../aegis/importers/DatabaseImporter.java | 5 +- .../aegis/ui/EditEntryActivity.java | 22 +-- .../aegis/ui/MainActivity.java | 8 +- .../aegis/ui/PreferencesFragment.java | 17 +- .../aegis/ui/views/EntryAdapter.java | 14 +- .../aegis/ui/views/EntryListView.java | 4 +- .../beemdevelopment/aegis/util/Cloner.java | 32 ++++ .../beemdevelopment/aegis/util/UUIDMap.java | 162 ++++++++++++++++++ .../com/beemdevelopment/aegis/HOTPTest.java | 4 +- .../com/beemdevelopment/aegis/SCryptTest.java | 4 +- .../com/beemdevelopment/aegis/TOTPTest.java | 4 +- .../beemdevelopment/aegis/UUIDMapTest.java | 104 +++++++++++ build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +- 21 files changed, 375 insertions(+), 243 deletions(-) delete mode 100644 app/src/main/java/com/beemdevelopment/aegis/db/DatabaseEntryList.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/util/Cloner.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/util/UUIDMap.java create mode 100644 app/src/test/java/com/beemdevelopment/aegis/UUIDMapTest.java diff --git a/app/build.gradle b/app/build.gradle index f9edf09c..bd6bd96f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,6 +10,13 @@ android { versionCode 14 versionName "0.4.3" } + + testOptions { + unitTests.all { + useJUnitPlatform() + } + } + buildTypes { debug { minifyEnabled false @@ -24,6 +31,7 @@ android { resValue "bool", "pref_secure_screen_default", "true" } } + compileOptions { targetCompatibility 1.8 sourceCompatibility 1.8 @@ -57,5 +65,6 @@ dependencies { } annotationProcessor 'androidx.annotation:annotation:1.1.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0' - testImplementation 'junit:junit:4.12' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2' } diff --git a/app/src/main/java/com/beemdevelopment/aegis/db/Database.java b/app/src/main/java/com/beemdevelopment/aegis/db/Database.java index c67fbe7a..4562cbe9 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/db/Database.java +++ b/app/src/main/java/com/beemdevelopment/aegis/db/Database.java @@ -2,17 +2,15 @@ package com.beemdevelopment.aegis.db; import com.beemdevelopment.aegis.encoding.Base64Exception; import com.beemdevelopment.aegis.otp.OtpInfoException; +import com.beemdevelopment.aegis.util.UUIDMap; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import java.util.List; -import java.util.UUID; - public class Database { private static final int VERSION = 1; - private DatabaseEntryList _entries = new DatabaseEntryList(); + private UUIDMap _entries = new UUIDMap<>(); public JSONObject toJson() { try { @@ -32,6 +30,7 @@ public class Database { public static Database fromJson(JSONObject obj) throws DatabaseException { Database db = new Database(); + UUIDMap entries = db.getEntries(); try { int ver = obj.getInt("version"); @@ -42,7 +41,7 @@ public class Database { JSONArray array = obj.getJSONArray("entries"); for (int i = 0; i < array.length(); i++) { DatabaseEntry entry = DatabaseEntry.fromJson(array.getJSONObject(i)); - db.addEntry(entry); + entries.add(entry); } } catch (Base64Exception | OtpInfoException | JSONException e) { throw new DatabaseException(e); @@ -51,27 +50,7 @@ public class Database { return db; } - public void addEntry(DatabaseEntry entry) { - _entries.add(entry); - } - - public void removeEntry(DatabaseEntry entry) { - _entries.remove(entry); - } - - public void replaceEntry(DatabaseEntry newEntry) { - _entries.replace(newEntry); - } - - public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) { - _entries.swap(entry1, entry2); - } - - public List getEntries() { - return _entries.getList(); - } - - public DatabaseEntry getEntryByUUID(UUID uuid) { - return _entries.getByUUID(uuid); + public UUIDMap getEntries() { + return _entries; } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/db/DatabaseEntry.java b/app/src/main/java/com/beemdevelopment/aegis/db/DatabaseEntry.java index fce67f9e..a7b1304d 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/db/DatabaseEntry.java +++ b/app/src/main/java/com/beemdevelopment/aegis/db/DatabaseEntry.java @@ -5,17 +5,16 @@ import com.beemdevelopment.aegis.encoding.Base64Exception; import com.beemdevelopment.aegis.otp.GoogleAuthInfo; import com.beemdevelopment.aegis.otp.OtpInfo; import com.beemdevelopment.aegis.otp.OtpInfoException; +import com.beemdevelopment.aegis.util.UUIDMap; import org.json.JSONException; import org.json.JSONObject; -import java.io.Serializable; import java.util.Arrays; import java.util.Objects; import java.util.UUID; -public class DatabaseEntry implements Serializable { - private UUID _uuid; +public class DatabaseEntry extends UUIDMap.Value { private String _name = ""; private String _issuer = ""; private String _group; @@ -23,12 +22,13 @@ public class DatabaseEntry implements Serializable { private byte[] _icon; private DatabaseEntry(UUID uuid, OtpInfo info) { - _uuid = uuid; + super(uuid); _info = info; } public DatabaseEntry(OtpInfo info) { - this(UUID.randomUUID(), info); + super(); + _info = info; } public DatabaseEntry(OtpInfo info, String name, String issuer) { @@ -46,7 +46,7 @@ public class DatabaseEntry implements Serializable { try { obj.put("type", _info.getType()); - obj.put("uuid", _uuid.toString()); + obj.put("uuid", getUUID().toString()); obj.put("name", _name); obj.put("issuer", _issuer); obj.put("group", _group); @@ -82,14 +82,6 @@ public class DatabaseEntry implements Serializable { return entry; } - public void resetUUID() { - _uuid = UUID.randomUUID(); - } - - public UUID getUUID() { - return _uuid; - } - public String getName() { return _name; } @@ -136,15 +128,12 @@ public class DatabaseEntry implements Serializable { @Override public boolean equals(Object o) { - if (this == o) { - return true; - } if (!(o instanceof DatabaseEntry)) { return false; } DatabaseEntry entry = (DatabaseEntry) o; - return getUUID().equals(entry.getUUID()) + return super.equals(entry) && getName().equals(entry.getName()) && getIssuer().equals(entry.getIssuer()) && Objects.equals(getGroup(), entry.getGroup()) diff --git a/app/src/main/java/com/beemdevelopment/aegis/db/DatabaseEntryList.java b/app/src/main/java/com/beemdevelopment/aegis/db/DatabaseEntryList.java deleted file mode 100644 index a0a7c56c..00000000 --- a/app/src/main/java/com/beemdevelopment/aegis/db/DatabaseEntryList.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.beemdevelopment.aegis.db; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.UUID; - -import androidx.annotation.NonNull; - -public class DatabaseEntryList implements Iterable, Serializable { - private List _entries = new ArrayList<>(); - - @NonNull - @Override - public Iterator iterator() { - return _entries.iterator(); - } - - public void add(DatabaseEntry entry) { - if (getByUUID(entry.getUUID()) != null) { - throw new AssertionError("entry found with the same uuid"); - } - _entries.add(entry); - } - - public void remove(DatabaseEntry entry) { - entry = mustGetByUUID(entry.getUUID()); - _entries.remove(entry); - } - - public void replace(DatabaseEntry newEntry) { - DatabaseEntry oldEntry = mustGetByUUID(newEntry.getUUID()); - _entries.set(_entries.indexOf(oldEntry), newEntry); - } - - public void swap(DatabaseEntry entry1, DatabaseEntry entry2) { - Collections.swap(_entries, _entries.indexOf(entry1), _entries.indexOf(entry2)); - } - - public List getList() { - return Collections.unmodifiableList(_entries); - } - - public DatabaseEntry getByUUID(UUID uuid) { - for (DatabaseEntry entry : _entries) { - if (entry.getUUID().equals(uuid)) { - return entry; - } - } - return null; - } - - private DatabaseEntry mustGetByUUID(UUID uuid) { - DatabaseEntry entry = getByUUID(uuid); - if (entry == null) { - throw new AssertionError("no entry found with the same uuid"); - } - return entry; - } -} diff --git a/app/src/main/java/com/beemdevelopment/aegis/db/DatabaseManager.java b/app/src/main/java/com/beemdevelopment/aegis/db/DatabaseManager.java index 4eec7a21..9ae2cf7f 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/db/DatabaseManager.java +++ b/app/src/main/java/com/beemdevelopment/aegis/db/DatabaseManager.java @@ -16,7 +16,7 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.text.Collator; -import java.util.List; +import java.util.Collection; import java.util.TreeSet; import java.util.UUID; @@ -136,32 +136,32 @@ public class DatabaseManager { public void addEntry(DatabaseEntry entry) { assertState(false, true); - _db.addEntry(entry); + _db.getEntries().add(entry); } - public void removeEntry(DatabaseEntry entry) { + public DatabaseEntry removeEntry(DatabaseEntry entry) { assertState(false, true); - _db.removeEntry(entry); + return _db.getEntries().remove(entry); } - public void replaceEntry(DatabaseEntry entry) { + public DatabaseEntry replaceEntry(DatabaseEntry entry) { assertState(false, true); - _db.replaceEntry(entry); + return _db.getEntries().replace(entry); } public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) { assertState(false, true); - _db.swapEntries(entry1, entry2); + _db.getEntries().swap(entry1, entry2); } - public List getEntries() { + public boolean isEntryDuplicate(DatabaseEntry entry) { assertState(false, true); - return _db.getEntries(); + return _db.getEntries().has(entry); } - public DatabaseEntry getEntryByUUID(UUID uuid) { + public Collection getEntries() { assertState(false, true); - return _db.getEntryByUUID(uuid); + return _db.getEntries().getValues(); } public TreeSet getGroups() { diff --git a/app/src/main/java/com/beemdevelopment/aegis/db/slots/Slot.java b/app/src/main/java/com/beemdevelopment/aegis/db/slots/Slot.java index 24608920..73781657 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/db/slots/Slot.java +++ b/app/src/main/java/com/beemdevelopment/aegis/db/slots/Slot.java @@ -7,12 +7,12 @@ import com.beemdevelopment.aegis.crypto.MasterKey; import com.beemdevelopment.aegis.crypto.SCryptParameters; import com.beemdevelopment.aegis.encoding.Hex; import com.beemdevelopment.aegis.encoding.HexException; +import com.beemdevelopment.aegis.util.UUIDMap; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; -import java.io.Serializable; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @@ -25,26 +25,29 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; -public abstract class Slot implements Serializable { +public abstract class Slot extends UUIDMap.Value { public final static byte TYPE_RAW = 0x00; public final static byte TYPE_DERIVED = 0x01; public final static byte TYPE_FINGERPRINT = 0x02; - private UUID _uuid; private byte[] _encryptedMasterKey; private CryptParameters _encryptedMasterKeyParams; protected Slot() { - _uuid = UUID.randomUUID(); + super(); } protected Slot(UUID uuid, byte[] key, CryptParameters keyParams) { - _uuid = uuid; + super(uuid); _encryptedMasterKey = key; _encryptedMasterKeyParams = keyParams; } - // getKey decrypts the encrypted master key in this slot using the given cipher and returns it. + /** + * Decrypts the encrypted master key in this slot using the given cipher and returns it. + * @throws SlotException if a generic crypto operation error occurred. + * @throws SlotIntegrityException if an error occurred while verifying the integrity of the slot. + */ public MasterKey getKey(Cipher cipher) throws SlotException, SlotIntegrityException { try { CryptResult res = CryptoUtils.decrypt(_encryptedMasterKey, cipher, _encryptedMasterKeyParams); @@ -57,7 +60,10 @@ public abstract class Slot implements Serializable { } } - // setKey encrypts the given master key using the given cipher and stores the result in this slot. + /** + * Encrypts the given master key using the given cipher and stores the result in this slot. + * @throws SlotException if a generic crypto operation error occurred. + */ public void setKey(MasterKey masterKey, Cipher cipher) throws SlotException { try { byte[] masterKeyBytes = masterKey.getBytes(); @@ -95,7 +101,7 @@ public abstract class Slot implements Serializable { try { JSONObject obj = new JSONObject(); obj.put("type", getType()); - obj.put("uuid", _uuid.toString()); + obj.put("uuid", getUUID().toString()); obj.put("key", Hex.encode(_encryptedMasterKey)); obj.put("key_params", _encryptedMasterKeyParams.toJson()); return obj; @@ -146,8 +152,4 @@ public abstract class Slot implements Serializable { } public abstract byte getType(); - - public UUID getUUID() { - return _uuid; - } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/db/slots/SlotList.java b/app/src/main/java/com/beemdevelopment/aegis/db/slots/SlotList.java index 81934647..d723d592 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/db/slots/SlotList.java +++ b/app/src/main/java/com/beemdevelopment/aegis/db/slots/SlotList.java @@ -1,18 +1,15 @@ package com.beemdevelopment.aegis.db.slots; +import com.beemdevelopment.aegis.util.UUIDMap; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import java.io.Serializable; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; -import java.util.UUID; - -public class SlotList implements Iterable, Serializable { - private List _slots = new ArrayList<>(); +public class SlotList extends UUIDMap { public JSONArray toJson() { JSONArray array = new JSONArray(); for (Slot slot : this) { @@ -38,23 +35,6 @@ public class SlotList implements Iterable, Serializable { return slots; } - public void add(Slot slot) { - for (Slot s : this) { - if (s.getUUID().equals(slot.getUUID())) { - throw new AssertionError("slot found with the same uuid"); - } - } - _slots.add(slot); - } - - public void remove(Slot slot) { - _slots.remove(slot); - } - - public int size() { - return _slots.size(); - } - public T find(Class type) { for (Slot slot : this) { if (slot.getClass() == type) { @@ -77,31 +57,4 @@ public class SlotList implements Iterable, Serializable { public boolean has(Class type) { return find(type) != null; } - - public void replace(Slot newSlot) { - Slot oldSlot = mustGetByUUID(newSlot.getUUID()); - _slots.set(_slots.indexOf(oldSlot), newSlot); - } - - public Slot getByUUID(UUID uuid) { - for (Slot slot : _slots) { - if (slot.getUUID().equals(uuid)) { - return slot; - } - } - return null; - } - - private Slot mustGetByUUID(UUID uuid) { - Slot slot = getByUUID(uuid); - if (slot == null) { - throw new AssertionError(String.format("no slot found with UUID: %s", uuid.toString())); - } - return slot; - } - - @Override - public Iterator iterator() { - return _slots.iterator(); - } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java index 9af5154b..c0916c5e 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java @@ -6,6 +6,7 @@ import android.net.Uri; import com.beemdevelopment.aegis.db.DatabaseEntry; import com.beemdevelopment.aegis.util.ByteInputStream; +import com.beemdevelopment.aegis.util.UUIDMap; import com.topjohnwu.superuser.io.SuFile; import com.topjohnwu.superuser.io.SuFileInputStream; @@ -115,7 +116,7 @@ public abstract class DatabaseImporter { } public static class Result { - private List _entries = new ArrayList<>(); + private UUIDMap _entries = new UUIDMap<>(); private List _errors = new ArrayList<>(); public void addEntry(DatabaseEntry entry) { @@ -126,7 +127,7 @@ public abstract class DatabaseImporter { _errors.add(error); } - public List getEntries() { + public UUIDMap getEntries() { return _entries; } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java index 31de8849..6f6e62f7 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java @@ -40,12 +40,9 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; +import com.beemdevelopment.aegis.util.Cloner; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; import java.text.Collator; import java.util.ArrayList; import java.util.List; @@ -391,6 +388,7 @@ public class EditEntryActivity extends AegisActivity { setResult(RESULT_OK, intent); finish(); } + @Override protected void onActivityResult(int requestCode, final int resultCode, Intent data) { if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) { @@ -492,7 +490,7 @@ public class EditEntryActivity extends AegisActivity { if (_origEntry == null) { entry = new DatabaseEntry(info); } else { - entry = cloneEntry(_origEntry); + entry = Cloner.clone(_origEntry); entry.setInfo(info); } entry.setIssuer(_textIssuer.getText().toString()); @@ -570,20 +568,6 @@ public class EditEntryActivity extends AegisActivity { return -1; } - private static DatabaseEntry cloneEntry(DatabaseEntry entry) { - try { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(baos); - oos.writeObject(entry); - - ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); - ObjectInputStream ois = new ObjectInputStream(bais); - return (DatabaseEntry) ois.readObject(); - } catch (ClassNotFoundException | IOException e) { - throw new RuntimeException(e); - } - } - private static class ParseException extends Exception { public ParseException(String message) { super(message); diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java index 6ac91b2b..52ddac1c 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java @@ -251,8 +251,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene } else { // this profile has been serialized/deserialized and is no longer the same instance it once was // to deal with this, the replaceEntry functions are used - _db.replaceEntry(entry); - _entryListView.replaceEntry(entry); + DatabaseEntry oldEntry = _db.replaceEntry(entry); + _entryListView.replaceEntry(oldEntry, entry); saveDatabase(); } } @@ -521,10 +521,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene } private void deleteEntry(DatabaseEntry entry) { - _db.removeEntry(entry); + DatabaseEntry oldEntry = _db.removeEntry(entry); saveDatabase(); - _entryListView.removeEntry(entry); + _entryListView.removeEntry(oldEntry); } @Override diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java index 235fa284..363bfd39 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java @@ -38,6 +38,7 @@ import com.beemdevelopment.aegis.importers.DatabaseImporterException; import com.beemdevelopment.aegis.services.NotificationService; import com.beemdevelopment.aegis.ui.models.ImportEntry; import com.beemdevelopment.aegis.ui.preferences.SwitchPreference; +import com.beemdevelopment.aegis.util.UUIDMap; import com.takisoft.preferencex.PreferenceFragmentCompat; import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.io.SuFile; @@ -76,7 +77,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat { // keep a reference to the type of database converter the user selected private Class _importerType; private AegisImporter.State _importerState; - private List _importerEntries; + private UUIDMap _importerEntries; private SwitchPreference _encryptionPreference; private SwitchPreference _fingerprintPreference; @@ -620,20 +621,10 @@ public class PreferencesFragment extends PreferenceFragmentCompat { List selectedEntries = (ArrayList) data.getSerializableExtra("entries"); for (ImportEntry selectedEntry : selectedEntries) { - DatabaseEntry savedEntry = null; - for (DatabaseEntry entry : _importerEntries) { - if (entry.getUUID().equals(selectedEntry.getUUID())) { - savedEntry = entry; - break; - } - } - - if (savedEntry == null) { - throw new RuntimeException(); - } + DatabaseEntry savedEntry = _importerEntries.getByUUID(selectedEntry.getUUID()); // temporary: randomize the UUID of duplicate entries and add them anyway - if (_db.getEntryByUUID(savedEntry.getUUID()) != null) { + if (_db.isEntryDuplicate(savedEntry)) { savedEntry.resetUUID(); } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryAdapter.java b/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryAdapter.java index 33aebc7f..8e2abb7b 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryAdapter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryAdapter.java @@ -17,7 +17,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; -import java.util.UUID; import androidx.recyclerview.widget.RecyclerView; @@ -109,7 +108,6 @@ public class EntryAdapter extends RecyclerView.Adapter implements I } public void removeEntry(DatabaseEntry entry) { - entry = getEntryByUUID(entry.getUUID()); _entries.remove(entry); if (_shownEntries.contains(entry)) { @@ -128,8 +126,7 @@ public class EntryAdapter extends RecyclerView.Adapter implements I checkPeriodUniformity(); } - public void replaceEntry(DatabaseEntry newEntry) { - DatabaseEntry oldEntry = getEntryByUUID(newEntry.getUUID()); + public void replaceEntry(DatabaseEntry oldEntry, DatabaseEntry newEntry) { _entries.set(_entries.indexOf(oldEntry), newEntry); if (_shownEntries.contains(oldEntry)) { @@ -163,15 +160,6 @@ public class EntryAdapter extends RecyclerView.Adapter implements I return _searchFilter != null && !issuer.contains(_searchFilter); } - private DatabaseEntry getEntryByUUID(UUID uuid) { - for (DatabaseEntry entry : _entries) { - if (entry.getUUID().equals(uuid)) { - return entry; - } - } - throw new AssertionError("no entry found with the same id"); - } - public void refresh(boolean hard) { if (hard) { notifyDataSetChanged(); diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryListView.java b/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryListView.java index d6ec82f4..9b591e8a 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryListView.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/views/EntryListView.java @@ -225,8 +225,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener { _adapter.clearEntries(); } - public void replaceEntry(DatabaseEntry entry) { - _adapter.replaceEntry(entry); + public void replaceEntry(DatabaseEntry oldEntry, DatabaseEntry newEntry) { + _adapter.replaceEntry(oldEntry, newEntry); } public void runEntriesAnimation() { diff --git a/app/src/main/java/com/beemdevelopment/aegis/util/Cloner.java b/app/src/main/java/com/beemdevelopment/aegis/util/Cloner.java new file mode 100644 index 00000000..64a2aed3 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/util/Cloner.java @@ -0,0 +1,32 @@ +package com.beemdevelopment.aegis.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +public class Cloner { + private Cloner() { + + } + + /** + * Returns an exact clone of the given Serializable object. + */ + @SuppressWarnings("unchecked cast") + public static T clone(T obj) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(obj); + + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + return (T) ois.readObject(); + } catch (ClassNotFoundException | IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/util/UUIDMap.java b/app/src/main/java/com/beemdevelopment/aegis/util/UUIDMap.java new file mode 100644 index 00000000..5ba85eff --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/util/UUIDMap.java @@ -0,0 +1,162 @@ +package com.beemdevelopment.aegis.util; + +import androidx.annotation.NonNull; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.UUID; + +/** + * A map data structure abstraction for storing values with a UUID as the key. Keys + * must be specified by the value itself, instead of separately. It uses a + * LinkedHashMap internally (a hash map with a separate linked list that maintains + * the order). + * @param The type of values in this map + */ +public class UUIDMap implements Iterable, Serializable { + private LinkedHashMap _map = new LinkedHashMap<>(); + + /** + * Adds a value to the internal map. + * @throws AssertionError if a map value with the UUID of the given value already exists. + */ + public void add(T value) { + UUID uuid = value.getUUID(); + if (_map.containsKey(uuid)) { + throw new AssertionError(String.format("Existing value found with UUID: %s", uuid)); + } + _map.put(uuid, value); + } + + /** + * Removes a value from the internal map. + * @throws AssertionError if no map value exists with the UUID of the given value. + * @return The old value that is now no longer present in the internal map. + */ + public T remove(T value) { + T oldValue = getByUUID(value.getUUID()); + _map.remove(oldValue.getUUID()); + return oldValue; + } + + /** + * Replaces an old value (with the same UUID as the new given value) in the + * internal map with the new given value. + * @throws AssertionError if no map value exists with the UUID of the given value. + * @return The old value that is now no longer present in the internal map. + */ + public T replace(T newValue) { + T oldValue = getByUUID(newValue.getUUID()); + _map.put(oldValue.getUUID(), newValue); + return oldValue; + } + + /** + * Swaps the position of value1 and value2 in the internal map. This operation is + * quite expensive because it has to reallocate the entire underlying LinkedHashMap. + * @throws AssertionError if no map value exists with the UUID of the given entries. + */ + public void swap(T value1, T value2) { + boolean found1 = false; + boolean found2 = false; + List values = new ArrayList<>(); + + for (T value : _map.values()) { + if (value.getUUID().equals(value1.getUUID())) { + values.add(value2); + found1 = true; + } else if (value.getUUID().equals(value2.getUUID())) { + values.add(value1); + found2 = true; + } else { + values.add(value); + } + } + + if (!found1) { + throw new AssertionError(String.format("No value found for value1 with UUID: %s", value1.getUUID())); + } + if (!found2) { + throw new AssertionError(String.format("No value found for value2 with UUID: %s", value2.getUUID())); + } + + _map.clear(); + for (T value : values) { + _map.put(value.getUUID(), value); + } + } + + /** + * 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()); + } + + /** + * Returns a read-only view of the values in the internal map. + */ + public Collection getValues() { + return Collections.unmodifiableCollection(_map.values()); + } + + /** + * Retrieves an entry from the internal map that has the given UUID. + * @throws AssertionError if no map value exists with the given UUID. + */ + public T getByUUID(UUID uuid) { + T value = _map.get(uuid); + if (value == null) { + throw new AssertionError(String.format("No value found with UUID: %s", uuid)); + } + return value; + } + + @NonNull + @Override + public Iterator iterator() { + return _map.values().iterator(); + } + + public static abstract class Value implements Serializable { + private UUID _uuid; + + protected Value(UUID uuid) { + _uuid = uuid; + } + + protected Value() { + this(UUID.randomUUID()); + } + + public final UUID getUUID() { + return _uuid; + } + + /** + * Resets the UUID of this value by generating a new random one. + * The caller must ensure that this Value is not in a UUIDMap yet. Otherwise, bad things will happen. + */ + public final void resetUUID() { + _uuid = UUID.randomUUID(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof Value)) { + return false; + } + + return getUUID().equals(((Value) o).getUUID()); + } + } +} diff --git a/app/src/test/java/com/beemdevelopment/aegis/HOTPTest.java b/app/src/test/java/com/beemdevelopment/aegis/HOTPTest.java index 98ab300d..1f05d066 100644 --- a/app/src/test/java/com/beemdevelopment/aegis/HOTPTest.java +++ b/app/src/test/java/com/beemdevelopment/aegis/HOTPTest.java @@ -3,12 +3,12 @@ package com.beemdevelopment.aegis; import com.beemdevelopment.aegis.crypto.otp.HOTP; import com.beemdevelopment.aegis.crypto.otp.OTP; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class HOTPTest { // https://tools.ietf.org/html/rfc4226#page-32 diff --git a/app/src/test/java/com/beemdevelopment/aegis/SCryptTest.java b/app/src/test/java/com/beemdevelopment/aegis/SCryptTest.java index 80b750de..2483aa9e 100644 --- a/app/src/test/java/com/beemdevelopment/aegis/SCryptTest.java +++ b/app/src/test/java/com/beemdevelopment/aegis/SCryptTest.java @@ -5,13 +5,13 @@ import com.beemdevelopment.aegis.crypto.SCryptParameters; import com.beemdevelopment.aegis.encoding.Hex; import com.beemdevelopment.aegis.encoding.HexException; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Arrays; import javax.crypto.SecretKey; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class SCryptTest { @Test diff --git a/app/src/test/java/com/beemdevelopment/aegis/TOTPTest.java b/app/src/test/java/com/beemdevelopment/aegis/TOTPTest.java index 6b97ba5a..58bad037 100644 --- a/app/src/test/java/com/beemdevelopment/aegis/TOTPTest.java +++ b/app/src/test/java/com/beemdevelopment/aegis/TOTPTest.java @@ -4,12 +4,12 @@ import com.beemdevelopment.aegis.crypto.otp.OTP; import com.beemdevelopment.aegis.crypto.otp.TOTP; import com.beemdevelopment.aegis.encoding.HexException; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class TOTPTest { private static class Vector { diff --git a/app/src/test/java/com/beemdevelopment/aegis/UUIDMapTest.java b/app/src/test/java/com/beemdevelopment/aegis/UUIDMapTest.java new file mode 100644 index 00000000..3f2f6d89 --- /dev/null +++ b/app/src/test/java/com/beemdevelopment/aegis/UUIDMapTest.java @@ -0,0 +1,104 @@ +package com.beemdevelopment.aegis; + +import com.beemdevelopment.aegis.util.Cloner; +import com.beemdevelopment.aegis.util.UUIDMap; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class UUIDMapTest { + private UUIDMap _map; + + @BeforeEach + public void init() { + _map = new UUIDMap<>(); + } + + @Test + public void addValue() { + // try adding a new value + Value value = addNewValue(); + + // try re-adding the value + assertThrows(AssertionError.class, () -> _map.add(value)); + + // try adding a clone of the value + assertThrows(AssertionError.class, () -> _map.add(Cloner.clone(value))); + } + + @Test + public void removeValue() { + // try removing a value + final Value value = addNewValue(); + Value oldValue = _map.remove(value); + assertFalse(_map.has(value)); + + // ensure we got the original value back + assertEquals(value, oldValue); + + // try removing a non-existent value + assertThrows(AssertionError.class, () -> _map.remove(value)); + + // try removing a value using a clone + Value value2 = addNewValue(); + _map.remove(Cloner.clone(value2)); + assertFalse(_map.has(value2)); + } + + @Test + public void replaceValue() { + Value value = addNewValue(); + + // replace the value with a clone + Value valueClone = Cloner.clone(value); + Value oldValue = _map.replace(valueClone); + + // ensure we got the original value back + assertEquals(value, oldValue); + + // ensure that the clone is now stored in the map + assertSame(_map.getByUUID(value.getUUID()), valueClone); + } + + @Test + public void swapValue() { + Collection values = _map.getValues(); + + // set up the map with some values + Value value1 = addNewValue(); + Value value2 = addNewValue(); + Value value3 = addNewValue(); + Value value4 = addNewValue(); + + // set up a reference list with the reverse order + List ref = new ArrayList<>(values); + Collections.reverse(ref); + + // the lists should not be equal at this point + assertNotEquals(values, ref); + + // swap the values and see if the lists are equal now + _map.swap(value1, value4); + _map.swap(value2, value3); + assertIterableEquals(values, ref); + } + + private Value addNewValue() { + Value value = new Value(); + assertFalse(_map.has(value)); + _map.add(value); + assertTrue(_map.has(value)); + return value; + } + + private static class Value extends UUIDMap.Value { + + } +} diff --git a/build.gradle b/build.gradle index 286f8e93..e0d39c38 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.2' + classpath 'com.android.tools.build:gradle:3.4.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 65249f0e..4aef3e86 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Feb 26 19:31:04 CET 2019 +#Mon Jun 10 14:39:47 CEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip