From 34550fff35f1c56ba5f9a0a04926fbaa973bb6e2 Mon Sep 17 00:00:00 2001 From: Pedro Ruiz Date: Mon, 22 Jul 2024 22:47:28 +0200 Subject: [PATCH] Add Proton Pass importer --- .../aegis/importers/DatabaseImporter.java | 1 + .../aegis/importers/ProtonPassImporter.java | 155 ++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + .../aegis/importers/DatabaseImporterTest.java | 14 ++ .../aegis/importers/proton_pass.zip | Bin 0 -> 962 bytes 5 files changed, 171 insertions(+) create mode 100644 app/src/main/java/com/beemdevelopment/aegis/importers/ProtonPassImporter.java create mode 100644 app/src/test/resources/com/beemdevelopment/aegis/importers/proton_pass.zip 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 62d846febd..bb95bb0838 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 Pass", ProtonPassImporter.class, R.string.importer_help_proton_pass, true)); _importers.add(new Definition("Steam", SteamImporter.class, R.string.importer_help_steam, true)); _importers.add(new Definition("TOTP Authenticator", TotpAuthenticatorImporter.class, R.string.importer_help_totp_authenticator, true)); _importers.add(new Definition("WinAuth", WinAuthImporter.class, R.string.importer_help_winauth, false)); diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/ProtonPassImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/ProtonPassImporter.java new file mode 100644 index 0000000000..f901c3cd80 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/ProtonPassImporter.java @@ -0,0 +1,155 @@ +package com.beemdevelopment.aegis.importers; + +import android.content.Context; +import android.net.Uri; + +import com.beemdevelopment.aegis.otp.GoogleAuthInfo; +import com.beemdevelopment.aegis.otp.GoogleAuthInfoException; +import com.beemdevelopment.aegis.vault.VaultEntry; +import com.beemdevelopment.aegis.vault.VaultGroup; +import com.topjohnwu.superuser.io.SuFile; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Iterator; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class ProtonPassImporter extends DatabaseImporter { + public ProtonPassImporter(Context context) { + super(context); + } + + @Override + protected SuFile getAppPath() { + throw new UnsupportedOperationException(); + } + + + @Override + protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException { + // Unzip + ZipInputStream zis = new ZipInputStream(stream); + + // Read file from zip + ZipEntry zipEntry; + try { + while((zipEntry = zis.getNextEntry()) != null) + { + if(!zipEntry.getName().equals("Proton Pass/data.json")) + { + continue; + } + + // Read file + BufferedReader br = new BufferedReader(new InputStreamReader(zis)); + StringBuilder json = new StringBuilder(); + String line; + while((line = br.readLine()) != null){ + json.append(line); + } + br.close(); + + // Parse JSON + JSONTokener tokener = new JSONTokener(json.toString()); + JSONObject jsonObject = new JSONObject(tokener); + + return new State(jsonObject); + } + }catch (IOException | JSONException e) + { + throw new DatabaseImporterException(e); + } + + //Json not found + throw new DatabaseImporterException("Invalid proton zip file"); + } + + public static class State extends DatabaseImporter.State { + private JSONObject _jsonObject; + + private State(JSONObject jsonObject) + { + super(false); + _jsonObject = jsonObject; + } + + public Result convert() throws DatabaseImporterException { + Result result = new Result(); + + try { + JSONObject vaults = this._jsonObject.getJSONObject("vaults"); + Iterator keys = vaults.keys(); + + // Iterate over vaults + while (keys.hasNext()) + { + JSONObject vault = vaults.getJSONObject(keys.next()); + JSONArray items = vault.getJSONArray("items"); + + //Create a new group + VaultGroup group = new VaultGroup(vault.getString("name")); + result.addGroup(group); + + // Iterate over items on the vault + for(int j = 0; j < items.length(); j++) + { + JSONObject item = items.getJSONObject(j); + + try{ + VaultEntry entry = this.fromItem(item); + + if(entry == null) + { + continue; + } + + entry.addGroup(group.getUUID()); + result.addEntry(entry); + }catch (JSONException | GoogleAuthInfoException e) + { + result.addError(new DatabaseImporterEntryException(e, "Can't import " + item.getString("itemId"))); + } + } + } + + return result; + }catch (JSONException e) + { + throw new DatabaseImporterException(e); + } + } + + public VaultEntry fromItem(JSONObject item) throws JSONException, GoogleAuthInfoException { + JSONObject data = item.getJSONObject("data"); + JSONObject metadata = data.getJSONObject("metadata"); + JSONObject content = data.getJSONObject("content"); + + //Only login items + if(!data.getString("type").equals("login")) + { + return null; + } + + + String uri = content.getString("totpUri"); + if(uri.isEmpty()) + { + return null; + } + + Uri toptURI = Uri.parse(content.getString("totpUri")); + + GoogleAuthInfo entry = GoogleAuthInfo.parseUri(toptURI); + + return new VaultEntry(entry.getOtpInfo(), metadata.getString("name"), entry.getIssuer()); + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9998e88e6d..70866eac2c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -506,6 +506,7 @@ 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. Supply an andOTP export/backup file. Supply a Bitwarden export/backup file. Encrypted files are not supported. + Supply a Proton pass export/backup zip. Encrypted files are not supported. Supply a copy of /data/data/com.blizzard.messenger/shared_prefs/com.blizzard.messenger.authenticator_preferences.xml, located in the internal storage directory of Battle.net Authenticator. Supply a copy of /data/data/com.duosecurity.duomobile/files/duokit/accounts.json, located in the internal storage directory of DUO. Supply a copy of /data/data/org.fedorahosted.freeotp/shared_prefs/tokens.xml, located in the internal storage directory of FreeOTP (1.x). 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 d376dfe3f3..5e29cae601 100644 --- a/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java +++ b/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java @@ -209,6 +209,12 @@ public void testImportBitwardenCsv() throws IOException, DatabaseImporterExcepti checkImportedBitwardenEntries(entries); } + @Test + public void testImportProtonPassZip() throws DatabaseImporterException, IOException, OtpInfoException { + List entries = importPlain(ProtonPassImporter.class, "proton_pass.zip"); + checkImportedProtonPassEntries(entries); + } + @Test public void testImportFreeOtp() throws IOException, DatabaseImporterException, OtpInfoException { List entries = importPlain(FreeOtpImporter.class, "freeotp.xml"); @@ -441,6 +447,14 @@ private void checkImportedBitwardenEntries(List entries) throws OtpI } } + private void checkImportedProtonPassEntries(List entries) throws OtpInfoException { + for (VaultEntry entry : entries) + { + entry.getGroups().clear(); + checkImportedEntry(entry); + } + } + private void checkImportedEntries(List entries) throws OtpInfoException { for (VaultEntry entry : entries) { checkImportedEntry(entry); diff --git a/app/src/test/resources/com/beemdevelopment/aegis/importers/proton_pass.zip b/app/src/test/resources/com/beemdevelopment/aegis/importers/proton_pass.zip new file mode 100644 index 0000000000000000000000000000000000000000..5aac8fb2cfb2aa2348e197bdc380e945db43582c GIT binary patch literal 962 zcmWIWW@Zs#0D-kVKO(>kD8a*^zz|TBUy`4v5Rh10tREV}%fP-}eqq{9AO_K;72FJr zEH9WD7{EjT*dz`H4iuA~aWXK7qM4MESdyrhRh*v(HxgtV79*dA`S#y35ZL=!JAiMF zM#8>Uk*zgTQp49?(Yq@bRVXnja>J`56W_kki~4v!d$x)I(~4W$vR2P~bf#Kv|Hm`k zpJKG8%ktQ6>=gNRZb1#dPtsMFYueCV^z883W z4dNs<(N3Is!wc|uHX+ky;dw?oq^?Q z4ylFZhFg=SonAWm@b=1G&Hi2orYL>Rec7!Xk*icK@L0QQjdOhd3WJd4U!-~SnHQYN zIi}8`)RsRf{p@4Mz*3eVyQ0L<*`*T&xu@m(x2>7l^<+Vuz0HG;tr|;$V_eIdW_|v` zm9+Jmq|XMsgA@4QoZY`H=$J-~@Jf#_ELz)b_s{IT75>?Cx9MLaskraQ&feU8d-iI6 zdF=yXF#%io?)=R0(6WmSRK1(0>g2fd{WhuPr~kFh4xT=LTadJtZ2RfGDGFD0?E-cd zvun;>Xcg2rSAJ4A(_)3o%UTVSElo1NP3dO1k}h&majuQ$f^&a5qqZ)&{od4L`ThJS z7L4Ct{#ASWWmen8S0%-BI@%uo+dWBcsA_!n$)fbr)8b9>{F(mw%gcL*BY3!Sx+MMxAEC>mKHmHBjvqT>werZnQ-5R z_heR}*Zpm~>o@x}ybIbKsO*;+{fjTao1Np|X%jamCI$vjQVj5BWD;SS&B_Kch6xDw0_pcaPcbk601z*6NB{r; literal 0 HcmV?d00001