S/MIME verify signature

pull/168/head
M66B 5 years ago
parent ab0ad41d89
commit d235ab4e19

@ -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).

@ -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).

@ -1569,7 +1569,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
cowner.start();
// Show encrypt actions
ibVerify.setVisibility(EntityMessage.PGP_SIGNONLY.equals(message.encrypt)
ibVerify.setVisibility(false ||
EntityMessage.PGP_SIGNONLY.equals(message.encrypt) ||
EntityMessage.SMIME_SIGNONLY.equals(message.encrypt)
? View.VISIBLE : View.GONE);
ibDecrypt.setVisibility(args.getBoolean("inline_encrypted") ||
EntityMessage.PGP_SIGNENCRYPT.equals(message.encrypt) ||

@ -109,11 +109,19 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.sun.mail.util.FolderClosedIOException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cms.CMSEnvelopedData;
import org.bouncycastle.cms.CMSProcessable;
import org.bouncycastle.cms.CMSProcessableFile;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.KeyTransRecipientInformation;
import org.bouncycastle.cms.RecipientInformation;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationStore;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
import org.bouncycastle.cms.jcajce.JceKeyTransEnvelopedRecipient;
import org.bouncycastle.cms.jcajce.JceKeyTransRecipient;
import org.bouncycastle.util.Store;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.openintents.openpgp.OpenPgpError;
@ -131,6 +139,9 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.Collator;
import java.text.DateFormat;
import java.text.NumberFormat;
@ -3904,7 +3915,8 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
boolean auto = intent.getBooleanExtra("auto", false);
int type = intent.getIntExtra("type", EntityMessage.ENCRYPT_NONE);
if (EntityMessage.SMIME_SIGNENCRYPT.equals(type)) {
if (EntityMessage.SMIME_SIGNONLY.equals(type) ||
EntityMessage.SMIME_SIGNENCRYPT.equals(type)) {
final Bundle args = new Bundle();
args.putLong("id", id);
args.putInt("type", type);
@ -4170,6 +4182,9 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
out = new FileOutputStream(plain);
} else if (EntityAttachment.PGP_SIGNATURE.equals(attachment.encryption)) {
if (!attachment.available)
throw new IllegalArgumentException(context.getString(R.string.title_attachments_missing));
File file = attachment.getFile(context);
byte[] signature = new byte[(int) file.length()];
try (FileInputStream fis = new FileInputStream(file)) {
@ -4341,7 +4356,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
}
private void onSmime(Bundle args) {
new SimpleTask() {
new SimpleTask<Boolean>() {
@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<EntityAttachment> 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");
FileInputStream fis = new FileInputStream(input);
CMSEnvelopedData envelopedData = new CMSEnvelopedData(fis);
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");
// Get content/signature
File content = null;
File signature = null;
List<EntityAttachment> 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);
if (content == null)
throw new IllegalArgumentException("Signed content missing");
if (signature == null)
throw new IllegalArgumentException("Signature missing");
// Build signed data
CMSProcessable signedContent = new CMSProcessableFile(content);
FileInputStream fis = new FileInputStream(signature);
CMSSignedData signedData = new CMSSignedData(signedContent, fis);
// 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;
}
Collection<RecipientInformation> recipients = envelopedData.getRecipientInfos().getRecipients();
KeyTransRecipientInformation recipientInfo = (KeyTransRecipientInformation) recipients.iterator().next();
JceKeyTransRecipient recipient = new JceKeyTransEnvelopedRecipient(pk);
InputStream is = recipientInfo.getContentStream(recipient).getContentStream();
// Check public key
PublicKey pubkey = chain[0].getPublicKey();
if (valid &&
signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(pubkey)))
return true;
else
return null;
}
}
// 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);
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<EntityAttachment> 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");
try {
db.beginTransaction();
// Build enveloped data
FileInputStream fis = new FileInputStream(input);
CMSEnvelopedData envelopedData = new CMSEnvelopedData(fis);
// Write decrypted body
String html = parts.getHtml(context);
Helper.writeText(EntityMessage.getFile(context, id), html);
// Decrypt message
Collection<RecipientInformation> 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);
// Remove existing attachments
db.attachment().deleteAttachments(id);
try {
db.beginTransaction();
// Add decrypted attachments
List<EntityAttachment> 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);
// 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<EntityAttachment> 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.message().setMessageEncrypt(id, parts.getEncryption());
db.message().setMessageStored(id, new Date().getTime());
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return true;
return null;
}
}
@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

@ -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))

Loading…
Cancel
Save