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