From 2a4bb902df4274b95acaa2cac4920abf90c1807f Mon Sep 17 00:00:00 2001 From: Alexander Bakker Date: Sat, 1 Feb 2020 20:52:14 +0100 Subject: [PATCH] Add support for importing encrypted and manually entered tokens from Authy --- .../aegis/importers/AuthyImporter.java | 115 +++++++++++++++--- .../com/beemdevelopment/aegis/ui/Dialogs.java | 23 +++- app/src/main/res/values/strings.xml | 1 + 3 files changed, 119 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/AuthyImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/AuthyImporter.java index a2e77c5f..8cccafa5 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/AuthyImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/AuthyImporter.java @@ -3,13 +3,16 @@ package com.beemdevelopment.aegis.importers; import android.content.Context; import android.util.Xml; -import com.beemdevelopment.aegis.vault.VaultEntry; +import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.encoding.Base32; +import com.beemdevelopment.aegis.encoding.Base64; import com.beemdevelopment.aegis.encoding.EncodingException; import com.beemdevelopment.aegis.otp.OtpInfo; import com.beemdevelopment.aegis.otp.OtpInfoException; import com.beemdevelopment.aegis.otp.TotpInfo; +import com.beemdevelopment.aegis.ui.Dialogs; import com.beemdevelopment.aegis.util.PreferenceParser; +import com.beemdevelopment.aegis.vault.VaultEntry; import org.json.JSONArray; import org.json.JSONException; @@ -18,11 +21,33 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; public class AuthyImporter extends DatabaseImporter { private static final String _subPath = "shared_prefs/com.authy.storage.tokens.authenticator.xml"; private static final String _pkgName = "com.authy.authy"; + private static final int ITERATIONS = 1000; + private static final int KEY_SIZE = 256; + private static final byte[] IV = new byte[]{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + public AuthyImporter(Context context) { super(context); } @@ -52,18 +77,76 @@ public class AuthyImporter extends DatabaseImporter { } } - return new State(array); + for (int i = 0; i < array.length(); i++) { + if (!array.getJSONObject(i).has("decryptedSecret")) { + return new EncryptedState(array); + } + } + + return new DecryptedState(array); } catch (XmlPullParserException | JSONException | IOException e) { throw new DatabaseImporterException(e); } } - public static class State extends DatabaseImporter.State { - private JSONArray _obj; + public static class EncryptedState extends DatabaseImporter.State { + private JSONArray _array; - private State(JSONArray obj) { + private EncryptedState(JSONArray array) { + super(true); + _array = array; + } + + @Override + public void decrypt(Context context, DecryptListener listener) { + Dialogs.showPasswordInputDialog(context, R.string.enter_password_authy_message, password -> { + try { + for (int i = 0; i < _array.length(); i++) { + JSONObject obj = _array.getJSONObject(i); + String secretString = obj.optString("encryptedSecret", null); + if (secretString == null) { + continue; + } + + byte[] encryptedSecret = Base64.decode(secretString); + byte[] salt = obj.getString("salt").getBytes(StandardCharsets.UTF_8); + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_SIZE); + SecretKey key = factory.generateSecret(spec); + + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); + IvParameterSpec ivSpec = new IvParameterSpec(IV); + cipher.init(Cipher.DECRYPT_MODE, key, ivSpec); + + byte[] secret = cipher.doFinal(encryptedSecret); + obj.remove("encryptedSecret"); + obj.remove("salt"); + obj.put("decryptedSecret", new String(secret, StandardCharsets.UTF_8)); + } + + DecryptedState state = new DecryptedState(_array); + listener.onStateDecrypted(state); + } catch (JSONException + | EncodingException + | NoSuchAlgorithmException + | InvalidKeySpecException + | InvalidAlgorithmParameterException + | InvalidKeyException + | NoSuchPaddingException + | BadPaddingException + | IllegalBlockSizeException e) { + listener.onError(e); + } + }); + } + } + + public static class DecryptedState extends DatabaseImporter.State { + private JSONArray _array; + + private DecryptedState(JSONArray array) { super(false); - _obj = obj; + _array = array; } @Override @@ -71,8 +154,8 @@ public class AuthyImporter extends DatabaseImporter { Result result = new Result(); try { - for (int i = 0; i < _obj.length(); i++) { - JSONObject entryObj = _obj.getJSONObject(i); + for (int i = 0; i < _array.length(); i++) { + JSONObject entryObj = _array.getJSONObject(i); try { VaultEntry entry = convertEntry(entryObj); result.addEntry(entry); @@ -90,7 +173,8 @@ public class AuthyImporter extends DatabaseImporter { private static VaultEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException { try { AuthyEntryInfo authyEntryInfo = new AuthyEntryInfo(); - authyEntryInfo.OriginalName = entry.getString("originalName"); + authyEntryInfo.OriginalName = entry.optString("originalName", null); + authyEntryInfo.OriginalIssuer = entry.optString("originalIssuer", null); authyEntryInfo.AccountType = entry.getString("accountType"); authyEntryInfo.Name = entry.optString("name"); @@ -108,24 +192,27 @@ public class AuthyImporter extends DatabaseImporter { } private static void sanitizeEntryInfo(AuthyEntryInfo info) { - String seperator = ""; + String separator = ""; - if (info.OriginalName.contains(":")) { + if (info.OriginalIssuer != null) { + info.Issuer = info.OriginalIssuer; + } else if (info.OriginalName != null && info.OriginalName.contains(":")) { info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":")); - seperator = ":"; + separator = ":"; } else if (info.Name.contains(" - ")) { info.Issuer = info.Name.substring(0, info.Name.indexOf(" - ")); - seperator = " - "; + separator = " - "; } else { info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1); } - info.Name = info.Name.replace(info.Issuer + seperator, ""); + info.Name = info.Name.replace(info.Issuer + separator, ""); } } private static class AuthyEntryInfo { String OriginalName; + String OriginalIssuer; String AccountType; String Issuer; String Name; diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java b/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java index 3731e2ef..3a8a0c52 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java @@ -156,7 +156,7 @@ public class Dialogs { showSecureDialog(dialog); } - private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) { + private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, boolean isSecret) { View view = LayoutInflater.from(context).inflate(R.layout.dialog_text_input, null); EditText input = view.findViewById(R.id.text_input); if (isSecret) { @@ -164,18 +164,25 @@ public class Dialogs { } input.setHint(hintId); - AlertDialog dialog = new AlertDialog.Builder(context) + AlertDialog.Builder builder = new AlertDialog.Builder(context) .setTitle(titleId) .setView(view) .setPositiveButton(android.R.string.ok, (dialog1, which) -> { - char[] text = EditTextHelper.getEditTextChars(input); - listener.onTextInputResult(text); - }) - .create(); + char[] text = EditTextHelper.getEditTextChars(input); + listener.onTextInputResult(text); + }); + if (messageId != 0) { + builder.setMessage(messageId); + } + AlertDialog dialog = builder.create(); showSecureDialog(dialog); } + private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) { + showTextInputDialog(context, titleId, 0, hintId, listener, isSecret); + } + public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) { showTextInputDialog(context, titleId, hintId, listener, false); } @@ -184,6 +191,10 @@ public class Dialogs { showTextInputDialog(context, R.string.set_password, R.string.password, listener, true); } + public static void showPasswordInputDialog(Context context, @StringRes int messageId, TextInputListener listener) { + showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, true); + } + public static void showNumberPickerDialog(Activity activity, NumberInputListener listener) { View view = activity.getLayoutInflater().inflate(R.layout.dialog_number_picker, null); NumberPicker numberPicker = view.findViewById(R.id.numberPicker); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ada120c2..8cb32a35 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -66,6 +66,7 @@ Please confirm the password A change in your device\'s security settings has been detected. Please go to \"Aegis -> Settings -> Biometrics\" and re-enable biometric unlock. It\'s been a while since you\'ve entered your password. Do you still remember it? + It looks like your Authy tokens are encrypted. Please close Aegis, open Authy and unlock the tokens with your password. Instead, Aegis can also attempt to decrypt your Authy tokens for you, if you enter your password below. Unlock Biometrics