From fddc29880ae398f292d3feab954a07e0296068c5 Mon Sep 17 00:00:00 2001 From: Alexander Bakker Date: Sat, 17 Apr 2021 12:50:40 +0200 Subject: [PATCH] Add support from importing from 2FAS Authenticator --- README.md | 8 +- .../aegis/importers/DatabaseImporter.java | 1 + .../aegis/importers/TwoFASImporter.java | 94 +++++++++++++++++++ .../aegis/ui/dialogs/Dialogs.java | 8 +- app/src/main/res/values/strings.xml | 1 + .../aegis/importers/DatabaseImporterTest.java | 13 ++- .../aegis/importers/2fas_authenticator.json | 93 ++++++++++++++++++ metadata/en-US/full_description.txt | 2 +- 8 files changed, 211 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/beemdevelopment/aegis/importers/TwoFASImporter.java create mode 100644 app/src/test/resources/com/beemdevelopment/aegis/importers/2fas_authenticator.json diff --git a/README.md b/README.md index a4dd6525..a7b0c784 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,10 @@ The security design of the app and the vault format is described in detail in - Lots of ways to add new entries - Scan a QR code or an image of one - Enter details manually - - Import from other authenticator apps: Authenticator Plus, Authy, andOTP, - FreeOTP, FreeOTP+, Google Authenticator, Microsoft Authenticator, Plain - text, Steam, TOTP Authenticator and WinAuth (root access is required for - some of these) + - Import from other authenticator apps: 2FAS Authenticator, Authenticator + Plus, Authy, andOTP, FreeOTP, FreeOTP+, Google Authenticator, Microsoft + Authenticator, Plain text, Steam, TOTP Authenticator and WinAuth (root + access is required for some of these) - Organization - Alphabetic/custom sorting - Custom or automatically generated icons 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 f3f58864..2e5fa4b4 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java @@ -27,6 +27,7 @@ public abstract class DatabaseImporter { static { // note: keep these lists sorted alphabetically _importers = new ArrayList<>(); + _importers.add(new Definition("2FAS Authenticator", TwoFASImporter.class, R.string.importer_help_2fas, false)); _importers.add(new Definition("Aegis", AegisImporter.class, R.string.importer_help_aegis, false)); _importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false)); _importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false)); diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/TwoFASImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/TwoFASImporter.java new file mode 100644 index 00000000..6a76474a --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/TwoFASImporter.java @@ -0,0 +1,94 @@ +package com.beemdevelopment.aegis.importers; + +import android.content.Context; + +import com.beemdevelopment.aegis.encoding.Base32; +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.util.IOUtils; +import com.beemdevelopment.aegis.vault.VaultEntry; +import com.topjohnwu.superuser.io.SuFile; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +public class TwoFASImporter extends DatabaseImporter { + public TwoFASImporter(Context context) { + super(context); + } + + @Override + protected SuFile getAppPath() { + throw new UnsupportedOperationException(); + } + + @Override + public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException { + try { + String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8); + JSONObject obj = new JSONObject(json); + int version = obj.getInt("schemaVersion"); + if (version > 1) { + throw new DatabaseImporterException(String.format("Unsupported schema version: %d", version)); + } + + JSONArray array = obj.getJSONArray("services"); + List entries = new ArrayList<>(); + for (int i = 0; i < array.length(); i++) { + entries.add(array.getJSONObject(i)); + } + + return new TwoFASImporter.State(entries); + } catch (IOException | JSONException e) { + throw new DatabaseImporterException(e); + } + } + + public static class State extends DatabaseImporter.State { + private final List _entries; + + public State(List entries) { + super(false); + _entries = entries; + } + + @Override + public Result convert() { + Result result = new Result(); + + for (JSONObject obj : _entries) { + try { + VaultEntry entry = convertEntry(obj); + result.addEntry(entry); + } catch (DatabaseImporterEntryException e) { + result.addError(e); + } + } + + return result; + } + + private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException { + try { + byte[] secret = Base32.decode(obj.getString("secret")); + JSONObject info = obj.getJSONObject("otp"); + String issuer = info.getString("issuer"); + String name = info.optString("account"); + + OtpInfo otp = new TotpInfo(secret); + return new VaultEntry(otp, name, issuer); + } catch (OtpInfoException | JSONException | EncodingException e) { + throw new DatabaseImporterEntryException(e, obj.toString()); + } + } + } +} 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 fe0e522f..44c76128 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 @@ -46,6 +46,7 @@ import com.nulabinc.zxcvbn.Zxcvbn; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; import javax.crypto.Cipher; @@ -399,14 +400,15 @@ public class Dialogs { 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); + List names = importers.stream().map(DatabaseImporter.Definition::getName).collect(Collectors.toList()); + int i = names.indexOf(context.getString(R.string.app_name)); 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); + setImporterHelpText(helpText, importers.get(i), isDirect); ListView listView = view.findViewById(R.id.list_importers); listView.setAdapter(new ArrayAdapter<>(context, R.layout.card_importer, names)); - listView.setItemChecked(0, true); + listView.setItemChecked(i, true); listView.setOnItemClickListener((parent, view1, position, id) -> { setImporterHelpText(helpText, importers.get(position), isDirect); }); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d60dd024..3aa65e0f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -374,6 +374,7 @@ Delete vault on panic trigger Delete vault when a panic trigger is received from Ripple + Supply a 2FAS Authenticator backup file. 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. 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 1fe31057..581b29f7 100644 --- a/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java +++ b/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java @@ -40,7 +40,7 @@ public class DatabaseImporterTest { /** * The procedure for adding new importer tests is as follows: * 1. Generate QR codes for each test vector: - * -> while read line; do (qrencode "$line" -o - | feh -); done < ./app/src/test/resources/com/beemdevelopment/aegis/importers/plain + * -> while read line; do (qrencode "$line" -o - | feh -); done < ./app/src/test/resources/com/beemdevelopment/aegis/importers/plain.txt * 2. Scan the QR codes with the app we want to test our import functionality of * 3. Create an export and add the file to the importers resource directory. * 4. Add a new test for it here. @@ -219,6 +219,17 @@ public class DatabaseImporterTest { checkImportedEntries(entries); } + @Test + public void testImportTwoFASAuthenticator() throws DatabaseImporterException, IOException, OtpInfoException { + List entries = importPlain(TwoFASImporter.class, "2fas_authenticator.json"); + for (VaultEntry entry : entries) { + // 2FAS Authenticator doesn't support HOTP, different hash algorithms, periods or digits, so fix those up here + VaultEntry entryVector = getEntryVectorBySecret(entry.getInfo().getSecret()); + entryVector.setInfo(new TotpInfo(entryVector.getInfo().getSecret())); + checkImportedEntry(entryVector, entry); + } + } + private List importPlain(Class type, String resName) throws IOException, DatabaseImporterException { return importPlain(type, resName, false); diff --git a/app/src/test/resources/com/beemdevelopment/aegis/importers/2fas_authenticator.json b/app/src/test/resources/com/beemdevelopment/aegis/importers/2fas_authenticator.json new file mode 100644 index 00000000..eb4cd07d --- /dev/null +++ b/app/src/test/resources/com/beemdevelopment/aegis/importers/2fas_authenticator.json @@ -0,0 +1,93 @@ +{ + "appOrigin": "android", + "appVersionCode": 300101, + "appVersionName": "3.1.1", + "schemaVersion": 1, + "services": [ + { + "name": "Deno", + "order": { + "position": 0 + }, + "otp": { + "account": "Mason", + "issuer": "Deno", + "label": "Deno:Mason" + }, + "secret": "4SJHB4GSD43FZBAI7C2HLRJGPQ", + "type": "Unknown", + "updatedAt": 1618417205843 + }, + { + "name": "SPDX", + "order": { + "position": 1 + }, + "otp": { + "account": "James", + "issuer": "SPDX", + "label": "SPDX:James" + }, + "secret": "5OM4WOOGPLQEF6UGN3CPEOOLWU", + "type": "Unknown", + "updatedAt": 1618417216085 + }, + { + "name": "Airbnb", + "order": { + "position": 2 + }, + "otp": { + "account": "Elijah", + "issuer": "Airbnb", + "label": "Airbnb:Elijah" + }, + "secret": "7ELGJSGXNCCTV3O6LKJWYFV2RA", + "type": "Unknown", + "updatedAt": 1618417225267 + }, + { + "name": "Issuu", + "order": { + "position": 3 + }, + "otp": { + "account": "James", + "issuer": "Issuu", + "label": "Issuu:James" + }, + "secret": "YOOMIXWS5GN6RTBPUFFWKTW5M4", + "type": "Unknown", + "updatedAt": 1618417234252 + }, + { + "name": "Air Canada", + "order": { + "position": 4 + }, + "otp": { + "account": "Benjamin", + "issuer": "Air Canada", + "label": "Air Canada:Benjamin" + }, + "secret": "KUVJJOM753IHTNDSZVCNKL7GII", + "type": "Unknown", + "updatedAt": 1618417242537 + }, + { + "name": "WWE", + "order": { + "position": 5 + }, + "otp": { + "account": "Mason", + "issuer": "WWE", + "label": "WWE:Mason" + }, + "secret": "5VAML3X35THCEBVRLV24CGBKOY", + "type": "Unknown", + "updatedAt": 1618417253862 + } + ], + "updatedAt": 1618417377507 +} \ No newline at end of file diff --git a/metadata/en-US/full_description.txt b/metadata/en-US/full_description.txt index 41e2166a..99bf5cf8 100644 --- a/metadata/en-US/full_description.txt +++ b/metadata/en-US/full_description.txt @@ -13,7 +13,7 @@ Over time, you'll likely accumulate tens of entries in your vault. Aegis Authent To make sure you will never lose access to your online accounts, Aegis Authenticator can create automatic backups of the vault to a location of your choosing. If your cloud provider supports the Storage Access Framework of Android (like Nextcloud does), it can even create automatic backups to the cloud. Creating manual exports of the vault is also supported. Making the switch -To make the switch easier, Aegis Authenticator can import the entries of lots of other authenticators, including: Authenticator Plus, Authy, andOTP, FreeOTP, FreeOTP+, Google Authenticator, Microsoft Authenticator, Steam, TOTP Authenticator and WinAuth (root access is required for the apps that don't have an option to export). +To make the switch easier, Aegis Authenticator can import the entries of lots of other authenticators, including: 2FAS Authenticator, Authenticator Plus, Authy, andOTP, FreeOTP, FreeOTP+, Google Authenticator, Microsoft Authenticator, Steam, TOTP Authenticator and WinAuth (root access is required for the apps that don't have an option to export). Feature overview