diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 64703425..a23781b5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -15,7 +15,7 @@
android:supportsRtl="true"
android:theme="@style/AppTheme">
-
+
+
+
diff --git a/app/src/main/java/me/impy/aegis/AegisApplication.java b/app/src/main/java/me/impy/aegis/AegisApplication.java
index 39f89d68..86f92ab7 100644
--- a/app/src/main/java/me/impy/aegis/AegisApplication.java
+++ b/app/src/main/java/me/impy/aegis/AegisApplication.java
@@ -13,6 +13,7 @@ import android.support.annotation.RequiresApi;
import java.util.Collections;
import me.impy.aegis.db.DatabaseManager;
+import me.impy.aegis.ui.MainActivity;
public class AegisApplication extends Application {
private boolean _running = false;
diff --git a/app/src/main/java/me/impy/aegis/EditProfileBottomSheetdialog.java b/app/src/main/java/me/impy/aegis/EditProfileBottomSheetdialog.java
deleted file mode 100644
index e9556799..00000000
--- a/app/src/main/java/me/impy/aegis/EditProfileBottomSheetdialog.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package me.impy.aegis;
-
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.support.design.widget.BottomSheetDialogFragment;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-
-public class EditProfileBottomSheetdialog extends BottomSheetDialogFragment {
- LinearLayout _copyLayout;
-
- public static EditProfileBottomSheetdialog getInstance() {
- return new EditProfileBottomSheetdialog();
- }
-
- public LinearLayout GetCopyLayout()
- {
- return _copyLayout;
- }
-
- @Nullable
- @Override
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- return inflater.inflate(R.layout.bottom_sheet_edit_profile, container, false);
- }
-}
diff --git a/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java b/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java
index 14c8db3c..6b336dca 100644
--- a/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java
+++ b/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java
@@ -44,10 +44,7 @@ public class CryptoUtils {
public static SecretKey deriveKey(char[] password, byte[] salt, int n, int r, int p) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] bytes = toBytes(password);
byte[] keyBytes = SCrypt.generate(bytes, salt, n, r, p, CRYPTO_KEY_SIZE);
- zero(bytes);
- SecretKey key = new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
- zero(keyBytes);
- return key;
+ return new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
}
public static Cipher createCipher(SecretKey key, int opmode) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
@@ -102,7 +99,6 @@ public class CryptoUtils {
byte[] bytes = key.getEncoded();
hash.update(bytes);
- CryptoUtils.zero(bytes);
return hash.digest();
}
@@ -120,21 +116,13 @@ public class CryptoUtils {
return generateRandomBytes(CRYPTO_NONCE_SIZE);
}
- private static byte[] generateRandomBytes(int length) {
+ public static byte[] generateRandomBytes(int length) {
SecureRandom random = new SecureRandom();
byte[] data = new byte[length];
random.nextBytes(data);
return data;
}
- public static void zero(char[] data) {
- Arrays.fill(data, '\0');
- }
-
- public static void zero(byte[] data) {
- Arrays.fill(data, (byte) 0);
- }
-
private static byte[] toBytes(char[] chars) {
CharBuffer charBuf = CharBuffer.wrap(chars);
ByteBuffer byteBuf = Charset.forName("UTF-8").encode(charBuf);
diff --git a/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandle.java b/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandle.java
index 91c5ae95..dbba7298 100644
--- a/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandle.java
+++ b/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandle.java
@@ -1,41 +1,53 @@
package me.impy.aegis.crypto;
+import android.annotation.SuppressLint;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
import java.io.IOException;
+import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
+import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
+import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
public class KeyStoreHandle {
private final KeyStore _keyStore;
- private static final String KEY_NAME = "AegisKey";
private static final String STORE_NAME = "AndroidKeyStore";
- public KeyStoreHandle() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
- _keyStore = KeyStore.getInstance(STORE_NAME);
- _keyStore.load(null);
+ public KeyStoreHandle() throws KeyStoreHandleException {
+ try {
+ _keyStore = KeyStore.getInstance(STORE_NAME);
+ _keyStore.load(null);
+ } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
+ throw new KeyStoreHandleException(e);
+ }
}
- public boolean keyExists() throws KeyStoreException {
- return _keyStore.containsAlias(KEY_NAME);
+ public boolean containsKey(String id) throws KeyStoreHandleException {
+ try {
+ return _keyStore.containsAlias(id);
+ } catch (KeyStoreException e) {
+ throw new KeyStoreHandleException(e);
+ }
}
- public SecretKey generateKey(boolean authRequired) throws Exception {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ public SecretKey generateKey(String id) throws Exception {
+ if (isSupported()) {
KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, STORE_NAME);
- generator.init(new KeyGenParameterSpec.Builder(KEY_NAME,
+ generator.init(new KeyGenParameterSpec.Builder(id,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_ECB)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
- .setUserAuthenticationRequired(authRequired)
+ .setUserAuthenticationRequired(true)
.setRandomizedEncryptionRequired(false)
.setKeySize(CryptoUtils.CRYPTO_KEY_SIZE * 8)
.build());
@@ -46,7 +58,32 @@ public class KeyStoreHandle {
}
}
- public SecretKey getKey() throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {
- return (SecretKey) _keyStore.getKey(KEY_NAME, null);
+ public SecretKey getKey(String id)
+ throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {
+ SecretKey key = (SecretKey) _keyStore.getKey(id, null);
+
+ // try to initialize a dummy cipher
+ // and see if KeyPermanentlyInvalidatedException is thrown
+ if (isSupported()) {
+ try {
+ @SuppressLint("GetInstance")
+ Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_CIPHER_RAW);
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ } catch (KeyPermanentlyInvalidatedException e) {
+ return null;
+ } catch (NoSuchPaddingException | InvalidKeyException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ return key;
+ }
+
+ public void deleteKey(String id) throws KeyStoreException {
+ _keyStore.deleteEntry(id);
+ }
+
+ public static boolean isSupported() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
}
diff --git a/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandleException.java b/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandleException.java
new file mode 100644
index 00000000..424f4533
--- /dev/null
+++ b/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandleException.java
@@ -0,0 +1,7 @@
+package me.impy.aegis.crypto;
+
+public class KeyStoreHandleException extends Exception {
+ public KeyStoreHandleException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/app/src/main/java/me/impy/aegis/crypto/slots/RawSlot.java b/app/src/main/java/me/impy/aegis/crypto/slots/RawSlot.java
deleted file mode 100644
index 803b673d..00000000
--- a/app/src/main/java/me/impy/aegis/crypto/slots/RawSlot.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package me.impy.aegis.crypto.slots;
-
-import me.impy.aegis.crypto.CryptoUtils;
-import me.impy.aegis.util.LittleByteBuffer;
-
-public class RawSlot extends Slot {
-
- public RawSlot() {
- super();
- }
-
- @Override
- public byte[] serialize() {
- LittleByteBuffer buffer = LittleByteBuffer.allocate(getSize());
- buffer.put(getType());
- buffer.put(_encryptedMasterKey);
- return buffer.array();
- }
-
- @Override
- public void deserialize(byte[] data) throws Exception {
- LittleByteBuffer buffer = LittleByteBuffer.wrap(data);
- if (buffer.get() != getType()) {
- throw new Exception("slot type mismatch");
- }
- _encryptedMasterKey = new byte[CryptoUtils.CRYPTO_KEY_SIZE];
- buffer.get(_encryptedMasterKey);
- }
-
- @Override
- public int getSize() {
- return 1 + CryptoUtils.CRYPTO_KEY_SIZE;
- }
-
- @Override
- public byte getType() {
- return TYPE_RAW;
- }
-}
diff --git a/app/src/main/java/me/impy/aegis/db/Database.java b/app/src/main/java/me/impy/aegis/db/Database.java
index abba70f6..a4075610 100644
--- a/app/src/main/java/me/impy/aegis/db/Database.java
+++ b/app/src/main/java/me/impy/aegis/db/Database.java
@@ -13,11 +13,7 @@ public class Database {
private List _entries = new ArrayList<>();
private long _counter = 0;
- public byte[] serialize() throws Exception {
- return serialize(false);
- }
-
- public byte[] serialize(boolean pretty) throws Exception {
+ public JSONObject serialize() throws Exception {
JSONArray array = new JSONArray();
for (DatabaseEntry e : _entries) {
array.put(e.serialize());
@@ -26,18 +22,14 @@ public class Database {
JSONObject obj = new JSONObject();
obj.put("version", VERSION);
obj.put("entries", array);
-
- String string = pretty ? obj.toString(4) : obj.toString();
- return string.getBytes("UTF-8");
+ return obj;
}
- public void deserialize(byte[] data) throws Exception {
- deserialize(data, true);
+ public void deserialize(JSONObject obj) throws Exception {
+ deserialize(obj, true);
}
- public void deserialize(byte[] data, boolean incCount) throws Exception {
- JSONObject obj = new JSONObject(new String(data, "UTF-8"));
-
+ public void deserialize(JSONObject obj, boolean incCount) throws Exception {
// TODO: support different VERSION deserialization providers
int ver = obj.getInt("version");
if (ver != VERSION) {
diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseFile.java b/app/src/main/java/me/impy/aegis/db/DatabaseFile.java
index 98c5df1f..51e8736b 100644
--- a/app/src/main/java/me/impy/aegis/db/DatabaseFile.java
+++ b/app/src/main/java/me/impy/aegis/db/DatabaseFile.java
@@ -1,163 +1,116 @@
package me.impy.aegis.db;
-import java.io.ByteArrayOutputStream;
-import java.io.DataOutputStream;
+import android.util.Base64;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
import java.io.IOException;
-import java.lang.reflect.UndeclaredThrowableException;
-import java.util.Arrays;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
import me.impy.aegis.crypto.CryptParameters;
-import me.impy.aegis.crypto.slots.SlotCollection;
-import me.impy.aegis.crypto.CryptoUtils;
-import me.impy.aegis.util.LittleByteBuffer;
+import me.impy.aegis.crypto.CryptResult;
+import me.impy.aegis.crypto.MasterKey;
+import me.impy.aegis.db.slots.SlotCollection;
+import me.impy.aegis.encoding.Hex;
public class DatabaseFile {
- private static final byte SECTION_ENCRYPTION_PARAMETERS = 0x00;
- private static final byte SECTION_SLOTS = 0x01;
- private static final byte SECTION_END = (byte) 0xFF;
- private static final byte VERSION = 1;
+ public static final byte VERSION = 1;
- private final byte[] HEADER;
-
- private byte[] _content;
+ private Object _content;
private CryptParameters _cryptParameters;
private SlotCollection _slots;
public DatabaseFile() {
- try {
- HEADER = "AEGIS".getBytes("US_ASCII");
- } catch (Exception e) {
- throw new UndeclaredThrowableException(e);
- }
_slots = new SlotCollection();
}
- public byte[] serialize() throws IOException {
- byte[] content = getContent();
- CryptParameters cryptParams = getCryptParameters();
-
- // this is dumb, java doesn't provide an endianness-aware data stream
- ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
- DataOutputStream stream = new DataOutputStream(byteStream);
- stream.write(HEADER);
- stream.write(VERSION);
-
- if (cryptParams != null) {
- LittleByteBuffer paramBuffer = LittleByteBuffer.allocate(CryptoUtils.CRYPTO_NONCE_SIZE + CryptoUtils.CRYPTO_TAG_SIZE);
- paramBuffer.put(cryptParams.Nonce);
- paramBuffer.put(cryptParams.Tag);
- writeSection(stream, SECTION_ENCRYPTION_PARAMETERS, paramBuffer.array());
+ public byte[] serialize() throws JSONException, UnsupportedEncodingException {
+ JSONObject cryptObj = null;
+ if (_cryptParameters != null) {
+ cryptObj = new JSONObject();
+ cryptObj.put("nonce", Hex.toString(_cryptParameters.Nonce));
+ cryptObj.put("tag", Hex.toString(_cryptParameters.Tag));
}
- if (!_slots.isEmpty()) {
- byte[] bytes = SlotCollection.serialize(_slots);
- writeSection(stream, SECTION_SLOTS, bytes);
- }
+ JSONObject headerObj = new JSONObject();
+ headerObj.put("slots", _slots.isEmpty() ? JSONObject.NULL : SlotCollection.serialize(_slots));
+ headerObj.put("params", cryptObj != null ? cryptObj : JSONObject.NULL);
- writeSection(stream, SECTION_END, null);
- stream.write(content);
- return byteStream.toByteArray();
+ JSONObject obj = new JSONObject();
+ obj.put("version", VERSION);
+ obj.put("header", headerObj);
+ obj.put("db", _content);
+
+ String string = obj.toString(4);
+ return string.getBytes("UTF-8");
}
public void deserialize(byte[] data) throws Exception {
- LittleByteBuffer buffer = LittleByteBuffer.wrap(data);
-
- byte[] header = new byte[HEADER.length];
- buffer.get(header);
- if (!Arrays.equals(header, HEADER)) {
- throw new Exception("Bad header");
+ JSONObject obj = new JSONObject(new String(data, "UTF-8"));
+ JSONObject headerObj = obj.getJSONObject("header");
+ if (obj.getInt("version") > VERSION) {
+ throw new Exception("unsupported version");
}
- // TODO: support different version deserialization providers
- byte version = buffer.get();
- if (version != VERSION) {
- throw new Exception("Unsupported version");
+ JSONObject slotObj = headerObj.optJSONObject("slots");
+ if (slotObj != null) {
+ _slots = SlotCollection.deserialize(slotObj);
}
- CryptParameters cryptParams = null;
- SlotCollection slots = new SlotCollection();
-
- for (section s = readSection(buffer); s.ID != SECTION_END; s = readSection(buffer)) {
- LittleByteBuffer sBuff = LittleByteBuffer.wrap(s.Data);
- switch (s.ID) {
- case SECTION_ENCRYPTION_PARAMETERS:
- assertLength(s.Data, CryptoUtils.CRYPTO_NONCE_SIZE + CryptoUtils.CRYPTO_TAG_SIZE);
-
- byte[] nonce = new byte[CryptoUtils.CRYPTO_NONCE_SIZE];
- byte[] tag = new byte[CryptoUtils.CRYPTO_TAG_SIZE];
- sBuff.get(nonce);
- sBuff.get(tag);
-
- cryptParams = new CryptParameters() {{
- Nonce = nonce;
- Tag = tag;
- }};
- break;
- case SECTION_SLOTS:
- slots = SlotCollection.deserialize(s.Data);
- break;
- }
+ JSONObject cryptObj = headerObj.optJSONObject("params");
+ if (cryptObj != null) {
+ _cryptParameters = new CryptParameters() {{
+ Nonce = Hex.toBytes(cryptObj.getString("nonce"));
+ Tag = Hex.toBytes(cryptObj.getString("tag"));
+ }};
}
- setCryptParameters(cryptParams);
- setSlots(slots);
-
- byte[] content = new byte[buffer.remaining()];
- buffer.get(content);
- setContent(content);
+ if (cryptObj == null || slotObj == null) {
+ _content = obj.getJSONObject("db");
+ } else {
+ _content = obj.getString("db");
+ }
}
public boolean isEncrypted() {
return !_slots.isEmpty() && _cryptParameters != null;
}
- private static void writeSection(DataOutputStream stream, byte id, byte[] data) throws IOException {
- stream.write(id);
-
- LittleByteBuffer buffer = LittleByteBuffer.allocate(/* sizeof uint32_t */ 4);
- if (data == null) {
- buffer.putInt(0);
- } else {
- buffer.putInt(data.length);
- }
- stream.write(buffer.array());
-
- if (data != null) {
- stream.write(data);
- }
+ public JSONObject getContent() {
+ return (JSONObject) _content;
}
- private static section readSection(LittleByteBuffer buffer) {
- section s = new section();
- s.ID = buffer.get();
-
- int len = buffer.getInt();
- s.Data = new byte[len];
- buffer.get(s.Data);
-
- return s;
+ public JSONObject getContent(MasterKey key)
+ throws NoSuchPaddingException, InvalidKeyException,
+ NoSuchAlgorithmException, IllegalBlockSizeException,
+ BadPaddingException, InvalidAlgorithmParameterException, IOException, JSONException {
+ byte[] bytes = Base64.decode((String) _content, Base64.NO_WRAP);
+ CryptResult result = key.decrypt(bytes, _cryptParameters);
+ return new JSONObject(new String(result.Data, "UTF-8"));
}
- private static void assertLength(byte[] bytes, int length) throws Exception {
- if (bytes.length != length) {
- throw new Exception("Bad length");
- }
+ public void setContent(JSONObject dbObj) {
+ _content = dbObj;
}
- public byte[] getContent() {
- return _content;
- }
+ public void setContent(JSONObject dbObj, MasterKey key)
+ throws JSONException, UnsupportedEncodingException,
+ NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException,
+ IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
+ String string = dbObj.toString(4);
+ byte[] dbBytes = string.getBytes("UTF-8");
- public void setContent(byte[] content) {
- _content = content;
- }
-
- public CryptParameters getCryptParameters() {
- return _cryptParameters;
- }
-
- public void setCryptParameters(CryptParameters parameters) {
- _cryptParameters = parameters;
+ CryptResult result = key.encrypt(dbBytes);
+ _content = new String(Base64.encode(result.Data, Base64.NO_WRAP), "UTF-8");
+ _cryptParameters = result.Parameters;
}
public SlotCollection getSlots() {
@@ -167,9 +120,4 @@ public class DatabaseFile {
public void setSlots(SlotCollection slots) {
_slots = slots;
}
-
- private static class section {
- byte ID;
- byte[] Data;
- }
}
diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseManager.java b/app/src/main/java/me/impy/aegis/db/DatabaseManager.java
index d48c14c6..5db8d5d7 100644
--- a/app/src/main/java/me/impy/aegis/db/DatabaseManager.java
+++ b/app/src/main/java/me/impy/aegis/db/DatabaseManager.java
@@ -3,6 +3,9 @@ package me.impy.aegis.db;
import android.content.Context;
import android.os.Environment;
+import org.json.JSONException;
+import org.json.JSONObject;
+
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
@@ -16,9 +19,9 @@ import me.impy.aegis.crypto.CryptResult;
import me.impy.aegis.crypto.MasterKey;
public class DatabaseManager {
- private static final String FILENAME = "aegis.db";
- private static final String FILENAME_EXPORT = "aegis_export.db";
- private static final String FILENAME_EXPORT_PLAIN = "aegis_export.json";
+ private static final String FILENAME = "aegis.json";
+ private static final String FILENAME_EXPORT = "aegis_export.json";
+ private static final String FILENAME_EXPORT_PLAIN = "aegis_export_plain.json";
private MasterKey _key;
private DatabaseFile _file;
@@ -58,9 +61,9 @@ public class DatabaseManager {
_file.deserialize(fileBytes);
if (!_file.isEncrypted()) {
- byte[] contentBytes = _file.getContent();
+ JSONObject obj = _file.getContent();
_db = new Database();
- _db.deserialize(contentBytes);
+ _db.deserialize(obj);
}
}
@@ -73,15 +76,13 @@ public class DatabaseManager {
public void unlock(MasterKey key) throws Exception {
assertState(true, true);
- byte[] encrypted = _file.getContent();
- CryptParameters params = _file.getCryptParameters();
- CryptResult result = key.decrypt(encrypted, params);
+ JSONObject obj = _file.getContent(key);
_db = new Database();
- _db.deserialize(result.Data);
+ _db.deserialize(obj);
_key = key;
}
- public static void save(Context context, DatabaseFile file) throws IOException {
+ public static void save(Context context, DatabaseFile file) throws IOException, JSONException {
byte[] bytes = file.serialize();
FileOutputStream stream = null;
@@ -98,26 +99,21 @@ public class DatabaseManager {
public void save() throws Exception {
assertState(false, true);
- byte[] dbBytes = _db.serialize();
- if (!_file.isEncrypted()) {
- _file.setContent(dbBytes);
+ JSONObject obj = _db.serialize();
+ if (_file.isEncrypted()) {
+ _file.setContent(obj, _key);
} else {
- CryptResult result = _key.encrypt(dbBytes);
- _file.setContent(result.Data);
- _file.setCryptParameters(result.Parameters);
+ _file.setContent(obj);
}
save(_context, _file);
}
public String export(boolean encrypt) throws Exception {
assertState(false, true);
- byte[] bytes = _db.serialize(!encrypt);
- encrypt = encrypt && getFile().isEncrypted();
- if (encrypt) {
- CryptResult result = _key.encrypt(bytes);
- _file.setContent(result.Data);
- _file.setCryptParameters(result.Parameters);
- bytes = _file.serialize();
+ if (encrypt && getFile().isEncrypted()) {
+ _file.setContent(_db.serialize(), _key);
+ } else {
+ _file.setContent(_db.serialize());
}
File file;
@@ -129,6 +125,7 @@ public class DatabaseManager {
throw new IOException("error creating external storage directory");
}
+ byte[] bytes = _file.serialize();
file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN);
stream = new FileOutputStream(file);
stream.write(bytes);
@@ -167,6 +164,11 @@ public class DatabaseManager {
return _db.getKeys();
}
+ public MasterKey getMasterKey() throws Exception {
+ assertState(false, true);
+ return _key;
+ }
+
public DatabaseFile getFile() {
return _file;
}
diff --git a/app/src/main/java/me/impy/aegis/crypto/slots/FingerprintSlot.java b/app/src/main/java/me/impy/aegis/db/slots/FingerprintSlot.java
similarity index 59%
rename from app/src/main/java/me/impy/aegis/crypto/slots/FingerprintSlot.java
rename to app/src/main/java/me/impy/aegis/db/slots/FingerprintSlot.java
index 6f47a666..37e8c3ca 100644
--- a/app/src/main/java/me/impy/aegis/crypto/slots/FingerprintSlot.java
+++ b/app/src/main/java/me/impy/aegis/db/slots/FingerprintSlot.java
@@ -1,6 +1,11 @@
-package me.impy.aegis.crypto.slots;
+package me.impy.aegis.db.slots;
public class FingerprintSlot extends RawSlot {
+
+ public FingerprintSlot() {
+ super();
+ }
+
@Override
public byte getType() {
return TYPE_FINGERPRINT;
diff --git a/app/src/main/java/me/impy/aegis/crypto/slots/PasswordSlot.java b/app/src/main/java/me/impy/aegis/db/slots/PasswordSlot.java
similarity index 51%
rename from app/src/main/java/me/impy/aegis/crypto/slots/PasswordSlot.java
rename to app/src/main/java/me/impy/aegis/db/slots/PasswordSlot.java
index 37ad7827..6835292f 100644
--- a/app/src/main/java/me/impy/aegis/crypto/slots/PasswordSlot.java
+++ b/app/src/main/java/me/impy/aegis/db/slots/PasswordSlot.java
@@ -1,4 +1,7 @@
-package me.impy.aegis.crypto.slots;
+package me.impy.aegis.db.slots;
+
+import org.json.JSONException;
+import org.json.JSONObject;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
@@ -6,7 +9,7 @@ import java.security.spec.InvalidKeySpecException;
import javax.crypto.SecretKey;
import me.impy.aegis.crypto.CryptoUtils;
-import me.impy.aegis.util.LittleByteBuffer;
+import me.impy.aegis.encoding.Hex;
public class PasswordSlot extends RawSlot {
private int _n;
@@ -19,27 +22,22 @@ public class PasswordSlot extends RawSlot {
}
@Override
- public byte[] serialize() {
- byte[] bytes = super.serialize();
- LittleByteBuffer buffer = LittleByteBuffer.wrap(bytes);
- buffer.position(super.getSize());
- buffer.putInt(_n);
- buffer.putInt(_r);
- buffer.putInt(_p);
- buffer.put(_salt);
- return buffer.array();
+ public JSONObject serialize() throws JSONException {
+ JSONObject obj = super.serialize();
+ obj.put("n", _n);
+ obj.put("r", _r);
+ obj.put("p", _p);
+ obj.put("salt", Hex.toString(_salt));
+ return obj;
}
@Override
- public void deserialize(byte[] data) throws Exception {
- super.deserialize(data);
- LittleByteBuffer buffer = LittleByteBuffer.wrap(data);
- buffer.position(super.getSize());
- _n = buffer.getInt();
- _r = buffer.getInt();
- _p = buffer.getInt();
- _salt = new byte[CryptoUtils.CRYPTO_SALT_SIZE];
- buffer.get(_salt);
+ public void deserialize(JSONObject obj) throws Exception {
+ super.deserialize(obj);
+ _n = obj.getInt("n");
+ _r = obj.getInt("r");
+ _p = obj.getInt("p");
+ _salt = Hex.toBytes(obj.getString("salt"));
}
public SecretKey deriveKey(char[] password, byte[] salt, int n, int r, int p) throws InvalidKeySpecException, NoSuchAlgorithmException {
@@ -55,11 +53,6 @@ public class PasswordSlot extends RawSlot {
return CryptoUtils.deriveKey(password, _salt, _n, _r, _p);
}
- @Override
- public int getSize() {
- return 1 + CryptoUtils.CRYPTO_KEY_SIZE + /* _n, _r, _p */ 4 + 4 + 4 + CryptoUtils.CRYPTO_SALT_SIZE;
- }
-
@Override
public byte getType() {
return TYPE_DERIVED;
diff --git a/app/src/main/java/me/impy/aegis/db/slots/RawSlot.java b/app/src/main/java/me/impy/aegis/db/slots/RawSlot.java
new file mode 100644
index 00000000..cbf83d1d
--- /dev/null
+++ b/app/src/main/java/me/impy/aegis/db/slots/RawSlot.java
@@ -0,0 +1,13 @@
+package me.impy.aegis.db.slots;
+
+public class RawSlot extends Slot {
+
+ public RawSlot() {
+ super();
+ }
+
+ @Override
+ public byte getType() {
+ return TYPE_RAW;
+ }
+}
diff --git a/app/src/main/java/me/impy/aegis/crypto/slots/Slot.java b/app/src/main/java/me/impy/aegis/db/slots/Slot.java
similarity index 63%
rename from app/src/main/java/me/impy/aegis/crypto/slots/Slot.java
rename to app/src/main/java/me/impy/aegis/db/slots/Slot.java
index fe887dfb..a5d8ec9a 100644
--- a/app/src/main/java/me/impy/aegis/crypto/slots/Slot.java
+++ b/app/src/main/java/me/impy/aegis/db/slots/Slot.java
@@ -1,7 +1,10 @@
-package me.impy.aegis.crypto.slots;
+package me.impy.aegis.db.slots;
import android.annotation.SuppressLint;
+import org.json.JSONException;
+import org.json.JSONObject;
+
import java.io.Serializable;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
@@ -15,30 +18,34 @@ import javax.crypto.spec.SecretKeySpec;
import me.impy.aegis.crypto.CryptoUtils;
import me.impy.aegis.crypto.MasterKey;
+import me.impy.aegis.encoding.Hex;
public abstract class Slot implements Serializable {
public final static byte TYPE_RAW = 0x00;
public final static byte TYPE_DERIVED = 0x01;
public final static byte TYPE_FINGERPRINT = 0x02;
+ public final static int ID_SIZE = 16;
+ protected byte[] _id;
protected byte[] _encryptedMasterKey;
+ protected Slot() {
+ _id = CryptoUtils.generateRandomBytes(ID_SIZE);
+ }
+
// getKey decrypts the encrypted master key in this slot with the given key and returns it.
public SecretKey getKey(Cipher cipher) throws BadPaddingException, IllegalBlockSizeException {
byte[] decryptedKeyBytes = cipher.doFinal(_encryptedMasterKey);
- SecretKey decryptedKey = new SecretKeySpec(decryptedKeyBytes, CryptoUtils.CRYPTO_CIPHER_AEAD);
- CryptoUtils.zero(decryptedKeyBytes);
- return decryptedKey;
+ return new SecretKeySpec(decryptedKeyBytes, CryptoUtils.CRYPTO_CIPHER_AEAD);
}
// setKey encrypts the given master key with the given key and stores the result in this slot.
public void setKey(MasterKey masterKey, Cipher cipher) throws BadPaddingException, IllegalBlockSizeException {
byte[] masterKeyBytes = masterKey.getBytes();
_encryptedMasterKey = cipher.doFinal(masterKeyBytes);
- CryptoUtils.zero(masterKeyBytes);
}
- // suppressing the AES ECB warning
+ // suppress the AES ECB warning
// this is perfectly safe because we discard this cipher after passing CryptoUtils.CRYPTO_KEY_SIZE bytes through it
@SuppressLint("getInstance")
public static Cipher createCipher(SecretKey key, int mode) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException {
@@ -47,10 +54,24 @@ public abstract class Slot implements Serializable {
return cipher;
}
- public abstract int getSize();
- public abstract byte getType();
+ public JSONObject serialize() throws JSONException {
+ JSONObject obj = new JSONObject();
+ obj.put("type", getType());
+ obj.put("id", Hex.toString(_id));
+ obj.put("key", Hex.toString(_encryptedMasterKey));
+ return obj;
+ }
- // a slot has a binary representation
- public abstract byte[] serialize();
- public abstract void deserialize(byte[] data) throws Exception;
+ public void deserialize(JSONObject obj) throws Exception {
+ if (obj.getInt("type") != getType()) {
+ throw new Exception("slot type mismatch");
+ }
+ _id = Hex.toBytes(obj.getString("id"));
+ _encryptedMasterKey = Hex.toBytes(obj.getString("key"));
+ }
+
+ public abstract byte getType();
+ public String getID() {
+ return Hex.toString(_id);
+ }
}
diff --git a/app/src/main/java/me/impy/aegis/crypto/slots/SlotCollection.java b/app/src/main/java/me/impy/aegis/db/slots/SlotCollection.java
similarity index 73%
rename from app/src/main/java/me/impy/aegis/crypto/slots/SlotCollection.java
rename to app/src/main/java/me/impy/aegis/db/slots/SlotCollection.java
index 9ff26ed9..e34b2db5 100644
--- a/app/src/main/java/me/impy/aegis/crypto/slots/SlotCollection.java
+++ b/app/src/main/java/me/impy/aegis/db/slots/SlotCollection.java
@@ -1,4 +1,8 @@
-package me.impy.aegis.crypto.slots;
+package me.impy.aegis.db.slots;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
import java.io.Serializable;
import java.util.ArrayList;
@@ -9,44 +13,38 @@ import java.util.List;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
-import me.impy.aegis.crypto.CryptoUtils;
import me.impy.aegis.crypto.MasterKey;
-import me.impy.aegis.util.LittleByteBuffer;
+import me.impy.aegis.encoding.Hex;
public class SlotCollection implements Iterable, Serializable {
private List _slots = new ArrayList<>();
private byte[] _masterHash;
- public static byte[] serialize(SlotCollection slots) {
- // yep, no streams at this api level
- int size = 0;
- for (Slot slot : slots) {
- size += slot.getSize();
- }
- size += CryptoUtils.CRYPTO_HASH_SIZE;
-
- LittleByteBuffer buffer = LittleByteBuffer.allocate(size);
- buffer.put(slots.getMasterHash());
+ public static JSONObject serialize(SlotCollection slots) throws JSONException {
+ JSONObject obj = new JSONObject();
+ obj.put("hash", Hex.toString(slots.getMasterHash()));
+ JSONArray entries = new JSONArray();
for (Slot slot : slots) {
- byte[] bytes = slot.serialize();
- buffer.put(bytes);
+ entries.put(slot.serialize());
}
- return buffer.array();
+
+ obj.put("entries", entries);
+ return obj;
}
- public static SlotCollection deserialize(byte[] data) throws Exception {
- LittleByteBuffer buffer = LittleByteBuffer.wrap(data);
- byte[] masterHash = new byte[CryptoUtils.CRYPTO_HASH_SIZE];
- buffer.get(masterHash);
-
+ public static SlotCollection deserialize(JSONObject obj) throws Exception {
SlotCollection slots = new SlotCollection();
+
+ byte[] masterHash = Hex.toBytes(obj.getString("hash"));
slots.setMasterHash(masterHash);
- while (buffer.remaining() > 0) {
+ JSONArray entries = obj.getJSONArray("entries");
+ for (int i = 0; i < entries.length(); i++) {
Slot slot;
+ JSONObject slotObj = entries.getJSONObject(i);
- switch (buffer.peek()) {
+ switch (slotObj.getInt("type")) {
case Slot.TYPE_RAW:
slot = new RawSlot();
break;
@@ -60,10 +58,7 @@ public class SlotCollection implements Iterable, Serializable {
throw new Exception("unrecognized slot type");
}
- byte[] bytes = new byte[slot.getSize()];
- buffer.get(bytes);
-
- slot.deserialize(bytes);
+ slot.deserialize(slotObj);
slots.add(slot);
}
diff --git a/app/src/main/java/me/impy/aegis/crypto/slots/SlotIntegrityException.java b/app/src/main/java/me/impy/aegis/db/slots/SlotIntegrityException.java
similarity index 62%
rename from app/src/main/java/me/impy/aegis/crypto/slots/SlotIntegrityException.java
rename to app/src/main/java/me/impy/aegis/db/slots/SlotIntegrityException.java
index d3090359..79061a5e 100644
--- a/app/src/main/java/me/impy/aegis/crypto/slots/SlotIntegrityException.java
+++ b/app/src/main/java/me/impy/aegis/db/slots/SlotIntegrityException.java
@@ -1,4 +1,4 @@
-package me.impy.aegis.crypto.slots;
+package me.impy.aegis.db.slots;
public class SlotIntegrityException extends Exception {
diff --git a/app/src/main/java/me/impy/aegis/encoding/Base32.java b/app/src/main/java/me/impy/aegis/encoding/Base32.java
index 1a07905c..a217701d 100644
--- a/app/src/main/java/me/impy/aegis/encoding/Base32.java
+++ b/app/src/main/java/me/impy/aegis/encoding/Base32.java
@@ -87,9 +87,7 @@ public class Base32 {
base32[j++] = base32Chars.charAt(digit);
}
- char[] res = Arrays.copyOf(base32, j);
- CryptoUtils.zero(base32);
- return res;
+ return Arrays.copyOf(base32, j);
}
/**
diff --git a/app/src/main/java/me/impy/aegis/encoding/Hex.java b/app/src/main/java/me/impy/aegis/encoding/Hex.java
new file mode 100644
index 00000000..604dd944
--- /dev/null
+++ b/app/src/main/java/me/impy/aegis/encoding/Hex.java
@@ -0,0 +1,46 @@
+package me.impy.aegis.encoding;
+
+// The hexadecimal utility functions in this file were taken and modified from: http://www.docjar.com/html/api/com/sun/xml/internal/bind/DatatypeConverterImpl.java.html
+// It is licensed under GPLv2 with a classpath exception.
+public class Hex {
+ private Hex() {
+ }
+
+ private static int hexToBin(char ch) {
+ if ('0' <= ch && ch <= '9') return ch - '0';
+ if ('A' <= ch && ch <= 'F') return ch - 'A' + 10;
+ if ('a' <= ch && ch <= 'f') return ch - 'a' + 10;
+ return -1;
+ }
+
+ private static final char[] hexCode = "0123456789abcdef".toCharArray();
+
+ public static byte[] toBytes(String s) throws HexException {
+ final int len = s.length();
+
+ if (len % 2 != 0)
+ throw new HexException("hexBinary needs to be even-length: " + s);
+
+ byte[] out = new byte[len / 2];
+
+ for (int i = 0; i < len; i += 2) {
+ int h = hexToBin(s.charAt(i));
+ int l = hexToBin(s.charAt(i + 1));
+ if (h == -1 || l == -1)
+ throw new HexException("contains illegal character for hexBinary: " + s);
+
+ out[i / 2] = (byte) (h * 16 + l);
+ }
+
+ return out;
+ }
+
+ public static String toString(byte[] data) {
+ StringBuilder r = new StringBuilder(data.length * 2);
+ for (byte b : data) {
+ r.append(hexCode[(b >> 4) & 0xF]);
+ r.append(hexCode[(b & 0xF)]);
+ }
+ return r.toString();
+ }
+}
diff --git a/app/src/main/java/me/impy/aegis/encoding/HexException.java b/app/src/main/java/me/impy/aegis/encoding/HexException.java
new file mode 100644
index 00000000..200f8d78
--- /dev/null
+++ b/app/src/main/java/me/impy/aegis/encoding/HexException.java
@@ -0,0 +1,7 @@
+package me.impy.aegis.encoding;
+
+public class HexException extends Exception {
+ public HexException(String message) {
+ super(message);
+ }
+}
diff --git a/app/src/main/java/me/impy/aegis/helpers/AuthHelper.java b/app/src/main/java/me/impy/aegis/helpers/EditTextHelper.java
similarity index 50%
rename from app/src/main/java/me/impy/aegis/helpers/AuthHelper.java
rename to app/src/main/java/me/impy/aegis/helpers/EditTextHelper.java
index 675e1f82..dd5baced 100644
--- a/app/src/main/java/me/impy/aegis/helpers/AuthHelper.java
+++ b/app/src/main/java/me/impy/aegis/helpers/EditTextHelper.java
@@ -7,19 +7,11 @@ import java.util.Arrays;
import me.impy.aegis.crypto.CryptoUtils;
-public class AuthHelper {
- private AuthHelper() {
+public class EditTextHelper {
+ private EditTextHelper() {
}
- public static char[] getPassword(EditText text, boolean clear) {
- char[] password = getEditTextChars(text);
- if (clear) {
- clearPassword(text);
- }
- return password;
- }
-
- public static void clearPassword(EditText text) {
+ public static void clearEditText(EditText text) {
text.getText().clear();
}
@@ -30,12 +22,9 @@ public class AuthHelper {
return chars;
}
- public static boolean arePasswordsEqual(EditText text1, EditText text2) {
+ public static boolean areEditTextsEqual(EditText text1, EditText text2) {
char[] password = getEditTextChars(text1);
char[] passwordConfirm = getEditTextChars(text2);
- boolean equal = password.length != 0 && Arrays.equals(password, passwordConfirm);
- CryptoUtils.zero(password);
- CryptoUtils.zero(passwordConfirm);
- return equal;
+ return password.length != 0 && Arrays.equals(password, passwordConfirm);
}
}
diff --git a/app/src/main/java/me/impy/aegis/helpers/FingerprintHelper.java b/app/src/main/java/me/impy/aegis/helpers/FingerprintHelper.java
index 20fa7fab..12cd200e 100644
--- a/app/src/main/java/me/impy/aegis/helpers/FingerprintHelper.java
+++ b/app/src/main/java/me/impy/aegis/helpers/FingerprintHelper.java
@@ -11,14 +11,16 @@ public class FingerprintHelper {
}
public static FingerprintManager getManager(Context context) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- if (PermissionHelper.granted(context, Manifest.permission.USE_FINGERPRINT)) {
- FingerprintManager manager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
- if (manager != null && manager.isHardwareDetected() && manager.hasEnrolledFingerprints()) {
- return manager;
- }
+ if (isSupported() && PermissionHelper.granted(context, Manifest.permission.USE_FINGERPRINT)) {
+ FingerprintManager manager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
+ if (manager != null && manager.isHardwareDetected() && manager.hasEnrolledFingerprints()) {
+ return manager;
}
}
return null;
}
+
+ public static boolean isSupported() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
+ }
}
diff --git a/app/src/main/java/me/impy/aegis/importers/AegisImporter.java b/app/src/main/java/me/impy/aegis/importers/AegisImporter.java
index 94ba2051..9f42ce67 100644
--- a/app/src/main/java/me/impy/aegis/importers/AegisImporter.java
+++ b/app/src/main/java/me/impy/aegis/importers/AegisImporter.java
@@ -4,6 +4,7 @@ import java.util.List;
import me.impy.aegis.db.Database;
import me.impy.aegis.db.DatabaseEntry;
+import me.impy.aegis.db.DatabaseFile;
import me.impy.aegis.util.ByteInputStream;
public class AegisImporter extends DatabaseImporter {
@@ -15,8 +16,10 @@ public class AegisImporter extends DatabaseImporter {
@Override
public List convert() throws Exception {
byte[] bytes = _stream.getBytes();
+ DatabaseFile file = new DatabaseFile();
+ file.deserialize(bytes);
Database db = new Database();
- db.deserialize(bytes, false);
+ db.deserialize(file.getContent(), false);
return db.getKeys();
}
diff --git a/app/src/main/java/me/impy/aegis/AegisActivity.java b/app/src/main/java/me/impy/aegis/ui/AegisActivity.java
similarity index 90%
rename from app/src/main/java/me/impy/aegis/AegisActivity.java
rename to app/src/main/java/me/impy/aegis/ui/AegisActivity.java
index f7b96355..709bd1da 100644
--- a/app/src/main/java/me/impy/aegis/AegisActivity.java
+++ b/app/src/main/java/me/impy/aegis/ui/AegisActivity.java
@@ -1,8 +1,10 @@
-package me.impy.aegis;
+package me.impy.aegis.ui;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
+import me.impy.aegis.AegisApplication;
+
public abstract class AegisActivity extends AppCompatActivity {
private AegisApplication _app;
diff --git a/app/src/main/java/me/impy/aegis/AuthActivity.java b/app/src/main/java/me/impy/aegis/ui/AuthActivity.java
similarity index 71%
rename from app/src/main/java/me/impy/aegis/AuthActivity.java
rename to app/src/main/java/me/impy/aegis/ui/AuthActivity.java
index acb062ec..9bf4b21c 100644
--- a/app/src/main/java/me/impy/aegis/AuthActivity.java
+++ b/app/src/main/java/me/impy/aegis/ui/AuthActivity.java
@@ -1,6 +1,5 @@
-package me.impy.aegis;
+package me.impy.aegis.ui;
-import android.content.DialogInterface;
import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
@@ -20,20 +19,19 @@ import java.lang.reflect.UndeclaredThrowableException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
+import me.impy.aegis.R;
import me.impy.aegis.crypto.KeyStoreHandle;
import me.impy.aegis.crypto.MasterKey;
-import me.impy.aegis.crypto.slots.FingerprintSlot;
-import me.impy.aegis.crypto.slots.PasswordSlot;
-import me.impy.aegis.crypto.slots.Slot;
-import me.impy.aegis.crypto.slots.SlotCollection;
+import me.impy.aegis.db.slots.FingerprintSlot;
+import me.impy.aegis.db.slots.PasswordSlot;
+import me.impy.aegis.db.slots.Slot;
+import me.impy.aegis.db.slots.SlotCollection;
import me.impy.aegis.helpers.FingerprintHelper;
import me.impy.aegis.helpers.FingerprintUiHelper;
-import me.impy.aegis.helpers.AuthHelper;
+import me.impy.aegis.helpers.EditTextHelper;
+import me.impy.aegis.ui.tasks.SlotCollectionTask;
public class AuthActivity extends AegisActivity implements FingerprintUiHelper.Callback, SlotCollectionTask.Callback {
- public static final int RESULT_OK = 0;
- public static final int RESULT_EXCEPTION = 1;
-
private EditText _textPassword;
private SlotCollection _slots;
@@ -46,6 +44,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
setContentView(R.layout.activity_auth);
_textPassword = findViewById(R.id.text_password);
LinearLayout boxFingerprint = findViewById(R.id.box_fingerprint);
+ LinearLayout boxFingerprintInfo = findViewById(R.id.box_fingerprint_info);
TextView textFingerprint = findViewById(R.id.text_fingerprint);
SwirlView imgFingerprint = null;
@@ -61,24 +60,41 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
// only show the fingerprint controls if the api version is new enough, permission is granted, a scanner is found and a fingerprint slot is found
FingerprintManager manager = FingerprintHelper.getManager(this);
if (manager != null && _slots.has(FingerprintSlot.class)) {
+ boolean invalidated = false;
try {
- KeyStoreHandle handle = new KeyStoreHandle();
- if (handle.keyExists()) {
- SecretKey key = handle.getKey();
- _fingerCipher = Slot.createCipher(key, Cipher.DECRYPT_MODE);
- _fingerHelper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
- boxFingerprint.setVisibility(View.VISIBLE);
+ // find a fingerprint slot with an id that matches an alias in the keystore
+ for (FingerprintSlot slot : _slots.findAll(FingerprintSlot.class)) {
+ String id = slot.getID();
+ KeyStoreHandle handle = new KeyStoreHandle();
+ if (handle.containsKey(id)) {
+ SecretKey key = handle.getKey(id);
+ // if 'key' is null, it was permanently invalidated
+ if (key == null) {
+ invalidated = true;
+ continue;
+ }
+ _fingerCipher = Slot.createCipher(key, Cipher.DECRYPT_MODE);
+ _fingerHelper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
+ boxFingerprint.setVisibility(View.VISIBLE);
+ invalidated = false;
+ break;
+ }
}
} catch (Exception e) {
throw new UndeclaredThrowableException(e);
}
+
+ // display a help message if a matching invalidated keystore entry was found
+ if (invalidated) {
+ boxFingerprintInfo.setVisibility(View.VISIBLE);
+ }
}
Button button = findViewById(R.id.button_decrypt);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- char[] password = AuthHelper.getPassword(_textPassword, true);
+ char[] password = EditTextHelper.getEditTextChars(_textPassword);
trySlots(PasswordSlot.class, password);
}
});
@@ -98,12 +114,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
builder.setTitle("Decryption error");
builder.setMessage("Master key integrity check failed for every slot. Make sure you didn't mistype your password.");
builder.setCancelable(false);
- builder.setPositiveButton("OK",
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int id) {
- dialog.cancel();
- }
- });
+ builder.setPositiveButton(android.R.string.ok, null);
builder.create().show();
}
diff --git a/app/src/main/java/me/impy/aegis/EditProfileActivity.java b/app/src/main/java/me/impy/aegis/ui/EditProfileActivity.java
similarity index 97%
rename from app/src/main/java/me/impy/aegis/EditProfileActivity.java
rename to app/src/main/java/me/impy/aegis/ui/EditProfileActivity.java
index cc2921d1..e556317e 100644
--- a/app/src/main/java/me/impy/aegis/EditProfileActivity.java
+++ b/app/src/main/java/me/impy/aegis/ui/EditProfileActivity.java
@@ -1,4 +1,4 @@
-package me.impy.aegis;
+package me.impy.aegis.ui;
import android.content.Intent;
import android.os.Bundle;
@@ -18,14 +18,15 @@ import android.widget.Spinner;
import com.amulyakhare.textdrawable.TextDrawable;
-import me.impy.aegis.crypto.CryptoUtils;
+import me.impy.aegis.R;
import me.impy.aegis.crypto.KeyInfo;
import me.impy.aegis.crypto.KeyInfoException;
import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.encoding.Base32;
-import me.impy.aegis.helpers.AuthHelper;
+import me.impy.aegis.helpers.EditTextHelper;
import me.impy.aegis.helpers.SpinnerHelper;
import me.impy.aegis.helpers.TextDrawableHelper;
+import me.impy.aegis.ui.views.KeyProfile;
public class EditProfileActivity extends AegisActivity {
private boolean _isNew = false;
@@ -217,9 +218,8 @@ public class EditProfileActivity extends AegisActivity {
KeyInfo info = entry.getInfo();
try {
- char[] secret = AuthHelper.getEditTextChars(_textSecret);
+ char[] secret = EditTextHelper.getEditTextChars(_textSecret);
info.setSecret(secret);
- CryptoUtils.zero(secret);
info.setIssuer(_textIssuer.getText().toString());
info.setPeriod(period);
info.setDigits(digits);
diff --git a/app/src/main/java/me/impy/aegis/IntroActivity.java b/app/src/main/java/me/impy/aegis/ui/IntroActivity.java
similarity index 91%
rename from app/src/main/java/me/impy/aegis/IntroActivity.java
rename to app/src/main/java/me/impy/aegis/ui/IntroActivity.java
index 115fc5a9..6ac33e8d 100644
--- a/app/src/main/java/me/impy/aegis/IntroActivity.java
+++ b/app/src/main/java/me/impy/aegis/ui/IntroActivity.java
@@ -1,4 +1,4 @@
-package me.impy.aegis;
+package me.impy.aegis.ui;
import android.Manifest;
import android.content.Intent;
@@ -9,18 +9,25 @@ import com.github.paolorotolo.appintro.AppIntro;
import com.github.paolorotolo.appintro.AppIntroFragment;
import com.github.paolorotolo.appintro.model.SliderPage;
+import org.json.JSONObject;
+
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
+import me.impy.aegis.AegisApplication;
+import me.impy.aegis.R;
import me.impy.aegis.crypto.CryptResult;
import me.impy.aegis.crypto.MasterKey;
-import me.impy.aegis.crypto.slots.FingerprintSlot;
-import me.impy.aegis.crypto.slots.PasswordSlot;
-import me.impy.aegis.crypto.slots.Slot;
-import me.impy.aegis.crypto.slots.SlotCollection;
+import me.impy.aegis.db.slots.FingerprintSlot;
+import me.impy.aegis.db.slots.PasswordSlot;
+import me.impy.aegis.db.slots.Slot;
+import me.impy.aegis.db.slots.SlotCollection;
import me.impy.aegis.db.Database;
import me.impy.aegis.db.DatabaseFile;
import me.impy.aegis.db.DatabaseManager;
+import me.impy.aegis.ui.slides.CustomAuthenticatedSlide;
+import me.impy.aegis.ui.slides.CustomAuthenticationSlide;
+import me.impy.aegis.ui.tasks.DerivationTask;
public class IntroActivity extends AppIntro implements DerivationTask.Callback {
public static final int RESULT_OK = 0;
@@ -159,7 +166,7 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback {
try {
// encrypt the master key with the fingerprint key
// and add it to the list of slots
- FingerprintSlot slot = new FingerprintSlot();
+ FingerprintSlot slot = _authenticatedSlide.getFingerSlot();
Cipher cipher = _authenticatedSlide.getFingerCipher();
slots.encrypt(slot, masterKey, cipher);
slots.add(slot);
@@ -171,13 +178,11 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback {
// finally, save the database
try {
- byte[] bytes = _database.serialize();
+ JSONObject obj = _database.serialize();
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
- _databaseFile.setContent(bytes);
+ _databaseFile.setContent(obj);
} else {
- CryptResult result = masterKey.encrypt(bytes);
- _databaseFile.setContent(result.Data);
- _databaseFile.setCryptParameters(result.Parameters);
+ _databaseFile.setContent(obj, masterKey);
}
DatabaseManager.save(getApplicationContext(), _databaseFile);
} catch (Exception e) {
diff --git a/app/src/main/java/me/impy/aegis/MainActivity.java b/app/src/main/java/me/impy/aegis/ui/MainActivity.java
similarity index 92%
rename from app/src/main/java/me/impy/aegis/MainActivity.java
rename to app/src/main/java/me/impy/aegis/ui/MainActivity.java
index 494df506..536f4978 100644
--- a/app/src/main/java/me/impy/aegis/MainActivity.java
+++ b/app/src/main/java/me/impy/aegis/ui/MainActivity.java
@@ -1,4 +1,4 @@
-package me.impy.aegis;
+package me.impy.aegis.ui;
import android.Manifest;
import android.content.ClipData;
@@ -25,11 +25,16 @@ import java.io.InputStream;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.List;
+import me.impy.aegis.AegisApplication;
+import me.impy.aegis.R;
import me.impy.aegis.crypto.MasterKey;
+import me.impy.aegis.db.slots.SlotCollection;
import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.db.DatabaseManager;
import me.impy.aegis.helpers.PermissionHelper;
import me.impy.aegis.importers.DatabaseImporter;
+import me.impy.aegis.ui.views.KeyProfile;
+import me.impy.aegis.ui.views.KeyProfileView;
import me.impy.aegis.util.ByteInputStream;
public class MainActivity extends AegisActivity implements KeyProfileView.Listener {
@@ -42,6 +47,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
private static final int CODE_DECRYPT = 5;
private static final int CODE_IMPORT = 6;
private static final int CODE_PREFERENCES = 7;
+ private static final int CODE_SLOTS = 8;
// permission request codes
private static final int CODE_PERM_EXPORT = 0;
@@ -183,6 +189,8 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
case CODE_PREFERENCES:
onPreferencesResult(resultCode, data);
break;
+ case CODE_SLOTS:
+ onSlotManagerResult(resultCode, data);
}
}
@@ -206,6 +214,16 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
}
}
+ private void onSlotManagerResult(int resultCode, Intent data) {
+ if (resultCode != RESULT_OK) {
+ return;
+ }
+
+ SlotCollection slots = (SlotCollection) data.getSerializableExtra("slots");
+ _db.getFile().setSlots(slots);
+ saveDatabase();
+ }
+
private void onPreferencesResult(int resultCode, Intent data) {
// refresh the entire key profile list if needed
if (data.getBooleanExtra("needsRefresh", false)) {
@@ -216,9 +234,28 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
// perform any pending actions
int action = data.getIntExtra("action", -1);
switch (action) {
+ case PreferencesActivity.ACTION_IMPORT:
+ if (PermissionHelper.request(this, CODE_PERM_IMPORT, Manifest.permission.READ_EXTERNAL_STORAGE)) {
+ onImport();
+ }
+ break;
case PreferencesActivity.ACTION_EXPORT:
onExport();
break;
+ case PreferencesActivity.ACTION_SLOTS:
+ MasterKey masterKey;
+ try {
+ masterKey = _db.getMasterKey();
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "An error occurred while trying to obtain the database key", Toast.LENGTH_SHORT).show();
+ break;
+ }
+ Intent intent = new Intent(this, SlotManagerActivity.class);
+ intent.putExtra("masterKey", masterKey);
+ intent.putExtra("slots", _db.getFile().getSlots());
+ startActivityForResult(intent, CODE_SLOTS);
+ break;
}
}
@@ -392,8 +429,6 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
}
private void addKey(KeyProfile profile) {
- profile.refreshCode();
-
DatabaseEntry entry = profile.getEntry();
entry.setName(entry.getInfo().getAccountName());
try {
@@ -485,6 +520,9 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
setPreferredTheme(nightMode);
recreate();
}
+
+ // refresh all codes to prevent showing old ones
+ _keyProfileView.refresh();
}
private BottomSheetDialog createBottomSheet(final KeyProfile profile) {
@@ -553,13 +591,9 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
- Intent preferencesActivity = new Intent(this, PreferencesActivity.class);
- startActivityForResult(preferencesActivity, CODE_PREFERENCES);
- return true;
- case R.id.action_import:
- if (PermissionHelper.request(this, CODE_PERM_IMPORT, Manifest.permission.CAMERA)) {
- onImport();
- }
+ Intent intent = new Intent(this, PreferencesActivity.class);
+ intent.putExtra("encrypted", _db.getFile().isEncrypted());
+ startActivityForResult(intent, CODE_PREFERENCES);
return true;
case R.id.action_lock:
_keyProfileView.clearKeys();
diff --git a/app/src/main/java/me/impy/aegis/PreferencesActivity.java b/app/src/main/java/me/impy/aegis/ui/PreferencesActivity.java
similarity index 55%
rename from app/src/main/java/me/impy/aegis/PreferencesActivity.java
rename to app/src/main/java/me/impy/aegis/ui/PreferencesActivity.java
index 25be06ce..f7717a86 100644
--- a/app/src/main/java/me/impy/aegis/PreferencesActivity.java
+++ b/app/src/main/java/me/impy/aegis/ui/PreferencesActivity.java
@@ -1,20 +1,26 @@
-package me.impy.aegis;
+package me.impy.aegis.ui;
import android.content.Intent;
import android.os.Bundle;
+import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
-import android.support.v7.app.AppCompatActivity;
import android.widget.Toast;
+import me.impy.aegis.R;
+
public class PreferencesActivity extends AegisActivity {
- public static final int ACTION_EXPORT = 0;
+ public static final int ACTION_IMPORT = 0;
+ public static final int ACTION_EXPORT = 1;
+ public static final int ACTION_SLOTS = 2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- getFragmentManager().beginTransaction().replace(android.R.id.content, new PreferencesFragment()).commit();
+ PreferencesFragment fragment = new PreferencesFragment();
+ fragment.setArguments(getIntent().getExtras());
+ getFragmentManager().beginTransaction().replace(android.R.id.content, fragment).commit();
}
@Override
@@ -55,8 +61,18 @@ public class PreferencesActivity extends AegisActivity {
}
});
- Preference exportPreference = findPreference("pref_export");
+ Preference exportPreference = findPreference("pref_import");
exportPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ _result.putExtra("action", ACTION_IMPORT);
+ finish();
+ return true;
+ }
+ });
+
+ Preference importPreference = findPreference("pref_export");
+ importPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
_result.putExtra("action", ACTION_EXPORT);
@@ -65,6 +81,27 @@ public class PreferencesActivity extends AegisActivity {
}
});
+ Preference slotsPreference = findPreference("pref_slots");
+ slotsPreference.setEnabled(getArguments().getBoolean("encrypted"));
+ slotsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ _result.putExtra("action", ACTION_SLOTS);
+ finish();
+ return true;
+ }
+ });
+
+ EditTextPreference timeoutPreference = (EditTextPreference) findPreference("pref_timeout");
+ timeoutPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ preference.setSummary(String.format(getString(R.string.pref_timeout_summary), (String) newValue));
+ return true;
+ }
+ });
+ timeoutPreference.getOnPreferenceChangeListener().onPreferenceChange(timeoutPreference, timeoutPreference.getText());
+
Preference issuerPreference = findPreference("pref_issuer");
issuerPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
diff --git a/app/src/main/java/me/impy/aegis/ScannerActivity.java b/app/src/main/java/me/impy/aegis/ui/ScannerActivity.java
similarity index 96%
rename from app/src/main/java/me/impy/aegis/ScannerActivity.java
rename to app/src/main/java/me/impy/aegis/ui/ScannerActivity.java
index a7c0bbc6..0e1128b3 100644
--- a/app/src/main/java/me/impy/aegis/ScannerActivity.java
+++ b/app/src/main/java/me/impy/aegis/ui/ScannerActivity.java
@@ -1,4 +1,4 @@
-package me.impy.aegis;
+package me.impy.aegis.ui;
import android.app.Activity;
import android.content.Context;
@@ -16,6 +16,7 @@ import me.dm7.barcodescanner.zxing.ZXingScannerView;
import me.impy.aegis.crypto.KeyInfo;
import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.helpers.SquareFinderView;
+import me.impy.aegis.ui.views.KeyProfile;
public class ScannerActivity extends AegisActivity implements ZXingScannerView.ResultHandler {
private ZXingScannerView _scannerView;
diff --git a/app/src/main/java/me/impy/aegis/ui/SlotManagerActivity.java b/app/src/main/java/me/impy/aegis/ui/SlotManagerActivity.java
new file mode 100644
index 00000000..888183fb
--- /dev/null
+++ b/app/src/main/java/me/impy/aegis/ui/SlotManagerActivity.java
@@ -0,0 +1,184 @@
+package me.impy.aegis.ui;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Toast;
+
+import javax.crypto.Cipher;
+
+import me.impy.aegis.R;
+import me.impy.aegis.crypto.KeyStoreHandle;
+import me.impy.aegis.crypto.KeyStoreHandleException;
+import me.impy.aegis.crypto.MasterKey;
+import me.impy.aegis.db.slots.FingerprintSlot;
+import me.impy.aegis.db.slots.PasswordSlot;
+import me.impy.aegis.db.slots.Slot;
+import me.impy.aegis.db.slots.SlotCollection;
+import me.impy.aegis.helpers.FingerprintHelper;
+import me.impy.aegis.ui.dialogs.FingerprintDialogFragment;
+import me.impy.aegis.ui.dialogs.PasswordDialogFragment;
+import me.impy.aegis.ui.views.SlotAdapter;
+import me.impy.aegis.ui.dialogs.SlotDialogFragment;
+
+public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener, SlotDialogFragment.Listener {
+ private MasterKey _masterKey;
+ private SlotCollection _slots;
+ private SlotAdapter _adapter;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // set up the view
+ setContentView(R.layout.activity_slots);
+ setSupportActionBar(findViewById(R.id.toolbar));
+ ActionBar bar = getSupportActionBar();
+ bar.setHomeAsUpIndicator(R.drawable.ic_close);
+ bar.setDisplayHomeAsUpEnabled(true);
+
+ findViewById(R.id.button_add_fingerprint).setOnClickListener(view -> {
+ FingerprintDialogFragment dialog = new FingerprintDialogFragment();
+ dialog.show(getSupportFragmentManager(), null);
+ });
+
+ findViewById(R.id.button_add_password).setOnClickListener(view -> {
+ PasswordDialogFragment dialog = new PasswordDialogFragment();
+ dialog.show(getSupportFragmentManager(), null);
+ });
+
+ // set up the recycler view
+ _adapter = new SlotAdapter(this);
+ RecyclerView slotsView = findViewById(R.id.list_slots);
+ LinearLayoutManager layoutManager = new LinearLayoutManager(this);
+ slotsView.setLayoutManager(layoutManager);
+ slotsView.setAdapter(_adapter);
+ slotsView.setNestedScrollingEnabled(false);
+
+ // load the slots and masterKey
+ _masterKey = (MasterKey) getIntent().getSerializableExtra("masterKey");
+ _slots = (SlotCollection) getIntent().getSerializableExtra("slots");
+ for (Slot slot : _slots) {
+ _adapter.addSlot(slot);
+ }
+
+ updateFingerprintButton();
+ }
+
+ private void updateFingerprintButton() {
+ // only show the fingerprint option if we can get an instance of the fingerprint manager
+ // and if none of the slots in the collection has a matching alias in the keystore
+ int visibility = View.VISIBLE;
+ if (FingerprintHelper.isSupported()) {
+ try {
+ KeyStoreHandle keyStore = new KeyStoreHandle();
+ for (FingerprintSlot slot : _slots.findAll(FingerprintSlot.class)) {
+ if (keyStore.containsKey(slot.getID()) && FingerprintHelper.getManager(this) != null) {
+ visibility = View.GONE;
+ break;
+ }
+ }
+ } catch (KeyStoreHandleException e) {
+ visibility = View.GONE;
+ }
+ } else {
+ visibility = View.GONE;
+ }
+ findViewById(R.id.button_add_fingerprint).setVisibility(visibility);
+ }
+
+ private boolean onSave() {
+ Intent intent = new Intent();
+ intent.putExtra("slots", _slots);
+ setResult(RESULT_OK, intent);
+ finish();
+ return true;
+ }
+
+ @Override
+ protected void setPreferredTheme(boolean nightMode) {
+ if (nightMode) {
+ setTheme(R.style.AppTheme_Dark_NoActionBar);
+ } else {
+ setTheme(R.style.AppTheme_Default_NoActionBar);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+ case R.id.action_save:
+ return onSave();
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_slots, menu);
+ return true;
+ }
+
+ @Override
+ public void onEditSlot(Slot slot) {
+ /*EditText textName = new EditText(this);
+ textName.setHint("Name");
+
+ new AlertDialog.Builder(this)
+ .setTitle("Edit slot name")
+ .setView(textName)
+ .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
+ String name = textName.getText().toString();
+ })
+ .setNegativeButton(android.R.string.cancel, null)
+ .show();*/
+ }
+
+ @Override
+ public void onRemoveSlot(Slot slot) {
+ if (slot instanceof PasswordSlot && _slots.findAll(PasswordSlot.class).size() <= 1) {
+ Toast.makeText(this, "You must have at least one password slot", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ new AlertDialog.Builder(this)
+ .setTitle("Remove slot")
+ .setMessage("Are you sure you want to remove this slot?")
+ .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
+ _slots.remove(slot);
+ _adapter.removeSlot(slot);
+ updateFingerprintButton();
+ })
+ .setNegativeButton(android.R.string.no, null)
+ .show();
+ }
+
+ @Override
+ public void onSlotResult(Slot slot, Cipher cipher) {
+ try {
+ _slots.encrypt(slot, _masterKey, cipher);
+ } catch (Exception e) {
+ onException(e);
+ return;
+ }
+
+ _slots.add(slot);
+ _adapter.addSlot(slot);
+ updateFingerprintButton();
+ }
+
+ @Override
+ public void onException(Exception e) {
+ Toast.makeText(this, "An error occurred while trying to add a new slot: " + e.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+}
diff --git a/app/src/main/java/me/impy/aegis/ui/dialogs/FingerprintDialogFragment.java b/app/src/main/java/me/impy/aegis/ui/dialogs/FingerprintDialogFragment.java
new file mode 100644
index 00000000..d1ac0be4
--- /dev/null
+++ b/app/src/main/java/me/impy/aegis/ui/dialogs/FingerprintDialogFragment.java
@@ -0,0 +1,80 @@
+package me.impy.aegis.ui.dialogs;
+
+import android.app.Dialog;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+import android.widget.TextView;
+
+import com.mattprecious.swirl.SwirlView;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+
+import me.impy.aegis.R;
+import me.impy.aegis.crypto.KeyStoreHandle;
+import me.impy.aegis.db.slots.FingerprintSlot;
+import me.impy.aegis.db.slots.Slot;
+import me.impy.aegis.helpers.FingerprintHelper;
+import me.impy.aegis.helpers.FingerprintUiHelper;
+
+public class FingerprintDialogFragment extends SlotDialogFragment implements FingerprintUiHelper.Callback {
+ private Cipher _cipher;
+ private FingerprintUiHelper _helper;
+ private FingerprintSlot _slot;
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ View view = getActivity().getLayoutInflater().inflate(R.layout.dialog_fingerprint, null);
+ TextView textFingerprint = view.findViewById(R.id.text_fingerprint);
+ SwirlView imgFingerprint = view.findViewById(R.id.img_fingerprint);
+
+ FingerprintManager manager = FingerprintHelper.getManager(getContext());
+ try {
+ _slot = new FingerprintSlot();
+ SecretKey key = new KeyStoreHandle().generateKey(_slot.getID());
+ _cipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE);
+ _helper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ return new AlertDialog.Builder(getActivity())
+ .setTitle("Register a new fingerprint")
+ .setView(view)
+ .setNegativeButton(android.R.string.cancel, null)
+ .create();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (_helper != null) {
+ _helper.startListening(new FingerprintManager.CryptoObject(_cipher));
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ if (_helper != null) {
+ _helper.stopListening();
+ }
+ }
+
+ @Override
+ public void onAuthenticated() {
+ getListener().onSlotResult(_slot, _cipher);
+ dismiss();
+ }
+
+ @Override
+ public void onError() {
+
+ }
+}
diff --git a/app/src/main/java/me/impy/aegis/ui/dialogs/PasswordDialogFragment.java b/app/src/main/java/me/impy/aegis/ui/dialogs/PasswordDialogFragment.java
new file mode 100644
index 00000000..aa105a44
--- /dev/null
+++ b/app/src/main/java/me/impy/aegis/ui/dialogs/PasswordDialogFragment.java
@@ -0,0 +1,82 @@
+package me.impy.aegis.ui.dialogs;
+
+import android.app.Dialog;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AlertDialog;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+
+import javax.crypto.Cipher;
+
+import me.impy.aegis.R;
+import me.impy.aegis.db.slots.PasswordSlot;
+import me.impy.aegis.db.slots.Slot;
+import me.impy.aegis.helpers.EditTextHelper;
+import me.impy.aegis.ui.tasks.DerivationTask;
+
+public class PasswordDialogFragment extends SlotDialogFragment {
+ private Button _buttonOK;
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ View view = getActivity().getLayoutInflater().inflate(R.layout.dialog_password, null);
+ EditText textPassword = view.findViewById(R.id.text_password);
+ EditText textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
+
+ AlertDialog alert = new AlertDialog.Builder(getActivity())
+ .setTitle("Enter a new password")
+ .setView(view)
+ .setPositiveButton(android.R.string.ok, null)
+ .setNegativeButton(android.R.string.cancel, null)
+ .create();
+
+ alert.setOnShowListener(dialog -> {
+ _buttonOK = alert.getButton(AlertDialog.BUTTON_POSITIVE);
+ _buttonOK.setEnabled(false);
+
+ // replace the default listener
+ _buttonOK.setOnClickListener(v -> {
+ if (!EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm)) {
+ return;
+ }
+
+ char[] password = EditTextHelper.getEditTextChars(textPassword);
+ PasswordSlot slot = new PasswordSlot();
+ DerivationTask task = new DerivationTask(getContext(), key -> {
+ Cipher cipher;
+ try {
+ cipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE);
+ } catch (Exception e) {
+ getListener().onException(e);
+ dialog.cancel();
+ return;
+ }
+ getListener().onSlotResult(slot, cipher);
+ dialog.dismiss();
+ });
+ task.execute(new DerivationTask.Params() {{
+ Slot = slot;
+ Password = password;
+ }});
+ });
+ });
+
+ TextWatcher watcher = new TextWatcher() {
+ public void onTextChanged(CharSequence c, int start, int before, int count) {
+ boolean equal = EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm);
+ _buttonOK.setEnabled(equal);
+ }
+ public void beforeTextChanged(CharSequence c, int start, int count, int after) { }
+ public void afterTextChanged(Editable c) { }
+ };
+ textPassword.addTextChangedListener(watcher);
+ textPasswordConfirm.addTextChangedListener(watcher);
+
+ return alert;
+ }
+}
diff --git a/app/src/main/java/me/impy/aegis/ui/dialogs/SlotDialogFragment.java b/app/src/main/java/me/impy/aegis/ui/dialogs/SlotDialogFragment.java
new file mode 100644
index 00000000..cc5972ac
--- /dev/null
+++ b/app/src/main/java/me/impy/aegis/ui/dialogs/SlotDialogFragment.java
@@ -0,0 +1,32 @@
+package me.impy.aegis.ui.dialogs;
+
+import android.content.Context;
+import android.support.v4.app.DialogFragment;
+
+import javax.crypto.Cipher;
+
+import me.impy.aegis.db.slots.Slot;
+
+public class SlotDialogFragment extends DialogFragment {
+ private Listener _listener;
+
+ protected Listener getListener() {
+ return _listener;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+
+ try {
+ _listener = (Listener) context;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(context.toString() + " must implement SlotDialogFragment.Listener");
+ }
+ }
+
+ public interface Listener {
+ void onSlotResult(Slot slot, Cipher cipher);
+ void onException(Exception e);
+ }
+}
diff --git a/app/src/main/java/me/impy/aegis/CustomAuthenticatedSlide.java b/app/src/main/java/me/impy/aegis/ui/slides/CustomAuthenticatedSlide.java
similarity index 87%
rename from app/src/main/java/me/impy/aegis/CustomAuthenticatedSlide.java
rename to app/src/main/java/me/impy/aegis/ui/slides/CustomAuthenticatedSlide.java
index 9ad940aa..6146f6b0 100644
--- a/app/src/main/java/me/impy/aegis/CustomAuthenticatedSlide.java
+++ b/app/src/main/java/me/impy/aegis/ui/slides/CustomAuthenticatedSlide.java
@@ -1,4 +1,4 @@
-package me.impy.aegis;
+package me.impy.aegis.ui.slides;
import android.content.Context;
import android.content.Intent;
@@ -23,10 +23,12 @@ import java.lang.reflect.UndeclaredThrowableException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
+import me.impy.aegis.R;
import me.impy.aegis.crypto.KeyStoreHandle;
-import me.impy.aegis.crypto.slots.Slot;
+import me.impy.aegis.db.slots.FingerprintSlot;
+import me.impy.aegis.db.slots.Slot;
import me.impy.aegis.helpers.FingerprintUiHelper;
-import me.impy.aegis.helpers.AuthHelper;
+import me.impy.aegis.helpers.EditTextHelper;
public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiHelper.Callback, ISlidePolicy, ISlideSelectionListener {
private int _cryptType;
@@ -39,6 +41,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
private TextView _textFingerprint;
private FingerprintUiHelper _fingerHelper;
private KeyStoreHandle _storeHandle;
+ private FingerprintSlot _fingerSlot;
private Cipher _fingerCipher;
private boolean _fingerAuthenticated;
@@ -65,14 +68,17 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
}
public char[] getPassword() {
- AuthHelper.clearPassword(_textPasswordConfirm);
- return AuthHelper.getPassword(_textPassword, true);
+ return EditTextHelper.getEditTextChars(_textPassword);
}
public Cipher getFingerCipher() {
return _fingerCipher;
}
+ public FingerprintSlot getFingerSlot() {
+ return _fingerSlot;
+ }
+
public void setBgColor(int color) {
_bgColor = color;
}
@@ -93,13 +99,9 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
try {
if (_storeHandle == null) {
_storeHandle = new KeyStoreHandle();
+ _fingerSlot = new FingerprintSlot();
}
- // TODO: consider regenerating the key here if it already exists
- if (!_storeHandle.keyExists()) {
- key = _storeHandle.generateKey(true);
- } else {
- key = _storeHandle.getKey();
- }
+ key = _storeHandle.generateKey(_fingerSlot.getID());
} catch (Exception e) {
throw new UndeclaredThrowableException(e);
}
@@ -141,7 +143,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
}
// intentional fallthrough
case CustomAuthenticationSlide.CRYPT_TYPE_PASS:
- return AuthHelper.arePasswordsEqual(_textPassword, _textPasswordConfirm);
+ return EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm);
default:
throw new RuntimeException();
}
@@ -150,7 +152,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
@Override
public void onUserIllegallyRequestedNextPage() {
String message;
- if (!AuthHelper.arePasswordsEqual(_textPassword, _textPasswordConfirm)) {
+ if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
message = "Passwords should be equal and non-empty";
} else if (!_fingerAuthenticated) {
message = "Register your fingerprint";
@@ -160,7 +162,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
View view = getView();
if (view != null) {
- Snackbar snackbar = Snackbar.make(getView(), message, Snackbar.LENGTH_LONG);
+ Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
snackbar.show();
}
}
diff --git a/app/src/main/java/me/impy/aegis/CustomAuthenticationSlide.java b/app/src/main/java/me/impy/aegis/ui/slides/CustomAuthenticationSlide.java
similarity index 98%
rename from app/src/main/java/me/impy/aegis/CustomAuthenticationSlide.java
rename to app/src/main/java/me/impy/aegis/ui/slides/CustomAuthenticationSlide.java
index fd3742d8..bf60c036 100644
--- a/app/src/main/java/me/impy/aegis/CustomAuthenticationSlide.java
+++ b/app/src/main/java/me/impy/aegis/ui/slides/CustomAuthenticationSlide.java
@@ -1,4 +1,4 @@
-package me.impy.aegis;
+package me.impy.aegis.ui.slides;
import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
@@ -14,6 +14,7 @@ import android.widget.TextView;
import com.github.paolorotolo.appintro.ISlidePolicy;
+import me.impy.aegis.R;
import me.impy.aegis.helpers.FingerprintHelper;
public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy, RadioGroup.OnCheckedChangeListener {
diff --git a/app/src/main/java/me/impy/aegis/DerivationTask.java b/app/src/main/java/me/impy/aegis/ui/tasks/DerivationTask.java
similarity index 87%
rename from app/src/main/java/me/impy/aegis/DerivationTask.java
rename to app/src/main/java/me/impy/aegis/ui/tasks/DerivationTask.java
index cc446920..70d458d5 100644
--- a/app/src/main/java/me/impy/aegis/DerivationTask.java
+++ b/app/src/main/java/me/impy/aegis/ui/tasks/DerivationTask.java
@@ -1,4 +1,4 @@
-package me.impy.aegis;
+package me.impy.aegis.ui.tasks;
import android.content.Context;
import android.os.Process;
@@ -6,7 +6,7 @@ import android.os.Process;
import javax.crypto.SecretKey;
import me.impy.aegis.crypto.CryptoUtils;
-import me.impy.aegis.crypto.slots.PasswordSlot;
+import me.impy.aegis.db.slots.PasswordSlot;
public class DerivationTask extends ProgressDialogTask {
private Callback _cb;
@@ -24,7 +24,6 @@ public class DerivationTask extends ProgressDialogTask extends ProgressDialogTask {
private Callback _cb;
@@ -42,7 +41,6 @@ public class SlotCollectionTask extends ProgressDialogTask extends ProgressDialogTask implements ItemTouchHelperAdapter {
@@ -54,6 +55,13 @@ public class KeyProfileAdapter extends RecyclerView.Adapter im
notifyItemChanged(position);
}
+ public void refresh() {
+ for (KeyProfile profile : _keyProfiles) {
+ profile.refreshCode();
+ }
+ notifyDataSetChanged();
+ }
+
private KeyProfile getKeyByID(long id) {
for (KeyProfile profile : _keyProfiles) {
if (profile.getEntry().getID() == id) {
@@ -99,7 +107,7 @@ public class KeyProfileAdapter extends RecyclerView.Adapter im
public void onBindViewHolder(final KeyProfileHolder holder, int position) {
final KeyProfile profile = _keyProfiles.get(position);
holder.setData(profile, _showIssuer);
- holder.startUpdateLoop();
+ holder.startRefreshLoop();
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
diff --git a/app/src/main/java/me/impy/aegis/KeyProfileHolder.java b/app/src/main/java/me/impy/aegis/ui/views/KeyProfileHolder.java
similarity index 78%
rename from app/src/main/java/me/impy/aegis/KeyProfileHolder.java
rename to app/src/main/java/me/impy/aegis/ui/views/KeyProfileHolder.java
index 4e19cfac..6028b59d 100644
--- a/app/src/main/java/me/impy/aegis/KeyProfileHolder.java
+++ b/app/src/main/java/me/impy/aegis/ui/views/KeyProfileHolder.java
@@ -1,4 +1,4 @@
-package me.impy.aegis;
+package me.impy.aegis.ui.views;
import android.animation.ObjectAnimator;
import android.graphics.Color;
@@ -12,14 +12,15 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import com.amulyakhare.textdrawable.TextDrawable;
-import com.amulyakhare.textdrawable.util.ColorGenerator;
+
+import me.impy.aegis.R;
public class KeyProfileHolder extends RecyclerView.ViewHolder {
private TextView _profileName;
private TextView _profileCode;
private TextView _profileIssuer;
private ImageView _profileDrawable;
- private KeyProfile _keyProfile;
+ private KeyProfile _profile;
private ProgressBar _progressBar;
private Handler _uiHandler;
@@ -39,10 +40,12 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder {
}
public void setData(KeyProfile profile, boolean showIssuer) {
- if ((_keyProfile = profile) == null) {
+ if (profile == null) {
+ _profile = null;
_running = false;
return;
}
+ _profile = profile;
_profileName.setText(profile.getEntry().getName());
_profileCode.setText(profile.getCode());
@@ -55,36 +58,35 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder {
_profileDrawable.setImageDrawable(drawable);
}
- public void startUpdateLoop() {
+ public void startRefreshLoop() {
if (_running) {
return;
}
_running = true;
- updateCode();
+ refreshCode();
_uiHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (_running) {
- updateCode();
- _uiHandler.postDelayed(this, _keyProfile.getEntry().getInfo().getMillisTillNextRotation());
+ refreshCode();
+ _uiHandler.postDelayed(this, _profile.getEntry().getInfo().getMillisTillNextRotation());
}
}
- }, _keyProfile.getEntry().getInfo().getMillisTillNextRotation());
+ }, _profile.getEntry().getInfo().getMillisTillNextRotation());
}
- private boolean updateCode() {
+ public void refreshCode() {
+ String otp = _profile.refreshCode();
+
// reset the progress bar
int maxProgress = _progressBar.getMax();
_progressBar.setProgress(maxProgress);
-
- // refresh the code
- String otp = _keyProfile.refreshCode();
_profileCode.setText(otp.substring(0, otp.length() / 2) + " " + otp.substring(otp.length() / 2));
// calculate the progress the bar should start at
- long millisTillRotation = _keyProfile.getEntry().getInfo().getMillisTillNextRotation();
- long period = _keyProfile.getEntry().getInfo().getPeriod() * maxProgress;
+ long millisTillRotation = _profile.getEntry().getInfo().getMillisTillNextRotation();
+ long period = _profile.getEntry().getInfo().getPeriod() * maxProgress;
int currentProgress = maxProgress - (int) ((((double) period - millisTillRotation) / period) * maxProgress);
// start progress animation
@@ -92,6 +94,5 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder {
animation.setDuration(millisTillRotation);
animation.setInterpolator(new LinearInterpolator());
animation.start();
- return true;
}
}
diff --git a/app/src/main/java/me/impy/aegis/KeyProfileView.java b/app/src/main/java/me/impy/aegis/ui/views/KeyProfileView.java
similarity index 95%
rename from app/src/main/java/me/impy/aegis/KeyProfileView.java
rename to app/src/main/java/me/impy/aegis/ui/views/KeyProfileView.java
index 214834f3..d78b7ac1 100644
--- a/app/src/main/java/me/impy/aegis/KeyProfileView.java
+++ b/app/src/main/java/me/impy/aegis/ui/views/KeyProfileView.java
@@ -1,4 +1,4 @@
-package me.impy.aegis;
+package me.impy.aegis.ui.views;
import android.os.Bundle;
import android.support.v4.app.Fragment;
@@ -9,6 +9,7 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import me.impy.aegis.R;
import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.helpers.SimpleItemTouchHelperCallback;
@@ -83,6 +84,10 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
_adapter.replaceKey(profile);
}
+ public void refresh() {
+ _adapter.refresh();
+ }
+
public interface Listener {
void onEntryClick(KeyProfile profile);
void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2);
diff --git a/app/src/main/java/me/impy/aegis/ui/views/SlotAdapter.java b/app/src/main/java/me/impy/aegis/ui/views/SlotAdapter.java
new file mode 100644
index 00000000..6a0c084b
--- /dev/null
+++ b/app/src/main/java/me/impy/aegis/ui/views/SlotAdapter.java
@@ -0,0 +1,71 @@
+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 me.impy.aegis.R;
+import me.impy.aegis.db.slots.Slot;
+
+public class SlotAdapter extends RecyclerView.Adapter {
+ private Listener _listener;
+ private ArrayList _slots;
+
+ public SlotAdapter(Listener listener) {
+ _listener = listener;
+ _slots = new ArrayList<>();
+ }
+
+ public void addSlot(Slot slot) {
+ _slots.add(slot);
+
+ int position = getItemCount() - 1;
+ if (position == 0) {
+ notifyDataSetChanged();
+ } else {
+ notifyItemInserted(position);
+ }
+ }
+
+ public void updateSlot(Slot slot) {
+ notifyItemChanged(_slots.indexOf(slot));
+ }
+
+ public void removeSlot(Slot slot) {
+ int position = _slots.indexOf(slot);
+ _slots.remove(position);
+ notifyItemRemoved(position);
+ }
+
+ @Override
+ public SlotHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_slot, parent, false);
+ return new SlotHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(SlotHolder holder, int position) {
+ holder.setData(_slots.get(position));
+ holder.setOnEditClickListener(v -> {
+ int position1 = holder.getAdapterPosition();
+ _listener.onEditSlot(_slots.get(position1));
+ });
+ holder.setOnDeleteClickListener(v -> {
+ int position12 = holder.getAdapterPosition();
+ _listener.onRemoveSlot(_slots.get(position12));
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return _slots.size();
+ }
+
+ public interface Listener {
+ void onEditSlot(Slot slot);
+ void onRemoveSlot(Slot slot);
+ }
+}
diff --git a/app/src/main/java/me/impy/aegis/ui/views/SlotHolder.java b/app/src/main/java/me/impy/aegis/ui/views/SlotHolder.java
new file mode 100644
index 00000000..8d6002cb
--- /dev/null
+++ b/app/src/main/java/me/impy/aegis/ui/views/SlotHolder.java
@@ -0,0 +1,64 @@
+package me.impy.aegis.ui.views;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import me.impy.aegis.R;
+import me.impy.aegis.crypto.KeyStoreHandle;
+import me.impy.aegis.crypto.KeyStoreHandleException;
+import me.impy.aegis.db.slots.FingerprintSlot;
+import me.impy.aegis.db.slots.PasswordSlot;
+import me.impy.aegis.db.slots.RawSlot;
+import me.impy.aegis.db.slots.Slot;
+import me.impy.aegis.helpers.FingerprintHelper;
+
+public class SlotHolder extends RecyclerView.ViewHolder {
+ private TextView _slotUsed;
+ private TextView _slotName;
+ private ImageView _slotImg;
+ private LinearLayout _buttonEdit;
+ private ImageView _buttonDelete;
+
+ public SlotHolder(final View view) {
+ super(view);
+ _slotUsed = view.findViewById(R.id.text_slot_used);
+ _slotName = view.findViewById(R.id.text_slot_name);
+ _slotImg = view.findViewById(R.id.img_slot);
+ _buttonEdit = view.findViewById(R.id.button_edit);
+ _buttonDelete = view.findViewById(R.id.button_delete);
+ }
+
+ public void setData(Slot slot) {
+ if (slot instanceof PasswordSlot) {
+ _slotName.setText("Password");
+ _slotImg.setImageResource(R.drawable.ic_create_black_24dp);
+ } else if (slot instanceof FingerprintSlot) {
+ _slotName.setText("Finger");
+ _slotImg.setImageResource(R.drawable.ic_fingerprint_black_24dp);
+ if (FingerprintHelper.isSupported()) {
+ try {
+ KeyStoreHandle keyStore = new KeyStoreHandle();
+ if (keyStore.containsKey(slot.getID())) {
+ _slotUsed.setVisibility(View.VISIBLE);
+ }
+ } catch (KeyStoreHandleException e) { }
+ }
+ } else if (slot instanceof RawSlot) {
+ _slotName.setText("Raw");
+ _slotImg.setImageResource(R.drawable.ic_vpn_key_black_24dp);
+ } else {
+ throw new RuntimeException();
+ }
+ }
+
+ public void setOnEditClickListener(View.OnClickListener listener) {
+ _buttonEdit.setOnClickListener(listener);
+ }
+
+ public void setOnDeleteClickListener(View.OnClickListener listener) {
+ _buttonDelete.setOnClickListener(listener);
+ }
+}
diff --git a/app/src/main/java/me/impy/aegis/util/LittleByteBuffer.java b/app/src/main/java/me/impy/aegis/util/LittleByteBuffer.java
deleted file mode 100644
index 314abecc..00000000
--- a/app/src/main/java/me/impy/aegis/util/LittleByteBuffer.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package me.impy.aegis.util;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-
-// LittleByteBuffer wraps a ByteBuffer to extend its API a little.
-// Its byte order is set to little endian by default.
-// All this boilerplate just to change the default byte order and add a peek method... Is it worth it? Probably not.
-public class LittleByteBuffer {
- private ByteBuffer _buffer;
-
- private LittleByteBuffer(ByteBuffer buffer) {
- _buffer = buffer;
- _buffer.order(ByteOrder.LITTLE_ENDIAN);
- }
-
- public byte peek() {
- _buffer.mark();
- byte b = _buffer.get();
- _buffer.reset();
- return b;
- }
-
- public byte get() { return _buffer.get(); }
- public LittleByteBuffer get(byte[] dst) {_buffer.get(dst); return this; }
- public LittleByteBuffer put(byte b) { _buffer.put(b); return this; }
- public LittleByteBuffer put(byte[] bytes) { _buffer.put(bytes); return this; }
- public int remaining() { return _buffer.remaining(); }
- public byte[] array() { return _buffer.array(); }
- public LittleByteBuffer putInt(int i) { _buffer.putInt(i); return this; }
- public LittleByteBuffer putLong(long l) { _buffer.putLong(l); return this; }
- public int getInt() { return _buffer.getInt(); }
- public long getLong() { return _buffer.getLong(); }
- public int position() { return _buffer.position(); }
- public LittleByteBuffer position(int i) { _buffer.position(i); return this; }
- public static LittleByteBuffer allocate(int size) { return new LittleByteBuffer(ByteBuffer.allocate(size)); }
- public static LittleByteBuffer wrap(byte[] bytes) { return new LittleByteBuffer(ByteBuffer.wrap(bytes)); }
-}
diff --git a/app/src/main/res/drawable/ic_fingerprint_black_24dp.xml b/app/src/main/res/drawable/ic_fingerprint_black_24dp.xml
new file mode 100644
index 00000000..d0f76e34
--- /dev/null
+++ b/app/src/main/res/drawable/ic_fingerprint_black_24dp.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_plus_black_24dp.xml b/app/src/main/res/drawable/ic_plus_black_24dp.xml
new file mode 100644
index 00000000..ddd6dfe6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_plus_black_24dp.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_auth.xml b/app/src/main/res/layout/activity_auth.xml
index b7cda272..23fc8329 100644
--- a/app/src/main/res/layout/activity_auth.xml
+++ b/app/src/main/res/layout/activity_auth.xml
@@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
- tools:context="me.impy.aegis.AuthActivity">
+ tools:context="me.impy.aegis.ui.AuthActivity">
+ android:visibility="gone">
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_edit_profile.xml b/app/src/main/res/layout/activity_edit_profile.xml
index d30c1447..e1ececdf 100644
--- a/app/src/main/res/layout/activity_edit_profile.xml
+++ b/app/src/main/res/layout/activity_edit_profile.xml
@@ -1,188 +1,183 @@
-
+ android:layout_height="match_parent">
-
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:descendantFocusability="beforeDescendants"
+ android:focusableInTouchMode="true">
-
+
+
+
+
+
+ android:stretchColumns="1"
+ android:layout_marginEnd="35dp">
-
-
+
+
-
+
+
-
+
+
+
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+ android:layout_weight="4"
+ style="@style/Base.Widget.AppCompat.Spinner.Underlined"/>
+
+
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+ android:orientation="vertical">
+
+
+
-
-
-
-
-
-
-
+ android:layout_weight="1"/>
+
+
-
-
+
+
-
+
+
+ android:hint="Secret (base32)"
+ android:inputType="textPassword"/>
+
+
-
-
-
+
-
-
-
-
-
+
+
diff --git a/app/src/main/res/layout/activity_intro.xml b/app/src/main/res/layout/activity_intro.xml
index 3882726a..ebf7abe0 100644
--- a/app/src/main/res/layout/activity_intro.xml
+++ b/app/src/main/res/layout/activity_intro.xml
@@ -5,6 +5,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
- tools:context="me.impy.aegis.IntroActivity">
+ tools:context="me.impy.aegis.ui.IntroActivity">
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index a11b3ef6..4d8f0bb3 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
- tools:context="me.impy.aegis.MainActivity">
+ tools:context="me.impy.aegis.ui.MainActivity">
+ tools:context="me.impy.aegis.ui.ScannerActivity">
diff --git a/app/src/main/res/layout/activity_slots.xml b/app/src/main/res/layout/activity_slots.xml
new file mode 100644
index 00000000..b4d5007f
--- /dev/null
+++ b/app/src/main/res/layout/activity_slots.xml
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/card_slot.xml b/app/src/main/res/layout/card_slot.xml
new file mode 100644
index 00000000..86435f8f
--- /dev/null
+++ b/app/src/main/res/layout/card_slot.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_fingerprint.xml b/app/src/main/res/layout/dialog_fingerprint.xml
new file mode 100644
index 00000000..2c2a339f
--- /dev/null
+++ b/app/src/main/res/layout/dialog_fingerprint.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_new_profile.xml b/app/src/main/res/layout/dialog_new_profile.xml
deleted file mode 100644
index a67fa6d3..00000000
--- a/app/src/main/res/layout/dialog_new_profile.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_password.xml b/app/src/main/res/layout/dialog_password.xml
new file mode 100644
index 00000000..f66ba768
--- /dev/null
+++ b/app/src/main/res/layout/dialog_password.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_authenticated_slide.xml b/app/src/main/res/layout/fragment_authenticated_slide.xml
index 81c36a02..a79b7b93 100644
--- a/app/src/main/res/layout/fragment_authenticated_slide.xml
+++ b/app/src/main/res/layout/fragment_authenticated_slide.xml
@@ -95,7 +95,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="15dp"
- android:text="Touch sensor"/>
+ android:text="@string/fingerprint_hint"/>
diff --git a/app/src/main/res/menu/menu_edit.xml b/app/src/main/res/menu/menu_edit.xml
index 68ae0392..de645b02 100644
--- a/app/src/main/res/menu/menu_edit.xml
+++ b/app/src/main/res/menu/menu_edit.xml
@@ -2,7 +2,7 @@