From a4e5403f9d2e48bb7d2427a81590708d6da174f2 Mon Sep 17 00:00:00 2001 From: M66B Date: Wed, 4 Dec 2019 13:10:33 +0100 Subject: [PATCH] Added certificate import --- FAQ.md | 8 +- .../eu/faircode/email/AdapterCertificate.java | 35 +++++++- .../eu/faircode/email/DaoCertificate.java | 3 + .../eu/faircode/email/FragmentCompose.java | 85 ++++++++++++++++++- .../main/res/layout/dialog_certificate.xml | 16 +++- app/src/main/res/layout/item_certificate.xml | 1 - app/src/main/res/values/strings.xml | 3 +- 7 files changed, 142 insertions(+), 9 deletions(-) diff --git a/FAQ.md b/FAQ.md index 5931d23b29..373f40a718 100644 --- a/FAQ.md +++ b/FAQ.md @@ -86,7 +86,7 @@ Related questions: * ~~Unified starred messages view~~ (there is already a special search for this) * ~~Notification move action~~ * Search for settings: low priority -* S/MIME: waiting for sponsoring +* S/MIME support Anything on this list is in random order and *might* be added in the near future. @@ -591,6 +591,12 @@ Signed-only messages are supported, encrypted-only messages are not supported. For S/MIME support, please see the [planned features](#user-content-planned-features). +Extract a public key from a S/MIME certificate: + +``` +openssl pkcs12 -in filename.pfx -clcerts -nokeys -out cert.pem +``` + Please see [this comment](https://forum.xda-developers.com/showpost.php?p=79444379&postcount=5609) about [these vulnerabilities](https://amp.thehackernews.com/thn/2019/04/email-signature-spoofing.html). diff --git a/app/src/main/java/eu/faircode/email/AdapterCertificate.java b/app/src/main/java/eu/faircode/email/AdapterCertificate.java index 62d6e932e4..1da683a3e8 100644 --- a/app/src/main/java/eu/faircode/email/AdapterCertificate.java +++ b/app/src/main/java/eu/faircode/email/AdapterCertificate.java @@ -20,6 +20,7 @@ package eu.faircode.email; */ import android.content.Context; +import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -43,7 +44,7 @@ public class AdapterCertificate extends RecyclerView.Adapter items = new ArrayList<>(); - public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { private View view; private TextView tvEmail; private TextView tvSubject; @@ -66,8 +67,40 @@ public class AdapterCertificate extends RecyclerView.Adapter() { + @Override + protected Void onExecute(Context context, Bundle args) throws Throwable { + long id = args.getLong("id"); + + DB db = DB.getInstance(context); + db.certificate().deleteCertificate(id); + + return null; + } + + @Override + protected void onException(Bundle args, Throwable ex) { + // TODO: report error + } + }.execute(context, owner, args, "certificate:delete"); + + return true; + } + private void wire() { view.setOnClickListener(this); + view.setOnLongClickListener(this); } private void unwire() { diff --git a/app/src/main/java/eu/faircode/email/DaoCertificate.java b/app/src/main/java/eu/faircode/email/DaoCertificate.java index d9a54cd226..0fee73dbf2 100644 --- a/app/src/main/java/eu/faircode/email/DaoCertificate.java +++ b/app/src/main/java/eu/faircode/email/DaoCertificate.java @@ -47,4 +47,7 @@ public interface DaoCertificate { @Insert long insertCertificate(EntityCertificate certificate); + + @Query("DELETE FROM certificate WHERE id = :id") + void deleteCertificate(long id); } diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index 84b6d8dfa7..5f7e4f9b8e 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -71,6 +71,7 @@ import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.ArrayAdapter; +import android.widget.Button; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.EditText; @@ -119,6 +120,8 @@ import org.bouncycastle.operator.OutputEncryptor; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; import org.bouncycastle.util.Store; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.nodes.TextNode; @@ -135,6 +138,7 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.OutputStream; import java.net.UnknownHostException; import java.security.PrivateKey; @@ -169,6 +173,7 @@ import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; import javax.mail.internet.ParseException; import javax.mail.util.ByteArrayDataSource; +import javax.security.auth.x500.X500Principal; import static android.app.Activity.RESULT_CANCELED; import static android.app.Activity.RESULT_OK; @@ -255,7 +260,7 @@ public class FragmentCompose extends FragmentBase { private static final int REQUEST_LINK = 12; private static final int REQUEST_DISCARD = 13; private static final int REQUEST_SEND = 14; - private static final int REQUEST_SELECT_CERTIFICATE = 15; + private static final int REQUEST_CERTIFICATE = 15; @Override public void onCreate(Bundle savedInstanceState) { @@ -1248,15 +1253,20 @@ public class FragmentCompose extends FragmentBase { @Override public void run() { try { + String email = null; + if (draft.to != null && draft.to.length == 1) + email = ((InternetAddress) draft.to[0]).getAddress(); + Bundle args = new Bundle(); args.putLong("id", draft.id); args.putInt("type", draft.encrypt); + args.putString("email", email); args.putString("alias", alias); if (EntityMessage.SMIME_SIGNENCRYPT.equals(draft.encrypt)) { FragmentDialogCertificate fragment = new FragmentDialogCertificate(); fragment.setArguments(args); - fragment.setTargetFragment(FragmentCompose.this, REQUEST_SELECT_CERTIFICATE); + fragment.setTargetFragment(FragmentCompose.this, REQUEST_CERTIFICATE); fragment.show(getParentFragmentManager(), "compose:certificate"); } else onSmime(args); @@ -1379,7 +1389,7 @@ public class FragmentCompose extends FragmentBase { if (resultCode == RESULT_OK) onActionSend(); break; - case REQUEST_SELECT_CERTIFICATE: + case REQUEST_CERTIFICATE: if (resultCode == RESULT_OK && data != null) onSmime(data.getBundleExtra("args")); break; @@ -3938,11 +3948,16 @@ public class FragmentCompose extends FragmentBase { } public static class FragmentDialogCertificate extends FragmentDialogBase { + private String email; + @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + email = getArguments().getString("email"); + View dview = LayoutInflater.from(getContext()).inflate(R.layout.dialog_certificate, null); final RecyclerView rvCertificate = dview.findViewById(R.id.rvCertificate); + final Button btnImport = dview.findViewById(R.id.btnImport); final ProgressBar pbWait = dview.findViewById(R.id.pbWait); final Dialog dialog = new AlertDialog.Builder(getContext()) @@ -3964,6 +3979,22 @@ public class FragmentCompose extends FragmentBase { }); rvCertificate.setAdapter(adapter); + btnImport.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + Helper.openAdvanced(intent); + PackageManager pm = getContext().getPackageManager(); + if (intent.resolveActivity(pm) == null) + ToastEx.makeText(getContext(), R.string.title_no_saf, Toast.LENGTH_LONG).show(); + else + startActivityForResult(Helper.getChooser(getContext(), intent), 1); + } + }); + btnImport.setEnabled(email != null); + rvCertificate.setVisibility(View.GONE); pbWait.setVisibility(View.VISIBLE); @@ -3979,6 +4010,54 @@ public class FragmentCompose extends FragmentBase { return dialog; } + + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + if (resultCode == RESULT_OK && data != null) { + Uri uri = data.getData(); + if (uri != null) { + Bundle args = new Bundle(); + args.putParcelable("uri", uri); + + new SimpleTask() { + @Override + protected Void onExecute(Context context, Bundle args) throws Throwable { + Uri uri = args.getParcelable("uri"); + + PemObject pem; + try (InputStream is = context.getContentResolver().openInputStream(uri)) { + pem = new PemReader(new InputStreamReader(is)).readPemObject(); + } + + ByteArrayInputStream bis = new ByteArrayInputStream(pem.getContent()); + CertificateFactory fact = CertificateFactory.getInstance("X.509"); + X509Certificate cert = (X509Certificate) fact.generateCertificate(bis); + + String fingerprint = Helper.sha256(cert.getEncoded()); + + DB db = DB.getInstance(context); + EntityCertificate record = db.certificate().getCertificate(fingerprint, email); + if (record == null) { + record = new EntityCertificate(); + record.fingerprint = Helper.sha256(cert.getEncoded()); + record.email = email; + record.subject = cert.getSubjectX500Principal().getName(X500Principal.RFC2253); + record.setEncoded(cert.getEncoded()); + record.id = db.certificate().insertCertificate(record); + } + // TODO: report exists + + return null; + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Helper.unexpectedError(getParentFragmentManager(), ex); + } + }.execute(this, args, "compose:cert"); + } + } + } } public static class FragmentDialogSend extends FragmentDialogBase { diff --git a/app/src/main/res/layout/dialog_certificate.xml b/app/src/main/res/layout/dialog_certificate.xml index ff77d6a8e4..6deab15eb5 100644 --- a/app/src/main/res/layout/dialog_certificate.xml +++ b/app/src/main/res/layout/dialog_certificate.xml @@ -2,19 +2,31 @@ + android:layout_height="wrap_content" + android:padding="12dp"> +