Bunch of refactoring

- Get rid of KeyProfile and use DatabaseEntry directly
- Don't store Google auth style urls in the db, but use separate fields
- Update testdata to reflect db format changes
- Lay the ground work for HOTP support
- Refactor KeyInfo and split it into OtpInfo, TotpInto and HotpInfo
- Surely some other stuff I forgot about
This commit is contained in:
Alexander Bakker 2018-06-06 16:15:31 +02:00
parent 9859011a6d
commit 4a4ab1a82c
47 changed files with 1230 additions and 861 deletions

View file

@ -34,7 +34,7 @@
android:configChanges="keyboardHidden|orientation|screenSize"> android:configChanges="keyboardHidden|orientation|screenSize">
</activity> </activity>
<activity <activity
android:name=".ui.EditProfileActivity" android:name=".ui.EditEntryActivity"
android:label="Edit profile" android:label="Edit profile"
android:theme="@style/AppTheme.TransparentActionBar"> android:theme="@style/AppTheme.TransparentActionBar">
</activity> </activity>

View file

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

View file

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

View file

@ -1,7 +0,0 @@
package me.impy.aegis.crypto.otp;
public class OTPException extends Exception {
public OTPException(Throwable cause) {
super(cause);
}
}

View file

@ -4,19 +4,16 @@ import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID;
import me.impy.aegis.crypto.KeyInfoException; import me.impy.aegis.otp.OtpInfoException;
public class Database { public class Database {
private static final int VERSION = 1; private static final int VERSION = 1;
private List<DatabaseEntry> _entries = new ArrayList<>(); private DatabaseEntryList _entries = new DatabaseEntryList();
public JSONObject serialize() throws DatabaseException { public JSONObject serialize() {
try { try {
JSONArray array = new JSONArray(); JSONArray array = new JSONArray();
for (DatabaseEntry e : _entries) { for (DatabaseEntry e : _entries) {
@ -28,7 +25,7 @@ public class Database {
obj.put("entries", array); obj.put("entries", array);
return obj; return obj;
} catch (JSONException e) { } 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++) { for (int i = 0; i < array.length(); i++) {
DatabaseEntry entry = new DatabaseEntry(null); DatabaseEntry entry = new DatabaseEntry(null);
entry.deserialize(array.getJSONObject(i)); entry.deserialize(array.getJSONObject(i));
addKey(entry); addEntry(entry);
} }
} catch (JSONException | KeyInfoException e) { } catch (OtpInfoException | JSONException e) {
throw new DatabaseException(e); throw new DatabaseException(e);
} }
} }
public void addKey(DatabaseEntry entry) { public void addEntry(DatabaseEntry entry) {
if (tryGetKeyByUUID(entry.getUUID()) != null) {
throw new AssertionError("entry found with the same uuid");
}
_entries.add(entry); _entries.add(entry);
} }
public void removeKey(DatabaseEntry entry) { public void removeEntry(DatabaseEntry entry) {
entry = getKeyByUUID(entry.getUUID());
_entries.remove(entry); _entries.remove(entry);
} }
public void replaceKey(DatabaseEntry newEntry) { public void replaceEntry(DatabaseEntry newEntry) {
DatabaseEntry oldEntry = getKeyByUUID(newEntry.getUUID()); _entries.replace(newEntry);
_entries.set(_entries.indexOf(oldEntry), newEntry);
} }
public void swapKeys(DatabaseEntry entry1, DatabaseEntry entry2) { public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) {
Collections.swap(_entries, _entries.indexOf(entry1), _entries.indexOf(entry2)); _entries.swap(entry1, entry2);
} }
public List<DatabaseEntry> getKeys() { public List<DatabaseEntry> getEntries() {
return Collections.unmodifiableList(_entries); return _entries.getList();
}
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;
} }
} }

View file

