Export/import improvements

pull/147/head
M66B 6 years ago
parent 1dc5a4fad0
commit eb55cc2490

@ -39,6 +39,7 @@ import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.textfield.TextInputLayout;
@ -46,6 +47,8 @@ import com.google.android.material.textfield.TextInputLayout;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
@ -67,6 +70,7 @@ import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.documentfile.provider.DocumentFile;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
@ -81,6 +85,7 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On
private ActionBarDrawerToggle drawerToggle;
private boolean hasAccount;
private String password;
private static final int KEY_ITERATIONS = 65536;
private static final int KEY_LENGTH = 256;
@ -290,39 +295,11 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == ActivitySetup.REQUEST_EXPORT || requestCode == ActivitySetup.REQUEST_IMPORT)
if (resultCode == RESULT_OK && data != null)
fileSelected(requestCode == ActivitySetup.REQUEST_EXPORT, data);
}
private void fileSelected(final boolean export, final Intent data) {
View dview = LayoutInflater.from(this).inflate(R.layout.dialog_password, null);
final TextInputLayout etPassword1 = dview.findViewById(R.id.tilPassword1);
final TextInputLayout etPassword2 = dview.findViewById(R.id.tilPassword2);
new DialogBuilderLifecycle(this, this)
.setView(dview)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String password1 = etPassword1.getEditText().getText().toString();
String password2 = etPassword2.getEditText().getText().toString();
if (TextUtils.isEmpty(password1))
Snackbar.make(view, R.string.title_setup_password_missing, Snackbar.LENGTH_LONG).show();
else {
if (password1.equals(password2)) {
if (export)
handleExport(data, password1);
else
handleImport(data, password1);
} else
Snackbar.make(view, R.string.title_setup_password_different, Snackbar.LENGTH_LONG).show();
}
}
})
.show();
if (requestCode == REQUEST_EXPORT)
handleExport(data, this.password);
else if (requestCode == REQUEST_IMPORT)
handleImport(data, this.password);
}
private void onManageNotifications() {
@ -332,7 +309,7 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On
private void onMenuExport() {
if (Helper.isPro(this))
try {
startActivityForResult(Helper.getChooser(this, getIntentExport()), ActivitySetup.REQUEST_EXPORT);
askPassword(true);
} catch (Throwable ex) {
Helper.unexpectedError(this, this, ex);
}
@ -345,12 +322,47 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On
private void onMenuImport() {
try {
startActivityForResult(Helper.getChooser(this, getIntentImport()), ActivitySetup.REQUEST_IMPORT);
askPassword(false);
} catch (Throwable ex) {
Helper.unexpectedError(this, this, ex);
}
}
private void askPassword(final boolean export) {
View dview = LayoutInflater.from(this).inflate(R.layout.dialog_password, null);
final TextInputLayout etPassword1 = dview.findViewById(R.id.tilPassword1);
final TextInputLayout etPassword2 = dview.findViewById(R.id.tilPassword2);
TextView tvImportHint = dview.findViewById(R.id.tvImporthint);
etPassword2.setVisibility(export ? View.VISIBLE : View.GONE);
tvImportHint.setVisibility(export ? View.GONE : View.VISIBLE);
new DialogBuilderLifecycle(this, this)
.setView(dview)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String password1 = etPassword1.getEditText().getText().toString();
String password2 = etPassword2.getEditText().getText().toString();
if (!BuildConfig.DEBUG && TextUtils.isEmpty(password1))
Snackbar.make(view, R.string.title_setup_password_missing, Snackbar.LENGTH_LONG).show();
else {
if (!export || password1.equals(password2)) {
ActivitySetup.this.password = password1;
startActivityForResult(
Helper.getChooser(
ActivitySetup.this,
export ? getIntentExport() : getIntentImport()),
export ? REQUEST_EXPORT : REQUEST_IMPORT);
} else
Snackbar.make(view, R.string.title_setup_password_different, Snackbar.LENGTH_LONG).show();
}
}
})
.show();
}
private void onMenuTheme(int id) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
switch (id) {
@ -403,8 +415,8 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_TITLE, "fairemail_backup_" +
new SimpleDateFormat("yyyyMMdd").format(new Date().getTime()) + ".json");
intent.putExtra(Intent.EXTRA_TITLE, "fairemail_" +
new SimpleDateFormat("yyyyMMdd").format(new Date().getTime()) + ".backup");
return intent;
}
@ -431,26 +443,7 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On
throw new IllegalArgumentException(context.getString(R.string.title_no_stream));
}
OutputStream out = null;
try {
Log.i("Writing URI=" + uri);
byte[] salt = new byte[16];
SecureRandom random = new SecureRandom();
random.nextBytes(salt);
// https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, KEY_ITERATIONS, KEY_LENGTH);
SecretKey secret = keyFactory.generateSecret(keySpec);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secret);
OutputStream raw = context.getContentResolver().openOutputStream(uri);
raw.write(salt);
raw.write(cipher.getIV());
out = new CipherOutputStream(raw, cipher);
Log.i("Collecting data");
DB db = DB.getInstance(context);
// Accounts
@ -501,12 +494,42 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On
jexport.put("answers", janswers);
jexport.put("settings", jsettings);
out.write(jexport.toString(2).getBytes());
ContentResolver resolver = context.getContentResolver();
DocumentFile file = DocumentFile.fromSingleUri(context, uri);
OutputStream raw = null;
try {
raw = new BufferedOutputStream(resolver.openOutputStream(uri));
Log.i("Writing URI=" + uri + " name=" + file.getName() + " virtual=" + file.isVirtual());
if (TextUtils.isEmpty(password))
raw.write(jexport.toString(2).getBytes());
else {
byte[] salt = new byte[16];
SecureRandom random = new SecureRandom();
random.nextBytes(salt);
// https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, KEY_ITERATIONS, KEY_LENGTH);
SecretKey secret = keyFactory.generateSecret(keySpec);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secret);
raw.write(salt);
raw.write(cipher.getIV());
OutputStream cout = new CipherOutputStream(raw, cipher);
cout.write(jexport.toString(2).getBytes());
cout.flush();
cout.close();
}
raw.flush();
Log.i("Exported data");
} finally {
if (out != null)
out.close();
if (raw != null)
raw.close();
}
return null;
@ -543,13 +566,18 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On
throw new IllegalArgumentException(context.getString(R.string.title_no_stream));
}
InputStream in = null;
InputStream raw = null;
StringBuilder data = new StringBuilder();
try {
Log.i("Reading URI=" + uri);
ContentResolver resolver = context.getContentResolver();
AssetFileDescriptor descriptor = resolver.openTypedAssetFileDescriptor(uri, "*/*", null);
InputStream raw = descriptor.createInputStream();
raw = new BufferedInputStream(descriptor.createInputStream());
InputStream in;
if (TextUtils.isEmpty(password))
in = raw;
else {
byte[] salt = new byte[16];
byte[] prefix = new byte[16];
if (raw.read(salt) != salt.length)
@ -565,15 +593,19 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On
cipher.init(Cipher.DECRYPT_MODE, secret, iv);
in = new CipherInputStream(raw, cipher);
}
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null)
response.append(line);
Log.i("Importing " + resolver.toString());
data.append(line);
} finally {
if (raw != null)
raw.close();
}
JSONObject jimport = new JSONObject(response.toString());
Log.i("Importing data");
JSONObject jimport = new JSONObject(data.toString());
DB db = DB.getInstance(context);
try {
@ -657,10 +689,6 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On
Log.i("Imported data");
ServiceSynchronize.reload(context, "import");
} finally {
if (in != null)
in.close();
}
return null;
}

@ -39,8 +39,8 @@
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tvImporthint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"

Loading…
Cancel
Save