diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java index 31e467e6..e8ab4d90 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java @@ -32,6 +32,7 @@ public class AegisImporter extends DatabaseImporter { throw new UnsupportedOperationException(); } + @Override public State read(FileReader reader) throws DatabaseImporterException { try { byte[] bytes = reader.readAll(); diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpImporter.java index 8ef5017a..4635fbb5 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpImporter.java @@ -50,6 +50,7 @@ public class AndOtpImporter extends DatabaseImporter { throw new UnsupportedOperationException(); } + @Override public State read(FileReader reader) throws DatabaseImporterException { byte[] bytes; try { @@ -110,6 +111,7 @@ public class AndOtpImporter extends DatabaseImporter { } } + @Override public void decrypt(Context context, DecryptListener listener) { Dialogs.showPasswordInputDialog(context, password -> { try { diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/AuthyImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/AuthyImporter.java index 25102760..72890ef1 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/AuthyImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/AuthyImporter.java @@ -9,6 +9,7 @@ import com.beemdevelopment.aegis.encoding.Base32Exception; import com.beemdevelopment.aegis.otp.OtpInfo; import com.beemdevelopment.aegis.otp.OtpInfoException; import com.beemdevelopment.aegis.otp.TotpInfo; +import com.beemdevelopment.aegis.util.PreferenceParser; import org.json.JSONArray; import org.json.JSONException; @@ -36,6 +37,7 @@ public class AuthyImporter extends DatabaseImporter { return _subPath; } + @Override public State read(FileReader reader) throws DatabaseImporterException { try { XmlPullParser parser = Xml.newPullParser(); @@ -43,8 +45,14 @@ public class AuthyImporter extends DatabaseImporter { parser.setInput(reader.getStream(), null); parser.nextTag(); - JSONArray entries = parse(parser); - return new State(entries); + JSONArray array = new JSONArray(); + for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) { + if (entry.Name.equals("com.authy.storage.tokens.authenticator.key")) { + array = new JSONArray(entry.Value); + } + } + + return new State(array); } catch (XmlPullParserException | JSONException | IOException e) { throw new DatabaseImporterException(e); } @@ -58,7 +66,6 @@ public class AuthyImporter extends DatabaseImporter { _obj = obj; } - @Override public Result convert() throws DatabaseImporterException { Result result = new Result(); @@ -117,72 +124,6 @@ public class AuthyImporter extends DatabaseImporter { } } - private static JSONArray parse(XmlPullParser parser) - throws IOException, XmlPullParserException, JSONException { - JSONArray entries = new JSONArray(); - - parser.require(XmlPullParser.START_TAG, null, "map"); - while (parser.next() != XmlPullParser.END_TAG) { - if (parser.getEventType() != XmlPullParser.START_TAG) { - continue; - } - - if (!parser.getName().equals("string")) { - skip(parser); - continue; - } - - return new JSONArray(parseEntry(parser).Value); - } - - return entries; - } - - private static XmlEntry parseEntry(XmlPullParser parser) throws IOException, XmlPullParserException { - parser.require(XmlPullParser.START_TAG, null, "string"); - String name = parser.getAttributeValue(null, "name"); - String value = parseText(parser); - parser.require(XmlPullParser.END_TAG, null, "string"); - - XmlEntry entry = new XmlEntry(); - entry.Name = name; - entry.Value = value; - return entry; - } - - private static String parseText(XmlPullParser parser) throws IOException, XmlPullParserException { - String text = ""; - if (parser.next() == XmlPullParser.TEXT) { - text = parser.getText(); - parser.nextTag(); - } - return text; - } - - private static void skip(XmlPullParser parser) throws IOException, XmlPullParserException { - // source: https://developer.android.com/training/basics/network-ops/xml.html - if (parser.getEventType() != XmlPullParser.START_TAG) { - throw new IllegalStateException(); - } - - int depth = 1; - while (depth != 0) { - switch (parser.next()) { - case XmlPullParser.END_TAG: - depth--; - break; - case XmlPullParser.START_TAG: - depth++; - break; - } - } - } - - private static class XmlEntry { - String Name; - String Value; - } - private static class AuthyEntryInfo { String OriginalName; String AccountType; 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 daa92003..9af5154b 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java @@ -34,12 +34,14 @@ public abstract class DatabaseImporter { _importers.put("Authy", AuthyImporter.class); _importers.put("andOTP", AndOtpImporter.class); _importers.put("FreeOTP", FreeOtpImporter.class); + _importers.put("FreeOTP+", FreeOtpPlusImporter.class); _importers.put("Google Authenticator", GoogleAuthImporter.class); _importers.put("Steam", SteamImporter.class); _appImporters = new LinkedHashMap<>(); _appImporters.put("Authy", AuthyImporter.class); _appImporters.put("FreeOTP", FreeOtpImporter.class); + _appImporters.put("FreeOTP+", FreeOtpPlusImporter.class); _appImporters.put("Google Authenticator", GoogleAuthImporter.class); _appImporters.put("Steam", SteamImporter.class); } @@ -133,43 +135,35 @@ public abstract class DatabaseImporter { } } - public static class FileReader implements Closeable { + public static class FileReader { private InputStream _stream; + private boolean _internal; - private FileReader(InputStream stream) { + public FileReader(InputStream stream) { + this(stream, false); + } + + public FileReader(InputStream stream, boolean internal) { _stream = stream; - } - - public static FileReader open(String filename) - throws FileNotFoundException { - FileInputStream stream = new FileInputStream(filename); - return new FileReader(stream); - } - - public static FileReader open(SuFile file) - throws FileNotFoundException { - SuFileInputStream stream = new SuFileInputStream(file); - return new FileReader(stream); - } - - public static FileReader open(Context context, Uri uri) - throws FileNotFoundException { - InputStream stream = context.getContentResolver().openInputStream(uri); - return new FileReader(stream); + _internal = internal; } public byte[] readAll() throws IOException { - ByteInputStream stream = ByteInputStream.create(_stream); - return stream.getBytes(); + try (ByteInputStream stream = ByteInputStream.create(_stream)) { + return stream.getBytes(); + } } public InputStream getStream() { return _stream; } - @Override - public void close() throws IOException { - _stream.close(); + /** + * Reports whether this reader reads the internal state of an app. + * @return true if reading from internal file, false if reading from external file + */ + public boolean isInternal() { + return _internal; } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/FreeOtpImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/FreeOtpImporter.java index a27aed5c..982d66aa 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/FreeOtpImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/FreeOtpImporter.java @@ -7,7 +7,9 @@ import com.beemdevelopment.aegis.db.DatabaseEntry; import com.beemdevelopment.aegis.otp.HotpInfo; import com.beemdevelopment.aegis.otp.OtpInfo; import com.beemdevelopment.aegis.otp.OtpInfoException; +import com.beemdevelopment.aegis.otp.SteamInfo; import com.beemdevelopment.aegis.otp.TotpInfo; +import com.beemdevelopment.aegis.util.PreferenceParser; import org.json.JSONArray; import org.json.JSONException; @@ -37,6 +39,7 @@ public class FreeOtpImporter extends DatabaseImporter { return _subPath; } + @Override public State read(FileReader reader) throws DatabaseImporterException { try { XmlPullParser parser = Xml.newPullParser(); @@ -44,17 +47,22 @@ public class FreeOtpImporter extends DatabaseImporter { parser.setInput(reader.getStream(), null); parser.nextTag(); - List entries = parse(parser); + List entries = new ArrayList<>(); + for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) { + if (!entry.Name.equals("tokenOrder")) { + entries.add(new JSONObject(entry.Value)); + } + } return new State(entries); - } catch (XmlPullParserException | IOException e) { + } catch (XmlPullParserException | IOException | JSONException e) { throw new DatabaseImporterException(e); } } public static class State extends DatabaseImporter.State { - private List _entries; + private List _entries; - private State(List entries) { + public State(List entries) { super(false); _entries = entries; } @@ -63,34 +71,37 @@ public class FreeOtpImporter extends DatabaseImporter { public Result convert() { Result result = new Result(); - for (XmlEntry xmlEntry : _entries) { - // TODO: order - if (!xmlEntry.Name.equals("tokenOrder")) { - try { - DatabaseEntry entry = convertEntry(xmlEntry); - result.addEntry(entry); - } catch (DatabaseImporterEntryException e) { - result.addError(e); - } + for (JSONObject obj : _entries) { + try { + DatabaseEntry entry = convertEntry(obj); + result.addEntry(entry); + } catch (DatabaseImporterEntryException e) { + result.addError(e); } } return result; } - private static DatabaseEntry convertEntry(XmlEntry entry) throws DatabaseImporterEntryException { + private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException { try { - JSONObject obj = new JSONObject(entry.Value); - String type = obj.getString("type").toLowerCase(); String algo = obj.getString("algo"); int digits = obj.getInt("digits"); byte[] secret = toBytes(obj.getJSONArray("secret")); + String issuer = obj.getString("issuerExt"); + String name = obj.optString("label"); + OtpInfo info; switch (type) { case "totp": - info = new TotpInfo(secret, algo, digits, obj.getInt("period")); + int period = obj.getInt("period"); + if (issuer.equals("Steam")) { + info = new SteamInfo(secret, algo, digits, period); + } else { + info = new TotpInfo(secret, algo, digits, period); + } break; case "hotp": info = new HotpInfo(secret, algo, digits, obj.getLong("counter")); @@ -99,18 +110,16 @@ public class FreeOtpImporter extends DatabaseImporter { throw new DatabaseImporterException("unsupported otp type: " + type); } - String issuer = obj.getString("issuerExt"); - String name = obj.optString("label"); return new DatabaseEntry(info, name, issuer); } catch (DatabaseImporterException | OtpInfoException | JSONException e) { - throw new DatabaseImporterEntryException(e, entry.Value); + throw new DatabaseImporterEntryException(e, obj.toString()); } } } - private static List parse(XmlPullParser parser) - throws IOException, XmlPullParserException { - List entries = new ArrayList<>(); + private static List parseXml(XmlPullParser parser) + throws IOException, XmlPullParserException, JSONException { + List entries = new ArrayList<>(); parser.require(XmlPullParser.START_TAG, null, "map"); while (parser.next() != XmlPullParser.END_TAG) { @@ -123,7 +132,10 @@ public class FreeOtpImporter extends DatabaseImporter { continue; } - entries.add(parseEntry(parser)); + JSONObject entry = parseXmlEntry(parser); + if (entry != null) { + entries.add(entry); + } } return entries; @@ -137,19 +149,21 @@ public class FreeOtpImporter extends DatabaseImporter { return bytes; } - private static XmlEntry parseEntry(XmlPullParser parser) throws IOException, XmlPullParserException { + private static JSONObject parseXmlEntry(XmlPullParser parser) + throws IOException, XmlPullParserException, JSONException { parser.require(XmlPullParser.START_TAG, null, "string"); String name = parser.getAttributeValue(null, "name"); - String value = parseText(parser); + String value = parseXmlText(parser); parser.require(XmlPullParser.END_TAG, null, "string"); - XmlEntry entry = new XmlEntry(); - entry.Name = name; - entry.Value = value; - return entry; + if (name.equals("tokenOrder")) { + return null; + } + + return new JSONObject(value); } - private static String parseText(XmlPullParser parser) throws IOException, XmlPullParserException { + private static String parseXmlText(XmlPullParser parser) throws IOException, XmlPullParserException { String text = ""; if (parser.next() == XmlPullParser.TEXT) { text = parser.getText(); @@ -176,9 +190,4 @@ public class FreeOtpImporter extends DatabaseImporter { } } } - - private static class XmlEntry { - String Name; - String Value; - } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/FreeOtpPlusImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/FreeOtpPlusImporter.java new file mode 100644 index 00000000..00a46ba6 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/FreeOtpPlusImporter.java @@ -0,0 +1,57 @@ +package com.beemdevelopment.aegis.importers; + +import android.content.Context; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +public class FreeOtpPlusImporter extends DatabaseImporter { + private static final String _subPath = "shared_prefs/tokens.xml"; + private static final String _pkgName = "org.liberty.android.freeotpplus"; + + public FreeOtpPlusImporter(Context context) { + super(context); + } + + @Override + protected String getAppPkgName() { + return _pkgName; + } + + @Override + protected String getAppSubPath() { + return _subPath; + } + + @Override + public State read(FileReader reader) throws DatabaseImporterException { + State state; + + if (reader.isInternal()) { + state = new FreeOtpImporter(getContext()).read(reader); + } else { + try { + String json = new String(reader.readAll(), StandardCharsets.UTF_8); + JSONObject obj = new JSONObject(json); + JSONArray array = obj.getJSONArray("tokens"); + + List entries = new ArrayList<>(); + for (int i = 0; i < array.length(); i++) { + entries.add(array.getJSONObject(i)); + } + + state = new FreeOtpImporter.State(entries); + } catch (IOException | JSONException e) { + throw new DatabaseImporterException(e); + } + } + + return state; + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/GoogleAuthImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/GoogleAuthImporter.java index a0b320d6..ffea66ee 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/GoogleAuthImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/GoogleAuthImporter.java @@ -43,6 +43,7 @@ public class GoogleAuthImporter extends DatabaseImporter { return _subPath; } + @Override public State read(FileReader reader) throws DatabaseImporterException { File file; diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/SteamImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/SteamImporter.java index 2e8f10cc..86f719de 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/SteamImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/SteamImporter.java @@ -43,6 +43,7 @@ public class SteamImporter extends DatabaseImporter { return new SuFile(_subDir, files[0].getName()).getPath(); } + @Override public State read(FileReader reader) throws DatabaseImporterException { try (ByteInputStream stream = ByteInputStream.create(reader.getStream())) { JSONObject obj = new JSONObject(new String(stream.getBytes(), StandardCharsets.UTF_8)); diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java index f816696c..235fa284 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java @@ -41,9 +41,11 @@ import com.beemdevelopment.aegis.ui.preferences.SwitchPreference; import com.takisoft.preferencex.PreferenceFragmentCompat; import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.io.SuFile; +import com.topjohnwu.superuser.io.SuFileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -439,7 +441,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat { } SuFile file = importer.getAppPath(); - try (DatabaseImporter.FileReader reader = DatabaseImporter.FileReader.open(file)) { + try (SuFileInputStream stream = new SuFileInputStream(file)) { + DatabaseImporter.FileReader reader = new DatabaseImporter.FileReader(stream, true); importDatabase(importer, reader); } } catch (PackageManager.NameNotFoundException e) { @@ -511,8 +514,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat { return; } - try (DatabaseImporter.FileReader reader = DatabaseImporter.FileReader.open(getContext(), uri)) { + try (InputStream stream = getContext().getContentResolver().openInputStream(uri)) { DatabaseImporter importer = DatabaseImporter.create(getContext(), _importerType); + DatabaseImporter.FileReader reader = new DatabaseImporter.FileReader(stream); importDatabase(importer, reader); } catch (FileNotFoundException e) { Toast.makeText(getActivity(), R.string.file_not_found, Toast.LENGTH_SHORT).show(); diff --git a/app/src/main/java/com/beemdevelopment/aegis/util/PreferenceParser.java b/app/src/main/java/com/beemdevelopment/aegis/util/PreferenceParser.java new file mode 100644 index 00000000..e43fd570 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/util/PreferenceParser.java @@ -0,0 +1,79 @@ +package com.beemdevelopment.aegis.util; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class PreferenceParser { + private PreferenceParser() { + + } + + public static List parse(XmlPullParser parser) throws IOException, XmlPullParserException { + List entries = new ArrayList<>(); + + parser.require(XmlPullParser.START_TAG, null, "map"); + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + + if (!parser.getName().equals("string")) { + skip(parser); + continue; + } + + entries.add(parseEntry(parser)); + } + + return entries; + } + + private static XmlEntry parseEntry(XmlPullParser parser) throws IOException, XmlPullParserException { + parser.require(XmlPullParser.START_TAG, null, "string"); + String name = parser.getAttributeValue(null, "name"); + String value = parseText(parser); + parser.require(XmlPullParser.END_TAG, null, "string"); + + XmlEntry entry = new XmlEntry(); + entry.Name = name; + entry.Value = value; + return entry; + } + + private static String parseText(XmlPullParser parser) throws IOException, XmlPullParserException { + String text = ""; + if (parser.next() == XmlPullParser.TEXT) { + text = parser.getText(); + parser.nextTag(); + } + return text; + } + + private static void skip(XmlPullParser parser) throws IOException, XmlPullParserException { + // source: https://developer.android.com/training/basics/network-ops/xml.html + if (parser.getEventType() != XmlPullParser.START_TAG) { + throw new IllegalStateException(); + } + + int depth = 1; + while (depth != 0) { + switch (parser.next()) { + case XmlPullParser.END_TAG: + depth--; + break; + case XmlPullParser.START_TAG: + depth++; + break; + } + } + } + + public static class XmlEntry { + public String Name; + public String Value; + } +}