mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-04-19 21:39:18 +00:00
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:
parent
6769fefd00
commit
2323d89938
21 changed files with 375 additions and 243 deletions
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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() {
|
||||
|
|
32
app/src/main/java/com/beemdevelopment/aegis/util/Cloner.java
Normal file
32
app/src/main/java/com/beemdevelopment/aegis/util/Cloner.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
162
app/src/main/java/com/beemdevelopment/aegis/util/UUIDMap.java
Normal file
162
app/src/main/java/com/beemdevelopment/aegis/util/UUIDMap.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
104
app/src/test/java/com/beemdevelopment/aegis/UUIDMapTest.java
Normal file
104
app/src/test/java/com/beemdevelopment/aegis/UUIDMapTest.java
Normal 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 {
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue