diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 26cf9e0a..7da6d8ae 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,7 +34,7 @@ android:configChanges="keyboardHidden|orientation|screenSize"> diff --git a/app/src/main/java/me/impy/aegis/crypto/KeyInfo.java b/app/src/main/java/me/impy/aegis/crypto/KeyInfo.java deleted file mode 100644 index 5d6f9231..00000000 --- a/app/src/main/java/me/impy/aegis/crypto/KeyInfo.java +++ /dev/null @@ -1,236 +0,0 @@ -package me.impy.aegis.crypto; - -import android.net.Uri; - -import java.io.Serializable; -import java.util.Arrays; - -import me.impy.aegis.encoding.Base32; -import me.impy.aegis.encoding.Base32Exception; - -public class KeyInfo implements Serializable { - private String _type = "totp"; - private byte[] _secret; - private String _accountName = ""; - private String _issuer = ""; - private long _counter = 0; - private String _algorithm = "SHA1"; - private int _digits = 6; - private int _period = 30; - - public String getURL() { - Uri.Builder builder = new Uri.Builder(); - builder.scheme("otpauth"); - builder.authority(_type); - - builder.appendQueryParameter("digits", Integer.toString(_digits)); - builder.appendQueryParameter("period", Integer.toString(_period)); - builder.appendQueryParameter("algorithm", _algorithm); - builder.appendQueryParameter("secret", new String(Base32.encode(_secret))); - if (_type.equals("hotp")) { - builder.appendQueryParameter("counter", Long.toString(_counter)); - } - - if (_issuer != null && !_issuer.equals("")) { - builder.path(String.format("%s:%s", _issuer, _accountName)); - builder.appendQueryParameter("issuer", _issuer); - } else { - builder.path(_accountName); - } - - return builder.build().toString(); - } - - public long getMillisTillNextRotation() { - return KeyInfo.getMillisTillNextRotation(_period); - } - - public static long getMillisTillNextRotation(int period) { - long p = period * 1000; - return p - (System.currentTimeMillis() % p); - } - - public static KeyInfo fromURL(String s) throws KeyInfoException { - final Uri url = Uri.parse(s); - if (!url.getScheme().equals("otpauth")) { - throw new KeyInfoException("unsupported protocol"); - } - - KeyInfo info = new KeyInfo(); - info.setType(url.getHost()); - - // 'secret' is a required parameter - String secret = url.getQueryParameter("secret"); - if (secret == null) { - throw new KeyInfoException("'secret' is not set"); - } - info.setSecret(secret.toCharArray()); - - // provider info used to disambiguate accounts - String path = url.getPath(); - String label = path != null && path.length() > 0 ? path.substring(1) : ""; - - if (label.contains(":")) { - // a label can only contain one colon - // it's ok to fail if that's not the case - String[] strings = label.split(":"); - - if (strings.length == 2) { - info.setIssuer(strings[0]); - info.setAccountName(strings[1]); - } else { - // at this point, just dump the whole thing into the accountName - info.setAccountName(label); - } - } else { - // label only contains the account name - // grab the issuer's info from the 'issuer' parameter if it's present - String issuer = url.getQueryParameter("issuer"); - info.setIssuer(issuer != null ? issuer : ""); - info.setAccountName(label); - } - - // just use the defaults if these parameters aren't set - String algorithm = url.getQueryParameter("algorithm"); - if (algorithm != null) { - info.setAlgorithm(algorithm); - } - String period = url.getQueryParameter("period"); - if (period != null) { - info.setPeriod(Integer.parseInt(period)); - } - String digits = url.getQueryParameter("digits"); - if (digits != null) { - info.setDigits(Integer.parseInt(digits)); - } - - // 'counter' is required if the type is 'hotp' - String counter = url.getQueryParameter("counter"); - if (counter != null) { - info.setCounter(Long.parseLong(counter)); - } else if (info.getType().equals("hotp")) { - throw new KeyInfoException("'counter' was not set which is required for 'hotp'"); - } - - return info; - } - - public String getType() { - return _type; - } - - public byte[] getSecret() { - return _secret; - } - - public String getAccountName() { - return _accountName; - } - - public String getIssuer() { - return _issuer; - } - - public String getAlgorithm(boolean java) { - if (java) { - return "Hmac" + _algorithm; - } - return _algorithm; - } - - public int getDigits() { - return _digits; - } - - public long getCounter() { - return _counter; - } - - public int getPeriod() { - return _period; - } - - public boolean isTypeValid(String type) { - return type.equals("totp") || type.equals("hotp"); - } - - public void setType(String type) throws KeyInfoException { - type = type.toLowerCase(); - if (!isTypeValid(type)) { - throw new KeyInfoException(String.format("unsupported otp type: %s", type)); - } - _type = type; - } - - public void setSecret(char[] base32) throws KeyInfoException { - byte[] secret; - try { - secret = Base32.decode(base32); - } catch (Base32Exception e) { - throw new KeyInfoException("bad secret", e); - } - - setSecret(secret); - } - - public void setSecret(byte[] secret) { - _secret = secret; - } - - public void setAccountName(String accountName) { - _accountName = accountName; - } - - public void setIssuer(String issuer) { - _issuer = issuer; - } - - public boolean isAlgorithmValid(String algorithm) { - return algorithm.equals("SHA1") || algorithm.equals("SHA256") || algorithm.equals("SHA512"); - } - - public void setAlgorithm(String algorithm) throws KeyInfoException { - if (algorithm.startsWith("Hmac")) { - algorithm = algorithm.substring(4); - } - algorithm = algorithm.toUpperCase(); - - if (!isAlgorithmValid(algorithm)) { - throw new KeyInfoException(String.format("unsupported algorithm: %s", algorithm)); - } - _algorithm = algorithm; - } - - public boolean isDigitsValid(int digits) { - return digits == 6 || digits == 8; - } - - public void setDigits(int digits) throws KeyInfoException { - if (!isDigitsValid(digits)) { - throw new KeyInfoException(String.format("unsupported amount of digits: %d", digits)); - } - _digits = digits; - } - - public boolean isCounterValid(long count) { - return count >= 0; - } - - public void setCounter(long count) throws KeyInfoException { - if (!isCounterValid(count)) { - throw new KeyInfoException(String.format("bad count: %d", count)); - } - _counter = count; - } - - public boolean isPeriodValid(int period) { - return period > 0; - } - - public void setPeriod(int period) throws KeyInfoException { - if (!isPeriodValid(period)) { - throw new KeyInfoException(String.format("bad period: %d", period)); - } - _period = period; - } -} diff --git a/app/src/main/java/me/impy/aegis/crypto/otp/OTP.java b/app/src/main/java/me/impy/aegis/crypto/otp/OTP.java deleted file mode 100644 index 72bbaba6..00000000 --- a/app/src/main/java/me/impy/aegis/crypto/otp/OTP.java +++ /dev/null @@ -1,33 +0,0 @@ -package me.impy.aegis.crypto.otp; - -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - -import me.impy.aegis.crypto.KeyInfo; - -public class OTP { - private OTP() { - } - - public static String generateOTP(KeyInfo info) throws OTPException { - String otp; - - try { - switch (info.getType()) { - case "totp": - String time = Long.toHexString(System.currentTimeMillis() / 1000 / info.getPeriod()); - otp = TOTP.generateTOTP(info.getSecret(), time, info.getDigits(), info.getAlgorithm(true)); - break; - case "hotp": - otp = HOTP.generateOTP(info.getSecret(), info.getCounter(), info.getDigits(), false, -1); - break; - default: - throw new RuntimeException("Bad OTP type"); - } - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new OTPException(e); - } - - return otp; - } -} diff --git a/app/src/main/java/me/impy/aegis/crypto/otp/OTPException.java b/app/src/main/java/me/impy/aegis/crypto/otp/OTPException.java deleted file mode 100644 index 8866b7a1..00000000 --- a/app/src/main/java/me/impy/aegis/crypto/otp/OTPException.java +++ /dev/null @@ -1,7 +0,0 @@ -package me.impy.aegis.crypto.otp; - -public class OTPException extends Exception { - public OTPException(Throwable cause) { - super(cause); - } -} 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 abc30308..c1725899 100644 --- a/app/src/main/java/me/impy/aegis/db/Database.java +++ b/app/src/main/java/me/impy/aegis/db/Database.java @@ -4,19 +4,16 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.UUID; -import me.impy.aegis.crypto.KeyInfoException; +import me.impy.aegis.otp.OtpInfoException; public class Database { private static final int VERSION = 1; - private List _entries = new ArrayList<>(); + private DatabaseEntryList _entries = new DatabaseEntryList(); - public JSONObject serialize() throws DatabaseException { + public JSONObject serialize() { try { JSONArray array = new JSONArray(); for (DatabaseEntry e : _entries) { @@ -28,7 +25,7 @@ public class Database { obj.put("entries", array); return obj; } catch (JSONException e) { - throw new DatabaseException(e); + throw new RuntimeException(e); } } @@ -44,52 +41,30 @@ public class Database { for (int i = 0; i < array.length(); i++) { DatabaseEntry entry = new DatabaseEntry(null); entry.deserialize(array.getJSONObject(i)); - addKey(entry); + addEntry(entry); } - } catch (JSONException | KeyInfoException e) { + } catch (OtpInfoException | JSONException e) { throw new DatabaseException(e); } } - public void addKey(DatabaseEntry entry) { - if (tryGetKeyByUUID(entry.getUUID()) != null) { - throw new AssertionError("entry found with the same uuid"); - } + public void addEntry(DatabaseEntry entry) { _entries.add(entry); } - public void removeKey(DatabaseEntry entry) { - entry = getKeyByUUID(entry.getUUID()); + public void removeEntry(DatabaseEntry entry) { _entries.remove(entry); } - public void replaceKey(DatabaseEntry newEntry) { - DatabaseEntry oldEntry = getKeyByUUID(newEntry.getUUID()); - _entries.set(_entries.indexOf(oldEntry), newEntry); + public void replaceEntry(DatabaseEntry newEntry) { + _entries.replace(newEntry); } - public void swapKeys(DatabaseEntry entry1, DatabaseEntry entry2) { - Collections.swap(_entries, _entries.indexOf(entry1), _entries.indexOf(entry2)); + public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) { + _entries.swap(entry1, entry2); } - public List getKeys() { - return Collections.unmodifiableList(_entries); - } - - private DatabaseEntry tryGetKeyByUUID(UUID uuid) { - for (DatabaseEntry entry : _entries) { - if (entry.getUUID().equals(uuid)) { - return entry; - } - } - return null; - } - - private DatabaseEntry getKeyByUUID(UUID uuid) { - DatabaseEntry entry = tryGetKeyByUUID(uuid); - if (entry == null) { - throw new AssertionError("no entry found with the same uuid"); - } - return entry; + public List getEntries() { + return _entries.getList(); } } 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 564ed88f..624f02b2 100644 --- a/app/src/main/java/me/impy/aegis/db/DatabaseEntry.java +++ b/app/src/main/java/me/impy/aegis/db/DatabaseEntry.java @@ -6,33 +6,44 @@ import org.json.JSONObject; import java.io.Serializable; import java.util.UUID; -import me.impy.aegis.crypto.KeyInfo; -import me.impy.aegis.crypto.KeyInfoException; +import me.impy.aegis.otp.OtpInfo; +import me.impy.aegis.otp.OtpInfoException; public class DatabaseEntry implements Serializable { private UUID _uuid; private String _name = ""; + private String _issuer = ""; private String _icon = ""; - private KeyInfo _info; + private OtpInfo _info; - public DatabaseEntry() { - this(new KeyInfo()); - } - - public DatabaseEntry(KeyInfo info) { + public DatabaseEntry(OtpInfo info) { _info = info; _uuid = UUID.randomUUID(); } - public JSONObject serialize() throws JSONException { + public DatabaseEntry(OtpInfo info, String name, String issuer) { + this(info); + setName(name); + setIssuer(issuer); + } + + public JSONObject serialize() { JSONObject obj = new JSONObject(); - obj.put("uuid", _uuid.toString()); - obj.put("name", _name); - obj.put("url", _info.getURL()); + + try { + obj.put("type", _info.getType()); + obj.put("uuid", _uuid.toString()); + obj.put("name", _name); + obj.put("issuer", _issuer); + obj.put("info", _info.toJson()); + } catch (JSONException e) { + throw new RuntimeException(e); + } + return obj; } - public void deserialize(JSONObject obj) throws JSONException, KeyInfoException { + public void deserialize(JSONObject obj) throws JSONException, OtpInfoException { // if there is no uuid, generate a new one if (!obj.has("uuid")) { _uuid = UUID.randomUUID(); @@ -40,29 +51,43 @@ public class DatabaseEntry implements Serializable { _uuid = UUID.fromString(obj.getString("uuid")); } _name = obj.getString("name"); - _info = KeyInfo.fromURL(obj.getString("url")); + _issuer = obj.getString("issuer"); + _info = OtpInfo.parseJson(obj.getString("type"), obj.getJSONObject("info")); } public UUID getUUID() { return _uuid; } + public String getName() { return _name; } + + public String getIssuer() { + return _issuer; + } + public String getIcon() { return _icon; } - public KeyInfo getInfo() { + + public OtpInfo getInfo() { return _info; } public void setName(String name) { _name = name; } + + public void setIssuer(String issuer) { + _issuer = issuer; + } + public void setIcon(String icon) { _icon = icon; } - public void setInfo(KeyInfo info) { + + public void setInfo(OtpInfo info) { _info = info; } } diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseEntryList.java b/app/src/main/java/me/impy/aegis/db/DatabaseEntryList.java new file mode 100644 index 00000000..ec0ba801 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/db/DatabaseEntryList.java @@ -0,0 +1,62 @@ +package me.impy.aegis.db; + +import android.support.annotation.NonNull; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; + +public class DatabaseEntryList implements Iterable, Serializable { + private List _entries = new ArrayList<>(); + + @NonNull + @Override + public Iterator iterator() { + return _entries.iterator(); + } + + public void add(DatabaseEntry entry) { + if (tryGetByUUID(entry.getUUID()) != null) { + throw new AssertionError("entry found with the same uuid"); + } + _entries.add(entry); + } + + public void remove(DatabaseEntry entry) { + entry = getByUUID(entry.getUUID()); + _entries.remove(entry); + } + + public void replace(DatabaseEntry newEntry) { + DatabaseEntry oldEntry = getByUUID(newEntry.getUUID()); + _entries.set(_entries.indexOf(oldEntry), newEntry); + } + + public void swap(DatabaseEntry entry1, DatabaseEntry entry2) { + Collections.swap(_entries, _entries.indexOf(entry1), _entries.indexOf(entry2)); + } + + public List getList() { + return Collections.unmodifiableList(_entries); + } + + private DatabaseEntry tryGetByUUID(UUID uuid) { + for (DatabaseEntry entry : _entries) { + if (entry.getUUID().equals(uuid)) { + return entry; + } + } + return null; + } + + private DatabaseEntry getByUUID(UUID uuid) { + DatabaseEntry entry = tryGetByUUID(uuid); + if (entry == null) { + throw new AssertionError("no entry found with the same uuid"); + } + return entry; + } +} 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 8f9d216a..6e2f0878 100644 --- a/app/src/main/java/me/impy/aegis/db/DatabaseFile.java +++ b/app/src/main/java/me/impy/aegis/db/DatabaseFile.java @@ -23,7 +23,7 @@ public class DatabaseFile { private CryptParameters _cryptParameters; private SlotCollection _slots; - public byte[] serialize() throws DatabaseFileException { + public byte[] serialize() { try { JSONObject cryptObj = null; if (isEncrypted()) { @@ -45,8 +45,8 @@ public class DatabaseFile { String string = obj.toString(4); return string.getBytes("UTF-8"); - } catch (SlotCollectionException | UnsupportedEncodingException | JSONException e) { - throw new DatabaseFileException(e); + } catch (JSONException | UnsupportedEncodingException e) { + throw new RuntimeException(e); } } 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 a50f92dd..87f29255 100644 --- a/app/src/main/java/me/impy/aegis/db/DatabaseManager.java +++ b/app/src/main/java/me/impy/aegis/db/DatabaseManager.java @@ -103,7 +103,7 @@ public class DatabaseManager { stream.close(); } } - } catch (IOException | DatabaseFileException e) { + } catch (IOException e) { throw new DatabaseManagerException(e); } } @@ -119,7 +119,7 @@ public class DatabaseManager { _file.setContent(obj); } save(_context, _file); - } catch (DatabaseException | DatabaseFileException e) { + } catch (DatabaseFileException e) { throw new DatabaseManagerException(e); } } @@ -157,34 +157,34 @@ public class DatabaseManager { } return file.getAbsolutePath(); - } catch (DatabaseException | IOException | DatabaseFileException e) { + } catch (IOException | DatabaseFileException e) { throw new DatabaseManagerException(e); } } - public void addKey(DatabaseEntry entry) { + public void addEntry(DatabaseEntry entry) { assertState(false, true); - _db.addKey(entry); + _db.addEntry(entry); } - public void removeKey(DatabaseEntry entry) { + public void removeEntry(DatabaseEntry entry) { assertState(false, true); - _db.removeKey(entry); + _db.removeEntry(entry); } - public void replaceKey(DatabaseEntry entry) { + public void replaceEntry(DatabaseEntry entry) { assertState(false, true); - _db.replaceKey(entry); + _db.replaceEntry(entry); } - public void swapKeys(DatabaseEntry entry1, DatabaseEntry entry2) { + public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) { assertState(false, true); - _db.swapKeys(entry1, entry2); + _db.swapEntries(entry1, entry2); } - public List getKeys() { + public List getEntries() { assertState(false, true); - return _db.getKeys(); + return _db.getEntries(); } public MasterKey getMasterKey() { 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 c3a67941..3cb88af8 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 @@ -3,9 +3,6 @@ package me.impy.aegis.db.slots; import org.json.JSONException; import org.json.JSONObject; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; - import javax.crypto.SecretKey; import me.impy.aegis.crypto.CryptoUtils; @@ -23,7 +20,7 @@ public class PasswordSlot extends RawSlot { } @Override - public JSONObject serialize() throws SlotException { + public JSONObject serialize() { try { JSONObject obj = super.serialize(); obj.put("n", _n); @@ -32,7 +29,7 @@ public class PasswordSlot extends RawSlot { obj.put("salt", Hex.encode(_salt)); return obj; } catch (JSONException e) { - throw new SlotException(e); + throw new RuntimeException(e); } } 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 5d969de8..cbb0278d 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 @@ -67,7 +67,7 @@ public abstract class Slot implements Serializable { } } - public JSONObject serialize() throws SlotException { + public JSONObject serialize() { try { JSONObject obj = new JSONObject(); obj.put("type", getType()); @@ -75,7 +75,7 @@ public abstract class Slot implements Serializable { obj.put("key", Hex.encode(_encryptedMasterKey)); return obj; } catch (JSONException e) { - throw new SlotException(e); + throw new RuntimeException(e); } } diff --git a/app/src/main/java/me/impy/aegis/db/slots/SlotCollection.java b/app/src/main/java/me/impy/aegis/db/slots/SlotCollection.java index e9ae476e..787f8666 100644 --- a/app/src/main/java/me/impy/aegis/db/slots/SlotCollection.java +++ b/app/src/main/java/me/impy/aegis/db/slots/SlotCollection.java @@ -19,7 +19,7 @@ public class SlotCollection implements Iterable, Serializable { private List _slots = new ArrayList<>(); private byte[] _masterHash; - public static JSONObject serialize(SlotCollection slots) throws SlotCollectionException { + public static JSONObject serialize(SlotCollection slots) { try { JSONObject obj = new JSONObject(); obj.put("hash", Hex.encode(slots.getMasterHash())); @@ -31,8 +31,8 @@ public class SlotCollection implements Iterable, Serializable { obj.put("entries", entries); return obj; - } catch (SlotException | JSONException e) { - throw new SlotCollectionException(e); + } catch (JSONException e) { + throw new RuntimeException(e); } } diff --git a/app/src/main/java/me/impy/aegis/encoding/Base32.java b/app/src/main/java/me/impy/aegis/encoding/Base32.java index a217701d..c17f78e3 100644 --- a/app/src/main/java/me/impy/aegis/encoding/Base32.java +++ b/app/src/main/java/me/impy/aegis/encoding/Base32.java @@ -24,8 +24,6 @@ package me.impy.aegis.encoding; import java.util.Arrays; -import me.impy.aegis.crypto.CryptoUtils; - /** * Base32 - encodes and decodes RFC3548 Base32 * (see http://www.faqs.org/rfcs/rfc3548.html ) diff --git a/app/src/main/java/me/impy/aegis/helpers/EditTextHelper.java b/app/src/main/java/me/impy/aegis/helpers/EditTextHelper.java index dd5baced..c570d804 100644 --- a/app/src/main/java/me/impy/aegis/helpers/EditTextHelper.java +++ b/app/src/main/java/me/impy/aegis/helpers/EditTextHelper.java @@ -5,8 +5,6 @@ import android.widget.EditText; import java.util.Arrays; -import me.impy.aegis.crypto.CryptoUtils; - public class EditTextHelper { private EditTextHelper() { } diff --git a/app/src/main/java/me/impy/aegis/helpers/UIRefresher.java b/app/src/main/java/me/impy/aegis/helpers/UiRefresher.java similarity index 92% rename from app/src/main/java/me/impy/aegis/helpers/UIRefresher.java rename to app/src/main/java/me/impy/aegis/helpers/UiRefresher.java index 4ac16a7c..dc24bf64 100644 --- a/app/src/main/java/me/impy/aegis/helpers/UIRefresher.java +++ b/app/src/main/java/me/impy/aegis/helpers/UiRefresher.java @@ -2,12 +2,12 @@ package me.impy.aegis.helpers; import android.os.Handler; -public class UIRefresher { +public class UiRefresher { private boolean _running; private Listener _listener; private Handler _handler; - public UIRefresher(Listener listener) { + public UiRefresher(Listener listener) { _listener = listener; _handler = new Handler(); } 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 e3f79727..a5c01ff9 100644 --- a/app/src/main/java/me/impy/aegis/importers/AegisImporter.java +++ b/app/src/main/java/me/impy/aegis/importers/AegisImporter.java @@ -43,7 +43,7 @@ public class AegisImporter extends DatabaseImporter { Database db = new Database(); db.deserialize(obj); - return db.getKeys(); + return db.getEntries(); } catch (DatabaseException | DatabaseFileException e) { throw new DatabaseImporterException(e); } diff --git a/app/src/main/java/me/impy/aegis/importers/AndOTPImporter.java b/app/src/main/java/me/impy/aegis/importers/AndOtpImporter.java similarity index 57% rename from app/src/main/java/me/impy/aegis/importers/AndOTPImporter.java rename to app/src/main/java/me/impy/aegis/importers/AndOtpImporter.java index e94bf608..d796dc4b 100644 --- a/app/src/main/java/me/impy/aegis/importers/AndOTPImporter.java +++ b/app/src/main/java/me/impy/aegis/importers/AndOtpImporter.java @@ -8,17 +8,19 @@ import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; -import me.impy.aegis.crypto.KeyInfo; -import me.impy.aegis.crypto.KeyInfoException; import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.encoding.Base32; import me.impy.aegis.encoding.Base32Exception; +import me.impy.aegis.otp.HotpInfo; +import me.impy.aegis.otp.OtpInfo; +import me.impy.aegis.otp.OtpInfoException; +import me.impy.aegis.otp.TotpInfo; import me.impy.aegis.util.ByteInputStream; -public class AndOTPImporter extends DatabaseImporter { +public class AndOtpImporter extends DatabaseImporter { private JSONArray _obj; - public AndOTPImporter(ByteInputStream stream) { + public AndOtpImporter(ByteInputStream stream) { super(stream); } @@ -41,30 +43,35 @@ public class AndOTPImporter extends DatabaseImporter { for (int i = 0; i < _obj.length(); i++) { JSONObject obj = _obj.getJSONObject(i); - KeyInfo key = new KeyInfo(); - key.setAlgorithm(obj.getString("algorithm")); - key.setDigits(obj.getInt("digits")); - key.setPeriod(obj.getInt("period")); - key.setType(obj.getString("type")); - if (key.getType().equals("hotp")) { - key.setCounter(obj.getLong("counter")); + String type = obj.getString("type"); + String algo = obj.getString("algorithm"); + int digits = obj.getInt("digits"); + byte[] secret = Base32.decode(obj.getString("secret").toCharArray()); + + OtpInfo info; + if (type.equals("totp")) { + info = new TotpInfo(secret, algo, digits, obj.getInt("period")); + } else if (type.equals("hotp")) { + info = new HotpInfo(secret, algo, digits, obj.getLong("counter")); + } else { + throw new DatabaseImporterException("unsupported otp type: " + type); } + String issuer = ""; + String name = ""; + String[] parts = obj.getString("label").split(" - "); if (parts.length > 1) { - key.setIssuer(parts[0]); - key.setAccountName(parts[1]); + issuer = parts[0]; + name = parts[1]; } else { - key.setAccountName(parts[0]); + name = parts[0]; } - byte[] secret = Base32.decode(obj.getString("secret").toCharArray()); - key.setSecret(secret); - - DatabaseEntry entry = new DatabaseEntry(key); + DatabaseEntry entry = new DatabaseEntry(info, name, issuer); entries.add(entry); } - } catch (Base32Exception | KeyInfoException | JSONException e) { + } catch (Base32Exception | OtpInfoException | JSONException e) { throw new DatabaseImporterException(e); } 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 286bcdd4..d4827baf 100644 --- a/app/src/main/java/me/impy/aegis/importers/DatabaseImporter.java +++ b/app/src/main/java/me/impy/aegis/importers/DatabaseImporter.java @@ -16,8 +16,8 @@ public abstract class DatabaseImporter { // note: keep this list sorted alphabetically LinkedHashMap> importers = new LinkedHashMap<>(); importers.put("Aegis", AegisImporter.class); - importers.put("andOTP", AndOTPImporter.class); - importers.put("FreeOTP", FreeOTPImporter.class); + importers.put("andOTP", AndOtpImporter.class); + importers.put("FreeOTP", FreeOtpImporter.class); _importers = Collections.unmodifiableMap(importers); } diff --git a/app/src/main/java/me/impy/aegis/importers/DatabaseImporterException.java b/app/src/main/java/me/impy/aegis/importers/DatabaseImporterException.java index 1dfd4116..d7411d1d 100644 --- a/app/src/main/java/me/impy/aegis/importers/DatabaseImporterException.java +++ b/app/src/main/java/me/impy/aegis/importers/DatabaseImporterException.java @@ -4,4 +4,8 @@ public class DatabaseImporterException extends Exception { public DatabaseImporterException(Throwable cause) { super(cause); } + + public DatabaseImporterException(String message) { + super(message); + } } diff --git a/app/src/main/java/me/impy/aegis/importers/FreeOTPImporter.java b/app/src/main/java/me/impy/aegis/importers/FreeOtpImporter.java similarity index 55% rename from app/src/main/java/me/impy/aegis/importers/FreeOTPImporter.java rename to app/src/main/java/me/impy/aegis/importers/FreeOtpImporter.java index 4624f946..2a57b92c 100644 --- a/app/src/main/java/me/impy/aegis/importers/FreeOTPImporter.java +++ b/app/src/main/java/me/impy/aegis/importers/FreeOtpImporter.java @@ -12,47 +12,86 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; -import me.impy.aegis.crypto.KeyInfo; -import me.impy.aegis.crypto.KeyInfoException; import me.impy.aegis.db.DatabaseEntry; +import me.impy.aegis.otp.HotpInfo; +import me.impy.aegis.otp.OtpInfo; +import me.impy.aegis.otp.OtpInfoException; +import me.impy.aegis.otp.TotpInfo; import me.impy.aegis.util.ByteInputStream; -public class FreeOTPImporter extends DatabaseImporter { - public FreeOTPImporter(ByteInputStream stream) { +public class FreeOtpImporter extends DatabaseImporter { + private List _xmlEntries; + + public FreeOtpImporter(ByteInputStream stream) { super(stream); } - private static class Entry { + private static class XmlEntry { String Name; String Value; } @Override public void parse() throws DatabaseImporterException { - - } - - @Override - public List convert() throws DatabaseImporterException { try { XmlPullParser parser = Xml.newPullParser(); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); parser.setInput(_stream, null); parser.nextTag(); - return parse(parser); - } catch (KeyInfoException | XmlPullParserException | JSONException | IOException e) { + _xmlEntries = parse(parser); + } catch (XmlPullParserException | IOException e) { throw new DatabaseImporterException(e); } } + @Override + public List convert() throws DatabaseImporterException { + List entries = new ArrayList<>(); + + try { + for (XmlEntry xmlEntry : _xmlEntries) { + if (xmlEntry.Name.equals("tokenOrder")) { + // TODO: order + JSONArray array = new JSONArray(xmlEntry.Value); + } else { + JSONObject obj = new JSONObject(xmlEntry.Value); + + String type = obj.getString("type"); + String algo = obj.getString("algo"); + int digits = obj.getInt("digits"); + byte[] secret = toBytes(obj.getJSONArray("secret")); + + OtpInfo info; + if (type.equals("totp")) { + info = new TotpInfo(secret, algo, digits, obj.getInt("period")); + } else if (type.equals("hotp")) { + info = new HotpInfo(secret, algo, digits, obj.getLong("counter")); + } else { + throw new DatabaseImporterException("unsupported otp type: " + type); + } + + String issuer = obj.getString("issuerExt"); + String name = obj.optString("label"); + + DatabaseEntry entry = new DatabaseEntry(info, name, issuer); + entries.add(entry); + } + } + } catch (OtpInfoException | JSONException e) { + throw new DatabaseImporterException(e); + } + + return entries; + } + @Override public boolean isEncrypted() { return false; } - private static List parse(XmlPullParser parser) - throws IOException, XmlPullParserException, JSONException, KeyInfoException { - List entries = new ArrayList<>(); + private static List parse(XmlPullParser parser) + throws IOException, XmlPullParserException { + List entries = new ArrayList<>(); parser.require(XmlPullParser.START_TAG, null, "map"); while (parser.next() != XmlPullParser.END_TAG) { @@ -68,32 +107,7 @@ public class FreeOTPImporter extends DatabaseImporter { entries.add(parseEntry(parser)); } - List profiles = new ArrayList<>(); - - for (Entry entry : entries) { - if (entry.Name.equals("tokenOrder")) { - // TODO: order - JSONArray array = new JSONArray(entry.Value); - } else { - JSONObject obj = new JSONObject(entry.Value); - - KeyInfo key = new KeyInfo(); - key.setAlgorithm(obj.getString("algo")); - key.setCounter(obj.getLong("counter")); - key.setDigits(obj.getInt("digits")); - key.setIssuer(obj.getString("issuerExt")); - key.setAccountName(obj.optString("label")); - key.setPeriod(obj.getInt("period")); - key.setType(obj.getString("type")); - byte[] secret = toBytes(obj.getJSONArray("secret")); - key.setSecret(secret); - - DatabaseEntry profile = new DatabaseEntry(key); - profiles.add(profile); - } - } - - return profiles; + return entries; } private static byte[] toBytes(JSONArray array) throws JSONException { @@ -104,12 +118,12 @@ public class FreeOTPImporter extends DatabaseImporter { return bytes; } - private static Entry parseEntry(XmlPullParser parser) throws IOException, XmlPullParserException { + private static XmlEntry parseEntry(XmlPullParser parser) throws IOException, XmlPullParserException { parser.require(XmlPullParser.START_TAG, null, "string"); String name = parser.getAttributeValue(null, "name"); String value = parseText(parser); parser.require(XmlPullParser.END_TAG, null, "string"); - return new Entry() {{ Name = name; Value = value; }}; + return new XmlEntry() {{ Name = name; Value = value; }}; } private static String parseText(XmlPullParser parser) throws IOException, XmlPullParserException { diff --git a/app/src/main/java/me/impy/aegis/otp/GoogleAuthInfo.java b/app/src/main/java/me/impy/aegis/otp/GoogleAuthInfo.java new file mode 100644 index 00000000..478833c4 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/otp/GoogleAuthInfo.java @@ -0,0 +1,149 @@ +package me.impy.aegis.otp; + +import android.net.Uri; + +import me.impy.aegis.encoding.Base32; +import me.impy.aegis.encoding.Base32Exception; + +public class GoogleAuthInfo { + private OtpInfo _info; + private String _accountName; + private String _issuer; + + public GoogleAuthInfo(OtpInfo info, String accountName, String issuer) { + _info = info; + _accountName = accountName; + _issuer = issuer; + } + + public OtpInfo getOtpInfo() { + return _info; + } + + public Uri getUri() { + Uri.Builder builder = new Uri.Builder(); + builder.scheme("otpauth"); + + if (_info instanceof TotpInfo) { + builder.authority("totp"); + builder.appendQueryParameter("period", Integer.toString(((TotpInfo)_info).getPeriod())); + } else if (_info instanceof HotpInfo) { + builder.authority("hotp"); + builder.appendQueryParameter("counter", Long.toString(((HotpInfo)_info).getCounter())); + } else { + throw new RuntimeException(); + } + + builder.appendQueryParameter("digits", Integer.toString(_info.getDigits())); + builder.appendQueryParameter("algorithm", _info.getAlgorithm(false)); + builder.appendQueryParameter("secret", new String(Base32.encode(_info.getSecret()))); + + if (_issuer != null && !_issuer.equals("")) { + builder.path(String.format("%s:%s", _issuer, _accountName)); + builder.appendQueryParameter("issuer", _issuer); + } else { + builder.path(_accountName); + } + + return builder.build(); + } + + public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException { + if (!uri.getScheme().equals("otpauth")) { + throw new GoogleAuthInfoException("unsupported protocol"); + } + + // 'secret' is a required parameter + String encodedSecret = uri.getQueryParameter("secret"); + if (encodedSecret == null) { + throw new GoogleAuthInfoException("'secret' is not set"); + } + + // decode secret + byte[] secret; + try { + secret = Base32.decode(encodedSecret.toCharArray()); + } catch (Base32Exception e) { + throw new GoogleAuthInfoException("bad secret", e); + } + + // check the otp type + OtpInfo info; + try { + String type = uri.getHost(); + switch (type) { + case "totp": + TotpInfo totpInfo = new TotpInfo(secret); + String period = uri.getQueryParameter("period"); + if (period != null) { + totpInfo.setPeriod(Integer.parseInt(period)); + } + info = totpInfo; + break; + case "hotp": + HotpInfo hotpInfo = new HotpInfo(secret); + String counter = uri.getQueryParameter("counter"); + if (counter == null) { + throw new GoogleAuthInfoException("'counter' was not set"); + } + hotpInfo.setCounter(Long.parseLong(counter)); + info = hotpInfo; + break; + default: + throw new GoogleAuthInfoException(String.format("unsupported otp type: %s", type)); + } + } catch (OtpInfoException e) { + throw new GoogleAuthInfoException(e); + } + + // provider info used to disambiguate accounts + String path = uri.getPath(); + String label = path != null && path.length() > 0 ? path.substring(1) : ""; + + String accountName = ""; + String issuer = ""; + + if (label.contains(":")) { + // a label can only contain one colon + // it's ok to fail if that's not the case + String[] strings = label.split(":"); + if (strings.length == 2) { + issuer = strings[0]; + accountName = strings[1]; + } else { + // at this point, just dump the whole thing into the accountName + accountName = label; + } + } else { + // label only contains the account name + // grab the issuer's info from the 'issuer' parameter if it's present + String issuerParam = uri.getQueryParameter("issuer"); + issuer = issuerParam != null ? issuerParam : ""; + accountName = label; + } + + // just use the defaults if these parameters aren't set + try { + String algorithm = uri.getQueryParameter("algorithm"); + if (algorithm != null) { + info.setAlgorithm(algorithm); + } + String digits = uri.getQueryParameter("digits"); + if (digits != null) { + info.setDigits(Integer.parseInt(digits)); + } + } catch (OtpInfoException e) { + throw new GoogleAuthInfoException(e); + } + + return new GoogleAuthInfo(info, accountName, issuer); + } + + public String getIssuer() { + return _issuer; + } + + public String getAccountName() { + return _accountName; + } +} diff --git a/app/src/main/java/me/impy/aegis/crypto/KeyInfoException.java b/app/src/main/java/me/impy/aegis/otp/GoogleAuthInfoException.java similarity index 52% rename from app/src/main/java/me/impy/aegis/crypto/KeyInfoException.java rename to app/src/main/java/me/impy/aegis/otp/GoogleAuthInfoException.java index 13231b43..1984164b 100644 --- a/app/src/main/java/me/impy/aegis/crypto/KeyInfoException.java +++ b/app/src/main/java/me/impy/aegis/otp/GoogleAuthInfoException.java @@ -1,11 +1,15 @@ -package me.impy.aegis.crypto; +package me.impy.aegis.otp; -public class KeyInfoException extends Exception { - public KeyInfoException(String message) { +public class GoogleAuthInfoException extends Exception { + public GoogleAuthInfoException(Throwable cause) { + super(cause); + } + + public GoogleAuthInfoException(String message) { super(message); } - public KeyInfoException(String message, Throwable cause) { + public GoogleAuthInfoException(String message, Throwable cause) { super(message, cause); } diff --git a/app/src/main/java/me/impy/aegis/otp/HotpInfo.java b/app/src/main/java/me/impy/aegis/otp/HotpInfo.java new file mode 100644 index 00000000..de465c96 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/otp/HotpInfo.java @@ -0,0 +1,63 @@ +package me.impy.aegis.otp; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import me.impy.aegis.crypto.otp.HOTP; + +public class HotpInfo extends OtpInfo { + private long _counter; + + public HotpInfo(byte[] secret) throws OtpInfoException { + super(secret); + setCounter(0); + } + + public HotpInfo(byte[] secret, String algorithm, int digits, long counter) throws OtpInfoException { + super(secret, algorithm, digits); + setCounter(counter); + } + + @Override + public String getOtp() { + try { + return HOTP.generateOTP(getSecret(), getCounter(), getDigits(), false, -1); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new RuntimeException(); + } + } + + @Override + public String getType() { + return "hotp"; + } + + @Override + public JSONObject toJson() { + JSONObject obj = super.toJson(); + try { + obj.put("counter", getCounter()); + } catch (JSONException e) { + throw new RuntimeException(e); + } + return obj; + } + + public long getCounter() { + return _counter; + } + + public static boolean isCounterValid(long counter) { + return counter >= 0; + } + + public void setCounter(long counter) throws OtpInfoException { + if (!isCounterValid(counter)) { + throw new OtpInfoException(String.format("bad counter: %d", counter)); + } + _counter = counter; + } +} diff --git a/app/src/main/java/me/impy/aegis/otp/OtpInfo.java b/app/src/main/java/me/impy/aegis/otp/OtpInfo.java new file mode 100644 index 00000000..03d36dcd --- /dev/null +++ b/app/src/main/java/me/impy/aegis/otp/OtpInfo.java @@ -0,0 +1,117 @@ +package me.impy.aegis.otp; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.Serializable; + +import me.impy.aegis.encoding.Base32; +import me.impy.aegis.encoding.Base32Exception; + +public abstract class OtpInfo implements Serializable { + private byte[] _secret; + private String _algorithm; + private int _digits; + + public OtpInfo(byte[] secret) throws OtpInfoException { + this(secret, "SHA1", 6); + } + + public OtpInfo(byte[] secret, String algorithm, int digits) throws OtpInfoException { + setSecret(secret); + setAlgorithm(algorithm); + setDigits(digits); + } + + public abstract String getOtp(); + + public abstract String getType(); + + public JSONObject toJson() { + JSONObject obj = new JSONObject(); + + try { + obj.put("secret", new String(Base32.encode(getSecret()))); + obj.put("algo", getAlgorithm(false)); + obj.put("digits", getDigits()); + } catch (JSONException e) { + throw new RuntimeException(e); + } + + return obj; + } + + public byte[] getSecret() { + return _secret; + } + + public String getAlgorithm(boolean java) { + if (java) { + return "Hmac" + _algorithm; + } + return _algorithm; + } + + public int getDigits() { + return _digits; + } + + public void setSecret(byte[] secret) { + _secret = secret; + } + + public static boolean isAlgorithmValid(String algorithm) { + return algorithm.equals("SHA1") || algorithm.equals("SHA256") || algorithm.equals("SHA512"); + } + + public void setAlgorithm(String algorithm) throws OtpInfoException { + if (algorithm.startsWith("Hmac")) { + algorithm = algorithm.substring(4); + } + algorithm = algorithm.toUpperCase(); + + if (!isAlgorithmValid(algorithm)) { + throw new OtpInfoException(String.format("unsupported algorithm: %s", algorithm)); + } + _algorithm = algorithm; + } + + public static boolean isDigitsValid(int digits) { + return digits == 6 || digits == 8; + } + + public void setDigits(int digits) throws OtpInfoException { + if (!isDigitsValid(digits)) { + throw new OtpInfoException(String.format("unsupported amount of digits: %d", digits)); + } + _digits = digits; + } + + public static OtpInfo parseJson(String type, JSONObject obj) throws OtpInfoException { + OtpInfo info; + + try { + byte[] secret = Base32.decode(obj.getString("secret").toCharArray()); + String algo = obj.getString("algo"); + int digits = obj.getInt("digits"); + + + switch (type) { + case "totp": + int period = obj.getInt("period"); + info = new TotpInfo(secret, algo, digits, period); + break; + case "hotp": + long counter = obj.getLong("counter"); + info = new HotpInfo(secret, algo, digits, counter); + break; + default: + throw new OtpInfoException("unsupported otp type: " + type); + } + } catch (Base32Exception | JSONException e) { + throw new OtpInfoException(e); + } + + return info; + } +} diff --git a/app/src/main/java/me/impy/aegis/otp/OtpInfoException.java b/app/src/main/java/me/impy/aegis/otp/OtpInfoException.java new file mode 100644 index 00000000..37c25510 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/otp/OtpInfoException.java @@ -0,0 +1,11 @@ +package me.impy.aegis.otp; + +public class OtpInfoException extends Exception { + public OtpInfoException(Throwable cause) { + super(cause); + } + + public OtpInfoException(String message) { + super(message); + } +} diff --git a/app/src/main/java/me/impy/aegis/otp/TotpInfo.java b/app/src/main/java/me/impy/aegis/otp/TotpInfo.java new file mode 100644 index 00000000..e1a72f72 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/otp/TotpInfo.java @@ -0,0 +1,66 @@ +package me.impy.aegis.otp; + +import org.json.JSONException; +import org.json.JSONObject; + +import me.impy.aegis.crypto.otp.TOTP; + +public class TotpInfo extends OtpInfo { + private int _period; + + public TotpInfo(byte[] secret) throws OtpInfoException { + super(secret); + setPeriod(30); + } + + public TotpInfo(byte[] secret, String algorithm, int digits, int period) throws OtpInfoException { + super(secret, algorithm, digits); + setPeriod(period); + } + + @Override + public String getOtp() { + String time = Long.toHexString(System.currentTimeMillis() / 1000 / getPeriod()); + return TOTP.generateTOTP(getSecret(), time, getDigits(), getAlgorithm(true)); + } + + @Override + public String getType() { + return "totp"; + } + + @Override + public JSONObject toJson() { + JSONObject obj = super.toJson(); + try { + obj.put("period", getPeriod()); + } catch (JSONException e) { + throw new RuntimeException(e); + } + return obj; + } + + public int getPeriod() { + return _period; + } + + public static boolean isPeriodValid(int period) { + return period > 0; + } + + public void setPeriod(int period) throws OtpInfoException { + if (!isPeriodValid(period)) { + throw new OtpInfoException(String.format("bad period: %d", period)); + } + _period = period; + } + + public long getMillisTillNextRotation() { + return TotpInfo.getMillisTillNextRotation(_period); + } + + public static long getMillisTillNextRotation(int period) { + long p = period * 1000; + return p - (System.currentTimeMillis() % p); + } +} diff --git a/app/src/main/java/me/impy/aegis/ui/EditProfileActivity.java b/app/src/main/java/me/impy/aegis/ui/EditEntryActivity.java similarity index 65% rename from app/src/main/java/me/impy/aegis/ui/EditProfileActivity.java rename to app/src/main/java/me/impy/aegis/ui/EditEntryActivity.java index 6430dde8..d8b7a325 100644 --- a/app/src/main/java/me/impy/aegis/ui/EditProfileActivity.java +++ b/app/src/main/java/me/impy/aegis/ui/EditEntryActivity.java @@ -19,32 +19,39 @@ import android.widget.EditText; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.Spinner; +import android.widget.TableRow; import com.amulyakhare.textdrawable.TextDrawable; import me.impy.aegis.R; -import me.impy.aegis.crypto.KeyInfo; -import me.impy.aegis.crypto.KeyInfoException; import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.encoding.Base32; +import me.impy.aegis.encoding.Base32Exception; import me.impy.aegis.helpers.EditTextHelper; import me.impy.aegis.helpers.SpinnerHelper; import me.impy.aegis.helpers.TextDrawableHelper; +import me.impy.aegis.otp.HotpInfo; +import me.impy.aegis.otp.OtpInfo; +import me.impy.aegis.otp.OtpInfoException; +import me.impy.aegis.otp.TotpInfo; import me.impy.aegis.ui.dialogs.Dialogs; -import me.impy.aegis.ui.views.KeyProfile; -public class EditProfileActivity extends AegisActivity { +public class EditEntryActivity extends AegisActivity { private boolean _isNew = false; private boolean _edited = false; - private KeyProfile _profile; + private DatabaseEntry _entry; private ImageView _iconView; private EditText _textName; private EditText _textIssuer; private EditText _textPeriod; + private EditText _textCounter; private EditText _textSecret; + private TableRow _rowPeriod; + private TableRow _rowCounter; + private Spinner _spinnerType; private Spinner _spinnerAlgo; private Spinner _spinnerDigits; @@ -56,27 +63,28 @@ public class EditProfileActivity extends AegisActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_edit_profile); + setContentView(R.layout.activity_edit_entry); ActionBar bar = getSupportActionBar(); bar.setHomeAsUpIndicator(R.drawable.ic_close); bar.setDisplayHomeAsUpEnabled(true); - // if the intent doesn't contain a KeyProfile, create a new one + // retrieve info from the calling activity Intent intent = getIntent(); - _profile = (KeyProfile) intent.getSerializableExtra("KeyProfile"); + _entry = (DatabaseEntry) intent.getSerializableExtra("entry"); _isNew = intent.getBooleanExtra("isNew", false); - if (_profile == null) { - _profile = new KeyProfile(); - } if (_isNew) { setTitle("Add profile"); } + // set up fields _iconView = findViewById(R.id.profile_drawable); _textName = findViewById(R.id.text_name); _textIssuer = findViewById(R.id.text_issuer); _textPeriod = findViewById(R.id.text_period); + _rowPeriod = findViewById(R.id.row_period); + _textCounter = findViewById(R.id.text_counter); + _rowCounter = findViewById(R.id.row_counter); _textSecret = findViewById(R.id.text_secret); _spinnerType = findViewById(R.id.spinner_type); SpinnerHelper.fillSpinner(this, _spinnerType, R.array.otp_types_array); @@ -84,18 +92,50 @@ public class EditProfileActivity extends AegisActivity { SpinnerHelper.fillSpinner(this, _spinnerAlgo, R.array.otp_algo_array); _spinnerDigits = findViewById(R.id.spinner_digits); SpinnerHelper.fillSpinner(this, _spinnerDigits, R.array.otp_digits_array); - _advancedSettingsHeader = findViewById(R.id.accordian_header); _advancedSettings = findViewById(R.id.expandableLayout); - updateFields(); + // fill the fields with values if possible + if (_entry != null) { + TextDrawable drawable = TextDrawableHelper.generate(_entry.getName()); + _iconView.setImageDrawable(drawable); + _textName.setText(_entry.getName()); + _textIssuer.setText(_entry.getIssuer()); + + OtpInfo info = _entry.getInfo(); + if (info instanceof TotpInfo) { + _textPeriod.setText(Integer.toString(((TotpInfo) info).getPeriod())); + _rowPeriod.setVisibility(View.VISIBLE); + } else if (info instanceof HotpInfo) { + _textCounter.setText(Long.toString(((HotpInfo) info).getCounter())); + _rowCounter.setVisibility(View.VISIBLE); + } else { + throw new RuntimeException(); + } + + byte[] secretBytes = _entry.getInfo().getSecret(); + if (secretBytes != null) { + char[] secretChars = Base32.encode(secretBytes); + _textSecret.setText(secretChars, 0, secretChars.length); + } + + String type = _entry.getInfo().getType(); + _spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type.toUpperCase()), false); + + String algo = _entry.getInfo().getAlgorithm(false); + _spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false); + + String digits = Integer.toString(_entry.getInfo().getDigits()); + _spinnerDigits.setSelection(getStringResourceIndex(R.array.otp_digits_array, digits), false); + } + + // listen for changes to any of the fields _textName.addTextChangedListener(_textListener); _textIssuer.addTextChangedListener(_textListener); _textPeriod.addTextChangedListener(_textListener); + _textCounter.addTextChangedListener(_textListener); _textSecret.addTextChangedListener(_textListener); - _spinnerType.setOnTouchListener(_selectedListener); - _spinnerType.setOnItemSelectedListener(_selectedListener); _spinnerAlgo.setOnTouchListener(_selectedListener); _spinnerAlgo.setOnItemSelectedListener(_selectedListener); _spinnerDigits.setOnTouchListener(_selectedListener); @@ -118,40 +158,44 @@ public class EditProfileActivity extends AegisActivity { } }); + // show/hide period and counter fields on type change + _spinnerType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + String type = _spinnerType.getSelectedItem().toString(); + + switch (type.toLowerCase()) { + case "totp": + _rowCounter.setVisibility(View.GONE); + _rowPeriod.setVisibility(View.VISIBLE); + break; + case "hotp": + _rowPeriod.setVisibility(View.GONE); + _rowCounter.setVisibility(View.VISIBLE); + break; + default: + throw new RuntimeException(); + } + + _selectedListener.onItemSelected(parent, view, position, id); + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + }); + _advancedSettingsHeader.setOnClickListener(v -> { openAdvancedSettings(); }); - // Automatically open advanced settings since 'Secret' is required. + // automatically open advanced settings since 'Secret' is required. if(_isNew){ openAdvancedSettings(); } } - private void updateFields() { - DatabaseEntry entry = _profile.getEntry(); - _iconView.setImageDrawable(_profile.getDrawable()); - - _textName.setText(entry.getName()); - _textIssuer.setText(entry.getInfo().getIssuer()); - _textPeriod.setText(Integer.toString(entry.getInfo().getPeriod())); - - byte[] secretBytes = entry.getInfo().getSecret(); - if (secretBytes != null) { - char[] secretChars = Base32.encode(secretBytes); - _textSecret.setText(secretChars, 0, secretChars.length); - } - - String type = entry.getInfo().getType(); - _spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type), false); - - String algo = entry.getInfo().getAlgorithm(false); - _spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false); - - String digits = Integer.toString(entry.getInfo().getDigits()); - _spinnerDigits.setSelection(getStringResourceIndex(R.array.otp_digits_array, digits), false); - } - @Override protected void setPreferredTheme(boolean darkMode) { if (darkMode) { @@ -252,7 +296,7 @@ public class EditProfileActivity extends AegisActivity { private void finish(boolean delete) { Intent intent = new Intent(); - intent.putExtra("KeyProfile", _profile); + intent.putExtra("entry", _entry); intent.putExtra("delete", delete); setResult(RESULT_OK, intent); finish(); @@ -264,14 +308,6 @@ public class EditProfileActivity extends AegisActivity { return false; } - int period; - try { - period = Integer.parseInt(_textPeriod.getText().toString()); - } catch (NumberFormatException e) { - onError("Period is not an integer."); - return false; - } - String type = _spinnerType.getSelectedItem().toString(); String algo = _spinnerAlgo.getSelectedItem().toString(); @@ -283,24 +319,60 @@ public class EditProfileActivity extends AegisActivity { return false; } - DatabaseEntry entry = _profile.getEntry(); - entry.setName(_textName.getText().toString()); - KeyInfo info = entry.getInfo(); - + byte[] secret; try { - char[] secret = EditTextHelper.getEditTextChars(_textSecret); - info.setSecret(secret); - info.setIssuer(_textIssuer.getText().toString()); - info.setPeriod(period); + secret = Base32.decode(EditTextHelper.getEditTextChars(_textSecret)); + } catch (Base32Exception e) { + onError("Secret is not valid base32."); + return false; + } + + // set otp info + OtpInfo info; + try { + switch (type.toLowerCase()) { + case "totp": + int period; + try { + period = Integer.parseInt(_textPeriod.getText().toString()); + } catch (NumberFormatException e) { + onError("Period is not an integer."); + return false; + } + info = new TotpInfo(secret, algo, digits, period); + break; + case "hotp": + long counter; + try { + counter = Long.parseLong(_textCounter.getText().toString()); + } catch (NumberFormatException e) { + onError("Counter is not an integer."); + return false; + } + info = new HotpInfo(secret, algo, digits, counter); + break; + default: + throw new RuntimeException(); + } + info.setDigits(digits); info.setAlgorithm(algo); - info.setType(type); - info.setAccountName(_textName.getText().toString()); - } catch (KeyInfoException e) { + } catch (OtpInfoException e) { onError("The entered info is incorrect: " + e.getMessage()); return false; } + // set database entry info + DatabaseEntry entry = _entry; + if (entry == null) { + entry = new DatabaseEntry(info); + } else { + entry.setInfo(info); + } + entry.setIssuer(_textIssuer.getText().toString()); + entry.setName(_textName.getText().toString()); + + _entry = entry; finish(false); return true; } 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 81d81645..7ebea295 100644 --- a/app/src/main/java/me/impy/aegis/ui/IntroActivity.java +++ b/app/src/main/java/me/impy/aegis/ui/IntroActivity.java @@ -18,7 +18,6 @@ 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.DatabaseException; import me.impy.aegis.db.DatabaseFileException; import me.impy.aegis.db.DatabaseManagerException; import me.impy.aegis.db.slots.FingerprintSlot; @@ -186,7 +185,7 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback { _databaseFile.setContent(obj, masterKey); } DatabaseManager.save(getApplicationContext(), _databaseFile); - } catch (DatabaseException | DatabaseManagerException | DatabaseFileException e) { + } catch (DatabaseManagerException | DatabaseFileException e) { setException(e); return; } 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 4c603dc0..f65a2ff9 100644 --- a/app/src/main/java/me/impy/aegis/ui/MainActivity.java +++ b/app/src/main/java/me/impy/aegis/ui/MainActivity.java @@ -26,15 +26,14 @@ import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.db.DatabaseManager; import me.impy.aegis.helpers.PermissionHelper; import me.impy.aegis.ui.dialogs.Dialogs; -import me.impy.aegis.ui.views.KeyProfile; -import me.impy.aegis.ui.views.KeyProfileView; +import me.impy.aegis.ui.views.EntryListView; -public class MainActivity extends AegisActivity implements KeyProfileView.Listener { +public class MainActivity extends AegisActivity implements EntryListView.Listener { // activity request codes - private static final int CODE_SCAN_KEYINFO = 0; - private static final int CODE_ADD_KEYINFO = 1; - private static final int CODE_EDIT_KEYINFO = 2; - private static final int CODE_ENTER_KEYINFO = 3; + private static final int CODE_SCAN = 0; + private static final int CODE_ADD_ENTRY = 1; + private static final int CODE_EDIT_ENTRY = 2; + private static final int CODE_ENTER_ENTRY = 3; private static final int CODE_DO_INTRO = 4; private static final int CODE_DECRYPT = 5; private static final int CODE_PREFERENCES = 6; @@ -44,7 +43,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen private AegisApplication _app; private DatabaseManager _db; - private KeyProfileView _keyProfileView; + private EntryListView _entryListView; private Menu _menu; private FloatingActionsMenu _fabMenu; @@ -58,20 +57,20 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen // set up the main view setContentView(R.layout.activity_main); - // set up the key profile view - _keyProfileView = (KeyProfileView) getSupportFragmentManager().findFragmentById(R.id.key_profiles); - _keyProfileView.setListener(this); - _keyProfileView.setShowIssuer(getPreferences().isIssuerVisible()); + // set up the entry view + _entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles); + _entryListView.setListener(this); + _entryListView.setShowIssuer(getPreferences().isIssuerVisible()); // set up the floating action button _fabMenu = findViewById(R.id.fab); findViewById(R.id.fab_enter).setOnClickListener(view -> { _fabMenu.collapse(); - onEnterKeyInfo(); + onEnterEntry(); }); findViewById(R.id.fab_scan).setOnClickListener(view -> { _fabMenu.collapse(); - onScanKeyInfo(); + onScan(); }); // skip this part if this is the not initial startup and the database has been unlocked @@ -101,9 +100,9 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen } } - // if the database has been decrypted at this point, we can load the key profiles + // if the database has been decrypted at this point, we can load the entries if (!_db.isLocked()) { - loadKeyProfiles(); + loadEntries(); } } @@ -141,17 +140,17 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen } switch (requestCode) { - case CODE_SCAN_KEYINFO: - onScanKeyInfoResult(resultCode, data); + case CODE_SCAN: + onScanResult(resultCode, data); break; - case CODE_ADD_KEYINFO: - onAddKeyInfoResult(resultCode, data); + case CODE_ADD_ENTRY: + onAddEntryResult(resultCode, data); break; - case CODE_EDIT_KEYINFO: - onEditKeyInfoResult(resultCode, data); + case CODE_EDIT_ENTRY: + onEditEntryResult(resultCode, data); break; - case CODE_ENTER_KEYINFO: - onEnterKeyInfoResult(resultCode, data); + case CODE_ENTER_ENTRY: + onEnterEntryResult(resultCode, data); break; case CODE_DO_INTRO: onDoIntroResult(resultCode, data); @@ -174,35 +173,35 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen switch (requestCode) { case CODE_PERM_CAMERA: - onScanKeyInfo(); + onScan(); break; } } private void onPreferencesResult(int resultCode, Intent data) { - // refresh the entire key profile list if needed + // refresh the entire entry list if needed if (data.getBooleanExtra("needsRecreate", false)) { recreate(); } else if (data.getBooleanExtra("needsRefresh", false)) { boolean showIssuer = getPreferences().isIssuerVisible(); - _keyProfileView.setShowIssuer(showIssuer); + _entryListView.setShowIssuer(showIssuer); } } - private void startEditProfileActivity(int requestCode, KeyProfile profile, boolean isNew) { - Intent intent = new Intent(this, EditProfileActivity.class); - if (profile != null) { - intent.putExtra("KeyProfile", profile); + private void startEditProfileActivity(int requestCode, DatabaseEntry entry, boolean isNew) { + Intent intent = new Intent(this, EditEntryActivity.class); + if (entry != null) { + intent.putExtra("entry", entry); } intent.putExtra("isNew", isNew); startActivityForResult(intent, requestCode); } - private void onEnterKeyInfo() { - startEditProfileActivity(CODE_ENTER_KEYINFO, null, true); + private void onEnterEntry() { + startEditProfileActivity(CODE_ENTER_ENTRY, null, true); } - private void onScanKeyInfo() { + private void onScan() { if (!PermissionHelper.request(this, CODE_PERM_CAMERA, Manifest.permission.CAMERA)) { return; } @@ -210,49 +209,47 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen startScanActivity(); } - private void onScanKeyInfoResult(int resultCode, Intent data) { + private void onScanResult(int resultCode, Intent data) { if (resultCode == RESULT_OK) { - KeyProfile profile = (KeyProfile)data.getSerializableExtra("KeyProfile"); - startEditProfileActivity(CODE_ADD_KEYINFO, profile, true); + DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry"); + startEditProfileActivity(CODE_ADD_ENTRY, entry, true); } } - private void onAddKeyInfoResult(int resultCode, Intent data) { + private void onAddEntryResult(int resultCode, Intent data) { if (resultCode == RESULT_OK) { - KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile"); - addKey(profile); + DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry"); + addEntry(entry); saveDatabase(); } } - private void onEditKeyInfoResult(int resultCode, Intent data) { + private void onEditEntryResult(int resultCode, Intent data) { if (resultCode == RESULT_OK) { - KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile"); + DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry"); if (!data.getBooleanExtra("delete", false)) { // this profile has been serialized/deserialized and is no longer the same instance it once was - // to deal with this, the replaceKey functions are used - _db.replaceKey(profile.getEntry()); - _keyProfileView.replaceKey(profile); + // to deal with this, the replaceEntry functions are used + _db.replaceEntry(entry); + _entryListView.replaceEntry(entry); saveDatabase(); } else { - deleteProfile(profile); + deleteEntry(entry); } } } - private void onEnterKeyInfoResult(int resultCode, Intent data) { + private void onEnterEntryResult(int resultCode, Intent data) { if (resultCode == RESULT_OK) { - KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile"); - addKey(profile); + DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry"); + addEntry(entry); saveDatabase(); } } - private void addKey(KeyProfile profile) { - DatabaseEntry entry = profile.getEntry(); - entry.setName(entry.getInfo().getAccountName()); - _db.addKey(entry); - _keyProfileView.addKey(profile); + private void addEntry(DatabaseEntry entry) { + _db.addEntry(entry); + _entryListView.addEntry(entry); } private void onDoIntroResult(int resultCode, Intent data) { @@ -275,7 +272,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen return; } - loadKeyProfiles(); + loadEntries(); } private void onDecryptResult(int resultCode, Intent intent) { @@ -289,13 +286,13 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen return; } - loadKeyProfiles(); + loadEntries(); doShortcutActions(); } private void startScanActivity() { Intent scannerActivity = new Intent(getApplicationContext(), ScannerActivity.class); - startActivityForResult(scannerActivity, CODE_SCAN_KEYINFO); + startActivityForResult(scannerActivity, CODE_SCAN); } private boolean doShortcutActions() { @@ -330,12 +327,12 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen updateLockIcon(); // refresh all codes to prevent showing old ones - _keyProfileView.refresh(); + _entryListView.refresh(); } - private BottomSheetDialog createBottomSheet(final KeyProfile profile) { + private BottomSheetDialog createBottomSheet(final DatabaseEntry entry) { BottomSheetDialog dialog = new BottomSheetDialog(this); - dialog.setContentView(R.layout.bottom_sheet_edit_profile); + dialog.setContentView(R.layout.bottom_sheet_edit_entry); dialog.setCancelable(true); dialog.getWindow().setLayout(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); @@ -344,7 +341,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen dialog.findViewById(R.id.copy_button).setOnClickListener(view -> { dialog.dismiss(); ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("text/plain", profile.getCode()); + ClipData clip = ClipData.newPlainText("text/plain", entry.getInfo().getOtp()); clipboard.setPrimaryClip(clip); Toast.makeText(this, "Code copied to the clipboard", Toast.LENGTH_SHORT).show(); }); @@ -352,23 +349,23 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen dialog.findViewById(R.id.delete_button).setOnClickListener(view -> { dialog.dismiss(); Dialogs.showDeleteEntryDialog(this, (d, which) -> { - deleteProfile(profile); + deleteEntry(entry); }); }); dialog.findViewById(R.id.edit_button).setOnClickListener(view -> { dialog.dismiss(); - startEditProfileActivity(CODE_EDIT_KEYINFO, profile, false); + startEditProfileActivity(CODE_EDIT_ENTRY, entry, false); }); return dialog; } - private void deleteProfile(KeyProfile profile) { - _db.removeKey(profile.getEntry()); + private void deleteEntry(DatabaseEntry entry) { + _db.removeEntry(entry); saveDatabase(); - _keyProfileView.removeKey(profile); + _entryListView.removeEntry(entry); } @Override @@ -387,7 +384,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen startActivityForResult(intent, CODE_PREFERENCES); return true; case R.id.action_lock: - _keyProfileView.clearKeys(); + _entryListView.clearEntries(); _db.lock(); startAuthActivity(); return true; @@ -411,11 +408,11 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen } } - private void loadKeyProfiles() { + private void loadEntries() { updateLockIcon(); - for (DatabaseEntry entry : _db.getKeys()) { - _keyProfileView.addKey(new KeyProfile(entry)); + for (DatabaseEntry entry : _db.getEntries()) { + _entryListView.addEntry(entry); } } @@ -428,13 +425,13 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen } @Override - public void onEntryClick(KeyProfile profile) { - createBottomSheet(profile).show(); + public void onEntryClick(DatabaseEntry entry) { + createBottomSheet(entry).show(); } @Override public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) { - _db.swapKeys(entry1, entry2); + _db.swapEntries(entry1, entry2); } @Override 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 8857ec50..a51f4609 100644 --- a/app/src/main/java/me/impy/aegis/ui/PreferencesFragment.java +++ b/app/src/main/java/me/impy/aegis/ui/PreferencesFragment.java @@ -313,7 +313,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas private void importDatabase(DatabaseImporter importer) throws DatabaseImporterException { List entries = importer.convert(); for (DatabaseEntry entry : entries) { - _db.addKey(entry); + _db.addEntry(entry); } if (!saveDatabase()) { 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 d0de6467..65f787cf 100644 --- a/app/src/main/java/me/impy/aegis/ui/ScannerActivity.java +++ b/app/src/main/java/me/impy/aegis/ui/ScannerActivity.java @@ -3,6 +3,7 @@ package me.impy.aegis.ui; import android.content.Context; import android.content.Intent; import android.hardware.Camera; +import android.net.Uri; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; @@ -16,11 +17,10 @@ import java.util.Collections; import me.dm7.barcodescanner.core.IViewFinder; import me.dm7.barcodescanner.zxing.ZXingScannerView; import me.impy.aegis.R; -import me.impy.aegis.crypto.KeyInfo; -import me.impy.aegis.crypto.KeyInfoException; import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.helpers.SquareFinderView; -import me.impy.aegis.ui.views.KeyProfile; +import me.impy.aegis.otp.GoogleAuthInfo; +import me.impy.aegis.otp.GoogleAuthInfoException; import static android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK; import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT; @@ -107,16 +107,19 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R @Override public void handleResult(Result rawResult) { try { - KeyInfo info = KeyInfo.fromURL(rawResult.getText()); - KeyProfile profile = new KeyProfile(new DatabaseEntry(info)); - profile.getEntry().setName(info.getAccountName()); + // parse google auth uri + Uri uri = Uri.parse(rawResult.getText()); + GoogleAuthInfo info = GoogleAuthInfo.parseUri(uri); - Intent resultIntent = new Intent(); - resultIntent.putExtra("KeyProfile", profile); + DatabaseEntry entry = new DatabaseEntry(info.getOtpInfo()); + entry.setIssuer(info.getIssuer()); + entry.setName(info.getAccountName()); - setResult(RESULT_OK, resultIntent); + Intent intent = new Intent(); + intent.putExtra("entry", entry); + setResult(RESULT_OK, intent); finish(); - } catch (KeyInfoException e) { + } catch (GoogleAuthInfoException e) { Toast.makeText(this, "An error occurred while trying to parse the QR code contents", Toast.LENGTH_SHORT).show(); } diff --git a/app/src/main/java/me/impy/aegis/ui/tasks/DerivationTask.java b/app/src/main/java/me/impy/aegis/ui/tasks/DerivationTask.java index 7b7600a7..744296d7 100644 --- a/app/src/main/java/me/impy/aegis/ui/tasks/DerivationTask.java +++ b/app/src/main/java/me/impy/aegis/ui/tasks/DerivationTask.java @@ -6,7 +6,6 @@ import javax.crypto.SecretKey; import me.impy.aegis.crypto.CryptoUtils; import me.impy.aegis.db.slots.PasswordSlot; -import me.impy.aegis.db.slots.SlotException; public class DerivationTask extends ProgressDialogTask { private Callback _cb; diff --git a/app/src/main/java/me/impy/aegis/ui/views/EntryAdapter.java b/app/src/main/java/me/impy/aegis/ui/views/EntryAdapter.java new file mode 100644 index 00000000..755e8a20 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/ui/views/EntryAdapter.java @@ -0,0 +1,167 @@ +package me.impy.aegis.ui.views; + +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import me.impy.aegis.R; +import me.impy.aegis.db.DatabaseEntry; +import me.impy.aegis.helpers.ItemTouchHelperAdapter; +import me.impy.aegis.otp.OtpInfo; +import me.impy.aegis.otp.TotpInfo; + +public class EntryAdapter extends RecyclerView.Adapter implements ItemTouchHelperAdapter { + private List _entries; + private static Listener _listener; + private boolean _showIssuer; + + public EntryAdapter(Listener listener) { + _entries = new ArrayList<>(); + _listener = listener; + } + + public void setShowIssuer(boolean showIssuer) { + _showIssuer = showIssuer; + } + + public void addEntry(DatabaseEntry entry) { + _entries.add(entry); + + int position = getItemCount() - 1; + if (position == 0) { + notifyDataSetChanged(); + } else { + notifyItemInserted(position); + } + } + + public void removeEntry(DatabaseEntry entry) { + entry = getEntryByUUID(entry.getUUID()); + int position = _entries.indexOf(entry); + _entries.remove(position); + notifyItemRemoved(position); + } + + public void clearEntries() { + _entries.clear(); + notifyDataSetChanged(); + } + + public void replaceEntry(DatabaseEntry newEntry) { + DatabaseEntry oldEntry = getEntryByUUID(newEntry.getUUID()); + int position = _entries.indexOf(oldEntry); + _entries.set(position, newEntry); + notifyItemChanged(position); + } + + 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"); + } + + @Override + public void onItemDismiss(int position) { + + } + + @Override + public void onItemDrop(int position) { + _listener.onEntryDrop(_entries.get(position)); + } + + @Override + public void onItemMove(int firstPosition, int secondPosition) { + // notify the database first + _listener.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition)); + + // update our side of things + Collections.swap(_entries, firstPosition, secondPosition); + notifyItemMoved(firstPosition, secondPosition); + } + + @Override + public EntryHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_entry, parent, false); + return new EntryHolder(view); + } + + @Override + public void onViewRecycled(EntryHolder holder) { + holder.stopRefreshLoop(); + super.onViewRecycled(holder); + } + + @Override + public void onBindViewHolder(final EntryHolder holder, int position) { + DatabaseEntry entry = _entries.get(position); + boolean showProgress = !isPeriodUniform() && entry.getInfo() instanceof TotpInfo; + holder.setData(entry, _showIssuer, showProgress); + if (showProgress) { + holder.startRefreshLoop(); + } + + holder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + int position = holder.getAdapterPosition(); + _listener.onEntryClick(_entries.get(position)); + } + }); + holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + int position = holder.getAdapterPosition(); + return _listener.onLongEntryClick(_entries.get(position)); + } + }); + } + + public int getUniformPeriod() { + List infos = new ArrayList<>(); + for (DatabaseEntry entry : _entries) { + OtpInfo info = entry.getInfo(); + if (info instanceof TotpInfo) { + infos.add((TotpInfo) info); + } + } + + if (infos.isEmpty()) { + return -1; + } + + int period = infos.get(0).getPeriod(); + for (TotpInfo info : infos) { + if (period != info.getPeriod()) { + return -1; + } + } + + return period; + } + + public boolean isPeriodUniform() { + return getUniformPeriod() != -1; + } + + @Override + public int getItemCount() { + return _entries.size(); + } + + public interface Listener { + void onEntryClick(DatabaseEntry entry); + boolean onLongEntryClick(DatabaseEntry entry); + void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2); + void onEntryDrop(DatabaseEntry entry); + } +} diff --git a/app/src/main/java/me/impy/aegis/ui/views/KeyProfileHolder.java b/app/src/main/java/me/impy/aegis/ui/views/EntryHolder.java similarity index 66% rename from app/src/main/java/me/impy/aegis/ui/views/KeyProfileHolder.java rename to app/src/main/java/me/impy/aegis/ui/views/EntryHolder.java index da19b193..8024833c 100644 --- a/app/src/main/java/me/impy/aegis/ui/views/KeyProfileHolder.java +++ b/app/src/main/java/me/impy/aegis/ui/views/EntryHolder.java @@ -9,20 +9,23 @@ import android.widget.TextView; import com.amulyakhare.textdrawable.TextDrawable; import me.impy.aegis.R; -import me.impy.aegis.helpers.UIRefresher; +import me.impy.aegis.db.DatabaseEntry; +import me.impy.aegis.helpers.TextDrawableHelper; +import me.impy.aegis.helpers.UiRefresher; +import me.impy.aegis.otp.TotpInfo; -public class KeyProfileHolder extends RecyclerView.ViewHolder { +public class EntryHolder extends RecyclerView.ViewHolder { private TextView _profileName; private TextView _profileCode; private TextView _profileIssuer; private ImageView _profileDrawable; - private KeyProfile _profile; + private DatabaseEntry _entry; private PeriodProgressBar _progressBar; - private UIRefresher _refresher; + private UiRefresher _refresher; - public KeyProfileHolder(final View view) { + public EntryHolder(final View view) { super(view); _profileName = view.findViewById(R.id.profile_name); _profileCode = view.findViewById(R.id.profile_code); @@ -33,7 +36,7 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder { int primaryColorId = view.getContext().getResources().getColor(R.color.colorPrimary); _progressBar.getProgressDrawable().setColorFilter(primaryColorId, PorterDuff.Mode.SRC_IN); - _refresher = new UIRefresher(new UIRefresher.Listener() { + _refresher = new UiRefresher(new UiRefresher.Listener() { @Override public void onRefresh() { refreshCode(); @@ -42,26 +45,26 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder { @Override public long getMillisTillNextRefresh() { - return _profile.getEntry().getInfo().getMillisTillNextRotation(); + return ((TotpInfo)_entry.getInfo()).getMillisTillNextRotation(); } }); } - public void setData(KeyProfile profile, boolean showIssuer, boolean showProgress) { - _profile = profile; + public void setData(DatabaseEntry entry, boolean showIssuer, boolean showProgress) { + _entry = entry; _progressBar.setVisibility(showProgress ? View.VISIBLE : View.INVISIBLE); if (showProgress) { - _progressBar.setPeriod(profile.getEntry().getInfo().getPeriod()); + _progressBar.setPeriod(((TotpInfo)entry.getInfo()).getPeriod()); } - _profileName.setText(profile.getEntry().getName()); + _profileName.setText(entry.getName()); _profileIssuer.setText(""); if (showIssuer) { - _profileIssuer.setText(" - " + profile.getEntry().getInfo().getIssuer()); + _profileIssuer.setText(" - " + entry.getIssuer()); } - TextDrawable drawable = profile.getDrawable(); + TextDrawable drawable = TextDrawableHelper.generate(entry.getName()); _profileDrawable.setImageDrawable(drawable); refreshCode(); @@ -76,7 +79,7 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder { } private void refreshCode() { - String otp = _profile.refreshCode(); + String otp = _entry.getInfo().getOtp(); _profileCode.setText(otp.substring(0, otp.length() / 2) + " " + otp.substring(otp.length() / 2)); } } diff --git a/app/src/main/java/me/impy/aegis/ui/views/KeyProfileView.java b/app/src/main/java/me/impy/aegis/ui/views/EntryListView.java similarity index 71% rename from app/src/main/java/me/impy/aegis/ui/views/KeyProfileView.java rename to app/src/main/java/me/impy/aegis/ui/views/EntryListView.java index 30ac7ff7..2531887e 100644 --- a/app/src/main/java/me/impy/aegis/ui/views/KeyProfileView.java +++ b/app/src/main/java/me/impy/aegis/ui/views/EntryListView.java @@ -11,29 +11,29 @@ import android.view.View; import android.view.ViewGroup; import me.impy.aegis.R; -import me.impy.aegis.crypto.KeyInfo; import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.helpers.SimpleItemTouchHelperCallback; -import me.impy.aegis.helpers.UIRefresher; +import me.impy.aegis.helpers.UiRefresher; +import me.impy.aegis.otp.TotpInfo; -public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listener { - private KeyProfileAdapter _adapter; +public class EntryListView extends Fragment implements EntryAdapter.Listener { + private EntryAdapter _adapter; private Listener _listener; private PeriodProgressBar _progressBar; private boolean _showProgress = false; - private UIRefresher _refresher; + private UiRefresher _refresher; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - _adapter = new KeyProfileAdapter(this); + _adapter = new EntryAdapter(this); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_keyprofile_view, container, false); + View view = inflater.inflate(R.layout.fragment_entry_list_view, container, false); _progressBar = view.findViewById(R.id.progressBar); int primaryColorId = getResources().getColor(R.color.colorPrimary); @@ -48,7 +48,7 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen touchHelper.attachToRecyclerView(rvKeyProfiles); rvKeyProfiles.setAdapter(_adapter); - _refresher = new UIRefresher(new UIRefresher.Listener() { + _refresher = new UiRefresher(new UiRefresher.Listener() { @Override public void onRefresh() { refresh(); @@ -56,7 +56,7 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen @Override public long getMillisTillNextRefresh() { - return KeyInfo.getMillisTillNextRotation(_adapter.getUniformPeriod()); + return TotpInfo.getMillisTillNextRotation(_adapter.getUniformPeriod()); } }); @@ -101,23 +101,23 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen } @Override - public void onKeyProfileClick(KeyProfile profile) { - _listener.onEntryClick(profile); + public void onEntryClick(DatabaseEntry entry) { + _listener.onEntryClick(entry); } @Override - public boolean onLongKeyProfileClick(KeyProfile profile) { + public boolean onLongEntryClick(DatabaseEntry entry) { return false; } @Override - public void onKeyProfileMove(KeyProfile profile1, KeyProfile profile2) { - _listener.onEntryMove(profile1.getEntry(), profile2.getEntry()); + public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) { + _listener.onEntryMove(entry1, entry2); } @Override - public void onKeyProfileDrop(KeyProfile profile) { - _listener.onEntryDrop(profile.getEntry()); + public void onEntryDrop(DatabaseEntry entry) { + _listener.onEntryDrop(entry); } public void setShowIssuer(boolean showIssuer) { @@ -125,28 +125,28 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen _adapter.notifyDataSetChanged(); } - public void addKey(KeyProfile profile) { - _adapter.addKey(profile); + public void addEntry(DatabaseEntry entry) { + _adapter.addEntry(entry); checkPeriodUniformity(); } - public void removeKey(KeyProfile profile) { - _adapter.removeKey(profile); + public void removeEntry(DatabaseEntry entry) { + _adapter.removeEntry(entry); checkPeriodUniformity(); } - public void clearKeys() { - _adapter.clearKeys(); + public void clearEntries() { + _adapter.clearEntries(); checkPeriodUniformity(); } - public void replaceKey(KeyProfile profile) { - _adapter.replaceKey(profile); + public void replaceEntry(DatabaseEntry entry) { + _adapter.replaceEntry(entry); checkPeriodUniformity(); } public interface Listener { - void onEntryClick(KeyProfile profile); + void onEntryClick(DatabaseEntry entry); void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2); void onEntryDrop(DatabaseEntry entry); } diff --git a/app/src/main/java/me/impy/aegis/ui/views/KeyProfile.java b/app/src/main/java/me/impy/aegis/ui/views/KeyProfile.java deleted file mode 100644 index addc14d6..00000000 --- a/app/src/main/java/me/impy/aegis/ui/views/KeyProfile.java +++ /dev/null @@ -1,44 +0,0 @@ -package me.impy.aegis.ui.views; - -import com.amulyakhare.textdrawable.TextDrawable; - -import java.io.Serializable; -import java.lang.reflect.UndeclaredThrowableException; - -import me.impy.aegis.crypto.otp.OTP; -import me.impy.aegis.crypto.otp.OTPException; -import me.impy.aegis.db.DatabaseEntry; -import me.impy.aegis.helpers.TextDrawableHelper; - -public class KeyProfile implements Serializable { - private String _code; - private DatabaseEntry _entry; - - public KeyProfile() { - this(new DatabaseEntry()); - } - - public KeyProfile(DatabaseEntry entry) { - _entry = entry; - } - - public DatabaseEntry getEntry() { - return _entry; - } - public String getCode() { - return _code; - } - - public String refreshCode() { - try { - _code = OTP.generateOTP(_entry.getInfo()); - } catch (OTPException e) { - throw new UndeclaredThrowableException(e); - } - return _code; - } - - public TextDrawable getDrawable() { - return TextDrawableHelper.generate(getEntry().getName()); - } -} diff --git a/app/src/main/java/me/impy/aegis/ui/views/KeyProfileAdapter.java b/app/src/main/java/me/impy/aegis/ui/views/KeyProfileAdapter.java deleted file mode 100644 index cfa18e7d..00000000 --- a/app/src/main/java/me/impy/aegis/ui/views/KeyProfileAdapter.java +++ /dev/null @@ -1,155 +0,0 @@ -package me.impy.aegis.ui.views; - -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.UUID; - -import me.impy.aegis.R; -import me.impy.aegis.helpers.ItemTouchHelperAdapter; - -public class KeyProfileAdapter extends RecyclerView.Adapter implements ItemTouchHelperAdapter { - private ArrayList _keyProfiles; - private static Listener _listener; - private boolean _showIssuer; - - public KeyProfileAdapter(Listener listener) { - _keyProfiles = new ArrayList<>(); - _listener = listener; - } - - public void setShowIssuer(boolean showIssuer) { - _showIssuer = showIssuer; - } - - public void addKey(KeyProfile profile) { - _keyProfiles.add(profile); - - int position = getItemCount() - 1; - if (position == 0) { - notifyDataSetChanged(); - } else { - notifyItemInserted(position); - } - } - - public void removeKey(KeyProfile profile) { - profile = getKeyByUUID(profile.getEntry().getUUID()); - int position = _keyProfiles.indexOf(profile); - _keyProfiles.remove(position); - notifyItemRemoved(position); - } - - public void clearKeys() { - _keyProfiles.clear(); - notifyDataSetChanged(); - } - - public void replaceKey(KeyProfile newProfile) { - KeyProfile oldProfile = getKeyByUUID(newProfile.getEntry().getUUID()); - int position = _keyProfiles.indexOf(oldProfile); - _keyProfiles.set(position, newProfile); - notifyItemChanged(position); - } - - private KeyProfile getKeyByUUID(UUID uuid) { - for (KeyProfile profile : _keyProfiles) { - if (profile.getEntry().getUUID().equals(uuid)) { - return profile; - } - } - throw new AssertionError("no key profile found with the same id"); - } - - @Override - public void onItemDismiss(int position) { - - } - - @Override - public void onItemDrop(int position) { - _listener.onKeyProfileDrop(_keyProfiles.get(position)); - } - - @Override - public void onItemMove(int firstPosition, int secondPosition) { - // notify the database first - _listener.onKeyProfileMove(_keyProfiles.get(firstPosition), _keyProfiles.get(secondPosition)); - - // update our side of things - Collections.swap(_keyProfiles, firstPosition, secondPosition); - notifyItemMoved(firstPosition, secondPosition); - } - - @Override - public KeyProfileHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_keyprofile, parent, false); - return new KeyProfileHolder(view); - } - - @Override - public void onViewRecycled(KeyProfileHolder holder) { - holder.stopRefreshLoop(); - super.onViewRecycled(holder); - } - - @Override - public void onBindViewHolder(final KeyProfileHolder holder, int position) { - boolean uniform = isPeriodUniform(); - final KeyProfile profile = _keyProfiles.get(position); - holder.setData(profile, _showIssuer, !uniform); - if (!uniform) { - holder.startRefreshLoop(); - } - - holder.itemView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - int position = holder.getAdapterPosition(); - _listener.onKeyProfileClick(_keyProfiles.get(position)); - } - }); - holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - int position = holder.getAdapterPosition(); - return _listener.onLongKeyProfileClick(_keyProfiles.get(position)); - } - }); - } - - public int getUniformPeriod() { - if (_keyProfiles.isEmpty()) { - return -1; - } - - int period = _keyProfiles.get(0).getEntry().getInfo().getPeriod(); - for (KeyProfile profile : _keyProfiles) { - if (period != profile.getEntry().getInfo().getPeriod()) { - return -1; - } - } - - return period; - } - - public boolean isPeriodUniform() { - return getUniformPeriod() != -1; - } - - @Override - public int getItemCount() { - return _keyProfiles.size(); - } - - public interface Listener { - void onKeyProfileClick(KeyProfile profile); - boolean onLongKeyProfileClick(KeyProfile profile); - void onKeyProfileMove(KeyProfile profile1, KeyProfile profile2); - void onKeyProfileDrop(KeyProfile profile); - } -} diff --git a/app/src/main/java/me/impy/aegis/ui/views/PeriodProgressBar.java b/app/src/main/java/me/impy/aegis/ui/views/PeriodProgressBar.java index c4152c2d..3552ba66 100644 --- a/app/src/main/java/me/impy/aegis/ui/views/PeriodProgressBar.java +++ b/app/src/main/java/me/impy/aegis/ui/views/PeriodProgressBar.java @@ -8,7 +8,7 @@ import android.util.AttributeSet; import android.view.animation.LinearInterpolator; import android.widget.ProgressBar; -import me.impy.aegis.crypto.KeyInfo; +import me.impy.aegis.otp.TotpInfo; public class PeriodProgressBar extends ProgressBar { private int _period; @@ -40,7 +40,7 @@ public class PeriodProgressBar extends ProgressBar { setProgress(maxProgress); // calculate the progress the bar should start at - long millisTillRotation = KeyInfo.getMillisTillNextRotation(_period); + long millisTillRotation = TotpInfo.getMillisTillNextRotation(_period); long period = _period * maxProgress; int currentProgress = maxProgress - (int) ((((double) period - millisTillRotation) / period) * maxProgress); diff --git a/app/src/main/res/drawable/ic_counter_black_24dp.xml b/app/src/main/res/drawable/ic_counter_black_24dp.xml new file mode 100644 index 00000000..740d85d9 --- /dev/null +++ b/app/src/main/res/drawable/ic_counter_black_24dp.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/layout/activity_edit_profile.xml b/app/src/main/res/layout/activity_edit_entry.xml similarity index 86% rename from app/src/main/res/layout/activity_edit_profile.xml rename to app/src/main/res/layout/activity_edit_entry.xml index 55078e00..54211460 100644 --- a/app/src/main/res/layout/activity_edit_profile.xml +++ b/app/src/main/res/layout/activity_edit_entry.xml @@ -168,6 +168,8 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 3be9deed..e3e901c8 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -9,7 +9,7 @@ tools:context="me.impy.aegis.ui.MainActivity"> + tools:context="me.impy.aegis.ui.EditEntryActivity">