Add initial export function to save the database as plain-text JSON file
This commit is contained in:
parent
6e68141638
commit
7f0784b6bb
5 changed files with 145 additions and 19 deletions
|
@ -2,6 +2,9 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="org.shadowice.flocke.andotp">
|
package="org.shadowice.flocke.andotp">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
|
|
@ -26,6 +26,7 @@ import android.Manifest;
|
||||||
import android.animation.ObjectAnimator;
|
import android.animation.ObjectAnimator;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageInfo;
|
import android.content.pm.PackageInfo;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
@ -65,6 +66,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
private Runnable handlerTask;
|
private Runnable handlerTask;
|
||||||
|
|
||||||
private static final int PERMISSIONS_REQUEST_CAMERA = 42;
|
private static final int PERMISSIONS_REQUEST_CAMERA = 42;
|
||||||
|
private static final int PERMISSIONS_REQUEST_WRITE_EXPORT = 24;
|
||||||
|
|
||||||
private void doScanQRCode(){
|
private void doScanQRCode(){
|
||||||
new IntentIntegrator(MainActivity.this)
|
new IntentIntegrator(MainActivity.this)
|
||||||
|
@ -74,17 +76,14 @@ public class MainActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scanQRCode(){
|
private void scanQRCode(){
|
||||||
// check Android 6 permission
|
|
||||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||||
doScanQRCode();
|
doScanQRCode();
|
||||||
} else {
|
} else {
|
||||||
ActivityCompat.requestPermissions(this,
|
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, PERMISSIONS_REQUEST_CAMERA);
|
||||||
new String[]{Manifest.permission.CAMERA}, PERMISSIONS_REQUEST_CAMERA);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showAbout() {
|
private void showAbout() {
|
||||||
// Inflate the dialog_about message contents
|
|
||||||
View messageView = getLayoutInflater().inflate(R.layout.dialog_about, null, false);
|
View messageView = getLayoutInflater().inflate(R.layout.dialog_about, null, false);
|
||||||
|
|
||||||
String versionName = "";
|
String versionName = "";
|
||||||
|
@ -106,31 +105,72 @@ public class MainActivity extends AppCompatActivity {
|
||||||
builder.show();
|
builder.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void doExportJSON() {
|
||||||
|
boolean success = SettingsHelper.exportAsJSON(this);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
showSimpleSnackbar(R.string.msg_export_success);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exportJSON() {
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
doExportJSON();
|
||||||
|
} else {
|
||||||
|
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSIONS_REQUEST_WRITE_EXPORT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exportJSONWithWarning() {
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
|
|
||||||
|
builder.setTitle(getString(R.string.msg_security_warning))
|
||||||
|
.setMessage(getString(R.string.msg_export_warning))
|
||||||
|
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialogInterface, int i) {
|
||||||
|
exportJSON();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialogInterface, int i) {}
|
||||||
|
})
|
||||||
|
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
||||||
if(requestCode == PERMISSIONS_REQUEST_CAMERA) {
|
if(requestCode == PERMISSIONS_REQUEST_CAMERA) {
|
||||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
// permission was granted
|
|
||||||
doScanQRCode();
|
doScanQRCode();
|
||||||
} else {
|
} else {
|
||||||
Snackbar.make(fab, R.string.msg_camera_permission, Snackbar.LENGTH_LONG).setCallback(new Snackbar.Callback() {
|
showSimpleSnackbar(R.string.msg_camera_permission);
|
||||||
@Override
|
|
||||||
public void onDismissed(Snackbar snackbar, int event) {
|
|
||||||
super.onDismissed(snackbar, event);
|
|
||||||
|
|
||||||
if (entries.isEmpty()) {
|
|
||||||
showNoAccount();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).show();
|
|
||||||
}
|
}
|
||||||
}
|
} else if (requestCode == PERMISSIONS_REQUEST_WRITE_EXPORT) {
|
||||||
else {
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
doExportJSON();
|
||||||
|
} else {
|
||||||
|
showSimpleSnackbar(R.string.msg_storage_permissions);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Entry nextSelection = null;
|
private void showSimpleSnackbar(int string_res) {
|
||||||
|
Snackbar.make(fab, string_res, Snackbar.LENGTH_LONG).setCallback(new Snackbar.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onDismissed(Snackbar snackbar, int event) {
|
||||||
|
super.onDismissed(snackbar, event);
|
||||||
|
|
||||||
|
if (entries.isEmpty()) {
|
||||||
|
showNoAccount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).show();
|
||||||
|
}
|
||||||
|
|
||||||
private void showNoAccount(){
|
private void showNoAccount(){
|
||||||
Snackbar noAccountSnackbar = Snackbar.make(fab, R.string.no_accounts, Snackbar.LENGTH_INDEFINITE)
|
Snackbar noAccountSnackbar = Snackbar.make(fab, R.string.no_accounts, Snackbar.LENGTH_INDEFINITE)
|
||||||
.setAction(R.string.button_add, new View.OnClickListener() {
|
.setAction(R.string.button_add, new View.OnClickListener() {
|
||||||
|
@ -286,7 +326,15 @@ public class MainActivity extends AppCompatActivity {
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
int id = item.getItemId();
|
int id = item.getItemId();
|
||||||
|
|
||||||
if(id == R.id.action_about){
|
if (id == R.id.action_export) {
|
||||||
|
exportJSONWithWarning();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else if (id == R.id.action_import) {
|
||||||
|
SettingsHelper.importFromJSON(this);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else if (id == R.id.action_about){
|
||||||
showAbout();
|
showAbout();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -27,11 +27,15 @@ import android.content.Context;
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
|
|
||||||
|
import java.io.BufferedWriter;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.Writer;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
|
import static android.os.Environment.getExternalStorageDirectory;
|
||||||
import static org.shadowice.flocke.andotp.Utils.readFully;
|
import static org.shadowice.flocke.andotp.Utils.readFully;
|
||||||
import static org.shadowice.flocke.andotp.Utils.writeFully;
|
import static org.shadowice.flocke.andotp.Utils.writeFully;
|
||||||
|
|
||||||
|
@ -39,6 +43,8 @@ public class SettingsHelper {
|
||||||
public static final String KEY_FILE = "otp.key";
|
public static final String KEY_FILE = "otp.key";
|
||||||
public static final String SETTINGS_FILE = "secrets.dat";
|
public static final String SETTINGS_FILE = "secrets.dat";
|
||||||
|
|
||||||
|
public static final String EXPORT_FILE = "otp_accounts.json";
|
||||||
|
|
||||||
public static void store(Context context, ArrayList<Entry> entries){
|
public static void store(Context context, ArrayList<Entry> entries){
|
||||||
JSONArray a = new JSONArray();
|
JSONArray a = new JSONArray();
|
||||||
|
|
||||||
|
@ -81,4 +87,44 @@ public class SettingsHelper {
|
||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static JSONArray readJSON(Context context) {
|
||||||
|
JSONArray json = new JSONArray();
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] data = readFully(new File(context.getFilesDir() + "/" + SETTINGS_FILE));
|
||||||
|
|
||||||
|
SecretKey key = EncryptionHelper.loadOrGenerateKeys(context, new File(context.getFilesDir() + "/" + KEY_FILE));
|
||||||
|
data = EncryptionHelper.decrypt(key, data);
|
||||||
|
|
||||||
|
json = new JSONArray(new String(data));
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean exportAsJSON(Context context) {
|
||||||
|
File outputFile = new File(getExternalStorageDirectory() + "/" + EXPORT_FILE);
|
||||||
|
|
||||||
|
JSONArray data = readJSON(context);
|
||||||
|
|
||||||
|
boolean success = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Writer output = new BufferedWriter(new FileWriter(outputFile));
|
||||||
|
output.write(data.toString());
|
||||||
|
output.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
success = false;
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void importFromJSON(Context context) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,25 @@
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/submenu_backup"
|
||||||
|
android:title="@string/menu_submenu_backup">
|
||||||
|
|
||||||
|
<menu>
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_export"
|
||||||
|
android:orderInCategory="100"
|
||||||
|
android:title="@string/menu_export"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_import"
|
||||||
|
android:orderInCategory="100"
|
||||||
|
android:title="@string/menu_import"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
</menu>
|
||||||
|
</item>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_about"
|
android:id="@+id/action_about"
|
||||||
android:orderInCategory="100"
|
android:orderInCategory="100"
|
||||||
|
|
|
@ -6,6 +6,9 @@
|
||||||
<string name="menu_edit">Edit</string>
|
<string name="menu_edit">Edit</string>
|
||||||
<string name="menu_scan">Scan QR Code</string>
|
<string name="menu_scan">Scan QR Code</string>
|
||||||
<string name="menu_about">About</string>
|
<string name="menu_about">About</string>
|
||||||
|
<string name="menu_submenu_backup">Import / Export</string>
|
||||||
|
<string name="menu_export">Export as JSON</string>
|
||||||
|
<string name="menu_import">Import from JSON</string>
|
||||||
<string name="msg_invalid_qr_code">Invalid QR Code</string>
|
<string name="msg_invalid_qr_code">Invalid QR Code</string>
|
||||||
<string name="msg_account_added">Account added</string>
|
<string name="msg_account_added">Account added</string>
|
||||||
<string name="button_cancel">Cancel</string>
|
<string name="button_cancel">Cancel</string>
|
||||||
|
@ -17,6 +20,13 @@
|
||||||
<string name="alert_rename">Rename</string>
|
<string name="alert_rename">Rename</string>
|
||||||
<string name="alert_remove">"Remove "</string>
|
<string name="alert_remove">"Remove "</string>
|
||||||
<string name="msg_camera_permission">Camera permission not granted</string>
|
<string name="msg_camera_permission">Camera permission not granted</string>
|
||||||
|
<string name="msg_storage_permissions">Storage permissions not granted</string>
|
||||||
|
<string name="msg_security_warning">Security warning</string>
|
||||||
|
<string name="msg_export_warning">
|
||||||
|
Do you really want to export the database as plain-text JSON file?
|
||||||
|
This file contains all your secret keys, please keep it safe!
|
||||||
|
</string>
|
||||||
|
<string name="msg_export_success">Export to external storage successful</string>
|
||||||
|
|
||||||
<!-- About dialog -->
|
<!-- About dialog -->
|
||||||
<string name="about_description">An open-source two-factor authentication App for Android 4.3+</string>
|
<string name="about_description">An open-source two-factor authentication App for Android 4.3+</string>
|
||||||
|
|
Loading…
Reference in a new issue