From 043451382044a8d9c96b1f4a897419133ca7add6 Mon Sep 17 00:00:00 2001 From: Alexander Bakker Date: Sat, 6 Oct 2018 22:23:38 +0200 Subject: [PATCH] Refactor the database classes to be more reusable --- .../me/impy/aegis/crypto/CryptParameters.java | 2 +- .../main/java/me/impy/aegis/db/Database.java | 17 +- .../java/me/impy/aegis/db/DatabaseEntry.java | 33 ++-- .../java/me/impy/aegis/db/DatabaseFile.java | 141 ++++++++++------ .../aegis/db/DatabaseFileCredentials.java | 40 +++++ .../me/impy/aegis/db/DatabaseManager.java | 155 ++++++++---------- .../me/impy/aegis/db/slots/PasswordSlot.java | 4 +- .../java/me/impy/aegis/db/slots/Slot.java | 5 +- .../java/me/impy/aegis/db/slots/SlotList.java | 8 +- .../impy/aegis/importers/AegisImporter.java | 18 +- .../aegis/importers/DatabaseImporter.java | 8 - .../main/java/me/impy/aegis/otp/OtpInfo.java | 2 +- .../java/me/impy/aegis/ui/AuthActivity.java | 3 +- .../java/me/impy/aegis/ui/IntroActivity.java | 22 ++- .../java/me/impy/aegis/ui/MainActivity.java | 22 ++- .../me/impy/aegis/ui/PreferencesFragment.java | 59 +++---- .../me/impy/aegis/ui/ScannerActivity.java | 5 +- .../me/impy/aegis/ui/SlotManagerActivity.java | 23 ++- 18 files changed, 311 insertions(+), 256 deletions(-) create mode 100644 app/src/main/java/me/impy/aegis/db/DatabaseFileCredentials.java diff --git a/app/src/main/java/me/impy/aegis/crypto/CryptParameters.java b/app/src/main/java/me/impy/aegis/crypto/CryptParameters.java index 7641c720..ac555be2 100644 --- a/app/src/main/java/me/impy/aegis/crypto/CryptParameters.java +++ b/app/src/main/java/me/impy/aegis/crypto/CryptParameters.java @@ -30,7 +30,7 @@ public class CryptParameters implements Serializable { return obj; } - public static CryptParameters parseJson(JSONObject obj) throws JSONException, HexException { + public static CryptParameters fromJson(JSONObject obj) throws JSONException, HexException { byte[] nonce = Hex.decode(obj.getString("nonce")); byte[] tag = Hex.decode(obj.getString("tag")); return new CryptParameters(nonce, tag); diff --git a/app/src/main/java/me/impy/aegis/db/Database.java b/app/src/main/java/me/impy/aegis/db/Database.java index 1580a47a..fbc7976e 100644 --- a/app/src/main/java/me/impy/aegis/db/Database.java +++ b/app/src/main/java/me/impy/aegis/db/Database.java @@ -12,14 +12,13 @@ import me.impy.aegis.otp.OtpInfoException; public class Database { private static final int VERSION = 1; - private DatabaseEntryList _entries = new DatabaseEntryList(); - public JSONObject serialize() { + public JSONObject toJson() { try { JSONArray array = new JSONArray(); for (DatabaseEntry e : _entries) { - array.put(e.serialize()); + array.put(e.toJson()); } JSONObject obj = new JSONObject(); @@ -31,8 +30,9 @@ public class Database { } } - public void deserialize(JSONObject obj) throws DatabaseException { - // TODO: support different VERSION deserialization providers + public static Database fromJson(JSONObject obj) throws DatabaseException { + Database db = new Database(); + try { int ver = obj.getInt("version"); if (ver != VERSION) { @@ -41,13 +41,14 @@ public class Database { JSONArray array = obj.getJSONArray("entries"); for (int i = 0; i < array.length(); i++) { - DatabaseEntry entry = new DatabaseEntry(null); - entry.deserialize(array.getJSONObject(i)); - addEntry(entry); + DatabaseEntry entry = DatabaseEntry.fromJson(array.getJSONObject(i)); + db.addEntry(entry); } } catch (Base64Exception | OtpInfoException | JSONException e) { throw new DatabaseException(e); } + + return db; } public void addEntry(DatabaseEntry entry) { diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseEntry.java b/app/src/main/java/me/impy/aegis/db/DatabaseEntry.java index f9e89f63..7d9ffbff 100644 --- a/app/src/main/java/me/impy/aegis/db/DatabaseEntry.java +++ b/app/src/main/java/me/impy/aegis/db/DatabaseEntry.java @@ -9,6 +9,7 @@ import java.util.UUID; import me.impy.aegis.encoding.Base64; import me.impy.aegis.encoding.Base64Exception; +import me.impy.aegis.otp.GoogleAuthInfo; import me.impy.aegis.otp.OtpInfo; import me.impy.aegis.otp.OtpInfoException; @@ -19,9 +20,13 @@ public class DatabaseEntry implements Serializable { private OtpInfo _info; private byte[] _icon; - public DatabaseEntry(OtpInfo info) { + private DatabaseEntry(UUID uuid, OtpInfo info) { + _uuid = uuid; _info = info; - _uuid = UUID.randomUUID(); + } + + public DatabaseEntry(OtpInfo info) { + this(UUID.randomUUID(), info); } public DatabaseEntry(OtpInfo info, String name, String issuer) { @@ -30,7 +35,11 @@ public class DatabaseEntry implements Serializable { setIssuer(issuer); } - public JSONObject serialize() { + public DatabaseEntry(GoogleAuthInfo info) { + this(info.getOtpInfo(), info.getAccountName(), info.getIssuer()); + } + + public JSONObject toJson() { JSONObject obj = new JSONObject(); try { @@ -47,22 +56,26 @@ public class DatabaseEntry implements Serializable { return obj; } - public void deserialize(JSONObject obj) throws JSONException, OtpInfoException, Base64Exception { + public static DatabaseEntry fromJson(JSONObject obj) throws JSONException, OtpInfoException, Base64Exception { // if there is no uuid, generate a new one + UUID uuid; if (!obj.has("uuid")) { - _uuid = UUID.randomUUID(); + uuid = UUID.randomUUID(); } else { - _uuid = UUID.fromString(obj.getString("uuid")); + uuid = UUID.fromString(obj.getString("uuid")); } - _name = obj.getString("name"); - _issuer = obj.getString("issuer"); + + OtpInfo info = OtpInfo.fromJson(obj.getString("type"), obj.getJSONObject("info")); + DatabaseEntry entry = new DatabaseEntry(uuid, info); + entry.setName(obj.getString("name")); + entry.setIssuer(obj.getString("issuer")); Object icon = obj.get("icon"); if (icon != JSONObject.NULL) { - _icon = Base64.decode((String) icon); + entry.setIcon(Base64.decode((String) icon)); } - _info = OtpInfo.parseJson(obj.getString("type"), obj.getJSONObject("info")); + return entry; } public void resetUUID() { diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseFile.java b/app/src/main/java/me/impy/aegis/db/DatabaseFile.java index d932ec7d..6757664a 100644 --- a/app/src/main/java/me/impy/aegis/db/DatabaseFile.java +++ b/app/src/main/java/me/impy/aegis/db/DatabaseFile.java @@ -1,6 +1,5 @@ package me.impy.aegis.db; -import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -8,7 +7,6 @@ import java.io.UnsupportedEncodingException; import me.impy.aegis.crypto.CryptParameters; import me.impy.aegis.crypto.CryptResult; -import me.impy.aegis.crypto.MasterKey; import me.impy.aegis.crypto.MasterKeyException; import me.impy.aegis.db.slots.SlotList; import me.impy.aegis.db.slots.SlotListException; @@ -20,22 +18,41 @@ public class DatabaseFile { public static final byte VERSION = 1; private Object _content; - private CryptParameters _cryptParameters; - private SlotList _slots; + private Header _header; - public byte[] serialize() { - try { - // don't write the crypt parameters and slots if the content is not encrypted - boolean plain = _content instanceof JSONObject || !isEncrypted(); - JSONObject headerObj = new JSONObject(); - headerObj.put("slots", plain ? JSONObject.NULL : SlotList.serialize(_slots)); - headerObj.put("params", plain ? JSONObject.NULL : _cryptParameters.toJson()); + public DatabaseFile() { + + } + private DatabaseFile(Object content, Header header) { + _content = content; + _header = header; + } + + public Header getHeader() { + return _header; + } + + public boolean isEncrypted() { + return !_header.isEmpty(); + } + + public JSONObject toJson() { + try { JSONObject obj = new JSONObject(); obj.put("version", VERSION); - obj.put("header", headerObj); + obj.put("header", _header.toJson()); obj.put("db", _content); + return obj; + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + public byte[] toBytes() { + JSONObject obj = toJson(); + + try { String string = obj.toString(4); return string.getBytes("UTF-8"); } catch (JSONException | UnsupportedEncodingException e) { @@ -43,76 +60,108 @@ public class DatabaseFile { } } - public void deserialize(byte[] data) throws DatabaseFileException { + public static DatabaseFile fromJson(JSONObject obj) throws DatabaseFileException { try { - JSONObject obj = new JSONObject(new String(data, "UTF-8")); - JSONObject headerObj = obj.getJSONObject("header"); if (obj.getInt("version") > VERSION) { throw new DatabaseFileException("unsupported version"); } - JSONArray slotObj = headerObj.optJSONArray("slots"); - if (slotObj != null) { - _slots = SlotList.deserialize(slotObj); - } - - JSONObject cryptObj = headerObj.optJSONObject("params"); - if (cryptObj != null) { - _cryptParameters = CryptParameters.parseJson(cryptObj); + Header header = Header.fromJson(obj.getJSONObject("header")); + if (!header.isEmpty()) { + return new DatabaseFile(obj.getString("db"), header); } - if (cryptObj == null || slotObj == null) { - _content = obj.getJSONObject("db"); - } else { - _content = obj.getString("db"); - } - } catch (SlotListException | UnsupportedEncodingException | JSONException | HexException e) { + return new DatabaseFile(obj.getJSONObject("db"), header); + } catch (JSONException e) { throw new DatabaseFileException(e); } } - public boolean isEncrypted() { - return _slots != null; + public static DatabaseFile fromBytes(byte[] data) throws DatabaseFileException { + try { + JSONObject obj = new JSONObject(new String(data, "UTF-8")); + return DatabaseFile.fromJson(obj); + } catch (UnsupportedEncodingException | JSONException e) { + throw new DatabaseFileException(e); + } } public JSONObject getContent() { return (JSONObject) _content; } - public JSONObject getContent(MasterKey key) throws DatabaseFileException { + public JSONObject getContent(DatabaseFileCredentials creds) throws DatabaseFileException { try { byte[] bytes = Base64.decode((String) _content); - CryptResult result = key.decrypt(bytes, _cryptParameters); + CryptResult result = creds.decrypt(bytes, _header.getParams()); return new JSONObject(new String(result.getData(), "UTF-8")); } catch (MasterKeyException | JSONException | UnsupportedEncodingException | Base64Exception e) { throw new DatabaseFileException(e); } } - public void setContent(JSONObject dbObj) { - _content = dbObj; - _cryptParameters = null; - _slots = null; + public void setContent(JSONObject obj) { + _content = obj; + _header = new Header(null, null); } - public void setContent(JSONObject dbObj, MasterKey key) throws DatabaseFileException { + public void setContent(JSONObject obj, DatabaseFileCredentials creds) throws DatabaseFileException { try { - String string = dbObj.toString(4); + String string = obj.toString(4); byte[] dbBytes = string.getBytes("UTF-8"); - CryptResult result = key.encrypt(dbBytes); + CryptResult result = creds.encrypt(dbBytes); _content = Base64.encode(result.getData()); - _cryptParameters = result.getParams(); + _header = new Header(creds.getSlots(), result.getParams()); } catch (MasterKeyException | UnsupportedEncodingException | JSONException e) { throw new DatabaseFileException(e); } } - public SlotList getSlots() { - return _slots; - } + public static class Header { + private SlotList _slots; + private CryptParameters _params; - public void setSlots(SlotList slots) { - _slots = slots; + public Header(SlotList slots, CryptParameters params) { + _slots = slots; + _params = params; + } + + public static Header fromJson(JSONObject obj) throws DatabaseFileException { + if (obj.isNull("slots") && obj.isNull("params")) { + return new Header(null, null); + } + + try { + SlotList slots = SlotList.fromJson(obj.getJSONArray("slots")); + CryptParameters params = CryptParameters.fromJson(obj.getJSONObject("params")); + return new Header(slots, params); + } catch (SlotListException | JSONException | HexException e) { + throw new DatabaseFileException(e); + } + } + + public JSONObject toJson() { + try { + JSONObject obj = new JSONObject(); + obj.put("slots", _slots != null ? _slots.toJson() : JSONObject.NULL); + obj.put("params", _params != null ? _params.toJson() : JSONObject.NULL); + return obj; + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + public SlotList getSlots() { + return _slots; + } + + public CryptParameters getParams() { + return _params; + } + + public boolean isEmpty() { + return _slots == null && _params == null; + } } } diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseFileCredentials.java b/app/src/main/java/me/impy/aegis/db/DatabaseFileCredentials.java new file mode 100644 index 00000000..c6185009 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/db/DatabaseFileCredentials.java @@ -0,0 +1,40 @@ +package me.impy.aegis.db; + +import java.io.Serializable; + +import me.impy.aegis.crypto.CryptParameters; +import me.impy.aegis.crypto.CryptResult; +import me.impy.aegis.crypto.MasterKey; +import me.impy.aegis.crypto.MasterKeyException; +import me.impy.aegis.db.slots.SlotList; + +public class DatabaseFileCredentials implements Serializable { + private MasterKey _key; + private SlotList _slots; + + public DatabaseFileCredentials() { + _key = MasterKey.generate(); + _slots = new SlotList(); + } + + public DatabaseFileCredentials(MasterKey key, SlotList slots) { + _key = key; + _slots = slots; + } + + public CryptResult encrypt(byte[] bytes) throws MasterKeyException { + return _key.encrypt(bytes); + } + + public CryptResult decrypt(byte[] bytes, CryptParameters params) throws MasterKeyException { + return _key.decrypt(bytes, params); + } + + public MasterKey getKey() { + return _key; + } + + public SlotList getSlots() { + return _slots; + } +} diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseManager.java b/app/src/main/java/me/impy/aegis/db/DatabaseManager.java index e7c66a7c..0e2907a5 100644 --- a/app/src/main/java/me/impy/aegis/db/DatabaseManager.java +++ b/app/src/main/java/me/impy/aegis/db/DatabaseManager.java @@ -14,17 +14,17 @@ import java.util.List; import java.util.UUID; import me.impy.aegis.BuildConfig; -import me.impy.aegis.crypto.MasterKey; -import me.impy.aegis.db.slots.SlotList; public class DatabaseManager { private static final String FILENAME = "aegis.json"; private static final String FILENAME_EXPORT = "aegis_export.json"; private static final String FILENAME_EXPORT_PLAIN = "aegis_export_plain.json"; - private MasterKey _key; - private DatabaseFile _file; private Database _db; + private DatabaseFile _file; + private DatabaseFileCredentials _creds; + private boolean _encrypt; + private Context _context; public DatabaseManager(Context context) { @@ -39,31 +39,17 @@ public class DatabaseManager { public void load() throws DatabaseManagerException { assertState(true, false); - try { - byte[] fileBytes; - FileInputStream file = null; - - try { - file = _context.openFileInput(FILENAME); - fileBytes = new byte[(int) file.getChannel().size()]; - DataInputStream stream = new DataInputStream(file); - stream.readFully(fileBytes); - stream.close(); - } finally { - // always close the file stream - // there is no need to close the DataInputStream - if (file != null) { - file.close(); - } - } - - _file = new DatabaseFile(); - _file.deserialize(fileBytes); + try (FileInputStream file = _context.openFileInput(FILENAME)) { + byte[] fileBytes = new byte[(int) file.getChannel().size()]; + DataInputStream stream = new DataInputStream(file); + stream.readFully(fileBytes); + stream.close(); - if (!_file.isEncrypted()) { + _file = DatabaseFile.fromBytes(fileBytes); + _encrypt = _file.isEncrypted(); + if (!isEncryptionEnabled()) { JSONObject obj = _file.getContent(); - _db = new Database(); - _db.deserialize(obj); + _db = Database.fromJson(obj); } } catch (IOException | DatabaseFileException | DatabaseException e) { throw new DatabaseManagerException(e); @@ -73,37 +59,26 @@ public class DatabaseManager { public void lock() { assertState(false, true); // TODO: properly clear everything - _key = null; + _creds = null; _db = null; } - public void unlock(MasterKey key) throws DatabaseManagerException { + public void unlock(DatabaseFileCredentials creds) throws DatabaseManagerException { assertState(true, true); try { - JSONObject obj = _file.getContent(key); - _db = new Database(); - _db.deserialize(obj); - _key = key; + JSONObject obj = _file.getContent(creds); + _db = Database.fromJson(obj); + _creds = creds; } catch (DatabaseFileException | DatabaseException e) { throw new DatabaseManagerException(e); } } public static void save(Context context, DatabaseFile file) throws DatabaseManagerException { - try { - byte[] bytes = file.serialize(); - - FileOutputStream stream = null; - try { - stream = context.openFileOutput(FILENAME, Context.MODE_PRIVATE); - stream.write(bytes); - } finally { - // always close the file stream - if (stream != null) { - stream.close(); - } - } + byte[] bytes = file.toBytes(); + try (FileOutputStream stream = context.openFileOutput(FILENAME, Context.MODE_PRIVATE)) { + stream.write(bytes); } catch (IOException e) { throw new DatabaseManagerException(e); } @@ -113,9 +88,9 @@ public class DatabaseManager { assertState(false, true); try { - JSONObject obj = _db.serialize(); - if (_file.isEncrypted()) { - _file.setContent(obj, _key); + JSONObject obj = _db.toJson(); + if (isEncryptionEnabled()) { + _file.setContent(obj, _creds); } else { _file.setContent(obj); } @@ -130,31 +105,22 @@ public class DatabaseManager { try { DatabaseFile dbFile = new DatabaseFile(); - dbFile.setSlots(_file.getSlots()); - if (encrypt && getFile().isEncrypted()) { - dbFile.setContent(_db.serialize(), _key); + if (encrypt && isEncryptionEnabled()) { + dbFile.setContent(_db.toJson(), _creds); } else { - dbFile.setContent(_db.serialize()); + dbFile.setContent(_db.toJson()); } - File file; - FileOutputStream stream = null; - try { - String dirName = !BuildConfig.DEBUG ? "Aegis" : "AegisDebug"; - File dir = new File(Environment.getExternalStorageDirectory(), dirName); - if (!dir.exists() && !dir.mkdirs()) { - throw new IOException("error creating external storage directory"); - } - - byte[] bytes = dbFile.serialize(); - file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN); - stream = new FileOutputStream(file); + String dirName = !BuildConfig.DEBUG ? "Aegis" : "AegisDebug"; + File dir = new File(Environment.getExternalStorageDirectory(), dirName); + if (!dir.exists() && !dir.mkdirs()) { + throw new IOException("error creating external storage directory"); + } + + byte[] bytes = dbFile.toBytes(); + File file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN); + try (FileOutputStream stream = new FileOutputStream(file)) { stream.write(bytes); - } finally { - // always close the file stream - if (stream != null) { - stream.close(); - } } return file.getAbsolutePath(); @@ -193,25 +159,38 @@ public class DatabaseManager { return _db.getEntryByUUID(uuid); } - public MasterKey getMasterKey() { + public DatabaseFileCredentials getCredentials() { assertState(false, true); - return _key; + return _creds; } - public DatabaseFile getFile() { - return _file; + public void setCredentials(DatabaseFileCredentials creds) { + assertState(false, true); + _creds = creds; } - public void enableEncryption(MasterKey key, SlotList slots) { + public DatabaseFile.Header getFileHeader() { + assertLoaded(true); + return _file.getHeader(); + } + + public boolean isEncryptionEnabled() { + assertLoaded(true); + return _encrypt; + } + + public void enableEncryption(DatabaseFileCredentials creds) throws DatabaseManagerException { assertState(false, true); - _key = key; - _file.setSlots(slots); + _creds = creds; + _encrypt = true; + save(); } - public void disableEncryption() { + public void disableEncryption() throws DatabaseManagerException { assertState(false, true); - _key = null; - _file.setSlots(null); + _creds = null; + _encrypt = false; + save(); } public boolean isLoaded() { @@ -223,16 +202,20 @@ public class DatabaseManager { } private void assertState(boolean locked, boolean loaded) { - if (isLoaded() && !loaded) { - throw new AssertionError("database file has not been loaded yet"); - } else if (!isLoaded() && loaded) { - throw new AssertionError("database file has is already been loaded"); - } + assertLoaded(loaded); if (isLocked() && !locked) { throw new AssertionError("database file has not been unlocked yet"); } else if (!isLocked() && locked) { - throw new AssertionError("database file has is already been unlocked"); + throw new AssertionError("database file has already been unlocked"); + } + } + + private void assertLoaded(boolean loaded) { + if (isLoaded() && !loaded) { + throw new AssertionError("database file has already been loaded"); + } else if (!isLoaded() && loaded) { + throw new AssertionError("database file has not been loaded yet"); } } } diff --git a/app/src/main/java/me/impy/aegis/db/slots/PasswordSlot.java b/app/src/main/java/me/impy/aegis/db/slots/PasswordSlot.java index 3cb88af8..88ae65a6 100644 --- a/app/src/main/java/me/impy/aegis/db/slots/PasswordSlot.java +++ b/app/src/main/java/me/impy/aegis/db/slots/PasswordSlot.java @@ -20,9 +20,9 @@ public class PasswordSlot extends RawSlot { } @Override - public JSONObject serialize() { + public JSONObject toJson() { try { - JSONObject obj = super.serialize(); + JSONObject obj = super.toJson(); obj.put("n", _n); obj.put("r", _r); obj.put("p", _p); diff --git a/app/src/main/java/me/impy/aegis/db/slots/Slot.java b/app/src/main/java/me/impy/aegis/db/slots/Slot.java index be9eee3e..1ee06427 100644 --- a/app/src/main/java/me/impy/aegis/db/slots/Slot.java +++ b/app/src/main/java/me/impy/aegis/db/slots/Slot.java @@ -10,7 +10,6 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.UUID; -import javax.crypto.AEADBadTagException; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; @@ -85,7 +84,7 @@ public abstract class Slot implements Serializable { } } - public JSONObject serialize() { + public JSONObject toJson() { try { JSONObject obj = new JSONObject(); JSONObject paramObj = _encryptedMasterKeyParams.toJson(); @@ -114,7 +113,7 @@ public abstract class Slot implements Serializable { JSONObject paramObj = obj.getJSONObject("key_params"); _encryptedMasterKey = Hex.decode(obj.getString("key")); - _encryptedMasterKeyParams = CryptParameters.parseJson(paramObj); + _encryptedMasterKeyParams = CryptParameters.fromJson(paramObj); } catch (JSONException | HexException e) { throw new SlotException(e); } diff --git a/app/src/main/java/me/impy/aegis/db/slots/SlotList.java b/app/src/main/java/me/impy/aegis/db/slots/SlotList.java index a8c75733..8f8c721f 100644 --- a/app/src/main/java/me/impy/aegis/db/slots/SlotList.java +++ b/app/src/main/java/me/impy/aegis/db/slots/SlotList.java @@ -12,16 +12,16 @@ import java.util.List; public class SlotList implements Iterable, Serializable { private List _slots = new ArrayList<>(); - public static JSONArray serialize(SlotList slots) { + public JSONArray toJson() { JSONArray array = new JSONArray(); - for (Slot slot : slots) { - array.put(slot.serialize()); + for (Slot slot : this) { + array.put(slot.toJson()); } return array; } - public static SlotList deserialize(JSONArray array) throws SlotListException { + public static SlotList fromJson(JSONArray array) throws SlotListException { SlotList slots = new SlotList(); try { diff --git a/app/src/main/java/me/impy/aegis/importers/AegisImporter.java b/app/src/main/java/me/impy/aegis/importers/AegisImporter.java index a5c01ff9..f23fbab9 100644 --- a/app/src/main/java/me/impy/aegis/importers/AegisImporter.java +++ b/app/src/main/java/me/impy/aegis/importers/AegisImporter.java @@ -4,16 +4,16 @@ import org.json.JSONObject; import java.util.List; -import me.impy.aegis.crypto.MasterKey; import me.impy.aegis.db.Database; import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.db.DatabaseException; import me.impy.aegis.db.DatabaseFile; +import me.impy.aegis.db.DatabaseFileCredentials; import me.impy.aegis.db.DatabaseFileException; import me.impy.aegis.util.ByteInputStream; public class AegisImporter extends DatabaseImporter { - private MasterKey _key; + private DatabaseFileCredentials _creds; private DatabaseFile _file; public AegisImporter(ByteInputStream stream) { @@ -24,8 +24,7 @@ public class AegisImporter extends DatabaseImporter { public void parse() throws DatabaseImporterException { try { byte[] bytes = _stream.getBytes(); - _file = new DatabaseFile(); - _file.deserialize(bytes); + _file = DatabaseFile.fromBytes(bytes); } catch (DatabaseFileException e) { throw new DatabaseImporterException(e); } @@ -35,14 +34,13 @@ public class AegisImporter extends DatabaseImporter { public List convert() throws DatabaseImporterException { try { JSONObject obj; - if (_file.isEncrypted() && _key != null) { - obj = _file.getContent(_key); + if (_file.isEncrypted() && _creds != null) { + obj = _file.getContent(_creds); } else { obj = _file.getContent(); } - Database db = new Database(); - db.deserialize(obj); + Database db = Database.fromJson(obj); return db.getEntries(); } catch (DatabaseException | DatabaseFileException e) { throw new DatabaseImporterException(e); @@ -54,8 +52,8 @@ public class AegisImporter extends DatabaseImporter { return _file.isEncrypted(); } - public void setKey(MasterKey key) { - _key = key; + public void setCredentials(DatabaseFileCredentials creds) { + _creds = creds; } public DatabaseFile getFile() { diff --git a/app/src/main/java/me/impy/aegis/importers/DatabaseImporter.java b/app/src/main/java/me/impy/aegis/importers/DatabaseImporter.java index d4827baf..758fd430 100644 --- a/app/src/main/java/me/impy/aegis/importers/DatabaseImporter.java +++ b/app/src/main/java/me/impy/aegis/importers/DatabaseImporter.java @@ -42,14 +42,6 @@ public abstract class DatabaseImporter { } } - public static List create(ByteInputStream stream) { - List list = new ArrayList<>(); - for (Class type : _importers.values()) { - list.add(create(stream, type)); - } - return list; - } - public static Map> getImporters() { return _importers; } diff --git a/app/src/main/java/me/impy/aegis/otp/OtpInfo.java b/app/src/main/java/me/impy/aegis/otp/OtpInfo.java index 714e49ea..cc6830f1 100644 --- a/app/src/main/java/me/impy/aegis/otp/OtpInfo.java +++ b/app/src/main/java/me/impy/aegis/otp/OtpInfo.java @@ -88,7 +88,7 @@ public abstract class OtpInfo implements Serializable { _digits = digits; } - public static OtpInfo parseJson(String type, JSONObject obj) throws OtpInfoException { + public static OtpInfo fromJson(String type, JSONObject obj) throws OtpInfoException { OtpInfo info; try { diff --git a/app/src/main/java/me/impy/aegis/ui/AuthActivity.java b/app/src/main/java/me/impy/aegis/ui/AuthActivity.java index 2e38d6fd..679e745f 100644 --- a/app/src/main/java/me/impy/aegis/ui/AuthActivity.java +++ b/app/src/main/java/me/impy/aegis/ui/AuthActivity.java @@ -25,6 +25,7 @@ import me.impy.aegis.R; import me.impy.aegis.crypto.KeyStoreHandle; import me.impy.aegis.crypto.KeyStoreHandleException; import me.impy.aegis.crypto.MasterKey; +import me.impy.aegis.db.DatabaseFileCredentials; import me.impy.aegis.db.slots.FingerprintSlot; import me.impy.aegis.db.slots.PasswordSlot; import me.impy.aegis.db.slots.Slot; @@ -128,7 +129,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C private void setKey(MasterKey key) { // send the master key back to the main activity Intent result = new Intent(); - result.putExtra("key", key); + result.putExtra("creds", new DatabaseFileCredentials(key, _slots)); setResult(RESULT_OK, result); finish(); } diff --git a/app/src/main/java/me/impy/aegis/ui/IntroActivity.java b/app/src/main/java/me/impy/aegis/ui/IntroActivity.java index 1ba39290..fe8aa06c 100644 --- a/app/src/main/java/me/impy/aegis/ui/IntroActivity.java +++ b/app/src/main/java/me/impy/aegis/ui/IntroActivity.java @@ -16,7 +16,7 @@ import javax.crypto.SecretKey; import me.impy.aegis.Preferences; import me.impy.aegis.R; -import me.impy.aegis.crypto.MasterKey; +import me.impy.aegis.db.DatabaseFileCredentials; import me.impy.aegis.db.DatabaseFileException; import me.impy.aegis.db.DatabaseManagerException; import me.impy.aegis.db.slots.FingerprintSlot; @@ -128,9 +128,9 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback } // generate the master key - MasterKey masterKey = null; + DatabaseFileCredentials creds = null; if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) { - masterKey = MasterKey.generate(); + creds = new DatabaseFileCredentials(); } SlotList slots = null; @@ -141,10 +141,8 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback throw new RuntimeException(); } try { - _passwordSlot.setKey(masterKey, _passwordCipher); - slots = new SlotList(); - slots.add(_passwordSlot); - _databaseFile.setSlots(slots); + _passwordSlot.setKey(creds.getKey(), _passwordCipher); + creds.getSlots().add(_passwordSlot); } catch (SlotException e) { setException(e); } @@ -156,8 +154,8 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback // and add it to the list of slots FingerprintSlot slot = _authenticatedSlide.getFingerSlot(); Cipher cipher = _authenticatedSlide.getFingerCipher(); - slot.setKey(masterKey, cipher); - slots.add(slot); + slot.setKey(creds.getKey(), cipher); + creds.getSlots().add(slot); } catch (SlotException e) { setException(e); return; @@ -166,11 +164,11 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback // finally, save the database try { - JSONObject obj = _database.serialize(); + JSONObject obj = _database.toJson(); if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) { _databaseFile.setContent(obj); } else { - _databaseFile.setContent(obj, masterKey); + _databaseFile.setContent(obj, creds); } DatabaseManager.save(getApplicationContext(), _databaseFile); } catch (DatabaseManagerException | DatabaseFileException e) { @@ -180,7 +178,7 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback // send the master key back to the main activity Intent result = new Intent(); - result.putExtra("key", masterKey); + result.putExtra("creds", creds); setResult(RESULT_OK, result); // skip the intro from now on diff --git a/app/src/main/java/me/impy/aegis/ui/MainActivity.java b/app/src/main/java/me/impy/aegis/ui/MainActivity.java index a148d80e..11622530 100644 --- a/app/src/main/java/me/impy/aegis/ui/MainActivity.java +++ b/app/src/main/java/me/impy/aegis/ui/MainActivity.java @@ -20,7 +20,7 @@ import java.lang.reflect.UndeclaredThrowableException; import me.impy.aegis.AegisApplication; import me.impy.aegis.R; -import me.impy.aegis.crypto.MasterKey; +import me.impy.aegis.db.DatabaseFileCredentials; import me.impy.aegis.db.DatabaseManagerException; import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.db.DatabaseManager; @@ -218,13 +218,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene throw new UndeclaredThrowableException(e); } - MasterKey key = (MasterKey) data.getSerializableExtra("key"); - unlockDatabase(key); + DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds"); + unlockDatabase(creds); } private void onDecryptResult(int resultCode, Intent intent) { - MasterKey key = (MasterKey) intent.getSerializableExtra("key"); - unlockDatabase(key); + DatabaseFileCredentials creds = (DatabaseFileCredentials) intent.getSerializableExtra("creds"); + unlockDatabase(creds); doShortcutActions(); } @@ -361,7 +361,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene } } - private void unlockDatabase(MasterKey key) { + private void unlockDatabase(DatabaseFileCredentials creds) { if (_loaded) { return; } @@ -371,15 +371,14 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene _db.load(); } if (_db.isLocked()) { - if (key == null) { + if (creds == null) { startAuthActivity(); return; } else { - _db.unlock(key); + _db.unlock(creds); } } } catch (DatabaseManagerException e) { - e.printStackTrace(); Toast.makeText(this, "An error occurred while trying to load/decrypt the database", Toast.LENGTH_LONG).show(); startAuthActivity(); return; @@ -396,7 +395,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene private void startAuthActivity() { Intent intent = new Intent(this, AuthActivity.class); - intent.putExtra("slots", _db.getFile().getSlots()); + intent.putExtra("slots", _db.getFileHeader().getSlots()); startActivityForResult(intent, CODE_DECRYPT); } @@ -404,7 +403,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene try { _db.save(); } catch (DatabaseManagerException e) { - e.printStackTrace(); Toast.makeText(this, "An error occurred while trying to save the database", Toast.LENGTH_LONG).show(); } } @@ -413,7 +411,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene // hide the lock icon if the database is not unlocked if (_menu != null && !_db.isLocked()) { MenuItem item = _menu.findItem(R.id.action_lock); - item.setVisible(_db.getFile().isEncrypted()); + item.setVisible(_db.isEncryptionEnabled()); } } diff --git a/app/src/main/java/me/impy/aegis/ui/PreferencesFragment.java b/app/src/main/java/me/impy/aegis/ui/PreferencesFragment.java index 191ea6bc..01b666e3 100644 --- a/app/src/main/java/me/impy/aegis/ui/PreferencesFragment.java +++ b/app/src/main/java/me/impy/aegis/ui/PreferencesFragment.java @@ -26,12 +26,12 @@ import javax.crypto.Cipher; import me.impy.aegis.AegisApplication; import me.impy.aegis.R; -import me.impy.aegis.crypto.MasterKey; import me.impy.aegis.db.DatabaseEntry; +import me.impy.aegis.db.DatabaseFileCredentials; +import me.impy.aegis.db.DatabaseFileException; import me.impy.aegis.db.DatabaseManager; import me.impy.aegis.db.DatabaseManagerException; import me.impy.aegis.db.slots.Slot; -import me.impy.aegis.db.slots.SlotList; import me.impy.aegis.db.slots.SlotException; import me.impy.aegis.helpers.PermissionHelper; import me.impy.aegis.importers.AegisImporter; @@ -138,7 +138,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas _encryptionPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - if (!_db.getFile().isEncrypted()) { + if (!_db.isEncryptionEnabled()) { PasswordDialogFragment dialog = new PasswordDialogFragment(); // TODO: find a less ugly way to obtain the fragment manager dialog.show(getActivity().getSupportFragmentManager(), null); @@ -148,8 +148,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas .setMessage("Are you sure you want to disable encryption? This will cause the database to be stored in plain text") .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { - _db.disableEncryption(); - saveDatabase(); + try { + _db.disableEncryption(); + } catch (DatabaseManagerException e) { + Toast.makeText(getActivity(), "An error occurred while enabling encryption", Toast.LENGTH_SHORT).show(); + } updateEncryptionPreference(); } }) @@ -163,10 +166,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas _slotsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { - MasterKey masterKey = _db.getMasterKey(); Intent intent = new Intent(getActivity(), SlotManagerActivity.class); - intent.putExtra("masterKey", masterKey); - intent.putExtra("slots", _db.getFile().getSlots()); + intent.putExtra("creds", _db.getCredentials()); startActivityForResult(intent, CODE_SLOTS); return true; } @@ -249,13 +250,12 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas return; } - MasterKey key = (MasterKey) data.getSerializableExtra("key"); - ((AegisImporter)_importer).setKey(key); + DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds"); + ((AegisImporter)_importer).setCredentials(creds); try { importDatabase(_importer); } catch (DatabaseImporterException e) { - e.printStackTrace(); Toast.makeText(getActivity(), "An error occurred while trying to parse the file", Toast.LENGTH_SHORT).show(); } @@ -269,25 +269,16 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas } ByteInputStream stream; - InputStream fileStream = null; - try { - fileStream = getActivity().getContentResolver().openInputStream(uri); + try (InputStream fileStream = getActivity().getContentResolver().openInputStream(uri)) { stream = ByteInputStream.create(fileStream); } catch (FileNotFoundException e) { Toast.makeText(getActivity(), "Error: File not found", Toast.LENGTH_SHORT).show(); return; } catch (IOException e) { + e.printStackTrace(); Toast.makeText(getActivity(), "An error occurred while trying to read the file", Toast.LENGTH_SHORT).show(); return; - } finally { - if (fileStream != null) { - try { - fileStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } } try { @@ -299,7 +290,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas _importer = importer; Intent intent = new Intent(getActivity(), AuthActivity.class); - intent.putExtra("slots", ((AegisImporter)_importer).getFile().getSlots()); + intent.putExtra("slots", ((AegisImporter)_importer).getFile().getHeader().getSlots()); startActivityForResult(intent, CODE_IMPORT_DECRYPT); return; } @@ -344,7 +335,6 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas try { filename = _db.export(checked[0]); } catch (DatabaseManagerException e) { - e.printStackTrace(); Toast.makeText(getActivity(), "An error occurred while trying to export the database", Toast.LENGTH_SHORT).show(); return; } @@ -355,7 +345,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas Toast.makeText(getActivity(), "The database has been exported to: " + filename, Toast.LENGTH_SHORT).show(); }) .setNegativeButton(android.R.string.cancel, null); - if (_db.getFile().isEncrypted()) { + if (_db.isEncryptionEnabled()) { final String[] items = {"Keep the database encrypted"}; final boolean[] checkedItems = {true}; builder.setMultiChoiceItems(items, checkedItems, new DialogInterface.OnMultiChoiceClickListener() { @@ -375,8 +365,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas return; } - SlotList slots = (SlotList) data.getSerializableExtra("slots"); - _db.getFile().setSlots(slots); + DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds"); + _db.setCredentials(creds); saveDatabase(); } @@ -384,7 +374,6 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas try { _db.save(); } catch (DatabaseManagerException e) { - e.printStackTrace(); Toast.makeText(getActivity(), "An error occurred while trying to save the database", Toast.LENGTH_LONG).show(); return false; } @@ -394,19 +383,17 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas @Override public void onSlotResult(Slot slot, Cipher cipher) { - MasterKey masterKey = MasterKey.generate(); + DatabaseFileCredentials creds = new DatabaseFileCredentials(); - SlotList slots = new SlotList(); try { - slot.setKey(masterKey, cipher); - } catch (SlotException e) { + slot.setKey(creds.getKey(), cipher); + creds.getSlots().add(slot); + _db.enableEncryption(creds); + } catch (DatabaseManagerException | SlotException e) { onException(e); return; } - slots.add(slot); - _db.enableEncryption(masterKey, slots); - saveDatabase(); updateEncryptionPreference(); } @@ -417,7 +404,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas } private void updateEncryptionPreference() { - boolean encrypted = _db.getFile().isEncrypted(); + boolean encrypted = _db.isEncryptionEnabled(); _encryptionPreference.setChecked(encrypted, true); _slotsPreference.setEnabled(encrypted); } diff --git a/app/src/main/java/me/impy/aegis/ui/ScannerActivity.java b/app/src/main/java/me/impy/aegis/ui/ScannerActivity.java index 65f787cf..bc887da5 100644 --- a/app/src/main/java/me/impy/aegis/ui/ScannerActivity.java +++ b/app/src/main/java/me/impy/aegis/ui/ScannerActivity.java @@ -110,10 +110,7 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R // parse google auth uri Uri uri = Uri.parse(rawResult.getText()); GoogleAuthInfo info = GoogleAuthInfo.parseUri(uri); - - DatabaseEntry entry = new DatabaseEntry(info.getOtpInfo()); - entry.setIssuer(info.getIssuer()); - entry.setName(info.getAccountName()); + DatabaseEntry entry = new DatabaseEntry(info); Intent intent = new Intent(); intent.putExtra("entry", entry); diff --git a/app/src/main/java/me/impy/aegis/ui/SlotManagerActivity.java b/app/src/main/java/me/impy/aegis/ui/SlotManagerActivity.java index 05359d6c..e6b974e9 100644 --- a/app/src/main/java/me/impy/aegis/ui/SlotManagerActivity.java +++ b/app/src/main/java/me/impy/aegis/ui/SlotManagerActivity.java @@ -16,7 +16,7 @@ import javax.crypto.Cipher; import me.impy.aegis.R; import me.impy.aegis.crypto.KeyStoreHandle; import me.impy.aegis.crypto.KeyStoreHandleException; -import me.impy.aegis.crypto.MasterKey; +import me.impy.aegis.db.DatabaseFileCredentials; import me.impy.aegis.db.slots.FingerprintSlot; import me.impy.aegis.db.slots.PasswordSlot; import me.impy.aegis.db.slots.Slot; @@ -29,8 +29,7 @@ import me.impy.aegis.ui.views.SlotAdapter; import me.impy.aegis.ui.dialogs.SlotDialogFragment; public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener, SlotDialogFragment.Listener { - private MasterKey _masterKey; - private SlotList _slots; + private DatabaseFileCredentials _creds; private SlotAdapter _adapter; private boolean _edited; @@ -59,9 +58,8 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li slotsView.setNestedScrollingEnabled(false); // load the slots and masterKey - _masterKey = (MasterKey) getIntent().getSerializableExtra("masterKey"); - _slots = (SlotList) getIntent().getSerializableExtra("slots"); - for (Slot slot : _slots) { + _creds = (DatabaseFileCredentials) getIntent().getSerializableExtra("creds"); + for (Slot slot : _creds.getSlots()) { _adapter.addSlot(slot); } @@ -75,7 +73,7 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li if (FingerprintHelper.getManager(this) != null) { try { KeyStoreHandle keyStore = new KeyStoreHandle(); - for (FingerprintSlot slot : _slots.findAll(FingerprintSlot.class)) { + for (FingerprintSlot slot : _creds.getSlots().findAll(FingerprintSlot.class)) { if (keyStore.containsKey(slot.getUUID().toString())) { visibility = View.GONE; break; @@ -92,7 +90,7 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li private void onSave() { Intent intent = new Intent(); - intent.putExtra("slots", _slots); + intent.putExtra("creds", _creds); setResult(RESULT_OK, intent); finish(); } @@ -150,7 +148,8 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li @Override public void onRemoveSlot(Slot slot) { - if (slot instanceof PasswordSlot && _slots.findAll(PasswordSlot.class).size() <= 1) { + SlotList slots = _creds.getSlots(); + if (slot instanceof PasswordSlot && slots.findAll(PasswordSlot.class).size() <= 1) { Toast.makeText(this, "You must have at least one password slot", Toast.LENGTH_SHORT).show(); return; } @@ -159,7 +158,7 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li .setTitle("Remove slot") .setMessage("Are you sure you want to remove this slot?") .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> { - _slots.remove(slot); + slots.remove(slot); _adapter.removeSlot(slot); _edited = true; updateFingerprintButton(); @@ -171,13 +170,13 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li @Override public void onSlotResult(Slot slot, Cipher cipher) { try { - slot.setKey(_masterKey, cipher); + slot.setKey(_creds.getKey(), cipher); } catch (SlotException e) { onException(e); return; } - _slots.add(slot); + _creds.getSlots().add(slot); _adapter.addSlot(slot); _edited = true; updateFingerprintButton();