diff --git a/app/src/main/java/eu/faircode/email/AdapterMessage.java b/app/src/main/java/eu/faircode/email/AdapterMessage.java index c89e7ac945..fa918e19f8 100644 --- a/app/src/main/java/eu/faircode/email/AdapterMessage.java +++ b/app/src/main/java/eu/faircode/email/AdapterMessage.java @@ -5854,6 +5854,16 @@ public class AdapterMessage extends RecyclerView.Adapter CLEAR_STYLES = Collections.unmodifiableList(Arrays.asList( @@ -641,15 +656,15 @@ public class StyleHelper { random.nextBytes(iv); // Iterations = 120,000; Keylength = 256 bits = 32 bytes - PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 120000, 256); + PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, DECRYPT_ITERATIONS, DECRYPT_KEYLEN); - SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); + SecretKeyFactory skf = SecretKeyFactory.getInstance(DECRYPT_DERIVATION); SecretKey key = skf.generateSecret(spec); // Authentication tag length = 128 bits = 16 bytes - GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv); + GCMParameterSpec parameterSpec = new GCMParameterSpec(DECRYPT_TAGLEN, iv); - final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + final Cipher cipher = Cipher.getInstance(DECRYPT_TRANSFORMATION); cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec); byte[] cipherText = cipher.doFinal(html.getBytes(StandardCharsets.UTF_8)); @@ -1463,5 +1478,203 @@ public class StyleHelper { } } + public static boolean isProtectedContent(Uri uri) { + return uri.toString().startsWith(DECRYPT_URL); + } + + public static final class FragmentDialogDecrypt extends FragmentDialogBase { + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Bundle args = getArguments(); + + final Context context = getContext(); + final View view = LayoutInflater.from(context).inflate(R.layout.dialog_decrypt, null); + final TextInputLayout tilPassword = view.findViewById(R.id.tilPassword); + final Button btnDecrypt = view.findViewById(R.id.btnDecrypt); + final TextView tvContent = view.findViewById(R.id.tvContent); + final TextView tvError = view.findViewById(R.id.tvError); + final TextView tvErrorDetail = view.findViewById(R.id.tvErrorDetail); + + tilPassword.setVisibility(View.VISIBLE); + btnDecrypt.setVisibility(View.VISIBLE); + tvContent.setVisibility(View.GONE); + tvError.setVisibility(View.GONE); + tvErrorDetail.setVisibility(View.GONE); + + String password = args.getString("password"); + tilPassword.getEditText().setText(password); + btnDecrypt.setEnabled(!TextUtils.isEmpty(password)); + + tilPassword.getEditText().addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Do nothing + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Do nothing + } + + @Override + public void afterTextChanged(Editable s) { + String password = s.toString(); + btnDecrypt.setEnabled(!TextUtils.isEmpty(password)); + args.putString("password", password); + } + }); + + btnDecrypt.setEnabled(false); + + tvContent.setMovementMethod(new ArrowKeyMovementMethod() { + private GestureDetector gestureDetector = new GestureDetector(context, + new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapUp(MotionEvent event) { + return onClick(event); + } + + private boolean onClick(MotionEvent event) { + Spannable buffer = (Spannable) tvContent.getText(); + int off = Helper.getOffset(tvContent, buffer, event); + + URLSpan[] link = buffer.getSpans(off, off, URLSpan.class); + if (link.length > 0) { + String url = link[0].getURL(); + Uri uri = Uri.parse(url); + + int start = buffer.getSpanStart(link[0]); + int end = buffer.getSpanEnd(link[0]); + String title = (start < 0 || end < 0 || end <= start + ? null : buffer.subSequence(start, end).toString()); + if (url.equals(title)) + title = null; + + Bundle args = new Bundle(); + args.putParcelable("uri", uri); + args.putString("title", title); + + FragmentDialogOpenLink fragment = new FragmentDialogOpenLink(); + fragment.setArguments(args); + fragment.show(getParentFragmentManager(), "open:link"); + + return true; + } + + return false; + } + }); + + @Override + public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { + return gestureDetector.onTouchEvent(event); + } + }); + + btnDecrypt.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SimpleTask() { + @Override + protected void onPreExecute(Bundle args) { + tilPassword.setEnabled(false); + btnDecrypt.setEnabled(false); + tvError.setVisibility(View.GONE); + tvErrorDetail.setVisibility(View.GONE); + } + + @Override + protected void onPostExecute(Bundle args) { + tilPassword.setEnabled(true); + btnDecrypt.setEnabled(true); + } + + @Override + protected Spanned onExecute(Context context, Bundle args) throws Throwable { + Uri uri = args.getParcelable("uri"); + String password = args.getString("password"); + + byte[] msg = Base64.decode(uri.getFragment(), Base64.URL_SAFE | Base64.NO_WRAP); + + int version = msg[0]; + + byte[] salt = new byte[16]; // 128 bits + System.arraycopy(msg, 1, salt, 0, salt.length); + + byte[] iv = new byte[12]; // 96 bites + System.arraycopy(msg, 1 + salt.length, iv, 0, iv.length); + + byte[] encrypted = new byte[msg.length - 1 - salt.length - iv.length]; + System.arraycopy(msg, 1 + salt.length + iv.length, encrypted, 0, encrypted.length); + + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DECRYPT_DERIVATION); + KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, DECRYPT_ITERATIONS, DECRYPT_KEYLEN); + SecretKey secret = keyFactory.generateSecret(keySpec); + Cipher cipher = Cipher.getInstance(DECRYPT_TRANSFORMATION); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + cipher.init(Cipher.DECRYPT_MODE, secret, ivSpec); + byte[] decrypted = cipher.doFinal(encrypted); + + String html = new String(decrypted, StandardCharsets.UTF_8); + + return HtmlHelper.fromHtml(html, new HtmlHelper.ImageGetterEx() { + @Override + public Drawable getDrawable(Element element) { + return ImageHelper.decodeImage(context, + -1, element, true, 0, 1.0f, tvContent); + } + }, null, context); + } + + @Override + protected void onExecuted(Bundle args, Spanned content) { + tilPassword.setVisibility(View.GONE); + btnDecrypt.setVisibility(View.GONE); + tvContent.setText(content); + tvContent.setVisibility(View.VISIBLE); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + tvError.setText(ex.getMessage()); + tvErrorDetail.setText(ex.toString()); + tvError.setVisibility(View.VISIBLE); + tvErrorDetail.setVisibility(View.VISIBLE); + } + }.execute(FragmentDialogDecrypt.this, args, "decypt"); + } + }); + + tilPassword.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + btnDecrypt.performClick(); + return true; + } else + return false; + } + }); + + if (!TextUtils.isEmpty(password)) + btnDecrypt.post(new Runnable() { + @Override + public void run() { + btnDecrypt.performClick(); + } + }); + + Dialog dialog = new AlertDialog.Builder(context) + .setView(view) + .setNegativeButton(android.R.string.cancel, null) + .create(); + + dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + + return dialog; + } + } + //TextUtils.dumpSpans(text, new LogPrinter(android.util.Log.INFO, "FairEmail"), "afterTextChanged "); } diff --git a/app/src/main/res/layout/dialog_decrypt.xml b/app/src/main/res/layout/dialog_decrypt.xml new file mode 100644 index 0000000000..82a0751e50 --- /dev/null +++ b/app/src/main/res/layout/dialog_decrypt.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + +