diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/AuthenticatorProImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/AuthenticatorProImporter.java index 354ded2d..1f755962 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/AuthenticatorProImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/AuthenticatorProImporter.java @@ -16,11 +16,13 @@ import com.beemdevelopment.aegis.otp.OtpInfoException; import com.beemdevelopment.aegis.otp.SteamInfo; import com.beemdevelopment.aegis.otp.TotpInfo; import com.beemdevelopment.aegis.ui.dialogs.Dialogs; +import com.beemdevelopment.aegis.ui.tasks.Argon2Task; import com.beemdevelopment.aegis.ui.tasks.PBKDFTask; import com.beemdevelopment.aegis.util.IOUtils; import com.beemdevelopment.aegis.vault.VaultEntry; import com.topjohnwu.superuser.io.SuFile; +import org.bouncycastle.crypto.params.Argon2Parameters; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -44,9 +46,8 @@ import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; public class AuthenticatorProImporter extends DatabaseImporter { - private static final String HEADER = "AuthenticatorPro"; - private static final int ITERATIONS = 64000; - private static final int KEY_SIZE = 32 * Byte.SIZE; + private static final String HEADER = "AUTHENTICATORPRO"; + private static final String HEADER_LEGACY = "AuthenticatorPro"; private static final String PKG_NAME = "me.jmh.authenticatorpro"; private static final String PKG_DB_PATH = "files/proauth.db3"; @@ -90,24 +91,19 @@ public class AuthenticatorProImporter extends DatabaseImporter { } } - private static EncryptedState readEncrypted(DataInputStream stream) throws DatabaseImporterException { + private static State readEncrypted(DataInputStream stream) throws DatabaseImporterException { try { byte[] headerBytes = new byte[HEADER.getBytes(StandardCharsets.UTF_8).length]; stream.readFully(headerBytes); String header = new String(headerBytes, StandardCharsets.UTF_8); - if (!header.equals(HEADER)) { - throw new DatabaseImporterException("Invalid file header"); + switch (header) { + case HEADER: + return EncryptedState.parseHeader(stream); + case HEADER_LEGACY: + return LegacyEncryptedState.parseHeader(stream); + default: + throw new DatabaseImporterException("Invalid file header"); } - - int saltSize = 20; - byte[] salt = new byte[saltSize]; - stream.readFully(salt); - - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - int ivSize = cipher.getBlockSize(); - byte[] iv = new byte[ivSize]; - stream.readFully(iv); - return new EncryptedState(cipher, salt, iv, IOUtils.readAll(stream)); } catch (UTFDataFormatException e) { throw new DatabaseImporterException("Invalid file header"); } catch (IOException | NoSuchPaddingException | NoSuchAlgorithmException e) { @@ -130,6 +126,13 @@ public class AuthenticatorProImporter extends DatabaseImporter { } static class EncryptedState extends State { + private static final int KEY_SIZE = 32; + private static final int MEMORY_COST = 16; // 2^16 KiB = 64 MiB + private static final int PARALLELISM = 4; + private static final int ITERATIONS = 3; + private static final int SALT_SIZE = 16; + private static final int IV_SIZE = 12; + private final Cipher _cipher; private final byte[] _salt; private final byte[] _iv; @@ -143,6 +146,81 @@ public class AuthenticatorProImporter extends DatabaseImporter { _data = data; } + public JsonState decrypt(char[] password) throws DatabaseImporterException { + Argon2Task.Params params = getKeyDerivationParams(password); + SecretKey key = Argon2Task.deriveKey(params); + return decrypt(key); + } + + public JsonState decrypt(SecretKey key) throws DatabaseImporterException { + try { + _cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(_iv)); + byte[] decrypted = _cipher.doFinal(_data); + return new JsonState(new JSONObject(new String(decrypted, StandardCharsets.UTF_8))); + } catch (InvalidAlgorithmParameterException | IllegalBlockSizeException + | JSONException | InvalidKeyException | BadPaddingException e) { + throw new DatabaseImporterException(e); + } + } + + @Override + public void decrypt(Context context, DecryptListener listener) throws DatabaseImporterException { + Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, (Dialogs.TextInputListener) password -> { + Argon2Task.Params params = getKeyDerivationParams(password); + Argon2Task task = new Argon2Task(context, key -> { + try { + AuthenticatorProImporter.JsonState state = decrypt(key); + listener.onStateDecrypted(state); + } catch (DatabaseImporterException e) { + listener.onError(e); + } + }); + Lifecycle lifecycle = ContextHelper.getLifecycle(context); + task.execute(lifecycle, params); + }, dialog -> listener.onCanceled()); + } + + private Argon2Task.Params getKeyDerivationParams(char[] password) { + Argon2Parameters argon2Params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id) + .withIterations(ITERATIONS) + .withParallelism(PARALLELISM) + .withMemoryPowOfTwo(MEMORY_COST) + .withSalt(_salt) + .build(); + return new Argon2Task.Params(password, argon2Params, KEY_SIZE); + } + + private static EncryptedState parseHeader(DataInputStream stream) + throws IOException, NoSuchPaddingException, NoSuchAlgorithmException { + byte[] salt = new byte[SALT_SIZE]; + stream.readFully(salt); + + byte[] iv = new byte[IV_SIZE]; + stream.readFully(iv); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + return new EncryptedState(cipher, salt, iv, IOUtils.readAll(stream)); + } + } + + static class LegacyEncryptedState extends State { + private static final int ITERATIONS = 64000; + private static final int KEY_SIZE = 32 * Byte.SIZE; + private static final int SALT_SIZE = 20; + + private final Cipher _cipher; + private final byte[] _salt; + private final byte[] _iv; + private final byte[] _data; + + public LegacyEncryptedState(Cipher cipher, byte[] salt, byte[] iv, byte[] data) { + super(true); + _cipher = cipher; + _salt = salt; + _iv = iv; + _data = data; + } + public JsonState decrypt(char[] password) throws DatabaseImporterException { PBKDFTask.Params params = getKeyDerivationParams(password); SecretKey key = PBKDFTask.deriveKey(params); @@ -180,6 +258,18 @@ public class AuthenticatorProImporter extends DatabaseImporter { private PBKDFTask.Params getKeyDerivationParams(char[] password) { return new PBKDFTask.Params("PBKDF2WithHmacSHA1", KEY_SIZE, password, _salt, ITERATIONS); } + + private static LegacyEncryptedState parseHeader(DataInputStream stream) + throws IOException, NoSuchPaddingException, NoSuchAlgorithmException { + byte[] salt = new byte[SALT_SIZE]; + stream.readFully(salt); + + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + int ivSize = cipher.getBlockSize(); + byte[] iv = new byte[ivSize]; + stream.readFully(iv); + return new LegacyEncryptedState(cipher, salt, iv, IOUtils.readAll(stream)); + } } private static class JsonState extends State { diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/tasks/Argon2Task.java b/app/src/main/java/com/beemdevelopment/aegis/ui/tasks/Argon2Task.java new file mode 100644 index 00000000..98b35b6a --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/tasks/Argon2Task.java @@ -0,0 +1,71 @@ +package com.beemdevelopment.aegis.ui.tasks; + +import android.content.Context; + +import com.beemdevelopment.aegis.R; + +import org.bouncycastle.crypto.generators.Argon2BytesGenerator; +import org.bouncycastle.crypto.params.Argon2Parameters; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +public class Argon2Task extends ProgressDialogTask { + private final Callback _cb; + + public Argon2Task(Context context, Callback cb) { + super(context, context.getString(R.string.unlocking_vault)); + _cb = cb; + } + + @Override + protected SecretKey doInBackground(Params... args) { + setPriority(); + + Params params = args[0]; + return deriveKey(params); + } + + public static SecretKey deriveKey(Params params) { + Argon2BytesGenerator gen = new Argon2BytesGenerator(); + gen.init(params.getArgon2Params()); + + byte[] key = new byte[params.getKeySize()]; + gen.generateBytes(params.getPassword(), key); + return new SecretKeySpec(key, 0, key.length, "AES"); + } + + @Override + protected void onPostExecute(SecretKey key) { + super.onPostExecute(key); + _cb.onTaskFinished(key); + } + + public interface Callback { + void onTaskFinished(SecretKey key); + } + + public static class Params { + private final char[] _password; + private final Argon2Parameters _argon2Params; + private final int _keySize; + + public Params(char[] password, Argon2Parameters argon2Params, int keySize) { + _password = password; + _argon2Params = argon2Params; + _keySize = keySize; + } + + public char[] getPassword() { + return _password; + } + + public Argon2Parameters getArgon2Params() { + return _argon2Params; + } + + public int getKeySize() { + return _keySize; + } + } +} diff --git a/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java b/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java index d255f68a..c9f1030a 100644 --- a/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java +++ b/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java @@ -146,11 +146,16 @@ public class DatabaseImporterTest { public void testImportAuthProEncrypted() throws DatabaseImporterException, IOException, OtpInfoException { List entries = importEncrypted(AuthenticatorProImporter.class, "authpro_encrypted.bin", state -> { char[] password = "test".toCharArray(); - try { - return ((AuthenticatorProImporter.EncryptedState) state).decrypt(password); - } catch (DatabaseImporterException e) { - throw new DatabaseImporterException(e); - } + return ((AuthenticatorProImporter.EncryptedState) state).decrypt(password); + }); + checkImportedEntries(entries); + } + + @Test + public void testImportAuthProEncryptedLegacy() throws DatabaseImporterException, IOException, OtpInfoException { + List entries = importEncrypted(AuthenticatorProImporter.class, "authpro_encrypted_legacy.bin", state -> { + char[] password = "test".toCharArray(); + return ((AuthenticatorProImporter.LegacyEncryptedState) state).decrypt(password); }); checkImportedEntries(entries); } diff --git a/app/src/test/resources/com/beemdevelopment/aegis/importers/authpro_encrypted.bin b/app/src/test/resources/com/beemdevelopment/aegis/importers/authpro_encrypted.bin index 25858829..b515a7b3 100644 Binary files a/app/src/test/resources/com/beemdevelopment/aegis/importers/authpro_encrypted.bin and b/app/src/test/resources/com/beemdevelopment/aegis/importers/authpro_encrypted.bin differ diff --git a/app/src/test/resources/com/beemdevelopment/aegis/importers/authpro_encrypted_legacy.bin b/app/src/test/resources/com/beemdevelopment/aegis/importers/authpro_encrypted_legacy.bin new file mode 100644 index 00000000..25858829 Binary files /dev/null and b/app/src/test/resources/com/beemdevelopment/aegis/importers/authpro_encrypted_legacy.bin differ