From ad1d3f04a69ed6e1f90c6ceb207ec83310efbad1 Mon Sep 17 00:00:00 2001 From: Alexander Bakker Date: Sat, 23 Mar 2019 20:48:02 +0100 Subject: [PATCH] Add support for importing from Google Authenticator Close #29 --- app/build.gradle | 3 + ...isImporter.java => AegisFileImporter.java} | 8 +- ...pImporter.java => AndOtpFileImporter.java} | 8 +- .../aegis/importers/DatabaseAppImporter.java | 50 +++++++ .../aegis/importers/DatabaseFileImporter.java | 55 ++++++++ .../aegis/importers/DatabaseImporter.java | 47 +------ ...Importer.java => FreeOtpFileImporter.java} | 7 +- .../importers/GoogleAuthAppImporter.java | 124 ++++++++++++++++++ .../beemdevelopment/aegis/otp/HotpInfo.java | 8 +- .../aegis/ui/PreferencesFragment.java | 74 +++++++++-- .../me/impy/aegis/db/DatabaseEntryGroup.java | 58 ++++++++ app/src/main/res/values/strings.xml | 7 +- app/src/main/res/xml/preferences.xml | 9 +- 13 files changed, 391 insertions(+), 67 deletions(-) rename app/src/main/java/com/beemdevelopment/aegis/importers/{AegisImporter.java => AegisFileImporter.java} (89%) rename app/src/main/java/com/beemdevelopment/aegis/importers/{AndOtpImporter.java => AndOtpFileImporter.java} (93%) create mode 100644 app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseAppImporter.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseFileImporter.java rename app/src/main/java/com/beemdevelopment/aegis/importers/{FreeOtpImporter.java => FreeOtpFileImporter.java} (96%) create mode 100644 app/src/main/java/com/beemdevelopment/aegis/importers/GoogleAuthAppImporter.java create mode 100644 app/src/main/java/me/impy/aegis/db/DatabaseEntryGroup.java diff --git a/app/build.gradle b/app/build.gradle index bcf755aa..da897ee5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,6 +29,7 @@ android { } dependencies { + def libsuVersion = '2.3.2' implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'androidx.appcompat:appcompat:1.0.2' @@ -45,5 +46,7 @@ dependencies { implementation 'com.mattprecious.swirl:swirl:1.0.0' implementation 'de.hdodenhof:circleimageview:2.2.0' implementation 'me.dm7.barcodescanner:zxing:1.9' + implementation "com.github.topjohnwu.libsu:core:${libsuVersion}" + implementation "com.github.topjohnwu.libsu:io:${libsuVersion}" testImplementation 'junit:junit:4.12' } diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/AegisFileImporter.java similarity index 89% rename from app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java rename to app/src/main/java/com/beemdevelopment/aegis/importers/AegisFileImporter.java index 3c7a3baf..10b5de6f 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/AegisFileImporter.java @@ -1,5 +1,7 @@ package com.beemdevelopment.aegis.importers; +import android.content.Context; + import org.json.JSONObject; import java.util.List; @@ -12,12 +14,12 @@ import com.beemdevelopment.aegis.db.DatabaseFileCredentials; import com.beemdevelopment.aegis.db.DatabaseFileException; import com.beemdevelopment.aegis.util.ByteInputStream; -public class AegisImporter extends DatabaseImporter { +public class AegisFileImporter extends DatabaseFileImporter { private DatabaseFileCredentials _creds; private DatabaseFile _file; - public AegisImporter(ByteInputStream stream) { - super(stream); + public AegisFileImporter(Context context, ByteInputStream stream) { + super(context, stream); } @Override diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpFileImporter.java similarity index 93% rename from app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpImporter.java rename to app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpFileImporter.java index 6716a636..e7e36afc 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpFileImporter.java @@ -1,5 +1,7 @@ package com.beemdevelopment.aegis.importers; +import android.content.Context; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -17,11 +19,11 @@ import com.beemdevelopment.aegis.otp.OtpInfoException; import com.beemdevelopment.aegis.otp.TotpInfo; import com.beemdevelopment.aegis.util.ByteInputStream; -public class AndOtpImporter extends DatabaseImporter { +public class AndOtpFileImporter extends DatabaseFileImporter { private JSONArray _obj; - public AndOtpImporter(ByteInputStream stream) { - super(stream); + public AndOtpFileImporter(Context context, ByteInputStream stream) { + super(context, stream); } @Override diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseAppImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseAppImporter.java new file mode 100644 index 00000000..c3e92f79 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseAppImporter.java @@ -0,0 +1,50 @@ +package com.beemdevelopment.aegis.importers; + +import android.content.Context; + +import com.beemdevelopment.aegis.db.DatabaseEntry; + +import java.lang.reflect.InvocationTargetException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public abstract class DatabaseAppImporter implements DatabaseImporter { + private static Map> _importers; + static { + // note: keep this list sorted alphabetically + LinkedHashMap> importers = new LinkedHashMap<>(); + importers.put("Google Authenticator", GoogleAuthAppImporter.class); + _importers = Collections.unmodifiableMap(importers); + } + + private Context _context; + + protected DatabaseAppImporter(Context context) { + _context = context; + } + + public abstract void parse() throws DatabaseImporterException; + + public abstract List convert() throws DatabaseImporterException; + + public abstract boolean isEncrypted(); + + public Context getContext() { + return _context; + } + + public static DatabaseAppImporter create(Context context, Class type) { + try { + return type.getConstructor(Context.class).newInstance(context); + } catch (IllegalAccessException | InstantiationException + | NoSuchMethodException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + public static Map> getImporters() { + return _importers; + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseFileImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseFileImporter.java new file mode 100644 index 00000000..8341529f --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseFileImporter.java @@ -0,0 +1,55 @@ +package com.beemdevelopment.aegis.importers; + +import android.content.Context; + +import java.lang.reflect.InvocationTargetException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.beemdevelopment.aegis.db.DatabaseEntry; +import com.beemdevelopment.aegis.util.ByteInputStream; + +public abstract class DatabaseFileImporter implements DatabaseImporter { + private static Map> _importers; + static { + // note: keep this list sorted alphabetically + LinkedHashMap> 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 List convert() throws DatabaseImporterException; + + public abstract boolean isEncrypted(); + + public Context getContext() { + return _context; + } + + public static DatabaseFileImporter create(Context context, ByteInputStream stream, Class 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> getImporters() { + return _importers; + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java index d2efc59a..b66bb7e1 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java @@ -1,47 +1,14 @@ package com.beemdevelopment.aegis.importers; -import java.lang.reflect.InvocationTargetException; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import android.content.Context; import com.beemdevelopment.aegis.db.DatabaseEntry; -import com.beemdevelopment.aegis.util.ByteInputStream; -public abstract class DatabaseImporter { - private static Map> _importers; - static { - // note: keep this list sorted alphabetically - LinkedHashMap> importers = new LinkedHashMap<>(); - importers.put("Aegis", AegisImporter.class); - importers.put("andOTP", AndOtpImporter.class); - importers.put("FreeOTP", FreeOtpImporter.class); - _importers = Collections.unmodifiableMap(importers); - } +import java.util.List; - protected ByteInputStream _stream; - - protected DatabaseImporter(ByteInputStream stream) { - _stream = stream; - } - - public abstract void parse() throws DatabaseImporterException; - - public abstract List convert() throws DatabaseImporterException; - - public abstract boolean isEncrypted(); - - public static DatabaseImporter create(ByteInputStream stream, Class type) { - try { - return type.getConstructor(ByteInputStream.class).newInstance(stream); - } catch (IllegalAccessException | InstantiationException - | NoSuchMethodException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } - - public static Map> getImporters() { - return _importers; - } +public interface DatabaseImporter { + void parse() throws DatabaseImporterException; + List convert() throws DatabaseImporterException; + boolean isEncrypted(); + Context getContext(); } diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/FreeOtpImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/FreeOtpFileImporter.java similarity index 96% rename from app/src/main/java/com/beemdevelopment/aegis/importers/FreeOtpImporter.java rename to app/src/main/java/com/beemdevelopment/aegis/importers/FreeOtpFileImporter.java index be340c62..3f2f51de 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/FreeOtpImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/FreeOtpFileImporter.java @@ -1,5 +1,6 @@ package com.beemdevelopment.aegis.importers; +import android.content.Context; import android.util.Xml; import org.json.JSONArray; @@ -19,11 +20,11 @@ import com.beemdevelopment.aegis.otp.OtpInfoException; import com.beemdevelopment.aegis.otp.TotpInfo; import com.beemdevelopment.aegis.util.ByteInputStream; -public class FreeOtpImporter extends DatabaseImporter { +public class FreeOtpFileImporter extends DatabaseFileImporter { private List _xmlEntries; - public FreeOtpImporter(ByteInputStream stream) { - super(stream); + public FreeOtpFileImporter(Context context, ByteInputStream stream) { + super(context, stream); } private static class XmlEntry { diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/GoogleAuthAppImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/GoogleAuthAppImporter.java new file mode 100644 index 00000000..cd448cf3 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/GoogleAuthAppImporter.java @@ -0,0 +1,124 @@ +package com.beemdevelopment.aegis.importers; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; + +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.TotpInfo; +import com.topjohnwu.superuser.ShellUtils; +import com.topjohnwu.superuser.io.SuFile; +import com.topjohnwu.superuser.io.SuFileInputStream; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY; + +public class GoogleAuthAppImporter extends DatabaseAppImporter { + private static final int TYPE_TOTP = 0; + private static final int TYPE_HOTP = 1; + + @SuppressLint("SdCardPath") + private static final String _filename = "/data/data/com.google.android.apps.authenticator2/databases/databases"; + + private List _entries = new ArrayList<>(); + + public GoogleAuthAppImporter(Context context) { + super(context); + } + + @Override + public void parse() 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(new SuFile(_filename))) { + try (FileOutputStream out = new FileOutputStream(file)) { + ShellUtils.pump(in, out); + } + } + } catch (IOException e) { + throw new DatabaseImporterException(e); + } + + try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY, null)) { + try (Cursor cursor = db.rawQuery("SELECT * FROM accounts", null)) { + if (!cursor.moveToFirst()) { + return; + } + + do { + int type = getInt(cursor, "type"); + byte[] secret = Base32.decode(getString(cursor, "secret").toCharArray()); + + OtpInfo info; + switch (type) { + case TYPE_TOTP: + info = new TotpInfo(secret); + break; + case TYPE_HOTP: + info = new HotpInfo(secret, getInt(cursor, "counter")); + break; + default: + throw new DatabaseImporterException("unsupported otp type: " + type); + } + + String name = getString(cursor, "email", ""); + String issuer = getString(cursor, "issuer", ""); + + String[] parts = name.split(":"); + if (parts.length == 2) { + name = parts[1]; + } + + DatabaseEntry entry = new DatabaseEntry(info, name, issuer); + _entries.add(entry); + } while(cursor.moveToNext()); + } + } catch (SQLiteException | OtpInfoException | Base32Exception e) { + throw new DatabaseImporterException(e); + } finally { + // always delete the temporary file + file.delete(); + } + } + + @Override + public List convert() { + return _entries; + } + + @Override + public boolean isEncrypted() { + return false; + } + + private static String getString(Cursor cursor, String columnName) { + return getString(cursor, columnName, null); + } + + private static String getString(Cursor cursor, String columnName, String def) { + String res = cursor.getString(cursor.getColumnIndex(columnName)); + if (res == null) { + return def; + } + return res; + } + + private static int getInt(Cursor cursor, String columnName) { + return cursor.getInt(cursor.getColumnIndex(columnName)); + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/otp/HotpInfo.java b/app/src/main/java/com/beemdevelopment/aegis/otp/HotpInfo.java index 502a6a2c..59d6b8d7 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/otp/HotpInfo.java +++ b/app/src/main/java/com/beemdevelopment/aegis/otp/HotpInfo.java @@ -11,9 +11,13 @@ import java.security.NoSuchAlgorithmException; public class HotpInfo extends OtpInfo { private long _counter; - public HotpInfo(byte[] secret) throws OtpInfoException { + public HotpInfo(byte[] secret, long counter) throws OtpInfoException { super(secret); - setCounter(0); + setCounter(counter); + } + + public HotpInfo(byte[] secret) throws OtpInfoException { + this(secret, 0); } public HotpInfo(byte[] secret, String algorithm, int digits, long counter) throws OtpInfoException { diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java index b8f63191..a6b05454 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java @@ -16,7 +16,9 @@ import android.widget.Toast; import com.beemdevelopment.aegis.db.DatabaseFileCredentials; import com.beemdevelopment.aegis.helpers.FingerprintHelper; import com.beemdevelopment.aegis.helpers.PermissionHelper; -import com.beemdevelopment.aegis.importers.AegisImporter; +import com.beemdevelopment.aegis.importers.AegisFileImporter; +import com.beemdevelopment.aegis.importers.DatabaseAppImporter; +import com.beemdevelopment.aegis.importers.DatabaseFileImporter; import com.beemdevelopment.aegis.importers.DatabaseImporter; import com.beemdevelopment.aegis.importers.DatabaseImporterException; import com.beemdevelopment.aegis.ui.preferences.SwitchPreference; @@ -46,6 +48,7 @@ import com.beemdevelopment.aegis.db.slots.PasswordSlot; import com.beemdevelopment.aegis.db.slots.Slot; import com.beemdevelopment.aegis.db.slots.SlotException; import com.beemdevelopment.aegis.db.slots.SlotList; +import com.topjohnwu.superuser.Shell; public class PreferencesFragment extends PreferenceFragmentCompat { // activity request codes @@ -63,8 +66,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat { // this is used to keep a reference to a database converter // while the user provides credentials to decrypt it - private DatabaseImporter _importer; - private Class _importerType; + private DatabaseFileImporter _importer; + private Class _importerType; private SwitchPreference _encryptionPreference; private SwitchPreference _fingerprintPreference; @@ -92,8 +95,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat { } }); - Preference exportPreference = findPreference("pref_import"); - exportPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + Preference importPreference = findPreference("pref_import"); + importPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { onImport(); @@ -101,8 +104,18 @@ public class PreferencesFragment extends PreferenceFragmentCompat { } }); - Preference importPreference = findPreference("pref_export"); - importPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + Preference importAppPreference = findPreference("pref_import_app"); + importAppPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + onImportApp(); + return true; + } + }); + + + Preference exportPreference = findPreference("pref_export"); + exportPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { onExport(); @@ -277,7 +290,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat { return; } - Map> importers = DatabaseImporter.getImporters(); + Map> importers = DatabaseFileImporter.getImporters(); String[] names = importers.keySet().toArray(new String[importers.size()]); Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) @@ -296,6 +309,43 @@ public class PreferencesFragment extends PreferenceFragmentCompat { .create()); } + private void onImportApp() { + Map> importers = DatabaseAppImporter.getImporters(); + String[] names = importers.keySet().toArray(new String[importers.size()]); + + 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(); + } + } + }) + .create()); + } + private void onImportDecryptResult(int resultCode, Intent data) { if (resultCode != Activity.RESULT_OK) { _importer = null; @@ -303,7 +353,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat { } DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds"); - ((AegisImporter)_importer).setCredentials(creds); + ((AegisFileImporter)_importer).setCredentials(creds); try { importDatabase(_importer); @@ -337,15 +387,15 @@ public class PreferencesFragment extends PreferenceFragmentCompat { } try { - DatabaseImporter importer = DatabaseImporter.create(stream, _importerType); + DatabaseFileImporter importer = DatabaseFileImporter.create(getContext(), stream, _importerType); importer.parse(); // special case to decrypt encrypted aegis databases - if (importer.isEncrypted() && importer instanceof AegisImporter) { + if (importer.isEncrypted() && importer instanceof AegisFileImporter) { _importer = importer; Intent intent = new Intent(getActivity(), AuthActivity.class); - intent.putExtra("slots", ((AegisImporter)_importer).getFile().getHeader().getSlots()); + intent.putExtra("slots", ((AegisFileImporter)_importer).getFile().getHeader().getSlots()); startActivityForResult(intent, CODE_IMPORT_DECRYPT); return; } diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseEntryGroup.java b/app/src/main/java/me/impy/aegis/db/DatabaseEntryGroup.java new file mode 100644 index 00000000..6585433b --- /dev/null +++ b/app/src/main/java/me/impy/aegis/db/DatabaseEntryGroup.java @@ -0,0 +1,58 @@ +package me.impy.aegis.db; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.Serializable; +import java.util.UUID; + +public class DatabaseEntryGroup implements Serializable { + private UUID _uuid; + private String _name; + + private DatabaseEntryGroup(UUID uuid, String name) { + _uuid = uuid; + _name = name; + } + + public DatabaseEntryGroup(String name) { + this(UUID.randomUUID(), name); + } + + public JSONObject toJson() { + JSONObject obj = new JSONObject(); + + try { + obj.put("uuid", _uuid.toString()); + obj.put("name", _name); + } catch (JSONException e) { + throw new RuntimeException(e); + } + + return obj; + } + + public static DatabaseEntryGroup fromJson(JSONObject obj) throws JSONException { + // if there is no uuid, generate a new one + UUID uuid; + if (!obj.has("uuid")) { + uuid = UUID.randomUUID(); + } else { + uuid = UUID.fromString(obj.getString("uuid")); + } + + return new DatabaseEntryGroup(uuid, obj.getString("name")); + } + + public UUID getUUID() { + return _uuid; + } + + public String getName() { + return _name; + } + + public void setName(String name) { + _name = name; + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ce4d941b..2f34d542 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,8 +19,10 @@ Automatically lock the database after %1$s seconds of inactivity Key slots Manage the list of keys that can decrypt the database - Import - Import a database + Import from file + Import a database from a file + Import from app + Import a database from an app (requires root access) Export Export the database Screen security @@ -121,6 +123,7 @@ An error occurred while trying to parse the file Error: File not found An error occurred while trying to read the file + Error: unable to obtain root access Imported %d entries An error occurred while trying to export the database The database has been exported to: diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 587857c8..84c140ce 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -78,8 +78,13 @@ app:iconSpaceReserved="false"> +