Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Proton Pass importer #1438

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your importer does not support reading directly from the Proton Pass' internal storage, so the boolean at the end should be false.

_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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrap this in a try-with-resources statement.


// 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();
Comment on lines +52 to +58
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace this block with IOUtils.readAll.


// Parse JSON
JSONTokener tokener = new JSONTokener(json.toString());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can instantiate a JSONObject from the string directly, no need for a JSONTokener.

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");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Unable to find \"Proton Pass/data.json\" in the 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<String> 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To stay a bit more consistent with the other importers, please rename fromItem to convertItem.

Check whether it's a TOTP item here instead of returning null from fromItem. Catch all possible exceptions inside convertItem and wrap them in a DatabaseImporterEntryException there.


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"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GoogleAuthInfo.parseUri has an overload that takes a string.


GoogleAuthInfo entry = GoogleAuthInfo.parseUri(toptURI);

return new VaultEntry(entry.getOtpInfo(), metadata.getString("name"), entry.getIssuer());
}
}
}
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@
<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_andotp">Supply an andOTP export/backup file.</string>
<string name="importer_help_bitwarden">Supply a Bitwarden export/backup file. Encrypted files are not supported.</string>
<string name="importer_help_proton_pass">Supply a Proton pass export/backup zip. Encrypted files are not supported.</string>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Supply a Proton Pass export ZIP containing a JSON file. Encrypted files are not supported."

<string name="importer_help_battle_net_authenticator">Supply a copy of <b>/data/data/com.blizzard.messenger/shared_prefs/com.blizzard.messenger.authenticator_preferences.xml</b>, located in the internal storage directory of Battle.net Authenticator.</string>
<string name="importer_help_duo">Supply a copy of <b>/data/data/com.duosecurity.duomobile/files/duokit/accounts.json</b>, located in the internal storage directory of DUO.</string>
<string name="importer_help_freeotp">Supply a copy of <b>/data/data/org.fedorahosted.freeotp/shared_prefs/tokens.xml</b>, located in the internal storage directory of FreeOTP (1.x).</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@ public void testImportBitwardenCsv() throws IOException, DatabaseImporterExcepti
checkImportedBitwardenEntries(entries);
}

@Test
public void testImportProtonPassZip() throws DatabaseImporterException, IOException, OtpInfoException {
List<VaultEntry> entries = importPlain(ProtonPassImporter.class, "proton_pass.zip");
checkImportedProtonPassEntries(entries);
}

@Test
public void testImportFreeOtp() throws IOException, DatabaseImporterException, OtpInfoException {
List<VaultEntry> entries = importPlain(FreeOtpImporter.class, "freeotp.xml");
Expand Down Expand Up @@ -441,6 +447,14 @@ private void checkImportedBitwardenEntries(List<VaultEntry> entries) throws OtpI
}
}

private void checkImportedProtonPassEntries(List<VaultEntry> entries) throws OtpInfoException {
for (VaultEntry entry : entries)
{
entry.getGroups().clear();
checkImportedEntry(entry);
}
}

private void checkImportedEntries(List<VaultEntry> entries) throws OtpInfoException {
for (VaultEntry entry : entries) {
checkImportedEntry(entry);
Expand Down
Binary file not shown.