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 @@ + tools:context="me.impy.aegis.ui.EditProfileActivity"> + tools:context="me.impy.aegis.ui.MainActivity"> - + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5bad2312..94afbe47 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,11 +6,20 @@ Discard Save IntroActivity + Preferences - Night mode - Enable this to use darker colors - Show the issuer - Enable this to show the issuer next to the profile name + Night mode + Enable this to use darker colors + Show the issuer + Enable this to show the issuer next to the profile name + Timeout + Automatically lock the database after %1$s seconds of inactivity + Key slots + Manage the list of keys that can decrypt the database + Import + Import a database + Export + Export the database Touch sensor Fingerprint not recognized. Try again diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 0eb2e2c0..da5ebeab 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -2,21 +2,46 @@ - + + - + + - + + + + + + + + + diff --git a/doc/db.md b/doc/db.md index c5c9ba15..2a8eec03 100644 --- a/doc/db.md +++ b/doc/db.md @@ -70,6 +70,7 @@ A slot has the following structure. | Length | Contents | |:-------|:--------------------| | `1` | `uint8_t` Type | +| `16` | ID | | `32` | Encrypted key | | `?` | Additional data | diff --git a/testdata/aegis_export.json b/testdata/aegis_export.json index 803136e2..6a01793e 100755 --- a/testdata/aegis_export.json +++ b/testdata/aegis_export.json @@ -1,45 +1,52 @@ { "version": 1, - "entries": [ - { - "name": "Bob", - "url": "otpauth:\/\/totp\/Google%3ABob?digits=6&period=30&algorithm=SHA1&secret=KD3WHMYQ7K7DGV5QFJNN2ZUATBC44C7F&issuer=Google" - }, - { - "name": "Henk", - "url": "otpauth:\/\/totp\/Scaleway%3AHenk?digits=6&period=10&algorithm=SHA256&secret=MBQX47S3XOSOF7YT2CO3LZNACULZNY4P&issuer=Scaleway" - }, - { - "name": "Oh wait, they don't have TOTP", - "url": "otpauth:\/\/totp\/Namecheap%3AOh%20wait%2C%20they%20don't%20have%20TOTP?digits=8&period=30&algorithm=SHA512&secret=HIMV4HX3REXMUG236OOLN4GS7URI247Y&issuer=Namecheap" - }, - { - "name": "Delete Me", - "url": "otpauth:\/\/totp\/Facebook%3ADelete%20Me?digits=6&period=60&algorithm=SHA1&secret=GSQ7E5FO3UFFVANS3NSMFE7RTMRMYNCV&issuer=Facebook" - }, - { - "name": "nodejsc0d3r", - "url": "otpauth:\/\/totp\/Github%3Anodejsc0d3r?digits=6&period=50&algorithm=SHA1&secret=642GK5EROWKUAEBFRH6FWSMM4FRKS5IA&issuer=Github" - }, - { - "name": "MoneroMan", - "url": "otpauth:\/\/totp\/Poloniex%3AMoneroMan?digits=8&period=30&algorithm=SHA256&secret=NUSLOF6CFEWNPAARBVJ5WFA5YRRQU4FG&issuer=Poloniex" - }, - { - "name": "Lil' Droplet", - "url": "otpauth:\/\/totp\/DigitalOcean%3ALil'%20Droplet?digits=6&period=45&algorithm=SHA512&secret=6CAIGVYB5MQ6TSZLJ56HJBWU5S3H7FUC&issuer=DigitalOcean" - }, - { - "name": "givemeabucket", - "url": "otpauth:\/\/totp\/Bitbucket%3Agivemeabucket?digits=6&period=20&algorithm=SHA1&secret=5KY3D2N53MRMIOMNKLLTNF3TDQJRDMEY&issuer=Bitbucket" - }, - { - "name": "Pepe", - "url": "otpauth:\/\/totp\/4chan%3APepe?digits=8&period=30&algorithm=SHA1&secret=EIQMT7NHFYJUMBKQ35P34JGLG3MO7L2W&issuer=4chan" - }, - { - "name": "alex", - "url": "otpauth:\/\/totp\/TTRSS%3Aalex?digits=6&period=30&algorithm=SHA1&secret=BEBXDJJVXKX3ZMAZLJUU5I5PONI4IYAL&issuer=TTRSS" - } - ] -} \ No newline at end of file + "header": { + "slots": null, + "params": null + }, + "db": { + "version": 1, + "entries": [ + { + "name": "Bob", + "url": "otpauth:\/\/totp\/Google%3ABob?digits=6&period=30&algorithm=SHA1&secret=KD3WHMYQ7K7DGV5QFJNN2ZUATBC44C7F&issuer=Google" + }, + { + "name": "Henk", + "url": "otpauth:\/\/totp\/Scaleway%3AHenk?digits=6&period=10&algorithm=SHA256&secret=MBQX47S3XOSOF7YT2CO3LZNACULZNY4P&issuer=Scaleway" + }, + { + "name": "Oh wait, they don't have TOTP", + "url": "otpauth:\/\/totp\/Namecheap%3AOh%20wait%2C%20they%20don't%20have%20TOTP?digits=8&period=30&algorithm=SHA512&secret=HIMV4HX3REXMUG236OOLN4GS7URI247Y&issuer=Namecheap" + }, + { + "name": "Delete Me", + "url": "otpauth:\/\/totp\/Facebook%3ADelete%20Me?digits=6&period=60&algorithm=SHA1&secret=GSQ7E5FO3UFFVANS3NSMFE7RTMRMYNCV&issuer=Facebook" + }, + { + "name": "nodejsc0d3r", + "url": "otpauth:\/\/totp\/Github%3Anodejsc0d3r?digits=6&period=50&algorithm=SHA1&secret=642GK5EROWKUAEBFRH6FWSMM4FRKS5IA&issuer=Github" + }, + { + "name": "MoneroMan", + "url": "otpauth:\/\/totp\/Poloniex%3AMoneroMan?digits=8&period=30&algorithm=SHA256&secret=NUSLOF6CFEWNPAARBVJ5WFA5YRRQU4FG&issuer=Poloniex" + }, + { + "name": "Lil' Droplet", + "url": "otpauth:\/\/totp\/DigitalOcean%3ALil'%20Droplet?digits=6&period=45&algorithm=SHA512&secret=6CAIGVYB5MQ6TSZLJ56HJBWU5S3H7FUC&issuer=DigitalOcean" + }, + { + "name": "givemeabucket", + "url": "otpauth:\/\/totp\/Bitbucket%3Agivemeabucket?digits=6&period=20&algorithm=SHA1&secret=5KY3D2N53MRMIOMNKLLTNF3TDQJRDMEY&issuer=Bitbucket" + }, + { + "name": "Pepe", + "url": "otpauth:\/\/totp\/4chan%3APepe?digits=8&period=30&algorithm=SHA1&secret=EIQMT7NHFYJUMBKQ35P34JGLG3MO7L2W&issuer=4chan" + }, + { + "name": "alex", + "url": "otpauth:\/\/totp\/TTRSS%3Aalex?digits=6&period=30&algorithm=SHA1&secret=BEBXDJJVXKX3ZMAZLJUU5I5PONI4IYAL&issuer=TTRSS" + } + ] + } +}