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 fe1b5b5f..6c9bc6fe 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java @@ -44,6 +44,7 @@ public abstract class DatabaseImporter { _importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true)); _importers.add(new Definition("Microsoft Authenticator", MicrosoftAuthImporter.class, R.string.importer_help_microsoft_authenticator, true)); _importers.add(new Definition("Plain text", GoogleAuthUriImporter.class, R.string.importer_help_plain_text, false)); + _importers.add(new Definition("Proton Authenticator", ProtonAuthenticatorImporter.class, R.string.importer_help_proton_authenticator, false)); _importers.add(new Definition("Steam", SteamImporter.class, R.string.importer_help_steam, true)); _importers.add(new Definition("Stratum (Authenticator Pro)", StratumImporter.class, R.string.importer_help_stratum, true)); _importers.add(new Definition("TOTP Authenticator", TotpAuthenticatorImporter.class, R.string.importer_help_totp_authenticator, true)); diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/ProtonAuthenticatorImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/ProtonAuthenticatorImporter.java new file mode 100644 index 00000000..2ac34db4 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/ProtonAuthenticatorImporter.java @@ -0,0 +1,96 @@ +package com.beemdevelopment.aegis.importers; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.NonNull; + +import com.beemdevelopment.aegis.otp.GoogleAuthInfo; +import com.beemdevelopment.aegis.otp.GoogleAuthInfoException; +import com.beemdevelopment.aegis.otp.OtpInfo; +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; + +public class ProtonAuthenticatorImporter extends DatabaseImporter { + + public ProtonAuthenticatorImporter(Context context) { + super(context); + } + + @Override + protected SuFile getAppPath() { + throw new UnsupportedOperationException(); + } + + @Override + protected @NonNull State read(@NonNull InputStream stream, boolean isInternal) throws DatabaseImporterException { + try { + String contents = new String(IOUtils.readAll(stream), UTF_8); + JSONObject json = new JSONObject(contents); + + return new DecryptedState(json); + } catch (JSONException | IOException e) { + throw new DatabaseImporterException(e); + } + } + + public static class DecryptedState extends DatabaseImporter.State { + private final JSONObject _json; + + public DecryptedState(@NonNull JSONObject json) { + super(false); + _json = json; + } + + @Override + public @NonNull Result convert() throws DatabaseImporterException { + Result result = new Result(); + + try { + JSONArray entries = _json.getJSONArray("entries"); + for (int i = 0; i < entries.length(); i++) { + JSONObject entry = entries.getJSONObject(i); + try { + result.addEntry(convertEntry(entry)); + } catch (DatabaseImporterEntryException e) { + result.addError(e); + } + } + } catch (JSONException e) { + throw new DatabaseImporterException(e); + } + + return result; + } + + private static @NonNull VaultEntry convertEntry(@NonNull JSONObject entry) throws DatabaseImporterEntryException { + try { + JSONObject content = entry.getJSONObject("content"); + String name = content.getString("name"); + String uriString = content.getString("uri"); + + Uri uri = Uri.parse(uriString); + try { + GoogleAuthInfo info = GoogleAuthInfo.parseUri(uri); + OtpInfo otp = info.getOtpInfo(); + + return new VaultEntry(otp, name, info.getIssuer()); + } catch (GoogleAuthInfoException e) { + throw new DatabaseImporterEntryException(e, uriString); + } + } catch (JSONException e) { + throw new DatabaseImporterEntryException(e, entry.toString()); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4fe617bd..9014aa46 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -565,6 +565,7 @@ Only database files from Google Authenticator v5.10 and prior are supported.\n\nSupply a copy of /data/data/com.google.android.apps.authenticator2/databases/databases, located in the internal storage directory of Google Authenticator. Supply a copy of /data/data/com.azure.authenticator/databases/PhoneFactor, located in the internal storage directory of Microsoft Authenticator. Supply a plain text file with a Google Authenticator URI on each line. + Supply a Proton Authenticator export file (.json) obtained through Settings -> Export. Steam v3.0 and newer are not supported. Supply a copy of /data/data/com.valvesoftware.android.steam.community/files/Steamguard-*.json, located in the internal storage directory of Steam. Supply a Stratum export file obtained through Settings -> Back up -> Back up to encrypted file (recommended). Supply a TOTP Authenticator export file. 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 1457d118..d1b7beb6 100644 --- a/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java +++ b/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java @@ -390,6 +390,12 @@ public class DatabaseImporterTest { checkImportedEntries(entries); } + @Test + public void testImportProtonAuthenticator() throws IOException, DatabaseImporterException, OtpInfoException { + List entries = importPlain(ProtonAuthenticatorImporter.class, "proton_authenticator.json"); + checkImportedEntries(entries); + } + 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/proton_authenticator.json b/app/src/test/resources/com/beemdevelopment/aegis/importers/proton_authenticator.json new file mode 100644 index 00000000..d7897990 --- /dev/null +++ b/app/src/test/resources/com/beemdevelopment/aegis/importers/proton_authenticator.json @@ -0,0 +1,32 @@ +{ + "version": 1, + "entries": [ + { + "id": "a19e0019-47e3-4d3b-af63-6dad0cb49e31", + "content": { + "uri": "otpauth://totp/Deno:Mason?secret=4SJHB4GSD43FZBAI7C2HLRJGPQ&issuer=Deno&algorithm=SHA1&digits=6&period=30", + "entry_type": "Totp", + "name": "Mason" + }, + "note": null + }, + { + "id": "4603c576-5d93-4c7f-8c26-bed6dc4493fc", + "content": { + "uri": "otpauth://totp/SPDX:James?secret=5OM4WOOGPLQEF6UGN3CPEOOLWU&issuer=SPDX&algorithm=SHA256&digits=7&period=20", + "entry_type": "Totp", + "name": "James" + }, + "note": null + }, + { + "id": "4fae25ae-16ed-4f15-a887-3bbb50b34d5e", + "content": { + "uri": "otpauth://totp/Airbnb:Elijah?secret=7ELGJSGXNCCTV3O6LKJWYFV2RA&issuer=Airbnb&algorithm=SHA512&digits=8&period=50", + "entry_type": "Totp", + "name": "Elijah" + }, + "note": null + } + ] +} \ No newline at end of file