This commit is contained in:
Michael Schättgen 2018-02-20 21:14:53 +01:00
commit 457f578102
65 changed files with 1613 additions and 752 deletions

View file

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

View file

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

View file

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

View file

@ -1,41 +1,53 @@
package me.impy.aegis.crypto;
import android.annotation.SuppressLint;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
public class KeyStoreHandle {
private final KeyStore _keyStore;
private static final String KEY_NAME = "AegisKey";
private static final String STORE_NAME = "AndroidKeyStore";
public KeyStoreHandle() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
_keyStore = KeyStore.getInstance(STORE_NAME);
_keyStore.load(null);
public KeyStoreHandle() throws KeyStoreHandleException {
try {
_keyStore = KeyStore.getInstance(STORE_NAME);
_keyStore.load(null);
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
throw new KeyStoreHandleException(e);
}
}
public boolean keyExists() throws KeyStoreException {
return _keyStore.containsAlias(KEY_NAME);
public boolean containsKey(String id) throws KeyStoreHandleException {
try {
return _keyStore.containsAlias(id);
} catch (KeyStoreException e) {
throw new KeyStoreHandleException(e);
}
}
public SecretKey generateKey(boolean authRequired) throws Exception {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
public SecretKey generateKey(String id) throws Exception {
if (isSupported()) {
KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, STORE_NAME);
generator.init(new KeyGenParameterSpec.Builder(KEY_NAME,
generator.init(new KeyGenParameterSpec.Builder(id,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_ECB)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(authRequired)
.setUserAuthenticationRequired(true)
.setRandomizedEncryptionRequired(false)
.setKeySize(CryptoUtils.CRYPTO_KEY_SIZE * 8)
.build());
@ -46,7 +58,32 @@ public class KeyStoreHandle {
}
}
public SecretKey getKey() throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {
return (SecretKey) _keyStore.getKey(KEY_NAME, null);
public SecretKey getKey(String id)
throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {
SecretKey key = (SecretKey) _keyStore.getKey(id, null);
// try to initialize a dummy cipher
// and see if KeyPermanentlyInvalidatedException is thrown
if (isSupported()) {
try {
@SuppressLint("GetInstance")
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_CIPHER_RAW);
cipher.init(Cipher.ENCRYPT_MODE, key);
} catch (KeyPermanentlyInvalidatedException e) {
return null;
} catch (NoSuchPaddingException | InvalidKeyException e) {
throw new RuntimeException(e);
}
}
return key;
}
public void deleteKey(String id) throws KeyStoreException {
_keyStore.deleteEntry(id);
}
public static boolean isSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
}

View file

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

View file

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

View file

@ -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) {

View file

@ -1,163 +1,116 @@
package me.impy.aegis.db;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import android.util.Base64;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.Arrays;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import me.impy.aegis.crypto.CryptParameters;
import me.impy.aegis.crypto.slots.SlotCollection;
import me.impy.aegis.crypto.CryptoUtils;
import me.impy.aegis.util.LittleByteBuffer;
import me.impy.aegis.crypto.CryptResult;
import me.impy.aegis.crypto.MasterKey;
import me.impy.aegis.db.slots.SlotCollection;
import me.impy.aegis.encoding.Hex;
public class DatabaseFile {
private static final byte SECTION_ENCRYPTION_PARAMETERS = 0x00;
private static final byte SECTION_SLOTS = 0x01;
private static final byte SECTION_END = (byte) 0xFF;
private static final byte VERSION = 1;
public static final byte VERSION = 1;
private final byte[] HEADER;
private byte[] _content;
private Object _content;
private CryptParameters _cryptParameters;
private SlotCollection _slots;
public DatabaseFile() {
try {
HEADER = "AEGIS".getBytes("US_ASCII");
} catch (Exception e) {
throw new UndeclaredThrowableException(e);
}
_slots = new SlotCollection();
}
public byte[] serialize() throws IOException {
byte[] content = getContent();
CryptParameters cryptParams = getCryptParameters();
// this is dumb, java doesn't provide an endianness-aware data stream
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
DataOutputStream stream = new DataOutputStream(byteStream);
stream.write(HEADER);
stream.write(VERSION);
if (cryptParams != null) {
LittleByteBuffer paramBuffer = LittleByteBuffer.allocate(CryptoUtils.CRYPTO_NONCE_SIZE + CryptoUtils.CRYPTO_TAG_SIZE);
paramBuffer.put(cryptParams.Nonce);
paramBuffer.put(cryptParams.Tag);
writeSection(stream, SECTION_ENCRYPTION_PARAMETERS, paramBuffer.array());
public byte[] serialize() throws JSONException, UnsupportedEncodingException {
JSONObject cryptObj = null;
if (_cryptParameters != null) {
cryptObj = new JSONObject();
cryptObj.put("nonce", Hex.toString(_cryptParameters.Nonce));
cryptObj.put("tag", Hex.toString(_cryptParameters.Tag));
}
if (!_slots.isEmpty()) {
byte[] bytes = SlotCollection.serialize(_slots);
writeSection(stream, SECTION_SLOTS, bytes);
}
JSONObject headerObj = new JSONObject();
headerObj.put("slots", _slots.isEmpty() ? JSONObject.NULL : SlotCollection.serialize(_slots));
headerObj.put("params", cryptObj != null ? cryptObj : JSONObject.NULL);
writeSection(stream, SECTION_END, null);
stream.write(content);
return byteStream.toByteArray();
JSONObject obj = new JSONObject();
obj.put("version", VERSION);
obj.put("header", headerObj);
obj.put("db", _content);
String string = obj.toString(4);
return string.getBytes("UTF-8");
}
public void deserialize(byte[] data) throws Exception {
LittleByteBuffer buffer = LittleByteBuffer.wrap(data);
byte[] header = new byte[HEADER.length];
buffer.get(header);
if (!Arrays.equals(header, HEADER)) {
throw new Exception("Bad header");
JSONObject obj = new JSONObject(new String(data, "UTF-8"));
JSONObject headerObj = obj.getJSONObject("header");
if (obj.getInt("version") > VERSION) {
throw new Exception("unsupported version");
}
// TODO: support different version deserialization providers
byte version = buffer.get();
if (version != VERSION) {
throw new Exception("Unsupported version");
JSONObject slotObj = headerObj.optJSONObject("slots");
if (slotObj != null) {
_slots = SlotCollection.deserialize(slotObj);
}
CryptParameters cryptParams = null;
SlotCollection slots = new SlotCollection();
for (section s = readSection(buffer); s.ID != SECTION_END; s = readSection(buffer)) {
LittleByteBuffer sBuff = LittleByteBuffer.wrap(s.Data);
switch (s.ID) {
case SECTION_ENCRYPTION_PARAMETERS:
assertLength(s.Data, CryptoUtils.CRYPTO_NONCE_SIZE + CryptoUtils.CRYPTO_TAG_SIZE);
byte[] nonce = new byte[CryptoUtils.CRYPTO_NONCE_SIZE];
byte[] tag = new byte[CryptoUtils.CRYPTO_TAG_SIZE];
sBuff.get(nonce);
sBuff.get(tag);
cryptParams = new CryptParameters() {{
Nonce = nonce;
Tag = tag;
}};
break;
case SECTION_SLOTS:
slots = SlotCollection.deserialize(s.Data);
break;
}
JSONObject cryptObj = headerObj.optJSONObject("params");
if (cryptObj != null) {
_cryptParameters = new CryptParameters() {{
Nonce = Hex.toBytes(cryptObj.getString("nonce"));
Tag = Hex.toBytes(cryptObj.getString("tag"));
}};
}
setCryptParameters(cryptParams);
setSlots(slots);
byte[] content = new byte[buffer.remaining()];
buffer.get(content);
setContent(content);
if (cryptObj == null || slotObj == null) {
_content = obj.getJSONObject("db");
} else {
_content = obj.getString("db");
}
}
public boolean isEncrypted() {
return !_slots.isEmpty() && _cryptParameters != null;
}
private static void writeSection(DataOutputStream stream, byte id, byte[] data) throws IOException {
stream.write(id);
LittleByteBuffer buffer = LittleByteBuffer.allocate(/* sizeof uint32_t */ 4);
if (data == null) {
buffer.putInt(0);
} else {
buffer.putInt(data.length);
}
stream.write(buffer.array());
if (data != null) {
stream.write(data);
}
public JSONObject getContent() {
return (JSONObject) _content;
}
private static section readSection(LittleByteBuffer buffer) {
section s = new section();
s.ID = buffer.get();
int len = buffer.getInt();
s.Data = new byte[len];
buffer.get(s.Data);
return s;
public JSONObject getContent(MasterKey key)
throws NoSuchPaddingException, InvalidKeyException,
NoSuchAlgorithmException, IllegalBlockSizeException,
BadPaddingException, InvalidAlgorithmParameterException, IOException, JSONException {
byte[] bytes = Base64.decode((String) _content, Base64.NO_WRAP);
CryptResult result = key.decrypt(bytes, _cryptParameters);
return new JSONObject(new String(result.Data, "UTF-8"));
}
private static void assertLength(byte[] bytes, int length) throws Exception {
if (bytes.length != length) {
throw new Exception("Bad length");
}
public void setContent(JSONObject dbObj) {
_content = dbObj;
}
public byte[] getContent() {
return _content;
}
public void setContent(JSONObject dbObj, MasterKey key)
throws JSONException, UnsupportedEncodingException,
NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException,
IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
String string = dbObj.toString(4);
byte[] dbBytes = string.getBytes("UTF-8");
public void setContent(byte[] content) {
_content = content;
}
public CryptParameters getCryptParameters() {
return _cryptParameters;
}
public void setCryptParameters(CryptParameters parameters) {
_cryptParameters = parameters;
CryptResult result = key.encrypt(dbBytes);
_content = new String(Base64.encode(result.Data, Base64.NO_WRAP), "UTF-8");
_cryptParameters = result.Parameters;
}
public SlotCollection getSlots() {
@ -167,9 +120,4 @@ public class DatabaseFile {
public void setSlots(SlotCollection slots) {
_slots = slots;
}
private static class section {
byte ID;
byte[] Data;
}
}

View file

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

View 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;

View file

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

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

View file

@ -1,7 +1,10 @@
package me.impy.aegis.crypto.slots;
package me.impy.aegis.db.slots;
import android.annotation.SuppressLint;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.Serializable;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
@ -15,30 +18,34 @@ import javax.crypto.spec.SecretKeySpec;
import me.impy.aegis.crypto.CryptoUtils;
import me.impy.aegis.crypto.MasterKey;
import me.impy.aegis.encoding.Hex;
public abstract class Slot implements Serializable {
public final static byte TYPE_RAW = 0x00;
public final static byte TYPE_DERIVED = 0x01;
public final static byte TYPE_FINGERPRINT = 0x02;
public final static int ID_SIZE = 16;
protected byte[] _id;
protected byte[] _encryptedMasterKey;
protected Slot() {
_id = CryptoUtils.generateRandomBytes(ID_SIZE);
}
// getKey decrypts the encrypted master key in this slot with the given key and returns it.
public SecretKey getKey(Cipher cipher) throws BadPaddingException, IllegalBlockSizeException {
byte[] decryptedKeyBytes = cipher.doFinal(_encryptedMasterKey);
SecretKey decryptedKey = new SecretKeySpec(decryptedKeyBytes, CryptoUtils.CRYPTO_CIPHER_AEAD);
CryptoUtils.zero(decryptedKeyBytes);
return decryptedKey;
return new SecretKeySpec(decryptedKeyBytes, CryptoUtils.CRYPTO_CIPHER_AEAD);
}
// setKey encrypts the given master key with the given key and stores the result in this slot.
public void setKey(MasterKey masterKey, Cipher cipher) throws BadPaddingException, IllegalBlockSizeException {
byte[] masterKeyBytes = masterKey.getBytes();
_encryptedMasterKey = cipher.doFinal(masterKeyBytes);
CryptoUtils.zero(masterKeyBytes);
}
// suppressing the AES ECB warning
// suppress the AES ECB warning
// this is perfectly safe because we discard this cipher after passing CryptoUtils.CRYPTO_KEY_SIZE bytes through it
@SuppressLint("getInstance")
public static Cipher createCipher(SecretKey key, int mode) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException {
@ -47,10 +54,24 @@ public abstract class Slot implements Serializable {
return cipher;
}
public abstract int getSize();
public abstract byte getType();
public JSONObject serialize() throws JSONException {
JSONObject obj = new JSONObject();
obj.put("type", getType());
obj.put("id", Hex.toString(_id));
obj.put("key", Hex.toString(_encryptedMasterKey));
return obj;
}
// a slot has a binary representation
public abstract byte[] serialize();
public abstract void deserialize(byte[] data) throws Exception;
public void deserialize(JSONObject obj) throws Exception {
if (obj.getInt("type") != getType()) {
throw new Exception("slot type mismatch");
}
_id = Hex.toBytes(obj.getString("id"));
_encryptedMasterKey = Hex.toBytes(obj.getString("key"));
}
public abstract byte getType();
public String getID() {
return Hex.toString(_id);
}
}

View file

@ -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;
for (Slot slot : slots) {
size += slot.getSize();
}
size += CryptoUtils.CRYPTO_HASH_SIZE;
LittleByteBuffer buffer = LittleByteBuffer.allocate(size);
buffer.put(slots.getMasterHash());
public static JSONObject serialize(SlotCollection slots) throws JSONException {
JSONObject obj = new JSONObject();
obj.put("hash", Hex.toString(slots.getMasterHash()));
JSONArray entries = new JSONArray();
for (Slot slot : slots) {
byte[] bytes = slot.serialize();
buffer.put(bytes);
entries.put(slot.serialize());
}
return buffer.array();
obj.put("entries", entries);
return obj;
}
public static SlotCollection deserialize(byte[] data) throws Exception {
LittleByteBuffer buffer = LittleByteBuffer.wrap(data);
byte[] masterHash = new byte[CryptoUtils.CRYPTO_HASH_SIZE];
buffer.get(masterHash);
public static SlotCollection deserialize(JSONObject obj) throws Exception {
SlotCollection slots = new SlotCollection();
byte[] masterHash = Hex.toBytes(obj.getString("hash"));
slots.setMasterHash(masterHash);
while (buffer.remaining() > 0) {
JSONArray entries = obj.getJSONArray("entries");
for (int i = 0; i < entries.length(); i++) {
Slot slot;
JSONObject slotObj = entries.getJSONObject(i);
switch (buffer.peek()) {
switch (slotObj.getInt("type")) {
case Slot.TYPE_RAW:
slot = new RawSlot();
break;
@ -60,10 +58,7 @@ public class SlotCollection implements Iterable<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);
}

View file

@ -1,4 +1,4 @@
package me.impy.aegis.crypto.slots;
package me.impy.aegis.db.slots;
public class SlotIntegrityException extends Exception {

View file

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

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

View file

@ -0,0 +1,7 @@
package me.impy.aegis.encoding;
public class HexException extends Exception {
public HexException(String message) {
super(message);
}
}

View file

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

View file

@ -11,14 +11,16 @@ public class FingerprintHelper {
}
public static FingerprintManager getManager(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (PermissionHelper.granted(context, Manifest.permission.USE_FINGERPRINT)) {
FingerprintManager manager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
if (manager != null && manager.isHardwareDetected() && manager.hasEnrolledFingerprints()) {
return manager;
}
if (isSupported() && PermissionHelper.granted(context, Manifest.permission.USE_FINGERPRINT)) {
FingerprintManager manager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
if (manager != null && manager.isHardwareDetected() && manager.hasEnrolledFingerprints()) {
return manager;
}
}
return null;
}
public static boolean isSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
}

View file

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

View file

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

View file

@ -1,6 +1,5 @@
package me.impy.aegis;
package me.impy.aegis.ui;
import android.content.DialogInterface;
import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
@ -20,20 +19,19 @@ import java.lang.reflect.UndeclaredThrowableException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import me.impy.aegis.R;
import me.impy.aegis.crypto.KeyStoreHandle;
import me.impy.aegis.crypto.MasterKey;
import me.impy.aegis.crypto.slots.FingerprintSlot;
import me.impy.aegis.crypto.slots.PasswordSlot;
import me.impy.aegis.crypto.slots.Slot;
import me.impy.aegis.crypto.slots.SlotCollection;
import me.impy.aegis.db.slots.FingerprintSlot;
import me.impy.aegis.db.slots.PasswordSlot;
import me.impy.aegis.db.slots.Slot;
import me.impy.aegis.db.slots.SlotCollection;
import me.impy.aegis.helpers.FingerprintHelper;
import me.impy.aegis.helpers.FingerprintUiHelper;
import me.impy.aegis.helpers.AuthHelper;
import me.impy.aegis.helpers.EditTextHelper;
import me.impy.aegis.ui.tasks.SlotCollectionTask;
public class AuthActivity extends AegisActivity implements FingerprintUiHelper.Callback, SlotCollectionTask.Callback {
public static final int RESULT_OK = 0;
public static final int RESULT_EXCEPTION = 1;
private EditText _textPassword;
private SlotCollection _slots;
@ -46,6 +44,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
setContentView(R.layout.activity_auth);
_textPassword = findViewById(R.id.text_password);
LinearLayout boxFingerprint = findViewById(R.id.box_fingerprint);
LinearLayout boxFingerprintInfo = findViewById(R.id.box_fingerprint_info);
TextView textFingerprint = findViewById(R.id.text_fingerprint);
SwirlView imgFingerprint = null;
@ -61,24 +60,41 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
// only show the fingerprint controls if the api version is new enough, permission is granted, a scanner is found and a fingerprint slot is found
FingerprintManager manager = FingerprintHelper.getManager(this);
if (manager != null && _slots.has(FingerprintSlot.class)) {
boolean invalidated = false;
try {
KeyStoreHandle handle = new KeyStoreHandle();
if (handle.keyExists()) {
SecretKey key = handle.getKey();
_fingerCipher = Slot.createCipher(key, Cipher.DECRYPT_MODE);
_fingerHelper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
boxFingerprint.setVisibility(View.VISIBLE);
// find a fingerprint slot with an id that matches an alias in the keystore
for (FingerprintSlot slot : _slots.findAll(FingerprintSlot.class)) {
String id = slot.getID();
KeyStoreHandle handle = new KeyStoreHandle();
if (handle.containsKey(id)) {
SecretKey key = handle.getKey(id);
// if 'key' is null, it was permanently invalidated
if (key == null) {
invalidated = true;
continue;
}
_fingerCipher = Slot.createCipher(key, Cipher.DECRYPT_MODE);
_fingerHelper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
boxFingerprint.setVisibility(View.VISIBLE);
invalidated = false;
break;
}
}
} catch (Exception e) {
throw new UndeclaredThrowableException(e);
}
// display a help message if a matching invalidated keystore entry was found
if (invalidated) {
boxFingerprintInfo.setVisibility(View.VISIBLE);
}
}
Button button = findViewById(R.id.button_decrypt);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
char[] password = AuthHelper.getPassword(_textPassword, true);
char[] password = EditTextHelper.getEditTextChars(_textPassword);
trySlots(PasswordSlot.class, password);
}
});
@ -98,12 +114,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
builder.setTitle("Decryption error");
builder.setMessage("Master key integrity check failed for every slot. Make sure you didn't mistype your password.");
builder.setCancelable(false);
builder.setPositiveButton("OK",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
});
builder.setPositiveButton(android.R.string.ok, null);
builder.create().show();
}

View file

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

View file

@ -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) {

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 {

View file

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

View file

@ -1,4 +1,4 @@
package me.impy.aegis;
package me.impy.aegis.ui.tasks;
import android.app.ProgressDialog;
import android.content.Context;

View file

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

View file

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

View file

@ -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) {

View file

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

View file

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

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

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

View file

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