Protected content: improved repeated password check

pull/209/head
M66B 2 years ago
parent d89b3edc73
commit 69d5c619a3

@ -19,7 +19,6 @@ package eu.faircode.email;
Copyright 2018-2022 by Marcel Bokhorst (M66B) Copyright 2018-2022 by Marcel Bokhorst (M66B)
*/ */
import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
@ -39,6 +38,7 @@ import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import android.text.TextPaint; import android.text.TextPaint;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.style.AlignmentSpan; import android.text.style.AlignmentSpan;
import android.text.style.BackgroundColorSpan; import android.text.style.BackgroundColorSpan;
import android.text.style.BulletSpan; import android.text.style.BulletSpan;
@ -593,7 +593,7 @@ public class StyleHelper {
} }
}); });
Dialog dialog = new AlertDialog.Builder(context) AlertDialog dialog = new AlertDialog.Builder(context)
.setView(dview) .setView(dview)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override @Override
@ -601,112 +601,102 @@ public class StyleHelper {
String password1 = etPassword1.getEditText().getText().toString(); String password1 = etPassword1.getEditText().getText().toString();
String password2 = etPassword2.getEditText().getText().toString(); String password2 = etPassword2.getEditText().getText().toString();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); int start = etBody.getSelectionStart();
boolean debug = prefs.getBoolean("debug", false); int end = etBody.getSelectionEnd();
boolean selection = (start >= 0 && start < end);
if (TextUtils.isEmpty(password1) && !(debug || BuildConfig.DEBUG)) if (selection) {
ToastEx.makeText(context, R.string.title_setup_password_missing, Toast.LENGTH_LONG).show(); Bundle args = new Bundle();
else { args.putCharSequence("text", edit.subSequence(start, end));
if (password1.equals(password2)) { args.putString("password", password1);
int start = etBody.getSelectionStart(); args.putInt("start", start);
int end = etBody.getSelectionEnd(); args.putInt("end", end);
boolean selection = (start >= 0 && start < end);
if (selection) { new SimpleTask<String>() {
Bundle args = new Bundle(); @Override
args.putCharSequence("text", edit.subSequence(start, end)); protected String onExecute(Context context, Bundle args) throws Throwable {
args.putString("password", password1); Spanned text = (Spanned) args.getCharSequence("text");
args.putInt("start", start); String password = args.getString("password");
args.putInt("end", end);
Drawable d = ContextCompat.getDrawable(context, R.drawable.twotone_image_24);
new SimpleTask<String>() { d.setTint(Color.GRAY);
@Override Bitmap bm = Bitmap.createBitmap(24, 24, Bitmap.Config.ARGB_8888);
protected String onExecute(Context context, Bundle args) throws Throwable { Canvas c = new Canvas(bm);
Spanned text = (Spanned) args.getCharSequence("text"); d.setBounds(0, 0, c.getWidth(), c.getHeight());
String password = args.getString("password"); d.draw(c);
Drawable d = ContextCompat.getDrawable(context, R.drawable.twotone_image_24); ByteArrayOutputStream bos = new ByteArrayOutputStream();
d.setTint(Color.GRAY); bm.compress(Bitmap.CompressFormat.PNG, 100, bos);
Bitmap bm = Bitmap.createBitmap(24, 24, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bm); StringBuilder sb = new StringBuilder();
d.setBounds(0, 0, c.getWidth(), c.getHeight()); sb.append("data:image/png;base64,");
d.draw(c); sb.append(Base64.encodeToString(bos.toByteArray(), Base64.NO_WRAP));
ByteArrayOutputStream bos = new ByteArrayOutputStream(); String html = HtmlHelper.toHtml(text, context);
bm.compress(Bitmap.CompressFormat.PNG, 100, bos); Document doc = JsoupEx.parse(html);
for (Element img : doc.select("img"))
StringBuilder sb = new StringBuilder(); img.attr("src", sb.toString());
sb.append("data:image/png;base64,"); html = doc.body().html();
sb.append(Base64.encodeToString(bos.toByteArray(), Base64.NO_WRAP));
if (html.length() > MAX_PROTECTED_TEXT)
String html = HtmlHelper.toHtml(text, context); throw new IllegalArgumentException(context.getString(R.string.title_style_protect_size));
Document doc = JsoupEx.parse(html);
for (Element img : doc.select("img")) SecureRandom random = new SecureRandom();
img.attr("src", sb.toString());
html = doc.body().html(); byte[] salt = new byte[16]; // 128 bits
random.nextBytes(salt);
if (html.length() > MAX_PROTECTED_TEXT)
throw new IllegalArgumentException(context.getString(R.string.title_style_protect_size)); byte[] iv = new byte[12]; // 96 bites
random.nextBytes(iv);
SecureRandom random = new SecureRandom();
// Iterations = 120,000; Keylength = 256 bits = 32 bytes
byte[] salt = new byte[16]; // 128 bits PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 120000, 256);
random.nextBytes(salt);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
byte[] iv = new byte[12]; // 96 bites SecretKey key = skf.generateSecret(spec);
random.nextBytes(iv);
// Authentication tag length = 128 bits = 16 bytes
// Iterations = 120,000; Keylength = 256 bits = 32 bytes GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv);
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 120000, 256);
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
SecretKey key = skf.generateSecret(spec);
byte[] cipherText = cipher.doFinal(html.getBytes(StandardCharsets.UTF_8));
// Authentication tag length = 128 bits = 16 bytes
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv); ByteBuffer out = ByteBuffer.allocate(1 + salt.length + iv.length + cipherText.length);
out.put((byte) 1); // version
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); out.put(salt);
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec); out.put(iv);
out.put(cipherText);
byte[] cipherText = cipher.doFinal(html.getBytes(StandardCharsets.UTF_8));
String fragment = Base64.encodeToString(out.array(), Base64.URL_SAFE | Base64.NO_WRAP);
ByteBuffer out = ByteBuffer.allocate(1 + salt.length + iv.length + cipherText.length); String url = "https://email.faircode.eu/decrypt/#" + fragment;
out.put((byte) 1); // version
out.put(salt); return url;
out.put(iv); }
out.put(cipherText);
@Override
String fragment = Base64.encodeToString(out.array(), Base64.URL_SAFE | Base64.NO_WRAP); protected void onExecuted(Bundle args, String url) {
String url = "https://email.faircode.eu/decrypt/#" + fragment; if (etBody.getSelectionStart() != start ||
etBody.getSelectionEnd() != end)
return url; return;
}
String title = context.getString(R.string.title_decrypt);
@Override edit.delete(start, end);
protected void onExecuted(Bundle args, String url) { edit.insert(start, title);
if (etBody.getSelectionStart() != start || edit.setSpan(new URLSpan(url), start, start + title.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
etBody.getSelectionEnd() != end) etBody.setSelection(start + title.length());
return; }
String title = context.getString(R.string.title_decrypt); @Override
edit.delete(start, end); protected void onException(Bundle args, Throwable ex) {
edit.insert(start, title); if (ex instanceof IllegalArgumentException)
edit.setSpan(new URLSpan(url), start, start + title.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); ToastEx.makeText(context, ex.getMessage(), Toast.LENGTH_LONG).show();
etBody.setSelection(start + title.length()); else {
} Log.e(ex);
ToastEx.makeText(context, ex.toString(), Toast.LENGTH_LONG).show();
@Override }
protected void onException(Bundle args, Throwable ex) {
if (ex instanceof IllegalArgumentException)
ToastEx.makeText(context, ex.getMessage(), Toast.LENGTH_LONG).show();
else {
Log.e(ex);
ToastEx.makeText(context, ex.toString(), Toast.LENGTH_LONG).show();
}
}
}.execute(context, owner, args, "protect");
} }
} else }.execute(context, owner, args, "protect");
ToastEx.makeText(context, R.string.title_setup_password_different, Toast.LENGTH_LONG).show();
} }
} }
}) })
@ -717,6 +707,34 @@ public class StyleHelper {
// WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE // WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
dialog.show(); dialog.show();
Button btnOk = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
TextWatcher w = 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 p1 = etPassword1.getEditText().getText().toString();
String p2 = etPassword2.getEditText().getText().toString();
btnOk.setEnabled(!TextUtils.isEmpty(p1) && p1.equals(p2));
etPassword2.setHint(!TextUtils.isEmpty(p2) && !p2.equals(p1)
? R.string.title_setup_password_different
: R.string.title_setup_password_repeat);
}
};
etPassword1.getEditText().addTextChangedListener(w);
etPassword2.getEditText().addTextChangedListener(w);
w.afterTextChanged(null);
return true; return true;
} }

Loading…
Cancel
Save