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

@ -15,7 +15,7 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<activity <activity
android:name=".MainActivity" android:name=".ui.MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/AppTheme.Default.NoActionBar" android:theme="@style/AppTheme.Default.NoActionBar"
android:alwaysRetainTaskState="true" android:alwaysRetainTaskState="true"
@ -26,28 +26,33 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".ScannerActivity" android:name=".ui.ScannerActivity"
android:label="Scan a QR code" android:label="Scan a QR code"
android:theme="@style/AppTheme.Fullscreen" android:theme="@style/AppTheme.Fullscreen"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:configChanges="keyboardHidden|orientation|screenSize"> android:configChanges="keyboardHidden|orientation|screenSize">
</activity> </activity>
<activity <activity
android:name=".EditProfileActivity" android:name=".ui.EditProfileActivity"
android:label="Edit profile" android:label="Edit profile"
android:theme="@style/AppTheme.Default.TransparentActionBar"> android:theme="@style/AppTheme.Default.TransparentActionBar">
</activity> </activity>
<activity <activity
android:name=".IntroActivity" android:name=".ui.IntroActivity"
android:theme="@style/Theme.Intro" android:theme="@style/Theme.Intro"
android:launchMode="singleTop"> android:launchMode="singleTop">
</activity> </activity>
<activity <activity
android:name=".AuthActivity" android:name=".ui.AuthActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/AppTheme.Default.NoActionBar"> android:theme="@style/AppTheme.Default.NoActionBar">
</activity> </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> </activity>
</application> </application>
</manifest> </manifest>

View file

@ -13,6 +13,7 @@ import android.support.annotation.RequiresApi;
import java.util.Collections; import java.util.Collections;
import me.impy.aegis.db.DatabaseManager; import me.impy.aegis.db.DatabaseManager;
import me.impy.aegis.ui.MainActivity;
public class AegisApplication extends Application { public class AegisApplication extends Application {
private boolean _running = false; 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 { public static SecretKey deriveKey(char[] password, byte[] salt, int n, int r, int p) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] bytes = toBytes(password); byte[] bytes = toBytes(password);
byte[] keyBytes = SCrypt.generate(bytes, salt, n, r, p, CRYPTO_KEY_SIZE); byte[] keyBytes = SCrypt.generate(bytes, salt, n, r, p, CRYPTO_KEY_SIZE);
zero(bytes); return new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
SecretKey key = new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
zero(keyBytes);
return key;
} }
public static Cipher createCipher(SecretKey key, int opmode) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException { public static Cipher createCipher(SecretKey key, int opmode) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
@ -102,7 +99,6 @@ public class CryptoUtils {
byte[] bytes = key.getEncoded(); byte[] bytes = key.getEncoded();
hash.update(bytes); hash.update(bytes);
CryptoUtils.zero(bytes);
return hash.digest(); return hash.digest();
} }
@ -120,21 +116,13 @@ public class CryptoUtils {
return generateRandomBytes(CRYPTO_NONCE_SIZE); return generateRandomBytes(CRYPTO_NONCE_SIZE);
} }
private static byte[] generateRandomBytes(int length) { public static byte[] generateRandomBytes(int length) {
SecureRandom random = new SecureRandom(); SecureRandom random = new SecureRandom();
byte[] data = new byte[length]; byte[] data = new byte[length];
random.nextBytes(data); random.nextBytes(data);
return 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) { private static byte[] toBytes(char[] chars) {
CharBuffer charBuf = CharBuffer.wrap(chars); CharBuffer charBuf = CharBuffer.wrap(chars);
ByteBuffer byteBuf = Charset.forName("UTF-8").encode(charBuf); ByteBuffer byteBuf = Charset.forName("UTF-8").encode(charBuf);

View file

@ -1,41 +1,53 @@
package me.impy.aegis.crypto; package me.impy.aegis.crypto;
import android.annotation.SuppressLint;
import android.os.Build; import android.os.Build;
import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties; import android.security.keystore.KeyProperties;
import java.io.IOException; import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.KeyStoreException; import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException; import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator; import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
public class KeyStoreHandle { public class KeyStoreHandle {
private final KeyStore _keyStore; private final KeyStore _keyStore;
private static final String KEY_NAME = "AegisKey";
private static final String STORE_NAME = "AndroidKeyStore"; private static final String STORE_NAME = "AndroidKeyStore";
public KeyStoreHandle() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { public KeyStoreHandle() throws KeyStoreHandleException {
_keyStore = KeyStore.getInstance(STORE_NAME); try {
_keyStore.load(null); _keyStore = KeyStore.getInstance(STORE_NAME);
_keyStore.load(null);
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
throw new KeyStoreHandleException(e);
}
} }
public boolean keyExists() throws KeyStoreException { public boolean containsKey(String id) throws KeyStoreHandleException {
return _keyStore.containsAlias(KEY_NAME); try {
return _keyStore.containsAlias(id);
} catch (KeyStoreException e) {
throw new KeyStoreHandleException(e);
}
} }
public SecretKey generateKey(boolean authRequired) throws Exception { public SecretKey generateKey(String id) throws Exception {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (isSupported()) {
KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, STORE_NAME); 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) KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_ECB) .setBlockModes(KeyProperties.BLOCK_MODE_ECB)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(authRequired) .setUserAuthenticationRequired(true)
.setRandomizedEncryptionRequired(false) .setRandomizedEncryptionRequired(false)
.setKeySize(CryptoUtils.CRYPTO_KEY_SIZE * 8) .setKeySize(CryptoUtils.CRYPTO_KEY_SIZE * 8)
.build()); .build());
@ -46,7 +58,32 @@ public class KeyStoreHandle {
} }
} }
public SecretKey getKey() throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException { public SecretKey getKey(String id)
return (SecretKey) _keyStore.getKey(KEY_NAME, null); 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 List<DatabaseEntry> _entries = new ArrayList<>();
private long _counter = 0; private long _counter = 0;
public byte[] serialize() throws Exception { public JSONObject serialize() throws Exception {
return serialize(false);
}
public byte[] serialize(boolean pretty) throws Exception {
JSONArray array = new JSONArray(); JSONArray array = new JSONArray();
for (DatabaseEntry e : _entries) { for (DatabaseEntry e : _entries) {
array.put(e.serialize()); array.put(e.serialize());
@ -26,18 +22,14 @@ public class Database {
JSONObject obj = new JSONObject(); JSONObject obj = new JSONObject();
obj.put("version", VERSION); obj.put("version", VERSION);
obj.put("entries", array); obj.put("entries", array);
return obj;
String string = pretty ? obj.toString(4) : obj.toString();
return string.getBytes("UTF-8");
} }
public void deserialize(byte[] data) throws Exception { public void deserialize(JSONObject obj) throws Exception {
deserialize(data, true); deserialize(obj, true);
} }
public void deserialize(byte[] data, boolean incCount) throws Exception { public void deserialize(JSONObject obj, boolean incCount) throws Exception {
JSONObject obj = new JSONObject(new String(data, "UTF-8"));
// TODO: support different VERSION deserialization providers // TODO: support different VERSION deserialization providers
int ver = obj.getInt("version"); int ver = obj.getInt("version");
if (ver != VERSION) { if (ver != VERSION) {

View file

@ -1,163 +1,116 @@
package me.impy.aegis.db; package me.impy.aegis.db;
import java.io.ByteArrayOutputStream; import android.util.Base64;
import java.io.DataOutputStream;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.UndeclaredThrowableException; import java.io.UnsupportedEncodingException;
import java.util.Arrays; 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.CryptParameters;
import me.impy.aegis.crypto.slots.SlotCollection; import me.impy.aegis.crypto.CryptResult;
import me.impy.aegis.crypto.CryptoUtils; import me.impy.aegis.crypto.MasterKey;
import me.impy.aegis.util.LittleByteBuffer; import me.impy.aegis.db.slots.SlotCollection;
import me.impy.aegis.encoding.Hex;
public class DatabaseFile { public class DatabaseFile {
private static final byte SECTION_ENCRYPTION_PARAMETERS = 0x00; public static final byte VERSION = 1;
private static final byte SECTION_SLOTS = 0x01;
private static final byte SECTION_END = (byte) 0xFF;
private static final byte VERSION = 1;
private final byte[] HEADER; private Object _content;
private byte[] _content;
private CryptParameters _cryptParameters; private CryptParameters _cryptParameters;
private SlotCollection _slots; private SlotCollection _slots;
public DatabaseFile() { public DatabaseFile() {
try {
HEADER = "AEGIS".getBytes("US_ASCII");
} catch (Exception e) {
throw new UndeclaredThrowableException(e);
}
_slots = new SlotCollection(); _slots = new SlotCollection();
} }
public byte[] serialize() throws IOException { public byte[] serialize() throws JSONException, UnsupportedEncodingException {
byte[] content = getContent(); JSONObject cryptObj = null;
CryptParameters cryptParams = getCryptParameters(); if (_cryptParameters != null) {
cryptObj = new JSONObject();
// this is dumb, java doesn't provide an endianness-aware data stream cryptObj.put("nonce", Hex.toString(_cryptParameters.Nonce));
ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); cryptObj.put("tag", Hex.toString(_cryptParameters.Tag));
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());
} }
if (!_slots.isEmpty()) { JSONObject headerObj = new JSONObject();
byte[] bytes = SlotCollection.serialize(_slots); headerObj.put("slots", _slots.isEmpty() ? JSONObject.NULL : SlotCollection.serialize(_slots));
writeSection(stream, SECTION_SLOTS, bytes); headerObj.put("params", cryptObj != null ? cryptObj : JSONObject.NULL);
}
writeSection(stream, SECTION_END, null); JSONObject obj = new JSONObject();
stream.write(content); obj.put("version", VERSION);
return byteStream.toByteArray(); 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 { public void deserialize(byte[] data) throws Exception {
LittleByteBuffer buffer = LittleByteBuffer.wrap(data); JSONObject obj = new JSONObject(new String(data, "UTF-8"));
JSONObject headerObj = obj.getJSONObject("header");
byte[] header = new byte[HEADER.length]; if (obj.getInt("version") > VERSION) {
buffer.get(header); throw new Exception("unsupported version");
if (!Arrays.equals(header, HEADER)) {
throw new Exception("Bad header");
} }
// TODO: support different version deserialization providers JSONObject slotObj = headerObj.optJSONObject("slots");
byte version = buffer.get(); if (slotObj != null) {
if (version != VERSION) { _slots = SlotCollection.deserialize(slotObj);
throw new Exception("Unsupported version");
} }
CryptParameters cryptParams = null; JSONObject cryptObj = headerObj.optJSONObject("params");
SlotCollection slots = new SlotCollection(); if (cryptObj != null) {
_cryptParameters = new CryptParameters() {{
for (section s = readSection(buffer); s.ID != SECTION_END; s = readSection(buffer)) { Nonce = Hex.toBytes(cryptObj.getString("nonce"));
LittleByteBuffer sBuff = LittleByteBuffer.wrap(s.Data); Tag = Hex.toBytes(cryptObj.getString("tag"));
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;
}
} }
setCryptParameters(cryptParams); if (cryptObj == null || slotObj == null) {
setSlots(slots); _content = obj.getJSONObject("db");
} else {
byte[] content = new byte[buffer.remaining()]; _content = obj.getString("db");
buffer.get(content); }
setContent(content);
} }
public boolean isEncrypted() { public boolean isEncrypted() {
return !_slots.isEmpty() && _cryptParameters != null; return !_slots.isEmpty() && _cryptParameters != null;
} }
private static void writeSection(DataOutputStream stream, byte id, byte[] data) throws IOException { public JSONObject getContent() {
stream.write(id); return (JSONObject) _content;
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);
}
} }
private static section readSection(LittleByteBuffer buffer) { public JSONObject getContent(MasterKey key)
section s = new section(); throws NoSuchPaddingException, InvalidKeyException,
s.ID = buffer.get(); NoSuchAlgorithmException, IllegalBlockSizeException,
BadPaddingException, InvalidAlgorithmParameterException, IOException, JSONException {
int len = buffer.getInt(); byte[] bytes = Base64.decode((String) _content, Base64.NO_WRAP);
s.Data = new byte[len]; CryptResult result = key.decrypt(bytes, _cryptParameters);
buffer.get(s.Data); return new JSONObject(new String(result.Data, "UTF-8"));
return s;
} }
private static void assertLength(byte[] bytes, int length) throws Exception { public void setContent(JSONObject dbObj) {
if (bytes.length != length) { _content = dbObj;
throw new Exception("Bad length");
}
} }
public byte[] getContent() { public void setContent(JSONObject dbObj, MasterKey key)
return _content; 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) { CryptResult result = key.encrypt(dbBytes);
_content = content; _content = new String(Base64.encode(result.Data, Base64.NO_WRAP), "UTF-8");
} _cryptParameters = result.Parameters;
public CryptParameters getCryptParameters() {
return _cryptParameters;
}
public void setCryptParameters(CryptParameters parameters) {
_cryptParameters = parameters;
} }
public SlotCollection getSlots() { public SlotCollection getSlots() {
@ -167,9 +120,4 @@ public class DatabaseFile {
public void setSlots(SlotCollection slots) { public void setSlots(SlotCollection slots) {
_slots = 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.content.Context;
import android.os.Environment; import android.os.Environment;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -16,9 +19,9 @@ import me.impy.aegis.crypto.CryptResult;
import me.impy.aegis.crypto.MasterKey; import me.impy.aegis.crypto.MasterKey;
public class DatabaseManager { public class DatabaseManager {
private static final String FILENAME = "aegis.db"; private static final String FILENAME = "aegis.json";
private static final String FILENAME_EXPORT = "aegis_export.db"; private static final String FILENAME_EXPORT = "aegis_export.json";
private static final String FILENAME_EXPORT_PLAIN = "aegis_export.json"; private static final String FILENAME_EXPORT_PLAIN = "aegis_export_plain.json";
private MasterKey _key; private MasterKey _key;
private DatabaseFile _file; private DatabaseFile _file;
@ -58,9 +61,9 @@ public class DatabaseManager {
_file.deserialize(fileBytes); _file.deserialize(fileBytes);
if (!_file.isEncrypted()) { if (!_file.isEncrypted()) {
byte[] contentBytes = _file.getContent(); JSONObject obj = _file.getContent();
_db = new Database(); _db = new Database();
_db.deserialize(contentBytes); _db.deserialize(obj);
} }
} }
@ -73,15 +76,13 @@ public class DatabaseManager {
public void unlock(MasterKey key) throws Exception { public void unlock(MasterKey key) throws Exception {
assertState(true, true); assertState(true, true);
byte[] encrypted = _file.getContent(); JSONObject obj = _file.getContent(key);
CryptParameters params = _file.getCryptParameters();
CryptResult result = key.decrypt(encrypted, params);
_db = new Database(); _db = new Database();
_db.deserialize(result.Data); _db.deserialize(obj);
_key = key; _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(); byte[] bytes = file.serialize();
FileOutputStream stream = null; FileOutputStream stream = null;
@ -98,26 +99,21 @@ public class DatabaseManager {
public void save() throws Exception { public void save() throws Exception {
assertState(false, true); assertState(false, true);
byte[] dbBytes = _db.serialize(); JSONObject obj = _db.serialize();
if (!_file.isEncrypted()) { if (_file.isEncrypted()) {
_file.setContent(dbBytes); _file.setContent(obj, _key);
} else { } else {
CryptResult result = _key.encrypt(dbBytes); _file.setContent(obj);
_file.setContent(result.Data);
_file.setCryptParameters(result.Parameters);
} }
save(_context, _file); save(_context, _file);
} }
public String export(boolean encrypt) throws Exception { public String export(boolean encrypt) throws Exception {
assertState(false, true); assertState(false, true);
byte[] bytes = _db.serialize(!encrypt); if (encrypt && getFile().isEncrypted()) {
encrypt = encrypt && getFile().isEncrypted(); _file.setContent(_db.serialize(), _key);
if (encrypt) { } else {
CryptResult result = _key.encrypt(bytes); _file.setContent(_db.serialize());
_file.setContent(result.Data);
_file.setCryptParameters(result.Parameters);
bytes = _file.serialize();
} }
File file; File file;
@ -129,6 +125,7 @@ public class DatabaseManager {
throw new IOException("error creating external storage directory"); throw new IOException("error creating external storage directory");
} }
byte[] bytes = _file.serialize();
file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN); file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN);
stream = new FileOutputStream(file); stream = new FileOutputStream(file);
stream.write(bytes); stream.write(bytes);
@ -167,6 +164,11 @@ public class DatabaseManager {
return _db.getKeys(); return _db.getKeys();
} }
public MasterKey getMasterKey() throws Exception {
assertState(false, true);
return _key;
}
public DatabaseFile getFile() { public DatabaseFile getFile() {
return _file; 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 class FingerprintSlot extends RawSlot {
public FingerprintSlot() {
super();
}
@Override @Override
public byte getType() { public byte getType() {
return TYPE_FINGERPRINT; 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.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException; import java.security.spec.InvalidKeySpecException;
@ -6,7 +9,7 @@ import java.security.spec.InvalidKeySpecException;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import me.impy.aegis.crypto.CryptoUtils; import me.impy.aegis.crypto.CryptoUtils;
import me.impy.aegis.util.LittleByteBuffer; import me.impy.aegis.encoding.Hex;
public class PasswordSlot extends RawSlot { public class PasswordSlot extends RawSlot {
private int _n; private int _n;
@ -19,27 +22,22 @@ public class PasswordSlot extends RawSlot {
} }
@Override @Override
public byte[] serialize() { public JSONObject serialize() throws JSONException {
byte[] bytes = super.serialize(); JSONObject obj = super.serialize();
LittleByteBuffer buffer = LittleByteBuffer.wrap(bytes); obj.put("n", _n);
buffer.position(super.getSize()); obj.put("r", _r);
buffer.putInt(_n); obj.put("p", _p);
buffer.putInt(_r); obj.put("salt", Hex.toString(_salt));
buffer.putInt(_p); return obj;
buffer.put(_salt);
return buffer.array();
} }
@Override @Override
public void deserialize(byte[] data) throws Exception { public void deserialize(JSONObject obj) throws Exception {
super.deserialize(data); super.deserialize(obj);
LittleByteBuffer buffer = LittleByteBuffer.wrap(data); _n = obj.getInt("n");
buffer.position(super.getSize()); _r = obj.getInt("r");
_n = buffer.getInt(); _p = obj.getInt("p");
_r = buffer.getInt(); _salt = Hex.toBytes(obj.getString("salt"));
_p = buffer.getInt();
_salt = new byte[CryptoUtils.CRYPTO_SALT_SIZE];
buffer.get(_salt);
} }
public SecretKey deriveKey(char[] password, byte[] salt, int n, int r, int p) throws InvalidKeySpecException, NoSuchAlgorithmException { 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); 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 @Override
public byte getType() { public byte getType() {
return TYPE_DERIVED; 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 android.annotation.SuppressLint;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.Serializable; import java.io.Serializable;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@ -15,30 +18,34 @@ import javax.crypto.spec.SecretKeySpec;
import me.impy.aegis.crypto.CryptoUtils; import me.impy.aegis.crypto.CryptoUtils;
import me.impy.aegis.crypto.MasterKey; import me.impy.aegis.crypto.MasterKey;
import me.impy.aegis.encoding.Hex;
public abstract class Slot implements Serializable { public abstract class Slot implements Serializable {
public final static byte TYPE_RAW = 0x00; public final static byte TYPE_RAW = 0x00;
public final static byte TYPE_DERIVED = 0x01; public final static byte TYPE_DERIVED = 0x01;
public final static byte TYPE_FINGERPRINT = 0x02; public final static byte TYPE_FINGERPRINT = 0x02;
public final static int ID_SIZE = 16;
protected byte[] _id;
protected byte[] _encryptedMasterKey; 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. // getKey decrypts the encrypted master key in this slot with the given key and returns it.
public SecretKey getKey(Cipher cipher) throws BadPaddingException, IllegalBlockSizeException { public SecretKey getKey(Cipher cipher) throws BadPaddingException, IllegalBlockSizeException {
byte[] decryptedKeyBytes = cipher.doFinal(_encryptedMasterKey); byte[] decryptedKeyBytes = cipher.doFinal(_encryptedMasterKey);
SecretKey decryptedKey = new SecretKeySpec(decryptedKeyBytes, CryptoUtils.CRYPTO_CIPHER_AEAD); return new SecretKeySpec(decryptedKeyBytes, CryptoUtils.CRYPTO_CIPHER_AEAD);
CryptoUtils.zero(decryptedKeyBytes);
return decryptedKey;
} }
// setKey encrypts the given master key with the given key and stores the result in this slot. // 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 { public void setKey(MasterKey masterKey, Cipher cipher) throws BadPaddingException, IllegalBlockSizeException {
byte[] masterKeyBytes = masterKey.getBytes(); byte[] masterKeyBytes = masterKey.getBytes();
_encryptedMasterKey = cipher.doFinal(masterKeyBytes); _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 // this is perfectly safe because we discard this cipher after passing CryptoUtils.CRYPTO_KEY_SIZE bytes through it
@SuppressLint("getInstance") @SuppressLint("getInstance")
public static Cipher createCipher(SecretKey key, int mode) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException { public static Cipher createCipher(SecretKey key, int mode) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException {
@ -47,10 +54,24 @@ public abstract class Slot implements Serializable {
return cipher; return cipher;
} }
public abstract int getSize(); public JSONObject serialize() throws JSONException {
public abstract byte getType(); 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 void deserialize(JSONObject obj) throws Exception {
public abstract byte[] serialize(); if (obj.getInt("type") != getType()) {
public abstract void deserialize(byte[] data) throws Exception; 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.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
@ -9,44 +13,38 @@ import java.util.List;
import javax.crypto.BadPaddingException; import javax.crypto.BadPaddingException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException; import javax.crypto.IllegalBlockSizeException;
import me.impy.aegis.crypto.CryptoUtils;
import me.impy.aegis.crypto.MasterKey; 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 { public class SlotCollection implements Iterable<Slot>, Serializable {
private List<Slot> _slots = new ArrayList<>(); private List<Slot> _slots = new ArrayList<>();
private byte[] _masterHash; private byte[] _masterHash;
public static byte[] serialize(SlotCollection slots) { public static JSONObject serialize(SlotCollection slots) throws JSONException {
// yep, no streams at this api level JSONObject obj = new JSONObject();
int size = 0; obj.put("hash", Hex.toString(slots.getMasterHash()));
for (Slot slot : slots) {
size += slot.getSize();
}
size += CryptoUtils.CRYPTO_HASH_SIZE;
LittleByteBuffer buffer = LittleByteBuffer.allocate(size);
buffer.put(slots.getMasterHash());
JSONArray entries = new JSONArray();
for (Slot slot : slots) { for (Slot slot : slots) {
byte[] bytes = slot.serialize(); entries.put(slot.serialize());
buffer.put(bytes);
} }
return buffer.array();
obj.put("entries", entries);
return obj;
} }
public static SlotCollection deserialize(byte[] data) throws Exception { public static SlotCollection deserialize(JSONObject obj) throws Exception {
LittleByteBuffer buffer = LittleByteBuffer.wrap(data);
byte[] masterHash = new byte[CryptoUtils.CRYPTO_HASH_SIZE];
buffer.get(masterHash);
SlotCollection slots = new SlotCollection(); SlotCollection slots = new SlotCollection();
byte[] masterHash = Hex.toBytes(obj.getString("hash"));
slots.setMasterHash(masterHash); slots.setMasterHash(masterHash);
while (buffer.remaining() > 0) { JSONArray entries = obj.getJSONArray("entries");
for (int i = 0; i < entries.length(); i++) {
Slot slot; Slot slot;
JSONObject slotObj = entries.getJSONObject(i);
switch (buffer.peek()) { switch (slotObj.getInt("type")) {
case Slot.TYPE_RAW: case Slot.TYPE_RAW:
slot = new RawSlot(); slot = new RawSlot();
break; break;
@ -60,10 +58,7 @@ public class SlotCollection implements Iterable<Slot>, Serializable {
throw new Exception("unrecognized slot type"); throw new Exception("unrecognized slot type");
} }
byte[] bytes = new byte[slot.getSize()]; slot.deserialize(slotObj);
buffer.get(bytes);
slot.deserialize(bytes);
slots.add(slot); 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 { public class SlotIntegrityException extends Exception {

View file

@ -87,9 +87,7 @@ public class Base32 {
base32[j++] = base32Chars.charAt(digit); base32[j++] = base32Chars.charAt(digit);
} }
char[] res = Arrays.copyOf(base32, j); return Arrays.copyOf(base32, j);
CryptoUtils.zero(base32);
return res;
} }
/** /**

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; import me.impy.aegis.crypto.CryptoUtils;
public class AuthHelper { public class EditTextHelper {
private AuthHelper() { private EditTextHelper() {
} }
public static char[] getPassword(EditText text, boolean clear) { public static void clearEditText(EditText text) {
char[] password = getEditTextChars(text);
if (clear) {
clearPassword(text);
}
return password;
}
public static void clearPassword(EditText text) {
text.getText().clear(); text.getText().clear();
} }
@ -30,12 +22,9 @@ public class AuthHelper {
return chars; return chars;
} }
public static boolean arePasswordsEqual(EditText text1, EditText text2) { public static boolean areEditTextsEqual(EditText text1, EditText text2) {
char[] password = getEditTextChars(text1); char[] password = getEditTextChars(text1);
char[] passwordConfirm = getEditTextChars(text2); char[] passwordConfirm = getEditTextChars(text2);
boolean equal = password.length != 0 && Arrays.equals(password, passwordConfirm); return password.length != 0 && Arrays.equals(password, passwordConfirm);
CryptoUtils.zero(password);
CryptoUtils.zero(passwordConfirm);
return equal;
} }
} }

View file

@ -11,14 +11,16 @@ public class FingerprintHelper {
} }
public static FingerprintManager getManager(Context context) { public static FingerprintManager getManager(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (isSupported() && PermissionHelper.granted(context, Manifest.permission.USE_FINGERPRINT)) {
if (PermissionHelper.granted(context, Manifest.permission.USE_FINGERPRINT)) { FingerprintManager manager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
FingerprintManager manager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE); if (manager != null && manager.isHardwareDetected() && manager.hasEnrolledFingerprints()) {
if (manager != null && manager.isHardwareDetected() && manager.hasEnrolledFingerprints()) { return manager;
return manager;
}
} }
} }
return null; 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.Database;
import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.db.DatabaseFile;
import me.impy.aegis.util.ByteInputStream; import me.impy.aegis.util.ByteInputStream;
public class AegisImporter extends DatabaseImporter { public class AegisImporter extends DatabaseImporter {
@ -15,8 +16,10 @@ public class AegisImporter extends DatabaseImporter {
@Override @Override
public List<DatabaseEntry> convert() throws Exception { public List<DatabaseEntry> convert() throws Exception {
byte[] bytes = _stream.getBytes(); byte[] bytes = _stream.getBytes();
DatabaseFile file = new DatabaseFile();
file.deserialize(bytes);
Database db = new Database(); Database db = new Database();
db.deserialize(bytes, false); db.deserialize(file.getContent(), false);
return db.getKeys(); return db.getKeys();
} }

View file

@ -1,8 +1,10 @@
package me.impy.aegis; package me.impy.aegis.ui;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import me.impy.aegis.AegisApplication;
public abstract class AegisActivity extends AppCompatActivity { public abstract class AegisActivity extends AppCompatActivity {
private AegisApplication _app; 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.content.Intent;
import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintManager;
import android.os.Build; import android.os.Build;
@ -20,20 +19,19 @@ import java.lang.reflect.UndeclaredThrowableException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import me.impy.aegis.R;
import me.impy.aegis.crypto.KeyStoreHandle; import me.impy.aegis.crypto.KeyStoreHandle;
import me.impy.aegis.crypto.MasterKey; import me.impy.aegis.crypto.MasterKey;
import me.impy.aegis.crypto.slots.FingerprintSlot; import me.impy.aegis.db.slots.FingerprintSlot;
import me.impy.aegis.crypto.slots.PasswordSlot; import me.impy.aegis.db.slots.PasswordSlot;
import me.impy.aegis.crypto.slots.Slot; import me.impy.aegis.db.slots.Slot;
import me.impy.aegis.crypto.slots.SlotCollection; import me.impy.aegis.db.slots.SlotCollection;
import me.impy.aegis.helpers.FingerprintHelper; import me.impy.aegis.helpers.FingerprintHelper;
import me.impy.aegis.helpers.FingerprintUiHelper; 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 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 EditText _textPassword;
private SlotCollection _slots; private SlotCollection _slots;
@ -46,6 +44,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
setContentView(R.layout.activity_auth); setContentView(R.layout.activity_auth);
_textPassword = findViewById(R.id.text_password); _textPassword = findViewById(R.id.text_password);
LinearLayout boxFingerprint = findViewById(R.id.box_fingerprint); LinearLayout boxFingerprint = findViewById(R.id.box_fingerprint);
LinearLayout boxFingerprintInfo = findViewById(R.id.box_fingerprint_info);
TextView textFingerprint = findViewById(R.id.text_fingerprint); TextView textFingerprint = findViewById(R.id.text_fingerprint);
SwirlView imgFingerprint = null; 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 // 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); FingerprintManager manager = FingerprintHelper.getManager(this);
if (manager != null && _slots.has(FingerprintSlot.class)) { if (manager != null && _slots.has(FingerprintSlot.class)) {
boolean invalidated = false;
try { try {
KeyStoreHandle handle = new KeyStoreHandle(); // find a fingerprint slot with an id that matches an alias in the keystore
if (handle.keyExists()) { for (FingerprintSlot slot : _slots.findAll(FingerprintSlot.class)) {
SecretKey key = handle.getKey(); String id = slot.getID();
_fingerCipher = Slot.createCipher(key, Cipher.DECRYPT_MODE); KeyStoreHandle handle = new KeyStoreHandle();
_fingerHelper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this); if (handle.containsKey(id)) {
boxFingerprint.setVisibility(View.VISIBLE); 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) { } catch (Exception e) {
throw new UndeclaredThrowableException(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 button = findViewById(R.id.button_decrypt);
button.setOnClickListener(new View.OnClickListener() { button.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
char[] password = AuthHelper.getPassword(_textPassword, true); char[] password = EditTextHelper.getEditTextChars(_textPassword);
trySlots(PasswordSlot.class, password); trySlots(PasswordSlot.class, password);
} }
}); });
@ -98,12 +114,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
builder.setTitle("Decryption error"); builder.setTitle("Decryption error");
builder.setMessage("Master key integrity check failed for every slot. Make sure you didn't mistype your password."); builder.setMessage("Master key integrity check failed for every slot. Make sure you didn't mistype your password.");
builder.setCancelable(false); builder.setCancelable(false);
builder.setPositiveButton("OK", builder.setPositiveButton(android.R.string.ok, null);
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
});
builder.create().show(); builder.create().show();
} }

View file

@ -1,4 +1,4 @@
package me.impy.aegis; package me.impy.aegis.ui;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
@ -18,14 +18,15 @@ import android.widget.Spinner;
import com.amulyakhare.textdrawable.TextDrawable; 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.KeyInfo;
import me.impy.aegis.crypto.KeyInfoException; import me.impy.aegis.crypto.KeyInfoException;
import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.encoding.Base32; import me.impy.aegis.encoding.Base32;
import me.impy.aegis.helpers.AuthHelper; import me.impy.aegis.helpers.EditTextHelper;
import me.impy.aegis.helpers.SpinnerHelper; import me.impy.aegis.helpers.SpinnerHelper;
import me.impy.aegis.helpers.TextDrawableHelper; import me.impy.aegis.helpers.TextDrawableHelper;
import me.impy.aegis.ui.views.KeyProfile;
public class EditProfileActivity extends AegisActivity { public class EditProfileActivity extends AegisActivity {
private boolean _isNew = false; private boolean _isNew = false;
@ -217,9 +218,8 @@ public class EditProfileActivity extends AegisActivity {
KeyInfo info = entry.getInfo(); KeyInfo info = entry.getInfo();
try { try {
char[] secret = AuthHelper.getEditTextChars(_textSecret); char[] secret = EditTextHelper.getEditTextChars(_textSecret);
info.setSecret(secret); info.setSecret(secret);
CryptoUtils.zero(secret);
info.setIssuer(_textIssuer.getText().toString()); info.setIssuer(_textIssuer.getText().toString());
info.setPeriod(period); info.setPeriod(period);
info.setDigits(digits); info.setDigits(digits);

View file

@ -1,4 +1,4 @@
package me.impy.aegis; package me.impy.aegis.ui;
import android.Manifest; import android.Manifest;
import android.content.Intent; 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.AppIntroFragment;
import com.github.paolorotolo.appintro.model.SliderPage; import com.github.paolorotolo.appintro.model.SliderPage;
import org.json.JSONObject;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.SecretKey; 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.CryptResult;
import me.impy.aegis.crypto.MasterKey; import me.impy.aegis.crypto.MasterKey;
import me.impy.aegis.crypto.slots.FingerprintSlot; import me.impy.aegis.db.slots.FingerprintSlot;
import me.impy.aegis.crypto.slots.PasswordSlot; import me.impy.aegis.db.slots.PasswordSlot;
import me.impy.aegis.crypto.slots.Slot; import me.impy.aegis.db.slots.Slot;
import me.impy.aegis.crypto.slots.SlotCollection; import me.impy.aegis.db.slots.SlotCollection;
import me.impy.aegis.db.Database; import me.impy.aegis.db.Database;
import me.impy.aegis.db.DatabaseFile; import me.impy.aegis.db.DatabaseFile;
import me.impy.aegis.db.DatabaseManager; 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 class IntroActivity extends AppIntro implements DerivationTask.Callback {
public static final int RESULT_OK = 0; public static final int RESULT_OK = 0;
@ -159,7 +166,7 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback {
try { try {
// encrypt the master key with the fingerprint key // encrypt the master key with the fingerprint key
// and add it to the list of slots // and add it to the list of slots
FingerprintSlot slot = new FingerprintSlot(); FingerprintSlot slot = _authenticatedSlide.getFingerSlot();
Cipher cipher = _authenticatedSlide.getFingerCipher(); Cipher cipher = _authenticatedSlide.getFingerCipher();
slots.encrypt(slot, masterKey, cipher); slots.encrypt(slot, masterKey, cipher);
slots.add(slot); slots.add(slot);
@ -171,13 +178,11 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback {
// finally, save the database // finally, save the database
try { try {
byte[] bytes = _database.serialize(); JSONObject obj = _database.serialize();
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) { if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
_databaseFile.setContent(bytes); _databaseFile.setContent(obj);
} else { } else {
CryptResult result = masterKey.encrypt(bytes); _databaseFile.setContent(obj, masterKey);
_databaseFile.setContent(result.Data);
_databaseFile.setCryptParameters(result.Parameters);
} }
DatabaseManager.save(getApplicationContext(), _databaseFile); DatabaseManager.save(getApplicationContext(), _databaseFile);
} catch (Exception e) { } catch (Exception e) {

View file

@ -1,4 +1,4 @@
package me.impy.aegis; package me.impy.aegis.ui;
import android.Manifest; import android.Manifest;
import android.content.ClipData; import android.content.ClipData;
@ -25,11 +25,16 @@ import java.io.InputStream;
import java.lang.reflect.UndeclaredThrowableException; import java.lang.reflect.UndeclaredThrowableException;
import java.util.List; import java.util.List;
import me.impy.aegis.AegisApplication;
import me.impy.aegis.R;
import me.impy.aegis.crypto.MasterKey; import me.impy.aegis.crypto.MasterKey;
import me.impy.aegis.db.slots.SlotCollection;
import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.db.DatabaseManager; import me.impy.aegis.db.DatabaseManager;
import me.impy.aegis.helpers.PermissionHelper; import me.impy.aegis.helpers.PermissionHelper;
import me.impy.aegis.importers.DatabaseImporter; 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; import me.impy.aegis.util.ByteInputStream;
public class MainActivity extends AegisActivity implements KeyProfileView.Listener { 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_DECRYPT = 5;
private static final int CODE_IMPORT = 6; private static final int CODE_IMPORT = 6;
private static final int CODE_PREFERENCES = 7; private static final int CODE_PREFERENCES = 7;
private static final int CODE_SLOTS = 8;
// permission request codes // permission request codes
private static final int CODE_PERM_EXPORT = 0; private static final int CODE_PERM_EXPORT = 0;
@ -183,6 +189,8 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
case CODE_PREFERENCES: case CODE_PREFERENCES:
onPreferencesResult(resultCode, data); onPreferencesResult(resultCode, data);
break; 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) { private void onPreferencesResult(int resultCode, Intent data) {
// refresh the entire key profile list if needed // refresh the entire key profile list if needed
if (data.getBooleanExtra("needsRefresh", false)) { if (data.getBooleanExtra("needsRefresh", false)) {
@ -216,9 +234,28 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
// perform any pending actions // perform any pending actions
int action = data.getIntExtra("action", -1); int action = data.getIntExtra("action", -1);
switch (action) { switch (action) {
case PreferencesActivity.ACTION_IMPORT:
if (PermissionHelper.request(this, CODE_PERM_IMPORT, Manifest.permission.READ_EXTERNAL_STORAGE)) {
onImport();
}
break;
case PreferencesActivity.ACTION_EXPORT: case PreferencesActivity.ACTION_EXPORT:
onExport(); onExport();
break; 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) { private void addKey(KeyProfile profile) {
profile.refreshCode();
DatabaseEntry entry = profile.getEntry(); DatabaseEntry entry = profile.getEntry();
entry.setName(entry.getInfo().getAccountName()); entry.setName(entry.getInfo().getAccountName());
try { try {
@ -485,6 +520,9 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
setPreferredTheme(nightMode); setPreferredTheme(nightMode);
recreate(); recreate();
} }
// refresh all codes to prevent showing old ones
_keyProfileView.refresh();
} }
private BottomSheetDialog createBottomSheet(final KeyProfile profile) { private BottomSheetDialog createBottomSheet(final KeyProfile profile) {
@ -553,13 +591,9 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.action_settings: case R.id.action_settings:
Intent preferencesActivity = new Intent(this, PreferencesActivity.class); Intent intent = new Intent(this, PreferencesActivity.class);
startActivityForResult(preferencesActivity, CODE_PREFERENCES); intent.putExtra("encrypted", _db.getFile().isEncrypted());
return true; startActivityForResult(intent, CODE_PREFERENCES);
case R.id.action_import:
if (PermissionHelper.request(this, CODE_PERM_IMPORT, Manifest.permission.CAMERA)) {
onImport();
}
return true; return true;
case R.id.action_lock: case R.id.action_lock:
_keyProfileView.clearKeys(); _keyProfileView.clearKeys();

View file

@ -1,20 +1,26 @@
package me.impy.aegis; package me.impy.aegis.ui;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.Preference; import android.preference.Preference;
import android.preference.PreferenceFragment; import android.preference.PreferenceFragment;
import android.support.v7.app.AppCompatActivity;
import android.widget.Toast; import android.widget.Toast;
import me.impy.aegis.R;
public class PreferencesActivity extends AegisActivity { 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 @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(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 @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() { 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 @Override
public boolean onPreferenceClick(Preference preference) { public boolean onPreferenceClick(Preference preference) {
_result.putExtra("action", ACTION_EXPORT); _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"); Preference issuerPreference = findPreference("pref_issuer");
issuerPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { issuerPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override @Override

View file

@ -1,4 +1,4 @@
package me.impy.aegis; package me.impy.aegis.ui;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
@ -16,6 +16,7 @@ import me.dm7.barcodescanner.zxing.ZXingScannerView;
import me.impy.aegis.crypto.KeyInfo; import me.impy.aegis.crypto.KeyInfo;
import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.helpers.SquareFinderView; import me.impy.aegis.helpers.SquareFinderView;
import me.impy.aegis.ui.views.KeyProfile;
public class ScannerActivity extends AegisActivity implements ZXingScannerView.ResultHandler { public class ScannerActivity extends AegisActivity implements ZXingScannerView.ResultHandler {
private ZXingScannerView _scannerView; 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.Context;
import android.content.Intent; import android.content.Intent;
@ -23,10 +23,12 @@ import java.lang.reflect.UndeclaredThrowableException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import me.impy.aegis.R;
import me.impy.aegis.crypto.KeyStoreHandle; 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.FingerprintUiHelper;
import me.impy.aegis.helpers.AuthHelper; import me.impy.aegis.helpers.EditTextHelper;
public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiHelper.Callback, ISlidePolicy, ISlideSelectionListener { public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiHelper.Callback, ISlidePolicy, ISlideSelectionListener {
private int _cryptType; private int _cryptType;
@ -39,6 +41,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
private TextView _textFingerprint; private TextView _textFingerprint;
private FingerprintUiHelper _fingerHelper; private FingerprintUiHelper _fingerHelper;
private KeyStoreHandle _storeHandle; private KeyStoreHandle _storeHandle;
private FingerprintSlot _fingerSlot;
private Cipher _fingerCipher; private Cipher _fingerCipher;
private boolean _fingerAuthenticated; private boolean _fingerAuthenticated;
@ -65,14 +68,17 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
} }
public char[] getPassword() { public char[] getPassword() {
AuthHelper.clearPassword(_textPasswordConfirm); return EditTextHelper.getEditTextChars(_textPassword);
return AuthHelper.getPassword(_textPassword, true);
} }
public Cipher getFingerCipher() { public Cipher getFingerCipher() {
return _fingerCipher; return _fingerCipher;
} }
public FingerprintSlot getFingerSlot() {
return _fingerSlot;
}
public void setBgColor(int color) { public void setBgColor(int color) {
_bgColor = color; _bgColor = color;
} }
@ -93,13 +99,9 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
try { try {
if (_storeHandle == null) { if (_storeHandle == null) {
_storeHandle = new KeyStoreHandle(); _storeHandle = new KeyStoreHandle();
_fingerSlot = new FingerprintSlot();
} }
// TODO: consider regenerating the key here if it already exists key = _storeHandle.generateKey(_fingerSlot.getID());
if (!_storeHandle.keyExists()) {
key = _storeHandle.generateKey(true);
} else {
key = _storeHandle.getKey();
}
} catch (Exception e) { } catch (Exception e) {
throw new UndeclaredThrowableException(e); throw new UndeclaredThrowableException(e);
} }
@ -141,7 +143,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
} }
// intentional fallthrough // intentional fallthrough
case CustomAuthenticationSlide.CRYPT_TYPE_PASS: case CustomAuthenticationSlide.CRYPT_TYPE_PASS:
return AuthHelper.arePasswordsEqual(_textPassword, _textPasswordConfirm); return EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm);
default: default:
throw new RuntimeException(); throw new RuntimeException();
} }
@ -150,7 +152,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
@Override @Override
public void onUserIllegallyRequestedNextPage() { public void onUserIllegallyRequestedNextPage() {
String message; String message;
if (!AuthHelper.arePasswordsEqual(_textPassword, _textPasswordConfirm)) { if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
message = "Passwords should be equal and non-empty"; message = "Passwords should be equal and non-empty";
} else if (!_fingerAuthenticated) { } else if (!_fingerAuthenticated) {
message = "Register your fingerprint"; message = "Register your fingerprint";
@ -160,7 +162,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
View view = getView(); View view = getView();
if (view != null) { if (view != null) {
Snackbar snackbar = Snackbar.make(getView(), message, Snackbar.LENGTH_LONG); Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
snackbar.show(); snackbar.show();
} }
} }

View file

@ -1,4 +1,4 @@
package me.impy.aegis; package me.impy.aegis.ui.slides;
import android.content.Intent; import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintManager;
@ -14,6 +14,7 @@ import android.widget.TextView;
import com.github.paolorotolo.appintro.ISlidePolicy; import com.github.paolorotolo.appintro.ISlidePolicy;
import me.impy.aegis.R;
import me.impy.aegis.helpers.FingerprintHelper; import me.impy.aegis.helpers.FingerprintHelper;
public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy, RadioGroup.OnCheckedChangeListener { 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.content.Context;
import android.os.Process; import android.os.Process;
@ -6,7 +6,7 @@ import android.os.Process;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import me.impy.aegis.crypto.CryptoUtils; 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> { public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, SecretKey> {
private Callback _cb; private Callback _cb;
@ -24,7 +24,6 @@ public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, Se
try { try {
byte[] salt = CryptoUtils.generateSalt(); byte[] salt = CryptoUtils.generateSalt();
SecretKey key = params.Slot.deriveKey(params.Password, salt, CryptoUtils.CRYPTO_SCRYPT_N, CryptoUtils.CRYPTO_SCRYPT_r, CryptoUtils.CRYPTO_SCRYPT_p); 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; return key;
} catch (Exception e) { } catch (Exception e) {
return null; return null;
@ -37,12 +36,12 @@ public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, Se
_cb.onTaskFinished(key); _cb.onTaskFinished(key);
} }
static class Params { public static class Params {
public PasswordSlot Slot; public PasswordSlot Slot;
public char[] Password; public char[] Password;
} }
interface Callback { public interface Callback {
void onTaskFinished(SecretKey key); 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.app.ProgressDialog;
import android.content.Context; 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.content.Context;
import android.os.Process; import android.os.Process;
@ -8,13 +8,12 @@ import java.lang.reflect.UndeclaredThrowableException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import me.impy.aegis.crypto.CryptoUtils;
import me.impy.aegis.crypto.MasterKey; import me.impy.aegis.crypto.MasterKey;
import me.impy.aegis.crypto.slots.FingerprintSlot; import me.impy.aegis.db.slots.FingerprintSlot;
import me.impy.aegis.crypto.slots.PasswordSlot; import me.impy.aegis.db.slots.PasswordSlot;
import me.impy.aegis.crypto.slots.Slot; import me.impy.aegis.db.slots.Slot;
import me.impy.aegis.crypto.slots.SlotCollection; import me.impy.aegis.db.slots.SlotCollection;
import me.impy.aegis.crypto.slots.SlotIntegrityException; import me.impy.aegis.db.slots.SlotIntegrityException;
public class SlotCollectionTask<T extends Slot> extends ProgressDialogTask<SlotCollectionTask.Params, MasterKey> { public class SlotCollectionTask<T extends Slot> extends ProgressDialogTask<SlotCollectionTask.Params, MasterKey> {
private Callback _cb; private Callback _cb;
@ -42,7 +41,6 @@ public class SlotCollectionTask<T extends Slot> extends ProgressDialogTask<SlotC
if (slot instanceof PasswordSlot) { if (slot instanceof PasswordSlot) {
char[] password = (char[])params.Obj; char[] password = (char[])params.Obj;
SecretKey key = ((PasswordSlot)slot).deriveKey(password); SecretKey key = ((PasswordSlot)slot).deriveKey(password);
CryptoUtils.zero(password);
Cipher cipher = Slot.createCipher(key, Cipher.DECRYPT_MODE); Cipher cipher = Slot.createCipher(key, Cipher.DECRYPT_MODE);
masterKey = params.Slots.decrypt(slot, cipher); masterKey = params.Slots.decrypt(slot, cipher);
} else if (slot instanceof FingerprintSlot) { } else if (slot instanceof FingerprintSlot) {
@ -72,12 +70,12 @@ public class SlotCollectionTask<T extends Slot> extends ProgressDialogTask<SlotC
_cb.onTaskFinished(masterKey); _cb.onTaskFinished(masterKey);
} }
static class Params { public static class Params {
public SlotCollection Slots; public SlotCollection Slots;
public Object Obj; public Object Obj;
} }
interface Callback { public interface Callback {
void onTaskFinished(MasterKey key); 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.TextDrawable;
import com.amulyakhare.textdrawable.util.ColorGenerator;
import java.io.Serializable; import java.io.Serializable;
import java.lang.reflect.UndeclaredThrowableException; 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.support.v7.widget.RecyclerView;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -8,6 +8,7 @@ import android.view.ViewGroup;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import me.impy.aegis.R;
import me.impy.aegis.helpers.ItemTouchHelperAdapter; import me.impy.aegis.helpers.ItemTouchHelperAdapter;
public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileHolder> implements ItemTouchHelperAdapter { public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileHolder> implements ItemTouchHelperAdapter {
@ -54,6 +55,13 @@ public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileHolder> im
notifyItemChanged(position); notifyItemChanged(position);
} }
public void refresh() {
for (KeyProfile profile : _keyProfiles) {
profile.refreshCode();
}
notifyDataSetChanged();
}
private KeyProfile getKeyByID(long id) { private KeyProfile getKeyByID(long id) {
for (KeyProfile profile : _keyProfiles) { for (KeyProfile profile : _keyProfiles) {
if (profile.getEntry().getID() == id) { 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) { public void onBindViewHolder(final KeyProfileHolder holder, int position) {
final KeyProfile profile = _keyProfiles.get(position); final KeyProfile profile = _keyProfiles.get(position);
holder.setData(profile, _showIssuer); holder.setData(profile, _showIssuer);
holder.startUpdateLoop(); holder.startRefreshLoop();
holder.itemView.setOnClickListener(new View.OnClickListener() { holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { 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.animation.ObjectAnimator;
import android.graphics.Color; import android.graphics.Color;
@ -12,14 +12,15 @@ import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import com.amulyakhare.textdrawable.TextDrawable; import com.amulyakhare.textdrawable.TextDrawable;
import com.amulyakhare.textdrawable.util.ColorGenerator;
import me.impy.aegis.R;
public class KeyProfileHolder extends RecyclerView.ViewHolder { public class KeyProfileHolder extends RecyclerView.ViewHolder {
private TextView _profileName; private TextView _profileName;
private TextView _profileCode; private TextView _profileCode;
private TextView _profileIssuer; private TextView _profileIssuer;
private ImageView _profileDrawable; private ImageView _profileDrawable;
private KeyProfile _keyProfile; private KeyProfile _profile;
private ProgressBar _progressBar; private ProgressBar _progressBar;
private Handler _uiHandler; private Handler _uiHandler;
@ -39,10 +40,12 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder {
} }
public void setData(KeyProfile profile, boolean showIssuer) { public void setData(KeyProfile profile, boolean showIssuer) {
if ((_keyProfile = profile) == null) { if (profile == null) {
_profile = null;
_running = false; _running = false;
return; return;
} }
_profile = profile;
_profileName.setText(profile.getEntry().getName()); _profileName.setText(profile.getEntry().getName());
_profileCode.setText(profile.getCode()); _profileCode.setText(profile.getCode());
@ -55,36 +58,35 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder {
_profileDrawable.setImageDrawable(drawable); _profileDrawable.setImageDrawable(drawable);
} }
public void startUpdateLoop() { public void startRefreshLoop() {
if (_running) { if (_running) {
return; return;
} }
_running = true; _running = true;
updateCode(); refreshCode();
_uiHandler.postDelayed(new Runnable() { _uiHandler.postDelayed(new Runnable() {
@Override @Override
public void run() { public void run() {
if (_running) { if (_running) {
updateCode(); refreshCode();
_uiHandler.postDelayed(this, _keyProfile.getEntry().getInfo().getMillisTillNextRotation()); _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 // reset the progress bar
int maxProgress = _progressBar.getMax(); int maxProgress = _progressBar.getMax();
_progressBar.setProgress(maxProgress); _progressBar.setProgress(maxProgress);
// refresh the code
String otp = _keyProfile.refreshCode();
_profileCode.setText(otp.substring(0, otp.length() / 2) + " " + otp.substring(otp.length() / 2)); _profileCode.setText(otp.substring(0, otp.length() / 2) + " " + otp.substring(otp.length() / 2));
// calculate the progress the bar should start at // calculate the progress the bar should start at
long millisTillRotation = _keyProfile.getEntry().getInfo().getMillisTillNextRotation(); long millisTillRotation = _profile.getEntry().getInfo().getMillisTillNextRotation();
long period = _keyProfile.getEntry().getInfo().getPeriod() * maxProgress; long period = _profile.getEntry().getInfo().getPeriod() * maxProgress;
int currentProgress = maxProgress - (int) ((((double) period - millisTillRotation) / period) * maxProgress); int currentProgress = maxProgress - (int) ((((double) period - millisTillRotation) / period) * maxProgress);
// start progress animation // start progress animation
@ -92,6 +94,5 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder {
animation.setDuration(millisTillRotation); animation.setDuration(millisTillRotation);
animation.setInterpolator(new LinearInterpolator()); animation.setInterpolator(new LinearInterpolator());
animation.start(); 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.os.Bundle;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
@ -9,6 +9,7 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import me.impy.aegis.R;
import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.helpers.SimpleItemTouchHelperCallback; import me.impy.aegis.helpers.SimpleItemTouchHelperCallback;
@ -83,6 +84,10 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
_adapter.replaceKey(profile); _adapter.replaceKey(profile);
} }
public void refresh() {
_adapter.refresh();
}
public interface Listener { public interface Listener {
void onEntryClick(KeyProfile profile); void onEntryClick(KeyProfile profile);
void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2); 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)); }
}

View file

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="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>

View file

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
</vector>

View file

@ -5,7 +5,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
tools:context="me.impy.aegis.AuthActivity"> tools:context="me.impy.aegis.ui.AuthActivity">
<LinearLayout <LinearLayout
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -48,7 +48,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:visibility="invisible"> android:visibility="gone">
<LinearLayout <LinearLayout
android:id="@+id/img_fingerprint_insert" android:id="@+id/img_fingerprint_insert"
@ -62,8 +62,27 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginLeft="15dp" android:layout_marginLeft="15dp"
android:text="Touch sensor" android:text="@string/fingerprint_hint"
android:textColor="?attr/secondaryText"/> android:textColor="?attr/secondaryText"/>
</LinearLayout> </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 &quot;Settings -> Key slots&quot; to readd your fingerprint."/>
</LinearLayout>
</LinearLayout> </LinearLayout>
</android.support.design.widget.CoordinatorLayout> </android.support.design.widget.CoordinatorLayout>

View file

@ -1,188 +1,183 @@
<?xml version="1.0" encoding="utf-8"?> <?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" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:orientation="vertical">
<ScrollView <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content"
android:orientation="vertical"
android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true">
<LinearLayout <RelativeLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@color/colorPrimary">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:id="@+id/profile_drawable"
android:layout_centerInParent="true"/>
</RelativeLayout>
<TableLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/background"
android:orientation="vertical" android:orientation="vertical"
android:descendantFocusability="beforeDescendants" android:stretchColumns="1"
android:focusableInTouchMode="true"> android:layout_marginEnd="35dp">
<RelativeLayout <TableRow
android:layout_width="match_parent" android:layout_marginTop="5dp"
android:layout_height="200dp" android:layout_marginBottom="5dp">
android:background="@color/colorPrimary"> <ImageView android:layout_column="0"
<ImageView android:layout_width="wrap_content"
android:layout_width="100dp" android:layout_height="wrap_content"
android:layout_height="100dp" android:src="@drawable/ic_person_black_24dp"
android:id="@+id/profile_drawable" android:layout_marginTop="10dp"
android:layout_centerInParent="true"/> android:tint="@color/cardview_dark_background"
android:layout_marginStart="15dp"
android:layout_marginEnd="20dp"/>
</RelativeLayout> <EditText android:layout_column="1"
android:id="@+id/text_name"
android:hint="Name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</TableRow>
<TableLayout <TableRow
android:layout_width="match_parent" android:layout_marginTop="5dp"
android:layout_height="wrap_content" android:layout_marginBottom="5dp">
android:background="?attr/background" <EditText android:layout_column="1"
android:orientation="vertical" android:id="@+id/text_issuer"
android:stretchColumns="1" android:hint="Issuer"
android:layout_marginEnd="35dp"> android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</TableRow>
<TableRow <TableRow
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"> android:layout_marginBottom="5dp">
<ImageView android:layout_column="0" <ImageView android:layout_column="0"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_info_outline_black_24dp"
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="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal"
android:gravity="center_vertical">
<Spinner
android:id="@+id/spinner_type"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:src="@drawable/ic_person_black_24dp" android:layout_weight="4"
android:layout_weight="1" style="@style/Base.Widget.AppCompat.Spinner.Underlined"/>
android:layout_marginTop="10dp" <Spinner
android:tint="@color/cardview_dark_background" android:id="@+id/spinner_algo"
android:layout_marginStart="15dp" android:layout_width="0dp"
android:layout_marginEnd="20dp"/>
<EditText android:layout_column="1"
android:id="@+id/text_name"
android:hint="Name"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</TableRow>
<TableRow
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp">
<EditText android:layout_column="1"
android:id="@+id/text_issuer"
android:hint="Issuer"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</TableRow>
<TableRow
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp">
<ImageView android:layout_column="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:src="@drawable/ic_info_outline_black_24dp" android:layout_weight="4"
android:layout_weight="1" style="@style/Base.Widget.AppCompat.Spinner.Underlined"/>
android:layout_marginTop="10dp" <Spinner
android:tint="@color/cardview_dark_background" android:id="@+id/spinner_digits"
android:layout_marginStart="15dp" android:layout_width="0dp"
android:layout_marginEnd="20dp"/> android:layout_height="wrap_content"
android:layout_weight="3"
style="@style/Base.Widget.AppCompat.Spinner.Underlined"/>
</LinearLayout>
<LinearLayout android:layout_column="1" </TableRow>
<TableRow
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp">
<ImageView android:layout_column="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_timelapse_black_24dp"
android:layout_marginTop="10dp"
android:tint="@color/cardview_dark_background"
android:layout_marginStart="15dp"
android:layout_marginEnd="20dp"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal">
<RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<Spinner
android:id="@+id/spinner_type"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="4"
style="@style/Base.Widget.AppCompat.Spinner.Underlined"/>
<Spinner
android:id="@+id/spinner_algo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="4"
style="@style/Base.Widget.AppCompat.Spinner.Underlined"/>
<Spinner
android:id="@+id/spinner_digits"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
style="@style/Base.Widget.AppCompat.Spinner.Underlined"/>
</LinearLayout>
</TableRow>
<TableRow
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp">
<ImageView android:layout_column="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_timelapse_black_24dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginTop="10dp" android:orientation="vertical">
android:tint="@color/cardview_dark_background" <EditText
android:layout_marginStart="15dp" android:id="@+id/text_period"
android:layout_marginEnd="20dp"/> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_gravity="center"
android:inputType="numberDecimal"/>
<TextView
android:id="@+id/text_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:text="seconds"
android:textSize="18sp"
android:layout_marginEnd="10dp"
android:textColor="#808080"/>
</RelativeLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:layout_weight="1"/>
<RelativeLayout </LinearLayout>
android:layout_width="match_parent" </TableRow>
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<EditText
android:id="@+id/text_period"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_gravity="center"
android:inputType="numberDecimal"/>
<TextView
android:id="@+id/text_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:text="seconds"
android:textSize="18sp"
android:layout_marginEnd="10dp"
android:textColor="#808080"/>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>
</TableRow>
<TableRow <TableRow
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"> android:layout_marginBottom="5dp">
<ImageView android:layout_column="0" <ImageView android:layout_column="0"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:src="@drawable/ic_vpn_key_black_24dp" android:src="@drawable/ic_vpn_key_black_24dp"
android:layout_weight="1" android:layout_marginTop="10dp"
android:layout_marginTop="10dp" android:tint="@color/cardview_dark_background"
android:tint="@color/cardview_dark_background" android:layout_marginStart="15dp"
android:layout_marginStart="15dp" android:layout_marginEnd="20dp"/>
android:layout_marginEnd="20dp"/>
<android.support.design.widget.TextInputLayout <android.support.design.widget.TextInputLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
app:hintEnabled="false"
app:passwordToggleEnabled="true">
<android.support.design.widget.TextInputEditText
android:id="@+id/text_secret"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:hintEnabled="false" android:hint="Secret (base32)"
app:passwordToggleEnabled="true"> android:inputType="textPassword"/>
</android.support.design.widget.TextInputLayout>
</TableRow>
<android.support.design.widget.TextInputEditText </TableLayout>
android:id="@+id/text_secret"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Secret (base32)"
android:inputType="textPassword"/>
</android.support.design.widget.TextInputLayout>
</TableRow>
</TableLayout> </LinearLayout>
</ScrollView>
</LinearLayout>
</ScrollView>
</LinearLayout>

View file

@ -5,6 +5,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
tools:context="me.impy.aegis.IntroActivity"> tools:context="me.impy.aegis.ui.IntroActivity">
</android.support.design.widget.CoordinatorLayout> </android.support.design.widget.CoordinatorLayout>

View file

@ -5,7 +5,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
tools:context="me.impy.aegis.MainActivity"> tools:context="me.impy.aegis.ui.MainActivity">
<android.support.design.widget.AppBarLayout <android.support.design.widget.AppBarLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -22,7 +22,7 @@
</android.support.design.widget.AppBarLayout> </android.support.design.widget.AppBarLayout>
<fragment <fragment
android:name="me.impy.aegis.KeyProfileView" android:name="me.impy.aegis.ui.views.KeyProfileView"
android:id="@+id/key_profiles" android:id="@+id/key_profiles"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -9,6 +9,6 @@
android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context="me.impy.aegis.ScannerActivity"> tools:context="me.impy.aegis.ui.ScannerActivity">
</RelativeLayout> </RelativeLayout>

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

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

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

View file

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

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

View file

@ -95,7 +95,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginLeft="15dp" android:layout_marginLeft="15dp"
android:text="Touch sensor"/> android:text="@string/fingerprint_hint"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View file

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

View file

@ -1,17 +1,12 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context="me.impy.aegis.MainActivity"> tools:context="me.impy.aegis.ui.MainActivity">
<item <item
android:id="@+id/action_lock" android:id="@+id/action_lock"
android:icon="@drawable/ic_lock" android:icon="@drawable/ic_lock"
app:showAsAction="ifRoom" app:showAsAction="ifRoom"
android:title=""/> android:title=""/>
<item
android:id="@+id/action_import"
android:orderInCategory="100"
android:title="@string/action_import"
app:showAsAction="never" />
<item <item
android:id="@+id/action_settings" android:id="@+id/action_settings"
android:orderInCategory="100" android:orderInCategory="100"

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

View file

@ -6,11 +6,20 @@
<string name="discard">Discard</string> <string name="discard">Discard</string>
<string name="save">Save</string> <string name="save">Save</string>
<string name="title_activity_intro">IntroActivity</string> <string name="title_activity_intro">IntroActivity</string>
<string name="settings">Preferences</string> <string name="settings">Preferences</string>
<string name="pref_night_mode">Night mode</string> <string name="pref_night_mode_title">Night mode</string>
<string name="pref_night_mode_description">Enable this to use darker colors</string> <string name="pref_night_mode_summary">Enable this to use darker colors</string>
<string name="pref_issuers">Show the issuer</string> <string name="pref_issuer_title">Show the issuer</string>
<string name="pref_issuers_description">Enable this to show the issuer next to the profile name</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_hint">Touch sensor</string>
<string name="fingerprint_not_recognized">Fingerprint not recognized. Try again</string> <string name="fingerprint_not_recognized">Fingerprint not recognized. Try again</string>

View file

@ -2,21 +2,46 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:title="@string/settings"> android:title="@string/settings">
<CheckBoxPreference <PreferenceCategory
android:defaultValue="false" android:title="Appearance">
android:key="pref_night_mode" <CheckBoxPreference
android:title="@string/pref_night_mode" android:defaultValue="false"
android:summary="@string/pref_night_mode_description"/> android:key="pref_night_mode"
android:title="@string/pref_night_mode_title"
android:summary="@string/pref_night_mode_summary"/>
<CheckBoxPreference <CheckBoxPreference
android:defaultValue="false" android:defaultValue="false"
android:key="pref_issuer" android:key="pref_issuer"
android:title="@string/pref_issuers" android:title="@string/pref_issuer_title"
android:summary="@string/pref_issuers_description"/> android:summary="@string/pref_issuer_summary"/>
</PreferenceCategory>
<Preference <PreferenceCategory
android:key="pref_export" android:title="Security">
android:title="Export" <EditTextPreference
android:summary="Export the database"/> 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="@string/pref_export_title"
android:summary="@string/pref_export_summary"/>
</PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View file

@ -70,6 +70,7 @@ A slot has the following structure.
| Length | Contents | | Length | Contents |
|:-------|:--------------------| |:-------|:--------------------|
| `1` | `uint8_t` Type | | `1` | `uint8_t` Type |
| `16` | ID |
| `32` | Encrypted key | | `32` | Encrypted key |
| `?` | Additional data | | `?` | Additional data |

View file

@ -1,45 +1,52 @@
{ {
"version": 1, "version": 1,
"entries": [ "header": {
{ "slots": null,
"name": "Bob", "params": null
"url": "otpauth:\/\/totp\/Google%3ABob?digits=6&period=30&algorithm=SHA1&secret=KD3WHMYQ7K7DGV5QFJNN2ZUATBC44C7F&issuer=Google" },
}, "db": {
{ "version": 1,
"name": "Henk", "entries": [
"url": "otpauth:\/\/totp\/Scaleway%3AHenk?digits=6&period=10&algorithm=SHA256&secret=MBQX47S3XOSOF7YT2CO3LZNACULZNY4P&issuer=Scaleway" {
}, "name": "Bob",
{ "url": "otpauth:\/\/totp\/Google%3ABob?digits=6&period=30&algorithm=SHA1&secret=KD3WHMYQ7K7DGV5QFJNN2ZUATBC44C7F&issuer=Google"
"name": "Oh wait, they don't have TOTP", },
"url": "otpauth:\/\/totp\/Namecheap%3AOh%20wait%2C%20they%20don't%20have%20TOTP?digits=8&period=30&algorithm=SHA512&secret=HIMV4HX3REXMUG236OOLN4GS7URI247Y&issuer=Namecheap" {
}, "name": "Henk",
{ "url": "otpauth:\/\/totp\/Scaleway%3AHenk?digits=6&period=10&algorithm=SHA256&secret=MBQX47S3XOSOF7YT2CO3LZNACULZNY4P&issuer=Scaleway"
"name": "Delete Me", },
"url": "otpauth:\/\/totp\/Facebook%3ADelete%20Me?digits=6&period=60&algorithm=SHA1&secret=GSQ7E5FO3UFFVANS3NSMFE7RTMRMYNCV&issuer=Facebook" {
}, "name": "Oh wait, they don't have TOTP",
{ "url": "otpauth:\/\/totp\/Namecheap%3AOh%20wait%2C%20they%20don't%20have%20TOTP?digits=8&period=30&algorithm=SHA512&secret=HIMV4HX3REXMUG236OOLN4GS7URI247Y&issuer=Namecheap"
"name": "nodejsc0d3r", },
"url": "otpauth:\/\/totp\/Github%3Anodejsc0d3r?digits=6&period=50&algorithm=SHA1&secret=642GK5EROWKUAEBFRH6FWSMM4FRKS5IA&issuer=Github" {
}, "name": "Delete Me",
{ "url": "otpauth:\/\/totp\/Facebook%3ADelete%20Me?digits=6&period=60&algorithm=SHA1&secret=GSQ7E5FO3UFFVANS3NSMFE7RTMRMYNCV&issuer=Facebook"
"name": "MoneroMan", },
"url": "otpauth:\/\/totp\/Poloniex%3AMoneroMan?digits=8&period=30&algorithm=SHA256&secret=NUSLOF6CFEWNPAARBVJ5WFA5YRRQU4FG&issuer=Poloniex" {
}, "name": "nodejsc0d3r",
{ "url": "otpauth:\/\/totp\/Github%3Anodejsc0d3r?digits=6&period=50&algorithm=SHA1&secret=642GK5EROWKUAEBFRH6FWSMM4FRKS5IA&issuer=Github"
"name": "Lil' Droplet", },
"url": "otpauth:\/\/totp\/DigitalOcean%3ALil'%20Droplet?digits=6&period=45&algorithm=SHA512&secret=6CAIGVYB5MQ6TSZLJ56HJBWU5S3H7FUC&issuer=DigitalOcean" {
}, "name": "MoneroMan",
{ "url": "otpauth:\/\/totp\/Poloniex%3AMoneroMan?digits=8&period=30&algorithm=SHA256&secret=NUSLOF6CFEWNPAARBVJ5WFA5YRRQU4FG&issuer=Poloniex"
"name": "givemeabucket", },
"url": "otpauth:\/\/totp\/Bitbucket%3Agivemeabucket?digits=6&period=20&algorithm=SHA1&secret=5KY3D2N53MRMIOMNKLLTNF3TDQJRDMEY&issuer=Bitbucket" {
}, "name": "Lil' Droplet",
{ "url": "otpauth:\/\/totp\/DigitalOcean%3ALil'%20Droplet?digits=6&period=45&algorithm=SHA512&secret=6CAIGVYB5MQ6TSZLJ56HJBWU5S3H7FUC&issuer=DigitalOcean"
"name": "Pepe", },
"url": "otpauth:\/\/totp\/4chan%3APepe?digits=8&period=30&algorithm=SHA1&secret=EIQMT7NHFYJUMBKQ35P34JGLG3MO7L2W&issuer=4chan" {
}, "name": "givemeabucket",
{ "url": "otpauth:\/\/totp\/Bitbucket%3Agivemeabucket?digits=6&period=20&algorithm=SHA1&secret=5KY3D2N53MRMIOMNKLLTNF3TDQJRDMEY&issuer=Bitbucket"
"name": "alex", },
"url": "otpauth:\/\/totp\/TTRSS%3Aalex?digits=6&period=30&algorithm=SHA1&secret=BEBXDJJVXKX3ZMAZLJUU5I5PONI4IYAL&issuer=TTRSS" {
} "name": "Pepe",
] "url": "otpauth:\/\/totp\/4chan%3APepe?digits=8&period=30&algorithm=SHA1&secret=EIQMT7NHFYJUMBKQ35P34JGLG3MO7L2W&issuer=4chan"
} },
{
"name": "alex",
"url": "otpauth:\/\/totp\/TTRSS%3Aalex?digits=6&period=30&algorithm=SHA1&secret=BEBXDJJVXKX3ZMAZLJUU5I5PONI4IYAL&issuer=TTRSS"
}
]
}
}