Add support from importing from 2FAS Authenticator

This commit is contained in:
Alexander Bakker 2021-04-17 12:50:40 +02:00
parent 2ccfcd62e1
commit fddc29880a
8 changed files with 211 additions and 9 deletions

View File

@ -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

View File

@ -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));

View File

@ -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<JSONObject> 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<JSONObject> _entries;
public State(List<JSONObject> 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());
}
}
}
}

View File

@ -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<DatabaseImporter.Definition> importers = DatabaseImporter.getImporters(isDirect);
String[] names = importers.stream().map(DatabaseImporter.Definition::getName).toArray(String[]::new);
List<String> 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);
});

View File

@ -374,6 +374,7 @@
<string name="pref_panic_trigger_title">Delete vault on panic trigger</string>
<string name="pref_panic_trigger_summary">Delete vault when a panic trigger is received from Ripple</string>
<string name="importer_help_2fas">Supply a 2FAS Authenticator backup file.</string>
<string name="importer_help_aegis">Supply an Aegis export/backup file.</string>
<string name="importer_help_authenticator_plus">Supply an Authenticator Plus export file obtained through <b>Settings -> Backup &amp; Restore -> Export as Text and HTML</b>.</string>
<string name="importer_help_authy">Supply a copy of <b>/data/data/com.authy.authy/shared_prefs/com.authy.storage.tokens.authenticator.xml</b>, located in the internal storage directory of Authy.</string>

View File

@ -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<VaultEntry> 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<VaultEntry> importPlain(Class<? extends DatabaseImporter> type, String resName)
throws IOException, DatabaseImporterException {
return importPlain(type, resName, false);

View File

@ -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
}

View File

@ -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.
<b>Making the switch</b>
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).
<b>Feature overview</b>
<ul>