diff --git a/app/build.gradle b/app/build.gradle
index 5606dfbb..46cd7508 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -38,7 +38,7 @@ dependencies {
implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'com.android.support:support-v4:27.1.1'
implementation 'com.mattprecious.swirl:swirl:1.0.0'
- implementation 'com.madgag.spongycastle:core:1.56.0.0'
+ implementation 'com.madgag.spongycastle:core:1.58.0.0'
implementation 'com.github.apl-devs:appintro:v4.2.2'
implementation 'com.getbase:floatingactionbutton:1.10.1'
testImplementation 'junit:junit:4.12'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 26cf9e0a..7da6d8ae 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -34,7 +34,7 @@
android:configChanges="keyboardHidden|orientation|screenSize">
diff --git a/app/src/main/java/me/impy/aegis/crypto/CryptParameters.java b/app/src/main/java/me/impy/aegis/crypto/CryptParameters.java
index c5971102..7641c720 100644
--- a/app/src/main/java/me/impy/aegis/crypto/CryptParameters.java
+++ b/app/src/main/java/me/impy/aegis/crypto/CryptParameters.java
@@ -1,6 +1,46 @@
package me.impy.aegis.crypto;
-public class CryptParameters {
- public byte[] Nonce;
- public byte[] Tag;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.Serializable;
+
+import me.impy.aegis.encoding.Hex;
+import me.impy.aegis.encoding.HexException;
+
+public class CryptParameters implements Serializable {
+ private byte[] _nonce;
+ private byte[] _tag;
+
+ public CryptParameters(byte[] nonce, byte[] tag) {
+ _nonce = nonce;
+ _tag = tag;
+ }
+
+ public JSONObject toJson() {
+ JSONObject obj = new JSONObject();
+
+ try {
+ obj.put("nonce", Hex.encode(_nonce));
+ obj.put("tag", Hex.encode(_tag));
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+
+ return obj;
+ }
+
+ public static CryptParameters parseJson(JSONObject obj) throws JSONException, HexException {
+ byte[] nonce = Hex.decode(obj.getString("nonce"));
+ byte[] tag = Hex.decode(obj.getString("tag"));
+ return new CryptParameters(nonce, tag);
+ }
+
+ public byte[] getNonce() {
+ return _nonce;
+ }
+
+ public byte[] getTag() {
+ return _tag;
+ }
}
diff --git a/app/src/main/java/me/impy/aegis/crypto/CryptResult.java b/app/src/main/java/me/impy/aegis/crypto/CryptResult.java
index 661c2c3e..b285b7f1 100644
--- a/app/src/main/java/me/impy/aegis/crypto/CryptResult.java
+++ b/app/src/main/java/me/impy/aegis/crypto/CryptResult.java
@@ -1,6 +1,19 @@
package me.impy.aegis.crypto;
public class CryptResult {
- public CryptParameters Parameters;
- public byte[] Data;
+ private byte[] _data;
+ private CryptParameters _params;
+
+ public CryptResult(byte[] data, CryptParameters params) {
+ _data = data;
+ _params = params;
+ }
+
+ public byte[] getData() {
+ return _data;
+ }
+
+ public CryptParameters getParams() {
+ return _params;
+ }
}
diff --git a/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java b/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java
index 37eb361e..0c0190c3 100644
--- a/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java
+++ b/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java
@@ -1,17 +1,17 @@
package me.impy.aegis.crypto;
+import android.os.Build;
+
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.lang.reflect.UndeclaredThrowableException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
-import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
-import java.security.spec.InvalidKeySpecException;
+import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
@@ -20,20 +20,17 @@ import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.spongycastle.crypto.generators.SCrypt;
public class CryptoUtils {
- public static final String CRYPTO_HASH = "SHA-256";
-
- public static final String CRYPTO_CIPHER_RAW = "AES/ECB/NoPadding";
- public static final byte CRYPTO_KEY_SIZE = 32;
-
- public static final String CRYPTO_CIPHER_AEAD = "AES/GCM/NoPadding";
- public static final byte CRYPTO_TAG_SIZE = 16;
- public static final byte CRYPTO_NONCE_SIZE = 12;
+ public static final String CRYPTO_AEAD = "AES/GCM/NoPadding";
+ public static final byte CRYPTO_AEAD_KEY_SIZE = 32;
+ public static final byte CRYPTO_AEAD_TAG_SIZE = 16;
+ public static final byte CRYPTO_AEAD_NONCE_SIZE = 12;
public static final int CRYPTO_SCRYPT_N = 1 << 15;
public static final int CRYPTO_SCRYPT_r = 8;
@@ -41,23 +38,42 @@ public class CryptoUtils {
public static SecretKey deriveKey(char[] password, byte[] salt, int n, int r, int p) {
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_AEAD_KEY_SIZE);
return new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
}
- public static Cipher createCipher(SecretKey key, int opmode)
+ public static Cipher createEncryptCipher(SecretKey key)
throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, InvalidKeyException {
- byte[] nonce = generateNonce();
- return createCipher(key, opmode, nonce);
+ return createCipher(key, Cipher.ENCRYPT_MODE, null);
}
- public static Cipher createCipher(SecretKey key, int opmode, byte[] nonce)
+ public static Cipher createDecryptCipher(SecretKey key, byte[] nonce)
+ throws InvalidAlgorithmParameterException, NoSuchAlgorithmException,
+ InvalidKeyException, NoSuchPaddingException {
+ return createCipher(key, Cipher.DECRYPT_MODE, nonce);
+ }
+
+ private static Cipher createCipher(SecretKey key, int opmode, byte[] nonce)
throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, InvalidKeyException {
- IvParameterSpec spec = new IvParameterSpec(nonce);
- Cipher cipher = Cipher.getInstance(CRYPTO_CIPHER_AEAD);
- cipher.init(opmode, key, spec);
+ Cipher cipher = Cipher.getInstance(CRYPTO_AEAD);
+
+ // generate the nonce if none is given
+ // we are not allowed to do this ourselves as "setRandomizedEncryptionRequired" is set to true
+ if (nonce != null) {
+ AlgorithmParameterSpec spec;
+ // apparently kitkat doesn't support GCMParameterSpec
+ if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
+ spec = new IvParameterSpec(nonce);
+ } else {
+ spec = new GCMParameterSpec(CRYPTO_AEAD_TAG_SIZE * 8, nonce);
+ }
+ cipher.init(opmode, key, spec);
+ } else {
+ cipher.init(opmode, key);
+ }
+
return cipher;
}
@@ -65,16 +81,10 @@ public class CryptoUtils {
throws BadPaddingException, IllegalBlockSizeException {
// split off the tag to store it separately
byte[] result = cipher.doFinal(data);
- byte[] tag = Arrays.copyOfRange(result, result.length - CRYPTO_TAG_SIZE, result.length);
- byte[] encrypted = Arrays.copyOfRange(result, 0, result.length - CRYPTO_TAG_SIZE);
+ byte[] tag = Arrays.copyOfRange(result, result.length - CRYPTO_AEAD_TAG_SIZE, result.length);
+ byte[] encrypted = Arrays.copyOfRange(result, 0, result.length - CRYPTO_AEAD_TAG_SIZE);
- return new CryptResult() {{
- Parameters = new CryptParameters() {{
- Nonce = cipher.getIV();
- Tag = tag;
- }};
- Data = encrypted;
- }};
+ return new CryptResult(encrypted, new CryptParameters(cipher.getIV(), tag));
}
public static CryptResult decrypt(byte[] encrypted, Cipher cipher, CryptParameters params)
@@ -82,34 +92,18 @@ public class CryptoUtils {
// append the tag to the ciphertext
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(encrypted);
- stream.write(params.Tag);
+ stream.write(params.getTag());
encrypted = stream.toByteArray();
byte[] decrypted = cipher.doFinal(encrypted);
- return new CryptResult() {{
- Parameters = params;
- Data = decrypted;
- }};
- }
-
- public static byte[] hashKey(SecretKey key) {
- MessageDigest hash;
- try {
- hash = MessageDigest.getInstance(CRYPTO_HASH);
- } catch (NoSuchAlgorithmException e) {
- throw new AssertionError(e);
- }
-
- byte[] bytes = key.getEncoded();
- hash.update(bytes);
- return hash.digest();
+ return new CryptResult(decrypted, params);
}
public static SecretKey generateKey() {
try {
KeyGenerator generator = KeyGenerator.getInstance("AES");
- generator.init(CRYPTO_KEY_SIZE * 8);
+ generator.init(CRYPTO_AEAD_KEY_SIZE * 8);
return generator.generateKey();
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
@@ -117,11 +111,7 @@ public class CryptoUtils {
}
public static byte[] generateSalt() {
- return generateRandomBytes(CRYPTO_KEY_SIZE);
- }
-
- public static byte[] generateNonce() {
- return generateRandomBytes(CRYPTO_NONCE_SIZE);
+ return generateRandomBytes(CRYPTO_AEAD_KEY_SIZE);
}
public static byte[] generateRandomBytes(int length) {
diff --git a/app/src/main/java/me/impy/aegis/crypto/KeyInfo.java b/app/src/main/java/me/impy/aegis/crypto/KeyInfo.java
deleted file mode 100644
index 5d6f9231..00000000
--- a/app/src/main/java/me/impy/aegis/crypto/KeyInfo.java
+++ /dev/null
@@ -1,236 +0,0 @@
-package me.impy.aegis.crypto;
-
-import android.net.Uri;
-
-import java.io.Serializable;
-import java.util.Arrays;
-
-import me.impy.aegis.encoding.Base32;
-import me.impy.aegis.encoding.Base32Exception;
-
-public class KeyInfo implements Serializable {
- private String _type = "totp";
- private byte[] _secret;
- private String _accountName = "";
- private String _issuer = "";
- private long _counter = 0;
- private String _algorithm = "SHA1";
- private int _digits = 6;
- private int _period = 30;
-
- public String getURL() {
- Uri.Builder builder = new Uri.Builder();
- builder.scheme("otpauth");
- builder.authority(_type);
-
- builder.appendQueryParameter("digits", Integer.toString(_digits));
- builder.appendQueryParameter("period", Integer.toString(_period));
- builder.appendQueryParameter("algorithm", _algorithm);
- builder.appendQueryParameter("secret", new String(Base32.encode(_secret)));
- if (_type.equals("hotp")) {
- builder.appendQueryParameter("counter", Long.toString(_counter));
- }
-
- if (_issuer != null && !_issuer.equals("")) {
- builder.path(String.format("%s:%s", _issuer, _accountName));
- builder.appendQueryParameter("issuer", _issuer);
- } else {
- builder.path(_accountName);
- }
-
- return builder.build().toString();
- }
-
- public long getMillisTillNextRotation() {
- return KeyInfo.getMillisTillNextRotation(_period);
- }
-
- public static long getMillisTillNextRotation(int period) {
- long p = period * 1000;
- return p - (System.currentTimeMillis() % p);
- }
-
- public static KeyInfo fromURL(String s) throws KeyInfoException {
- final Uri url = Uri.parse(s);
- if (!url.getScheme().equals("otpauth")) {
- throw new KeyInfoException("unsupported protocol");
- }
-
- KeyInfo info = new KeyInfo();
- info.setType(url.getHost());
-
- // 'secret' is a required parameter
- String secret = url.getQueryParameter("secret");
- if (secret == null) {
- throw new KeyInfoException("'secret' is not set");
- }
- info.setSecret(secret.toCharArray());
-
- // provider info used to disambiguate accounts
- String path = url.getPath();
- String label = path != null && path.length() > 0 ? path.substring(1) : "";
-
- if (label.contains(":")) {
- // a label can only contain one colon
- // it's ok to fail if that's not the case
- String[] strings = label.split(":");
-
- if (strings.length == 2) {
- info.setIssuer(strings[0]);
- info.setAccountName(strings[1]);
- } else {
- // at this point, just dump the whole thing into the accountName
- info.setAccountName(label);
- }
- } else {
- // label only contains the account name
- // grab the issuer's info from the 'issuer' parameter if it's present
- String issuer = url.getQueryParameter("issuer");
- info.setIssuer(issuer != null ? issuer : "");
- info.setAccountName(label);
- }
-
- // just use the defaults if these parameters aren't set
- String algorithm = url.getQueryParameter("algorithm");
- if (algorithm != null) {
- info.setAlgorithm(algorithm);
- }
- String period = url.getQueryParameter("period");
- if (period != null) {
- info.setPeriod(Integer.parseInt(period));
- }
- String digits = url.getQueryParameter("digits");
- if (digits != null) {
- info.setDigits(Integer.parseInt(digits));
- }
-
- // 'counter' is required if the type is 'hotp'
- String counter = url.getQueryParameter("counter");
- if (counter != null) {
- info.setCounter(Long.parseLong(counter));
- } else if (info.getType().equals("hotp")) {
- throw new KeyInfoException("'counter' was not set which is required for 'hotp'");
- }
-
- return info;
- }
-
- public String getType() {
- return _type;
- }
-
- public byte[] getSecret() {
- return _secret;
- }
-
- public String getAccountName() {
- return _accountName;
- }
-
- public String getIssuer() {
- return _issuer;
- }
-
- public String getAlgorithm(boolean java) {
- if (java) {
- return "Hmac" + _algorithm;
- }
- return _algorithm;
- }
-
- public int getDigits() {
- return _digits;
- }
-
- public long getCounter() {
- return _counter;
- }
-
- public int getPeriod() {
- return _period;
- }
-
- public boolean isTypeValid(String type) {
- return type.equals("totp") || type.equals("hotp");
- }
-
- public void setType(String type) throws KeyInfoException {
- type = type.toLowerCase();
- if (!isTypeValid(type)) {
- throw new KeyInfoException(String.format("unsupported otp type: %s", type));
- }
- _type = type;
- }
-
- public void setSecret(char[] base32) throws KeyInfoException {
- byte[] secret;
- try {
- secret = Base32.decode(base32);
- } catch (Base32Exception e) {
- throw new KeyInfoException("bad secret", e);
- }
-
- setSecret(secret);
- }
-
- public void setSecret(byte[] secret) {
- _secret = secret;
- }
-
- public void setAccountName(String accountName) {
- _accountName = accountName;
- }
-
- public void setIssuer(String issuer) {
- _issuer = issuer;
- }
-
- public boolean isAlgorithmValid(String algorithm) {
- return algorithm.equals("SHA1") || algorithm.equals("SHA256") || algorithm.equals("SHA512");
- }
-
- public void setAlgorithm(String algorithm) throws KeyInfoException {
- if (algorithm.startsWith("Hmac")) {
- algorithm = algorithm.substring(4);
- }
- algorithm = algorithm.toUpperCase();
-
- if (!isAlgorithmValid(algorithm)) {
- throw new KeyInfoException(String.format("unsupported algorithm: %s", algorithm));
- }
- _algorithm = algorithm;
- }
-
- public boolean isDigitsValid(int digits) {
- return digits == 6 || digits == 8;
- }
-
- public void setDigits(int digits) throws KeyInfoException {
- if (!isDigitsValid(digits)) {
- throw new KeyInfoException(String.format("unsupported amount of digits: %d", digits));
- }
- _digits = digits;
- }
-
- public boolean isCounterValid(long count) {
- return count >= 0;
- }
-
- public void setCounter(long count) throws KeyInfoException {
- if (!isCounterValid(count)) {
- throw new KeyInfoException(String.format("bad count: %d", count));
- }
- _counter = count;
- }
-
- public boolean isPeriodValid(int period) {
- return period > 0;
- }
-
- public void setPeriod(int period) throws KeyInfoException {
- if (!isPeriodValid(period)) {
- throw new KeyInfoException(String.format("bad period: %d", period));
- }
- _period = period;
- }
-}
diff --git a/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandle.java b/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandle.java
index c7f3afe5..94826d06 100644
--- a/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandle.java
+++ b/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandle.java
@@ -1,6 +1,5 @@
package me.impy.aegis.crypto;
-import android.annotation.SuppressLint;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
@@ -51,11 +50,11 @@ public class KeyStoreHandle {
KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, STORE_NAME);
generator.init(new KeyGenParameterSpec.Builder(id,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
- .setBlockModes(KeyProperties.BLOCK_MODE_ECB)
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
- .setRandomizedEncryptionRequired(false)
- .setKeySize(CryptoUtils.CRYPTO_KEY_SIZE * 8)
+ .setRandomizedEncryptionRequired(true)
+ .setKeySize(CryptoUtils.CRYPTO_AEAD_KEY_SIZE * 8)
.build());
return generator.generateKey();
@@ -81,8 +80,7 @@ public class KeyStoreHandle {
// and see if KeyPermanentlyInvalidatedException is thrown
if (isSupported()) {
try {
- @SuppressLint("GetInstance")
- Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_CIPHER_RAW);
+ Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_AEAD);
cipher.init(Cipher.ENCRYPT_MODE, key);
} catch (KeyPermanentlyInvalidatedException e) {
return null;
diff --git a/app/src/main/java/me/impy/aegis/crypto/MasterKey.java b/app/src/main/java/me/impy/aegis/crypto/MasterKey.java
index c41d1e30..50ef0479 100644
--- a/app/src/main/java/me/impy/aegis/crypto/MasterKey.java
+++ b/app/src/main/java/me/impy/aegis/crypto/MasterKey.java
@@ -28,7 +28,7 @@ public class MasterKey implements Serializable {
public CryptResult encrypt(byte[] bytes) throws MasterKeyException {
try {
- Cipher cipher = CryptoUtils.createCipher(_key, Cipher.ENCRYPT_MODE);
+ Cipher cipher = CryptoUtils.createEncryptCipher(_key);
return CryptoUtils.encrypt(bytes, cipher);
} catch (NoSuchPaddingException
| NoSuchAlgorithmException
@@ -42,7 +42,7 @@ public class MasterKey implements Serializable {
public CryptResult decrypt(byte[] bytes, CryptParameters params) throws MasterKeyException {
try {
- Cipher cipher = CryptoUtils.createCipher(_key, Cipher.DECRYPT_MODE, params.Nonce);
+ Cipher cipher = CryptoUtils.createDecryptCipher(_key, params.getNonce());
return CryptoUtils.decrypt(bytes, cipher, params);
} catch (NoSuchPaddingException
| NoSuchAlgorithmException
@@ -55,10 +55,6 @@ public class MasterKey implements Serializable {
}
}
- public byte[] getHash() {
- return CryptoUtils.hashKey(_key);
- }
-
public byte[] getBytes() {
return _key.getEncoded();
}
diff --git a/app/src/main/java/me/impy/aegis/crypto/otp/OTP.java b/app/src/main/java/me/impy/aegis/crypto/otp/OTP.java
deleted file mode 100644
index 72bbaba6..00000000
--- a/app/src/main/java/me/impy/aegis/crypto/otp/OTP.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package me.impy.aegis.crypto.otp;
-
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-
-import me.impy.aegis.crypto.KeyInfo;
-
-public class OTP {
- private OTP() {
- }
-
- public static String generateOTP(KeyInfo info) throws OTPException {
- String otp;
-
- try {
- switch (info.getType()) {
- case "totp":
- String time = Long.toHexString(System.currentTimeMillis() / 1000 / info.getPeriod());
- otp = TOTP.generateTOTP(info.getSecret(), time, info.getDigits(), info.getAlgorithm(true));
- break;
- case "hotp":
- otp = HOTP.generateOTP(info.getSecret(), info.getCounter(), info.getDigits(), false, -1);
- break;
- default:
- throw new RuntimeException("Bad OTP type");
- }
- } catch (NoSuchAlgorithmException | InvalidKeyException e) {
- throw new OTPException(e);
- }
-
- return otp;
- }
-}
diff --git a/app/src/main/java/me/impy/aegis/crypto/otp/OTPException.java b/app/src/main/java/me/impy/aegis/crypto/otp/OTPException.java
deleted file mode 100644
index 8866b7a1..00000000
--- a/app/src/main/java/me/impy/aegis/crypto/otp/OTPException.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package me.impy.aegis.crypto.otp;
-
-public class OTPException extends Exception {
- public OTPException(Throwable cause) {
- super(cause);
- }
-}
diff --git a/app/src/main/java/me/impy/aegis/db/Database.java b/app/src/main/java/me/impy/aegis/db/Database.java
index abc30308..c1725899 100644
--- a/app/src/main/java/me/impy/aegis/db/Database.java
+++ b/app/src/main/java/me/impy/aegis/db/Database.java
@@ -4,19 +4,16 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
-import java.util.UUID;
-import me.impy.aegis.crypto.KeyInfoException;
+import me.impy.aegis.otp.OtpInfoException;
public class Database {
private static final int VERSION = 1;
- private List _entries = new ArrayList<>();
+ private DatabaseEntryList _entries = new DatabaseEntryList();
- public JSONObject serialize() throws DatabaseException {
+ public JSONObject serialize() {
try {
JSONArray array = new JSONArray();
for (DatabaseEntry e : _entries) {
@@ -28,7 +25,7 @@ public class Database {
obj.put("entries", array);
return obj;
} catch (JSONException e) {
- throw new DatabaseException(e);
+ throw new RuntimeException(e);
}
}
@@ -44,52 +41,30 @@ public class Database {
for (int i = 0; i < array.length(); i++) {
DatabaseEntry entry = new DatabaseEntry(null);
entry.deserialize(array.getJSONObject(i));
- addKey(entry);
+ addEntry(entry);
}
- } catch (JSONException | KeyInfoException e) {
+ } catch (OtpInfoException | JSONException e) {
throw new DatabaseException(e);
}
}
- public void addKey(DatabaseEntry entry) {
- if (tryGetKeyByUUID(entry.getUUID()) != null) {
- throw new AssertionError("entry found with the same uuid");
- }
+ public void addEntry(DatabaseEntry entry) {
_entries.add(entry);
}
- public void removeKey(DatabaseEntry entry) {
- entry = getKeyByUUID(entry.getUUID());
+ public void removeEntry(DatabaseEntry entry) {
_entries.remove(entry);
}
- public void replaceKey(DatabaseEntry newEntry) {
- DatabaseEntry oldEntry = getKeyByUUID(newEntry.getUUID());
- _entries.set(_entries.indexOf(oldEntry), newEntry);
+ public void replaceEntry(DatabaseEntry newEntry) {
+ _entries.replace(newEntry);
}
- public void swapKeys(DatabaseEntry entry1, DatabaseEntry entry2) {
- Collections.swap(_entries, _entries.indexOf(entry1), _entries.indexOf(entry2));
+ public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) {
+ _entries.swap(entry1, entry2);
}
- public List getKeys() {
- return Collections.unmodifiableList(_entries);
- }
-
- private DatabaseEntry tryGetKeyByUUID(UUID uuid) {
- for (DatabaseEntry entry : _entries) {
- if (entry.getUUID().equals(uuid)) {
- return entry;
- }
- }
- return null;
- }
-
- private DatabaseEntry getKeyByUUID(UUID uuid) {
- DatabaseEntry entry = tryGetKeyByUUID(uuid);
- if (entry == null) {
- throw new AssertionError("no entry found with the same uuid");
- }
- return entry;
+ public List getEntries() {
+ return _entries.getList();
}
}
diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseEntry.java b/app/src/main/java/me/impy/aegis/db/DatabaseEntry.java
index 564ed88f..624f02b2 100644
--- a/app/src/main/java/me/impy/aegis/db/DatabaseEntry.java
+++ b/app/src/main/java/me/impy/aegis/db/DatabaseEntry.java
@@ -6,33 +6,44 @@ import org.json.JSONObject;
import java.io.Serializable;
import java.util.UUID;
-import me.impy.aegis.crypto.KeyInfo;
-import me.impy.aegis.crypto.KeyInfoException;
+import me.impy.aegis.otp.OtpInfo;
+import me.impy.aegis.otp.OtpInfoException;
public class DatabaseEntry implements Serializable {
private UUID _uuid;
private String _name = "";
+ private String _issuer = "";
private String _icon = "";
- private KeyInfo _info;
+ private OtpInfo _info;
- public DatabaseEntry() {
- this(new KeyInfo());
- }
-
- public DatabaseEntry(KeyInfo info) {
+ public DatabaseEntry(OtpInfo info) {
_info = info;
_uuid = UUID.randomUUID();
}
- public JSONObject serialize() throws JSONException {
+ public DatabaseEntry(OtpInfo info, String name, String issuer) {
+ this(info);
+ setName(name);
+ setIssuer(issuer);
+ }
+
+ public JSONObject serialize() {
JSONObject obj = new JSONObject();
- obj.put("uuid", _uuid.toString());
- obj.put("name", _name);
- obj.put("url", _info.getURL());
+
+ try {
+ obj.put("type", _info.getType());
+ obj.put("uuid", _uuid.toString());
+ obj.put("name", _name);
+ obj.put("issuer", _issuer);
+ obj.put("info", _info.toJson());
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+
return obj;
}
- public void deserialize(JSONObject obj) throws JSONException, KeyInfoException {
+ public void deserialize(JSONObject obj) throws JSONException, OtpInfoException {
// if there is no uuid, generate a new one
if (!obj.has("uuid")) {
_uuid = UUID.randomUUID();
@@ -40,29 +51,43 @@ public class DatabaseEntry implements Serializable {
_uuid = UUID.fromString(obj.getString("uuid"));
}
_name = obj.getString("name");
- _info = KeyInfo.fromURL(obj.getString("url"));
+ _issuer = obj.getString("issuer");
+ _info = OtpInfo.parseJson(obj.getString("type"), obj.getJSONObject("info"));
}
public UUID getUUID() {
return _uuid;
}
+
public String getName() {
return _name;
}
+
+ public String getIssuer() {
+ return _issuer;
+ }
+
public String getIcon() {
return _icon;
}
- public KeyInfo getInfo() {
+
+ public OtpInfo getInfo() {
return _info;
}
public void setName(String name) {
_name = name;
}
+
+ public void setIssuer(String issuer) {
+ _issuer = issuer;
+ }
+
public void setIcon(String icon) {
_icon = icon;
}
- public void setInfo(KeyInfo info) {
+
+ public void setInfo(OtpInfo info) {
_info = info;
}
}
diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseEntryList.java b/app/src/main/java/me/impy/aegis/db/DatabaseEntryList.java
new file mode 100644
index 00000000..ec0ba801
--- /dev/null
+++ b/app/src/main/java/me/impy/aegis/db/DatabaseEntryList.java
@@ -0,0 +1,62 @@
+package me.impy.aegis.db;
+
+import android.support.annotation.NonNull;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
+
+public class DatabaseEntryList implements Iterable, Serializable {
+ private List _entries = new ArrayList<>();
+
+ @NonNull
+ @Override
+ public Iterator iterator() {
+ return _entries.iterator();
+ }
+
+ public void add(DatabaseEntry entry) {
+ if (tryGetByUUID(entry.getUUID()) != null) {
+ throw new AssertionError("entry found with the same uuid");
+ }
+ _entries.add(entry);
+ }
+
+ public void remove(DatabaseEntry entry) {
+ entry = getByUUID(entry.getUUID());
+ _entries.remove(entry);
+ }
+
+ public void replace(DatabaseEntry newEntry) {
+ DatabaseEntry oldEntry = getByUUID(newEntry.getUUID());
+ _entries.set(_entries.indexOf(oldEntry), newEntry);
+ }
+
+ public void swap(DatabaseEntry entry1, DatabaseEntry entry2) {
+ Collections.swap(_entries, _entries.indexOf(entry1), _entries.indexOf(entry2));
+ }
+
+ public List getList() {
+ return Collections.unmodifiableList(_entries);
+ }
+
+ private DatabaseEntry tryGetByUUID(UUID uuid) {
+ for (DatabaseEntry entry : _entries) {
+ if (entry.getUUID().equals(uuid)) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ private DatabaseEntry getByUUID(UUID uuid) {
+ DatabaseEntry entry = tryGetByUUID(uuid);
+ if (entry == null) {
+ throw new AssertionError("no entry found with the same uuid");
+ }
+ return entry;
+ }
+}
diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseFile.java b/app/src/main/java/me/impy/aegis/db/DatabaseFile.java
index 8f9d216a..d932ec7d 100644
--- a/app/src/main/java/me/impy/aegis/db/DatabaseFile.java
+++ b/app/src/main/java/me/impy/aegis/db/DatabaseFile.java
@@ -1,5 +1,6 @@
package me.impy.aegis.db;
+import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -9,11 +10,10 @@ import me.impy.aegis.crypto.CryptParameters;
import me.impy.aegis.crypto.CryptResult;
import me.impy.aegis.crypto.MasterKey;
import me.impy.aegis.crypto.MasterKeyException;
-import me.impy.aegis.db.slots.SlotCollection;
-import me.impy.aegis.db.slots.SlotCollectionException;
+import me.impy.aegis.db.slots.SlotList;
+import me.impy.aegis.db.slots.SlotListException;
import me.impy.aegis.encoding.Base64;
import me.impy.aegis.encoding.Base64Exception;
-import me.impy.aegis.encoding.Hex;
import me.impy.aegis.encoding.HexException;
public class DatabaseFile {
@@ -21,22 +21,15 @@ public class DatabaseFile {
private Object _content;
private CryptParameters _cryptParameters;
- private SlotCollection _slots;
+ private SlotList _slots;
- public byte[] serialize() throws DatabaseFileException {
+ public byte[] serialize() {
try {
- JSONObject cryptObj = null;
- if (isEncrypted()) {
- cryptObj = new JSONObject();
- cryptObj.put("nonce", Hex.encode(_cryptParameters.Nonce));
- cryptObj.put("tag", Hex.encode(_cryptParameters.Tag));
- }
-
- // don't write the crypt parameters if the content is not encrypted
- boolean plain = _content instanceof JSONObject || _slots == null || cryptObj == null;
+ // don't write the crypt parameters and slots if the content is not encrypted
+ boolean plain = _content instanceof JSONObject || !isEncrypted();
JSONObject headerObj = new JSONObject();
- headerObj.put("slots", plain ? JSONObject.NULL : SlotCollection.serialize(_slots));
- headerObj.put("params", plain ? JSONObject.NULL : cryptObj);
+ headerObj.put("slots", plain ? JSONObject.NULL : SlotList.serialize(_slots));
+ headerObj.put("params", plain ? JSONObject.NULL : _cryptParameters.toJson());
JSONObject obj = new JSONObject();
obj.put("version", VERSION);
@@ -45,8 +38,8 @@ public class DatabaseFile {
String string = obj.toString(4);
return string.getBytes("UTF-8");
- } catch (SlotCollectionException | UnsupportedEncodingException | JSONException e) {
- throw new DatabaseFileException(e);
+ } catch (JSONException | UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
}
}
@@ -58,17 +51,14 @@ public class DatabaseFile {
throw new DatabaseFileException("unsupported version");
}
- JSONObject slotObj = headerObj.optJSONObject("slots");
+ JSONArray slotObj = headerObj.optJSONArray("slots");
if (slotObj != null) {
- _slots = SlotCollection.deserialize(slotObj);
+ _slots = SlotList.deserialize(slotObj);
}
JSONObject cryptObj = headerObj.optJSONObject("params");
if (cryptObj != null) {
- _cryptParameters = new CryptParameters() {{
- Nonce = Hex.decode(cryptObj.getString("nonce"));
- Tag = Hex.decode(cryptObj.getString("tag"));
- }};
+ _cryptParameters = CryptParameters.parseJson(cryptObj);
}
if (cryptObj == null || slotObj == null) {
@@ -76,7 +66,7 @@ public class DatabaseFile {
} else {
_content = obj.getString("db");
}
- } catch (SlotCollectionException | UnsupportedEncodingException | JSONException | HexException e) {
+ } catch (SlotListException | UnsupportedEncodingException | JSONException | HexException e) {
throw new DatabaseFileException(e);
}
}
@@ -93,7 +83,7 @@ public class DatabaseFile {
try {
byte[] bytes = Base64.decode((String) _content);
CryptResult result = key.decrypt(bytes, _cryptParameters);
- return new JSONObject(new String(result.Data, "UTF-8"));
+ return new JSONObject(new String(result.getData(), "UTF-8"));
} catch (MasterKeyException | JSONException | UnsupportedEncodingException | Base64Exception e) {
throw new DatabaseFileException(e);
}
@@ -111,18 +101,18 @@ public class DatabaseFile {
byte[] dbBytes = string.getBytes("UTF-8");
CryptResult result = key.encrypt(dbBytes);
- _content = Base64.encode(result.Data);
- _cryptParameters = result.Parameters;
+ _content = Base64.encode(result.getData());
+ _cryptParameters = result.getParams();
} catch (MasterKeyException | UnsupportedEncodingException | JSONException e) {
throw new DatabaseFileException(e);
}
}
- public SlotCollection getSlots() {
+ public SlotList getSlots() {
return _slots;
}
- public void setSlots(SlotCollection slots) {
+ public void setSlots(SlotList slots) {
_slots = slots;
}
}
diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseManager.java b/app/src/main/java/me/impy/aegis/db/DatabaseManager.java
index a50f92dd..9f7939c1 100644
--- a/app/src/main/java/me/impy/aegis/db/DatabaseManager.java
+++ b/app/src/main/java/me/impy/aegis/db/DatabaseManager.java
@@ -14,7 +14,7 @@ import java.util.List;
import me.impy.aegis.BuildConfig;
import me.impy.aegis.crypto.MasterKey;
-import me.impy.aegis.db.slots.SlotCollection;
+import me.impy.aegis.db.slots.SlotList;
public class DatabaseManager {
private static final String FILENAME = "aegis.json";
@@ -103,7 +103,7 @@ public class DatabaseManager {
stream.close();
}
}
- } catch (IOException | DatabaseFileException e) {
+ } catch (IOException e) {
throw new DatabaseManagerException(e);
}
}
@@ -119,7 +119,7 @@ public class DatabaseManager {
_file.setContent(obj);
}
save(_context, _file);
- } catch (DatabaseException | DatabaseFileException e) {
+ } catch (DatabaseFileException e) {
throw new DatabaseManagerException(e);
}
}
@@ -157,34 +157,34 @@ public class DatabaseManager {
}
return file.getAbsolutePath();
- } catch (DatabaseException | IOException | DatabaseFileException e) {
+ } catch (IOException | DatabaseFileException e) {
throw new DatabaseManagerException(e);
}
}
- public void addKey(DatabaseEntry entry) {
+ public void addEntry(DatabaseEntry entry) {
assertState(false, true);
- _db.addKey(entry);
+ _db.addEntry(entry);
}
- public void removeKey(DatabaseEntry entry) {
+ public void removeEntry(DatabaseEntry entry) {
assertState(false, true);
- _db.removeKey(entry);
+ _db.removeEntry(entry);
}
- public void replaceKey(DatabaseEntry entry) {
+ public void replaceEntry(DatabaseEntry entry) {
assertState(false, true);
- _db.replaceKey(entry);
+ _db.replaceEntry(entry);
}
- public void swapKeys(DatabaseEntry entry1, DatabaseEntry entry2) {
+ public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) {
assertState(false, true);
- _db.swapKeys(entry1, entry2);
+ _db.swapEntries(entry1, entry2);
}
- public List getKeys() {
+ public List getEntries() {
assertState(false, true);
- return _db.getKeys();
+ return _db.getEntries();
}
public MasterKey getMasterKey() {
@@ -196,7 +196,7 @@ public class DatabaseManager {
return _file;
}
- public void enableEncryption(MasterKey key, SlotCollection slots) {
+ public void enableEncryption(MasterKey key, SlotList slots) {
assertState(false, true);
_key = key;
_file.setSlots(slots);
diff --git a/app/src/main/java/me/impy/aegis/db/slots/PasswordSlot.java b/app/src/main/java/me/impy/aegis/db/slots/PasswordSlot.java
index c3a67941..3cb88af8 100644
--- a/app/src/main/java/me/impy/aegis/db/slots/PasswordSlot.java
+++ b/app/src/main/java/me/impy/aegis/db/slots/PasswordSlot.java
@@ -3,9 +3,6 @@ package me.impy.aegis.db.slots;
import org.json.JSONException;
import org.json.JSONObject;
-import java.security.NoSuchAlgorithmException;
-import java.security.spec.InvalidKeySpecException;
-
import javax.crypto.SecretKey;
import me.impy.aegis.crypto.CryptoUtils;
@@ -23,7 +20,7 @@ public class PasswordSlot extends RawSlot {
}
@Override
- public JSONObject serialize() throws SlotException {
+ public JSONObject serialize() {
try {
JSONObject obj = super.serialize();
obj.put("n", _n);
@@ -32,7 +29,7 @@ public class PasswordSlot extends RawSlot {
obj.put("salt", Hex.encode(_salt));
return obj;
} catch (JSONException e) {
- throw new SlotException(e);
+ throw new RuntimeException(e);
}
}
diff --git a/app/src/main/java/me/impy/aegis/db/slots/Slot.java b/app/src/main/java/me/impy/aegis/db/slots/Slot.java
index 5d969de8..be9eee3e 100644
--- a/app/src/main/java/me/impy/aegis/db/slots/Slot.java
+++ b/app/src/main/java/me/impy/aegis/db/slots/Slot.java
@@ -1,15 +1,16 @@
package me.impy.aegis.db.slots;
-import android.annotation.SuppressLint;
-
import org.json.JSONException;
import org.json.JSONObject;
+import java.io.IOException;
import java.io.Serializable;
+import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.UUID;
+import javax.crypto.AEADBadTagException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
@@ -17,6 +18,8 @@ import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
+import me.impy.aegis.crypto.CryptParameters;
+import me.impy.aegis.crypto.CryptResult;
import me.impy.aegis.crypto.CryptoUtils;
import me.impy.aegis.crypto.MasterKey;
import me.impy.aegis.encoding.Hex;
@@ -29,53 +32,70 @@ public abstract class Slot implements Serializable {
protected UUID _uuid;
protected byte[] _encryptedMasterKey;
+ protected CryptParameters _encryptedMasterKeyParams;
protected Slot() {
_uuid = UUID.randomUUID();
}
- // getKey decrypts the encrypted master key in this slot with the given key and returns it.
- public SecretKey getKey(Cipher cipher) throws SlotException {
+ // getKey decrypts the encrypted master key in this slot using the given cipher and returns it.
+ public MasterKey getKey(Cipher cipher) throws SlotException, SlotIntegrityException {
try {
- byte[] decryptedKeyBytes = cipher.doFinal(_encryptedMasterKey);
- return new SecretKeySpec(decryptedKeyBytes, CryptoUtils.CRYPTO_CIPHER_AEAD);
- } catch (BadPaddingException | IllegalBlockSizeException e) {
+ CryptResult res = CryptoUtils.decrypt(_encryptedMasterKey, cipher, _encryptedMasterKeyParams);
+ SecretKey key = new SecretKeySpec(res.getData(), CryptoUtils.CRYPTO_AEAD);
+ return new MasterKey(key);
+ } catch (BadPaddingException e) {
+ throw new SlotIntegrityException(e);
+ } catch (IOException | IllegalBlockSizeException e) {
throw new SlotException(e);
}
}
- // setKey encrypts the given master key with the given key and stores the result in this slot.
+ // setKey encrypts the given master key using the given cipher and stores the result in this slot.
public void setKey(MasterKey masterKey, Cipher cipher) throws SlotException {
try {
byte[] masterKeyBytes = masterKey.getBytes();
- _encryptedMasterKey = cipher.doFinal(masterKeyBytes);
+ CryptResult res = CryptoUtils.encrypt(masterKeyBytes, cipher);
+ _encryptedMasterKey = res.getData();
+ _encryptedMasterKeyParams = res.getParams();
} catch (BadPaddingException | IllegalBlockSizeException e) {
throw new SlotException(e);
}
}
- // suppress the AES ECB warning
- // this is perfectly safe because we discard this cipher after passing CryptoUtils.CRYPTO_KEY_SIZE bytes through it
- @SuppressLint("getInstance")
- public static Cipher createCipher(SecretKey key, int mode) throws SlotException {
+ public static Cipher createEncryptCipher(SecretKey key) throws SlotException {
try {
- Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_CIPHER_RAW);
- cipher.init(mode, key);
- return cipher;
- } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) {
+ return CryptoUtils.createEncryptCipher(key);
+ } catch (InvalidAlgorithmParameterException
+ | NoSuchPaddingException
+ | NoSuchAlgorithmException
+ | InvalidKeyException e) {
throw new SlotException(e);
}
}
- public JSONObject serialize() throws SlotException {
+ public Cipher createDecryptCipher(SecretKey key) throws SlotException {
+ try {
+ return CryptoUtils.createDecryptCipher(key, _encryptedMasterKeyParams.getNonce());
+ } catch (InvalidAlgorithmParameterException
+ | NoSuchAlgorithmException
+ | InvalidKeyException
+ | NoSuchPaddingException e) {
+ throw new SlotException(e);
+ }
+ }
+
+ public JSONObject serialize() {
try {
JSONObject obj = new JSONObject();
+ JSONObject paramObj = _encryptedMasterKeyParams.toJson();
obj.put("type", getType());
obj.put("uuid", _uuid.toString());
obj.put("key", Hex.encode(_encryptedMasterKey));
+ obj.put("key_params", paramObj);
return obj;
} catch (JSONException e) {
- throw new SlotException(e);
+ throw new RuntimeException(e);
}
}
@@ -84,13 +104,17 @@ public abstract class Slot implements Serializable {
if (obj.getInt("type") != getType()) {
throw new SlotException("slot type mismatch");
}
+
// if there is no uuid, generate a new one
if (!obj.has("uuid")) {
_uuid = UUID.randomUUID();
} else {
_uuid = UUID.fromString(obj.getString("uuid"));
}
+
+ JSONObject paramObj = obj.getJSONObject("key_params");
_encryptedMasterKey = Hex.decode(obj.getString("key"));
+ _encryptedMasterKeyParams = CryptParameters.parseJson(paramObj);
} catch (JSONException | HexException e) {
throw new SlotException(e);
}
diff --git a/app/src/main/java/me/impy/aegis/db/slots/SlotCollectionException.java b/app/src/main/java/me/impy/aegis/db/slots/SlotCollectionException.java
deleted file mode 100644
index 6ded4dec..00000000
--- a/app/src/main/java/me/impy/aegis/db/slots/SlotCollectionException.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package me.impy.aegis.db.slots;
-
-public class SlotCollectionException extends Exception {
- public SlotCollectionException(Throwable cause) {
- super(cause);
- }
-
- public SlotCollectionException(String message) {
- super(message);
- }
-}
diff --git a/app/src/main/java/me/impy/aegis/db/slots/SlotIntegrityException.java b/app/src/main/java/me/impy/aegis/db/slots/SlotIntegrityException.java
index 79061a5e..b83d6caf 100644
--- a/app/src/main/java/me/impy/aegis/db/slots/SlotIntegrityException.java
+++ b/app/src/main/java/me/impy/aegis/db/slots/SlotIntegrityException.java
@@ -1,5 +1,11 @@
package me.impy.aegis.db.slots;
public class SlotIntegrityException extends Exception {
+ public SlotIntegrityException() {
+ }
+
+ public SlotIntegrityException(Throwable cause) {
+ super(cause);
+ }
}
diff --git a/app/src/main/java/me/impy/aegis/db/slots/SlotCollection.java b/app/src/main/java/me/impy/aegis/db/slots/SlotList.java
similarity index 51%
rename from app/src/main/java/me/impy/aegis/db/slots/SlotCollection.java
rename to app/src/main/java/me/impy/aegis/db/slots/SlotList.java
index e9ae476e..a8c75733 100644
--- a/app/src/main/java/me/impy/aegis/db/slots/SlotCollection.java
+++ b/app/src/main/java/me/impy/aegis/db/slots/SlotList.java
@@ -6,47 +6,28 @@ import org.json.JSONObject;
import java.io.Serializable;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
-import javax.crypto.Cipher;
-import me.impy.aegis.crypto.MasterKey;
-import me.impy.aegis.encoding.Hex;
-import me.impy.aegis.encoding.HexException;
-
-public class SlotCollection implements Iterable, Serializable {
+public class SlotList implements Iterable, Serializable {
private List _slots = new ArrayList<>();
- private byte[] _masterHash;
- public static JSONObject serialize(SlotCollection slots) throws SlotCollectionException {
- try {
- JSONObject obj = new JSONObject();
- obj.put("hash", Hex.encode(slots.getMasterHash()));
-
- JSONArray entries = new JSONArray();
- for (Slot slot : slots) {
- entries.put(slot.serialize());
- }
-
- obj.put("entries", entries);
- return obj;
- } catch (SlotException | JSONException e) {
- throw new SlotCollectionException(e);
+ public static JSONArray serialize(SlotList slots) {
+ JSONArray array = new JSONArray();
+ for (Slot slot : slots) {
+ array.put(slot.serialize());
}
+
+ return array;
}
- public static SlotCollection deserialize(JSONObject obj) throws SlotCollectionException {
- SlotCollection slots = new SlotCollection();
+ public static SlotList deserialize(JSONArray array) throws SlotListException {
+ SlotList slots = new SlotList();
try {
- byte[] masterHash = Hex.decode(obj.getString("hash"));
- slots.setMasterHash(masterHash);
-
- JSONArray entries = obj.getJSONArray("entries");
- for (int i = 0; i < entries.length(); i++) {
+ for (int i = 0; i < array.length(); i++) {
Slot slot;
- JSONObject slotObj = entries.getJSONObject(i);
+ JSONObject slotObj = array.getJSONObject(i);
switch (slotObj.getInt("type")) {
case Slot.TYPE_RAW:
@@ -65,8 +46,8 @@ public class SlotCollection implements Iterable, Serializable {
slot.deserialize(slotObj);
slots.add(slot);
}
- } catch (SlotException | JSONException | HexException e) {
- throw new SlotCollectionException(e);
+ } catch (SlotException | JSONException e) {
+ throw new SlotListException(e);
}
return slots;
@@ -116,26 +97,4 @@ public class SlotCollection implements Iterable, Serializable {
public Iterator iterator() {
return _slots.iterator();
}
-
- public void encrypt(Slot slot, MasterKey key, Cipher cipher) throws SlotException {
- slot.setKey(key, cipher);
- setMasterHash(key.getHash());
- }
-
- public MasterKey decrypt(Slot slot, Cipher cipher) throws SlotException, SlotIntegrityException {
- byte[] hash = getMasterHash();
- MasterKey key = new MasterKey(slot.getKey(cipher));
- if (!Arrays.equals(hash, key.getHash())) {
- throw new SlotIntegrityException();
- }
- return key;
- }
-
- private void setMasterHash(byte[] masterHash) {
- _masterHash = masterHash;
- }
-
- private byte[] getMasterHash() {
- return _masterHash;
- }
}
diff --git a/app/src/main/java/me/impy/aegis/db/slots/SlotListException.java b/app/src/main/java/me/impy/aegis/db/slots/SlotListException.java
new file mode 100644
index 00000000..32d51342
--- /dev/null
+++ b/app/src/main/java/me/impy/aegis/db/slots/SlotListException.java
@@ -0,0 +1,11 @@
+package me.impy.aegis.db.slots;
+
+public class SlotListException extends Exception {
+ public SlotListException(Throwable cause) {
+ super(cause);
+ }
+
+ public SlotListException(String message) {
+ super(message);
+ }
+}
diff --git a/app/src/main/java/me/impy/aegis/encoding/Base32.java b/app/src/main/java/me/impy/aegis/encoding/Base32.java
index a217701d..c17f78e3 100644
--- a/app/src/main/java/me/impy/aegis/encoding/Base32.java
+++ b/app/src/main/java/me/impy/aegis/encoding/Base32.java
@@ -24,8 +24,6 @@ package me.impy.aegis.encoding;
import java.util.Arrays;
-import me.impy.aegis.crypto.CryptoUtils;
-
/**
* Base32 - encodes and decodes RFC3548 Base32
* (see http://www.faqs.org/rfcs/rfc3548.html )
diff --git a/app/src/main/java/me/impy/aegis/helpers/EditTextHelper.java b/app/src/main/java/me/impy/aegis/helpers/EditTextHelper.java
index dd5baced..c570d804 100644
--- a/app/src/main/java/me/impy/aegis/helpers/EditTextHelper.java
+++ b/app/src/main/java/me/impy/aegis/helpers/EditTextHelper.java
@@ -5,8 +5,6 @@ import android.widget.EditText;
import java.util.Arrays;
-import me.impy.aegis.crypto.CryptoUtils;
-
public class EditTextHelper {
private EditTextHelper() {
}
diff --git a/app/src/main/java/me/impy/aegis/helpers/TextDrawableHelper.java b/app/src/main/java/me/impy/aegis/helpers/TextDrawableHelper.java
index 7f4afeec..bcfc8497 100644
--- a/app/src/main/java/me/impy/aegis/helpers/TextDrawableHelper.java
+++ b/app/src/main/java/me/impy/aegis/helpers/TextDrawableHelper.java
@@ -8,13 +8,16 @@ public class TextDrawableHelper {
}
- public static TextDrawable generate(String s) {
- if (s == null || s.length() <= 1) {
- return null;
+ public static TextDrawable generate(String text, String fallback) {
+ if (text == null || text.isEmpty()) {
+ if (fallback == null || fallback.isEmpty()) {
+ return null;
+ }
+ text = fallback;
}
ColorGenerator generator = ColorGenerator.MATERIAL;
- int color = generator.getColor(s);
- return TextDrawable.builder().buildRound(s.substring(0, 1).toUpperCase(), color);
+ int color = generator.getColor(text);
+ return TextDrawable.builder().buildRound(text.substring(0, 1).toUpperCase(), color);
}
}
diff --git a/app/src/main/java/me/impy/aegis/helpers/UIRefresher.java b/app/src/main/java/me/impy/aegis/helpers/UiRefresher.java
similarity index 92%
rename from app/src/main/java/me/impy/aegis/helpers/UIRefresher.java
rename to app/src/main/java/me/impy/aegis/helpers/UiRefresher.java
index 4ac16a7c..dc24bf64 100644
--- a/app/src/main/java/me/impy/aegis/helpers/UIRefresher.java
+++ b/app/src/main/java/me/impy/aegis/helpers/UiRefresher.java
@@ -2,12 +2,12 @@ package me.impy.aegis.helpers;
import android.os.Handler;
-public class UIRefresher {
+public class UiRefresher {
private boolean _running;
private Listener _listener;
private Handler _handler;
- public UIRefresher(Listener listener) {
+ public UiRefresher(Listener listener) {
_listener = listener;
_handler = new Handler();
}
diff --git a/app/src/main/java/me/impy/aegis/importers/AegisImporter.java b/app/src/main/java/me/impy/aegis/importers/AegisImporter.java
index e3f79727..a5c01ff9 100644
--- a/app/src/main/java/me/impy/aegis/importers/AegisImporter.java
+++ b/app/src/main/java/me/impy/aegis/importers/AegisImporter.java
@@ -43,7 +43,7 @@ public class AegisImporter extends DatabaseImporter {
Database db = new Database();
db.deserialize(obj);
- return db.getKeys();
+ return db.getEntries();
} catch (DatabaseException | DatabaseFileException e) {
throw new DatabaseImporterException(e);
}
diff --git a/app/src/main/java/me/impy/aegis/importers/AndOTPImporter.java b/app/src/main/java/me/impy/aegis/importers/AndOtpImporter.java
similarity index 57%
rename from app/src/main/java/me/impy/aegis/importers/AndOTPImporter.java
rename to app/src/main/java/me/impy/aegis/importers/AndOtpImporter.java
index e94bf608..d796dc4b 100644
--- a/app/src/main/java/me/impy/aegis/importers/AndOTPImporter.java
+++ b/app/src/main/java/me/impy/aegis/importers/AndOtpImporter.java
@@ -8,17 +8,19 @@ import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
-import me.impy.aegis.crypto.KeyInfo;
-import me.impy.aegis.crypto.KeyInfoException;
import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.encoding.Base32;
import me.impy.aegis.encoding.Base32Exception;
+import me.impy.aegis.otp.HotpInfo;
+import me.impy.aegis.otp.OtpInfo;
+import me.impy.aegis.otp.OtpInfoException;
+import me.impy.aegis.otp.TotpInfo;
import me.impy.aegis.util.ByteInputStream;
-public class AndOTPImporter extends DatabaseImporter {
+public class AndOtpImporter extends DatabaseImporter {
private JSONArray _obj;
- public AndOTPImporter(ByteInputStream stream) {
+ public AndOtpImporter(ByteInputStream stream) {
super(stream);
}
@@ -41,30 +43,35 @@ public class AndOTPImporter extends DatabaseImporter {
for (int i = 0; i < _obj.length(); i++) {
JSONObject obj = _obj.getJSONObject(i);
- KeyInfo key = new KeyInfo();
- key.setAlgorithm(obj.getString("algorithm"));
- key.setDigits(obj.getInt("digits"));
- key.setPeriod(obj.getInt("period"));
- key.setType(obj.getString("type"));
- if (key.getType().equals("hotp")) {
- key.setCounter(obj.getLong("counter"));
+ String type = obj.getString("type");
+ String algo = obj.getString("algorithm");
+ int digits = obj.getInt("digits");
+ byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
+
+ OtpInfo info;
+ if (type.equals("totp")) {
+ info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
+ } else if (type.equals("hotp")) {
+ info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
+ } else {
+ throw new DatabaseImporterException("unsupported otp type: " + type);
}
+ String issuer = "";
+ String name = "";
+
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
- key.setIssuer(parts[0]);
- key.setAccountName(parts[1]);
+ issuer = parts[0];
+ name = parts[1];
} else {
- key.setAccountName(parts[0]);
+ name = parts[0];
}
- byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
- key.setSecret(secret);
-
- DatabaseEntry entry = new DatabaseEntry(key);
+ DatabaseEntry entry = new DatabaseEntry(info, name, issuer);
entries.add(entry);
}
- } catch (Base32Exception | KeyInfoException | JSONException e) {
+ } catch (Base32Exception | OtpInfoException | JSONException e) {
throw new DatabaseImporterException(e);
}
diff --git a/app/src/main/java/me/impy/aegis/importers/DatabaseImporter.java b/app/src/main/java/me/impy/aegis/importers/DatabaseImporter.java
index 286bcdd4..d4827baf 100644
--- a/app/src/main/java/me/impy/aegis/importers/DatabaseImporter.java
+++ b/app/src/main/java/me/impy/aegis/importers/DatabaseImporter.java
@@ -16,8 +16,8 @@ public abstract class DatabaseImporter {
// note: keep this list sorted alphabetically
LinkedHashMap> importers = new LinkedHashMap<>();
importers.put("Aegis", AegisImporter.class);
- importers.put("andOTP", AndOTPImporter.class);
- importers.put("FreeOTP", FreeOTPImporter.class);
+ importers.put("andOTP", AndOtpImporter.class);
+ importers.put("FreeOTP", FreeOtpImporter.class);
_importers = Collections.unmodifiableMap(importers);
}
diff --git a/app/src/main/java/me/impy/aegis/importers/DatabaseImporterException.java b/app/src/main/java/me/impy/aegis/importers/DatabaseImporterException.java
index 1dfd4116..d7411d1d 100644
--- a/app/src/main/java/me/impy/aegis/importers/DatabaseImporterException.java
+++ b/app/src/main/java/me/impy/aegis/importers/DatabaseImporterException.java
@@ -4,4 +4,8 @@ public class DatabaseImporterException extends Exception {
public DatabaseImporterException(Throwable cause) {
super(cause);
}
+
+ public DatabaseImporterException(String message) {
+ super(message);
+ }
}
diff --git a/app/src/main/java/me/impy/aegis/importers/FreeOTPImporter.java b/app/src/main/java/me/impy/aegis/importers/FreeOtpImporter.java
similarity index 55%
rename from app/src/main/java/me/impy/aegis/importers/FreeOTPImporter.java
rename to app/src/main/java/me/impy/aegis/importers/FreeOtpImporter.java
index 4624f946..3904c3bd 100644
--- a/app/src/main/java/me/impy/aegis/importers/FreeOTPImporter.java
+++ b/app/src/main/java/me/impy/aegis/importers/FreeOtpImporter.java
@@ -12,47 +12,86 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
-import me.impy.aegis.crypto.KeyInfo;
-import me.impy.aegis.crypto.KeyInfoException;
import me.impy.aegis.db.DatabaseEntry;
+import me.impy.aegis.otp.HotpInfo;
+import me.impy.aegis.otp.OtpInfo;
+import me.impy.aegis.otp.OtpInfoException;
+import me.impy.aegis.otp.TotpInfo;
import me.impy.aegis.util.ByteInputStream;
-public class FreeOTPImporter extends DatabaseImporter {
- public FreeOTPImporter(ByteInputStream stream) {
+public class FreeOtpImporter extends DatabaseImporter {
+ private List _xmlEntries;
+
+ public FreeOtpImporter(ByteInputStream stream) {
super(stream);
}
- private static class Entry {
+ private static class XmlEntry {
String Name;
String Value;
}
@Override
public void parse() throws DatabaseImporterException {
-
- }
-
- @Override
- public List convert() throws DatabaseImporterException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(_stream, null);
parser.nextTag();
- return parse(parser);
- } catch (KeyInfoException | XmlPullParserException | JSONException | IOException e) {
+ _xmlEntries = parse(parser);
+ } catch (XmlPullParserException | IOException e) {
throw new DatabaseImporterException(e);
}
}
+ @Override
+ public List convert() throws DatabaseImporterException {
+ List entries = new ArrayList<>();
+
+ try {
+ for (XmlEntry xmlEntry : _xmlEntries) {
+ if (xmlEntry.Name.equals("tokenOrder")) {
+ // TODO: order
+ JSONArray array = new JSONArray(xmlEntry.Value);
+ } else {
+ JSONObject obj = new JSONObject(xmlEntry.Value);
+
+ String type = obj.getString("type");
+ String algo = obj.getString("algo");
+ int digits = obj.getInt("digits");
+ byte[] secret = toBytes(obj.getJSONArray("secret"));
+
+ OtpInfo info;
+ if (type.equals("totp")) {
+ info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
+ } else if (type.equals("hotp")) {
+ info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
+ } else {
+ throw new DatabaseImporterException("unsupported otp type: " + type);
+ }
+
+ String issuer = obj.getString("issuerExt");
+ String name = obj.optString("label");
+
+ DatabaseEntry entry = new DatabaseEntry(info, name, issuer);
+ entries.add(entry);
+ }
+ }
+ } catch (OtpInfoException | JSONException e) {
+ throw new DatabaseImporterException(e);
+ }
+
+ return entries;
+ }
+
@Override
public boolean isEncrypted() {
return false;
}
- private static List parse(XmlPullParser parser)
- throws IOException, XmlPullParserException, JSONException, KeyInfoException {
- List entries = new ArrayList<>();
+ private static List parse(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ List entries = new ArrayList<>();
parser.require(XmlPullParser.START_TAG, null, "map");
while (parser.next() != XmlPullParser.END_TAG) {
@@ -68,32 +107,7 @@ public class FreeOTPImporter extends DatabaseImporter {
entries.add(parseEntry(parser));
}
- List profiles = new ArrayList<>();
-
- for (Entry entry : entries) {
- if (entry.Name.equals("tokenOrder")) {
- // TODO: order
- JSONArray array = new JSONArray(entry.Value);
- } else {
- JSONObject obj = new JSONObject(entry.Value);
-
- KeyInfo key = new KeyInfo();
- key.setAlgorithm(obj.getString("algo"));
- key.setCounter(obj.getLong("counter"));
- key.setDigits(obj.getInt("digits"));
- key.setIssuer(obj.getString("issuerExt"));
- key.setAccountName(obj.optString("label"));
- key.setPeriod(obj.getInt("period"));
- key.setType(obj.getString("type"));
- byte[] secret = toBytes(obj.getJSONArray("secret"));
- key.setSecret(secret);
-
- DatabaseEntry profile = new DatabaseEntry(key);
- profiles.add(profile);
- }
- }
-
- return profiles;
+ return entries;
}
private static byte[] toBytes(JSONArray array) throws JSONException {
@@ -104,12 +118,16 @@ public class FreeOTPImporter extends DatabaseImporter {
return bytes;
}
- private static Entry parseEntry(XmlPullParser parser) throws IOException, XmlPullParserException {
+ private static XmlEntry parseEntry(XmlPullParser parser) throws IOException, XmlPullParserException {
parser.require(XmlPullParser.START_TAG, null, "string");
String name = parser.getAttributeValue(null, "name");
String value = parseText(parser);
parser.require(XmlPullParser.END_TAG, null, "string");
- return new Entry() {{ Name = name; Value = value; }};
+
+ XmlEntry entry = new XmlEntry();
+ entry.Name = name;
+ entry.Value = value;
+ return entry;
}
private static String parseText(XmlPullParser parser) throws IOException, XmlPullParserException {
diff --git a/app/src/main/java/me/impy/aegis/otp/GoogleAuthInfo.java b/app/src/main/java/me/impy/aegis/otp/GoogleAuthInfo.java
new file mode 100644
index 00000000..478833c4
--- /dev/null
+++ b/app/src/main/java/me/impy/aegis/otp/GoogleAuthInfo.java
@@ -0,0 +1,149 @@
+package me.impy.aegis.otp;
+
+import android.net.Uri;
+
+import me.impy.aegis.encoding.Base32;
+import me.impy.aegis.encoding.Base32Exception;
+
+public class GoogleAuthInfo {
+ private OtpInfo _info;
+ private String _accountName;
+ private String _issuer;
+
+ public GoogleAuthInfo(OtpInfo info, String accountName, String issuer) {
+ _info = info;
+ _accountName = accountName;
+ _issuer = issuer;
+ }
+
+ public OtpInfo getOtpInfo() {
+ return _info;
+ }
+
+ public Uri getUri() {
+ Uri.Builder builder = new Uri.Builder();
+ builder.scheme("otpauth");
+
+ if (_info instanceof TotpInfo) {
+ builder.authority("totp");
+ builder.appendQueryParameter("period", Integer.toString(((TotpInfo)_info).getPeriod()));
+ } else if (_info instanceof HotpInfo) {
+ builder.authority("hotp");
+ builder.appendQueryParameter("counter", Long.toString(((HotpInfo)_info).getCounter()));
+ } else {
+ throw new RuntimeException();
+ }
+
+ builder.appendQueryParameter("digits", Integer.toString(_info.getDigits()));
+ builder.appendQueryParameter("algorithm", _info.getAlgorithm(false));
+ builder.appendQueryParameter("secret", new String(Base32.encode(_info.getSecret())));
+
+ if (_issuer != null && !_issuer.equals("")) {
+ builder.path(String.format("%s:%s", _issuer, _accountName));
+ builder.appendQueryParameter("issuer", _issuer);
+ } else {
+ builder.path(_accountName);
+ }
+
+ return builder.build();
+ }
+
+ public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
+ if (!uri.getScheme().equals("otpauth")) {
+ throw new GoogleAuthInfoException("unsupported protocol");
+ }
+
+ // 'secret' is a required parameter
+ String encodedSecret = uri.getQueryParameter("secret");
+ if (encodedSecret == null) {
+ throw new GoogleAuthInfoException("'secret' is not set");
+ }
+
+ // decode secret
+ byte[] secret;
+ try {
+ secret = Base32.decode(encodedSecret.toCharArray());
+ } catch (Base32Exception e) {
+ throw new GoogleAuthInfoException("bad secret", e);
+ }
+
+ // check the otp type
+ OtpInfo info;
+ try {
+ String type = uri.getHost();
+ switch (type) {
+ case "totp":
+ TotpInfo totpInfo = new TotpInfo(secret);
+ String period = uri.getQueryParameter("period");
+ if (period != null) {
+ totpInfo.setPeriod(Integer.parseInt(period));
+ }
+ info = totpInfo;
+ break;
+ case "hotp":
+ HotpInfo hotpInfo = new HotpInfo(secret);
+ String counter = uri.getQueryParameter("counter");
+ if (counter == null) {
+ throw new GoogleAuthInfoException("'counter' was not set");
+ }
+ hotpInfo.setCounter(Long.parseLong(counter));
+ info = hotpInfo;
+ break;
+ default:
+ throw new GoogleAuthInfoException(String.format("unsupported otp type: %s", type));
+ }
+ } catch (OtpInfoException e) {
+ throw new GoogleAuthInfoException(e);
+ }
+
+ // provider info used to disambiguate accounts
+ String path = uri.getPath();
+ String label = path != null && path.length() > 0 ? path.substring(1) : "";
+
+ String accountName = "";
+ String issuer = "";
+
+ if (label.contains(":")) {
+ // a label can only contain one colon
+ // it's ok to fail if that's not the case
+ String[] strings = label.split(":");
+ if (strings.length == 2) {
+ issuer = strings[0];
+ accountName = strings[1];
+ } else {
+ // at this point, just dump the whole thing into the accountName
+ accountName = label;
+ }
+ } else {
+ // label only contains the account name
+ // grab the issuer's info from the 'issuer' parameter if it's present
+ String issuerParam = uri.getQueryParameter("issuer");
+ issuer = issuerParam != null ? issuerParam : "";
+ accountName = label;
+ }
+
+ // just use the defaults if these parameters aren't set
+ try {
+ String algorithm = uri.getQueryParameter("algorithm");
+ if (algorithm != null) {
+ info.setAlgorithm(algorithm);
+ }
+ String digits = uri.getQueryParameter("digits");
+ if (digits != null) {
+ info.setDigits(Integer.parseInt(digits));
+ }
+ } catch (OtpInfoException e) {
+ throw new GoogleAuthInfoException(e);
+ }
+
+ return new GoogleAuthInfo(info, accountName, issuer);
+ }
+
+ public String getIssuer() {
+ return _issuer;
+ }
+
+ public String getAccountName() {
+ return _accountName;
+ }
+}
diff --git a/app/src/main/java/me/impy/aegis/crypto/KeyInfoException.java b/app/src/main/java/me/impy/aegis/otp/GoogleAuthInfoException.java
similarity index 52%
rename from app/src/main/java/me/impy/aegis/crypto/KeyInfoException.java
rename to app/src/main/java/me/impy/aegis/otp/GoogleAuthInfoException.java
index 13231b43..1984164b 100644
--- a/app/src/main/java/me/impy/aegis/crypto/KeyInfoException.java
+++ b/app/src/main/java/me/impy/aegis/otp/GoogleAuthInfoException.java
@@ -1,11 +1,15 @@
-package me.impy.aegis.crypto;
+package me.impy.aegis.otp;
-public class KeyInfoException extends Exception {
- public KeyInfoException(String message) {
+public class GoogleAuthInfoException extends Exception {
+ public GoogleAuthInfoException(Throwable cause) {
+ super(cause);
+ }
+
+ public GoogleAuthInfoException(String message) {
super(message);
}
- public KeyInfoException(String message, Throwable cause) {
+ public GoogleAuthInfoException(String message, Throwable cause) {
super(message, cause);
}
diff --git a/app/src/main/java/me/impy/aegis/otp/HotpInfo.java b/app/src/main/java/me/impy/aegis/otp/HotpInfo.java
new file mode 100644
index 00000000..1b406128
--- /dev/null
+++ b/app/src/main/java/me/impy/aegis/otp/HotpInfo.java
@@ -0,0 +1,67 @@
+package me.impy.aegis.otp;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import me.impy.aegis.crypto.otp.HOTP;
+
+public class HotpInfo extends OtpInfo {
+ private long _counter;
+
+ public HotpInfo(byte[] secret) throws OtpInfoException {
+ super(secret);
+ setCounter(0);
+ }
+
+ public HotpInfo(byte[] secret, String algorithm, int digits, long counter) throws OtpInfoException {
+ super(secret, algorithm, digits);
+ setCounter(counter);
+ }
+
+ @Override
+ public String getOtp() {
+ try {
+ return HOTP.generateOTP(getSecret(), getCounter(), getDigits(), false, -1);
+ } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+ throw new RuntimeException();
+ }
+ }
+
+ @Override
+ public String getType() {
+ return "hotp";
+ }
+
+ @Override
+ public JSONObject toJson() {
+ JSONObject obj = super.toJson();
+ try {
+ obj.put("counter", getCounter());
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ return obj;
+ }
+
+ public long getCounter() {
+ return _counter;
+ }
+
+ public static boolean isCounterValid(long counter) {
+ return counter >= 0;
+ }
+
+ public void setCounter(long counter) throws OtpInfoException {
+ if (!isCounterValid(counter)) {
+ throw new OtpInfoException(String.format("bad counter: %d", counter));
+ }
+ _counter = counter;
+ }
+
+ public void incrementCounter() throws OtpInfoException {
+ setCounter(getCounter() + 1);
+ }
+}
diff --git a/app/src/main/java/me/impy/aegis/otp/OtpInfo.java b/app/src/main/java/me/impy/aegis/otp/OtpInfo.java
new file mode 100644
index 00000000..fff34f45
--- /dev/null
+++ b/app/src/main/java/me/impy/aegis/otp/OtpInfo.java
@@ -0,0 +1,116 @@
+package me.impy.aegis.otp;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.Serializable;
+
+import me.impy.aegis.encoding.Base32;
+import me.impy.aegis.encoding.Base32Exception;
+
+public abstract class OtpInfo implements Serializable {
+ private byte[] _secret;
+ private String _algorithm;
+ private int _digits;
+
+ public OtpInfo(byte[] secret) throws OtpInfoException {
+ this(secret, "SHA1", 6);
+ }
+
+ public OtpInfo(byte[] secret, String algorithm, int digits) throws OtpInfoException {
+ setSecret(secret);
+ setAlgorithm(algorithm);
+ setDigits(digits);
+ }
+
+ public abstract String getOtp();
+
+ public abstract String getType();
+
+ public JSONObject toJson() {
+ JSONObject obj = new JSONObject();
+
+ try {
+ obj.put("secret", new String(Base32.encode(getSecret())));
+ obj.put("algo", getAlgorithm(false));
+ obj.put("digits", getDigits());
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+
+ return obj;
+ }
+
+ public byte[] getSecret() {
+ return _secret;
+ }
+
+ public String getAlgorithm(boolean java) {
+ if (java) {
+ return "Hmac" + _algorithm;
+ }
+ return _algorithm;
+ }
+
+ public int getDigits() {
+ return _digits;
+ }
+
+ public void setSecret(byte[] secret) {
+ _secret = secret;
+ }
+
+ public static boolean isAlgorithmValid(String algorithm) {
+ return algorithm.equals("SHA1") || algorithm.equals("SHA256") || algorithm.equals("SHA512");
+ }
+
+ public void setAlgorithm(String algorithm) throws OtpInfoException {
+ if (algorithm.startsWith("Hmac")) {
+ algorithm = algorithm.substring(4);
+ }
+ algorithm = algorithm.toUpperCase();
+
+ if (!isAlgorithmValid(algorithm)) {
+ throw new OtpInfoException(String.format("unsupported algorithm: %s", algorithm));
+ }
+ _algorithm = algorithm;
+ }
+
+ public static boolean isDigitsValid(int digits) {
+ return digits == 6 || digits == 8;
+ }
+
+ public void setDigits(int digits) throws OtpInfoException {
+ if (!isDigitsValid(digits)) {
+ throw new OtpInfoException(String.format("unsupported amount of digits: %d", digits));
+ }
+ _digits = digits;
+ }
+
+ public static OtpInfo parseJson(String type, JSONObject obj) throws OtpInfoException {
+ OtpInfo info;
+
+ try {
+ byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
+ String algo = obj.getString("algo");
+ int digits = obj.getInt("digits");
+
+ switch (type) {
+ case "totp":
+ int period = obj.getInt("period");
+ info = new TotpInfo(secret, algo, digits, period);
+ break;
+ case "hotp":
+ long counter = obj.getLong("counter");
+ info = new HotpInfo(secret, algo, digits, counter);
+ break;
+ default:
+ throw new OtpInfoException("unsupported otp type: " + type);
+ }
+ } catch (Base32Exception | JSONException e) {
+ throw new OtpInfoException(e);
+ }
+
+ return info;
+ }
+}
diff --git a/app/src/main/java/me/impy/aegis/otp/OtpInfoException.java b/app/src/main/java/me/impy/aegis/otp/OtpInfoException.java
new file mode 100644
index 00000000..37c25510
--- /dev/null
+++ b/app/src/main/java/me/impy/aegis/otp/OtpInfoException.java
@@ -0,0 +1,11 @@
+package me.impy.aegis.otp;
+
+public class OtpInfoException extends Exception {
+ public OtpInfoException(Throwable cause) {
+ super(cause);
+ }
+
+ public OtpInfoException(String message) {
+ super(message);
+ }
+}
diff --git a/app/src/main/java/me/impy/aegis/otp/TotpInfo.java b/app/src/main/java/me/impy/aegis/otp/TotpInfo.java
new file mode 100644
index 00000000..e1a72f72
--- /dev/null
+++ b/app/src/main/java/me/impy/aegis/otp/TotpInfo.java
@@ -0,0 +1,66 @@
+package me.impy.aegis.otp;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import me.impy.aegis.crypto.otp.TOTP;
+
+public class TotpInfo extends OtpInfo {
+ private int _period;
+
+ public TotpInfo(byte[] secret) throws OtpInfoException {
+ super(secret);
+ setPeriod(30);
+ }
+
+ public TotpInfo(byte[] secret, String algorithm, int digits, int period) throws OtpInfoException {
+ super(secret, algorithm, digits);
+ setPeriod(period);
+ }
+
+ @Override
+ public String getOtp() {
+ String time = Long.toHexString(System.currentTimeMillis() / 1000 / getPeriod());
+ return TOTP.generateTOTP(getSecret(), time, getDigits(), getAlgorithm(true));
+ }
+
+ @Override
+ public String getType() {
+ return "totp";
+ }
+
+ @Override
+ public JSONObject toJson() {
+ JSONObject obj = super.toJson();
+ try {
+ obj.put("period", getPeriod());
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ return obj;
+ }
+
+ public int getPeriod() {
+ return _period;
+ }
+
+ public static boolean isPeriodValid(int period) {
+ return period > 0;
+ }
+
+ public void setPeriod(int period) throws OtpInfoException {
+ if (!isPeriodValid(period)) {
+ throw new OtpInfoException(String.format("bad period: %d", period));
+ }
+ _period = period;
+ }
+
+ public long getMillisTillNextRotation() {
+ return TotpInfo.getMillisTillNextRotation(_period);
+ }
+
+ public static long getMillisTillNextRotation(int period) {
+ long p = period * 1000;
+ return p - (System.currentTimeMillis() % p);
+ }
+}
diff --git a/app/src/main/java/me/impy/aegis/ui/AuthActivity.java b/app/src/main/java/me/impy/aegis/ui/AuthActivity.java
index ab3311ff..75897ba2 100644
--- a/app/src/main/java/me/impy/aegis/ui/AuthActivity.java
+++ b/app/src/main/java/me/impy/aegis/ui/AuthActivity.java
@@ -26,17 +26,17 @@ 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.db.slots.SlotList;
import me.impy.aegis.db.slots.SlotException;
import me.impy.aegis.helpers.FingerprintHelper;
import me.impy.aegis.helpers.FingerprintUiHelper;
import me.impy.aegis.helpers.EditTextHelper;
-import me.impy.aegis.ui.tasks.SlotCollectionTask;
+import me.impy.aegis.ui.tasks.SlotListTask;
-public class AuthActivity extends AegisActivity implements FingerprintUiHelper.Callback, SlotCollectionTask.Callback {
+public class AuthActivity extends AegisActivity implements FingerprintUiHelper.Callback, SlotListTask.Callback {
private EditText _textPassword;
- private SlotCollection _slots;
+ private SlotList _slots;
private FingerprintUiHelper _fingerHelper;
private Cipher _fingerCipher;
@@ -57,7 +57,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
}
Intent intent = getIntent();
- _slots = (SlotCollection) intent.getSerializableExtra("slots");
+ _slots = (SlotList) intent.getSerializableExtra("slots");
// 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);
@@ -75,7 +75,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
invalidated = true;
continue;
}
- _fingerCipher = Slot.createCipher(key, Cipher.DECRYPT_MODE);
+ _fingerCipher = slot.createDecryptCipher(key);
_fingerHelper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
boxFingerprint.setVisibility(View.VISIBLE);
invalidated = false;
@@ -112,10 +112,8 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
}
private void trySlots(Class type, Object obj) {
- new SlotCollectionTask<>(type, this, this).execute(new SlotCollectionTask.Params(){{
- Slots = _slots;
- Obj = obj;
- }});
+ SlotListTask.Params params = new SlotListTask.Params(_slots, obj);
+ new SlotListTask<>(type, this, this).execute(params);
}
private void setKey(MasterKey key) {
diff --git a/app/src/main/java/me/impy/aegis/ui/EditProfileActivity.java b/app/src/main/java/me/impy/aegis/ui/EditEntryActivity.java
similarity index 61%
rename from app/src/main/java/me/impy/aegis/ui/EditProfileActivity.java
rename to app/src/main/java/me/impy/aegis/ui/EditEntryActivity.java
index 6430dde8..a694659b 100644
--- a/app/src/main/java/me/impy/aegis/ui/EditProfileActivity.java
+++ b/app/src/main/java/me/impy/aegis/ui/EditEntryActivity.java
@@ -19,32 +19,39 @@ import android.widget.EditText;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.Spinner;
+import android.widget.TableRow;
import com.amulyakhare.textdrawable.TextDrawable;
import me.impy.aegis.R;
-import me.impy.aegis.crypto.KeyInfo;
-import me.impy.aegis.crypto.KeyInfoException;
import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.encoding.Base32;
+import me.impy.aegis.encoding.Base32Exception;
import me.impy.aegis.helpers.EditTextHelper;
import me.impy.aegis.helpers.SpinnerHelper;
import me.impy.aegis.helpers.TextDrawableHelper;
+import me.impy.aegis.otp.HotpInfo;
+import me.impy.aegis.otp.OtpInfo;
+import me.impy.aegis.otp.OtpInfoException;
+import me.impy.aegis.otp.TotpInfo;
import me.impy.aegis.ui.dialogs.Dialogs;
-import me.impy.aegis.ui.views.KeyProfile;
-public class EditProfileActivity extends AegisActivity {
+public class EditEntryActivity extends AegisActivity {
private boolean _isNew = false;
private boolean _edited = false;
- private KeyProfile _profile;
+ private DatabaseEntry _entry;
private ImageView _iconView;
private EditText _textName;
private EditText _textIssuer;
private EditText _textPeriod;
+ private EditText _textCounter;
private EditText _textSecret;
+ private TableRow _rowPeriod;
+ private TableRow _rowCounter;
+
private Spinner _spinnerType;
private Spinner _spinnerAlgo;
private Spinner _spinnerDigits;
@@ -56,27 +63,28 @@ public class EditProfileActivity extends AegisActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_edit_profile);
+ setContentView(R.layout.activity_edit_entry);
ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setDisplayHomeAsUpEnabled(true);
- // if the intent doesn't contain a KeyProfile, create a new one
+ // retrieve info from the calling activity
Intent intent = getIntent();
- _profile = (KeyProfile) intent.getSerializableExtra("KeyProfile");
+ _entry = (DatabaseEntry) intent.getSerializableExtra("entry");
_isNew = intent.getBooleanExtra("isNew", false);
- if (_profile == null) {
- _profile = new KeyProfile();
- }
if (_isNew) {
setTitle("Add profile");
}
+ // set up fields
_iconView = findViewById(R.id.profile_drawable);
_textName = findViewById(R.id.text_name);
_textIssuer = findViewById(R.id.text_issuer);
_textPeriod = findViewById(R.id.text_period);
+ _rowPeriod = findViewById(R.id.row_period);
+ _textCounter = findViewById(R.id.text_counter);
+ _rowCounter = findViewById(R.id.row_counter);
_textSecret = findViewById(R.id.text_secret);
_spinnerType = findViewById(R.id.spinner_type);
SpinnerHelper.fillSpinner(this, _spinnerType, R.array.otp_types_array);
@@ -84,37 +92,84 @@ public class EditProfileActivity extends AegisActivity {
SpinnerHelper.fillSpinner(this, _spinnerAlgo, R.array.otp_algo_array);
_spinnerDigits = findViewById(R.id.spinner_digits);
SpinnerHelper.fillSpinner(this, _spinnerDigits, R.array.otp_digits_array);
-
_advancedSettingsHeader = findViewById(R.id.accordian_header);
_advancedSettings = findViewById(R.id.expandableLayout);
- updateFields();
+ // fill the fields with values if possible
+ if (_entry != null) {
+ TextDrawable drawable = TextDrawableHelper.generate(_entry.getIssuer(), _entry.getName());
+ _iconView.setImageDrawable(drawable);
+ _textName.setText(_entry.getName());
+ _textIssuer.setText(_entry.getIssuer());
+
+ OtpInfo info = _entry.getInfo();
+ if (info instanceof TotpInfo) {
+ _textPeriod.setText(Integer.toString(((TotpInfo) info).getPeriod()));
+ _rowPeriod.setVisibility(View.VISIBLE);
+ } else if (info instanceof HotpInfo) {
+ _textCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
+ _rowCounter.setVisibility(View.VISIBLE);
+ } else {
+ throw new RuntimeException();
+ }
+
+ byte[] secretBytes = _entry.getInfo().getSecret();
+ if (secretBytes != null) {
+ char[] secretChars = Base32.encode(secretBytes);
+ _textSecret.setText(secretChars, 0, secretChars.length);
+ }
+
+ String type = _entry.getInfo().getType();
+ _spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type.toUpperCase()), false);
+
+ String algo = _entry.getInfo().getAlgorithm(false);
+ _spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
+
+ String digits = Integer.toString(_entry.getInfo().getDigits());
+ _spinnerDigits.setSelection(getStringResourceIndex(R.array.otp_digits_array, digits), false);
+ }
+
+ // listen for changes to any of the fields
_textName.addTextChangedListener(_textListener);
_textIssuer.addTextChangedListener(_textListener);
_textPeriod.addTextChangedListener(_textListener);
+ _textCounter.addTextChangedListener(_textListener);
_textSecret.addTextChangedListener(_textListener);
- _spinnerType.setOnTouchListener(_selectedListener);
- _spinnerType.setOnItemSelectedListener(_selectedListener);
_spinnerAlgo.setOnTouchListener(_selectedListener);
_spinnerAlgo.setOnItemSelectedListener(_selectedListener);
_spinnerDigits.setOnTouchListener(_selectedListener);
_spinnerDigits.setOnItemSelectedListener(_selectedListener);
// update the icon if the text changed
- _textName.addTextChangedListener(new TextWatcher() {
+ _textIssuer.addTextChangedListener(_iconChangeListener);
+ _textName.addTextChangedListener(_iconChangeListener);
+
+ // show/hide period and counter fields on type change
+ _spinnerType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ String type = _spinnerType.getSelectedItem().toString();
+
+ switch (type.toLowerCase()) {
+ case "totp":
+ _rowCounter.setVisibility(View.GONE);
+ _rowPeriod.setVisibility(View.VISIBLE);
+ break;
+ case "hotp":
+ _rowPeriod.setVisibility(View.GONE);
+ _rowCounter.setVisibility(View.VISIBLE);
+ break;
+ default:
+ throw new RuntimeException();
+ }
+
+ _selectedListener.onItemSelected(parent, view, position, id);
}
@Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- }
+ public void onNothingSelected(AdapterView> parent) {
- @Override
- public void afterTextChanged(Editable s) {
- TextDrawable drawable = TextDrawableHelper.generate(s.toString());
- _iconView.setImageDrawable(drawable);
}
});
@@ -122,36 +177,12 @@ public class EditProfileActivity extends AegisActivity {
openAdvancedSettings();
});
- // Automatically open advanced settings since 'Secret' is required.
+ // automatically open advanced settings since 'Secret' is required.
if(_isNew){
openAdvancedSettings();
}
}
- private void updateFields() {
- DatabaseEntry entry = _profile.getEntry();
- _iconView.setImageDrawable(_profile.getDrawable());
-
- _textName.setText(entry.getName());
- _textIssuer.setText(entry.getInfo().getIssuer());
- _textPeriod.setText(Integer.toString(entry.getInfo().getPeriod()));
-
- byte[] secretBytes = entry.getInfo().getSecret();
- if (secretBytes != null) {
- char[] secretChars = Base32.encode(secretBytes);
- _textSecret.setText(secretChars, 0, secretChars.length);
- }
-
- String type = entry.getInfo().getType();
- _spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type), false);
-
- String algo = entry.getInfo().getAlgorithm(false);
- _spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
-
- String digits = Integer.toString(entry.getInfo().getDigits());
- _spinnerDigits.setSelection(getStringResourceIndex(R.array.otp_digits_array, digits), false);
- }
-
@Override
protected void setPreferredTheme(boolean darkMode) {
if (darkMode) {
@@ -252,7 +283,7 @@ public class EditProfileActivity extends AegisActivity {
private void finish(boolean delete) {
Intent intent = new Intent();
- intent.putExtra("KeyProfile", _profile);
+ intent.putExtra("entry", _entry);
intent.putExtra("delete", delete);
setResult(RESULT_OK, intent);
finish();
@@ -264,14 +295,6 @@ public class EditProfileActivity extends AegisActivity {
return false;
}
- int period;
- try {
- period = Integer.parseInt(_textPeriod.getText().toString());
- } catch (NumberFormatException e) {
- onError("Period is not an integer.");
- return false;
- }
-
String type = _spinnerType.getSelectedItem().toString();
String algo = _spinnerAlgo.getSelectedItem().toString();
@@ -283,24 +306,60 @@ public class EditProfileActivity extends AegisActivity {
return false;
}
- DatabaseEntry entry = _profile.getEntry();
- entry.setName(_textName.getText().toString());
- KeyInfo info = entry.getInfo();
-
+ byte[] secret;
try {
- char[] secret = EditTextHelper.getEditTextChars(_textSecret);
- info.setSecret(secret);
- info.setIssuer(_textIssuer.getText().toString());
- info.setPeriod(period);
+ secret = Base32.decode(EditTextHelper.getEditTextChars(_textSecret));
+ } catch (Base32Exception e) {
+ onError("Secret is not valid base32.");
+ return false;
+ }
+
+ // set otp info
+ OtpInfo info;
+ try {
+ switch (type.toLowerCase()) {
+ case "totp":
+ int period;
+ try {
+ period = Integer.parseInt(_textPeriod.getText().toString());
+ } catch (NumberFormatException e) {
+ onError("Period is not an integer.");
+ return false;
+ }
+ info = new TotpInfo(secret, algo, digits, period);
+ break;
+ case "hotp":
+ long counter;
+ try {
+ counter = Long.parseLong(_textCounter.getText().toString());
+ } catch (NumberFormatException e) {
+ onError("Counter is not an integer.");
+ return false;
+ }
+ info = new HotpInfo(secret, algo, digits, counter);
+ break;
+ default:
+ throw new RuntimeException();
+ }
+
info.setDigits(digits);
info.setAlgorithm(algo);
- info.setType(type);
- info.setAccountName(_textName.getText().toString());
- } catch (KeyInfoException e) {
+ } catch (OtpInfoException e) {
onError("The entered info is incorrect: " + e.getMessage());
return false;
}
+ // set database entry info
+ DatabaseEntry entry = _entry;
+ if (entry == null) {
+ entry = new DatabaseEntry(info);
+ } else {
+ entry.setInfo(info);
+ }
+ entry.setIssuer(_textIssuer.getText().toString());
+ entry.setName(_textName.getText().toString());
+
+ _entry = entry;
finish(false);
return true;
}
@@ -334,6 +393,22 @@ public class EditProfileActivity extends AegisActivity {
}
};
+ private TextWatcher _iconChangeListener = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ TextDrawable drawable = TextDrawableHelper.generate(_textIssuer.getText().toString(), _textName.getText().toString());
+ _iconView.setImageDrawable(drawable);
+ }
+ };
+
private class SpinnerItemSelectedListener implements AdapterView.OnItemSelectedListener, View.OnTouchListener {
private boolean _userSelect = false;
diff --git a/app/src/main/java/me/impy/aegis/ui/IntroActivity.java b/app/src/main/java/me/impy/aegis/ui/IntroActivity.java
index 877d1234..8870cb78 100644
--- a/app/src/main/java/me/impy/aegis/ui/IntroActivity.java
+++ b/app/src/main/java/me/impy/aegis/ui/IntroActivity.java
@@ -18,13 +18,12 @@ import javax.crypto.SecretKey;
import me.impy.aegis.Preferences;
import me.impy.aegis.R;
import me.impy.aegis.crypto.MasterKey;
-import me.impy.aegis.db.DatabaseException;
import me.impy.aegis.db.DatabaseFileException;
import me.impy.aegis.db.DatabaseManagerException;
import me.impy.aegis.db.slots.FingerprintSlot;
import me.impy.aegis.db.slots.PasswordSlot;
import me.impy.aegis.db.slots.Slot;
-import me.impy.aegis.db.slots.SlotCollection;
+import me.impy.aegis.db.slots.SlotList;
import me.impy.aegis.db.Database;
import me.impy.aegis.db.DatabaseFile;
import me.impy.aegis.db.DatabaseManager;
@@ -105,10 +104,8 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback {
if (newFragment == _endSlide && cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
_passwordSlot = new PasswordSlot();
- new DerivationTask(this, this).execute(new DerivationTask.Params() {{
- Slot = _passwordSlot;
- Password = _authenticatedSlide.getPassword();
- }});
+ DerivationTask.Params params = new DerivationTask.Params(_passwordSlot, _authenticatedSlide.getPassword());
+ new DerivationTask(this, this).execute(params);
} else if (oldFragment == _authenticationSlide && newFragment != _endSlide) {
// skip to the last slide if no encryption will be used
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
@@ -135,7 +132,7 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback {
masterKey = MasterKey.generate();
}
- SlotCollection slots = null;
+ SlotList slots = null;
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
// encrypt the master key with a key derived from the user's password
// and add it to the list of slots
@@ -143,8 +140,8 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback {
throw new RuntimeException();
}
try {
- slots = new SlotCollection();
- slots.encrypt(_passwordSlot, masterKey, _passwordCipher);
+ _passwordSlot.setKey(masterKey, _passwordCipher);
+ slots = new SlotList();
slots.add(_passwordSlot);
_databaseFile.setSlots(slots);
} catch (SlotException e) {
@@ -158,7 +155,7 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback {
// and add it to the list of slots
FingerprintSlot slot = _authenticatedSlide.getFingerSlot();
Cipher cipher = _authenticatedSlide.getFingerCipher();
- slots.encrypt(slot, masterKey, cipher);
+ slot.setKey(masterKey, cipher);
slots.add(slot);
} catch (SlotException e) {
setException(e);
@@ -175,7 +172,7 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback {
_databaseFile.setContent(obj, masterKey);
}
DatabaseManager.save(getApplicationContext(), _databaseFile);
- } catch (DatabaseException | DatabaseManagerException | DatabaseFileException e) {
+ } catch (DatabaseManagerException | DatabaseFileException e) {
setException(e);
return;
}
@@ -194,7 +191,7 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback {
public void onTaskFinished(SecretKey key) {
if (key != null) {
try {
- _passwordCipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE);
+ _passwordCipher = Slot.createEncryptCipher(key);
} catch (SlotException e) {
setException(e);
}
diff --git a/app/src/main/java/me/impy/aegis/ui/MainActivity.java b/app/src/main/java/me/impy/aegis/ui/MainActivity.java
index 4c603dc0..a0744bba 100644
--- a/app/src/main/java/me/impy/aegis/ui/MainActivity.java
+++ b/app/src/main/java/me/impy/aegis/ui/MainActivity.java
@@ -26,15 +26,14 @@ import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.db.DatabaseManager;
import me.impy.aegis.helpers.PermissionHelper;
import me.impy.aegis.ui.dialogs.Dialogs;
-import me.impy.aegis.ui.views.KeyProfile;
-import me.impy.aegis.ui.views.KeyProfileView;
+import me.impy.aegis.ui.views.EntryListView;
-public class MainActivity extends AegisActivity implements KeyProfileView.Listener {
+public class MainActivity extends AegisActivity implements EntryListView.Listener {
// activity request codes
- private static final int CODE_SCAN_KEYINFO = 0;
- private static final int CODE_ADD_KEYINFO = 1;
- private static final int CODE_EDIT_KEYINFO = 2;
- private static final int CODE_ENTER_KEYINFO = 3;
+ private static final int CODE_SCAN = 0;
+ private static final int CODE_ADD_ENTRY = 1;
+ private static final int CODE_EDIT_ENTRY = 2;
+ private static final int CODE_ENTER_ENTRY = 3;
private static final int CODE_DO_INTRO = 4;
private static final int CODE_DECRYPT = 5;
private static final int CODE_PREFERENCES = 6;
@@ -44,7 +43,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
private AegisApplication _app;
private DatabaseManager _db;
- private KeyProfileView _keyProfileView;
+ private EntryListView _entryListView;
private Menu _menu;
private FloatingActionsMenu _fabMenu;
@@ -58,20 +57,20 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
// set up the main view
setContentView(R.layout.activity_main);
- // set up the key profile view
- _keyProfileView = (KeyProfileView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
- _keyProfileView.setListener(this);
- _keyProfileView.setShowIssuer(getPreferences().isIssuerVisible());
+ // set up the entry view
+ _entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
+ _entryListView.setListener(this);
+ _entryListView.setShowIssuer(getPreferences().isIssuerVisible());
// set up the floating action button
_fabMenu = findViewById(R.id.fab);
findViewById(R.id.fab_enter).setOnClickListener(view -> {
_fabMenu.collapse();
- onEnterKeyInfo();
+ onEnterEntry();
});
findViewById(R.id.fab_scan).setOnClickListener(view -> {
_fabMenu.collapse();
- onScanKeyInfo();
+ onScan();
});
// skip this part if this is the not initial startup and the database has been unlocked
@@ -101,9 +100,9 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
}
}
- // if the database has been decrypted at this point, we can load the key profiles
+ // if the database has been decrypted at this point, we can load the entries
if (!_db.isLocked()) {
- loadKeyProfiles();
+ loadEntries();
}
}
@@ -141,17 +140,17 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
}
switch (requestCode) {
- case CODE_SCAN_KEYINFO:
- onScanKeyInfoResult(resultCode, data);
+ case CODE_SCAN:
+ onScanResult(resultCode, data);
break;
- case CODE_ADD_KEYINFO:
- onAddKeyInfoResult(resultCode, data);
+ case CODE_ADD_ENTRY:
+ onAddEntryResult(resultCode, data);
break;
- case CODE_EDIT_KEYINFO:
- onEditKeyInfoResult(resultCode, data);
+ case CODE_EDIT_ENTRY:
+ onEditEntryResult(resultCode, data);
break;
- case CODE_ENTER_KEYINFO:
- onEnterKeyInfoResult(resultCode, data);
+ case CODE_ENTER_ENTRY:
+ onEnterEntryResult(resultCode, data);
break;
case CODE_DO_INTRO:
onDoIntroResult(resultCode, data);
@@ -174,35 +173,35 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
switch (requestCode) {
case CODE_PERM_CAMERA:
- onScanKeyInfo();
+ onScan();
break;
}
}
private void onPreferencesResult(int resultCode, Intent data) {
- // refresh the entire key profile list if needed
+ // refresh the entire entry list if needed
if (data.getBooleanExtra("needsRecreate", false)) {
recreate();
} else if (data.getBooleanExtra("needsRefresh", false)) {
boolean showIssuer = getPreferences().isIssuerVisible();
- _keyProfileView.setShowIssuer(showIssuer);
+ _entryListView.setShowIssuer(showIssuer);
}
}
- private void startEditProfileActivity(int requestCode, KeyProfile profile, boolean isNew) {
- Intent intent = new Intent(this, EditProfileActivity.class);
- if (profile != null) {
- intent.putExtra("KeyProfile", profile);
+ private void startEditProfileActivity(int requestCode, DatabaseEntry entry, boolean isNew) {
+ Intent intent = new Intent(this, EditEntryActivity.class);
+ if (entry != null) {
+ intent.putExtra("entry", entry);
}
intent.putExtra("isNew", isNew);
startActivityForResult(intent, requestCode);
}
- private void onEnterKeyInfo() {
- startEditProfileActivity(CODE_ENTER_KEYINFO, null, true);
+ private void onEnterEntry() {
+ startEditProfileActivity(CODE_ENTER_ENTRY, null, true);
}
- private void onScanKeyInfo() {
+ private void onScan() {
if (!PermissionHelper.request(this, CODE_PERM_CAMERA, Manifest.permission.CAMERA)) {
return;
}
@@ -210,49 +209,47 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
startScanActivity();
}
- private void onScanKeyInfoResult(int resultCode, Intent data) {
+ private void onScanResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
- KeyProfile profile = (KeyProfile)data.getSerializableExtra("KeyProfile");
- startEditProfileActivity(CODE_ADD_KEYINFO, profile, true);
+ DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
+ startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
}
}
- private void onAddKeyInfoResult(int resultCode, Intent data) {
+ private void onAddEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
- KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile");
- addKey(profile);
+ DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
+ addEntry(entry);
saveDatabase();
}
}
- private void onEditKeyInfoResult(int resultCode, Intent data) {
+ private void onEditEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
- KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile");
+ DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
if (!data.getBooleanExtra("delete", false)) {
// this profile has been serialized/deserialized and is no longer the same instance it once was
- // to deal with this, the replaceKey functions are used
- _db.replaceKey(profile.getEntry());
- _keyProfileView.replaceKey(profile);
+ // to deal with this, the replaceEntry functions are used
+ _db.replaceEntry(entry);
+ _entryListView.replaceEntry(entry);
saveDatabase();
} else {
- deleteProfile(profile);
+ deleteEntry(entry);
}
}
}
- private void onEnterKeyInfoResult(int resultCode, Intent data) {
+ private void onEnterEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
- KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile");
- addKey(profile);
+ DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
+ addEntry(entry);
saveDatabase();
}
}
- private void addKey(KeyProfile profile) {
- DatabaseEntry entry = profile.getEntry();
- entry.setName(entry.getInfo().getAccountName());
- _db.addKey(entry);
- _keyProfileView.addKey(profile);
+ private void addEntry(DatabaseEntry entry) {
+ _db.addEntry(entry);
+ _entryListView.addEntry(entry);
}
private void onDoIntroResult(int resultCode, Intent data) {
@@ -275,7 +272,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
return;
}
- loadKeyProfiles();
+ loadEntries();
}
private void onDecryptResult(int resultCode, Intent intent) {
@@ -289,13 +286,13 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
return;
}
- loadKeyProfiles();
+ loadEntries();
doShortcutActions();
}
private void startScanActivity() {
Intent scannerActivity = new Intent(getApplicationContext(), ScannerActivity.class);
- startActivityForResult(scannerActivity, CODE_SCAN_KEYINFO);
+ startActivityForResult(scannerActivity, CODE_SCAN);
}
private boolean doShortcutActions() {
@@ -330,12 +327,12 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
updateLockIcon();
// refresh all codes to prevent showing old ones
- _keyProfileView.refresh();
+ _entryListView.refresh();
}
- private BottomSheetDialog createBottomSheet(final KeyProfile profile) {
+ private BottomSheetDialog createBottomSheet(final DatabaseEntry entry) {
BottomSheetDialog dialog = new BottomSheetDialog(this);
- dialog.setContentView(R.layout.bottom_sheet_edit_profile);
+ dialog.setContentView(R.layout.bottom_sheet_edit_entry);
dialog.setCancelable(true);
dialog.getWindow().setLayout(LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
@@ -344,7 +341,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
dialog.findViewById(R.id.copy_button).setOnClickListener(view -> {
dialog.dismiss();
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
- ClipData clip = ClipData.newPlainText("text/plain", profile.getCode());
+ ClipData clip = ClipData.newPlainText("text/plain", entry.getInfo().getOtp());
clipboard.setPrimaryClip(clip);
Toast.makeText(this, "Code copied to the clipboard", Toast.LENGTH_SHORT).show();
});
@@ -352,23 +349,23 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
dialog.findViewById(R.id.delete_button).setOnClickListener(view -> {
dialog.dismiss();
Dialogs.showDeleteEntryDialog(this, (d, which) -> {
- deleteProfile(profile);
+ deleteEntry(entry);
});
});
dialog.findViewById(R.id.edit_button).setOnClickListener(view -> {
dialog.dismiss();
- startEditProfileActivity(CODE_EDIT_KEYINFO, profile, false);
+ startEditProfileActivity(CODE_EDIT_ENTRY, entry, false);
});
return dialog;
}
- private void deleteProfile(KeyProfile profile) {
- _db.removeKey(profile.getEntry());
+ private void deleteEntry(DatabaseEntry entry) {
+ _db.removeEntry(entry);
saveDatabase();
- _keyProfileView.removeKey(profile);
+ _entryListView.removeEntry(entry);
}
@Override
@@ -387,7 +384,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
startActivityForResult(intent, CODE_PREFERENCES);
return true;
case R.id.action_lock:
- _keyProfileView.clearKeys();
+ _entryListView.clearEntries();
_db.lock();
startAuthActivity();
return true;
@@ -411,11 +408,11 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
}
}
- private void loadKeyProfiles() {
+ private void loadEntries() {
updateLockIcon();
- for (DatabaseEntry entry : _db.getKeys()) {
- _keyProfileView.addKey(new KeyProfile(entry));
+ for (DatabaseEntry entry : _db.getEntries()) {
+ _entryListView.addEntry(entry);
}
}
@@ -428,17 +425,22 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
}
@Override
- public void onEntryClick(KeyProfile profile) {
- createBottomSheet(profile).show();
+ public void onEntryClick(DatabaseEntry entry) {
+ createBottomSheet(entry).show();
}
@Override
public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) {
- _db.swapKeys(entry1, entry2);
+ _db.swapEntries(entry1, entry2);
}
@Override
public void onEntryDrop(DatabaseEntry entry) {
saveDatabase();
}
+
+ @Override
+ public void onEntryChange(DatabaseEntry entry) {
+ saveDatabase();
+ }
}
diff --git a/app/src/main/java/me/impy/aegis/ui/PreferencesFragment.java b/app/src/main/java/me/impy/aegis/ui/PreferencesFragment.java
index 8857ec50..f1134d80 100644
--- a/app/src/main/java/me/impy/aegis/ui/PreferencesFragment.java
+++ b/app/src/main/java/me/impy/aegis/ui/PreferencesFragment.java
@@ -32,7 +32,7 @@ import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.db.DatabaseManager;
import me.impy.aegis.db.DatabaseManagerException;
import me.impy.aegis.db.slots.Slot;
-import me.impy.aegis.db.slots.SlotCollection;
+import me.impy.aegis.db.slots.SlotList;
import me.impy.aegis.db.slots.SlotException;
import me.impy.aegis.helpers.PermissionHelper;
import me.impy.aegis.importers.AegisImporter;
@@ -313,7 +313,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas
private void importDatabase(DatabaseImporter importer) throws DatabaseImporterException {
List entries = importer.convert();
for (DatabaseEntry entry : entries) {
- _db.addKey(entry);
+ _db.addEntry(entry);
}
if (!saveDatabase()) {
@@ -369,7 +369,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas
return;
}
- SlotCollection slots = (SlotCollection) data.getSerializableExtra("slots");
+ SlotList slots = (SlotList) data.getSerializableExtra("slots");
_db.getFile().setSlots(slots);
saveDatabase();
}
@@ -390,9 +390,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas
public void onSlotResult(Slot slot, Cipher cipher) {
MasterKey masterKey = MasterKey.generate();
- SlotCollection slots = new SlotCollection();
+ SlotList slots = new SlotList();
try {
- slots.encrypt(slot, masterKey, cipher);
+ slot.setKey(masterKey, cipher);
} catch (SlotException e) {
onException(e);
return;
diff --git a/app/src/main/java/me/impy/aegis/ui/ScannerActivity.java b/app/src/main/java/me/impy/aegis/ui/ScannerActivity.java
index d0de6467..65f787cf 100644
--- a/app/src/main/java/me/impy/aegis/ui/ScannerActivity.java
+++ b/app/src/main/java/me/impy/aegis/ui/ScannerActivity.java
@@ -3,6 +3,7 @@ package me.impy.aegis.ui;
import android.content.Context;
import android.content.Intent;
import android.hardware.Camera;
+import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
@@ -16,11 +17,10 @@ import java.util.Collections;
import me.dm7.barcodescanner.core.IViewFinder;
import me.dm7.barcodescanner.zxing.ZXingScannerView;
import me.impy.aegis.R;
-import me.impy.aegis.crypto.KeyInfo;
-import me.impy.aegis.crypto.KeyInfoException;
import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.helpers.SquareFinderView;
-import me.impy.aegis.ui.views.KeyProfile;
+import me.impy.aegis.otp.GoogleAuthInfo;
+import me.impy.aegis.otp.GoogleAuthInfoException;
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK;
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT;
@@ -107,16 +107,19 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
@Override
public void handleResult(Result rawResult) {
try {
- KeyInfo info = KeyInfo.fromURL(rawResult.getText());
- KeyProfile profile = new KeyProfile(new DatabaseEntry(info));
- profile.getEntry().setName(info.getAccountName());
+ // parse google auth uri
+ Uri uri = Uri.parse(rawResult.getText());
+ GoogleAuthInfo info = GoogleAuthInfo.parseUri(uri);
- Intent resultIntent = new Intent();
- resultIntent.putExtra("KeyProfile", profile);
+ DatabaseEntry entry = new DatabaseEntry(info.getOtpInfo());
+ entry.setIssuer(info.getIssuer());
+ entry.setName(info.getAccountName());
- setResult(RESULT_OK, resultIntent);
+ Intent intent = new Intent();
+ intent.putExtra("entry", entry);
+ setResult(RESULT_OK, intent);
finish();
- } catch (KeyInfoException e) {
+ } catch (GoogleAuthInfoException e) {
Toast.makeText(this, "An error occurred while trying to parse the QR code contents", Toast.LENGTH_SHORT).show();
}
diff --git a/app/src/main/java/me/impy/aegis/ui/SlotManagerActivity.java b/app/src/main/java/me/impy/aegis/ui/SlotManagerActivity.java
index fb0f453b..461cac7c 100644
--- a/app/src/main/java/me/impy/aegis/ui/SlotManagerActivity.java
+++ b/app/src/main/java/me/impy/aegis/ui/SlotManagerActivity.java
@@ -20,7 +20,7 @@ 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.db.slots.SlotList;
import me.impy.aegis.db.slots.SlotException;
import me.impy.aegis.helpers.FingerprintHelper;
import me.impy.aegis.ui.dialogs.Dialogs;
@@ -31,7 +31,7 @@ import me.impy.aegis.ui.dialogs.SlotDialogFragment;
public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener, SlotDialogFragment.Listener {
private MasterKey _masterKey;
- private SlotCollection _slots;
+ private SlotList _slots;
private SlotAdapter _adapter;
private boolean _edited = false;
@@ -65,7 +65,7 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
// load the slots and masterKey
_masterKey = (MasterKey) getIntent().getSerializableExtra("masterKey");
- _slots = (SlotCollection) getIntent().getSerializableExtra("slots");
+ _slots = (SlotList) getIntent().getSerializableExtra("slots");
for (Slot slot : _slots) {
_adapter.addSlot(slot);
}
@@ -176,7 +176,7 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
try {
- _slots.encrypt(slot, _masterKey, cipher);
+ slot.setKey(_masterKey, cipher);
} catch (SlotException e) {
onException(e);
return;
diff --git a/app/src/main/java/me/impy/aegis/ui/dialogs/FingerprintDialogFragment.java b/app/src/main/java/me/impy/aegis/ui/dialogs/FingerprintDialogFragment.java
index c34a2cf9..225b0a14 100644
--- a/app/src/main/java/me/impy/aegis/ui/dialogs/FingerprintDialogFragment.java
+++ b/app/src/main/java/me/impy/aegis/ui/dialogs/FingerprintDialogFragment.java
@@ -38,7 +38,7 @@ public class FingerprintDialogFragment extends SlotDialogFragment implements Fin
try {
_slot = new FingerprintSlot();
SecretKey key = new KeyStoreHandle().generateKey(_slot.getUUID().toString());
- _cipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE);
+ _cipher = Slot.createEncryptCipher(key);
_helper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
} catch (KeyStoreHandleException | SlotException e) {
throw new RuntimeException(e);
diff --git a/app/src/main/java/me/impy/aegis/ui/dialogs/PasswordDialogFragment.java b/app/src/main/java/me/impy/aegis/ui/dialogs/PasswordDialogFragment.java
index 3f46a1e6..f0c5aa65 100644
--- a/app/src/main/java/me/impy/aegis/ui/dialogs/PasswordDialogFragment.java
+++ b/app/src/main/java/me/impy/aegis/ui/dialogs/PasswordDialogFragment.java
@@ -51,7 +51,7 @@ public class PasswordDialogFragment extends SlotDialogFragment {
DerivationTask task = new DerivationTask(getActivity(), key -> {
Cipher cipher;
try {
- cipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE);
+ cipher = Slot.createEncryptCipher(key);
} catch (SlotException e) {
getListener().onException(e);
dialog.cancel();
@@ -60,10 +60,7 @@ public class PasswordDialogFragment extends SlotDialogFragment {
getListener().onSlotResult(slot, cipher);
dialog.dismiss();
});
- task.execute(new DerivationTask.Params() {{
- Slot = slot;
- Password = password;
- }});
+ task.execute(new DerivationTask.Params(slot, password));
});
});
diff --git a/app/src/main/java/me/impy/aegis/ui/slides/CustomAuthenticatedSlide.java b/app/src/main/java/me/impy/aegis/ui/slides/CustomAuthenticatedSlide.java
index ef7f32b3..b0f6fe8f 100644
--- a/app/src/main/java/me/impy/aegis/ui/slides/CustomAuthenticatedSlide.java
+++ b/app/src/main/java/me/impy/aegis/ui/slides/CustomAuthenticatedSlide.java
@@ -113,7 +113,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
}
try {
- _fingerCipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE);
+ _fingerCipher = Slot.createEncryptCipher(key);
} catch (Exception e) {
throw new UndeclaredThrowableException(e);
}
diff --git a/app/src/main/java/me/impy/aegis/ui/tasks/DerivationTask.java b/app/src/main/java/me/impy/aegis/ui/tasks/DerivationTask.java
index 7b7600a7..73ed0065 100644
--- a/app/src/main/java/me/impy/aegis/ui/tasks/DerivationTask.java
+++ b/app/src/main/java/me/impy/aegis/ui/tasks/DerivationTask.java
@@ -6,7 +6,6 @@ import javax.crypto.SecretKey;
import me.impy.aegis.crypto.CryptoUtils;
import me.impy.aegis.db.slots.PasswordSlot;
-import me.impy.aegis.db.slots.SlotException;
public class DerivationTask extends ProgressDialogTask {
private Callback _cb;
@@ -22,7 +21,7 @@ public class DerivationTask extends ProgressDialogTask extends ProgressDialogTask {
+public class SlotListTask extends ProgressDialogTask {
private Callback _cb;
private Class _type;
- public SlotCollectionTask(Class type, Context context, Callback cb) {
+ public SlotListTask(Class type, Context context, Callback cb) {
super(context, "Decrypting database");
_cb = cb;
_type = type;
}
@Override
- protected MasterKey doInBackground(SlotCollectionTask.Params... args) {
+ protected MasterKey doInBackground(SlotListTask.Params... args) {
setPriority();
Params params = args[0];
+ SlotList slots = params.getSlots();
try {
- if (!params.Slots.has(_type)) {
+ if (!slots.has(_type)) {
throw new RuntimeException();
}
MasterKey masterKey = null;
- for (Slot slot : params.Slots.findAll(_type)) {
+ for (Slot slot : slots.findAll(_type)) {
try {
if (slot instanceof PasswordSlot) {
- char[] password = (char[])params.Obj;
+ char[] password = (char[])params.getObj();
SecretKey key = ((PasswordSlot)slot).deriveKey(password);
- Cipher cipher = Slot.createCipher(key, Cipher.DECRYPT_MODE);
- masterKey = params.Slots.decrypt(slot, cipher);
+ Cipher cipher = slot.createDecryptCipher(key);
+ masterKey = slot.getKey(cipher);
} else if (slot instanceof FingerprintSlot) {
- masterKey = params.Slots.decrypt(slot, (Cipher)params.Obj);
+ masterKey = slot.getKey((Cipher)params.getObj());
} else {
throw new RuntimeException();
}
break;
- } catch (SlotIntegrityException e) { }
+ } catch (SlotIntegrityException e) {
+
+ }
}
if (masterKey == null) {
@@ -60,7 +61,7 @@ public class SlotCollectionTask extends ProgressDialogTask extends ProgressDialogTask implements ItemTouchHelperAdapter {
+ private List _entries;
+ private static Listener _listener;
+ private boolean _showIssuer;
+
+ public EntryAdapter(Listener listener) {
+ _entries = new ArrayList<>();
+ _listener = listener;
+ }
+
+ public void setShowIssuer(boolean showIssuer) {
+ _showIssuer = showIssuer;
+ }
+
+ public void addEntry(DatabaseEntry entry) {
+ _entries.add(entry);
+
+ int position = getItemCount() - 1;
+ if (position == 0) {
+ notifyDataSetChanged();
+ } else {
+ notifyItemInserted(position);
+ }
+ }
+
+ public void removeEntry(DatabaseEntry entry) {
+ entry = getEntryByUUID(entry.getUUID());
+ int position = _entries.indexOf(entry);
+ _entries.remove(position);
+ notifyItemRemoved(position);
+ }
+
+ public void clearEntries() {
+ _entries.clear();
+ notifyDataSetChanged();
+ }
+
+ public void replaceEntry(DatabaseEntry newEntry) {
+ DatabaseEntry oldEntry = getEntryByUUID(newEntry.getUUID());
+ int position = _entries.indexOf(oldEntry);
+ _entries.set(position, newEntry);
+ notifyItemChanged(position);
+ }
+
+ private DatabaseEntry getEntryByUUID(UUID uuid) {
+ for (DatabaseEntry entry : _entries) {
+ if (entry.getUUID().equals(uuid)) {
+ return entry;
+ }
+ }
+ throw new AssertionError("no entry found with the same id");
+ }
+
+ @Override
+ public void onItemDismiss(int position) {
+
+ }
+
+ @Override
+ public void onItemDrop(int position) {
+ _listener.onEntryDrop(_entries.get(position));
+ }
+
+ @Override
+ public void onItemMove(int firstPosition, int secondPosition) {
+ // notify the database first
+ _listener.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition));
+
+ // update our side of things
+ Collections.swap(_entries, firstPosition, secondPosition);
+ notifyItemMoved(firstPosition, secondPosition);
+ }
+
+ @Override
+ public EntryHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_entry, parent, false);
+ return new EntryHolder(view);
+ }
+
+ @Override
+ public void onViewRecycled(EntryHolder holder) {
+ holder.stopRefreshLoop();
+ super.onViewRecycled(holder);
+ }
+
+ @Override
+ public void onBindViewHolder(final EntryHolder holder, int position) {
+ DatabaseEntry entry = _entries.get(position);
+ boolean showProgress = !isPeriodUniform() && entry.getInfo() instanceof TotpInfo;
+ holder.setData(entry, _showIssuer, showProgress);
+ if (showProgress) {
+ holder.startRefreshLoop();
+ }
+
+ holder.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int position = holder.getAdapterPosition();
+ _listener.onEntryClick(_entries.get(position));
+ }
+ });
+ holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ int position = holder.getAdapterPosition();
+ return _listener.onLongEntryClick(_entries.get(position));
+ }
+ });
+ holder.setOnRefreshClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // this will only be called if the entry is of type HotpInfo
+ try {
+ ((HotpInfo)entry.getInfo()).incrementCounter();
+ } catch (OtpInfoException e) {
+ throw new RuntimeException(e);
+ }
+
+ // notify the listener that the counter has been incremented
+ // this gives it a chance to save the database
+ _listener.onEntryChange(entry);
+
+ // finally, refresh the code in the UI
+ holder.refreshCode();
+ }
+ });
+ }
+
+ public int getUniformPeriod() {
+ List infos = new ArrayList<>();
+ for (DatabaseEntry entry : _entries) {
+ OtpInfo info = entry.getInfo();
+ if (info instanceof TotpInfo) {
+ infos.add((TotpInfo) info);
+ }
+ }
+
+ if (infos.isEmpty()) {
+ return -1;
+ }
+
+ int period = infos.get(0).getPeriod();
+ for (TotpInfo info : infos) {
+ if (period != info.getPeriod()) {
+ return -1;
+ }
+ }
+
+ return period;
+ }
+
+ public boolean isPeriodUniform() {
+ return getUniformPeriod() != -1;
+ }
+
+ @Override
+ public int getItemCount() {
+ return _entries.size();
+ }
+
+ public interface Listener {
+ void onEntryClick(DatabaseEntry entry);
+ boolean onLongEntryClick(DatabaseEntry entry);
+ void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2);
+ void onEntryDrop(DatabaseEntry entry);
+ void onEntryChange(DatabaseEntry entry);
+ }
+}
diff --git a/app/src/main/java/me/impy/aegis/ui/views/KeyProfileHolder.java b/app/src/main/java/me/impy/aegis/ui/views/EntryHolder.java
similarity index 53%
rename from app/src/main/java/me/impy/aegis/ui/views/KeyProfileHolder.java
rename to app/src/main/java/me/impy/aegis/ui/views/EntryHolder.java
index da19b193..38c05e0e 100644
--- a/app/src/main/java/me/impy/aegis/ui/views/KeyProfileHolder.java
+++ b/app/src/main/java/me/impy/aegis/ui/views/EntryHolder.java
@@ -9,31 +9,38 @@ import android.widget.TextView;
import com.amulyakhare.textdrawable.TextDrawable;
import me.impy.aegis.R;
-import me.impy.aegis.helpers.UIRefresher;
+import me.impy.aegis.db.DatabaseEntry;
+import me.impy.aegis.helpers.TextDrawableHelper;
+import me.impy.aegis.helpers.UiRefresher;
+import me.impy.aegis.otp.HotpInfo;
+import me.impy.aegis.otp.OtpInfoException;
+import me.impy.aegis.otp.TotpInfo;
-public class KeyProfileHolder extends RecyclerView.ViewHolder {
+public class EntryHolder extends RecyclerView.ViewHolder {
private TextView _profileName;
private TextView _profileCode;
private TextView _profileIssuer;
private ImageView _profileDrawable;
- private KeyProfile _profile;
+ private DatabaseEntry _entry;
+ private ImageView _buttonRefresh;
private PeriodProgressBar _progressBar;
- private UIRefresher _refresher;
+ private UiRefresher _refresher;
- public KeyProfileHolder(final View view) {
+ public EntryHolder(final View view) {
super(view);
_profileName = view.findViewById(R.id.profile_name);
_profileCode = view.findViewById(R.id.profile_code);
_profileIssuer = view.findViewById(R.id.profile_issuer);
_profileDrawable = view.findViewById(R.id.ivTextDrawable);
+ _buttonRefresh = view.findViewById(R.id.buttonRefresh);
_progressBar = view.findViewById(R.id.progressBar);
int primaryColorId = view.getContext().getResources().getColor(R.color.colorPrimary);
_progressBar.getProgressDrawable().setColorFilter(primaryColorId, PorterDuff.Mode.SRC_IN);
- _refresher = new UIRefresher(new UIRefresher.Listener() {
+ _refresher = new UiRefresher(new UiRefresher.Listener() {
@Override
public void onRefresh() {
refreshCode();
@@ -42,31 +49,39 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder {
@Override
public long getMillisTillNextRefresh() {
- return _profile.getEntry().getInfo().getMillisTillNextRotation();
+ return ((TotpInfo)_entry.getInfo()).getMillisTillNextRotation();
}
});
}
- public void setData(KeyProfile profile, boolean showIssuer, boolean showProgress) {
- _profile = profile;
+ public void setData(DatabaseEntry entry, boolean showIssuer, boolean showProgress) {
+ _entry = entry;
- _progressBar.setVisibility(showProgress ? View.VISIBLE : View.INVISIBLE);
+ // only show the progress bar if there is no uniform period and the entry type is TotpInfo
+ _progressBar.setVisibility(showProgress ? View.VISIBLE : View.GONE);
if (showProgress) {
- _progressBar.setPeriod(profile.getEntry().getInfo().getPeriod());
+ _progressBar.setPeriod(((TotpInfo)entry.getInfo()).getPeriod());
}
- _profileName.setText(profile.getEntry().getName());
+ // only show the button if this entry is of type HotpInfo
+ _buttonRefresh.setVisibility(entry.getInfo() instanceof HotpInfo ? View.VISIBLE : View.GONE);
+
+ _profileName.setText(entry.getName());
_profileIssuer.setText("");
if (showIssuer) {
- _profileIssuer.setText(" - " + profile.getEntry().getInfo().getIssuer());
+ _profileIssuer.setText(" - " + entry.getIssuer());
}
- TextDrawable drawable = profile.getDrawable();
+ TextDrawable drawable = TextDrawableHelper.generate(entry.getIssuer(), entry.getName());
_profileDrawable.setImageDrawable(drawable);
refreshCode();
}
+ public void setOnRefreshClickListener(View.OnClickListener listener) {
+ _buttonRefresh.setOnClickListener(listener);
+ }
+
public void startRefreshLoop() {
_refresher.start();
}
@@ -75,8 +90,8 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder {
_refresher.stop();
}
- private void refreshCode() {
- String otp = _profile.refreshCode();
+ public void refreshCode() {
+ String otp = _entry.getInfo().getOtp();
_profileCode.setText(otp.substring(0, otp.length() / 2) + " " + otp.substring(otp.length() / 2));
}
}
diff --git a/app/src/main/java/me/impy/aegis/ui/views/KeyProfileView.java b/app/src/main/java/me/impy/aegis/ui/views/EntryListView.java
similarity index 69%
rename from app/src/main/java/me/impy/aegis/ui/views/KeyProfileView.java
rename to app/src/main/java/me/impy/aegis/ui/views/EntryListView.java
index 30ac7ff7..3a652b2e 100644
--- a/app/src/main/java/me/impy/aegis/ui/views/KeyProfileView.java
+++ b/app/src/main/java/me/impy/aegis/ui/views/EntryListView.java
@@ -11,29 +11,29 @@ import android.view.View;
import android.view.ViewGroup;
import me.impy.aegis.R;
-import me.impy.aegis.crypto.KeyInfo;
import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.helpers.SimpleItemTouchHelperCallback;
-import me.impy.aegis.helpers.UIRefresher;
+import me.impy.aegis.helpers.UiRefresher;
+import me.impy.aegis.otp.TotpInfo;
-public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listener {
- private KeyProfileAdapter _adapter;
+public class EntryListView extends Fragment implements EntryAdapter.Listener {
+ private EntryAdapter _adapter;
private Listener _listener;
private PeriodProgressBar _progressBar;
private boolean _showProgress = false;
- private UIRefresher _refresher;
+ private UiRefresher _refresher;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- _adapter = new KeyProfileAdapter(this);
+ _adapter = new EntryAdapter(this);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View view = inflater.inflate(R.layout.fragment_keyprofile_view, container, false);
+ View view = inflater.inflate(R.layout.fragment_entry_list_view, container, false);
_progressBar = view.findViewById(R.id.progressBar);
int primaryColorId = getResources().getColor(R.color.colorPrimary);
@@ -48,7 +48,7 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
touchHelper.attachToRecyclerView(rvKeyProfiles);
rvKeyProfiles.setAdapter(_adapter);
- _refresher = new UIRefresher(new UIRefresher.Listener() {
+ _refresher = new UiRefresher(new UiRefresher.Listener() {
@Override
public void onRefresh() {
refresh();
@@ -56,7 +56,7 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
@Override
public long getMillisTillNextRefresh() {
- return KeyInfo.getMillisTillNextRotation(_adapter.getUniformPeriod());
+ return TotpInfo.getMillisTillNextRotation(_adapter.getUniformPeriod());
}
});
@@ -82,7 +82,7 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
_progressBar.setPeriod(_adapter.getUniformPeriod());
startRefreshLoop();
} else {
- _progressBar.setVisibility(View.INVISIBLE);
+ _progressBar.setVisibility(View.GONE);
stopRefreshLoop();
}
}
@@ -101,23 +101,28 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
}
@Override
- public void onKeyProfileClick(KeyProfile profile) {
- _listener.onEntryClick(profile);
+ public void onEntryClick(DatabaseEntry entry) {
+ _listener.onEntryClick(entry);
}
@Override
- public boolean onLongKeyProfileClick(KeyProfile profile) {
+ public boolean onLongEntryClick(DatabaseEntry entry) {
return false;
}
@Override
- public void onKeyProfileMove(KeyProfile profile1, KeyProfile profile2) {
- _listener.onEntryMove(profile1.getEntry(), profile2.getEntry());
+ public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) {
+ _listener.onEntryMove(entry1, entry2);
}
@Override
- public void onKeyProfileDrop(KeyProfile profile) {
- _listener.onEntryDrop(profile.getEntry());
+ public void onEntryDrop(DatabaseEntry entry) {
+ _listener.onEntryDrop(entry);
+ }
+
+ @Override
+ public void onEntryChange(DatabaseEntry entry) {
+ _listener.onEntryChange(entry);
}
public void setShowIssuer(boolean showIssuer) {
@@ -125,29 +130,30 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
_adapter.notifyDataSetChanged();
}
- public void addKey(KeyProfile profile) {
- _adapter.addKey(profile);
+ public void addEntry(DatabaseEntry entry) {
+ _adapter.addEntry(entry);
checkPeriodUniformity();
}
- public void removeKey(KeyProfile profile) {
- _adapter.removeKey(profile);
+ public void removeEntry(DatabaseEntry entry) {
+ _adapter.removeEntry(entry);
checkPeriodUniformity();
}
- public void clearKeys() {
- _adapter.clearKeys();
+ public void clearEntries() {
+ _adapter.clearEntries();
checkPeriodUniformity();
}
- public void replaceKey(KeyProfile profile) {
- _adapter.replaceKey(profile);
+ public void replaceEntry(DatabaseEntry entry) {
+ _adapter.replaceEntry(entry);
checkPeriodUniformity();
}
public interface Listener {
- void onEntryClick(KeyProfile profile);
+ void onEntryClick(DatabaseEntry entry);
void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2);
void onEntryDrop(DatabaseEntry entry);
+ void onEntryChange(DatabaseEntry entry);
}
}
diff --git a/app/src/main/java/me/impy/aegis/ui/views/KeyProfile.java b/app/src/main/java/me/impy/aegis/ui/views/KeyProfile.java
deleted file mode 100644
index addc14d6..00000000
--- a/app/src/main/java/me/impy/aegis/ui/views/KeyProfile.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package me.impy.aegis.ui.views;
-
-import com.amulyakhare.textdrawable.TextDrawable;
-
-import java.io.Serializable;
-import java.lang.reflect.UndeclaredThrowableException;
-
-import me.impy.aegis.crypto.otp.OTP;
-import me.impy.aegis.crypto.otp.OTPException;
-import me.impy.aegis.db.DatabaseEntry;
-import me.impy.aegis.helpers.TextDrawableHelper;
-
-public class KeyProfile implements Serializable {
- private String _code;
- private DatabaseEntry _entry;
-
- public KeyProfile() {
- this(new DatabaseEntry());
- }
-
- public KeyProfile(DatabaseEntry entry) {
- _entry = entry;
- }
-
- public DatabaseEntry getEntry() {
- return _entry;
- }
- public String getCode() {
- return _code;
- }
-
- public String refreshCode() {
- try {
- _code = OTP.generateOTP(_entry.getInfo());
- } catch (OTPException e) {
- throw new UndeclaredThrowableException(e);
- }
- return _code;
- }
-
- public TextDrawable getDrawable() {
- return TextDrawableHelper.generate(getEntry().getName());
- }
-}
diff --git a/app/src/main/java/me/impy/aegis/ui/views/KeyProfileAdapter.java b/app/src/main/java/me/impy/aegis/ui/views/KeyProfileAdapter.java
deleted file mode 100644
index cfa18e7d..00000000
--- a/app/src/main/java/me/impy/aegis/ui/views/KeyProfileAdapter.java
+++ /dev/null
@@ -1,155 +0,0 @@
-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 java.util.Collections;
-import java.util.UUID;
-
-import me.impy.aegis.R;
-import me.impy.aegis.helpers.ItemTouchHelperAdapter;
-
-public class KeyProfileAdapter extends RecyclerView.Adapter implements ItemTouchHelperAdapter {
- private ArrayList _keyProfiles;
- private static Listener _listener;
- private boolean _showIssuer;
-
- public KeyProfileAdapter(Listener listener) {
- _keyProfiles = new ArrayList<>();
- _listener = listener;
- }
-
- public void setShowIssuer(boolean showIssuer) {
- _showIssuer = showIssuer;
- }
-
- public void addKey(KeyProfile profile) {
- _keyProfiles.add(profile);
-
- int position = getItemCount() - 1;
- if (position == 0) {
- notifyDataSetChanged();
- } else {
- notifyItemInserted(position);
- }
- }
-
- public void removeKey(KeyProfile profile) {
- profile = getKeyByUUID(profile.getEntry().getUUID());
- int position = _keyProfiles.indexOf(profile);
- _keyProfiles.remove(position);
- notifyItemRemoved(position);
- }
-
- public void clearKeys() {
- _keyProfiles.clear();
- notifyDataSetChanged();
- }
-
- public void replaceKey(KeyProfile newProfile) {
- KeyProfile oldProfile = getKeyByUUID(newProfile.getEntry().getUUID());
- int position = _keyProfiles.indexOf(oldProfile);
- _keyProfiles.set(position, newProfile);
- notifyItemChanged(position);
- }
-
- private KeyProfile getKeyByUUID(UUID uuid) {
- for (KeyProfile profile : _keyProfiles) {
- if (profile.getEntry().getUUID().equals(uuid)) {
- return profile;
- }
- }
- throw new AssertionError("no key profile found with the same id");
- }
-
- @Override
- public void onItemDismiss(int position) {
-
- }
-
- @Override
- public void onItemDrop(int position) {
- _listener.onKeyProfileDrop(_keyProfiles.get(position));
- }
-
- @Override
- public void onItemMove(int firstPosition, int secondPosition) {
- // notify the database first
- _listener.onKeyProfileMove(_keyProfiles.get(firstPosition), _keyProfiles.get(secondPosition));
-
- // update our side of things
- Collections.swap(_keyProfiles, firstPosition, secondPosition);
- notifyItemMoved(firstPosition, secondPosition);
- }
-
- @Override
- public KeyProfileHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_keyprofile, parent, false);
- return new KeyProfileHolder(view);
- }
-
- @Override
- public void onViewRecycled(KeyProfileHolder holder) {
- holder.stopRefreshLoop();
- super.onViewRecycled(holder);
- }
-
- @Override
- public void onBindViewHolder(final KeyProfileHolder holder, int position) {
- boolean uniform = isPeriodUniform();
- final KeyProfile profile = _keyProfiles.get(position);
- holder.setData(profile, _showIssuer, !uniform);
- if (!uniform) {
- holder.startRefreshLoop();
- }
-
- holder.itemView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- int position = holder.getAdapterPosition();
- _listener.onKeyProfileClick(_keyProfiles.get(position));
- }
- });
- holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
- @Override
- public boolean onLongClick(View v) {
- int position = holder.getAdapterPosition();
- return _listener.onLongKeyProfileClick(_keyProfiles.get(position));
- }
- });
- }
-
- public int getUniformPeriod() {
- if (_keyProfiles.isEmpty()) {
- return -1;
- }
-
- int period = _keyProfiles.get(0).getEntry().getInfo().getPeriod();
- for (KeyProfile profile : _keyProfiles) {
- if (period != profile.getEntry().getInfo().getPeriod()) {
- return -1;
- }
- }
-
- return period;
- }
-
- public boolean isPeriodUniform() {
- return getUniformPeriod() != -1;
- }
-
- @Override
- public int getItemCount() {
- return _keyProfiles.size();
- }
-
- public interface Listener {
- void onKeyProfileClick(KeyProfile profile);
- boolean onLongKeyProfileClick(KeyProfile profile);
- void onKeyProfileMove(KeyProfile profile1, KeyProfile profile2);
- void onKeyProfileDrop(KeyProfile profile);
- }
-}
diff --git a/app/src/main/java/me/impy/aegis/ui/views/PeriodProgressBar.java b/app/src/main/java/me/impy/aegis/ui/views/PeriodProgressBar.java
index c4152c2d..3552ba66 100644
--- a/app/src/main/java/me/impy/aegis/ui/views/PeriodProgressBar.java
+++ b/app/src/main/java/me/impy/aegis/ui/views/PeriodProgressBar.java
@@ -8,7 +8,7 @@ import android.util.AttributeSet;
import android.view.animation.LinearInterpolator;
import android.widget.ProgressBar;
-import me.impy.aegis.crypto.KeyInfo;
+import me.impy.aegis.otp.TotpInfo;
public class PeriodProgressBar extends ProgressBar {
private int _period;
@@ -40,7 +40,7 @@ public class PeriodProgressBar extends ProgressBar {
setProgress(maxProgress);
// calculate the progress the bar should start at
- long millisTillRotation = KeyInfo.getMillisTillNextRotation(_period);
+ long millisTillRotation = TotpInfo.getMillisTillNextRotation(_period);
long period = _period * maxProgress;
int currentProgress = maxProgress - (int) ((((double) period - millisTillRotation) / period) * maxProgress);
diff --git a/app/src/main/res/drawable/ic_counter_black_24dp.xml b/app/src/main/res/drawable/ic_counter_black_24dp.xml
new file mode 100644
index 00000000..740d85d9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_counter_black_24dp.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_refresh_black_24dp.xml b/app/src/main/res/drawable/ic_refresh_black_24dp.xml
new file mode 100644
index 00000000..5e434f43
--- /dev/null
+++ b/app/src/main/res/drawable/ic_refresh_black_24dp.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_edit_profile.xml b/app/src/main/res/layout/activity_edit_entry.xml
similarity index 86%
rename from app/src/main/res/layout/activity_edit_profile.xml
rename to app/src/main/res/layout/activity_edit_entry.xml
index 55078e00..54211460 100644
--- a/app/src/main/res/layout/activity_edit_profile.xml
+++ b/app/src/main/res/layout/activity_edit_entry.xml
@@ -168,6 +168,8 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 3be9deed..e3e901c8 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -9,7 +9,7 @@
tools:context="me.impy.aegis.ui.MainActivity">
+
+ tools:context="me.impy.aegis.ui.EditEntryActivity">