mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-14 14:02:49 +00:00
Merge branch 'master' of https://github.com/alexbakker/Aegis
This commit is contained in:
commit
457f578102
65 changed files with 1613 additions and 752 deletions
|
@ -15,7 +15,7 @@
|
|||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:name=".ui.MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme.Default.NoActionBar"
|
||||
android:alwaysRetainTaskState="true"
|
||||
|
@ -26,28 +26,33 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ScannerActivity"
|
||||
android:name=".ui.ScannerActivity"
|
||||
android:label="Scan a QR code"
|
||||
android:theme="@style/AppTheme.Fullscreen"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".EditProfileActivity"
|
||||
android:name=".ui.EditProfileActivity"
|
||||
android:label="Edit profile"
|
||||
android:theme="@style/AppTheme.Default.TransparentActionBar">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".IntroActivity"
|
||||
android:name=".ui.IntroActivity"
|
||||
android:theme="@style/Theme.Intro"
|
||||
android:launchMode="singleTop">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".AuthActivity"
|
||||
android:name=".ui.AuthActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/AppTheme.Default.NoActionBar">
|
||||
</activity>
|
||||
<activity android:name=".PreferencesActivity">
|
||||
<activity android:name=".ui.PreferencesActivity">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.SlotManagerActivity"
|
||||
android:label="Manage key slots"
|
||||
android:theme="@style/AppTheme.Default.NoActionBar">
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package me.impy.aegis.crypto;
|
||||
|
||||
public class KeyStoreHandleException extends Exception {
|
||||
public KeyStoreHandleException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -13,11 +13,7 @@ public class Database {
|
|||
private List<DatabaseEntry> _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) {
|
||||
|
|
|
@ -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;
|
||||
JSONObject cryptObj = headerObj.optJSONObject("params");
|
||||
if (cryptObj != null) {
|
||||
_cryptParameters = new CryptParameters() {{
|
||||
Nonce = Hex.toBytes(cryptObj.getString("nonce"));
|
||||
Tag = Hex.toBytes(cryptObj.getString("tag"));
|
||||
}};
|
||||
break;
|
||||
case SECTION_SLOTS:
|
||||
slots = SlotCollection.deserialize(s.Data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
13
app/src/main/java/me/impy/aegis/db/slots/RawSlot.java
Normal file
13
app/src/main/java/me/impy/aegis/db/slots/RawSlot.java
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
// a slot has a binary representation
|
||||
public abstract byte[] serialize();
|
||||
public abstract void deserialize(byte[] data) throws Exception;
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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<Slot>, Serializable {
|
||||
private List<Slot> _slots = new ArrayList<>();
|
||||
private byte[] _masterHash;
|
||||
|
||||
public static byte[] serialize(SlotCollection slots) {
|
||||
// yep, no streams at this api level
|
||||
int size = 0;
|
||||
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) {
|
||||
size += slot.getSize();
|
||||
}
|
||||
size += CryptoUtils.CRYPTO_HASH_SIZE;
|
||||
|
||||
LittleByteBuffer buffer = LittleByteBuffer.allocate(size);
|
||||
buffer.put(slots.getMasterHash());
|
||||
|
||||
for (Slot slot : slots) {
|
||||
byte[] bytes = slot.serialize();
|
||||
buffer.put(bytes);
|
||||
}
|
||||
return buffer.array();
|
||||
entries.put(slot.serialize());
|
||||
}
|
||||
|
||||
public static SlotCollection deserialize(byte[] data) throws Exception {
|
||||
LittleByteBuffer buffer = LittleByteBuffer.wrap(data);
|
||||
byte[] masterHash = new byte[CryptoUtils.CRYPTO_HASH_SIZE];
|
||||
buffer.get(masterHash);
|
||||
obj.put("entries", entries);
|
||||
return obj;
|
||||
}
|
||||
|
||||
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<Slot>, 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);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package me.impy.aegis.crypto.slots;
|
||||
package me.impy.aegis.db.slots;
|
||||
|
||||
public class SlotIntegrityException extends Exception {
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
46
app/src/main/java/me/impy/aegis/encoding/Hex.java
Normal file
46
app/src/main/java/me/impy/aegis/encoding/Hex.java
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package me.impy.aegis.encoding;
|
||||
|
||||
public class HexException extends Exception {
|
||||
public HexException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<DatabaseEntry> 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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
@ -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 {
|
||||
// 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.keyExists()) {
|
||||
SecretKey key = handle.getKey();
|
||||
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();
|
||||
}
|
||||
|
|
@ -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);
|
|
@ -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) {
|
|
@ -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();
|
|
@ -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
|
|
@ -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;
|
184
app/src/main/java/me/impy/aegis/ui/SlotManagerActivity.java
Normal file
184
app/src/main/java/me/impy/aegis/ui/SlotManagerActivity.java
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 {
|
|
@ -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<DerivationTask.Params, SecretKey> {
|
||||
private Callback _cb;
|
||||
|
@ -24,7 +24,6 @@ public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, Se
|
|||
try {
|
||||
byte[] salt = CryptoUtils.generateSalt();
|
||||
SecretKey key = params.Slot.deriveKey(params.Password, salt, CryptoUtils.CRYPTO_SCRYPT_N, CryptoUtils.CRYPTO_SCRYPT_r, CryptoUtils.CRYPTO_SCRYPT_p);
|
||||
CryptoUtils.zero(params.Password);
|
||||
return key;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
|
@ -37,12 +36,12 @@ public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, Se
|
|||
_cb.onTaskFinished(key);
|
||||
}
|
||||
|
||||
static class Params {
|
||||
public static class Params {
|
||||
public PasswordSlot Slot;
|
||||
public char[] Password;
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
public interface Callback {
|
||||
void onTaskFinished(SecretKey key);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package me.impy.aegis;
|
||||
package me.impy.aegis.ui.tasks;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
|
@ -1,4 +1,4 @@
|
|||
package me.impy.aegis;
|
||||
package me.impy.aegis.ui.tasks;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Process;
|
||||
|
@ -8,13 +8,12 @@ import java.lang.reflect.UndeclaredThrowableException;
|
|||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import me.impy.aegis.crypto.CryptoUtils;
|
||||
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.crypto.slots.SlotIntegrityException;
|
||||
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.slots.SlotIntegrityException;
|
||||
|
||||
public class SlotCollectionTask<T extends Slot> extends ProgressDialogTask<SlotCollectionTask.Params, MasterKey> {
|
||||
private Callback _cb;
|
||||
|
@ -42,7 +41,6 @@ public class SlotCollectionTask<T extends Slot> extends ProgressDialogTask<SlotC
|
|||
if (slot instanceof PasswordSlot) {
|
||||
char[] password = (char[])params.Obj;
|
||||
SecretKey key = ((PasswordSlot)slot).deriveKey(password);
|
||||
CryptoUtils.zero(password);
|
||||
Cipher cipher = Slot.createCipher(key, Cipher.DECRYPT_MODE);
|
||||
masterKey = params.Slots.decrypt(slot, cipher);
|
||||
} else if (slot instanceof FingerprintSlot) {
|
||||
|
@ -72,12 +70,12 @@ public class SlotCollectionTask<T extends Slot> extends ProgressDialogTask<SlotC
|
|||
_cb.onTaskFinished(masterKey);
|
||||
}
|
||||
|
||||
static class Params {
|
||||
public static class Params {
|
||||
public SlotCollection Slots;
|
||||
public Object Obj;
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
public interface Callback {
|
||||
void onTaskFinished(MasterKey key);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package me.impy.aegis;
|
||||
package me.impy.aegis.ui.views;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.amulyakhare.textdrawable.util.ColorGenerator;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
|
@ -1,4 +1,4 @@
|
|||
package me.impy.aegis;
|
||||
package me.impy.aegis.ui.views;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
|
@ -8,6 +8,7 @@ import android.view.ViewGroup;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
import me.impy.aegis.R;
|
||||
import me.impy.aegis.helpers.ItemTouchHelperAdapter;
|
||||
|
||||
public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileHolder> implements ItemTouchHelperAdapter {
|
||||
|
@ -54,6 +55,13 @@ public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileHolder> 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<KeyProfileHolder> 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) {
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
71
app/src/main/java/me/impy/aegis/ui/views/SlotAdapter.java
Normal file
71
app/src/main/java/me/impy/aegis/ui/views/SlotAdapter.java
Normal file
|
@ -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<SlotHolder> {
|
||||
private Listener _listener;
|
||||
private ArrayList<Slot> _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);
|
||||
}
|
||||
}
|
64
app/src/main/java/me/impy/aegis/ui/views/SlotHolder.java
Normal file
64
app/src/main/java/me/impy/aegis/ui/views/SlotHolder.java
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)); }
|
||||
}
|
7
app/src/main/res/drawable/ic_fingerprint_black_24dp.xml
Normal file
7
app/src/main/res/drawable/ic_fingerprint_black_24dp.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="#000" android:pathData="M11.83,1.73C8.43,1.79 6.23,3.32 6.23,3.32C5.95,3.5 5.88,3.91 6.07,4.19C6.27,4.5 6.66,4.55 6.96,4.34C6.96,4.34 11.27,1.15 17.46,4.38C17.75,4.55 18.14,4.45 18.31,4.15C18.5,3.85 18.37,3.47 18.03,3.28C16.36,2.4 14.78,1.96 13.36,1.8C12.83,1.74 12.32,1.72 11.83,1.73M12.22,4.34C6.26,4.26 3.41,9.05 3.41,9.05C3.22,9.34 3.3,9.72 3.58,9.91C3.87,10.1 4.26,10 4.5,9.68C4.5,9.68 6.92,5.5 12.2,5.59C17.5,5.66 19.82,9.65 19.82,9.65C20,9.94 20.38,10.04 20.68,9.87C21,9.69 21.07,9.31 20.9,9C20.9,9 18.15,4.42 12.22,4.34M11.5,6.82C9.82,6.94 8.21,7.55 7,8.56C4.62,10.53 3.1,14.14 4.77,19C4.88,19.33 5.24,19.5 5.57,19.39C5.89,19.28 6.07,18.92 5.95,18.6V18.6C4.41,14.13 5.78,11.2 7.8,9.5C9.77,7.89 13.25,7.5 15.84,9.1C17.11,9.9 18.1,11.28 18.6,12.64C19.11,14 19.08,15.32 18.67,15.94C18.25,16.59 17.4,16.83 16.65,16.64C15.9,16.45 15.29,15.91 15.26,14.77C15.23,13.06 13.89,12 12.5,11.84C11.16,11.68 9.61,12.4 9.21,14C8.45,16.92 10.36,21.07 14.78,22.45C15.11,22.55 15.46,22.37 15.57,22.04C15.67,21.71 15.5,21.35 15.15,21.25C11.32,20.06 9.87,16.43 10.42,14.29C10.66,13.33 11.5,13 12.38,13.08C13.25,13.18 14,13.7 14,14.79C14.05,16.43 15.12,17.54 16.34,17.85C17.56,18.16 18.97,17.77 19.72,16.62C20.5,15.45 20.37,13.8 19.78,12.21C19.18,10.61 18.07,9.03 16.5,8.04C14.96,7.08 13.19,6.7 11.5,6.82M11.86,9.25V9.26C10.08,9.32 8.3,10.24 7.28,12.18C5.96,14.67 6.56,17.21 7.44,19.04C8.33,20.88 9.54,22.1 9.54,22.1C9.78,22.35 10.17,22.35 10.42,22.11C10.67,21.87 10.67,21.5 10.43,21.23C10.43,21.23 9.36,20.13 8.57,18.5C7.78,16.87 7.3,14.81 8.38,12.77C9.5,10.67 11.5,10.16 13.26,10.67C15.04,11.19 16.53,12.74 16.5,15.03C16.46,15.38 16.71,15.68 17.06,15.7C17.4,15.73 17.7,15.47 17.73,15.06C17.79,12.2 15.87,10.13 13.61,9.47C13.04,9.31 12.45,9.23 11.86,9.25M12.08,14.25C11.73,14.26 11.46,14.55 11.47,14.89C11.47,14.89 11.5,16.37 12.31,17.8C13.15,19.23 14.93,20.59 18.03,20.3C18.37,20.28 18.64,20 18.62,19.64C18.6,19.29 18.3,19.03 17.91,19.06C15.19,19.31 14.04,18.28 13.39,17.17C12.74,16.07 12.72,14.88 12.72,14.88C12.72,14.53 12.44,14.25 12.08,14.25Z" />
|
||||
</vector>
|
7
app/src/main/res/drawable/ic_plus_black_24dp.xml
Normal file
7
app/src/main/res/drawable/ic_plus_black_24dp.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="#000" android:pathData="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
|
||||
</vector>
|
|
@ -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">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -48,7 +48,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="12dp"
|
||||
android:visibility="invisible">
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/img_fingerprint_insert"
|
||||
|
@ -62,8 +62,27 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="15dp"
|
||||
android:text="Touch sensor"
|
||||
android:text="@string/fingerprint_hint"
|
||||
android:textColor="?attr/secondaryText"/>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/box_fingerprint_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="12dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_info_outline_black_24dp"
|
||||
android:layout_marginEnd="15dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="A fingerprint slot was found, but it appears to have been invalidated by the Android keystore. Go to "Settings -> Key slots" to readd your fingerprint."/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
@ -43,7 +38,6 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_person_black_24dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginTop="10dp"
|
||||
android:tint="@color/cardview_dark_background"
|
||||
android:layout_marginStart="15dp"
|
||||
|
@ -52,8 +46,9 @@
|
|||
<EditText android:layout_column="1"
|
||||
android:id="@+id/text_name"
|
||||
android:hint="Name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"/>
|
||||
</TableRow>
|
||||
|
||||
<TableRow
|
||||
|
@ -62,8 +57,9 @@
|
|||
<EditText android:layout_column="1"
|
||||
android:id="@+id/text_issuer"
|
||||
android:hint="Issuer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"/>
|
||||
</TableRow>
|
||||
|
||||
<TableRow
|
||||
|
@ -73,15 +69,15 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_info_outline_black_24dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginTop="10dp"
|
||||
android:tint="@color/cardview_dark_background"
|
||||
android:layout_marginStart="15dp"
|
||||
android:layout_marginEnd="20dp"/>
|
||||
|
||||
<LinearLayout android:layout_column="1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
<Spinner
|
||||
|
@ -113,15 +109,15 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_timelapse_black_24dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginTop="10dp"
|
||||
android:tint="@color/cardview_dark_background"
|
||||
android:layout_marginStart="15dp"
|
||||
android:layout_marginEnd="20dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
|
@ -160,15 +156,15 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_vpn_key_black_24dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginTop="10dp"
|
||||
android:tint="@color/cardview_dark_background"
|
||||
android:layout_marginStart="15dp"
|
||||
android:layout_marginEnd="20dp"/>
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
app:hintEnabled="false"
|
||||
app:passwordToggleEnabled="true">
|
||||
|
||||
|
@ -185,4 +181,3 @@
|
|||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
|
|
@ -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">
|
||||
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
|
|
|
@ -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">
|
||||
|
||||
<android.support.design.widget.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
|
@ -22,7 +22,7 @@
|
|||
</android.support.design.widget.AppBarLayout>
|
||||
|
||||
<fragment
|
||||
android:name="me.impy.aegis.KeyProfileView"
|
||||
android:name="me.impy.aegis.ui.views.KeyProfileView"
|
||||
android:id="@+id/key_profiles"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -9,6 +9,6 @@
|
|||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context="me.impy.aegis.ScannerActivity">
|
||||
tools:context="me.impy.aegis.ui.ScannerActivity">
|
||||
|
||||
</RelativeLayout>
|
||||
|
|
126
app/src/main/res/layout/activity_slots.xml
Normal file
126
app/src/main/res/layout/activity_slots.xml
Normal file
|
@ -0,0 +1,126 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context="me.impy.aegis.ui.SlotManagerActivity">
|
||||
|
||||
<android.support.design.widget.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/AppTheme.AppBarOverlay">
|
||||
|
||||
<android.support.v7.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
app:popupTheme="@style/AppTheme.PopupOverlay" />
|
||||
|
||||
</android.support.design.widget.AppBarLayout>
|
||||
|
||||
<android.support.v4.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/list_slots"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollbars="vertical"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_add_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="12.5dp"
|
||||
android:paddingBottom="12.5dp"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?android:attr/selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_plus_black_24dp"
|
||||
android:tint="@color/colorAccent"
|
||||
android:layout_marginEnd="15dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Add password"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?android:attr/textColorSecondary"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_add_fingerprint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="12.5dp"
|
||||
android:paddingBottom="12.5dp"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?android:attr/selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_plus_black_24dp"
|
||||
android:tint="@color/colorAccent"
|
||||
android:layout_marginEnd="15dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Add fingerprint"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?android:attr/textColorSecondary"/>
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@android:color/darker_gray"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="12.5dp"
|
||||
android:paddingBottom="12.5dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginEnd="10dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_info_outline_black_24dp"
|
||||
android:layout_marginEnd="15dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="The database is only as secure as your weakest key.\n\nWhen a new fingerprint is enrolled, all keys are wiped from the keystore. You should always have at least one password slot to prevent accidental data loss."/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</android.support.v4.widget.NestedScrollView>
|
||||
|
||||
</android.support.design.widget.CoordinatorLayout>
|
74
app/src/main/res/layout/card_slot.xml
Normal file
74
app/src/main/res/layout/card_slot.xml
Normal file
|
@ -0,0 +1,74 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_edit"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:paddingTop="12.5dp"
|
||||
android:paddingBottom="12.5dp"
|
||||
android:foreground="?android:attr/selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/img_slot"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="15dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:text="Slot name"
|
||||
android:id="@+id/text_slot_name"
|
||||
android:textColor="@color/primary_text"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_slot_used"
|
||||
android:text="(this device)"
|
||||
android:visibility="gone"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/darker_gray"
|
||||
android:paddingStart="15dp"
|
||||
android:paddingEnd="15dp"
|
||||
android:layout_marginTop="12.5dp"
|
||||
android:layout_marginBottom="12.5dp"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/button_delete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_delete_black_24dp"
|
||||
android:paddingStart="15dp"
|
||||
android:paddingEnd="15dp"
|
||||
android:paddingTop="12.5dp"
|
||||
android:paddingBottom="12.5dp"
|
||||
android:foreground="?android:attr/selectableItemBackground"/>
|
||||
|
||||
</LinearLayout>
|
21
app/src/main/res/layout/dialog_fingerprint.xml
Normal file
21
app/src/main/res/layout/dialog_fingerprint.xml
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:paddingTop="20dp">
|
||||
<com.mattprecious.swirl.SwirlView
|
||||
android:id="@+id/img_fingerprint"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"/>
|
||||
<TextView
|
||||
android:id="@+id/text_fingerprint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="15dp"
|
||||
android:text="@string/fingerprint_hint"
|
||||
android:textColor="?attr/secondaryText"
|
||||
android:layout_gravity="center_vertical"/>
|
||||
</LinearLayout>
|
|
@ -1,19 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingBottom="16dp"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
|
||||
<EditText
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPersonName"
|
||||
android:text="Name"
|
||||
android:ems="10"
|
||||
android:id="@+id/profile_name"
|
||||
android:layout_weight="0.96"/>
|
||||
</LinearLayout>
|
21
app/src/main/res/layout/dialog_password.xml
Normal file
21
app/src/main/res/layout/dialog_password.xml
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:paddingTop="20dp">
|
||||
<EditText
|
||||
android:id="@+id/text_password"
|
||||
android:hint="Password"
|
||||
android:inputType="textPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
<EditText
|
||||
android:hint="Confirm password"
|
||||
android:id="@+id/text_password_confirm"
|
||||
android:inputType="textPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
</LinearLayout>
|
|
@ -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"/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="me.impy.aegis.EditProfileActivity">
|
||||
tools:context="me.impy.aegis.ui.EditProfileActivity">
|
||||
<item
|
||||
android:id="@+id/action_save"
|
||||
app:showAsAction="ifRoom"
|
||||
|
|
|
@ -1,17 +1,12 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="me.impy.aegis.MainActivity">
|
||||
tools:context="me.impy.aegis.ui.MainActivity">
|
||||
<item
|
||||
android:id="@+id/action_lock"
|
||||
android:icon="@drawable/ic_lock"
|
||||
app:showAsAction="ifRoom"
|
||||
android:title=""/>
|
||||
<item
|
||||
android:id="@+id/action_import"
|
||||
android:orderInCategory="100"
|
||||
android:title="@string/action_import"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/action_settings"
|
||||
android:orderInCategory="100"
|
||||
|
|
10
app/src/main/res/menu/menu_slots.xml
Normal file
10
app/src/main/res/menu/menu_slots.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="me.impy.aegis.ui.SlotManagerActivity">
|
||||
<item
|
||||
android:id="@+id/action_save"
|
||||
app:showAsAction="ifRoom"
|
||||
android:title="@string/save"/>
|
||||
</menu>
|
|
@ -6,11 +6,20 @@
|
|||
<string name="discard">Discard</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="title_activity_intro">IntroActivity</string>
|
||||
|
||||
<string name="settings">Preferences</string>
|
||||
<string name="pref_night_mode">Night mode</string>
|
||||
<string name="pref_night_mode_description">Enable this to use darker colors</string>
|
||||
<string name="pref_issuers">Show the issuer</string>
|
||||
<string name="pref_issuers_description">Enable this to show the issuer next to the profile name</string>
|
||||
<string name="pref_night_mode_title">Night mode</string>
|
||||
<string name="pref_night_mode_summary">Enable this to use darker colors</string>
|
||||
<string name="pref_issuer_title">Show the issuer</string>
|
||||
<string name="pref_issuer_summary">Enable this to show the issuer next to the profile name</string>
|
||||
<string name="pref_timeout_title">Timeout</string>
|
||||
<string name="pref_timeout_summary">Automatically lock the database after %1$s seconds of inactivity</string>
|
||||
<string name="pref_slots_title">Key slots</string>
|
||||
<string name="pref_slots_summary">Manage the list of keys that can decrypt the database</string>
|
||||
<string name="pref_import_title">Import</string>
|
||||
<string name="pref_import_summary">Import a database</string>
|
||||
<string name="pref_export_title">Export</string>
|
||||
<string name="pref_export_summary">Export the database</string>
|
||||
|
||||
<string name="fingerprint_hint">Touch sensor</string>
|
||||
<string name="fingerprint_not_recognized">Fingerprint not recognized. Try again</string>
|
||||
|
|
|
@ -2,21 +2,46 @@
|
|||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:title="@string/settings">
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="Appearance">
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="false"
|
||||
android:key="pref_night_mode"
|
||||
android:title="@string/pref_night_mode"
|
||||
android:summary="@string/pref_night_mode_description"/>
|
||||
android:title="@string/pref_night_mode_title"
|
||||
android:summary="@string/pref_night_mode_summary"/>
|
||||
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="false"
|
||||
android:key="pref_issuer"
|
||||
android:title="@string/pref_issuers"
|
||||
android:summary="@string/pref_issuers_description"/>
|
||||
android:title="@string/pref_issuer_title"
|
||||
android:summary="@string/pref_issuer_summary"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="Security">
|
||||
<EditTextPreference
|
||||
android:key="pref_timeout"
|
||||
android:title="@string/pref_timeout_title"
|
||||
android:summary="@string/pref_timeout_summary"
|
||||
android:inputType="number"
|
||||
android:defaultValue="30"
|
||||
android:dialogTitle="Set number of seconds of inactivity before Aegis locks the database"/>
|
||||
<Preference
|
||||
android:key="pref_slots"
|
||||
android:title="@string/pref_slots_title"
|
||||
android:summary="@string/pref_slots_summary"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="Tools">
|
||||
<Preference
|
||||
android:key="pref_import"
|
||||
android:title="@string/pref_import_title"
|
||||
android:summary="@string/pref_import_summary"/>
|
||||
<Preference
|
||||
android:key="pref_export"
|
||||
android:title="Export"
|
||||
android:summary="Export the database"/>
|
||||
android:title="@string/pref_export_title"
|
||||
android:summary="@string/pref_export_summary"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
|
|
@ -70,6 +70,7 @@ A slot has the following structure.
|
|||
| Length | Contents |
|
||||
|:-------|:--------------------|
|
||||
| `1` | `uint8_t` Type |
|
||||
| `16` | ID |
|
||||
| `32` | Encrypted key |
|
||||
| `?` | Additional data |
|
||||
|
||||
|
|
7
testdata/aegis_export.json
vendored
7
testdata/aegis_export.json
vendored
|
@ -1,4 +1,10 @@
|
|||
{
|
||||
"version": 1,
|
||||
"header": {
|
||||
"slots": null,
|
||||
"params": null
|
||||
},
|
||||
"db": {
|
||||
"version": 1,
|
||||
"entries": [
|
||||
{
|
||||
|
@ -43,3 +49,4 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue