From 2767727ad446b4def5f98c3864c46989658f4b0d Mon Sep 17 00:00:00 2001 From: elena Date: Thu, 11 Aug 2022 15:23:11 +0100 Subject: [PATCH] Import google auth export from image --- .../aegis/otp/GoogleAuthInfo.java | 42 ++++++++++++++++++ .../aegis/ui/MainActivity.java | 43 +++++++++++++++++-- .../aegis/ui/dialogs/Dialogs.java | 40 +++++++++++++++++ app/src/main/res/values/strings.xml | 8 ++++ 4 files changed, 129 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java b/app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java index e75a9713..8138acf1 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java +++ b/app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java @@ -2,6 +2,8 @@ package com.beemdevelopment.aegis.otp; import android.net.Uri; +import androidx.annotation.NonNull; + import com.beemdevelopment.aegis.GoogleAuthProtos; import com.beemdevelopment.aegis.encoding.Base32; import com.beemdevelopment.aegis.encoding.Base64; @@ -13,6 +15,8 @@ import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; public class GoogleAuthInfo implements Serializable { public static final String SCHEME = "otpauth"; @@ -343,5 +347,43 @@ public class GoogleAuthInfo implements Serializable { public int getBatchId() { return _batchId; } + + public static List getMissingIndices(@NonNull List exports) throws IllegalArgumentException { + if (!isSingleBatch(exports)) { + throw new IllegalArgumentException("Export list contains entries from different batches"); + } + + List indicesMissing = new ArrayList<>(); + if (exports.isEmpty()) { + return indicesMissing; + } + + Set indicesPresent = exports.stream() + .map(Export::getBatchIndex) + .collect(Collectors.toSet()); + + for (int i = 0; i < exports.get(0).getBatchSize(); i++) { + if (!indicesPresent.contains(i)) { + indicesMissing.add(i); + } + } + + return indicesMissing; + } + + public static boolean isSingleBatch(@NonNull List exports) { + if (exports.isEmpty()) { + return true; + } + + int batchId = exports.get(0).getBatchId(); + for (Export export : exports) { + if (export.getBatchId() != batchId) { + return false; + } + } + + return true; + } } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java index 1c75cc2b..bd0d3383 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java @@ -46,6 +46,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.TreeSet; import java.util.UUID; @@ -340,6 +341,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene QrDecodeTask task = new QrDecodeTask(this, (results) -> { List errors = new ArrayList<>(); List entries = new ArrayList<>(); + List googleAuthExports = new ArrayList<>(); + for (QrDecodeTask.Result res : results) { if (res.getException() != null) { errors.add(buildImportError(res.getFileName(), res.getException())); @@ -347,15 +350,47 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene } try { - GoogleAuthInfo info = GoogleAuthInfo.parseUri(res.getResult().getText()); - VaultEntry entry = new VaultEntry(info); - entries.add(entry); - } catch (GoogleAuthInfoException e) { + Uri scanned = Uri.parse(res.getResult().getText()); + if (Objects.equals(scanned.getScheme(), GoogleAuthInfo.SCHEME_EXPORT)) { + GoogleAuthInfo.Export export = GoogleAuthInfo.parseExportUri(scanned); + for (GoogleAuthInfo info: export.getEntries()) { + VaultEntry entry = new VaultEntry(info); + entries.add(entry); + } + googleAuthExports.add(export); + } else { + GoogleAuthInfo info = GoogleAuthInfo.parseUri(res.getResult().getText()); + VaultEntry entry = new VaultEntry(info); + entries.add(entry); + } + } catch (GoogleAuthInfoException | IllegalArgumentException e) { errors.add(buildImportError(res.getFileName(), e)); } } final DialogInterface.OnClickListener dialogDismissHandler = (dialog, which) -> importScannedEntries(entries); + if (!googleAuthExports.isEmpty()) { + try { + if (!GoogleAuthInfo.Export.isSingleBatch(googleAuthExports) && errors.size() > 0) { + errors.add(getString(R.string.unrelated_google_auth_batches_error)); + Dialogs.showMultiMessageDialog(this, R.string.import_error_title, getString(R.string.no_tokens_can_be_imported), errors, null); + return; + } else if (!GoogleAuthInfo.Export.isSingleBatch(googleAuthExports)) { + Dialogs.showErrorDialog(this, R.string.import_google_auth_failure, getString(R.string.unrelated_google_auth_batches_error)); + return; + } else { + List missingIndices = GoogleAuthInfo.Export.getMissingIndices(googleAuthExports); + if (missingIndices.size() != 0) { + Dialogs.showPartialGoogleAuthImportWarningDialog(this, missingIndices, entries.size(), errors, dialogDismissHandler); + return; + } + } + } catch (IllegalArgumentException e) { + Dialogs.showErrorDialog(this, getString(R.string.import_google_auth_failure), e); + return; + } + } + if ((errors.size() > 0 && results.size() > 1) || errors.size() > 1) { Dialogs.showMultiMessageDialog(this, R.string.import_error_title, getString(R.string.unable_to_read_qrcode_files, uris.size() - errors.size(), uris.size()), errors, dialogDismissHandler); } else if (errors.size() > 0) { diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java b/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java index e7a49f9a..d44b75ec 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java @@ -515,6 +515,46 @@ public class Dialogs { .create()); } + public static void showPartialGoogleAuthImportWarningDialog(Context context, List missingIndexes, int entries, List scanningErrors, DialogInterface.OnClickListener dismissHandler) { + String missingIndexesAsString = missingIndexes.stream() + .map(index -> context.getString(R.string.missing_qr_code_descriptor, index + 1)) + .collect(Collectors.joining("\n")); + + View view = LayoutInflater.from(context).inflate(R.layout.dialog_error, null); + TextView errorDetails = view.findViewById(R.id.error_details); + for (CharSequence error: scanningErrors) { + errorDetails.append(error); + errorDetails.append("\n\n"); + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context) + .setTitle(R.string.partial_google_auth_import) + .setMessage(context.getString(R.string.partial_google_auth_import_warning, missingIndexesAsString)) + .setView(view) + .setCancelable(false) + .setPositiveButton(context.getString(R.string.import_partial_export_anyway, entries), (dialog, which) -> { + dismissHandler.onClick(dialog, which); + }) + .setNegativeButton(android.R.string.cancel, null); + + if (scanningErrors.size() > 0) { + builder.setNeutralButton(R.string.show_error_details, null); + } + + AlertDialog dialog = builder.create(); + dialog.setOnShowListener(d -> { + Button btnNeutral = dialog.getButton(DialogInterface.BUTTON_NEUTRAL); + if (btnNeutral != null) { + btnNeutral.setOnClickListener(b -> { + errorDetails.setVisibility(View.VISIBLE); + btnNeutral.setVisibility(View.GONE); + }); + } + }); + + showSecureDialog(dialog); + } + 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())); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b8be0cea..a1c3ede1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -232,6 +232,13 @@ No cameras available An error occurred while trying to read the QR code Aegis is not compatible with Microsoft\'s proprietary 2FA algorithm. Please make sure to select \"Setup application without notifications\" when configuring 2FA in Office 365. + Incomplete Google Authenticator export detected + Some QR codes are missing from your import. The following codes were not found:\n\n%s\n\nYou may continue importing this partial export but we recommend retrying with all of the QR codes so you don\'t risk losing access to any tokens. + • QR code %d + Import %d tokens anyway + Importing Google Authenticator export failed + Export contains information for an unrelated batch. Try importing 1 batch at a time. + No tokens can be imported as a result Raw Unlocking the vault (this devices) @@ -240,6 +247,7 @@ Remove icon pack Are you sure you want to remove this icon pack? Entries that use icons from this pack will not be affected. Details + Show error details Lock Name No group