diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 559d0315..001c8dfc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + _keyProfiles; @@ -140,6 +143,54 @@ public class MainActivity extends AppCompatActivity { case CODE_IMPORT: onImportResult(resultCode, data); break; + case CODE_PREFERENCES: + onPreferencesResult(resultCode, data); + break; + } + } + + private void onPreferencesResult(int resultCode, Intent data) { + if (resultCode != RESULT_OK) { + return; + } + + // TODO: create a custom layout to show a message AND a checkbox + int action = data.getIntExtra("action", -1); + switch (action) { + case PreferencesActivity.ACTION_EXPORT: + final boolean[] checked = {true}; + AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this) + .setTitle("Export the database") + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + String filename; + try { + filename = _db.export(checked[0]); + } catch (Exception e) { + e.printStackTrace(); + Toast.makeText(this, "An error occurred while trying to export the database", Toast.LENGTH_SHORT).show(); + return; + } + + // make sure the new file is visible + MediaScannerConnection.scanFile(this, new String[]{filename}, null, null); + + Toast.makeText(this, "The database has been exported to: " + filename, Toast.LENGTH_SHORT).show(); + }) + .setNegativeButton(android.R.string.cancel, null); + if (_db.getFile().isEncrypted()) { + final String[] items = {"Keep the database encrypted"}; + final boolean[] checkedItems = {true}; + builder.setMultiChoiceItems(items, checkedItems, new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialog, int index, boolean isChecked) { + checked[0] = isChecked; + } + }); + } else { + builder.setMessage("This action will export the database out of Android's private storage."); + } + builder.show(); + break; } } @@ -371,7 +422,7 @@ public class MainActivity extends AppCompatActivity { switch (item.getItemId()) { case R.id.action_settings: Intent preferencesActivity = new Intent(this, PreferencesActivity.class); - startActivity(preferencesActivity); + startActivityForResult(preferencesActivity, CODE_PREFERENCES); return true; case R.id.action_import: Intent intent = new Intent(Intent.ACTION_GET_CONTENT); diff --git a/app/src/main/java/me/impy/aegis/PreferencesActivity.java b/app/src/main/java/me/impy/aegis/PreferencesActivity.java index 071bd72d..147c4530 100644 --- a/app/src/main/java/me/impy/aegis/PreferencesActivity.java +++ b/app/src/main/java/me/impy/aegis/PreferencesActivity.java @@ -1,5 +1,7 @@ package me.impy.aegis; +import android.app.Activity; +import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.Preference; @@ -9,13 +11,14 @@ import android.support.v7.app.AppCompatActivity; import android.widget.Toast; public class PreferencesActivity extends AppCompatActivity { + public static final int ACTION_EXPORT = 0; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - SharedPreferences mySharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - if (mySharedPreferences.getBoolean("pref_night_mode", false)) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + if (preferences.getBoolean("pref_night_mode", false)) { setTheme(R.style.AppTheme_Dark); } else { setTheme(R.style.AppTheme_Default); @@ -29,7 +32,6 @@ public class PreferencesActivity extends AppCompatActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.preferences); final Preference nightModePreference = findPreference("pref_night_mode"); @@ -40,6 +42,20 @@ public class PreferencesActivity extends AppCompatActivity { return true; } }); + + Preference exportPreference = findPreference("pref_export"); + exportPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Intent intent = new Intent(); + intent.putExtra("action", ACTION_EXPORT); + + Activity activity = getActivity(); + activity.setResult(RESULT_OK, intent); + activity.finish(); + return true; + } + }); } } } diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseFile.java b/app/src/main/java/me/impy/aegis/db/DatabaseFile.java index 2d3730da..07beb9ce 100644 --- a/app/src/main/java/me/impy/aegis/db/DatabaseFile.java +++ b/app/src/main/java/me/impy/aegis/db/DatabaseFile.java @@ -5,6 +5,7 @@ import android.content.Context; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; +import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -116,37 +117,6 @@ public class DatabaseFile { return !_slots.isEmpty() && _cryptParameters != null; } - public void save(Context context, String filename) throws IOException { - byte[] data = serialize(); - - FileOutputStream file = context.openFileOutput(filename, Context.MODE_PRIVATE); - file.write(data); - file.close(); - } - - public static DatabaseFile load(Context context, String filename) throws Exception { - byte[] bytes; - FileInputStream file = null; - - try { - file = context.openFileInput(filename); - DataInputStream stream = new DataInputStream(file); - bytes = new byte[(int) file.getChannel().size()]; - stream.readFully(bytes); - stream.close(); - } finally { - // always close the file - // there is no need to close the DataInputStream - if (file != null) { - file.close(); - } - } - - DatabaseFile db = new DatabaseFile(); - db.deserialize(bytes); - return db; - } - private static void writeSection(DataOutputStream stream, byte id, byte[] data) throws IOException { stream.write(id); diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseManager.java b/app/src/main/java/me/impy/aegis/db/DatabaseManager.java index 878c4643..31733cdc 100644 --- a/app/src/main/java/me/impy/aegis/db/DatabaseManager.java +++ b/app/src/main/java/me/impy/aegis/db/DatabaseManager.java @@ -1,16 +1,23 @@ package me.impy.aegis.db; import android.content.Context; +import android.os.Environment; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; import java.util.List; -import me.impy.aegis.KeyProfile; import me.impy.aegis.crypto.CryptParameters; import me.impy.aegis.crypto.CryptResult; import me.impy.aegis.crypto.MasterKey; public class DatabaseManager { - public static final String FILENAME = "aegis.db"; + private static final String FILENAME = "aegis.db"; + private static final String FILENAME_EXPORT = "aegis_export.db"; + private static final String FILENAME_EXPORT_PLAIN = "aegis_export.json"; private MasterKey _key; private DatabaseFile _file; @@ -22,11 +29,30 @@ public class DatabaseManager { } public void load() throws Exception { - _file = DatabaseFile.load(_context, FILENAME); + byte[] fileBytes; + FileInputStream file = null; + + try { + file = _context.openFileInput(FILENAME); + fileBytes = new byte[(int) file.getChannel().size()]; + DataInputStream stream = new DataInputStream(file); + stream.readFully(fileBytes); + stream.close(); + } finally { + // always close the file stream + // there is no need to close the DataInputStream + if (file != null) { + file.close(); + } + } + + _file = new DatabaseFile(); + _file.deserialize(fileBytes); + if (!_file.isEncrypted()) { - byte[] bytes = _file.getContent(); + byte[] contentBytes = _file.getContent(); _db = new Database(); - _db.deserialize(bytes); + _db.deserialize(contentBytes); } } @@ -40,17 +66,64 @@ public class DatabaseManager { _key = key; } + public static void save(Context context, DatabaseFile file) throws IOException { + byte[] bytes = file.serialize(); + + FileOutputStream stream = null; + try { + stream = context.openFileOutput(FILENAME, Context.MODE_PRIVATE); + stream.write(bytes); + } finally { + // always close the file stream + if (stream != null) { + stream.close(); + } + } + } + public void save() throws Exception { assertDecrypted(); - byte[] bytes = _db.serialize(); + byte[] dbBytes = _db.serialize(); if (!_file.isEncrypted()) { - _file.setContent(bytes); + _file.setContent(dbBytes); } else { + CryptResult result = _key.encrypt(dbBytes); + _file.setContent(result.Data); + _file.setCryptParameters(result.Parameters); + } + save(_context, _file); + } + + public String export(boolean encrypt) throws Exception { + assertDecrypted(); + byte[] bytes = _db.serialize(); + encrypt = encrypt && getFile().isEncrypted(); + if (encrypt) { CryptResult result = _key.encrypt(bytes); _file.setContent(result.Data); _file.setCryptParameters(result.Parameters); + bytes = _file.serialize(); + } + + File file; + FileOutputStream stream = null; + try { + File dir = new File(Environment.getExternalStorageDirectory(), "Aegis"); + if (!dir.exists() && !dir.mkdirs()) { + throw new IOException("error creating external storage directory"); + } + + file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN); + stream = new FileOutputStream(file); + stream.write(bytes); + } finally { + // always close the file stream + if (stream != null) { + stream.close(); + } } - _file.save(_context, FILENAME); + + return file.getAbsolutePath(); } public void addKey(DatabaseEntry entry) throws Exception { diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 8b14fedb..0eb2e2c0 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -13,4 +13,10 @@ android:key="pref_issuer" android:title="@string/pref_issuers" android:summary="@string/pref_issuers_description"/> - \ No newline at end of file + + + +