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