Merge pull request #958 from orange-elephant/fix-scan-google-auth-export-from-image

Import Google Authenticator exports by image
This commit is contained in:
Alexander Bakker 2022-08-22 19:06:59 +02:00 committed by GitHub
commit 70ceca6a7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 129 additions and 4 deletions

View file

@ -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<Integer> getMissingIndices(@NonNull List<Export> exports) throws IllegalArgumentException {
if (!isSingleBatch(exports)) {
throw new IllegalArgumentException("Export list contains entries from different batches");
}
List<Integer> indicesMissing = new ArrayList<>();
if (exports.isEmpty()) {
return indicesMissing;
}
Set<Integer> 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<Export> exports) {
if (exports.isEmpty()) {
return true;
}
int batchId = exports.get(0).getBatchId();
for (Export export : exports) {
if (export.getBatchId() != batchId) {
return false;
}
}
return true;
}
}
}

View file

@ -50,6 +50,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;
@ -344,6 +345,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
QrDecodeTask task = new QrDecodeTask(this, (results) -> {
List<CharSequence> errors = new ArrayList<>();
List<VaultEntry> entries = new ArrayList<>();
List<GoogleAuthInfo.Export> googleAuthExports = new ArrayList<>();
for (QrDecodeTask.Result res : results) {
if (res.getException() != null) {
errors.add(buildImportError(res.getFileName(), res.getException()));
@ -351,15 +354,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<Integer> 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) {

View file

@ -515,6 +515,46 @@ public class Dialogs {
.create());
}
public static void showPartialGoogleAuthImportWarningDialog(Context context, List<Integer> missingIndexes, int entries, List<CharSequence> 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()));

View file

@ -233,6 +233,13 @@
<string name="no_cameras_available">No cameras available</string>
<string name="read_qr_error">An error occurred while trying to read the QR code</string>
<string name="read_qr_error_phonefactor">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.</string>
<string name="partial_google_auth_import">Incomplete Google Authenticator export detected</string>
<string name="partial_google_auth_import_warning">Some QR codes are missing from your import. The following codes were not found:\n\n<b>%s</b>\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.</string>
<string name="missing_qr_code_descriptor">• QR code %d</string>
<string name="import_partial_export_anyway">Import %d tokens anyway</string>
<string name="import_google_auth_failure">Importing Google Authenticator export failed</string>
<string name="unrelated_google_auth_batches_error">Export contains information for an unrelated batch. Try importing 1 batch at a time.</string>
<string name="no_tokens_can_be_imported">No tokens can be imported as a result</string>
<string name="authentication_method_raw">Raw</string>
<string name="unlocking_vault">Unlocking the vault</string>
<string name="slot_this_device">(this devices)</string>
@ -241,6 +248,7 @@
<string name="remove_icon_pack">Remove icon pack</string>
<string name="remove_icon_pack_description">Are you sure you want to remove this icon pack? Entries that use icons from this pack will not be affected.</string>
<string name="details">Details</string>
<string name="show_error_details">Show error details</string>
<string name="lock">Lock</string>
<string name="name">Name</string>
<string name="no_group">No group</string>