diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java index da6672bb..f877653f 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java @@ -3,6 +3,9 @@ package com.beemdevelopment.aegis.importers; import android.content.Context; import android.content.pm.PackageManager; +import androidx.annotation.StringRes; + +import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.util.UUIDMap; import com.beemdevelopment.aegis.vault.VaultEntry; import com.topjohnwu.superuser.io.SuFile; @@ -13,40 +16,29 @@ import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; +import java.util.stream.Collectors; public abstract class DatabaseImporter { private Context _context; - private static Map> _importers; - private static Map> _appImporters; + private static List _importers; static { // note: keep these lists sorted alphabetically - _importers = new LinkedHashMap<>(); - _importers.put("Aegis", AegisImporter.class); - _importers.put("Authenticator Plus", AuthenticatorPlusImporter.class); - _importers.put("Authy", AuthyImporter.class); - _importers.put("andOTP", AndOtpImporter.class); - _importers.put("FreeOTP", FreeOtpImporter.class); - _importers.put("FreeOTP+", FreeOtpPlusImporter.class); - _importers.put("Google Authenticator", GoogleAuthImporter.class); - _importers.put("Microsoft Authenticator", MicrosoftAuthImporter.class); - _importers.put("Plain text", GoogleAuthUriImporter.class); - _importers.put("Steam", SteamImporter.class); - _importers.put("TOTP Authenticator", TotpAuthenticatorImporter.class); - _importers.put("WinAuth", WinAuthImporter.class); - - _appImporters = new LinkedHashMap<>(); - _appImporters.put("Authy", AuthyImporter.class); - _appImporters.put("FreeOTP", FreeOtpImporter.class); - _appImporters.put("FreeOTP+", FreeOtpPlusImporter.class); - _appImporters.put("Google Authenticator", GoogleAuthImporter.class); - _appImporters.put("Microsoft Authenticator", MicrosoftAuthImporter.class); - _appImporters.put("Steam", SteamImporter.class); - _appImporters.put("TOTP Authenticator", TotpAuthenticatorImporter.class); + _importers = new ArrayList<>(); + _importers.add(new Definition("Aegis", AegisImporter.class, R.string.importer_help_aegis, false)); + _importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false)); + _importers.add(new Definition("Authy", AuthyImporter.class, R.string.importer_help_authy, true)); + _importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false)); + _importers.add(new Definition("FreeOTP", FreeOtpImporter.class, R.string.importer_help_freeotp, true)); + _importers.add(new Definition("FreeOTP+", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true)); + _importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true)); + _importers.add(new Definition("Microsoft Authenticator", MicrosoftAuthImporter.class, R.string.importer_help_microsoft_authenticator, true)); + _importers.add(new Definition("Plain text", GoogleAuthUriImporter.class, R.string.importer_help_plain_text, false)); + _importers.add(new Definition("Steam", SteamImporter.class, R.string.importer_help_steam, true)); + _importers.add(new Definition("TOTP Authenticator", TotpAuthenticatorImporter.class, R.string.importer_help_totp_authenticator, true)); + _importers.add(new Definition("WinAuth", WinAuthImporter.class, R.string.importer_help_winauth, false)); } public DatabaseImporter(Context context) { @@ -88,12 +80,42 @@ public abstract class DatabaseImporter { } } - public static Map> getImporters() { - return Collections.unmodifiableMap(_importers); + public static List getImporters(boolean isDirect) { + if (isDirect) { + return Collections.unmodifiableList(_importers.stream().filter(Definition::supportsDirect).collect(Collectors.toList())); + } + + return Collections.unmodifiableList(_importers); } - public static Map> getAppImporters() { - return Collections.unmodifiableMap(_appImporters); + public static class Definition { + private final String _name; + private final Class _type; + private final @StringRes int _help; + private final boolean _supportsDirect; + + public Definition(String name, Class type, @StringRes int help, boolean supportsDirect) { + _name = name; + _type = type; + _help = help; + _supportsDirect = supportsDirect; + } + + public String getName() { + return _name; + } + + public Class getType() { + return _type; + } + + public @StringRes int getHelp() { + return _help; + } + + public boolean supportsDirect() { + return _supportsDirect; + } } public static abstract class State { 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 6d111398..1040fc07 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java @@ -15,10 +15,12 @@ import android.text.method.PasswordTransformationMethod; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; +import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.EditText; +import android.widget.ListView; import android.widget.NumberPicker; import android.widget.ProgressBar; import android.widget.TextView; @@ -31,6 +33,7 @@ import com.beemdevelopment.aegis.Preferences; import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.helpers.EditTextHelper; import com.beemdevelopment.aegis.helpers.PasswordStrengthHelper; +import com.beemdevelopment.aegis.importers.DatabaseImporter; import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask; import com.beemdevelopment.aegis.vault.slots.PasswordSlot; import com.beemdevelopment.aegis.vault.slots.Slot; @@ -39,6 +42,7 @@ import com.google.android.material.textfield.TextInputLayout; import com.nulabinc.zxcvbn.Strength; import com.nulabinc.zxcvbn.Zxcvbn; +import java.util.List; import java.util.concurrent.atomic.AtomicReference; import javax.crypto.Cipher; @@ -383,6 +387,37 @@ public class Dialogs { .create()); } + public static void showImportersDialog(Context context, boolean isDirect, ImporterListener listener) { + List importers = DatabaseImporter.getImporters(isDirect); + String[] names = importers.stream().map(DatabaseImporter.Definition::getName).toArray(String[]::new); + + View view = LayoutInflater.from(context).inflate(R.layout.dialog_importers, null); + TextView helpText = view.findViewById(R.id.text_importer_help); + setImporterHelpText(helpText, importers.get(0), isDirect); + ListView listView = view.findViewById(R.id.list_importers); + listView.setAdapter(new ArrayAdapter<>(context, R.layout.card_importer, names)); + listView.setItemChecked(0, true); + listView.setOnItemClickListener((parent, view1, position, id) -> { + setImporterHelpText(helpText, importers.get(position), isDirect); + }); + + Dialogs.showSecureDialog(new AlertDialog.Builder(context) + .setTitle(R.string.choose_application) + .setView(view) + .setPositiveButton(android.R.string.ok, (dialog1, which) -> { + listener.onImporterSelectionResult(importers.get(listView.getCheckedItemPosition())); + }) + .create()); + } + + private static void setImporterHelpText(TextView view, DatabaseImporter.Definition definition, boolean isDirect) { + if (isDirect) { + view.setText(view.getResources().getString(R.string.importer_help_direct, definition.getName())); + } else { + view.setText(definition.getHelp()); + } + } + public interface CheckboxInputListener { void onCheckboxInputResult(boolean checkbox); } @@ -399,4 +434,8 @@ public class Dialogs { void onSlotResult(Slot slot, Cipher cipher); void onException(Exception e); } + + public interface ImporterListener { + void onImporterSelectionResult(DatabaseImporter.Definition definition); + } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java index ef0f6811..daee6e3a 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java @@ -57,8 +57,6 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import javax.crypto.Cipher; @@ -175,7 +173,13 @@ public class PreferencesFragment extends PreferenceFragmentCompat { importPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { - startImport(); + Dialogs.showImportersDialog(getContext(), false, definition -> { + _importerType = definition.getType(); + + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("*/*"); + startActivityForResult(intent, CODE_IMPORT); + }); return true; } }); @@ -184,7 +188,10 @@ public class PreferencesFragment extends PreferenceFragmentCompat { importAppPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { - onImportApp(); + Dialogs.showImportersDialog(getContext(), true, definition -> { + DatabaseImporter importer = DatabaseImporter.create(getContext(), definition.getType()); + importApp(importer); + }); return true; } }); @@ -540,42 +547,6 @@ public class PreferencesFragment extends PreferenceFragmentCompat { getActivity().setResult(Activity.RESULT_OK, _result); } - private void startImport() { - Map> importers = DatabaseImporter.getImporters(); - String[] names = importers.keySet().toArray(new String[0]); - - Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) - .setTitle(R.string.choose_application) - .setSingleChoiceItems(names, 0, null) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); - _importerType = importers.get(names[i]); - - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("*/*"); - startActivityForResult(intent, CODE_IMPORT); - } - }) - .create()); - } - - private void onImportApp() { - Map> importers = DatabaseImporter.getAppImporters(); - String[] names = importers.keySet().toArray(new String[0]); - - Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) - .setTitle(R.string.choose_application) - .setSingleChoiceItems(names, 0, null) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); - Class importerType = Objects.requireNonNull(importers.get(names[i])); - DatabaseImporter importer = DatabaseImporter.create(getContext(), importerType); - importApp(importer); - }) - .create()); - } - private void importApp(DatabaseImporter importer) { // obtain the global root shell and close it immediately after we're done // TODO: find a way to use SuFileInputStream with Shell.newInstance() diff --git a/app/src/main/res/layout/card_importer.xml b/app/src/main/res/layout/card_importer.xml new file mode 100644 index 00000000..d87f57ff --- /dev/null +++ b/app/src/main/res/layout/card_importer.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/layout/dialog_importers.xml b/app/src/main/res/layout/dialog_importers.xml new file mode 100644 index 00000000..6ec60498 --- /dev/null +++ b/app/src/main/res/layout/dialog_importers.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4372155d..ac92e1f3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -329,4 +329,19 @@ Aegis received panic trigger but setting is disabled, ignoring Delete vault on panic trigger Delete vault when a panic trigger is received from Ripple + + Supply an Aegis export/backup file. + Supply an Authenticator Plus export file obtained through Settings -> Backup & Restore -> Export as Text and HTML. + Supply a copy of /data/data/com.authy.authy/shared_prefs/com.authy.storage.tokens.authenticator.xml, located in the internal storage directory of Authy. + Supply an andOTP export/backup file. + Supply a copy of /data/data/org.fedorahosted.freeotp/shared_prefs/tokens.xml, located in the internal storage directory of FreeOTP. + Supply a FreeOTP+ export file. + Supply a copy of /data/data/com.google.android.apps.authenticator2/databases/databases, located in the internal storage directory of Google Authenticator. + Supply a copy of /data/data/com.azure.authenticator/databases/PhoneFactor, located in the internal storage directory of Microsoft Authenticator. + Supply a plain text file with a Google Authenticator URI on each line. + Supply a copy of /data/data/com.valvesoftware.android.steam.community/files/Steamguard-*.json, located in the internal storage directory of Steam. + Supply a TOTP Authenticator export file. + Supply a WinAuth export file. + + Import entries directly from %s. This requires the app to be installed on this device and for root access to be granted to Aegis.