Merge pull request #106 from alexbakker/feature-import-freeotp+

Add support for importing from FreeOTP+
This commit is contained in:
Michael Schättgen 2019-08-03 17:26:30 +02:00 committed by GitHub
commit 6769fefd00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 221 additions and 132 deletions

View file

@ -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();

View file

@ -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 {

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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<XmlEntry> entries = parse(parser);
List<JSONObject> 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<XmlEntry> _entries;
private List<JSONObject> _entries;
private State(List<XmlEntry> entries) {
public State(List<JSONObject> 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<XmlEntry> parse(XmlPullParser parser)
throws IOException, XmlPullParserException {
List<XmlEntry> entries = new ArrayList<>();
private static List<JSONObject> parseXml(XmlPullParser parser)
throws IOException, XmlPullParserException, JSONException {
List<JSONObject> 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;
}
}

View file

@ -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<JSONObject> 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;
}
}

View file

@ -43,6 +43,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
File file;

View file

@ -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));

View file

@ -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();

View file

@ -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<XmlEntry> parse(XmlPullParser parser) throws IOException, XmlPullParserException {
List<XmlEntry> 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;
}
}