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.
This commit is contained in:
Alexander Bakker 2019-06-10 18:25:44 +02:00
parent 6769fefd00
commit 2323d89938
21 changed files with 375 additions and 243 deletions

View File

@ -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'
}

View File

@ -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<DatabaseEntry> _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<DatabaseEntry> 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<DatabaseEntry> getEntries() {
return _entries.getList();
}
public DatabaseEntry getEntryByUUID(UUID uuid) {
return _entries.getByUUID(uuid);
public UUIDMap<DatabaseEntry> getEntries() {
return _entries;
}
}

View File

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

View File

@ -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<DatabaseEntry>, Serializable {
private List<DatabaseEntry> _entries = new ArrayList<>();
@NonNull
@Override
public Iterator<DatabaseEntry> 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<DatabaseEntry> 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;
}
}

View File

@ -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<DatabaseEntry> getEntries() {
public boolean isEntryDuplicate(DatabaseEntry entry) {
assertState(false, true);
return _db.getEntries();
return _db.getEntries().has(entry);
}
public DatabaseEntry getEntryByUUID(UUID uuid) {
public Collection<DatabaseEntry> getEntries() {
assertState(false, true);
return _db.getEntryByUUID(uuid);
return _db.getEntries().getValues();
}
public TreeSet<String> getGroups() {

View File

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

View File

@ -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<Slot>, Serializable {
private List<Slot> _slots = new ArrayList<>();
public class SlotList extends UUIDMap<Slot> {
public JSONArray toJson() {
JSONArray array = new JSONArray();
for (Slot slot : this) {
@ -38,23 +35,6 @@ public class SlotList implements Iterable<Slot>, 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 extends Slot> T find(Class<T> type) {
for (Slot slot : this) {
if (slot.getClass() == type) {
@ -77,31 +57,4 @@ public class SlotList implements Iterable<Slot>, Serializable {
public <T extends Slot> boolean has(Class<T> 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<Slot> iterator() {
return _slots.iterator();
}
}

View File

@ -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<DatabaseEntry> _entries = new ArrayList<>();
private UUIDMap<DatabaseEntry> _entries = new UUIDMap<>();
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
public void addEntry(DatabaseEntry entry) {
@ -126,7 +127,7 @@ public abstract class DatabaseImporter {
_errors.add(error);
}
public List<DatabaseEntry> getEntries() {
public UUIDMap<DatabaseEntry> getEntries() {
return _entries;
}

View File

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

View File

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

View File

@ -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<? extends DatabaseImporter> _importerType;
private AegisImporter.State _importerState;
private List<DatabaseEntry> _importerEntries;
private UUIDMap<DatabaseEntry> _importerEntries;
private SwitchPreference _encryptionPreference;
private SwitchPreference _fingerprintPreference;
@ -620,20 +621,10 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
List<ImportEntry> selectedEntries = (ArrayList<ImportEntry>) 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();
}

View File

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

View File

@ -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() {

View File

@ -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 extends Serializable> 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);
}
}
}

View File

@ -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 <T> The type of values in this map
*/
public class UUIDMap <T extends UUIDMap.Value> implements Iterable<T>, Serializable {
private LinkedHashMap<UUID, T> _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<T> 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<T> 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<T> 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());
}
}
}

View File

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

View File

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

View File

@ -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 {

View File

@ -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<Value> _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<Value> 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<Value> 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 {
}
}

View File

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

View File

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