Merge pull request #73 from alexbakker/refactor-import

Refactor database importer logic
This commit is contained in:
Alexander Bakker 2019-05-01 14:34:18 +02:00 committed by GitHub
commit 83b2f85ff4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 721 additions and 604 deletions

View file

@ -1,84 +0,0 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseFile;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.util.ByteInputStream;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class AegisFileImporter extends DatabaseFileImporter {
private DatabaseFileCredentials _creds;
private DatabaseFile _file;
public AegisFileImporter(Context context, ByteInputStream stream) {
super(context, stream);
}
@Override
public void parse() throws DatabaseImporterException {
try {
byte[] bytes = _stream.getBytes();
_file = DatabaseFile.fromBytes(bytes);
} catch (DatabaseFileException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public DatabaseImporterResult convert() throws DatabaseImporterException {
DatabaseImporterResult result = new DatabaseImporterResult();
try {
JSONObject obj;
if (_file.isEncrypted() && _creds != null) {
obj = _file.getContent(_creds);
} else {
obj = _file.getContent();
}
JSONArray array = obj.getJSONArray("entries");
for (int i = 0; i < array.length(); i++) {
JSONObject entryObj = array.getJSONObject(i);
try {
DatabaseEntry entry = convertEntry(entryObj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (JSONException | DatabaseFileException e) {
throw new DatabaseImporterException(e);
}
return result;
}
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
return DatabaseEntry.fromJson(obj);
} catch (JSONException | OtpInfoException | Base64Exception e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
@Override
public boolean isEncrypted() {
return _file.isEncrypted();
}
public void setCredentials(DatabaseFileCredentials creds) {
_creds = creds;
}
public DatabaseFile getFile() {
return _file;
}
}

View file

@ -0,0 +1,109 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseFile;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
public class AegisImporter extends DatabaseImporter {
public AegisImporter(Context context) {
super(context);
}
@Override
protected String getAppPkgName() {
throw new UnsupportedOperationException();
}
@Override
protected String getAppSubPath() {
throw new UnsupportedOperationException();
}
public State read(FileReader reader) throws DatabaseImporterException {
try {
byte[] bytes = reader.readAll();
DatabaseFile file = DatabaseFile.fromBytes(bytes);
if (file.isEncrypted()) {
return new EncryptedState(file);
}
return new DecryptedState(file.getContent());
} catch (DatabaseFileException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class EncryptedState extends State {
private DatabaseFile _file;
private EncryptedState(DatabaseFile file) {
super(true);
_file = file;
}
public SlotList getSlots() {
return _file.getHeader().getSlots();
}
public State decrypt(DatabaseFileCredentials creds) throws DatabaseFileException {
JSONObject obj = _file.getContent(creds);
return new DecryptedState(obj);
}
@Override
public void decrypt(Context context, DecryptListener listener) {
}
}
public static class DecryptedState extends State {
private JSONObject _obj;
private DecryptedState(JSONObject obj) {
super(false);
_obj = obj;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = new Result();
try {
JSONArray array = _obj.getJSONArray("entries");
for (int i = 0; i < array.length(); i++) {
JSONObject entryObj = array.getJSONObject(i);
try {
DatabaseEntry entry = convertEntry(entryObj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return result;
}
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
return DatabaseEntry.fromJson(obj);
} catch (JSONException | OtpInfoException | Base64Exception e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}

View file

@ -1,99 +0,0 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
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.ByteInputStream;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
public class AndOtpFileImporter extends DatabaseFileImporter {
private JSONArray _obj;
public AndOtpFileImporter(Context context, ByteInputStream stream) {
super(context, stream);
}
@Override
public void parse() throws DatabaseImporterException {
try {
_obj = new JSONArray(new String(_stream.getBytes(), StandardCharsets.UTF_8));
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public DatabaseImporterResult convert() throws DatabaseImporterException {
DatabaseImporterResult result = new DatabaseImporterResult();
for (int i = 0; i < _obj.length(); i++) {
try {
JSONObject obj = _obj.getJSONObject(i);
DatabaseEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (JSONException e) {
throw new DatabaseImporterException(e);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algorithm");
int digits = obj.getInt("digits");
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
OtpInfo info;
switch (type) {
case "hotp":
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
break;
case "totp":
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
break;
case "steam":
info = new SteamInfo(secret, algo, digits, obj.optInt("period", 30));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String name;
String issuer = "";
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
} else {
name = parts[0];
}
return new DatabaseEntry(info, name, issuer);
} catch (DatabaseImporterException | Base32Exception | OtpInfoException | JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
@Override
public boolean isEncrypted() {
return false;
}
}

View file

@ -0,0 +1,114 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
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 org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class AndOtpImporter extends DatabaseImporter {
public AndOtpImporter(Context context) {
super(context);
}
@Override
protected String getAppPkgName() {
throw new UnsupportedOperationException();
}
@Override
protected String getAppSubPath() {
throw new UnsupportedOperationException();
}
public State read(FileReader reader) throws DatabaseImporterException {
byte[] bytes;
try {
bytes = reader.readAll();
JSONArray array = new JSONArray(new String(bytes, StandardCharsets.UTF_8));
return new State(array);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private JSONArray _obj;
private State(JSONArray obj) {
super(false);
_obj = obj;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = new Result();
for (int i = 0; i < _obj.length(); i++) {
try {
JSONObject obj = _obj.getJSONObject(i);
DatabaseEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (JSONException e) {
throw new DatabaseImporterException(e);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algorithm");
int digits = obj.getInt("digits");
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
OtpInfo info;
switch (type) {
case "hotp":
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
break;
case "totp":
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
break;
case "steam":
info = new SteamInfo(secret, algo, digits, obj.optInt("period", 30));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String name;
String issuer = "";
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
} else {
name = parts[0];
}
return new DatabaseEntry(info, name, issuer);
} catch (DatabaseImporterException | Base32Exception | OtpInfoException | JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}

View file

@ -1,63 +0,0 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import com.topjohnwu.superuser.io.SuFile;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
public abstract class DatabaseAppImporter implements DatabaseImporter {
private static Map<String, Class<? extends DatabaseAppImporter>> _importers;
static {
// note: keep this list sorted alphabetically
LinkedHashMap<String, Class<? extends DatabaseAppImporter>> importers = new LinkedHashMap<>();
importers.put("Google Authenticator", GoogleAuthAppImporter.class);
importers.put("Steam", SteamAppImporter.class);
_importers = Collections.unmodifiableMap(importers);
}
private SuFile _path;
private Context _context;
protected DatabaseAppImporter(Context context, String pkgName, String subPath) throws DatabaseImporterException {
_context = context;
try {
PackageManager man = context.getPackageManager();
_path = new SuFile(man.getApplicationInfo(pkgName, 0).dataDir, subPath);
} catch (PackageManager.NameNotFoundException e) {
throw new DatabaseImporterException(e);
}
}
protected SuFile getPath() {
return _path;
}
public abstract void parse() throws DatabaseImporterException;
public abstract DatabaseImporterResult convert() throws DatabaseImporterException;
public abstract boolean isEncrypted();
public Context getContext() {
return _context;
}
public static DatabaseAppImporter create(Context context, Class<? extends DatabaseAppImporter> type) {
try {
return type.getConstructor(Context.class).newInstance(context);
} catch (IllegalAccessException | InstantiationException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public static Map<String, Class<? extends DatabaseAppImporter>> getImporters() {
return _importers;
}
}

View file

@ -1,53 +0,0 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.util.ByteInputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
public abstract class DatabaseFileImporter implements DatabaseImporter {
private static Map<String, Class<? extends DatabaseFileImporter>> _importers;
static {
// note: keep this list sorted alphabetically
LinkedHashMap<String, Class<? extends DatabaseFileImporter>> importers = new LinkedHashMap<>();
importers.put("Aegis", AegisFileImporter.class);
importers.put("andOTP", AndOtpFileImporter.class);
importers.put("FreeOTP", FreeOtpFileImporter.class);
_importers = Collections.unmodifiableMap(importers);
}
private Context _context;
protected ByteInputStream _stream;
protected DatabaseFileImporter(Context context, ByteInputStream stream) {
_context = context;
_stream = stream;
}
public abstract void parse() throws DatabaseImporterException;
public abstract DatabaseImporterResult convert() throws DatabaseImporterException;
public abstract boolean isEncrypted();
public Context getContext() {
return _context;
}
public static DatabaseFileImporter create(Context context, ByteInputStream stream, Class<? extends DatabaseFileImporter> type) {
try {
return type.getConstructor(Context.class, ByteInputStream.class).newInstance(context, stream);
} catch (IllegalAccessException | InstantiationException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public static Map<String, Class<? extends DatabaseFileImporter>> getImporters() {
return _importers;
}
}

View file

@ -1,10 +1,178 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.Uri;
public interface DatabaseImporter {
void parse() throws DatabaseImporterException;
DatabaseImporterResult convert() throws DatabaseImporterException;
boolean isEncrypted();
Context getContext();
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.util.ByteInputStream;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.Closeable;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public abstract class DatabaseImporter {
private Context _context;
private static Map<String, Class<? extends DatabaseImporter>> _importers;
private static Map<String, Class<? extends DatabaseImporter>> _appImporters;
static {
// note: keep these lists sorted alphabetically
_importers = new LinkedHashMap<>();
_importers.put("Aegis", AegisImporter.class);
_importers.put("andOTP", AndOtpImporter.class);
_importers.put("FreeOTP", FreeOtpImporter.class);
_importers.put("Google Authenticator", GoogleAuthImporter.class);
_importers.put("Steam", SteamImporter.class);
_appImporters = new LinkedHashMap<>();
_appImporters.put("FreeOTP", FreeOtpImporter.class);
_appImporters.put("Google Authenticator", GoogleAuthImporter.class);
_appImporters.put("Steam", SteamImporter.class);
}
public DatabaseImporter(Context context) {
_context = context;
}
protected Context getContext() {
return _context;
}
public SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
return getAppPath(getAppPkgName(), getAppSubPath());
}
protected SuFile getAppPath(String pkgName, String subPath) throws PackageManager.NameNotFoundException {
PackageManager man = getContext().getPackageManager();
return new SuFile(man.getApplicationInfo(pkgName, 0).dataDir, subPath);
}
protected abstract String getAppPkgName();
protected abstract String getAppSubPath() throws DatabaseImporterException, PackageManager.NameNotFoundException;
public abstract State read(FileReader reader) throws DatabaseImporterException;
public static DatabaseImporter create(Context context, Class<? extends DatabaseImporter> type) {
try {
return type.getConstructor(Context.class).newInstance(context);
} catch (IllegalAccessException | InstantiationException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public static Map<String, Class<? extends DatabaseImporter>> getImporters() {
return Collections.unmodifiableMap(_importers);
}
public static Map<String, Class<? extends DatabaseImporter>> getAppImporters() {
return Collections.unmodifiableMap(_appImporters);
}
public static abstract class State {
private boolean _encrypted;
public State(boolean encrypted) {
_encrypted = encrypted;
}
public boolean isEncrypted() {
return _encrypted;
}
public void decrypt(Context context, DecryptListener listener) throws DatabaseImporterException {
if (!_encrypted) {
throw new RuntimeException("Attempted to decrypt a plain text database");
}
throw new UnsupportedOperationException();
}
public Result convert() throws DatabaseImporterException {
if (_encrypted) {
throw new RuntimeException("Attempted to convert database before decrypting it");
}
throw new UnsupportedOperationException();
}
}
public static class Result {
private List<DatabaseEntry> _entries = new ArrayList<>();
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
public void addEntry(DatabaseEntry entry) {
_entries.add(entry);
}
public void addError(DatabaseImporterEntryException error) {
_errors.add(error);
}
public List<DatabaseEntry> getEntries() {
return _entries;
}
public List<DatabaseImporterEntryException> getErrors() {
return _errors;
}
}
public static class FileReader implements Closeable {
private InputStream _stream;
private FileReader(InputStream stream) {
_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);
}
public byte[] readAll() throws IOException {
ByteInputStream stream = ByteInputStream.create(_stream);
return stream.getBytes();
}
public InputStream getStream() {
return _stream;
}
@Override
public void close() throws IOException {
_stream.close();
}
}
public interface DecryptListener {
void onStateDecrypted(State state);
void onError(Exception e);
}
}

View file

@ -1,27 +0,0 @@
package com.beemdevelopment.aegis.importers;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import java.util.ArrayList;
import java.util.List;
public class DatabaseImporterResult {
private List<DatabaseEntry> _entries = new ArrayList<>();
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
public void addEntry(DatabaseEntry entry) {
_entries.add(entry);
}
public void addError(DatabaseImporterEntryException error) {
_errors.add(error);
}
public List<DatabaseEntry> getEntries() {
return _entries;
}
public List<DatabaseImporterEntryException> getErrors() {
return _errors;
}
}

View file

@ -8,7 +8,6 @@ import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.ByteInputStream;
import org.json.JSONArray;
import org.json.JSONException;
@ -20,81 +19,95 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class FreeOtpFileImporter extends DatabaseFileImporter {
private List<XmlEntry> _xmlEntries;
public class FreeOtpImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/tokens.xml";
private static final String _pkgName = "org.fedorahosted.freeotp";
public FreeOtpFileImporter(Context context, ByteInputStream stream) {
super(context, stream);
}
private static class XmlEntry {
String Name;
String Value;
public FreeOtpImporter(Context context) {
super(context);
}
@Override
public void parse() throws DatabaseImporterException {
protected String getAppPkgName() {
return _pkgName;
}
@Override
protected String getAppSubPath() {
return _subPath;
}
public State read(FileReader reader) throws DatabaseImporterException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(_stream, null);
parser.setInput(reader.getStream(), null);
parser.nextTag();
_xmlEntries = parse(parser);
List<XmlEntry> entries = parse(parser);
return new State(entries);
} catch (XmlPullParserException | IOException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public DatabaseImporterResult convert() {
DatabaseImporterResult result = new DatabaseImporterResult();
public static class State extends DatabaseImporter.State {
private List<XmlEntry> _entries;
for (XmlEntry xmlEntry : _xmlEntries) {
// TODO: order
if (!xmlEntry.Name.equals("tokenOrder")) {
try {
DatabaseEntry entry = convertEntry(xmlEntry);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
private State(List<XmlEntry> entries) {
super(false);
_entries = entries;
}
@Override
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);
}
}
}
return result;
}
return result;
}
private static DatabaseEntry convertEntry(XmlEntry entry) throws DatabaseImporterEntryException {
try {
JSONObject obj = new JSONObject(entry.Value);
private static DatabaseEntry convertEntry(XmlEntry xmlEntry) throws DatabaseImporterEntryException {
try {
JSONObject obj = new JSONObject(xmlEntry.Value);
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
byte[] secret = toBytes(obj.getJSONArray("secret"));
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
byte[] secret = toBytes(obj.getJSONArray("secret"));
OtpInfo info;
switch (type) {
case "totp":
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
break;
case "hotp":
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
}
OtpInfo info;
if (type.equals("totp")) {
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
} else if (type.equals("hotp")) {
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
} else {
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);
}
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, xmlEntry.Value);
}
}
@Override
public boolean isEncrypted() {
return false;
}
private static List<XmlEntry> parse(XmlPullParser parser)
throws IOException, XmlPullParserException {
List<XmlEntry> entries = new ArrayList<>();
@ -163,4 +176,9 @@ public class FreeOtpFileImporter extends DatabaseFileImporter {
}
}
}
private static class XmlEntry {
String Name;
String Value;
}
}

View file

@ -13,7 +13,6 @@ import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.topjohnwu.superuser.ShellUtils;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.File;
import java.io.FileOutputStream;
@ -23,30 +22,35 @@ import java.util.List;
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
public class GoogleAuthAppImporter extends DatabaseAppImporter {
public class GoogleAuthImporter extends DatabaseImporter {
private static final int TYPE_TOTP = 0;
private static final int TYPE_HOTP = 1;
private static final String _subPath = "databases/databases";
private static final String _pkgName = "com.google.android.apps.authenticator2";
private List<Entry> _entries = new ArrayList<>();
public GoogleAuthAppImporter(Context context) throws DatabaseImporterException {
super(context, _pkgName, _subPath);
public GoogleAuthImporter(Context context) {
super(context);
}
@Override
public void parse() throws DatabaseImporterException {
protected String getAppPkgName() {
return _pkgName;
}
@Override
protected String getAppSubPath() {
return _subPath;
}
public State read(FileReader reader) throws DatabaseImporterException {
File file;
try {
// create a temporary copy of the database so that SQLiteDatabase can open it
file = File.createTempFile("google-import-", "", getContext().getCacheDir());
try (SuFileInputStream in = new SuFileInputStream(getPath())) {
try (FileOutputStream out = new FileOutputStream(file)) {
ShellUtils.pump(in, out);
}
try (FileOutputStream out = new FileOutputStream(file)) {
ShellUtils.pump(reader.getStream(), out);
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
@ -54,14 +58,16 @@ public class GoogleAuthAppImporter extends DatabaseAppImporter {
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY, null)) {
try (Cursor cursor = db.rawQuery("SELECT * FROM accounts", null)) {
if (!cursor.moveToFirst()) {
return;
List<Entry> entries = new ArrayList<>();
if (cursor.moveToFirst()) {
do {
Entry entry = new Entry(cursor);
entries.add(entry);
} while(cursor.moveToNext());
}
do {
Entry entry = new Entry(cursor);
_entries.add(entry);
} while(cursor.moveToNext());
return new State(entries);
}
} catch (SQLiteException e) {
throw new DatabaseImporterException(e);
@ -71,53 +77,57 @@ public class GoogleAuthAppImporter extends DatabaseAppImporter {
}
}
@Override
public DatabaseImporterResult convert() {
DatabaseImporterResult result = new DatabaseImporterResult();
public static class State extends DatabaseImporter.State {
private List<Entry> _entries;
for (Entry sqlEntry : _entries) {
private State(List<Entry> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
for (Entry sqlEntry : _entries) {
try {
DatabaseEntry entry = convertEntry(sqlEntry);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static DatabaseEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
try {
DatabaseEntry entry = convertEntry(sqlEntry);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
byte[] secret = Base32.decode(entry.getSecret().toCharArray());
OtpInfo info;
switch (entry.getType()) {
case TYPE_TOTP:
info = new TotpInfo(secret);
break;
case TYPE_HOTP:
info = new HotpInfo(secret, entry.getCounter());
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + entry.getType());
}
String name = entry.getEmail();
String[] parts = name.split(":");
if (parts.length == 2) {
name = parts[1];
}
return new DatabaseEntry(info, name, entry.getIssuer());
} catch (Base32Exception | OtpInfoException | DatabaseImporterException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
return result;
}
private static DatabaseEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
try {
byte[] secret = Base32.decode(entry.getSecret().toCharArray());
OtpInfo info;
switch (entry.getType()) {
case TYPE_TOTP:
info = new TotpInfo(secret);
break;
case TYPE_HOTP:
info = new HotpInfo(secret, entry.getCounter());
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + entry.getType());
}
String name = entry.getEmail();
String[] parts = name.split(":");
if (parts.length == 2) {
name = parts[1];
}
return new DatabaseEntry(info, name, entry.getIssuer());
} catch (Base32Exception | OtpInfoException | DatabaseImporterException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
@Override
public boolean isEncrypted() {
return false;
}
private static String getString(Cursor cursor, String columnName) {

View file

@ -1,83 +0,0 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.util.ByteInputStream;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
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 SteamAppImporter extends DatabaseAppImporter {
private static final String _subDir = "files";
private static final String _pkgName = "com.valvesoftware.android.steam.community";
private List<JSONObject> _objs = new ArrayList<>();
public SteamAppImporter(Context context) throws DatabaseImporterException {
super(context, _pkgName, _subDir);
}
@Override
public void parse() throws DatabaseImporterException {
SuFile[] files = getPath().listFiles((d, name) -> name.startsWith("Steamguard-"));
if (files == null || files.length == 0) {
throw new DatabaseImporterException(String.format("Empty directory: %s", getPath().getAbsolutePath()));
}
for (SuFile file : files) {
try (SuFileInputStream in = new SuFileInputStream(file)) {
try (ByteInputStream stream = ByteInputStream.create(in)) {
JSONObject obj = new JSONObject(new String(stream.getBytes(), StandardCharsets.UTF_8));
_objs.add(obj);
}
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
}
@Override
public DatabaseImporterResult convert() {
DatabaseImporterResult result = new DatabaseImporterResult();
for (JSONObject obj : _objs) {
try {
DatabaseEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
public DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException{
try {
byte[] secret = Base64.decode(obj.getString("shared_secret"));
SteamInfo info = new SteamInfo(secret);
String account = obj.getString("account_name");
return new DatabaseEntry(info, account, "Steam");
} catch (JSONException | Base64Exception | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
@Override
public boolean isEncrypted() {
return false;
}
}

View file

@ -0,0 +1,89 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.util.ByteInputStream;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class SteamImporter extends DatabaseImporter {
private static final String _subDir = "files";
private static final String _pkgName = "com.valvesoftware.android.steam.community";
public SteamImporter(Context context) {
super(context);
}
@Override
protected String getAppPkgName() {
return _pkgName;
}
@Override
protected String getAppSubPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
// NOTE: this assumes that a global root shell has already been obtained by the caller
SuFile path = getAppPath(getAppPkgName(), _subDir);
SuFile[] files = path.listFiles((d, name) -> name.startsWith("Steamguard-"));
if (files == null || files.length == 0) {
throw new DatabaseImporterException(String.format("Empty directory: %s", path.getAbsolutePath()));
}
// TODO: handle multiple files (can this even occur?)
return new SuFile(_subDir, files[0].getName()).getPath();
}
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));
return new State(obj);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private JSONObject _obj;
private State(JSONObject obj) {
super(false);
_obj = obj;
}
@Override
public Result convert() {
Result result = new Result();
try {
DatabaseEntry entry = convertEntry(_obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
return result;
}
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
byte[] secret = Base64.decode(obj.getString("shared_secret"));
SteamInfo info = new SteamInfo(secret);
String account = obj.getString("account_name");
return new DatabaseEntry(info, account, "Steam");
} catch (JSONException | Base64Exception | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}

View file

@ -4,6 +4,7 @@ import android.Manifest;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Bundle;
@ -20,6 +21,7 @@ import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.db.DatabaseManagerException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
@ -29,25 +31,22 @@ import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.importers.AegisFileImporter;
import com.beemdevelopment.aegis.importers.DatabaseAppImporter;
import com.beemdevelopment.aegis.importers.DatabaseFileImporter;
import com.beemdevelopment.aegis.importers.AegisImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.importers.DatabaseImporterResult;
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
import com.beemdevelopment.aegis.util.ByteInputStream;
import com.takisoft.preferencex.PreferenceFragmentCompat;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.Cipher;
@ -70,10 +69,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
private Intent _result;
private DatabaseManager _db;
// this is used to keep a reference to a database converter
// while the user provides credentials to decrypt it
private DatabaseFileImporter _importer;
private Class<? extends DatabaseFileImporter> _importerType;
// keep a reference to the type of database converter the user selected
private Class<? extends DatabaseImporter> _importerType;
private AegisImporter.State _importerState;
private SwitchPreference _encryptionPreference;
private SwitchPreference _fingerprintPreference;
@ -377,6 +375,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
break;
case CODE_GROUPS:
onGroupManagerResult(resultCode, data);
break;
case CODE_SELECT_ENTRIES:
onSelectEntriesResult(resultCode, data);
break;
@ -397,8 +396,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return;
}
Map<String, Class<? extends DatabaseFileImporter>> importers = DatabaseFileImporter.getImporters();
String[] names = importers.keySet().toArray(new String[importers.size()]);
Map<String, Class<? extends DatabaseImporter>> importers = DatabaseImporter.getImporters();
String[] names = importers.keySet().toArray(new String[0]);
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(getString(R.string.choose_application))
@ -417,61 +416,95 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
private void onImportApp() {
Map<String, Class<? extends DatabaseAppImporter>> importers = DatabaseAppImporter.getImporters();
String[] names = importers.keySet().toArray(new String[importers.size()]);
Map<String, Class<? extends DatabaseImporter>> importers = DatabaseImporter.getAppImporters();
String[] names = importers.keySet().toArray(new String[0]);
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(getString(R.string.choose_application))
.setSingleChoiceItems(names, 0, null)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
try {
DatabaseAppImporter importer;
// obtain the global root shell and close it immediately after we're done
// TODO: find a way to use SuFileInputStream with Shell.newInstance()
try (Shell shell = Shell.getShell()) {
if (!shell.isRoot()) {
Toast.makeText(getActivity(), getString(R.string.root_error), Toast.LENGTH_SHORT).show();
return;
}
importer = DatabaseAppImporter.create(getContext(), importers.get(names[i]));
importer.parse();
} catch (IOException e) {
Toast.makeText(getActivity(), getString(R.string.root_error), Toast.LENGTH_SHORT).show();
return;
}
importDatabase(importer);
} catch (DatabaseImporterException e) {
e.printStackTrace();
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show();
}
}
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
Class<? extends DatabaseImporter> importerType = Objects.requireNonNull(importers.get(names[i]));
DatabaseImporter importer = DatabaseImporter.create(getContext(), importerType);
importApp(importer);
})
.create());
}
private void importApp(DatabaseImporter importer) {
// obtain the global root shell and close it immediately after we're done
// TODO: find a way to use SuFileInputStream with Shell.newInstance()
try (Shell shell = Shell.getShell()) {
if (!shell.isRoot()) {
Toast.makeText(getActivity(), R.string.root_error, Toast.LENGTH_SHORT).show();
return;
}
SuFile file = importer.getAppPath();
try (DatabaseImporter.FileReader reader = DatabaseImporter.FileReader.open(file)) {
importDatabase(importer, reader);
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
Toast.makeText(getActivity(), R.string.app_lookup_error, Toast.LENGTH_SHORT).show();
} catch (IOException | DatabaseImporterException e) {
e.printStackTrace();
Toast.makeText(getActivity(), getString(R.string.reading_file_error), Toast.LENGTH_SHORT).show();
}
}
private void importDatabase(DatabaseImporter importer, DatabaseImporter.FileReader reader) {
try {
DatabaseImporter.State state = importer.read(reader);
if (state.isEncrypted()) {
// temporary special case for encrypted Aegis databases
if (state instanceof AegisImporter.EncryptedState) {
_importerState = state;
Intent intent = new Intent(getActivity(), AuthActivity.class);
intent.putExtra("slots", ((AegisImporter.EncryptedState) state).getSlots());
startActivityForResult(intent, CODE_IMPORT_DECRYPT);
} else {
state.decrypt(getActivity(), new DatabaseImporter.DecryptListener() {
@Override
public void onStateDecrypted(DatabaseImporter.State state) {
importDatabase(state);
}
@Override
public void onError(Exception e) {
Toast.makeText(getActivity(), R.string.decryption_error, Toast.LENGTH_SHORT).show();
}
});
}
} else {
importDatabase(state);
}
} catch (DatabaseImporterException e) {
e.printStackTrace();
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show();
}
}
private void onImportDecryptResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
_importer = null;
_importerState = null;
return;
}
DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds");
((AegisFileImporter)_importer).setCredentials(creds);
DatabaseImporter.State state;
try {
importDatabase(_importer);
} catch (DatabaseImporterException e) {
state = ((AegisImporter.EncryptedState) _importerState).decrypt(creds);
} catch (DatabaseFileException e) {
e.printStackTrace();
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show();
Toast.makeText(getActivity(), R.string.decryption_error, Toast.LENGTH_SHORT).show();
return;
}
_importer = null;
importDatabase(state);
_importerState = null;
}
private void onImportResult(int resultCode, Intent data) {
@ -480,44 +513,28 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return;
}
ByteInputStream stream;
try (InputStream fileStream = getActivity().getContentResolver().openInputStream(uri)) {
stream = ByteInputStream.create(fileStream);
try (DatabaseImporter.FileReader reader = DatabaseImporter.FileReader.open(getContext(), uri)) {
DatabaseImporter importer = DatabaseImporter.create(getContext(), _importerType);
importDatabase(importer, reader);
} catch (FileNotFoundException e) {
Toast.makeText(getActivity(), getString(R.string.file_not_found), Toast.LENGTH_SHORT).show();
return;
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(getActivity(), getString(R.string.reading_file_error), Toast.LENGTH_SHORT).show();
return;
}
try {
DatabaseFileImporter importer = DatabaseFileImporter.create(getContext(), stream, _importerType);
importer.parse();
// special case to decrypt encrypted aegis databases
if (importer.isEncrypted() && importer instanceof AegisFileImporter) {
_importer = importer;
Intent intent = new Intent(getActivity(), AuthActivity.class);
intent.putExtra("slots", ((AegisFileImporter)_importer).getFile().getHeader().getSlots());
startActivityForResult(intent, CODE_IMPORT_DECRYPT);
return;
}
importDatabase(importer);
} catch (DatabaseImporterException e) {
e.printStackTrace();
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show();
}
}
private void importDatabase(DatabaseImporter importer) throws DatabaseImporterException {
DatabaseImporterResult result = importer.convert();
private void importDatabase(DatabaseImporter.State state) {
DatabaseImporter.Result result;
try {
result = state.convert();
} catch (DatabaseImporterException e) {
e.printStackTrace();
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
Toast.makeText(getContext(), msg, Toast.LENGTH_LONG).show();
return;
}
List<DatabaseEntry> entries = result.getEntries();
List<DatabaseImporterEntryException> errors = result.getErrors();

View file

@ -113,6 +113,7 @@
<string name="parsing_file_error">An error occurred while trying to parse the file</string>
<string name="file_not_found">Error: File not found</string>
<string name="reading_file_error">An error occurred while trying to read the file</string>
<string name="app_lookup_error">Error: App is not installed</string>
<string name="root_error">Error: unable to obtain root access</string>
<string name="imported_entries_count">Imported %d entries</string>
<string name="read_entries_count">Read %d entries. %d errors.</string>