diff --git a/app/build.gradle b/app/build.gradle index 1825732dbb..ce2c3474b6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -211,6 +211,7 @@ dependencies { def relinker_version = "1.3.1" def markwon_version = "4.1.2" def msal_version = "1.0.0" + def bouncycastle_version = "1.64" // https://developer.android.com/jetpack/androidx/releases/ @@ -334,5 +335,6 @@ dependencies { implementation "com.microsoft.identity.client:msal:$msal_version" // https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on - implementation "org.bouncycastle:bcpkix-jdk15to18:1.64" + implementation "org.bouncycastle:bcpkix-jdk15to18:$bouncycastle_version" + //implementation "org.bouncycastle:bcmail-jdk15to18:$bouncycastle_version" } diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index 7a351a251c..4cbb6954a4 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -60,6 +60,7 @@ import android.text.Spanned; import android.text.TextUtils; import android.text.style.ImageSpan; import android.text.style.QuoteSpan; +import android.util.Base64; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.Menu; @@ -100,15 +101,22 @@ import com.google.android.material.bottomnavigation.LabelVisibilityMode; import com.google.android.material.snackbar.Snackbar; import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cms.CMSAlgorithm; +import org.bouncycastle.cms.CMSEnvelopedData; +import org.bouncycastle.cms.CMSEnvelopedDataGenerator; +import org.bouncycastle.cms.CMSProcessableByteArray; import org.bouncycastle.cms.CMSProcessableFile; import org.bouncycastle.cms.CMSSignedData; import org.bouncycastle.cms.CMSSignedDataGenerator; import org.bouncycastle.cms.CMSTypedData; +import org.bouncycastle.cms.RecipientInfoGenerator; import org.bouncycastle.cms.SignerInfoGenerator; import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; -import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.cms.jcajce.JceCMSContentEncryptorBuilder; +import org.bouncycastle.cms.jcajce.JceKeyTransRecipientInfoGenerator; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.DigestCalculatorProvider; +import org.bouncycastle.operator.OutputEncryptor; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; import org.bouncycastle.util.Store; @@ -121,6 +129,7 @@ import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpServiceConnection; import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; @@ -130,6 +139,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.UnknownHostException; import java.security.PrivateKey; +import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -1243,7 +1253,6 @@ public class FragmentCompose extends FragmentBase { } }, null, null, null, -1, null); - } else { if (pgpService.isBound()) try { @@ -1852,20 +1861,6 @@ public class FragmentCompose extends FragmentBase { int type = args.getInt("type"); String alias = args.getString("alias"); - if (!EntityMessage.SMIME_SIGNONLY.equals(type)) - throw new UnsupportedOperationException("Not yet supported"); - - if (alias == null) - throw new IllegalArgumentException("Key alias missing"); - - // Get key - PrivateKey privkey = KeyChain.getPrivateKey(context, alias); - if (privkey == null) - throw new IllegalArgumentException("Private key missing"); - X509Certificate[] chain = KeyChain.getCertificateChain(context, alias); - if (chain == null || chain.length == 0) - throw new IllegalArgumentException("Certificate missing"); - DB db = DB.getInstance(context); // Get data @@ -1909,58 +1904,118 @@ public class FragmentCompose extends FragmentBase { }; bpContent.setContent(imessage.getContent(), imessage.getContentType()); - // Build content - EntityAttachment cattachment = new EntityAttachment(); - cattachment.message = draft.id; - cattachment.sequence = db.attachment().getAttachmentSequence(draft.id) + 1; - cattachment.name = "content.asc"; - cattachment.type = "text/plain"; - cattachment.disposition = Part.INLINE; - cattachment.encryption = EntityAttachment.SMIME_CONTENT; - cattachment.id = db.attachment().insertAttachment(cattachment); + if (EntityMessage.SMIME_SIGNONLY.equals(type)) { + if (alias == null) + throw new IllegalArgumentException("Key alias missing"); - File content = cattachment.getFile(context); - try (OutputStream os = new FileOutputStream(content)) { - bpContent.writeTo(os); + // Get key + PrivateKey privkey = KeyChain.getPrivateKey(context, alias); + if (privkey == null) + throw new IllegalArgumentException("Private key missing"); + X509Certificate[] chain = KeyChain.getCertificateChain(context, alias); + if (chain == null || chain.length == 0) + throw new IllegalArgumentException("Certificate missing"); + + // Build content + EntityAttachment cattachment = new EntityAttachment(); + cattachment.message = draft.id; + cattachment.sequence = db.attachment().getAttachmentSequence(draft.id) + 1; + cattachment.name = "content.asc"; + cattachment.type = "text/plain"; + cattachment.disposition = Part.INLINE; + cattachment.encryption = EntityAttachment.SMIME_CONTENT; + cattachment.id = db.attachment().insertAttachment(cattachment); + + File content = cattachment.getFile(context); + try (OutputStream os = new FileOutputStream(content)) { + bpContent.writeTo(os); + } + + db.attachment().setDownloaded(cattachment.id, content.length()); + + // Build signature + Store store = new JcaCertStore(Arrays.asList(chain[0])); + CMSSignedDataGenerator cmsGenerator = new CMSSignedDataGenerator(); + cmsGenerator.addCertificates(store); + + ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withRSA") + .build(privkey); + DigestCalculatorProvider digestCalculator = new JcaDigestCalculatorProviderBuilder() + .build(); + SignerInfoGenerator signerInfoGenerator = new JcaSignerInfoGeneratorBuilder(digestCalculator) + .build(contentSigner, chain[0]); + cmsGenerator.addSignerInfoGenerator(signerInfoGenerator); + + CMSTypedData cmsData = new CMSProcessableFile(content); + CMSSignedData cmsSignedData = cmsGenerator.generate(cmsData, true); + byte[] signedMessage = cmsSignedData.getEncoded(); + + ContentType ct = new ContentType("application/pkcs7-signature"); + ct.setParameter("micalg", "sha-256"); + + EntityAttachment sattachment = new EntityAttachment(); + sattachment.message = draft.id; + sattachment.sequence = db.attachment().getAttachmentSequence(draft.id) + 1; + sattachment.name = "smime.p7s"; + sattachment.type = ct.toString(); + sattachment.disposition = Part.INLINE; + sattachment.encryption = EntityAttachment.SMIME_SIGNATURE; + sattachment.id = db.attachment().insertAttachment(sattachment); + + File file = sattachment.getFile(context); + try (OutputStream os = new FileOutputStream(file)) { + os.write(signedMessage); + } + + db.attachment().setDownloaded(sattachment.id, file.length()); + } else if (EntityMessage.SMIME_SIGNENCRYPT.equals(draft.encrypt)) { + if (true) + throw new UnsupportedOperationException("Not implemented yet"); + // TODO: sign + if (draft.to == null || draft.to.length != 1) + throw new IllegalArgumentException(getString(R.string.title_to_missing)); + + String to = ((InternetAddress) draft.to[0]).getAddress(); + List c = db.certificate().getCertificateByEmail(to); + if (c == null || c.size() == 0) + throw new IllegalArgumentException("Certificate not found"); + + byte[] encoded = Base64.decode(c.get(0).data, Base64.NO_WRAP); + X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509") + .generateCertificate(new ByteArrayInputStream(encoded)); + + CMSEnvelopedDataGenerator cmsEnvelopedDataGenerator = new CMSEnvelopedDataGenerator(); + RecipientInfoGenerator gen = new JceKeyTransRecipientInfoGenerator(cert); + cmsEnvelopedDataGenerator.addRecipientInfoGenerator(gen); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bpContent.writeTo(bos); + CMSTypedData msg = new CMSProcessableByteArray(bos.toByteArray()); + + OutputEncryptor encryptor = new JceCMSContentEncryptorBuilder(CMSAlgorithm.AES256_CBC) + .build(); + CMSEnvelopedData cmsEnvelopedData = cmsEnvelopedDataGenerator + .generate(msg, encryptor); + + byte[] encryptedData = cmsEnvelopedData.toASN1Structure().getEncoded(); + + EntityAttachment attachment = new EntityAttachment(); + attachment.message = draft.id; + attachment.sequence = db.attachment().getAttachmentSequence(draft.id) + 1; + attachment.name = "smime.p7m"; + attachment.type = "application/pkcs7-mime"; + attachment.disposition = Part.INLINE; + attachment.encryption = EntityAttachment.SMIME_MESSAGE; + attachment.id = db.attachment().insertAttachment(attachment); + + File file = attachment.getFile(context); + try (OutputStream os = new FileOutputStream(file)) { + os.write(encryptedData); + } + + db.attachment().setDownloaded(attachment.id, file.length()); } - db.attachment().setDownloaded(cattachment.id, content.length()); - - // Build signature - Store store = new JcaCertStore(Arrays.asList(chain[0])); - CMSSignedDataGenerator cmsGenerator = new CMSSignedDataGenerator(); - ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withRSA") - .build(privkey); - DigestCalculatorProvider digestCalculator = new JcaDigestCalculatorProviderBuilder() - .setProvider(new BouncyCastleProvider()).build(); - SignerInfoGenerator signerInfoGenerator = new JcaSignerInfoGeneratorBuilder(digestCalculator) - .build(contentSigner, chain[0]); - cmsGenerator.addSignerInfoGenerator(signerInfoGenerator); - cmsGenerator.addCertificates(store); - - CMSTypedData cmsData = new CMSProcessableFile(content); - CMSSignedData cmsSignedData = cmsGenerator.generate(cmsData, true); - byte[] signedMessage = cmsSignedData.getEncoded(); - - ContentType ct = new ContentType("application/pkcs7-signature"); - ct.setParameter("micalg", "sha-256"); - - EntityAttachment sattachment = new EntityAttachment(); - sattachment.message = draft.id; - sattachment.sequence = db.attachment().getAttachmentSequence(draft.id) + 1; - sattachment.name = "smime.p7s"; - sattachment.type = ct.toString(); - sattachment.disposition = Part.INLINE; - sattachment.encryption = EntityAttachment.SMIME_SIGNATURE; - sattachment.id = db.attachment().insertAttachment(sattachment); - - File file = sattachment.getFile(context); - try (OutputStream os = new FileOutputStream(file)) { - os.write(signedMessage); - } - - db.attachment().setDownloaded(sattachment.id, file.length()); - return null; } diff --git a/app/src/main/java/eu/faircode/email/FragmentMessages.java b/app/src/main/java/eu/faircode/email/FragmentMessages.java index 193d3978eb..5bb3a2c769 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessages.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java @@ -4386,7 +4386,6 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. for (Object match : store.getMatches(signer.getSID())) { X509CertificateHolder certHolder = (X509CertificateHolder) match; X509Certificate cert = new JcaX509CertificateConverter() - .setProvider(new BouncyCastleProvider()) .getCertificate(certHolder); try { if (signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(cert))) { diff --git a/app/src/main/java/eu/faircode/email/MessageHelper.java b/app/src/main/java/eu/faircode/email/MessageHelper.java index a1442869c2..1db6e57124 100644 --- a/app/src/main/java/eu/faircode/email/MessageHelper.java +++ b/app/src/main/java/eu/faircode/email/MessageHelper.java @@ -23,6 +23,7 @@ import android.content.Context; import android.net.MailTo; import android.net.Uri; import android.text.TextUtils; +import android.util.Base64; import com.sun.mail.util.FolderClosedIOException; import com.sun.mail.util.MessageRemovedIOException; @@ -246,7 +247,8 @@ public class MessageHelper { " keydata=" + sb.toString()); } - // https://tools.ietf.org/html/rfc3156 + // PGP: https://tools.ietf.org/html/rfc3156 + // S/MIME: https://tools.ietf.org/html/rfc8551 for (final EntityAttachment attachment : attachments) if (EntityAttachment.PGP_SIGNATURE.equals(attachment.encryption)) { Log.i("Sending PGP signed message"); @@ -375,8 +377,25 @@ public class MessageHelper { return imessage; } throw new IllegalStateException("S/MIME content not found"); - } else if (EntityAttachment.SMIME_MESSAGE.equals(attachment.encryption)) - throw new UnsupportedOperationException(); + } else if (EntityAttachment.SMIME_MESSAGE.equals(attachment.encryption)) { + Log.i("Sending S/MIME encrypted message"); + + File file = attachment.getFile(context); + byte[] encryptedData = new byte[(int) file.length()]; + try (InputStream is = new FileInputStream(file)) { + is.read(encryptedData); + } + + // Build message + ContentType ct = new ContentType("application/pkcs7-mime"); + ct.setParameter("name", attachment.name); + ct.setParameter("smime-type", "enveloped-data"); + imessage.setDisposition(Part.ATTACHMENT); + imessage.setFileName(attachment.name); + imessage.setContent(Base64.encodeToString(encryptedData, Base64.DEFAULT), ct.toString()); + + return imessage; + } build(context, message, attachments, identity, imessage); @@ -1356,7 +1375,8 @@ public class MessageHelper { ContentType ct = new ContentType(imessage.getContentType()); String protocol = ct.getParameter("protocol"); if ("application/pgp-signature".equals(protocol) || - "application/pkcs7-signature".equals(protocol)) { + "application/pkcs7-signature".equals(protocol) || + "application/x-pkcs7-signature".equals(protocol)) { Multipart multipart = (Multipart) imessage.getContent(); if (multipart.getCount() == 2) { getMessageParts(multipart.getBodyPart(0), parts, null);