@ -6,33 +6,44 @@ import org.json.JSONObject;
import java.io.Serializable; import java.io.Serializable;
import java.util.UUID; import java.util.UUID;
import me.impy.aegis.crypto.KeyInfo; import me.impy.aegis.otp.OtpInfo;
import me.impy.aegis.crypto.KeyInfoException; import me.impy.aegis.otp.OtpInfoException;
public class DatabaseEntry implements Serializable { public class DatabaseEntry implements Serializable {
private UUID _uuid; private UUID _uuid;
private String _name = ""; private String _name = "";
private String _issuer = "";
private String _icon = ""; private String _icon = "";
private KeyInfo _info; private OtpInfo _info;
public DatabaseEntry() { public DatabaseEntry(OtpInfo info) {
this(new KeyInfo());
}
public DatabaseEntry(KeyInfo info) {
_info = info; _info = info;
_uuid = UUID.randomUUID(); _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(); JSONObject obj = new JSONObject();
obj.put("uuid", _uuid.toString());
obj.put("name", _name); try {
obj.put("url", _info.getURL()); 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; 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 there is no uuid, generate a new one
if (!obj.has("uuid")) { if (!obj.has("uuid")) {
_uuid = UUID.randomUUID(); _uuid = UUID.randomUUID();
@ -40,29 +51,43 @@ public class DatabaseEntry implements Serializable {
_uuid = UUID.fromString(obj.getString("uuid")); _uuid = UUID.fromString(obj.getString("uuid"));
} }
_name = obj.getString("name"); _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() { public UUID getUUID() {
return _uuid; return _uuid;
} }
public String getName() { public String getName() {
return _name; return _name;
} }
public String getIssuer() {
return _issuer;
}
public String getIcon() { public String getIcon() {
return _icon; return _icon;
} }
public KeyInfo getInfo() {
public OtpInfo getInfo() {
return _info; return _info;
} }
public void setName(String name) { public void setName(String name) {
_name = name; _name = name;
} }
public void setIssuer(String issuer) {
_issuer = issuer;
}
public void setIcon(String icon) { public void setIcon(String icon) {
_icon = icon; _icon = icon;
} }
public void setInfo(KeyInfo info) {
public void setInfo(OtpInfo info) {
_info = info; _info = info;
} }
} }

View file

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

View file

@ -23,7 +23,7 @@ public class DatabaseFile {
private CryptParameters _cryptParameters; private CryptParameters _cryptParameters;
private SlotCollection _slots; private SlotCollection _slots;
public byte[] serialize() throws DatabaseFileException { public byte[] serialize() {
try { try {
JSONObject cryptObj = null; JSONObject cryptObj = null;
if (isEncrypted()) { if (isEncrypted()) {
@ -45,8 +45,8 @@ public class DatabaseFile {
String string = obj.toString(4); String string = obj.toString(4);
return string.getBytes("UTF-8"); return string.getBytes("UTF-8");
} catch (SlotCollectionException | UnsupportedEncodingException | JSONException e) { } catch (JSONException | UnsupportedEncodingException e) {
throw new DatabaseFileException(e); throw new RuntimeException(e);
} }
} }

View file

@ -103,7 +103,7 @@ public class DatabaseManager {
stream.close(); stream.close();
} }
} }
} catch (IOException | DatabaseFileException e) { } catch (IOException e) {
throw new DatabaseManagerException(e); throw new DatabaseManagerException(e);
} }
} }
@ -119,7 +119,7 @@ public class DatabaseManager {
_file.setContent(obj); _file.setContent(obj);
} }
save(_context, _file); save(_context, _file);
} catch (DatabaseException | DatabaseFileException e) { } catch (DatabaseFileException e) {
throw new DatabaseManagerException(e); throw new DatabaseManagerException(e);
} }
} }
@ -157,34 +157,34 @@ public class DatabaseManager {
} }
return file.getAbsolutePath(); return file.getAbsolutePath();
} catch (DatabaseException | IOException | DatabaseFileException e) { } catch (IOException | DatabaseFileException e) {
throw new DatabaseManagerException(e); throw new DatabaseManagerException(e);
} }
} }
public void addKey(DatabaseEntry entry) { public void addEntry(DatabaseEntry entry) {
assertState(false, true); assertState(false, true);
_db.addKey(entry); _db.addEntry(entry);
} }
public void removeKey(DatabaseEntry entry) { public void removeEntry(DatabaseEntry entry) {
assertState(false, true); assertState(false, true);
_db.removeKey(entry); _db.removeEntry(entry);
} }
public void replaceKey(DatabaseEntry entry) { public void replaceEntry(DatabaseEntry entry) {
assertState(false, true); 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); assertState(false, true);
_db.swapKeys(entry1, entry2); _db.swapEntries(entry1, entry2);
} }
public List<DatabaseEntry> getKeys() { public List<DatabaseEntry> getEntries() {
assertState(false, true); assertState(false, true);
return _db.getKeys(); return _db.getEntries();
} }
public MasterKey getMasterKey() { public MasterKey getMasterKey() {

View file

@ -3,9 +3,6 @@ package me.impy.aegis.db.slots;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import me.impy.aegis.crypto.CryptoUtils; import me.impy.aegis.crypto.CryptoUtils;
@ -23,7 +20,7 @@ public class PasswordSlot extends RawSlot {
} }
@Override @Override
public JSONObject serialize() throws SlotException { public JSONObject serialize() {
try { try {
JSONObject obj = super.serialize(); JSONObject obj = super.serialize();
obj.put("n", _n); obj.put("n", _n);
@ -32,7 +29,7 @@ public class PasswordSlot extends RawSlot {
obj.put("salt", Hex.encode(_salt)); obj.put("salt", Hex.encode(_salt));
return obj; return obj;
} catch (JSONException e) { } catch (JSONException e) {
throw new SlotException(e); throw new RuntimeException(e);
} }
} }

View file

@ -67,7 +67,7 @@ public abstract class Slot implements Serializable {
} }
} }
public JSONObject serialize() throws SlotException { public JSONObject serialize() {
try { try {
JSONObject obj = new JSONObject(); JSONObject obj = new JSONObject();
obj.put("type", getType()); obj.put("type", getType());
@ -75,7 +75,7 @@ public abstract class Slot implements Serializable {
obj.put("key", Hex.encode(_encryptedMasterKey)); obj.put("key", Hex.encode(_encryptedMasterKey));
return obj; return obj;
} catch (JSONException e) { } catch (JSONException e) {
throw new SlotException(e); throw new RuntimeException(e);
} }
} }

View file

@ -19,7 +19,7 @@ public class SlotCollection implements Iterable<Slot>, Serializable {
private List<Slot> _slots = new ArrayList<>(); private List<Slot> _slots = new ArrayList<>();
private byte[] _masterHash; private byte[] _masterHash;
public static JSONObject serialize(SlotCollection slots) throws SlotCollectionException { public static JSONObject serialize(SlotCollection slots) {
try { try {
JSONObject obj = new JSONObject(); JSONObject obj = new JSONObject();
obj.put("hash", Hex.encode(slots.getMasterHash())); obj.put("hash", Hex.encode(slots.getMasterHash()));
@ -31,8 +31,8 @@ public class SlotCollection implements Iterable<Slot>, Serializable {
obj.put("entries", entries); obj.put("entries", entries);
return obj; return obj;
} catch (SlotException | JSONException e) { } catch (JSONException e) {
throw new SlotCollectionException(e); throw new RuntimeException(e);
} }
} }

View file

@ -24,8 +24,6 @@ package me.impy.aegis.encoding;
import java.util.Arrays; import java.util.Arrays;
import me.impy.aegis.crypto.CryptoUtils;
/** /**
* Base32 - encodes and decodes RFC3548 Base32 * Base32 - encodes and decodes RFC3548 Base32
* (see http://www.faqs.org/rfcs/rfc3548.html ) * (see http://www.faqs.org/rfcs/rfc3548.html )

View file

@ -5,8 +5,6 @@ import android.widget.EditText;
import java.util.Arrays; import java.util.Arrays;
import me.impy.aegis.crypto.CryptoUtils;
public class EditTextHelper { public class EditTextHelper {
private EditTextHelper() { private EditTextHelper() {
} }

View file

@ -2,12 +2,12 @@ package me.impy.aegis.helpers;
import android.os.Handler; import android.os.Handler;
public class UIRefresher { public class UiRefresher {
private boolean _running; private boolean _running;
private Listener _listener; private Listener _listener;
private Handler _handler; private Handler _handler;
public UIRefresher(Listener listener) { public UiRefresher(Listener listener) {
_listener = listener; _listener = listener;
_handler = new Handler(); _handler = new Handler();
} }

View file

@ -43,7 +43,7 @@ public class AegisImporter extends DatabaseImporter {
Database db = new Database(); Database db = new Database();
db.deserialize(obj); db.deserialize(obj);
return db.getKeys(); return db.getEntries();
} catch (DatabaseException | DatabaseFileException e) { } catch (DatabaseException | DatabaseFileException e) {
throw new DatabaseImporterException(e); throw new DatabaseImporterException(e);
} }

View file

@ -8,17 +8,19 @@ import java.io.UnsupportedEncodingException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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.db.DatabaseEntry;
import me.impy.aegis.encoding.Base32; import me.impy.aegis.encoding.Base32;
import me.impy.aegis.encoding.Base32Exception; 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; import me.impy.aegis.util.ByteInputStream;
public class AndOTPImporter extends DatabaseImporter { public class AndOtpImporter extends DatabaseImporter {
private JSONArray _obj; private JSONArray _obj;
public AndOTPImporter(ByteInputStream stream) { public AndOtpImporter(ByteInputStream stream) {
super(stream); super(stream);
} }
@ -41,30 +43,35 @@ public class AndOTPImporter extends DatabaseImporter {
for (int i = 0; i < _obj.length(); i++) { for (int i = 0; i < _obj.length(); i++) {
JSONObject obj = _obj.getJSONObject(i); JSONObject obj = _obj.getJSONObject(i);
KeyInfo key = new KeyInfo(); String type = obj.getString("type");
key.setAlgorithm(obj.getString("algorithm")); String algo = obj.getString("algorithm");
key.setDigits(obj.getInt("digits")); int digits = obj.getInt("digits");
key.setPeriod(obj.getInt("period")); byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
key.setType(obj.getString("type"));
if (key.getType().equals("hotp")) { OtpInfo info;
key.setCounter(obj.getLong("counter")); 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(" - "); String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) { if (parts.length > 1) {
key.setIssuer(parts[0]); issuer = parts[0];
key.setAccountName(parts[1]); name = parts[1];
} else { } else {
key.setAccountName(parts[0]); name = parts[0];
} }
byte[] secret = Base32.decode(obj.getString("secret").toCharArray()); DatabaseEntry entry = new DatabaseEntry(info, name, issuer);
key.setSecret(secret);
DatabaseEntry entry = new DatabaseEntry(key);
entries.add(entry); entries.add(entry);
} }
} catch (Base32Exception | KeyInfoException | JSONException e) { } catch (Base32Exception | OtpInfoException | JSONException e) {
throw new DatabaseImporterException(e); throw new DatabaseImporterException(e);
} }

View file

@ -16,8 +16,8 @@ public abstract class DatabaseImporter {
// note: keep this list sorted alphabetically // note: keep this list sorted alphabetically
LinkedHashMap<String, Class<? extends DatabaseImporter>> importers = new LinkedHashMap<>(); LinkedHashMap<String, Class<? extends DatabaseImporter>> importers = new LinkedHashMap<>();
importers.put("Aegis", AegisImporter.class); importers.put("Aegis", AegisImporter.class);
importers.put("andOTP", AndOTPImporter.class); importers.put("andOTP", AndOtpImporter.class);
importers.put("FreeOTP", FreeOTPImporter.class); importers.put("FreeOTP", FreeOtpImporter.class);
_importers = Collections.unmodifiableMap(importers); _importers = Collections.unmodifiableMap(importers);
} }

View file

@ -4,4 +4,8 @@ public class DatabaseImporterException extends Exception {
public DatabaseImporterException(Throwable cause) { public DatabaseImporterException(Throwable cause) {
super(cause); super(cause);
} }
public DatabaseImporterException(String message) {
super(message);
}
} }

View file

@ -12,47 +12,86 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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.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; import me.impy.aegis.util.ByteInputStream;
public class FreeOTPImporter extends DatabaseImporter { public class FreeOtpImporter extends DatabaseImporter {
public FreeOTPImporter(ByteInputStream stream) { private List<XmlEntry> _xmlEntries;
public FreeOtpImporter(ByteInputStream stream) {
super(stream); super(stream);
} }
private static class Entry { private static class XmlEntry {
String Name; String Name;
String Value; String Value;
} }
@Override @Override
public void parse() throws DatabaseImporterException { public void parse() throws DatabaseImporterException {
}
@Override
public List<DatabaseEntry> convert() throws DatabaseImporterException {
try { try {
XmlPullParser parser = Xml.newPullParser(); XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(_stream, null); parser.setInput(_stream, null);
parser.nextTag(); parser.nextTag();
return parse(parser); _xmlEntries = parse(parser);
} catch (KeyInfoException | XmlPullParserException | JSONException | IOException e) { } catch (XmlPullParserException | IOException e) {
throw new DatabaseImporterException(e); throw new DatabaseImporterException(e);
} }
} }
@Override
public List<DatabaseEntry> convert() throws DatabaseImporterException {
List<DatabaseEntry> 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 @Override
public boolean isEncrypted() { public boolean isEncrypted() {
return false; return false;
} }
private static List<DatabaseEntry> parse(XmlPullParser parser) private static List<XmlEntry> parse(XmlPullParser parser)
throws IOException, XmlPullParserException, JSONException, KeyInfoException { throws IOException, XmlPullParserException {
List<Entry> entries = new ArrayList<>(); List<XmlEntry> entries = new ArrayList<>();
parser.require(XmlPullParser.START_TAG, null, "map"); parser.require(XmlPullParser.START_TAG, null, "map");
while (parser.next() != XmlPullParser.END_TAG) { while (parser.next() != XmlPullParser.END_TAG) {
@ -68,32 +107,7 @@ public class FreeOTPImporter extends DatabaseImporter {
entries.add(parseEntry(parser)); entries.add(parseEntry(parser));
} }
List<DatabaseEntry> profiles = new ArrayList<>(); return entries;
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;
} }
private static byte[] toBytes(JSONArray array) throws JSONException { private static byte[] toBytes(JSONArray array) throws JSONException {
@ -104,12 +118,12 @@ public class FreeOTPImporter extends DatabaseImporter {
return bytes; 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"); parser.require(XmlPullParser.START_TAG, null, "string");
String name = parser.getAttributeValue(null, "name"); String name = parser.getAttributeValue(null, "name");
String value = parseText(parser); String value = parseText(parser);
parser.require(XmlPullParser.END_TAG, null, "string"); 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 { private static String parseText(XmlPullParser parser) throws IOException, XmlPullParserException {

View file

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

View file

@ -1,11 +1,15 @@
package me.impy.aegis.crypto; package me.impy.aegis.otp;
public class KeyInfoException extends Exception { public class GoogleAuthInfoException extends Exception {
public KeyInfoException(String message) { public GoogleAuthInfoException(Throwable cause) {
super(cause);
}
public GoogleAuthInfoException(String message) {
super(message); super(message);
} }
public KeyInfoException(String message, Throwable cause) { public GoogleAuthInfoException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -19,32 +19,39 @@ import android.widget.EditText;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TableRow;
import com.amulyakhare.textdrawable.TextDrawable; import com.amulyakhare.textdrawable.TextDrawable;
import me.impy.aegis.R; 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.db.DatabaseEntry;
import me.impy.aegis.encoding.Base32; import me.impy.aegis.encoding.Base32;
import me.impy.aegis.encoding.Base32Exception;
import me.impy.aegis.helpers.EditTextHelper; import me.impy.aegis.helpers.EditTextHelper;
import me.impy.aegis.helpers.SpinnerHelper; import me.impy.aegis.helpers.SpinnerHelper;
import me.impy.aegis.helpers.TextDrawableHelper; 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.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 _isNew = false;
private boolean _edited = false; private boolean _edited = false;
private KeyProfile _profile; private DatabaseEntry _entry;
private ImageView _iconView; private ImageView _iconView;
private EditText _textName; private EditText _textName;
private EditText _textIssuer; private EditText _textIssuer;
private EditText _textPeriod; private EditText _textPeriod;
private EditText _textCounter;
private EditText _textSecret; private EditText _textSecret;
private TableRow _rowPeriod;
private TableRow _rowCounter;
private Spinner _spinnerType; private Spinner _spinnerType;
private Spinner _spinnerAlgo; private Spinner _spinnerAlgo;
private Spinner _spinnerDigits; private Spinner _spinnerDigits;
@ -56,27 +63,28 @@ public class EditProfileActivity extends AegisActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit_profile); setContentView(R.layout.activity_edit_entry);
ActionBar bar = getSupportActionBar(); ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_close); bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setDisplayHomeAsUpEnabled(true); bar.setDisplayHomeAsUpEnabled(true);
// if the intent doesn't contain a KeyProfile, create a new one // retrieve info from the calling activity
Intent intent = getIntent(); Intent intent = getIntent();
_profile = (KeyProfile) intent.getSerializableExtra("KeyProfile"); _entry = (DatabaseEntry) intent.getSerializableExtra("entry");
_isNew = intent.getBooleanExtra("isNew", false); _isNew = intent.getBooleanExtra("isNew", false);
if (_profile == null) {
_profile = new KeyProfile();
}
if (_isNew) { if (_isNew) {
setTitle("Add profile"); setTitle("Add profile");
} }
// set up fields
_iconView = findViewById(R.id.profile_drawable); _iconView = findViewById(R.id.profile_drawable);
_textName = findViewById(R.id.text_name); _textName = findViewById(R.id.text_name);
_textIssuer = findViewById(R.id.text_issuer); _textIssuer = findViewById(R.id.text_issuer);
_textPeriod = findViewById(R.id.text_period); _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); _textSecret = findViewById(R.id.text_secret);
_spinnerType = findViewById(R.id.spinner_type); _spinnerType = findViewById(R.id.spinner_type);
SpinnerHelper.fillSpinner(this, _spinnerType, R.array.otp_types_array); 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); SpinnerHelper.fillSpinner(this, _spinnerAlgo, R.array.otp_algo_array);
_spinnerDigits = findViewById(R.id.spinner_digits); _spinnerDigits = findViewById(R.id.spinner_digits);
SpinnerHelper.fillSpinner(this, _spinnerDigits, R.array.otp_digits_array); SpinnerHelper.fillSpinner(this, _spinnerDigits, R.array.otp_digits_array);
_advancedSettingsHeader = findViewById(R.id.accordian_header); _advancedSettingsHeader = findViewById(R.id.accordian_header);
_advancedSettings = findViewById(R.id.expandableLayout); _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); _textName.addTextChangedListener(_textListener);
_textIssuer.addTextChangedListener(_textListener); _textIssuer.addTextChangedListener(_textListener);
_textPeriod.addTextChangedListener(_textListener); _textPeriod.addTextChangedListener(_textListener);
_textCounter.addTextChangedListener(_textListener);
_textSecret.addTextChangedListener(_textListener); _textSecret.addTextChangedListener(_textListener);
_spinnerType.setOnTouchListener(_selectedListener);
_spinnerType.setOnItemSelectedListener(_selectedListener);
_spinnerAlgo.setOnTouchListener(_selectedListener); _spinnerAlgo.setOnTouchListener(_selectedListener);
_spinnerAlgo.setOnItemSelectedListener(_selectedListener); _spinnerAlgo.setOnItemSelectedListener(_selectedListener);
_spinnerDigits.setOnTouchListener(_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 -> { _advancedSettingsHeader.setOnClickListener(v -> {
openAdvancedSettings(); openAdvancedSettings();
}); });
// Automatically open advanced settings since 'Secret' is required. // automatically open advanced settings since 'Secret' is required.
if(_isNew){ if(_isNew){
openAdvancedSettings(); 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 @Override
protected void setPreferredTheme(boolean darkMode) { protected void setPreferredTheme(boolean darkMode) {
if (darkMode) { if (darkMode) {
@ -252,7 +296,7 @@ public class EditProfileActivity extends AegisActivity {
private void finish(boolean delete) { private void finish(boolean delete) {
Intent intent = new Intent(); Intent intent = new Intent();
intent.putExtra("KeyProfile", _profile); intent.putExtra("entry", _entry);
intent.putExtra("delete", delete); intent.putExtra("delete", delete);
setResult(RESULT_OK, intent); setResult(RESULT_OK, intent);
finish(); finish();
@ -264,14 +308,6 @@ public class EditProfileActivity extends AegisActivity {
return false; 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 type = _spinnerType.getSelectedItem().toString();
String algo = _spinnerAlgo.getSelectedItem().toString(); String algo = _spinnerAlgo.getSelectedItem().toString();
@ -283,24 +319,60 @@ public class EditProfileActivity extends AegisActivity {
return false; return false;
} }
DatabaseEntry entry = _profile.getEntry(); byte[] secret;
entry.setName(_textName.getText().toString());
KeyInfo info = entry.getInfo();
try { try {
char[] secret = EditTextHelper.getEditTextChars(_textSecret); secret = Base32.decode(EditTextHelper.getEditTextChars(_textSecret));
info.setSecret(secret); } catch (Base32Exception e) {
info.setIssuer(_textIssuer.getText().toString()); onError("Secret is not valid base32.");
info.setPeriod(period); 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.setDigits(digits);
info.setAlgorithm(algo); info.setAlgorithm(algo);
info.setType(type); } catch (OtpInfoException e) {
info.setAccountName(_textName.getText().toString());
} catch (KeyInfoException e) {
onError("The entered info is incorrect: " + e.getMessage()); onError("The entered info is incorrect: " + e.getMessage());
return false; 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); finish(false);
return true; return true;
} }

View file

@ -18,7 +18,6 @@ import javax.crypto.SecretKey;
import me.impy.aegis.Preferences; import me.impy.aegis.Preferences;
import me.impy.aegis.R; import me.impy.aegis.R;
import me.impy.aegis.crypto.MasterKey; import me.impy.aegis.crypto.MasterKey;
import me.impy.aegis.db.DatabaseException;
import me.impy.aegis.db.DatabaseFileException; import me.impy.aegis.db.DatabaseFileException;
import me.impy.aegis.db.DatabaseManagerException; import me.impy.aegis.db.DatabaseManagerException;
import me.impy.aegis.db.slots.FingerprintSlot; import me.impy.aegis.db.slots.FingerprintSlot;
@ -186,7 +185,7 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback {
_databaseFile.setContent(obj, masterKey); _databaseFile.setContent(obj, masterKey);
} }
DatabaseManager.save(getApplicationContext(), _databaseFile); DatabaseManager.save(getApplicationContext(), _databaseFile);
} catch (DatabaseException | DatabaseManagerException | DatabaseFileException e) { } catch (DatabaseManagerException | DatabaseFileException e) {
setException(e); setException(e);
return; return;
} }

View file

@ -26,15 +26,14 @@ import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.db.DatabaseManager; import me.impy.aegis.db.DatabaseManager;
import me.impy.aegis.helpers.PermissionHelper; import me.impy.aegis.helpers.PermissionHelper;
import me.impy.aegis.ui.dialogs.Dialogs; import me.impy.aegis.ui.dialogs.Dialogs;
import me.impy.aegis.ui.views.KeyProfile; import me.impy.aegis.ui.views.EntryListView;
import me.impy.aegis.ui.views.KeyProfileView;
public class MainActivity extends AegisActivity implements KeyProfileView.Listener { public class MainActivity extends AegisActivity implements EntryListView.Listener {
// activity request codes // activity request codes
private static final int CODE_SCAN_KEYINFO = 0; private static final int CODE_SCAN = 0;
private static final int CODE_ADD_KEYINFO = 1; private static final int CODE_ADD_ENTRY = 1;
private static final int CODE_EDIT_KEYINFO = 2; private static final int CODE_EDIT_ENTRY = 2;
private static final int CODE_ENTER_KEYINFO = 3; private static final int CODE_ENTER_ENTRY = 3;
private static final int CODE_DO_INTRO = 4; private static final int CODE_DO_INTRO = 4;
private static final int CODE_DECRYPT = 5; private static final int CODE_DECRYPT = 5;
private static final int CODE_PREFERENCES = 6; private static final int CODE_PREFERENCES = 6;
@ -44,7 +43,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
private AegisApplication _app; private AegisApplication _app;
private DatabaseManager _db; private DatabaseManager _db;
private KeyProfileView _keyProfileView; private EntryListView _entryListView;
private Menu _menu; private Menu _menu;
private FloatingActionsMenu _fabMenu; private FloatingActionsMenu _fabMenu;
@ -58,20 +57,20 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
// set up the main view // set up the main view
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
// set up the key profile view // set up the entry view
_keyProfileView = (KeyProfileView) getSupportFragmentManager().findFragmentById(R.id.key_profiles); _entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
_keyProfileView.setListener(this); _entryListView.setListener(this);
_keyProfileView.setShowIssuer(getPreferences().isIssuerVisible()); _entryListView.setShowIssuer(getPreferences().isIssuerVisible());
// set up the floating action button // set up the floating action button
_fabMenu = findViewById(R.id.fab); _fabMenu = findViewById(R.id.fab);
findViewById(R.id.fab_enter).setOnClickListener(view -> { findViewById(R.id.fab_enter).setOnClickListener(view -> {
_fabMenu.collapse(); _fabMenu.collapse();
onEnterKeyInfo(); onEnterEntry();
}); });
findViewById(R.id.fab_scan).setOnClickListener(view -> { findViewById(R.id.fab_scan).setOnClickListener(view -> {
_fabMenu.collapse(); _fabMenu.collapse();
onScanKeyInfo(); onScan();
}); });
// skip this part if this is the not initial startup and the database has been unlocked // 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()) { if (!_db.isLocked()) {
loadKeyProfiles(); loadEntries();
} }
} }
@ -141,17 +140,17 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
} }
switch (requestCode) { switch (requestCode) {
case CODE_SCAN_KEYINFO: case CODE_SCAN:
onScanKeyInfoResult(resultCode, data); onScanResult(resultCode, data);
break; break;
case CODE_ADD_KEYINFO: case CODE_ADD_ENTRY:
onAddKeyInfoResult(resultCode, data); onAddEntryResult(resultCode, data);
break; break;
case CODE_EDIT_KEYINFO: case CODE_EDIT_ENTRY:
onEditKeyInfoResult(resultCode, data); onEditEntryResult(resultCode, data);
break; break;
case CODE_ENTER_KEYINFO: case CODE_ENTER_ENTRY:
onEnterKeyInfoResult(resultCode, data); onEnterEntryResult(resultCode, data);
break; break;
case CODE_DO_INTRO: case CODE_DO_INTRO:
onDoIntroResult(resultCode, data); onDoIntroResult(resultCode, data);
@ -174,35 +173,35 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
switch (requestCode) { switch (requestCode) {
case CODE_PERM_CAMERA: case CODE_PERM_CAMERA:
onScanKeyInfo(); onScan();
break; break;
} }
} }
private void onPreferencesResult(int resultCode, Intent data) { 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)) { if (data.getBooleanExtra("needsRecreate", false)) {
recreate(); recreate();
} else if (data.getBooleanExtra("needsRefresh", false)) { } else if (data.getBooleanExtra("needsRefresh", false)) {
boolean showIssuer = getPreferences().isIssuerVisible(); boolean showIssuer = getPreferences().isIssuerVisible();
_keyProfileView.setShowIssuer(showIssuer); _entryListView.setShowIssuer(showIssuer);
} }
} }
private void startEditProfileActivity(int requestCode, KeyProfile profile, boolean isNew) { private void startEditProfileActivity(int requestCode, DatabaseEntry entry, boolean isNew) {
Intent intent = new Intent(this, EditProfileActivity.class); Intent intent = new Intent(this, EditEntryActivity.class);
if (profile != null) { if (entry != null) {
intent.putExtra("KeyProfile", profile); intent.putExtra("entry", entry);
} }
intent.putExtra("isNew", isNew); intent.putExtra("isNew", isNew);
startActivityForResult(intent, requestCode); startActivityForResult(intent, requestCode);
} }
private void onEnterKeyInfo() { private void onEnterEntry() {
startEditProfileActivity(CODE_ENTER_KEYINFO, null, true); startEditProfileActivity(CODE_ENTER_ENTRY, null, true);
} }
private void onScanKeyInfo() { private void onScan() {
if (!PermissionHelper.request(this, CODE_PERM_CAMERA, Manifest.permission.CAMERA)) { if (!PermissionHelper.request(this, CODE_PERM_CAMERA, Manifest.permission.CAMERA)) {
return; return;
} }
@ -210,49 +209,47 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
startScanActivity(); startScanActivity();
} }
private void onScanKeyInfoResult(int resultCode, Intent data) { private void onScanResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
KeyProfile profile = (KeyProfile)data.getSerializableExtra("KeyProfile"); DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
startEditProfileActivity(CODE_ADD_KEYINFO, profile, true); startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
} }
} }
private void onAddKeyInfoResult(int resultCode, Intent data) { private void onAddEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile"); DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
addKey(profile); addEntry(entry);
saveDatabase(); saveDatabase();
} }
} }
private void onEditKeyInfoResult(int resultCode, Intent data) { private void onEditEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile"); DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
if (!data.getBooleanExtra("delete", false)) { if (!data.getBooleanExtra("delete", false)) {
// this profile has been serialized/deserialized and is no longer the same instance it once was // 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 // to deal with this, the replaceEntry functions are used
_db.replaceKey(profile.getEntry()); _db.replaceEntry(entry);
_keyProfileView.replaceKey(profile); _entryListView.replaceEntry(entry);
saveDatabase(); saveDatabase();
} else { } else {
deleteProfile(profile); deleteEntry(entry);
} }
} }
} }
private void onEnterKeyInfoResult(int resultCode, Intent data) { private void onEnterEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile"); DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
addKey(profile); addEntry(entry);
saveDatabase(); saveDatabase();
} }
} }
private void addKey(KeyProfile profile) { private void addEntry(DatabaseEntry entry) {
DatabaseEntry entry = profile.getEntry(); _db.addEntry(entry);
entry.setName(entry.getInfo().getAccountName()); _entryListView.addEntry(entry);
_db.addKey(entry);
_keyProfileView.addKey(profile);
} }
private void onDoIntroResult(int resultCode, Intent data) { private void onDoIntroResult(int resultCode, Intent data) {
@ -275,7 +272,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
return; return;
} }
loadKeyProfiles(); loadEntries();
} }
private void onDecryptResult(int resultCode, Intent intent) { private void onDecryptResult(int resultCode, Intent intent) {
@ -289,13 +286,13 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
return; return;
} }
loadKeyProfiles(); loadEntries();
doShortcutActions(); doShortcutActions();
} }
private void startScanActivity() { private void startScanActivity() {
Intent scannerActivity = new Intent(getApplicationContext(), ScannerActivity.class); Intent scannerActivity = new Intent(getApplicationContext(), ScannerActivity.class);
startActivityForResult(scannerActivity, CODE_SCAN_KEYINFO); startActivityForResult(scannerActivity, CODE_SCAN);
} }
private boolean doShortcutActions() { private boolean doShortcutActions() {
@ -330,12 +327,12 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
updateLockIcon(); updateLockIcon();
// refresh all codes to prevent showing old ones // 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); 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.setCancelable(true);
dialog.getWindow().setLayout(LinearLayout.LayoutParams.MATCH_PARENT, dialog.getWindow().setLayout(LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT); 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.findViewById(R.id.copy_button).setOnClickListener(view -> {
dialog.dismiss(); dialog.dismiss();
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); 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); clipboard.setPrimaryClip(clip);
Toast.makeText(this, "Code copied to the clipboard", Toast.LENGTH_SHORT).show(); 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.findViewById(R.id.delete_button).setOnClickListener(view -> {
dialog.dismiss(); dialog.dismiss();
Dialogs.showDeleteEntryDialog(this, (d, which) -> { Dialogs.showDeleteEntryDialog(this, (d, which) -> {
deleteProfile(profile); deleteEntry(entry);
}); });
}); });
dialog.findViewById(R.id.edit_button).setOnClickListener(view -> { dialog.findViewById(R.id.edit_button).setOnClickListener(view -> {
dialog.dismiss(); dialog.dismiss();
startEditProfileActivity(CODE_EDIT_KEYINFO, profile, false); startEditProfileActivity(CODE_EDIT_ENTRY, entry, false);
}); });
return dialog; return dialog;
} }
private void deleteProfile(KeyProfile profile) { private void deleteEntry(DatabaseEntry entry) {
_db.removeKey(profile.getEntry()); _db.removeEntry(entry);
saveDatabase(); saveDatabase();
_keyProfileView.removeKey(profile); _entryListView.removeEntry(entry);
} }
@Override @Override
@ -387,7 +384,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
startActivityForResult(intent, CODE_PREFERENCES); startActivityForResult(intent, CODE_PREFERENCES);
return true; return true;
case R.id.action_lock: case R.id.action_lock:
_keyProfileView.clearKeys(); _entryListView.clearEntries();
_db.lock(); _db.lock();
startAuthActivity(); startAuthActivity();
return true; return true;
@ -411,11 +408,11 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
} }
} }
private void loadKeyProfiles() { private void loadEntries() {
updateLockIcon(); updateLockIcon();
for (DatabaseEntry entry : _db.getKeys()) { for (DatabaseEntry entry : _db.getEntries()) {
_keyProfileView.addKey(new KeyProfile(entry)); _entryListView.addEntry(entry);
} }
} }
@ -428,13 +425,13 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
} }
@Override @Override
public void onEntryClick(KeyProfile profile) { public void onEntryClick(DatabaseEntry entry) {
createBottomSheet(profile).show(); createBottomSheet(entry).show();
} }
@Override @Override
public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) { public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) {
_db.swapKeys(entry1, entry2); _db.swapEntries(entry1, entry2);
} }
@Override @Override

View file

@ -313,7 +313,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas
private void importDatabase(DatabaseImporter importer) throws DatabaseImporterException { private void importDatabase(DatabaseImporter importer) throws DatabaseImporterException {
List<DatabaseEntry> entries = importer.convert(); List<DatabaseEntry> entries = importer.convert();
for (DatabaseEntry entry : entries) { for (DatabaseEntry entry : entries) {
_db.addKey(entry); _db.addEntry(entry);
} }
if (!saveDatabase()) { if (!saveDatabase()) {

View file

@ -3,6 +3,7 @@ package me.impy.aegis.ui;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.hardware.Camera; import android.hardware.Camera;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@ -16,11 +17,10 @@ import java.util.Collections;
import me.dm7.barcodescanner.core.IViewFinder; import me.dm7.barcodescanner.core.IViewFinder;
import me.dm7.barcodescanner.zxing.ZXingScannerView; import me.dm7.barcodescanner.zxing.ZXingScannerView;
import me.impy.aegis.R; 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.db.DatabaseEntry;
import me.impy.aegis.helpers.SquareFinderView; 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_BACK;
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT; import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT;
@ -107,16 +107,19 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
@Override @Override
public void handleResult(Result rawResult) { public void handleResult(Result rawResult) {
try { try {
KeyInfo info = KeyInfo.fromURL(rawResult.getText()); // parse google auth uri
KeyProfile profile = new KeyProfile(new DatabaseEntry(info)); Uri uri = Uri.parse(rawResult.getText());
profile.getEntry().setName(info.getAccountName()); GoogleAuthInfo info = GoogleAuthInfo.parseUri(uri);
Intent resultIntent = new Intent(); DatabaseEntry entry = new DatabaseEntry(info.getOtpInfo());
resultIntent.putExtra("KeyProfile", profile); 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(); 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(); Toast.makeText(this, "An error occurred while trying to parse the QR code contents", Toast.LENGTH_SHORT).show();
} }

View file

@ -6,7 +6,6 @@ import javax.crypto.SecretKey;
import me.impy.aegis.crypto.CryptoUtils; import me.impy.aegis.crypto.CryptoUtils;
import me.impy.aegis.db.slots.PasswordSlot; import me.impy.aegis.db.slots.PasswordSlot;
import me.impy.aegis.db.slots.SlotException;
public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, SecretKey> { public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, SecretKey> {
private Callback _cb; private Callback _cb;

View file

@ -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<EntryHolder> implements ItemTouchHelperAdapter {
private List<DatabaseEntry> _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<TotpInfo> 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);
}
}

View file

@ -9,20 +9,23 @@ import android.widget.TextView;
import com.amulyakhare.textdrawable.TextDrawable; import com.amulyakhare.textdrawable.TextDrawable;
import me.impy.aegis.R; 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 _profileName;
private TextView _profileCode; private TextView _profileCode;
private TextView _profileIssuer; private TextView _profileIssuer;
private ImageView _profileDrawable; private ImageView _profileDrawable;
private KeyProfile _profile; private DatabaseEntry _entry;
private PeriodProgressBar _progressBar; private PeriodProgressBar _progressBar;
private UIRefresher _refresher; private UiRefresher _refresher;
public KeyProfileHolder(final View view) { public EntryHolder(final View view) {
super(view); super(view);
_profileName = view.findViewById(R.id.profile_name); _profileName = view.findViewById(R.id.profile_name);
_profileCode = view.findViewById(R.id.profile_code); _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); int primaryColorId = view.getContext().getResources().getColor(R.color.colorPrimary);
_progressBar.getProgressDrawable().setColorFilter(primaryColorId, PorterDuff.Mode.SRC_IN); _progressBar.getProgressDrawable().setColorFilter(primaryColorId, PorterDuff.Mode.SRC_IN);
_refresher = new UIRefresher(new UIRefresher.Listener() { _refresher = new UiRefresher(new UiRefresher.Listener() {
@Override @Override
public void onRefresh() { public void onRefresh() {
refreshCode(); refreshCode();
@ -42,26 +45,26 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder {
@Override @Override
public long getMillisTillNextRefresh() { public long getMillisTillNextRefresh() {
return _profile.getEntry().getInfo().getMillisTillNextRotation(); return ((TotpInfo)_entry.getInfo()).getMillisTillNextRotation();
} }
}); });
} }
public void setData(KeyProfile profile, boolean showIssuer, boolean showProgress) { public void setData(DatabaseEntry entry, boolean showIssuer, boolean showProgress) {
_profile = profile; _entry = entry;
_progressBar.setVisibility(showProgress ? View.VISIBLE : View.INVISIBLE); _progressBar.setVisibility(showProgress ? View.VISIBLE : View.INVISIBLE);
if (showProgress) { 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(""); _profileIssuer.setText("");
if (showIssuer) { 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); _profileDrawable.setImageDrawable(drawable);
refreshCode(); refreshCode();
@ -76,7 +79,7 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder {
} }
private void refreshCode() { 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)); _profileCode.setText(otp.substring(0, otp.length() / 2) + " " + otp.substring(otp.length() / 2));
} }
} }

View file

@ -11,29 +11,29 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import me.impy.aegis.R; import me.impy.aegis.R;
import me.impy.aegis.crypto.KeyInfo;
import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.helpers.SimpleItemTouchHelperCallback; 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 { public class EntryListView extends Fragment implements EntryAdapter.Listener {
private KeyProfileAdapter _adapter; private EntryAdapter _adapter;
private Listener _listener; private Listener _listener;
private PeriodProgressBar _progressBar; private PeriodProgressBar _progressBar;
private boolean _showProgress = false; private boolean _showProgress = false;
private UIRefresher _refresher; private UiRefresher _refresher;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
_adapter = new KeyProfileAdapter(this); _adapter = new EntryAdapter(this);
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 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); _progressBar = view.findViewById(R.id.progressBar);
int primaryColorId = getResources().getColor(R.color.colorPrimary); int primaryColorId = getResources().getColor(R.color.colorPrimary);
@ -48,7 +48,7 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
touchHelper.attachToRecyclerView(rvKeyProfiles); touchHelper.attachToRecyclerView(rvKeyProfiles);
rvKeyProfiles.setAdapter(_adapter); rvKeyProfiles.setAdapter(_adapter);
_refresher = new UIRefresher(new UIRefresher.Listener() { _refresher = new UiRefresher(new UiRefresher.Listener() {
@Override @Override
public void onRefresh() { public void onRefresh() {
refresh(); refresh();
@ -56,7 +56,7 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
@Override @Override
public long getMillisTillNextRefresh() { 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 @Override
public void onKeyProfileClick(KeyProfile profile) { public void onEntryClick(DatabaseEntry entry) {
_listener.onEntryClick(profile); _listener.onEntryClick(entry);
} }
@Override @Override
public boolean onLongKeyProfileClick(KeyProfile profile) { public boolean onLongEntryClick(DatabaseEntry entry) {
return false; return false;
} }
@Override @Override
public void onKeyProfileMove(KeyProfile profile1, KeyProfile profile2) { public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) {
_listener.onEntryMove(profile1.getEntry(), profile2.getEntry()); _listener.onEntryMove(entry1, entry2);
} }
@Override @Override
public void onKeyProfileDrop(KeyProfile profile) { public void onEntryDrop(DatabaseEntry entry) {
_listener.onEntryDrop(profile.getEntry()); _listener.onEntryDrop(entry);
} }
public void setShowIssuer(boolean showIssuer) { public void setShowIssuer(boolean showIssuer) {
@ -125,28 +125,28 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
_adapter.notifyDataSetChanged(); _adapter.notifyDataSetChanged();
} }
public void addKey(KeyProfile profile) { public void addEntry(DatabaseEntry entry) {
_adapter.addKey(profile); _adapter.addEntry(entry);
checkPeriodUniformity(); checkPeriodUniformity();
} }
public void removeKey(KeyProfile profile) { public void removeEntry(DatabaseEntry entry) {
_adapter.removeKey(profile); _adapter.removeEntry(entry);
checkPeriodUniformity(); checkPeriodUniformity();
} }
public void clearKeys() { public void clearEntries() {
_adapter.clearKeys(); _adapter.clearEntries();
checkPeriodUniformity(); checkPeriodUniformity();
} }
public void replaceKey(KeyProfile profile) { public void replaceEntry(DatabaseEntry entry) {
_adapter.replaceKey(profile); _adapter.replaceEntry(entry);
checkPeriodUniformity(); checkPeriodUniformity();
} }
public interface Listener { public interface Listener {
void onEntryClick(KeyProfile profile); void onEntryClick(DatabaseEntry entry);
void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2); void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2);
void onEntryDrop(DatabaseEntry entry); void onEntryDrop(DatabaseEntry entry);
} }

View file

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

View file

@ -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<KeyProfileHolder> implements ItemTouchHelperAdapter {
private ArrayList<KeyProfile> _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);
}
}

View file

@ -8,7 +8,7 @@ import android.util.AttributeSet;
import android.view.animation.LinearInterpolator; import android.view.animation.LinearInterpolator;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import me.impy.aegis.crypto.KeyInfo; import me.impy.aegis.otp.TotpInfo;
public class PeriodProgressBar extends ProgressBar { public class PeriodProgressBar extends ProgressBar {
private int _period; private int _period;
@ -40,7 +40,7 @@ public class PeriodProgressBar extends ProgressBar {
setProgress(maxProgress); setProgress(maxProgress);
// calculate the progress the bar should start at // calculate the progress the bar should start at
long millisTillRotation = KeyInfo.getMillisTillNextRotation(_period); long millisTillRotation = TotpInfo.getMillisTillNextRotation(_period);
long period = _period * maxProgress; long period = _period * maxProgress;
int currentProgress = maxProgress - (int) ((((double) period - millisTillRotation) / period) * maxProgress); int currentProgress = maxProgress - (int) ((((double) period - millisTillRotation) / period) * maxProgress);

View file

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M4,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V6A2,2 0 0,1 4,4M4,6V18H11V6H4M20,18V6H18.76C19,6.54 18.95,7.07 18.95,7.13C18.88,7.8 18.41,8.5 18.24,8.75L15.91,11.3L19.23,11.28L19.24,12.5L14.04,12.47L14,11.47C14,11.47 17.05,8.24 17.2,7.95C17.34,7.67 17.91,6 16.5,6C15.27,6.05 15.41,7.3 15.41,7.3L13.87,7.31C13.87,7.31 13.88,6.65 14.25,6H13V18H15.58L15.57,17.14L16.54,17.13C16.54,17.13 17.45,16.97 17.46,16.08C17.5,15.08 16.65,15.08 16.5,15.08C16.37,15.08 15.43,15.13 15.43,15.95H13.91C13.91,15.95 13.95,13.89 16.5,13.89C19.1,13.89 18.96,15.91 18.96,15.91C18.96,15.91 19,17.16 17.85,17.63L18.37,18H20M8.92,16H7.42V10.2L5.62,10.76V9.53L8.76,8.41H8.92V16Z" />
</vector>

View file

@ -168,6 +168,8 @@
</TableRow> </TableRow>
<TableRow <TableRow
android:id="@+id/row_period"
android:visibility="gone"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"> android:layout_marginBottom="5dp">
<ImageView android:layout_column="0" <ImageView android:layout_column="0"
@ -216,6 +218,41 @@
</LinearLayout> </LinearLayout>
</TableRow> </TableRow>
<TableRow
android:id="@+id/row_counter"
android:visibility="gone"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp">
<ImageView android:layout_column="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_counter_black_24dp"
android:layout_marginTop="10dp"
android:tint="?attr/iconColorPrimary"
android:layout_marginStart="15dp"
android:layout_marginEnd="20dp"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal">
<EditText
android:id="@+id/text_counter"
android:hint="Counter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:backgroundTint="#949494"
android:layout_gravity="center"
android:inputType="numberDecimal"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>
</TableRow>
<TableRow <TableRow
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"> android:layout_marginBottom="5dp">

View file

@ -9,7 +9,7 @@
tools:context="me.impy.aegis.ui.MainActivity"> tools:context="me.impy.aegis.ui.MainActivity">
<fragment <fragment
android:name="me.impy.aegis.ui.views.KeyProfileView" android:name="me.impy.aegis.ui.views.EntryListView"
android:id="@+id/key_profiles" android:id="@+id/key_profiles"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -2,7 +2,7 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context="me.impy.aegis.ui.EditProfileActivity"> tools:context="me.impy.aegis.ui.EditEntryActivity">
<item <item
android:id="@+id/action_save" android:id="@+id/action_save"
app:showAsAction="ifRoom" app:showAsAction="ifRoom"

File diff suppressed because one or more lines are too long

View file

@ -8,54 +8,124 @@
"version": 1, "version": 1,
"entries": [ "entries": [
{ {
"type": "totp",
"uuid": "8e986927-a7a3-4d35-88fb-509ae7960155", "uuid": "8e986927-a7a3-4d35-88fb-509ae7960155",
"name": "Bob", "name": "Bob",
"url": "otpauth:\/\/totp\/Google%3ABob?digits=6&period=30&algorithm=SHA1&secret=KD3WHMYQ7K7DGV5QFJNN2ZUATBC44C7F&issuer=Google" "issuer": "Google",
"info": {
"secret": "KD3WHMYQ7K7DGV5QFJNN2ZUATBC44C7F",
"algo": "SHA1",
"digits": 6,
"period": 30
}
}, },
{ {
"type": "totp",
"uuid": "97155de0-fb6a-4dfa-ac59-2abe54f5d059", "uuid": "97155de0-fb6a-4dfa-ac59-2abe54f5d059",
"name": "Henk", "name": "Henk",
"url": "otpauth:\/\/totp\/Scaleway%3AHenk?digits=6&period=10&algorithm=SHA256&secret=MBQX47S3XOSOF7YT2CO3LZNACULZNY4P&issuer=Scaleway" "issuer": "Scaleway",
}, "info": {
{ "secret": "MBQX47S3XOSOF7YT2CO3LZNACULZNY4P",
"uuid": "7dca76af-fa6a-4585-8d47-5a8b78130c9c", "algo": "SHA256",
"name": "Oh wait, they don't have TOTP", "digits": 6,
"url": "otpauth:\/\/totp\/Namecheap%3AOh%20wait%2C%20they%20don't%20have%20TOTP?digits=8&period=30&algorithm=SHA512&secret=HIMV4HX3REXMUG236OOLN4GS7URI247Y&issuer=Namecheap" "period": 10
}, }
{
"uuid": "435a3542-c566-4cc0-a5f4-39cd6ffe316d",
"name": "Delete Me",
"url": "otpauth:\/\/totp\/Facebook%3ADelete%20Me?digits=6&period=60&algorithm=SHA1&secret=GSQ7E5FO3UFFVANS3NSMFE7RTMRMYNCV&issuer=Facebook"
}, },
{ {
"type": "totp",
"uuid": "a6276843-3c95-43b7-a3ef-e824c5534801", "uuid": "a6276843-3c95-43b7-a3ef-e824c5534801",
"name": "nodejsc0d3r", "name": "nodejsc0d3r",
"url": "otpauth:\/\/totp\/Github%3Anodejsc0d3r?digits=6&period=50&algorithm=SHA1&secret=642GK5EROWKUAEBFRH6FWSMM4FRKS5IA&issuer=Github" "issuer": "Github",
}, "info": {
{ "secret": "642GK5EROWKUAEBFRH6FWSMM4FRKS5IA",
"uuid": "159b3e5f-fff4-4ff5-997a-b8d1263ee446", "algo": "SHA1",
"name": "MoneroMan", "digits": 6,
"url": "otpauth:\/\/totp\/Poloniex%3AMoneroMan?digits=8&period=30&algorithm=SHA256&secret=NUSLOF6CFEWNPAARBVJ5WFA5YRRQU4FG&issuer=Poloniex" "period": 50
}
}, },
{ {
"type": "hotp",
"uuid": "1748b48c-7496-4722-9048-17248e843773", "uuid": "1748b48c-7496-4722-9048-17248e843773",
"name": "Lil' Droplet", "name": "Lil' Droplet",
"url": "otpauth:\/\/totp\/DigitalOcean%3ALil'%20Droplet?digits=6&period=45&algorithm=SHA512&secret=6CAIGVYB5MQ6TSZLJ56HJBWU5S3H7FUC&issuer=DigitalOcean" "issuer": "DigitalOcean",
}, "info": {
{ "secret": "6CAIGVYB5MQ6TSZLJ56HJBWU5S3H7FUC",
"uuid": "8cd843e5-4122-48e1-8089-a3d0c769741f", "algo": "SHA512",
"name": "givemeabucket", "digits": 6,
"url": "otpauth:\/\/totp\/Bitbucket%3Agivemeabucket?digits=6&period=20&algorithm=SHA1&secret=5KY3D2N53MRMIOMNKLLTNF3TDQJRDMEY&issuer=Bitbucket" "counter": 30
}, }
{
"uuid": "77c6267a-b38a-40b9-a3a7-5a5ce11230bb",
"name": "Pepe",
"url": "otpauth:\/\/totp\/4chan%3APepe?digits=8&period=30&algorithm=SHA1&secret=EIQMT7NHFYJUMBKQ35P34JGLG3MO7L2W&issuer=4chan"
}, },
{ {
"type": "totp",
"uuid": "9eef3fa3-bdde-4954-bf37-58bf1d5d6e5f", "uuid": "9eef3fa3-bdde-4954-bf37-58bf1d5d6e5f",
"name": "alex", "name": "alex",
"url": "otpauth:\/\/totp\/TTRSS%3Aalex?digits=6&period=30&algorithm=SHA1&secret=BEBXDJJVXKX3ZMAZLJUU5I5PONI4IYAL&issuer=TTRSS" "issuer": "TTRSS",
"info": {
"secret": "BEBXDJJVXKX3ZMAZLJUU5I5PONI4IYAL",
"algo": "SHA1",
"digits": 6,
"period": 30
}
},
{
"type": "totp",
"uuid": "159b3e5f-fff4-4ff5-997a-b8d1263ee446",
"name": "MoneroMan",
"issuer": "Poloniex",
"info": {
"secret": "NUSLOF6CFEWNPAARBVJ5WFA5YRRQU4FG",
"algo": "SHA256",
"digits": 8,
"period": 30
}
},
{
"type": "totp",
"uuid": "8cd843e5-4122-48e1-8089-a3d0c769741f",
"name": "givemeabucket",
"issuer": "Bitbucket",
"info": {
"secret": "5KY3D2N53MRMIOMNKLLTNF3TDQJRDMEY",
"algo": "SHA1",
"digits": 6,
"period": 20
}
},
{
"type": "hotp",
"uuid": "77c6267a-b38a-40b9-a3a7-5a5ce11230bb",
"name": "Pepe",
"issuer": "4chan",
"info": {
"secret": "EIQMT7NHFYJUMBKQ35P34JGLG3MO7L2W",
"algo": "SHA1",
"digits": 8,
"counter": 20
}
},
{
"type": "totp",
"uuid": "435a3542-c566-4cc0-a5f4-39cd6ffe316d",
"name": "Delete Me",
"issuer": "Facebook",
"info": {
"secret": "GSQ7E5FO3UFFVANS3NSMFE7RTMRMYNCV",
"algo": "SHA1",
"digits": 6,
"period": 60
}
},
{
"type": "totp",
"uuid": "7dca76af-fa6a-4585-8d47-5a8b78130c9c",
"name": "Oh wait, they don't have TOTP",
"issuer": "Namecheap",
"info": {
"secret": "HIMV4HX3REXMUG236OOLN4GS7URI247Y",
"algo": "SHA512",
"digits": 8,
"period": 30
}
} }
] ]
} }