Add support for importing encrypted Aegis databases

Close #6
This commit is contained in:
Alexander Bakker 2018-05-10 18:42:47 +02:00
parent 7f09eb5535
commit 6770ccd3b1
5 changed files with 141 additions and 47 deletions

View file

@ -1,7 +1,10 @@
package me.impy.aegis.importers; package me.impy.aegis.importers;
import org.json.JSONObject;
import java.util.List; import java.util.List;
import me.impy.aegis.crypto.MasterKey;
import me.impy.aegis.db.Database; import me.impy.aegis.db.Database;
import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.db.DatabaseException; import me.impy.aegis.db.DatabaseException;
@ -10,25 +13,55 @@ import me.impy.aegis.db.DatabaseFileException;
import me.impy.aegis.util.ByteInputStream; import me.impy.aegis.util.ByteInputStream;
public class AegisImporter extends DatabaseImporter { public class AegisImporter extends DatabaseImporter {
private MasterKey _key;
private DatabaseFile _file;
public AegisImporter(ByteInputStream stream) { public AegisImporter(ByteInputStream stream) {
super(stream); super(stream);
} }
@Override @Override
public List<DatabaseEntry> convert() throws DatabaseImporterException { public void parse() throws DatabaseImporterException {
try { try {
byte[] bytes = _stream.getBytes(); byte[] bytes = _stream.getBytes();
DatabaseFile file = new DatabaseFile(); _file = new DatabaseFile();
file.deserialize(bytes); _file.deserialize(bytes);
Database db = new Database(); } catch (DatabaseFileException e) {
db.deserialize(file.getContent());
return db.getKeys();
} catch (DatabaseFileException | DatabaseException e) {
throw new DatabaseImporterException(e); throw new DatabaseImporterException(e);
} }
} }
@Override
public List<DatabaseEntry> convert() throws DatabaseImporterException {
try {
JSONObject obj;
if (!_file.isEncrypted()) {
obj = _file.getContent();
} else {
obj = _file.getContent(_key);
}
Database db = new Database();
db.deserialize(obj);
return db.getKeys();
} catch (DatabaseException | DatabaseFileException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public boolean isEncrypted() {
return _file.isEncrypted();
}
public void setKey(MasterKey key) {
_key = key;
}
public DatabaseFile getFile() {
return _file;
}
@Override @Override
public String getName() { public String getName() {
return "Aegis"; return "Aegis";

View file

@ -20,8 +20,12 @@ public abstract class DatabaseImporter {
_stream = stream; _stream = stream;
} }
public abstract void parse() throws DatabaseImporterException;
public abstract List<DatabaseEntry> convert() throws DatabaseImporterException; public abstract List<DatabaseEntry> convert() throws DatabaseImporterException;
public abstract boolean isEncrypted();
public abstract String getName(); public abstract String getName();
public static DatabaseImporter create(ByteInputStream stream, Class<? extends DatabaseImporter> type) { public static DatabaseImporter create(ByteInputStream stream, Class<? extends DatabaseImporter> type) {

View file

@ -27,6 +27,11 @@ public class FreeOTPImporter extends DatabaseImporter {
String Value; String Value;
} }
@Override
public void parse() throws DatabaseImporterException {
}
@Override @Override
public List<DatabaseEntry> convert() throws DatabaseImporterException { public List<DatabaseEntry> convert() throws DatabaseImporterException {
try { try {
@ -40,6 +45,11 @@ public class FreeOTPImporter extends DatabaseImporter {
} }
} }
@Override
public boolean isEncrypted() {
return false;
}
@Override @Override
public String getName() { public String getName() {
return "FreeOTP"; return "FreeOTP";

View file

@ -3,13 +3,13 @@ package me.impy.aegis.ui;
import android.Manifest; import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.preference.EditTextPreference; import android.preference.EditTextPreference;
import android.preference.Preference; import android.preference.Preference;
import android.preference.PreferenceFragment; import android.preference.PreferenceFragment;
import android.widget.Toast; import android.widget.Toast;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -18,10 +18,12 @@ import java.util.Locale;
import me.impy.aegis.AegisApplication; import me.impy.aegis.AegisApplication;
import me.impy.aegis.R; import me.impy.aegis.R;
import me.impy.aegis.crypto.MasterKey;
import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.db.DatabaseManager; import me.impy.aegis.db.DatabaseManager;
import me.impy.aegis.db.DatabaseManagerException; import me.impy.aegis.db.DatabaseManagerException;
import me.impy.aegis.helpers.PermissionHelper; import me.impy.aegis.helpers.PermissionHelper;
import me.impy.aegis.importers.AegisImporter;
import me.impy.aegis.importers.DatabaseImporter; import me.impy.aegis.importers.DatabaseImporter;
import me.impy.aegis.importers.DatabaseImporterException; import me.impy.aegis.importers.DatabaseImporterException;
import me.impy.aegis.util.ByteInputStream; import me.impy.aegis.util.ByteInputStream;
@ -29,6 +31,7 @@ import me.impy.aegis.util.ByteInputStream;
public class PreferencesFragment extends PreferenceFragment { public class PreferencesFragment extends PreferenceFragment {
// activity request codes // activity request codes
private static final int CODE_IMPORT = 0; private static final int CODE_IMPORT = 0;
private static final int CODE_IMPORT_DECRYPT = 1;
// permission request codes // permission request codes
private static final int CODE_PERM_IMPORT = 0; private static final int CODE_PERM_IMPORT = 0;
@ -38,9 +41,12 @@ public class PreferencesFragment extends PreferenceFragment {
public static final int ACTION_SLOTS = 1; public static final int ACTION_SLOTS = 1;
private Intent _result = new Intent(); private Intent _result = new Intent();
private AegisApplication _app;
private DatabaseManager _db; private DatabaseManager _db;
// this is used to keep a reference to a database converter
// while the user provides credentials to decrypt it
private DatabaseImporter _converter;
private void setResult() { private void setResult() {
getActivity().setResult(Activity.RESULT_OK, _result); getActivity().setResult(Activity.RESULT_OK, _result);
} }
@ -54,8 +60,9 @@ public class PreferencesFragment extends PreferenceFragment {
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences); addPreferencesFromResource(R.xml.preferences);
_app = (AegisApplication) getActivity().getApplication();
_db = _app.getDatabaseManager(); AegisApplication app = (AegisApplication) getActivity().getApplication();
_db = app.getDatabaseManager();
// set the result intent in advance // set the result intent in advance
setResult(); setResult();
@ -145,6 +152,9 @@ public class PreferencesFragment extends PreferenceFragment {
case CODE_IMPORT: case CODE_IMPORT:
onImportResult(resultCode, data); onImportResult(resultCode, data);
break; break;
case CODE_IMPORT_DECRYPT:
onImportDecryptResult(resultCode, data);
break;
} }
} }
@ -154,44 +164,41 @@ public class PreferencesFragment extends PreferenceFragment {
startActivityForResult(intent, CODE_IMPORT); startActivityForResult(intent, CODE_IMPORT);
} }
private void onImportResult(int resultCode, Intent data) { private void onImportDecryptResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) { if (resultCode != Activity.RESULT_OK) {
_converter = null;
return; return;
} }
InputStream fileStream = null; MasterKey key = (MasterKey) data.getSerializableExtra("key");
List<DatabaseEntry> entries = null; ((AegisImporter)_converter).setKey(key);
try { try {
try { importDatabase(_converter);
fileStream = getActivity().getContentResolver().openInputStream(data.getData()); } catch (DatabaseImporterException e) {
} catch (FileNotFoundException e) { e.printStackTrace();
Toast.makeText(getActivity(), "Error: File not found", Toast.LENGTH_SHORT).show(); Toast.makeText(getActivity(), "An error occurred while trying to parse the file", Toast.LENGTH_SHORT).show();
return; }
} }
ByteInputStream stream; private void onImportResult(int resultCode, Intent data) {
try { Uri uri = data.getData();
int read; if (resultCode != Activity.RESULT_OK || uri == null) {
byte[] buf = new byte[4096]; return;
ByteArrayOutputStream outStream = new ByteArrayOutputStream(); }
while ((read = fileStream.read(buf, 0, buf.length)) != -1) {
outStream.write(buf, 0, read);
}
stream = new ByteInputStream(outStream.toByteArray());
} catch (IOException e) {
Toast.makeText(getActivity(), "An error occurred while trying to read the file", Toast.LENGTH_SHORT).show();
return;
}
for (DatabaseImporter converter : DatabaseImporter.create(stream)) { ByteInputStream stream;
try { InputStream fileStream = null;
entries = converter.convert();
break; try {
} catch (DatabaseImporterException e) { fileStream = getActivity().getContentResolver().openInputStream(uri);
stream.reset(); stream = ByteInputStream.create(fileStream);
} } catch (FileNotFoundException e) {
} Toast.makeText(getActivity(), "Error: File not found", Toast.LENGTH_SHORT).show();
return;
} catch (IOException e) {
Toast.makeText(getActivity(), "An error occurred while trying to read the file", Toast.LENGTH_SHORT).show();
return;
} finally { } finally {
if (fileStream != null) { if (fileStream != null) {
try { try {
@ -202,11 +209,37 @@ public class PreferencesFragment extends PreferenceFragment {
} }
} }
if (entries == null) { boolean imported = false;
Toast.makeText(getActivity(), "An error occurred while trying to parse the file", Toast.LENGTH_SHORT).show(); for (DatabaseImporter converter : DatabaseImporter.create(stream)) {
return; try {
converter.parse();
// special case to decrypt encrypted aegis databases
if (converter.isEncrypted() && converter instanceof AegisImporter) {
_converter = converter;
Intent intent = new Intent(getActivity(), AuthActivity.class);
intent.putExtra("slots", ((AegisImporter)_converter).getFile().getSlots());
startActivityForResult(intent, CODE_IMPORT_DECRYPT);
return;
}
importDatabase(converter);
imported = true;
break;
} catch (DatabaseImporterException e) {
e.printStackTrace();
stream.reset();
}
} }
if (!imported) {
Toast.makeText(getActivity(), "An error occurred while trying to parse the file", Toast.LENGTH_SHORT).show();
}
}
private void importDatabase(DatabaseImporter converter) throws DatabaseImporterException {
List<DatabaseEntry> entries = converter.convert();
for (DatabaseEntry entry : entries) { for (DatabaseEntry entry : entries) {
_db.addKey(entry); _db.addKey(entry);
} }
@ -220,6 +253,6 @@ public class PreferencesFragment extends PreferenceFragment {
} }
_result.putExtra("needsReload", true); _result.putExtra("needsReload", true);
Toast.makeText(getActivity(), String.format(Locale.getDefault(), "Imported %d entries", entries.size()), Toast.LENGTH_SHORT).show(); Toast.makeText(getActivity(), String.format(Locale.getDefault(), "Imported %d entries", entries.size()), Toast.LENGTH_LONG).show();
} }
} }

View file

@ -1,12 +1,26 @@
package me.impy.aegis.util; package me.impy.aegis.util;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class ByteInputStream extends ByteArrayInputStream { public class ByteInputStream extends ByteArrayInputStream {
public ByteInputStream(byte[] buf) { private ByteInputStream(byte[] buf) {
super(buf); super(buf);
} }
public static ByteInputStream create(InputStream fileStream) throws IOException {
int read;
byte[] buf = new byte[4096];
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
while ((read = fileStream.read(buf, 0, buf.length)) != -1) {
outStream.write(buf, 0, read);
}
return new ByteInputStream(outStream.toByteArray());
}
public byte[] getBytes() { public byte[] getBytes() {
return this.buf; return this.buf;
} }