From d235ab4e19590a8c196818bd98bebf5ce379f301 Mon Sep 17 00:00:00 2001 From: M66B Date: Mon, 2 Dec 2019 12:17:18 +0100 Subject: [PATCH] S/MIME verify signature --- ATTRIBUTION.md | 1 + app/src/main/assets/ATTRIBUTION.md | 1 + .../eu/faircode/email/AdapterMessage.java | 4 +- .../eu/faircode/email/FragmentMessages.java | 194 +++++++++++++----- .../java/eu/faircode/email/MessageHelper.java | 3 +- 5 files changed, 145 insertions(+), 58 deletions(-) diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 8b027e2d6e..3138144edd 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -18,3 +18,4 @@ FairEmail uses: * [ReLinker](https://github.com/KeepSafe/ReLinker). Copyright 2015 - 2016 KeepSafe Software, Inc. [Apache License 2.0](https://github.com/KeepSafe/ReLinker/blob/master/LICENSE). * [Markwon](https://github.com/noties/Markwon). Copyright 2019 Dimitry Ivanov. [Apache License 2.0](https://github.com/noties/Markwon/blob/master/LICENSE). * [Color Picker](https://github.com/QuadFlask/colorpicker). Copyright 2014-2017 QuadFlask. [Apache License 2.0](https://github.com/QuadFlask/colorpicker#user-content-license). +* [Bouncy Castle](https://www.bouncycastle.org/). Copyright (c) 2000 - 2019 The Legion of the Bouncy Castle Inc. [MIT License](https://www.bouncycastle.org/licence.html). diff --git a/app/src/main/assets/ATTRIBUTION.md b/app/src/main/assets/ATTRIBUTION.md index 8b027e2d6e..3138144edd 100644 --- a/app/src/main/assets/ATTRIBUTION.md +++ b/app/src/main/assets/ATTRIBUTION.md @@ -18,3 +18,4 @@ FairEmail uses: * [ReLinker](https://github.com/KeepSafe/ReLinker). Copyright 2015 - 2016 KeepSafe Software, Inc. [Apache License 2.0](https://github.com/KeepSafe/ReLinker/blob/master/LICENSE). * [Markwon](https://github.com/noties/Markwon). Copyright 2019 Dimitry Ivanov. [Apache License 2.0](https://github.com/noties/Markwon/blob/master/LICENSE). * [Color Picker](https://github.com/QuadFlask/colorpicker). Copyright 2014-2017 QuadFlask. [Apache License 2.0](https://github.com/QuadFlask/colorpicker#user-content-license). +* [Bouncy Castle](https://www.bouncycastle.org/). Copyright (c) 2000 - 2019 The Legion of the Bouncy Castle Inc. [MIT License](https://www.bouncycastle.org/licence.html). diff --git a/app/src/main/java/eu/faircode/email/AdapterMessage.java b/app/src/main/java/eu/faircode/email/AdapterMessage.java index a86bc9ebab..05a2b89106 100644 --- a/app/src/main/java/eu/faircode/email/AdapterMessage.java +++ b/app/src/main/java/eu/faircode/email/AdapterMessage.java @@ -1569,7 +1569,9 @@ public class AdapterMessage extends RecyclerView.Adapter() { @Override protected Boolean onExecute(Context context, Bundle args) throws Throwable { long id = args.getLong("id"); @@ -4351,75 +4366,142 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. if (alias == null) throw new IllegalArgumentException("Key alias missing"); - PrivateKey pk = KeyChain.getPrivateKey(context, alias); - if (pk == null) - throw new IllegalArgumentException("Private key missing"); - DB db = DB.getInstance(context); - File input = null; - List attachments = db.attachment().getAttachments(id); - for (EntityAttachment attachment : attachments) - if (EntityAttachment.SMIME_MESSAGE.equals(attachment.encryption)) { - input = attachment.getFile(context); - break; - } - if (input == null) - throw new IllegalArgumentException("Encrypted message missing"); + if (EntityMessage.SMIME_SIGNONLY.equals(type)) { + // Check public key + X509Certificate[] chain = KeyChain.getCertificateChain(context, alias); + if (chain == null || chain.length == 0) + throw new IllegalArgumentException("Public key missing"); - FileInputStream fis = new FileInputStream(input); - CMSEnvelopedData envelopedData = new CMSEnvelopedData(fis); + // Get content/signature + File content = null; + File signature = null; + List attachments = db.attachment().getAttachments(id); + for (EntityAttachment attachment : attachments) + if (EntityAttachment.SMIME_SIGNATURE.equals(attachment.encryption)) + signature = attachment.getFile(context); + else if (EntityAttachment.SMIME_CONTENT.equals(attachment.encryption)) + content = attachment.getFile(context); - Collection recipients = envelopedData.getRecipientInfos().getRecipients(); - KeyTransRecipientInformation recipientInfo = (KeyTransRecipientInformation) recipients.iterator().next(); - JceKeyTransRecipient recipient = new JceKeyTransEnvelopedRecipient(pk); - InputStream is = recipientInfo.getContentStream(recipient).getContentStream(); + if (content == null) + throw new IllegalArgumentException("Signed content missing"); + if (signature == null) + throw new IllegalArgumentException("Signature missing"); - // Decode message - Properties props = MessageHelper.getSessionProperties(); - Session isession = Session.getInstance(props, null); - MimeMessage imessage = new MimeMessage(isession, is); - MessageHelper helper = new MessageHelper(imessage); - MessageHelper.MessageParts parts = helper.getMessageParts(context); + // Build signed data + CMSProcessable signedContent = new CMSProcessableFile(content); + FileInputStream fis = new FileInputStream(signature); + CMSSignedData signedData = new CMSSignedData(signedContent, fis); - try { - db.beginTransaction(); + // Check signature + Store store = signedData.getCertificates(); + SignerInformationStore signerInfos = signedData.getSignerInfos(); + for (SignerInformation signer : signerInfos.getSigners()) + for (Object cert : store.getMatches(signer.getSID())) { + X509CertificateHolder certHolder = (X509CertificateHolder) cert; + if (signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(certHolder))) { + // Check validity + Date now = new Date(); + boolean valid; + try { + chain[0].checkValidity(now); + valid = certHolder.isValidOn(now); + } catch (CertificateException ignored) { + valid = false; + } - // Write decrypted body - String html = parts.getHtml(context); - Helper.writeText(EntityMessage.getFile(context, id), html); - - // Remove existing attachments - db.attachment().deleteAttachments(id); - - // Add decrypted attachments - List remotes = parts.getAttachments(); - for (int index = 0; index < remotes.size(); index++) { - EntityAttachment remote = remotes.get(index); - remote.message = id; - remote.sequence = index + 1; - remote.id = db.attachment().insertAttachment(remote); - try { - parts.downloadAttachment(context, index, remote); - } catch (Throwable ex) { - Log.e(ex); + // Check public key + PublicKey pubkey = chain[0].getPublicKey(); + if (valid && + signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(pubkey))) + return true; + else + return null; + } } + + return false; + } else { + // Check private key + PrivateKey privkey = KeyChain.getPrivateKey(context, alias); + if (privkey == null) + throw new IllegalArgumentException("Private key missing"); + + // Get encrypted message + File input = null; + List attachments = db.attachment().getAttachments(id); + for (EntityAttachment attachment : attachments) + if (EntityAttachment.SMIME_MESSAGE.equals(attachment.encryption)) { + input = attachment.getFile(context); + break; + } + if (input == null) + throw new IllegalArgumentException("Encrypted message missing"); + + // Build enveloped data + FileInputStream fis = new FileInputStream(input); + CMSEnvelopedData envelopedData = new CMSEnvelopedData(fis); + + // Decrypt message + Collection recipients = envelopedData.getRecipientInfos().getRecipients(); + KeyTransRecipientInformation recipientInfo = (KeyTransRecipientInformation) recipients.iterator().next(); + JceKeyTransRecipient recipient = new JceKeyTransEnvelopedRecipient(privkey); + InputStream is = recipientInfo.getContentStream(recipient).getContentStream(); + + // Decode message + Properties props = MessageHelper.getSessionProperties(); + Session isession = Session.getInstance(props, null); + MimeMessage imessage = new MimeMessage(isession, is); + MessageHelper helper = new MessageHelper(imessage); + MessageHelper.MessageParts parts = helper.getMessageParts(context); + + try { + db.beginTransaction(); + + // Write decrypted body + String html = parts.getHtml(context); + Helper.writeText(EntityMessage.getFile(context, id), html); + + // Remove existing attachments + db.attachment().deleteAttachments(id); + + // Add decrypted attachments + List remotes = parts.getAttachments(); + for (int index = 0; index < remotes.size(); index++) { + EntityAttachment remote = remotes.get(index); + remote.message = id; + remote.sequence = index + 1; + remote.id = db.attachment().insertAttachment(remote); + try { + parts.downloadAttachment(context, index, remote); + } catch (Throwable ex) { + Log.e(ex); + } + } + + db.message().setMessageEncrypt(id, parts.getEncryption()); + db.message().setMessageStored(id, new Date().getTime()); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); } - db.message().setMessageEncrypt(id, parts.getEncryption()); - db.message().setMessageStored(id, new Date().getTime()); - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); + return null; } - - return true; } @Override - protected void onExecuted(Bundle args, Object data) { + protected void onExecuted(Bundle args, Boolean result) { int type = args.getInt("type"); + if (EntityMessage.SMIME_SIGNONLY.equals(type)) + if (result == null) + Snackbar.make(view, R.string.title_signature_unconfirmed, Snackbar.LENGTH_LONG).show(); + else if (result) + Snackbar.make(view, R.string.title_signature_valid, Snackbar.LENGTH_LONG).show(); + else + Snackbar.make(view, R.string.title_signature_invalid, Snackbar.LENGTH_LONG).show(); } @Override diff --git a/app/src/main/java/eu/faircode/email/MessageHelper.java b/app/src/main/java/eu/faircode/email/MessageHelper.java index 64ec08317e..d080056c88 100644 --- a/app/src/main/java/eu/faircode/email/MessageHelper.java +++ b/app/src/main/java/eu/faircode/email/MessageHelper.java @@ -1179,7 +1179,8 @@ public class MessageHelper { File file = EntityAttachment.getFile(context, local.id, local.name); db.attachment().setProgress(local.id, null); - if (EntityAttachment.PGP_CONTENT.equals(apart.encrypt)) { + if (EntityAttachment.PGP_CONTENT.equals(apart.encrypt) || + EntityAttachment.SMIME_CONTENT.equals(apart.encrypt)) { ContentType ct = new ContentType(apart.part.getContentType()); String boundary = ct.getParameter("boundary"); if (TextUtils.isEmpty(boundary))