From 427721d8713df9d56ca2513d7c2bad1276a209e8 Mon Sep 17 00:00:00 2001 From: M66B Date: Mon, 17 Oct 2022 11:00:49 +0200 Subject: [PATCH] Moved password protecting to style menu --- .../eu/faircode/email/EditTextCompose.java | 85 ----------- .../java/eu/faircode/email/StyleHelper.java | 132 ++++++++++++++++++ .../res/layout/dialog_password_protect.xml | 78 +++++++++++ app/src/main/res/menu/popup_style.xml | 13 +- app/src/main/res/values/strings.xml | 3 +- 5 files changed, 223 insertions(+), 88 deletions(-) create mode 100644 app/src/main/res/layout/dialog_password_protect.xml diff --git a/app/src/main/java/eu/faircode/email/EditTextCompose.java b/app/src/main/java/eu/faircode/email/EditTextCompose.java index c830d65811..cb3cf73ae3 100644 --- a/app/src/main/java/eu/faircode/email/EditTextCompose.java +++ b/app/src/main/java/eu/faircode/email/EditTextCompose.java @@ -58,16 +58,9 @@ import androidx.preference.PreferenceManager; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; -import java.nio.ByteBuffer; -import java.security.SecureRandom; import java.util.List; import java.util.concurrent.ExecutorService; -import javax.crypto.Cipher; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.GCMParameterSpec; -import javax.crypto.spec.PBEKeySpec; import javax.mail.internet.AddressException; import javax.mail.internet.InternetAddress; @@ -122,7 +115,6 @@ public class EditTextCompose extends FixedEditText { int order = 1000; menu.add(Menu.CATEGORY_SECONDARY, R.string.title_insert_brackets, order++, context.getString(R.string.title_insert_brackets)); menu.add(Menu.CATEGORY_SECONDARY, R.string.title_insert_quotes, order++, context.getString(R.string.title_insert_quotes)); - menu.add(Menu.CATEGORY_SECONDARY, R.string.title_protect_text, order++, context.getString(R.string.title_protect_text)); } catch (Throwable ex) { Log.e(ex); } @@ -147,8 +139,6 @@ public class EditTextCompose extends FixedEditText { return surround("(", ")"); else if (id == R.string.title_insert_quotes) return surround("\"", "\""); - else if (id == R.string.title_protect_text) - return protect(); } return false; } @@ -178,81 +168,6 @@ public class EditTextCompose extends FixedEditText { } return selection; } - - private boolean protect() { - Editable edit = getText(); - int start = getSelectionStart(); - int end = getSelectionEnd(); - boolean selection = (edit != null && start >= 0 && start < end); - if (selection) - executor.submit(new Runnable() { - @Override - public void run() { - try { - String password = "FairEmail"; - String text = HtmlHelper.toHtml((Spanned) edit.subSequence(start, end), context); - - SecureRandom random = new SecureRandom(); - - byte[] salt = new byte[16]; // 128 bits - random.nextBytes(salt); - - byte[] iv = new byte[12]; // 96 bites - random.nextBytes(iv); - - // Iterations = 120,000; Keylength = 256 bits = 32 bytes - PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 120000, 256); - - SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); - SecretKey key = skf.generateSecret(spec); - - // Authentication tag length = 128 bits = 16 bytes - GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv); - - final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec); - - byte[] cipherText = cipher.doFinal(text.getBytes()); - - ByteBuffer out = ByteBuffer.allocate(1 + salt.length + iv.length + cipherText.length); - out.put((byte) 1); //version - out.put(salt); - out.put(iv); - out.put(cipherText); - String fragment = Base64.encodeToString(out.array(), Base64.URL_SAFE | Base64.NO_WRAP); - - post(new Runnable() { - @Override - public void run() { - try { - if (getSelectionStart() != start || getSelectionEnd() != end) - return; - String title = context.getString(R.string.title_decrypt); - String url = "https://email.faircode.eu/decrypt/#" + fragment; - edit.delete(start, end); - edit.insert(start, title); - edit.setSpan(new URLSpan(url), start, start + title.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - setSelection(start + title.length()); - } catch (Throwable ex) { - Log.e(ex); - } - } - }); - - } catch (Throwable ex) { - Log.e(ex); - post(new Runnable() { - @Override - public void run() { - ToastEx.makeText(context, ex.toString(), Toast.LENGTH_LONG).show(); - } - }); - } - } - }); - - return selection; - } }); setCustomInsertionActionModeCallback(new ActionMode.Callback() { diff --git a/app/src/main/java/eu/faircode/email/StyleHelper.java b/app/src/main/java/eu/faircode/email/StyleHelper.java index 758143795a..57945321ab 100644 --- a/app/src/main/java/eu/faircode/email/StyleHelper.java +++ b/app/src/main/java/eu/faircode/email/StyleHelper.java @@ -19,6 +19,7 @@ package eu.faircode.email; Copyright 2018-2022 by Marcel Bokhorst (M66B) */ +import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -26,6 +27,7 @@ import android.content.SharedPreferences; import android.graphics.Color; import android.graphics.Typeface; import android.os.Build; +import android.os.Bundle; import android.text.Editable; import android.text.Layout; import android.text.NoCopySpan; @@ -47,14 +49,20 @@ import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.URLSpan; import android.text.style.UnderlineSpan; +import android.util.Base64; import android.util.Pair; +import android.view.LayoutInflater; import android.view.MenuItem; import android.view.SubMenu; import android.view.View; +import android.view.WindowManager; import android.widget.EditText; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.PopupMenu; import androidx.core.content.res.ResourcesCompat; import androidx.lifecycle.LifecycleOwner; @@ -63,13 +71,23 @@ import androidx.preference.PreferenceManager; import com.flask.colorpicker.ColorPickerView; import com.flask.colorpicker.builder.ColorPickerClickListener; import com.flask.colorpicker.builder.ColorPickerDialogBuilder; +import com.google.android.material.textfield.TextInputLayout; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.PBEKeySpec; + public class StyleHelper { private static final List CLEAR_STYLES = Collections.unmodifiableList(Arrays.asList( StyleSpan.class, @@ -214,6 +232,7 @@ public class StyleHelper { popupMenu.getMenu().findItem(R.id.menu_style_indentation_increase).setEnabled(maxLevel == null); popupMenu.getMenu().findItem(R.id.menu_style_indentation_decrease).setEnabled(indents.length > 0); + popupMenu.getMenu().findItem(R.id.menu_style_password).setVisible(!BuildConfig.PLAY_STORE_RELEASE); popupMenu.getMenu().findItem(R.id.menu_style_code).setEnabled(BuildConfig.DEBUG); popupMenu.insertIcons(context); @@ -253,6 +272,8 @@ public class StyleHelper { return setSuperscript(item); } else if (groupId == R.id.group_style_strikethrough) { return setStrikeThrough(item); + } else if (groupId == R.id.group_style_password) { + return setPassword(item); } else if (groupId == R.id.group_style_code) { return setCode(item); } else if (groupId == R.id.group_style_clear) { @@ -544,6 +565,117 @@ public class StyleHelper { return true; } + private boolean setPassword(MenuItem item) { + if (!ActivityBilling.isPro(context)) { + context.startActivity(new Intent(context, ActivityBilling.class)); + return true; + } + + View dview = LayoutInflater.from(context).inflate(R.layout.dialog_password_protect, null); + TextInputLayout etPassword1 = dview.findViewById(R.id.tilPassword1); + TextInputLayout etPassword2 = dview.findViewById(R.id.tilPassword2); + + Dialog dialog = new AlertDialog.Builder(context) + .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(); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean debug = prefs.getBoolean("debug", false); + + if (TextUtils.isEmpty(password1) && !(debug || BuildConfig.DEBUG)) + ToastEx.makeText(context, R.string.title_setup_password_missing, Toast.LENGTH_LONG).show(); + else { + if (password1.equals(password2)) { + int start = etBody.getSelectionStart(); + int end = etBody.getSelectionEnd(); + boolean selection = (start >= 0 && start < end); + if (selection) { + Bundle args = new Bundle(); + args.putCharSequence("text", edit.subSequence(start, end)); + args.putString("password", password1); + args.putInt("start", start); + args.putInt("end", end); + + new SimpleTask() { + @Override + protected String onExecute(Context context, Bundle args) throws Throwable { + Spanned text = (Spanned) args.getCharSequence("text"); + String password = args.getString("password"); + String html = HtmlHelper.toHtml(text, context); + + SecureRandom random = new SecureRandom(); + + byte[] salt = new byte[16]; // 128 bits + random.nextBytes(salt); + + byte[] iv = new byte[12]; // 96 bites + random.nextBytes(iv); + + // Iterations = 120,000; Keylength = 256 bits = 32 bytes + PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 120000, 256); + + SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); + SecretKey key = skf.generateSecret(spec); + + // Authentication tag length = 128 bits = 16 bytes + GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv); + + final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec); + + byte[] cipherText = cipher.doFinal(html.getBytes(StandardCharsets.UTF_8)); + + ByteBuffer out = ByteBuffer.allocate(1 + salt.length + iv.length + cipherText.length); + out.put((byte) 1); // version + out.put(salt); + out.put(iv); + out.put(cipherText); + + String fragment = Base64.encodeToString(out.array(), Base64.URL_SAFE | Base64.NO_WRAP); + String url = "https://email.faircode.eu/decrypt/#" + fragment; + + return url; + } + + @Override + protected void onExecuted(Bundle args, String url) { + if (etBody.getSelectionStart() != start || + etBody.getSelectionEnd() != end) + return; + + String title = context.getString(R.string.title_decrypt); + edit.delete(start, end); + edit.insert(start, title); + edit.setSpan(new URLSpan(url), start, start + title.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + etBody.setSelection(start + title.length()); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Log.e(ex); + ToastEx.makeText(context, ex.toString(), Toast.LENGTH_LONG).show(); + } + }.execute(context, owner, args, "protect"); + } + } else + ToastEx.makeText(context, R.string.title_setup_password_different, Toast.LENGTH_LONG).show(); + } + } + }) + .setNegativeButton(android.R.string.cancel, null) + .create(); + + dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + // WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + dialog.show(); + + return true; + } + private boolean setCode(MenuItem item) { Log.breadcrumb("style", "action", "code"); _setSize(HtmlHelper.FONT_SMALL); diff --git a/app/src/main/res/layout/dialog_password_protect.xml b/app/src/main/res/layout/dialog_password_protect.xml new file mode 100644 index 0000000000..1064854508 --- /dev/null +++ b/app/src/main/res/layout/dialog_password_protect.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/popup_style.xml b/app/src/main/res/menu/popup_style.xml index 9234fdf148..7ebe5112e7 100644 --- a/app/src/main/res/menu/popup_style.xml +++ b/app/src/main/res/menu/popup_style.xml @@ -178,8 +178,17 @@ + + + + + android:orderInCategory="15"> Superscript Strikethrough Code + Password protect + AES/GCM 256 bit PBKDF2/SHA-256/120,000 Clear formatting Insert link Address @@ -1954,7 +1956,6 @@ Select block Bracket Quote - Protect Add Open with Info