mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-04-23 23:39:14 +00:00
Add support from importing from 2FAS Authenticator
This commit is contained in:
parent
2ccfcd62e1
commit
fddc29880a
8 changed files with 211 additions and 9 deletions
|
@ -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
|
- Lots of ways to add new entries
|
||||||
- Scan a QR code or an image of one
|
- Scan a QR code or an image of one
|
||||||
- Enter details manually
|
- Enter details manually
|
||||||
- Import from other authenticator apps: Authenticator Plus, Authy, andOTP,
|
- Import from other authenticator apps: 2FAS Authenticator, Authenticator
|
||||||
FreeOTP, FreeOTP+, Google Authenticator, Microsoft Authenticator, Plain
|
Plus, Authy, andOTP, FreeOTP, FreeOTP+, Google Authenticator, Microsoft
|
||||||
text, Steam, TOTP Authenticator and WinAuth (root access is required for
|
Authenticator, Plain text, Steam, TOTP Authenticator and WinAuth (root
|
||||||
some of these)
|
access is required for some of these)
|
||||||
- Organization
|
- Organization
|
||||||
- Alphabetic/custom sorting
|
- Alphabetic/custom sorting
|
||||||
- Custom or automatically generated icons
|
- Custom or automatically generated icons
|
||||||
|
|
|
@ -27,6 +27,7 @@ public abstract class DatabaseImporter {
|
||||||
static {
|
static {
|
||||||
// note: keep these lists sorted alphabetically
|
// note: keep these lists sorted alphabetically
|
||||||
_importers = new ArrayList<>();
|
_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("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("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
|
||||||
_importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false));
|
_importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false));
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -46,6 +46,7 @@ import com.nulabinc.zxcvbn.Zxcvbn;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
|
|
||||||
|
@ -399,14 +400,15 @@ public class Dialogs {
|
||||||
|
|
||||||
public static void showImportersDialog(Context context, boolean isDirect, ImporterListener listener) {
|
public static void showImportersDialog(Context context, boolean isDirect, ImporterListener listener) {
|
||||||
List<DatabaseImporter.Definition> importers = DatabaseImporter.getImporters(isDirect);
|
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);
|
View view = LayoutInflater.from(context).inflate(R.layout.dialog_importers, null);
|
||||||
TextView helpText = view.findViewById(R.id.text_importer_help);
|
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 listView = view.findViewById(R.id.list_importers);
|
||||||
listView.setAdapter(new ArrayAdapter<>(context, R.layout.card_importer, names));
|
listView.setAdapter(new ArrayAdapter<>(context, R.layout.card_importer, names));
|
||||||
listView.setItemChecked(0, true);
|
listView.setItemChecked(i, true);
|
||||||
listView.setOnItemClickListener((parent, view1, position, id) -> {
|
listView.setOnItemClickListener((parent, view1, position, id) -> {
|
||||||
setImporterHelpText(helpText, importers.get(position), isDirect);
|
setImporterHelpText(helpText, importers.get(position), isDirect);
|
||||||
});
|
});
|
||||||
|
|
|
@ -374,6 +374,7 @@
|
||||||
<string name="pref_panic_trigger_title">Delete vault on panic trigger</string>
|
<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="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_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 & Restore -> Export as Text and HTML</b>.</string>
|
<string name="importer_help_authenticator_plus">Supply an Authenticator Plus export file obtained through <b>Settings -> Backup & 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>
|
<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>
|
||||||
|
|
|
@ -40,7 +40,7 @@ public class DatabaseImporterTest {
|
||||||
/**
|
/**
|
||||||
* The procedure for adding new importer tests is as follows:
|
* The procedure for adding new importer tests is as follows:
|
||||||
* 1. Generate QR codes for each test vector:
|
* 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
|
* 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.
|
* 3. Create an export and add the file to the importers resource directory.
|
||||||
* 4. Add a new test for it here.
|
* 4. Add a new test for it here.
|
||||||
|
@ -219,6 +219,17 @@ public class DatabaseImporterTest {
|
||||||
checkImportedEntries(entries);
|
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)
|
private List<VaultEntry> importPlain(Class<? extends DatabaseImporter> type, String resName)
|
||||||
throws IOException, DatabaseImporterException {
|
throws IOException, DatabaseImporterException {
|
||||||
return importPlain(type, resName, false);
|
return importPlain(type, resName, false);
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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.
|
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>
|
<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>
|
<b>Feature overview</b>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
Loading…
Add table
Reference in a new issue