mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-04-22 14:59:14 +00:00
Merge pull request #106 from alexbakker/feature-import-freeotp+
Add support for importing from FreeOTP+
This commit is contained in:
commit
6769fefd00
10 changed files with 221 additions and 132 deletions
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -43,6 +43,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
|||
return _subPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
File 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));
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue