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 6b336dca..5bbc9260 100644 --- a/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java +++ b/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java @@ -27,7 +27,6 @@ import org.spongycastle.crypto.generators.SCrypt; public class CryptoUtils { public static final String CRYPTO_HASH = "SHA-256"; - public static final byte CRYPTO_HASH_SIZE = 32; public static final String CRYPTO_CIPHER_RAW = "AES/ECB/NoPadding"; public static final byte CRYPTO_KEY_SIZE = 32; @@ -35,31 +34,36 @@ public class CryptoUtils { 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 byte CRYPTO_SALT_SIZE = 32; public static final int CRYPTO_SCRYPT_N = 1 << 15; public static final int CRYPTO_SCRYPT_r = 8; public static final int CRYPTO_SCRYPT_p = 1; - public static SecretKey deriveKey(char[] password, byte[] salt, int n, int r, int p) throws NoSuchAlgorithmException, InvalidKeySpecException { + public static SecretKey deriveKey(char[] password, byte[] salt, int n, int r, int p) + throws NoSuchAlgorithmException, InvalidKeySpecException { byte[] bytes = toBytes(password); byte[] keyBytes = SCrypt.generate(bytes, salt, n, r, p, CRYPTO_KEY_SIZE); return new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES"); } - public static Cipher createCipher(SecretKey key, int opmode) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException { + public static Cipher createCipher(SecretKey key, int opmode) + throws NoSuchPaddingException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException, InvalidKeyException { byte[] nonce = generateNonce(); return createCipher(key, opmode, nonce); } - public static Cipher createCipher(SecretKey key, int opmode, byte[] nonce) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException { + public 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); return cipher; } - public static CryptResult encrypt(byte[] data, Cipher cipher) throws BadPaddingException, IllegalBlockSizeException { + public static CryptResult encrypt(byte[] data, Cipher cipher) + 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); @@ -74,7 +78,8 @@ public class CryptoUtils { }}; } - public static CryptResult decrypt(byte[] encrypted, Cipher cipher, CryptParameters params) throws IOException, BadPaddingException, IllegalBlockSizeException { + public static CryptResult decrypt(byte[] encrypted, Cipher cipher, CryptParameters params) + throws IOException, BadPaddingException, IllegalBlockSizeException { // append the tag to the ciphertext ByteArrayOutputStream stream = new ByteArrayOutputStream(); stream.write(encrypted); @@ -94,7 +99,7 @@ public class CryptoUtils { try { hash = MessageDigest.getInstance(CRYPTO_HASH); } catch (NoSuchAlgorithmException e) { - throw new UndeclaredThrowableException(e); + throw new AssertionError(e); } byte[] bytes = key.getEncoded(); @@ -102,10 +107,14 @@ public class CryptoUtils { return hash.digest(); } - public static SecretKey generateKey() throws NoSuchAlgorithmException { - KeyGenerator generator = KeyGenerator.getInstance("AES"); - generator.init(CRYPTO_KEY_SIZE * 8); - return generator.generateKey(); + public static SecretKey generateKey() { + try { + KeyGenerator generator = KeyGenerator.getInstance("AES"); + generator.init(CRYPTO_KEY_SIZE * 8); + return generator.generateKey(); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } } public static byte[] generateSalt() { 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 dbba7298..c7f3afe5 100644 --- a/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandle.java +++ b/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandle.java @@ -7,10 +7,12 @@ import android.security.keystore.KeyPermanentlyInvalidatedException; import android.security.keystore.KeyProperties; import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; @@ -40,27 +42,40 @@ public class KeyStoreHandle { } } - public SecretKey generateKey(String id) throws Exception { - if (isSupported()) { + public SecretKey generateKey(String id) throws KeyStoreHandleException { + if (!isSupported()) { + throw new KeyStoreHandleException("Symmetric KeyStore keys are not supported in this version of Android"); + } + + try { 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) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) - .setUserAuthenticationRequired(true) - .setRandomizedEncryptionRequired(false) - .setKeySize(CryptoUtils.CRYPTO_KEY_SIZE * 8) - .build()); + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_ECB) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setUserAuthenticationRequired(true) + .setRandomizedEncryptionRequired(false) + .setKeySize(CryptoUtils.CRYPTO_KEY_SIZE * 8) + .build()); return generator.generateKey(); - } else { - throw new Exception("Symmetric KeyStore keys are not supported in this version of Android"); + } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) { + throw new KeyStoreHandleException(e); } } - public SecretKey getKey(String id) - throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException { - SecretKey key = (SecretKey) _keyStore.getKey(id, null); + public SecretKey getKey(String id) throws KeyStoreHandleException { + SecretKey key; + + try { + key = (SecretKey) _keyStore.getKey(id, null); + } catch (UnrecoverableKeyException e) { + return null; + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } catch (KeyStoreException e) { + throw new KeyStoreHandleException(e); + } // try to initialize a dummy cipher // and see if KeyPermanentlyInvalidatedException is thrown @@ -71,7 +86,7 @@ public class KeyStoreHandle { cipher.init(Cipher.ENCRYPT_MODE, key); } catch (KeyPermanentlyInvalidatedException e) { return null; - } catch (NoSuchPaddingException | InvalidKeyException e) { + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) { throw new RuntimeException(e); } } @@ -79,8 +94,12 @@ public class KeyStoreHandle { return key; } - public void deleteKey(String id) throws KeyStoreException { - _keyStore.deleteEntry(id); + public void deleteKey(String id) throws KeyStoreHandleException { + try { + _keyStore.deleteEntry(id); + } catch (KeyStoreException e) { + throw new KeyStoreHandleException(e); + } } public static boolean isSupported() { diff --git a/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandleException.java b/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandleException.java index 424f4533..2a60e629 100644 --- a/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandleException.java +++ b/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandleException.java @@ -4,4 +4,8 @@ public class KeyStoreHandleException extends Exception { public KeyStoreHandleException(Throwable cause) { super(cause); } + + public KeyStoreHandleException(String message) { + super(message); + } } 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 cfbb1cbe..29c859b3 100644 --- a/app/src/main/java/me/impy/aegis/crypto/MasterKey.java +++ b/app/src/main/java/me/impy/aegis/crypto/MasterKey.java @@ -22,22 +22,36 @@ public class MasterKey implements Serializable { _key = key; } - public static MasterKey generate() throws NoSuchAlgorithmException { + public static MasterKey generate() { return new MasterKey(CryptoUtils.generateKey()); } - public CryptResult encrypt(byte[] bytes) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeyException, - NoSuchPaddingException, BadPaddingException, IllegalBlockSizeException { - Cipher cipher = CryptoUtils.createCipher(_key, Cipher.ENCRYPT_MODE); - return CryptoUtils.encrypt(bytes, cipher); + public CryptResult encrypt(byte[] bytes) throws MasterKeyException { + try { + Cipher cipher = CryptoUtils.createCipher(_key, Cipher.ENCRYPT_MODE); + return CryptoUtils.encrypt(bytes, cipher); + } catch (NoSuchPaddingException + | NoSuchAlgorithmException + | InvalidAlgorithmParameterException + | InvalidKeyException | BadPaddingException + | IllegalBlockSizeException e) { + throw new MasterKeyException(e); + } } - public CryptResult decrypt(byte[] bytes, CryptParameters params) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeyException, - NoSuchPaddingException, BadPaddingException, IllegalBlockSizeException, IOException { - Cipher cipher = CryptoUtils.createCipher(_key, Cipher.DECRYPT_MODE, params.Nonce); - return CryptoUtils.decrypt(bytes, cipher, params); + public CryptResult decrypt(byte[] bytes, CryptParameters params) throws MasterKeyException { + try { + Cipher cipher = CryptoUtils.createCipher(_key, Cipher.DECRYPT_MODE, params.Nonce); + return CryptoUtils.decrypt(bytes, cipher, params); + } catch (NoSuchPaddingException + | NoSuchAlgorithmException + | InvalidAlgorithmParameterException + | InvalidKeyException + | BadPaddingException + | IOException + | IllegalBlockSizeException e) { + throw new MasterKeyException(e); + } } public byte[] getHash() { diff --git a/app/src/main/java/me/impy/aegis/crypto/MasterKeyException.java b/app/src/main/java/me/impy/aegis/crypto/MasterKeyException.java new file mode 100644 index 00000000..7f219f4a --- /dev/null +++ b/app/src/main/java/me/impy/aegis/crypto/MasterKeyException.java @@ -0,0 +1,7 @@ +package me.impy.aegis.crypto; + +public class MasterKeyException extends Exception { + public MasterKeyException(Throwable cause) { + super(cause); + } +} 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 index d9615d24..72bbaba6 100644 --- a/app/src/main/java/me/impy/aegis/crypto/otp/OTP.java +++ b/app/src/main/java/me/impy/aegis/crypto/otp/OTP.java @@ -9,19 +9,23 @@ public class OTP { private OTP() { } - public static String generateOTP(KeyInfo info) throws InvalidKeyException, NoSuchAlgorithmException { + public static String generateOTP(KeyInfo info) throws OTPException { String otp; - 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(); + 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 new file mode 100644 index 00000000..8866b7a1 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/crypto/otp/OTPException.java @@ -0,0 +1,7 @@ +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 f68439fc..abc30308 100644 --- a/app/src/main/java/me/impy/aegis/db/Database.java +++ b/app/src/main/java/me/impy/aegis/db/Database.java @@ -1,6 +1,7 @@ package me.impy.aegis.db; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; @@ -8,35 +9,45 @@ import java.util.Collections; import java.util.List; import java.util.UUID; +import me.impy.aegis.crypto.KeyInfoException; + public class Database { private static final int VERSION = 1; private List _entries = new ArrayList<>(); - public JSONObject serialize() throws Exception { - JSONArray array = new JSONArray(); - for (DatabaseEntry e : _entries) { - array.put(e.serialize()); - } + public JSONObject serialize() throws DatabaseException { + try { + JSONArray array = new JSONArray(); + for (DatabaseEntry e : _entries) { + array.put(e.serialize()); + } - JSONObject obj = new JSONObject(); - obj.put("version", VERSION); - obj.put("entries", array); - return obj; + JSONObject obj = new JSONObject(); + obj.put("version", VERSION); + obj.put("entries", array); + return obj; + } catch (JSONException e) { + throw new DatabaseException(e); + } } - public void deserialize(JSONObject obj) throws Exception { + public void deserialize(JSONObject obj) throws DatabaseException { // TODO: support different VERSION deserialization providers - int ver = obj.getInt("version"); - if (ver != VERSION) { - throw new Exception("Unsupported version"); - } + try { + int ver = obj.getInt("version"); + if (ver != VERSION) { + throw new DatabaseException("Unsupported version"); + } - JSONArray array = obj.getJSONArray("entries"); - for (int i = 0; i < array.length(); i++) { - DatabaseEntry entry = new DatabaseEntry(null); - entry.deserialize(array.getJSONObject(i)); - addKey(entry); + JSONArray array = obj.getJSONArray("entries"); + for (int i = 0; i < array.length(); i++) { + DatabaseEntry entry = new DatabaseEntry(null); + entry.deserialize(array.getJSONObject(i)); + addKey(entry); + } + } catch (JSONException | KeyInfoException e) { + throw new DatabaseException(e); } } diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseException.java b/app/src/main/java/me/impy/aegis/db/DatabaseException.java new file mode 100644 index 00000000..9fb33853 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/db/DatabaseException.java @@ -0,0 +1,11 @@ +package me.impy.aegis.db; + +public class DatabaseException extends Exception { + public DatabaseException(Throwable cause) { + super(cause); + } + + public DatabaseException(String message) { + super(message); + } +} 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 24ce5a97..86a009b4 100644 --- a/app/src/main/java/me/impy/aegis/db/DatabaseFile.java +++ b/app/src/main/java/me/impy/aegis/db/DatabaseFile.java @@ -5,21 +5,16 @@ import android.util.Base64; import org.json.JSONException; import org.json.JSONObject; -import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; import me.impy.aegis.crypto.CryptParameters; import me.impy.aegis.crypto.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.encoding.Hex; +import me.impy.aegis.encoding.HexException; public class DatabaseFile { public static final byte VERSION = 1; @@ -32,53 +27,61 @@ public class DatabaseFile { _slots = new SlotCollection(); } - public byte[] serialize() throws JSONException, UnsupportedEncodingException { - JSONObject cryptObj = null; - if (_cryptParameters != null) { - cryptObj = new JSONObject(); - cryptObj.put("nonce", Hex.toString(_cryptParameters.Nonce)); - cryptObj.put("tag", Hex.toString(_cryptParameters.Tag)); + public byte[] serialize() throws DatabaseFileException { + try { + JSONObject cryptObj = null; + if (_cryptParameters != null) { + cryptObj = new JSONObject(); + cryptObj.put("nonce", Hex.toString(_cryptParameters.Nonce)); + cryptObj.put("tag", Hex.toString(_cryptParameters.Tag)); + } + + // don't write the crypt parameters if the content is not encrypted + boolean plain = _content instanceof JSONObject || _slots.isEmpty() || cryptObj == null; + JSONObject headerObj = new JSONObject(); + headerObj.put("slots", plain ? JSONObject.NULL : SlotCollection.serialize(_slots)); + headerObj.put("params", plain ? JSONObject.NULL : cryptObj); + + JSONObject obj = new JSONObject(); + obj.put("version", VERSION); + obj.put("header", headerObj); + obj.put("db", _content); + + String string = obj.toString(4); + return string.getBytes("UTF-8"); + } catch (SlotCollectionException | UnsupportedEncodingException | JSONException e) { + throw new DatabaseFileException(e); } - - // don't write the crypt parameters if the content is not encrypted - boolean plain = _content instanceof JSONObject || _slots.isEmpty() || cryptObj == null; - JSONObject headerObj = new JSONObject(); - headerObj.put("slots", plain ? JSONObject.NULL : SlotCollection.serialize(_slots)); - headerObj.put("params", plain ? JSONObject.NULL : cryptObj); - - JSONObject obj = new JSONObject(); - obj.put("version", VERSION); - obj.put("header", headerObj); - obj.put("db", _content); - - String string = obj.toString(4); - return string.getBytes("UTF-8"); } - public void deserialize(byte[] data) throws Exception { - JSONObject obj = new JSONObject(new String(data, "UTF-8")); - JSONObject headerObj = obj.getJSONObject("header"); - if (obj.getInt("version") > VERSION) { - throw new Exception("unsupported version"); - } + public void deserialize(byte[] data) throws DatabaseFileException { + try { + JSONObject obj = new JSONObject(new String(data, "UTF-8")); + JSONObject headerObj = obj.getJSONObject("header"); + if (obj.getInt("version") > VERSION) { + throw new DatabaseFileException("unsupported version"); + } - JSONObject slotObj = headerObj.optJSONObject("slots"); - if (slotObj != null) { - _slots = SlotCollection.deserialize(slotObj); - } + JSONObject slotObj = headerObj.optJSONObject("slots"); + if (slotObj != null) { + _slots = SlotCollection.deserialize(slotObj); + } - JSONObject cryptObj = headerObj.optJSONObject("params"); - if (cryptObj != null) { - _cryptParameters = new CryptParameters() {{ - Nonce = Hex.toBytes(cryptObj.getString("nonce")); - Tag = Hex.toBytes(cryptObj.getString("tag")); - }}; - } + JSONObject cryptObj = headerObj.optJSONObject("params"); + if (cryptObj != null) { + _cryptParameters = new CryptParameters() {{ + Nonce = Hex.toBytes(cryptObj.getString("nonce")); + Tag = Hex.toBytes(cryptObj.getString("tag")); + }}; + } - if (cryptObj == null || slotObj == null) { - _content = obj.getJSONObject("db"); - } else { - _content = obj.getString("db"); + if (cryptObj == null || slotObj == null) { + _content = obj.getJSONObject("db"); + } else { + _content = obj.getString("db"); + } + } catch (SlotCollectionException | UnsupportedEncodingException | JSONException | HexException e) { + throw new DatabaseFileException(e); } } @@ -90,13 +93,14 @@ public class DatabaseFile { return (JSONObject) _content; } - public JSONObject getContent(MasterKey key) - throws NoSuchPaddingException, InvalidKeyException, - NoSuchAlgorithmException, IllegalBlockSizeException, - BadPaddingException, InvalidAlgorithmParameterException, IOException, JSONException { - byte[] bytes = Base64.decode((String) _content, Base64.NO_WRAP); - CryptResult result = key.decrypt(bytes, _cryptParameters); - return new JSONObject(new String(result.Data, "UTF-8")); + public JSONObject getContent(MasterKey key) throws DatabaseFileException { + try { + byte[] bytes = Base64.decode((String) _content, Base64.NO_WRAP); + CryptResult result = key.decrypt(bytes, _cryptParameters); + return new JSONObject(new String(result.Data, "UTF-8")); + } catch (MasterKeyException | JSONException | UnsupportedEncodingException e) { + throw new DatabaseFileException(e); + } } public void setContent(JSONObject dbObj) { @@ -104,16 +108,17 @@ public class DatabaseFile { _cryptParameters = null; } - public void setContent(JSONObject dbObj, MasterKey key) - throws JSONException, UnsupportedEncodingException, - NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, - IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { - String string = dbObj.toString(4); - byte[] dbBytes = string.getBytes("UTF-8"); + public void setContent(JSONObject dbObj, MasterKey key) throws DatabaseFileException { + try { + String string = dbObj.toString(4); + byte[] dbBytes = string.getBytes("UTF-8"); - CryptResult result = key.encrypt(dbBytes); - _content = new String(Base64.encode(result.Data, Base64.NO_WRAP), "UTF-8"); - _cryptParameters = result.Parameters; + CryptResult result = key.encrypt(dbBytes); + _content = new String(Base64.encode(result.Data, Base64.NO_WRAP), "UTF-8"); + _cryptParameters = result.Parameters; + } catch (MasterKeyException | UnsupportedEncodingException | JSONException e) { + throw new DatabaseFileException(e); + } } public SlotCollection getSlots() { diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseFileException.java b/app/src/main/java/me/impy/aegis/db/DatabaseFileException.java new file mode 100644 index 00000000..818d003d --- /dev/null +++ b/app/src/main/java/me/impy/aegis/db/DatabaseFileException.java @@ -0,0 +1,11 @@ +package me.impy.aegis.db; + +public class DatabaseFileException extends Exception { + public DatabaseFileException(Throwable cause) { + super(cause); + } + + public DatabaseFileException(String message) { + super(message); + } +} 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 1cadb9d8..28e8ac1c 100644 --- a/app/src/main/java/me/impy/aegis/db/DatabaseManager.java +++ b/app/src/main/java/me/impy/aegis/db/DatabaseManager.java @@ -3,7 +3,6 @@ package me.impy.aegis.db; import android.content.Context; import android.os.Environment; -import org.json.JSONException; import org.json.JSONObject; import java.io.DataInputStream; @@ -35,137 +34,159 @@ public class DatabaseManager { return file.exists() && file.isFile(); } - public void load() throws Exception { + public void load() throws DatabaseManagerException { assertState(true, false); - byte[] fileBytes; - FileInputStream file = null; - try { - file = _context.openFileInput(FILENAME); - fileBytes = new byte[(int) file.getChannel().size()]; - DataInputStream stream = new DataInputStream(file); - stream.readFully(fileBytes); - stream.close(); - } finally { - // always close the file stream - // there is no need to close the DataInputStream - if (file != null) { - file.close(); + byte[] fileBytes; + FileInputStream file = null; + + try { + file = _context.openFileInput(FILENAME); + fileBytes = new byte[(int) file.getChannel().size()]; + DataInputStream stream = new DataInputStream(file); + stream.readFully(fileBytes); + stream.close(); + } finally { + // always close the file stream + // there is no need to close the DataInputStream + if (file != null) { + file.close(); + } } - } - _file = new DatabaseFile(); - _file.deserialize(fileBytes); + _file = new DatabaseFile(); + _file.deserialize(fileBytes); - if (!_file.isEncrypted()) { - JSONObject obj = _file.getContent(); - _db = new Database(); - _db.deserialize(obj); + if (!_file.isEncrypted()) { + JSONObject obj = _file.getContent(); + _db = new Database(); + _db.deserialize(obj); + } + } catch (IOException | DatabaseFileException | DatabaseException e) { + throw new DatabaseManagerException(e); } } - public void lock() throws Exception { + public void lock() { assertState(false, true); // TODO: properly clear everything _key = null; _db = null; } - public void unlock(MasterKey key) throws Exception { + public void unlock(MasterKey key) throws DatabaseManagerException { assertState(true, true); - JSONObject obj = _file.getContent(key); - _db = new Database(); - _db.deserialize(obj); - _key = key; - } - public static void save(Context context, DatabaseFile file) throws IOException, JSONException { - byte[] bytes = file.serialize(); - - FileOutputStream stream = null; try { - stream = context.openFileOutput(FILENAME, Context.MODE_PRIVATE); - stream.write(bytes); - } finally { - // always close the file stream - if (stream != null) { - stream.close(); + JSONObject obj = _file.getContent(key); + _db = new Database(); + _db.deserialize(obj); + _key = key; + } catch (DatabaseFileException | DatabaseException e) { + throw new DatabaseManagerException(e); + } + } + + public static void save(Context context, DatabaseFile file) throws DatabaseManagerException { + try { + byte[] bytes = file.serialize(); + + FileOutputStream stream = null; + try { + stream = context.openFileOutput(FILENAME, Context.MODE_PRIVATE); + stream.write(bytes); + } finally { + // always close the file stream + if (stream != null) { + stream.close(); + } } + } catch (IOException | DatabaseFileException e) { + throw new DatabaseManagerException(e); } } - public void save() throws Exception { - assertState(false, true); - JSONObject obj = _db.serialize(); - if (_file.isEncrypted()) { - _file.setContent(obj, _key); - } else { - _file.setContent(obj); - } - save(_context, _file); - } - - public String export(boolean encrypt) throws Exception { + public void save() throws DatabaseManagerException { assertState(false, true); - DatabaseFile dbFile = new DatabaseFile(); - dbFile.setSlots(_file.getSlots()); - if (encrypt && getFile().isEncrypted()) { - dbFile.setContent(_db.serialize(), _key); - } else { - dbFile.setContent(_db.serialize()); - } - - File file; - FileOutputStream stream = null; try { - String dirName = !BuildConfig.DEBUG ? "Aegis" : "AegisDebug"; - File dir = new File(Environment.getExternalStorageDirectory(), dirName); - if (!dir.exists() && !dir.mkdirs()) { - throw new IOException("error creating external storage directory"); - } - - byte[] bytes = dbFile.serialize(); - file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN); - stream = new FileOutputStream(file); - stream.write(bytes); - } finally { - // always close the file stream - if (stream != null) { - stream.close(); + JSONObject obj = _db.serialize(); + if (_file.isEncrypted()) { + _file.setContent(obj, _key); + } else { + _file.setContent(obj); } + save(_context, _file); + } catch (DatabaseException | DatabaseFileException e) { + throw new DatabaseManagerException(e); } - - return file.getAbsolutePath(); } - public void addKey(DatabaseEntry entry) throws Exception { + public String export(boolean encrypt) throws DatabaseManagerException { + assertState(false, true); + + try { + DatabaseFile dbFile = new DatabaseFile(); + dbFile.setSlots(_file.getSlots()); + if (encrypt && getFile().isEncrypted()) { + dbFile.setContent(_db.serialize(), _key); + } else { + dbFile.setContent(_db.serialize()); + } + + File file; + FileOutputStream stream = null; + try { + String dirName = !BuildConfig.DEBUG ? "Aegis" : "AegisDebug"; + File dir = new File(Environment.getExternalStorageDirectory(), dirName); + if (!dir.exists() && !dir.mkdirs()) { + throw new IOException("error creating external storage directory"); + } + + byte[] bytes = dbFile.serialize(); + file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN); + stream = new FileOutputStream(file); + stream.write(bytes); + } finally { + // always close the file stream + if (stream != null) { + stream.close(); + } + } + + return file.getAbsolutePath(); + } catch (DatabaseException | IOException | DatabaseFileException e) { + throw new DatabaseManagerException(e); + } + } + + public void addKey(DatabaseEntry entry) { assertState(false, true); _db.addKey(entry); } - public void removeKey(DatabaseEntry entry) throws Exception { + public void removeKey(DatabaseEntry entry) { assertState(false, true); _db.removeKey(entry); } - public void replaceKey(DatabaseEntry entry) throws Exception { + public void replaceKey(DatabaseEntry entry) { assertState(false, true); _db.replaceKey(entry); } - public void swapKeys(DatabaseEntry entry1, DatabaseEntry entry2) throws Exception { + public void swapKeys(DatabaseEntry entry1, DatabaseEntry entry2) { assertState(false, true); _db.swapKeys(entry1, entry2); } - public List getKeys() throws Exception { + public List getKeys() { assertState(false, true); return _db.getKeys(); } - public MasterKey getMasterKey() throws Exception { + public MasterKey getMasterKey() { assertState(false, true); return _key; } @@ -182,17 +203,17 @@ public class DatabaseManager { return _db == null; } - private void assertState(boolean locked, boolean loaded) throws Exception { + private void assertState(boolean locked, boolean loaded) { if (isLoaded() && !loaded) { - throw new Exception("database file has not been loaded yet"); + throw new AssertionError("database file has not been loaded yet"); } else if (!isLoaded() && loaded) { - throw new Exception("database file has is already been loaded"); + throw new AssertionError("database file has is already been loaded"); } if (isLocked() && !locked) { - throw new Exception("database file has not been unlocked yet"); + throw new AssertionError("database file has not been unlocked yet"); } else if (!isLocked() && locked) { - throw new Exception("database file has is already been unlocked"); + throw new AssertionError("database file has is already been unlocked"); } } } diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseManagerException.java b/app/src/main/java/me/impy/aegis/db/DatabaseManagerException.java new file mode 100644 index 00000000..ca89a0dc --- /dev/null +++ b/app/src/main/java/me/impy/aegis/db/DatabaseManagerException.java @@ -0,0 +1,7 @@ +package me.impy.aegis.db; + +public class DatabaseManagerException extends Exception { + public DatabaseManagerException(Throwable cause) { + super(cause); + } +} 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 6835292f..5661ec72 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 @@ -10,6 +10,7 @@ import javax.crypto.SecretKey; import me.impy.aegis.crypto.CryptoUtils; import me.impy.aegis.encoding.Hex; +import me.impy.aegis.encoding.HexException; public class PasswordSlot extends RawSlot { private int _n; @@ -22,35 +23,51 @@ public class PasswordSlot extends RawSlot { } @Override - public JSONObject serialize() throws JSONException { - JSONObject obj = super.serialize(); - obj.put("n", _n); - obj.put("r", _r); - obj.put("p", _p); - obj.put("salt", Hex.toString(_salt)); - return obj; + public JSONObject serialize() throws SlotException { + try { + JSONObject obj = super.serialize(); + obj.put("n", _n); + obj.put("r", _r); + obj.put("p", _p); + obj.put("salt", Hex.toString(_salt)); + return obj; + } catch (JSONException e) { + throw new SlotException(e); + } } @Override - public void deserialize(JSONObject obj) throws Exception { - super.deserialize(obj); - _n = obj.getInt("n"); - _r = obj.getInt("r"); - _p = obj.getInt("p"); - _salt = Hex.toBytes(obj.getString("salt")); + public void deserialize(JSONObject obj) throws SlotException { + try { + super.deserialize(obj); + _n = obj.getInt("n"); + _r = obj.getInt("r"); + _p = obj.getInt("p"); + _salt = Hex.toBytes(obj.getString("salt")); + } catch (JSONException | HexException e) { + throw new SlotException(e); + } } - public SecretKey deriveKey(char[] password, byte[] salt, int n, int r, int p) throws InvalidKeySpecException, NoSuchAlgorithmException { - SecretKey key = CryptoUtils.deriveKey(password, salt, n, r, p); - _n = n; - _r = r; - _p = p; - _salt = salt; - return key; + public SecretKey deriveKey(char[] password, byte[] salt, int n, int r, int p) throws SlotException { + try { + SecretKey key = CryptoUtils.deriveKey(password, salt, n, r, p); + _n = n; + _r = r; + _p = p; + _salt = salt; + return key; + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new SlotException(e); + } } - public SecretKey deriveKey(char[] password) throws InvalidKeySpecException, NoSuchAlgorithmException { - return CryptoUtils.deriveKey(password, _salt, _n, _r, _p); + public SecretKey deriveKey(char[] password) throws SlotException { + try { + return CryptoUtils.deriveKey(password, _salt, _n, _r, _p); + } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { + throw new SlotException(e); + } } @Override 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 ab054dba..5b178434 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 @@ -20,6 +20,7 @@ import javax.crypto.spec.SecretKeySpec; import me.impy.aegis.crypto.CryptoUtils; import me.impy.aegis.crypto.MasterKey; import me.impy.aegis.encoding.Hex; +import me.impy.aegis.encoding.HexException; public abstract class Slot implements Serializable { public final static byte TYPE_RAW = 0x00; @@ -34,45 +35,65 @@ public abstract class Slot implements Serializable { } // getKey decrypts the encrypted master key in this slot with the given key and returns it. - public SecretKey getKey(Cipher cipher) throws BadPaddingException, IllegalBlockSizeException { - byte[] decryptedKeyBytes = cipher.doFinal(_encryptedMasterKey); - return new SecretKeySpec(decryptedKeyBytes, CryptoUtils.CRYPTO_CIPHER_AEAD); + public SecretKey getKey(Cipher cipher) throws SlotException { + try { + byte[] decryptedKeyBytes = cipher.doFinal(_encryptedMasterKey); + return new SecretKeySpec(decryptedKeyBytes, CryptoUtils.CRYPTO_CIPHER_AEAD); + } catch (BadPaddingException | IllegalBlockSizeException e) { + throw new SlotException(e); + } } // setKey encrypts the given master key with the given key and stores the result in this slot. - public void setKey(MasterKey masterKey, Cipher cipher) throws BadPaddingException, IllegalBlockSizeException { - byte[] masterKeyBytes = masterKey.getBytes(); - _encryptedMasterKey = cipher.doFinal(masterKeyBytes); + public void setKey(MasterKey masterKey, Cipher cipher) throws SlotException { + try { + byte[] masterKeyBytes = masterKey.getBytes(); + _encryptedMasterKey = cipher.doFinal(masterKeyBytes); + } 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 NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException { - Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_CIPHER_RAW); - cipher.init(mode, key); - return cipher; + public static Cipher createCipher(SecretKey key, int mode) throws SlotException { + try { + Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_CIPHER_RAW); + cipher.init(mode, key); + return cipher; + } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) { + throw new SlotException(e); + } } - public JSONObject serialize() throws JSONException { - JSONObject obj = new JSONObject(); - obj.put("type", getType()); - obj.put("uuid", _uuid.toString()); - obj.put("key", Hex.toString(_encryptedMasterKey)); - return obj; + public JSONObject serialize() throws SlotException { + try { + JSONObject obj = new JSONObject(); + obj.put("type", getType()); + obj.put("uuid", _uuid.toString()); + obj.put("key", Hex.toString(_encryptedMasterKey)); + return obj; + } catch (JSONException e) { + throw new SlotException(e); + } } - public void deserialize(JSONObject obj) throws Exception { - if (obj.getInt("type") != getType()) { - throw new Exception("slot type mismatch"); + public void deserialize(JSONObject obj) throws SlotException { + try { + 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")); + } + _encryptedMasterKey = Hex.toBytes(obj.getString("key")); + } catch (JSONException | HexException e) { + throw new SlotException(e); } - // if there is no uuid, generate a new one - if (!obj.has("uuid")) { - _uuid = UUID.randomUUID(); - } else { - _uuid = UUID.fromString(obj.getString("uuid")); - } - _encryptedMasterKey = Hex.toBytes(obj.getString("key")); } public abstract byte getType(); diff --git a/app/src/main/java/me/impy/aegis/db/slots/SlotCollection.java b/app/src/main/java/me/impy/aegis/db/slots/SlotCollection.java index b6a16030..1790ab61 100644 --- a/app/src/main/java/me/impy/aegis/db/slots/SlotCollection.java +++ b/app/src/main/java/me/impy/aegis/db/slots/SlotCollection.java @@ -10,56 +10,63 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; -import javax.crypto.BadPaddingException; import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; import me.impy.aegis.crypto.MasterKey; import me.impy.aegis.encoding.Hex; +import me.impy.aegis.encoding.HexException; public class SlotCollection implements Iterable, Serializable { private List _slots = new ArrayList<>(); private byte[] _masterHash; - public static JSONObject serialize(SlotCollection slots) throws JSONException { - JSONObject obj = new JSONObject(); - obj.put("hash", Hex.toString(slots.getMasterHash())); + public static JSONObject serialize(SlotCollection slots) throws SlotCollectionException { + try { + JSONObject obj = new JSONObject(); + obj.put("hash", Hex.toString(slots.getMasterHash())); - JSONArray entries = new JSONArray(); - for (Slot slot : slots) { - entries.put(slot.serialize()); - } - - obj.put("entries", entries); - return obj; - } - - public static SlotCollection deserialize(JSONObject obj) throws Exception { - SlotCollection slots = new SlotCollection(); - - byte[] masterHash = Hex.toBytes(obj.getString("hash")); - slots.setMasterHash(masterHash); - - JSONArray entries = obj.getJSONArray("entries"); - for (int i = 0; i < entries.length(); i++) { - Slot slot; - JSONObject slotObj = entries.getJSONObject(i); - - switch (slotObj.getInt("type")) { - case Slot.TYPE_RAW: - slot = new RawSlot(); - break; - case Slot.TYPE_DERIVED: - slot = new PasswordSlot(); - break; - case Slot.TYPE_FINGERPRINT: - slot = new FingerprintSlot(); - break; - default: - throw new Exception("unrecognized slot type"); + JSONArray entries = new JSONArray(); + for (Slot slot : slots) { + entries.put(slot.serialize()); } - slot.deserialize(slotObj); - slots.add(slot); + obj.put("entries", entries); + return obj; + } catch (SlotException | JSONException e) { + throw new SlotCollectionException(e); + } + } + + public static SlotCollection deserialize(JSONObject obj) throws SlotCollectionException { + SlotCollection slots = new SlotCollection(); + + try { + byte[] masterHash = Hex.toBytes(obj.getString("hash")); + slots.setMasterHash(masterHash); + + JSONArray entries = obj.getJSONArray("entries"); + for (int i = 0; i < entries.length(); i++) { + Slot slot; + JSONObject slotObj = entries.getJSONObject(i); + + switch (slotObj.getInt("type")) { + case Slot.TYPE_RAW: + slot = new RawSlot(); + break; + case Slot.TYPE_DERIVED: + slot = new PasswordSlot(); + break; + case Slot.TYPE_FINGERPRINT: + slot = new FingerprintSlot(); + break; + default: + throw new SlotException("unrecognized slot type"); + } + + slot.deserialize(slotObj); + slots.add(slot); + } + } catch (SlotException | JSONException | HexException e) { + throw new SlotCollectionException(e); } return slots; @@ -114,14 +121,12 @@ public class SlotCollection implements Iterable, Serializable { return _slots.iterator(); } - public void encrypt(Slot slot, MasterKey key, Cipher cipher) - throws BadPaddingException, IllegalBlockSizeException { + 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 SlotIntegrityException, BadPaddingException, IllegalBlockSizeException { + 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())) { 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 new file mode 100644 index 00000000..6ded4dec --- /dev/null +++ b/app/src/main/java/me/impy/aegis/db/slots/SlotCollectionException.java @@ -0,0 +1,11 @@ +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/SlotException.java b/app/src/main/java/me/impy/aegis/db/slots/SlotException.java new file mode 100644 index 00000000..0eb98f4e --- /dev/null +++ b/app/src/main/java/me/impy/aegis/db/slots/SlotException.java @@ -0,0 +1,11 @@ +package me.impy.aegis.db.slots; + +public class SlotException extends Exception { + public SlotException(Throwable cause) { + super(cause); + } + + public SlotException(String message) { + super(message); + } +} 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 4f24619c..66cffcbf 100644 --- a/app/src/main/java/me/impy/aegis/importers/AegisImporter.java +++ b/app/src/main/java/me/impy/aegis/importers/AegisImporter.java @@ -4,7 +4,9 @@ import java.util.List; import me.impy.aegis.db.Database; import me.impy.aegis.db.DatabaseEntry; +import me.impy.aegis.db.DatabaseException; import me.impy.aegis.db.DatabaseFile; +import me.impy.aegis.db.DatabaseFileException; import me.impy.aegis.util.ByteInputStream; public class AegisImporter extends DatabaseImporter { @@ -14,13 +16,17 @@ public class AegisImporter extends DatabaseImporter { } @Override - public List convert() throws Exception { - byte[] bytes = _stream.getBytes(); - DatabaseFile file = new DatabaseFile(); - file.deserialize(bytes); - Database db = new Database(); - db.deserialize(file.getContent()); - return db.getKeys(); + public List convert() throws DatabaseImporterException { + try { + byte[] bytes = _stream.getBytes(); + DatabaseFile file = new DatabaseFile(); + file.deserialize(bytes); + Database db = new Database(); + db.deserialize(file.getContent()); + return db.getKeys(); + } catch (DatabaseFileException | DatabaseException e) { + throw new DatabaseImporterException(e); + } } @Override 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 18f7be83..6c382f38 100644 --- a/app/src/main/java/me/impy/aegis/importers/DatabaseImporter.java +++ b/app/src/main/java/me/impy/aegis/importers/DatabaseImporter.java @@ -1,5 +1,6 @@ package me.impy.aegis.importers; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -19,14 +20,15 @@ public abstract class DatabaseImporter { _stream = stream; } - public abstract List convert() throws Exception; + public abstract List convert() throws DatabaseImporterException; public abstract String getName(); public static DatabaseImporter create(ByteInputStream stream, Class type) { try { return type.getConstructor(ByteInputStream.class).newInstance(stream); - } catch (Exception e) { + } catch (IllegalAccessException | InstantiationException + | NoSuchMethodException | InvocationTargetException e) { throw new RuntimeException(e); } } diff --git a/app/src/main/java/me/impy/aegis/importers/DatabaseImporterException.java b/app/src/main/java/me/impy/aegis/importers/DatabaseImporterException.java new file mode 100644 index 00000000..1dfd4116 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/importers/DatabaseImporterException.java @@ -0,0 +1,7 @@ +package me.impy.aegis.importers; + +public class DatabaseImporterException extends Exception { + public DatabaseImporterException(Throwable cause) { + super(cause); + } +} diff --git a/app/src/main/java/me/impy/aegis/importers/FreeOTPImporter.java b/app/src/main/java/me/impy/aegis/importers/FreeOTPImporter.java index 17d0925d..4c50449d 100644 --- a/app/src/main/java/me/impy/aegis/importers/FreeOTPImporter.java +++ b/app/src/main/java/me/impy/aegis/importers/FreeOTPImporter.java @@ -28,12 +28,16 @@ public class FreeOTPImporter extends DatabaseImporter { } @Override - public List convert() throws Exception { - XmlPullParser parser = Xml.newPullParser(); - parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); - parser.setInput(_stream, null); - parser.nextTag(); - return parse(parser); + 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) { + throw new DatabaseImporterException(e); + } } @Override 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 67f9b527..8ca30f7d 100644 --- a/app/src/main/java/me/impy/aegis/ui/AuthActivity.java +++ b/app/src/main/java/me/impy/aegis/ui/AuthActivity.java @@ -21,11 +21,13 @@ import javax.crypto.SecretKey; import me.impy.aegis.R; import me.impy.aegis.crypto.KeyStoreHandle; +import me.impy.aegis.crypto.KeyStoreHandleException; import me.impy.aegis.crypto.MasterKey; import me.impy.aegis.db.slots.FingerprintSlot; import me.impy.aegis.db.slots.PasswordSlot; import me.impy.aegis.db.slots.Slot; import me.impy.aegis.db.slots.SlotCollection; +import me.impy.aegis.db.slots.SlotException; import me.impy.aegis.helpers.FingerprintHelper; import me.impy.aegis.helpers.FingerprintUiHelper; import me.impy.aegis.helpers.EditTextHelper; @@ -80,7 +82,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C break; } } - } catch (Exception e) { + } catch (KeyStoreHandleException | SlotException e) { throw new UndeclaredThrowableException(e); } 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 6ac33e8d..4c69ac84 100644 --- a/app/src/main/java/me/impy/aegis/ui/IntroActivity.java +++ b/app/src/main/java/me/impy/aegis/ui/IntroActivity.java @@ -18,6 +18,9 @@ import me.impy.aegis.AegisApplication; import me.impy.aegis.R; import me.impy.aegis.crypto.CryptResult; import me.impy.aegis.crypto.MasterKey; +import me.impy.aegis.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; @@ -25,6 +28,7 @@ import me.impy.aegis.db.slots.SlotCollection; import me.impy.aegis.db.Database; import me.impy.aegis.db.DatabaseFile; import me.impy.aegis.db.DatabaseManager; +import me.impy.aegis.db.slots.SlotException; import me.impy.aegis.ui.slides.CustomAuthenticatedSlide; import me.impy.aegis.ui.slides.CustomAuthenticationSlide; import me.impy.aegis.ui.tasks.DerivationTask; @@ -134,31 +138,21 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback { // generate the master key MasterKey masterKey = null; if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) { - try { - masterKey = MasterKey.generate(); - } catch (Exception e) { - setException(e); - return; - } + masterKey = MasterKey.generate(); } SlotCollection slots = _databaseFile.getSlots(); 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 + if (_passwordSlot == null || _passwordCipher == null) { + throw new RuntimeException(); + } try { - // encrypt the master key with a key derived from the user's password - // and add it to the list of slots - if (_passwordSlot == null || _passwordCipher == null) { - throw new RuntimeException(); - } - try { - slots.encrypt(_passwordSlot, masterKey, _passwordCipher); - slots.add(_passwordSlot); - } catch (Exception e) { - setException(e); - } - } catch (Exception e) { + slots.encrypt(_passwordSlot, masterKey, _passwordCipher); + slots.add(_passwordSlot); + } catch (SlotException e) { setException(e); - return; } } @@ -170,7 +164,7 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback { Cipher cipher = _authenticatedSlide.getFingerCipher(); slots.encrypt(slot, masterKey, cipher); slots.add(slot); - } catch (Exception e) { + } catch (SlotException e) { setException(e); return; } @@ -185,7 +179,7 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback { _databaseFile.setContent(obj, masterKey); } DatabaseManager.save(getApplicationContext(), _databaseFile); - } catch (Exception e) { + } catch (DatabaseException | DatabaseManagerException | DatabaseFileException e) { setException(e); return; } @@ -205,7 +199,7 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback { if (key != null) { try { _passwordCipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE); - } catch (Exception e) { + } catch (SlotException e) { setException(e); } } else { 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 536f4978..14b3e7e9 100644 --- a/app/src/main/java/me/impy/aegis/ui/MainActivity.java +++ b/app/src/main/java/me/impy/aegis/ui/MainActivity.java @@ -21,6 +21,8 @@ import android.widget.Toast; import com.getbase.floatingactionbutton.FloatingActionsMenu; import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; import java.io.InputStream; import java.lang.reflect.UndeclaredThrowableException; import java.util.List; @@ -28,11 +30,13 @@ import java.util.List; import me.impy.aegis.AegisApplication; import me.impy.aegis.R; import me.impy.aegis.crypto.MasterKey; +import me.impy.aegis.db.DatabaseManagerException; import me.impy.aegis.db.slots.SlotCollection; import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.db.DatabaseManager; import me.impy.aegis.helpers.PermissionHelper; import me.impy.aegis.importers.DatabaseImporter; +import me.impy.aegis.importers.DatabaseImporterException; import me.impy.aegis.ui.views.KeyProfile; import me.impy.aegis.ui.views.KeyProfileView; import me.impy.aegis.util.ByteInputStream; @@ -107,10 +111,10 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen if (_db.isLocked()) { startAuthActivity(); } - } catch (Exception e) { + } catch (DatabaseManagerException e) { e.printStackTrace(); Toast.makeText(this, "An error occurred while trying to deserialize the database", Toast.LENGTH_LONG).show(); - throw new UndeclaredThrowableException(e); + finish(); } } } @@ -243,14 +247,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen onExport(); break; case PreferencesActivity.ACTION_SLOTS: - MasterKey masterKey; - try { - masterKey = _db.getMasterKey(); - } catch (Exception e) { - e.printStackTrace(); - Toast.makeText(this, "An error occurred while trying to obtain the database key", Toast.LENGTH_SHORT).show(); - break; - } + MasterKey masterKey = _db.getMasterKey(); Intent intent = new Intent(this, SlotManagerActivity.class); intent.putExtra("masterKey", masterKey); intent.putExtra("slots", _db.getFile().getSlots()); @@ -272,7 +269,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen String filename; try { filename = _db.export(checked[0]); - } catch (Exception e) { + } catch (DatabaseManagerException e) { e.printStackTrace(); Toast.makeText(this, "An error occurred while trying to export the database", Toast.LENGTH_SHORT).show(); return; @@ -314,8 +311,8 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen try { try { fileStream = getContentResolver().openInputStream(data.getData()); - } catch (Exception e) { - Toast.makeText(this, "An error occurred while trying to open the file", Toast.LENGTH_SHORT).show(); + } catch (FileNotFoundException e) { + Toast.makeText(this, "Error: File not found", Toast.LENGTH_SHORT).show(); return; } @@ -328,7 +325,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen outStream.write(buf, 0, read); } stream = new ByteInputStream(outStream.toByteArray()); - } catch (Exception e) { + } catch (IOException e) { Toast.makeText(this, "An error occurred while trying to read the file", Toast.LENGTH_SHORT).show(); return; } @@ -338,7 +335,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen try { entries = converter.convert(); break; - } catch (Exception e) { + } catch (DatabaseImporterException e) { stream.reset(); } } @@ -355,7 +352,8 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen if (fileStream != null) { try { fileStream.close(); - } catch (Exception e) { + } catch (IOException e) { + e.printStackTrace(); } } } @@ -405,13 +403,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen 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 - try { - _db.replaceKey(profile.getEntry()); - } catch (Exception e) { - e.printStackTrace(); - Toast.makeText(this, "An error occurred while trying to update an entry", Toast.LENGTH_SHORT).show(); - return; - } + _db.replaceKey(profile.getEntry()); _keyProfileView.replaceKey(profile); saveDatabase(); } else { @@ -431,14 +423,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen private void addKey(KeyProfile profile) { DatabaseEntry entry = profile.getEntry(); entry.setName(entry.getInfo().getAccountName()); - try { - _db.addKey(entry); - } catch (Exception e) { - e.printStackTrace(); - Toast.makeText(this, "An error occurred while trying to add an entry", Toast.LENGTH_SHORT).show(); - return; - } - + _db.addKey(entry); _keyProfileView.addKey(profile); } @@ -455,7 +440,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen if (_db.isLocked()) { _db.unlock(key); } - } catch (Exception e) { + } catch (DatabaseManagerException e) { e.printStackTrace(); Toast.makeText(this, "An error occurred while trying to load/decrypt the database", Toast.LENGTH_LONG).show(); startAuthActivity(); @@ -469,7 +454,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen MasterKey key = (MasterKey) intent.getSerializableExtra("key"); try { _db.unlock(key); - } catch (Exception e) { + } catch (DatabaseManagerException e) { e.printStackTrace(); Toast.makeText(this, "An error occurred while trying to decrypt the database", Toast.LENGTH_LONG).show(); startAuthActivity(); @@ -564,13 +549,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen .setTitle("Delete entry") .setMessage("Are you sure you want to delete this profile?") .setPositiveButton(android.R.string.yes, (dialog, which) -> { - try { - _db.removeKey(profile.getEntry()); - } catch (Exception e) { - e.printStackTrace(); - Toast.makeText(this, "An error occurred while trying to delete an entry", Toast.LENGTH_SHORT).show(); - return; - } + _db.removeKey(profile.getEntry()); saveDatabase(); _keyProfileView.removeKey(profile); @@ -597,12 +576,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen return true; case R.id.action_lock: _keyProfileView.clearKeys(); - try { - _db.lock(); - } catch (Exception e) { - e.printStackTrace(); - Toast.makeText(this, "An error occurred while trying to lock the database", Toast.LENGTH_LONG).show(); - } + _db.lock(); startAuthActivity(); return true; default: @@ -619,7 +593,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen private void saveDatabase() { try { _db.save(); - } catch (Exception e) { + } catch (DatabaseManagerException e) { e.printStackTrace(); Toast.makeText(this, "An error occurred while trying to save the database", Toast.LENGTH_LONG).show(); } @@ -628,14 +602,8 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen private void loadKeyProfiles() { updateLockIcon(); - try { - for (DatabaseEntry entry : _db.getKeys()) { - _keyProfileView.addKey(new KeyProfile(entry)); - } - } catch (Exception e) { - e.printStackTrace(); - Toast.makeText(this, "An error occurred while trying to load database entries", Toast.LENGTH_SHORT).show(); - return; + for (DatabaseEntry entry : _db.getKeys()) { + _keyProfileView.addKey(new KeyProfile(entry)); } } @@ -654,12 +622,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen @Override public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) { - try { - _db.swapKeys(entry1, entry2); - } catch (Exception e) { - e.printStackTrace(); - throw new UndeclaredThrowableException(e); - } + _db.swapKeys(entry1, entry2); } @Override 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 0e1128b3..8175f547 100644 --- a/app/src/main/java/me/impy/aegis/ui/ScannerActivity.java +++ b/app/src/main/java/me/impy/aegis/ui/ScannerActivity.java @@ -1,6 +1,5 @@ package me.impy.aegis.ui; -import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -14,6 +13,7 @@ import java.util.Collections; import me.dm7.barcodescanner.core.IViewFinder; import me.dm7.barcodescanner.zxing.ZXingScannerView; 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; @@ -64,9 +64,9 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R Intent resultIntent = new Intent(); resultIntent.putExtra("KeyProfile", profile); - setResult(Activity.RESULT_OK, resultIntent); + setResult(RESULT_OK, resultIntent); finish(); - } catch (Exception e) { + } catch (KeyInfoException 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 fde3a7d2..a31d459c 100644 --- a/app/src/main/java/me/impy/aegis/ui/SlotManagerActivity.java +++ b/app/src/main/java/me/impy/aegis/ui/SlotManagerActivity.java @@ -21,6 +21,7 @@ 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.SlotException; import me.impy.aegis.helpers.FingerprintHelper; import me.impy.aegis.ui.dialogs.FingerprintDialogFragment; import me.impy.aegis.ui.dialogs.PasswordDialogFragment; @@ -167,7 +168,7 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li public void onSlotResult(Slot slot, Cipher cipher) { try { _slots.encrypt(slot, _masterKey, cipher); - } catch (Exception e) { + } 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 1321dbb8..c2081d1d 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 @@ -15,8 +15,10 @@ import javax.crypto.SecretKey; import me.impy.aegis.R; import me.impy.aegis.crypto.KeyStoreHandle; +import me.impy.aegis.crypto.KeyStoreHandleException; import me.impy.aegis.db.slots.FingerprintSlot; import me.impy.aegis.db.slots.Slot; +import me.impy.aegis.db.slots.SlotException; import me.impy.aegis.helpers.FingerprintHelper; import me.impy.aegis.helpers.FingerprintUiHelper; @@ -38,7 +40,7 @@ public class FingerprintDialogFragment extends SlotDialogFragment implements Fin SecretKey key = new KeyStoreHandle().generateKey(_slot.getUUID().toString()); _cipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE); _helper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this); - } catch (Exception e) { + } 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 aa105a44..65b1e8d4 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 @@ -15,6 +15,7 @@ import javax.crypto.Cipher; import me.impy.aegis.R; import me.impy.aegis.db.slots.PasswordSlot; import me.impy.aegis.db.slots.Slot; +import me.impy.aegis.db.slots.SlotException; import me.impy.aegis.helpers.EditTextHelper; import me.impy.aegis.ui.tasks.DerivationTask; @@ -51,7 +52,7 @@ public class PasswordDialogFragment extends SlotDialogFragment { Cipher cipher; try { cipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE); - } catch (Exception e) { + } catch (SlotException e) { getListener().onException(e); dialog.cancel(); return; 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 d3ab78a4..ef7f32b3 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 @@ -25,6 +25,7 @@ import javax.crypto.SecretKey; import me.impy.aegis.R; import me.impy.aegis.crypto.KeyStoreHandle; +import me.impy.aegis.crypto.KeyStoreHandleException; import me.impy.aegis.db.slots.FingerprintSlot; import me.impy.aegis.db.slots.Slot; import me.impy.aegis.helpers.FingerprintUiHelper; @@ -102,7 +103,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH _fingerSlot = new FingerprintSlot(); } key = _storeHandle.generateKey(_fingerSlot.getUUID().toString()); - } catch (Exception e) { + } catch (KeyStoreHandleException 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 70d458d5..60bc75dd 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 @@ -3,10 +3,14 @@ package me.impy.aegis.ui.tasks; import android.content.Context; import android.os.Process; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + 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; @@ -23,9 +27,8 @@ public class DerivationTask extends ProgressDialogTask extends ProgressDialogTask { @@ -59,7 +60,7 @@ public class SlotCollectionTask extends ProgressDialogTask