diff --git a/pom.xml b/pom.xml index a065631..d148b48 100644 --- a/pom.xml +++ b/pom.xml @@ -3,28 +3,33 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 - - com.github.bbottema - standard-project-parent - 1.0.24 - + + + + + org.simplejavamail utils-mail-smime jar utils-mail-smime - 2.0.1 - A S/MIME library for JavaMail + 2.1.0-SNAPSHOT + An S/MIME library for JavaMail http:///github.com/bbottema/java-utils-mail-smime 2021 + UTF-8 + UTF-8 com.github.bbottema.java-utils-mail-smime com/mycila/maven/plugin/license/templates/APACHE-2.txt Benny Bottema benny@bennybottema.com + 5.8.2 + 17 + 17 @@ -72,6 +77,18 @@ https://github.com/bbottema/java-utils-mail-smime/issues + + + + org.junit + junit-bom + ${junit.version} + pom + import + + + + org.jetbrains @@ -85,10 +102,37 @@ bcjmail-jdk15to18 1.70 + + jakarta.activation + jakarta.activation-api + 2.0.1 + + + com.sun.activation + jakarta.activation + 2.0.1 + + + jakarta.mail + jakarta.mail-api + 2.0.1 + com.sun.mail jakarta.mail 2.0.1 + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + \ No newline at end of file diff --git a/src/main/java/org/simplejavamail/utils/mail/smime/KeyEncapsulationAlgorithm.java b/src/main/java/org/simplejavamail/utils/mail/smime/KeyEncapsulationAlgorithm.java new file mode 100644 index 0000000..fe93fc0 --- /dev/null +++ b/src/main/java/org/simplejavamail/utils/mail/smime/KeyEncapsulationAlgorithm.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2022. Ulrich Schuster, 10827 Berlin, Germany + */ + +package org.simplejavamail.utils.mail.smime; + +public enum KeyEncapsulationAlgorithm { + RSA, + RSA_OAEP_SHA224, + RSA_OAEP_SHA256, + RSA_OAEP_SHA384, + RSA_OAEP_SHA512, +} \ No newline at end of file diff --git a/src/main/java/org/simplejavamail/utils/mail/smime/SmimeKey.java b/src/main/java/org/simplejavamail/utils/mail/smime/SmimeKey.java index 6aeb848..81f5a7c 100644 --- a/src/main/java/org/simplejavamail/utils/mail/smime/SmimeKey.java +++ b/src/main/java/org/simplejavamail/utils/mail/smime/SmimeKey.java @@ -89,7 +89,7 @@ private void extractAssociatedAddresses() { try { X509Certificate certificate = getCertificate(); if (null != certificate) { - Principal principal = certificate.getSubjectDN(); + Principal principal = certificate.getSubjectX500Principal(); if (null != principal) { String name = principal.getName(); StringTokenizer tokenizer = new StringTokenizer(name, ","); diff --git a/src/main/java/org/simplejavamail/utils/mail/smime/SmimeKeyStore.java b/src/main/java/org/simplejavamail/utils/mail/smime/SmimeKeyStore.java index 8189cad..ba0f689 100644 --- a/src/main/java/org/simplejavamail/utils/mail/smime/SmimeKeyStore.java +++ b/src/main/java/org/simplejavamail/utils/mail/smime/SmimeKeyStore.java @@ -14,7 +14,7 @@ /** * A wrapper around a {@link KeyStore} that can be initialized with a PKCS12 * keystore and is used to obtain {@link SmimeKey SmimeKeys}. - * + * * @author Allen Petersen (akp at sourceforge dot net) * @author Torsten Krause (tk at markenwerk dot net) * @since 1.0.0 @@ -26,11 +26,11 @@ public class SmimeKeyStore { /** * Creates a new {@code SmimeKeyStore} by loading a PKCS12 keystore from * the given input stream. - * + * *

* The character array holding the password is overwritten with {@code 0s} * after it has been used. - * + * * @param stream * The {@link InputStream} to read the PKCS12 keystore from. * @param password @@ -43,12 +43,12 @@ public SmimeKeyStore(InputStream stream, char[] password) { /** * Creates a new {@code SmimeKeyStore} by loading a PKCS12 keystore from * the given input stream. - * + * *

* If {@code discardPassword} is set to {@code true}, the character array * holding the password is overwritten with {@code 0s} after it has been * used. - * + * * @param stream * The {@link InputStream} to read the PKCS12 keystore from. * @param password @@ -95,11 +95,11 @@ public int size() { /** * Returns the S/MIME key associated with the given alias, using the given * password to recover it. - * + * *

* The character array holding the password is overwritten with {@code 0s} * after it has been used. - * + * * @param alias * The alias. * @param password @@ -115,12 +115,12 @@ public SmimeKey getPrivateKey(String alias, char[] password) { /** * Returns the S/MIME key associated with the given alias, using the given * password to recover it. - * + * *

* If {@code discardPassword} is set to {@code true}, the character array * holding the password is overwritten with {@code 0s} after it has been * used. - * + * * @param alias * The alias. * @param password diff --git a/src/main/java/org/simplejavamail/utils/mail/smime/SmimeUtil.java b/src/main/java/org/simplejavamail/utils/mail/smime/SmimeUtil.java index 7340ae4..03ba41d 100644 --- a/src/main/java/org/simplejavamail/utils/mail/smime/SmimeUtil.java +++ b/src/main/java/org/simplejavamail/utils/mail/smime/SmimeUtil.java @@ -1,9 +1,18 @@ package org.simplejavamail.utils.mail.smime; import com.sun.mail.smtp.SMTPMessage; +import jakarta.activation.CommandMap; +import jakarta.activation.MailcapCommandMap; +import jakarta.mail.Header; +import jakarta.mail.MessagingException; +import jakarta.mail.Multipart; +import jakarta.mail.Session; +import jakarta.mail.internet.*; import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.cms.AttributeTable; import org.bouncycastle.asn1.cms.IssuerAndSerialNumber; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.bouncycastle.asn1.smime.SMIMECapabilitiesAttribute; import org.bouncycastle.asn1.smime.SMIMECapability; import org.bouncycastle.asn1.smime.SMIMECapabilityVector; @@ -12,47 +21,24 @@ import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.style.BCStyle; import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaCertStore; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; -import org.bouncycastle.cms.CMSAlgorithm; -import org.bouncycastle.cms.CMSException; -import org.bouncycastle.cms.RecipientInformation; -import org.bouncycastle.cms.RecipientInformationStore; -import org.bouncycastle.cms.SignerId; -import org.bouncycastle.cms.SignerInfoGenerator; -import org.bouncycastle.cms.SignerInformation; -import org.bouncycastle.cms.SignerInformationVerifier; -import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoGeneratorBuilder; -import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; -import org.bouncycastle.cms.jcajce.JceCMSContentEncryptorBuilder; -import org.bouncycastle.cms.jcajce.JceKeyTransEnvelopedRecipient; -import org.bouncycastle.cms.jcajce.JceKeyTransRecipient; -import org.bouncycastle.cms.jcajce.JceKeyTransRecipientId; -import org.bouncycastle.cms.jcajce.JceKeyTransRecipientInfoGenerator; +import org.bouncycastle.cms.*; +import org.bouncycastle.cms.jcajce.*; import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.mail.smime.SMIMEEnveloped; -import org.bouncycastle.mail.smime.SMIMEEnvelopedGenerator; -import org.bouncycastle.mail.smime.SMIMESigned; -import org.bouncycastle.mail.smime.SMIMESignedGenerator; -import org.bouncycastle.mail.smime.SMIMEUtil; +import org.bouncycastle.mail.smime.*; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.OutputEncryptor; +import org.bouncycastle.operator.jcajce.JcaAlgorithmParametersConverter; import org.bouncycastle.util.Store; -import jakarta.activation.CommandMap; -import jakarta.activation.MailcapCommandMap; -import jakarta.mail.Header; -import jakarta.mail.MessagingException; -import jakarta.mail.Multipart; -import jakarta.mail.Session; -import jakarta.mail.internet.ContentType; -import jakarta.mail.internet.MimeBodyPart; -import jakarta.mail.internet.MimeMessage; -import jakarta.mail.internet.MimeMultipart; -import jakarta.mail.internet.MimePart; +import javax.crypto.spec.OAEPParameterSpec; +import javax.crypto.spec.PSource; import java.io.IOException; import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Security; @@ -60,596 +46,656 @@ import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Enumeration; -import java.util.Iterator; -import java.util.List; +import java.security.spec.MGF1ParameterSpec; +import java.util.*; /** * Utilities for handling S/MIME specific operations on MIME messages from * JavaMail. - * + * * @author Allen Petersen (akp at sourceforge dot net) * @author Torsten Krause (tk at markenwerk dot net) * @since 1.0.0 */ public final class SmimeUtil { - static { - if (null == Security.getProvider(BouncyCastleProvider.PROVIDER_NAME)) { - Security.addProvider(new BouncyCastleProvider()); - updateMailcapCommandMap(); - } - } - - @SuppressWarnings("unused") - private SmimeUtil() { - } - - private static void updateMailcapCommandMap() { - MailcapCommandMap map = (MailcapCommandMap) CommandMap.getDefaultCommandMap(); - map.addMailcap("application/pkcs7-signature;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_signature"); - map.addMailcap("application/pkcs7-mime;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_mime"); - map.addMailcap("application/x-pkcs7-signature;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_signature"); - map.addMailcap("application/x-pkcs7-mime;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_mime"); - map.addMailcap("multipart/signed;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.multipart_signed"); - CommandMap.setDefaultCommandMap(map); - } - - /** - * Encrypts a MIME message and yields a new S/MIME encrypted MIME message. - * - * @param session - * The {@link Session} that is used in conjunction with the - * original {@link MimeMessage}. - * @param mimeMessage - * The original {@link MimeMessage} to be encrypted. - * @param certificate - * The {@link X509Certificate} used to obtain the - * {@link PublicKey} to encrypt the original message with. - * @return The new S/MIME encrypted {@link MimeMessage}. - */ - public static MimeMessage encrypt(Session session, MimeMessage mimeMessage, X509Certificate certificate) { - try { - MimeMessage encryptedMimeMessage = new MimeMessage(session); - copyHeaders(mimeMessage, encryptedMimeMessage); - - SMIMEEnvelopedGenerator generator = prepareGenerator(certificate); - OutputEncryptor encryptor = prepareEncryptor(); - - MimeBodyPart encryptedMimeBodyPart = generator.generate(mimeMessage, encryptor); - copyContent(encryptedMimeBodyPart, encryptedMimeMessage); - copyHeaders(encryptedMimeBodyPart, encryptedMimeMessage); - encryptedMimeMessage.saveChanges(); - return encryptedMimeMessage; - } catch (Exception e) { - throw handledException(e); - } - } - - /** - * Encrypts a MIME body part and yields a new S/MIME encrypted MIME body - * part. - * - * @param mimeBodyPart - * The original {@link MimeBodyPart} to be encrypted. - * @param certificate - * The {@link X509Certificate} used to obtain the - * {@link PublicKey} to encrypt the original body part with. - * @return The new S/MIME encrypted {@link MimeBodyPart}. - */ - public static MimeBodyPart encrypt(MimeBodyPart mimeBodyPart, X509Certificate certificate) { - try { - SMIMEEnvelopedGenerator generator = prepareGenerator(certificate); - OutputEncryptor encryptor = prepareEncryptor(); - - return generator.generate(mimeBodyPart, encryptor); - - } catch (Exception e) { - throw handledException(e); - } - } - - private static void copyHeaders(MimeBodyPart fromBodyPart, MimeMessage toMessage) throws MessagingException { - Enumeration

headers = fromBodyPart.getAllHeaders(); - copyHeaders(headers, toMessage); - } - - private static void copyHeaders(MimeMessage fromMessage, MimeMessage toMessage) throws MessagingException { - Enumeration
headers = fromMessage.getAllHeaders(); - copyHeaders(headers, toMessage); - } - - private static void copyHeaders(Enumeration
headers, MimeMessage toMessage) throws MessagingException { - while (headers.hasMoreElements()) { - Header header = headers.nextElement(); - toMessage.setHeader(header.getName(), header.getValue()); - } - } - - private static SMIMEEnvelopedGenerator prepareGenerator(X509Certificate certificate) - throws CertificateEncodingException { - JceKeyTransRecipientInfoGenerator infoGenerator = new JceKeyTransRecipientInfoGenerator(certificate); - infoGenerator.setProvider(BouncyCastleProvider.PROVIDER_NAME); - SMIMEEnvelopedGenerator generator = new SMIMEEnvelopedGenerator(); - generator.addRecipientInfoGenerator(infoGenerator); - return generator; - } - - private static OutputEncryptor prepareEncryptor() throws CMSException { - return new JceCMSContentEncryptorBuilder(CMSAlgorithm.DES_EDE3_CBC).setProvider( - BouncyCastleProvider.PROVIDER_NAME).build(); - } - - /** - * Decrypts an S/MIME encrypted MIME message and yields a new MIME message. - * - * @param session - * The {@link Session} that is used in conjunction with the - * encrypted {@link MimeMessage}. - * @param mimeMessage - * The encrypted {@link MimeMessage} to be decrypted. - * @param smimeKey - * The {@link SmimeKey} used to obtain the {@link PrivateKey} to - * decrypt the encrypted message with. - * @return The new S/MIME decrypted {@link MimeMessage}. - */ - public static MimeMessage decrypt(Session session, MimeMessage mimeMessage, SmimeKey smimeKey) { - try { - byte[] content = decryptContent(new SMIMEEnveloped(mimeMessage), smimeKey); - MimeBodyPart mimeBodyPart = SMIMEUtil.toMimeBodyPart(content); - - MimeMessage decryptedMessage = new MimeMessage(session); - copyHeaderLines(mimeMessage, decryptedMessage); - copyContent(mimeBodyPart, decryptedMessage); - decryptedMessage.setHeader("Content-Type", mimeBodyPart.getContentType()); - return decryptedMessage; - - } catch (Exception e) { - throw handledException(e); - } - } - - /** - * Decrypts an S/MIME encrypted MIME body part and yields a new MIME body - * part. - * - * @param mimeBodyPart - * The encrypted {@link MimeBodyPart} to be decrypted. - * @param smimeKey - * The {@link SmimeKey} used to obtain the {@link PrivateKey} to - * decrypt the encrypted body part with. - * @return The new S/MIME decrypted {@link MimeBodyPart}. - */ - public static MimeBodyPart decrypt(MimeBodyPart mimeBodyPart, SmimeKey smimeKey) { - try { - return SMIMEUtil.toMimeBodyPart(decryptContent(new SMIMEEnveloped(mimeBodyPart), smimeKey)); - } catch (Exception e) { - throw handledException(e); - } - - } - - /** - * Decrypts an S/MIME encrypted MIME multipart and yields a new MIME body - * part. - * - * @param mimeMultipart - * The encrypted {@link MimeMultipart} to be decrypted. - * @param smimeKey - * The {@link SmimeKey} used to obtain the {@link PrivateKey} to - * decrypt the encrypted multipart with. - * @return The new S/MIME decrypted {@link MimeBodyPart}. - */ - public static MimeBodyPart decrypt(MimeMultipart mimeMultipart, SmimeKey smimeKey) { - try { - MimeBodyPart mimeBodyPart = new MimeBodyPart(); - mimeBodyPart.setContent(mimeMultipart); - mimeBodyPart.setHeader("Content-Type", mimeMultipart.getContentType()); - return decrypt(mimeBodyPart, smimeKey); - } catch (Exception e) { - throw handledException(e); - } - } - - private static byte[] decryptContent(SMIMEEnveloped smimeEnveloped, SmimeKey smimeKey) throws MessagingException, - CMSException { - X509Certificate certificate = smimeKey.getCertificate(); - PrivateKey privateKey = smimeKey.getPrivateKey(); - - RecipientInformationStore recipients = smimeEnveloped.getRecipientInfos(); - RecipientInformation recipient = recipients.get(new JceKeyTransRecipientId(certificate)); - - if (null == recipient) { - throw new MessagingException("no recipient"); - } - - JceKeyTransRecipient transportRecipient = new JceKeyTransEnvelopedRecipient(privateKey); - transportRecipient.setProvider(BouncyCastleProvider.PROVIDER_NAME); - return recipient.getContent(transportRecipient); - } - - private static void copyHeaderLines(MimeMessage fromMessage, MimeMessage toMessage) throws MessagingException { - Enumeration headerLines = fromMessage.getAllHeaderLines(); - while (headerLines.hasMoreElements()) { - String nextElement = headerLines.nextElement(); - toMessage.addHeaderLine(nextElement); - } - } - - private static void copyContent(MimeBodyPart fromBodyPart, MimeMessage toMessage) throws MessagingException, - IOException { - toMessage.setContent(fromBodyPart.getContent(), fromBodyPart.getContentType()); - } - - /** - * Signs a MIME body part and yields a new S/MIME signed MIME body part. - * - * @param mimeBodyPart - * The original {@link MimeBodyPart} to be signed. - * @param smimeKey - * The {@link SmimeKey} used to obtain the {@link PrivateKey} to - * sign the original body part with. - * @return The new S/MIME signed {@link MimeBodyPart}. - */ - public static MimeBodyPart sign(MimeBodyPart mimeBodyPart, SmimeKey smimeKey) { - try { - SMIMESignedGenerator generator = getGenerator(smimeKey); - MimeMultipart signedMimeMultipart = generator.generate(MimeUtil.canonicalize(mimeBodyPart)); - MimeBodyPart signedMimeBodyPart = new MimeBodyPart(); - signedMimeBodyPart.setContent(signedMimeMultipart); - return signedMimeBodyPart; - - } catch (Exception e) { - throw handledException(e); - } - - } - - private static SMIMESignedGenerator getGenerator(SmimeKey smimeKey) throws CertificateEncodingException, - OperatorCreationException { - SMIMESignedGenerator generator = new SMIMESignedGenerator(); - generator.addCertificates(getCertificateStore(smimeKey)); - generator.addSignerInfoGenerator(getInfoGenerator(smimeKey)); - return generator; - } - - private static SignerInfoGenerator getInfoGenerator(SmimeKey smimeKey) throws OperatorCreationException, - CertificateEncodingException { - JcaSimpleSignerInfoGeneratorBuilder builder = new JcaSimpleSignerInfoGeneratorBuilder(); - builder.setSignedAttributeGenerator(new AttributeTable(getSignedAttributes(smimeKey))); - builder.setProvider(BouncyCastleProvider.PROVIDER_NAME); - - PrivateKey privateKey = smimeKey.getPrivateKey(); - X509Certificate certificate = smimeKey.getCertificate(); - return builder.build("SHA256withRSA", privateKey, certificate); - } - - private static ASN1EncodableVector getSignedAttributes(SmimeKey smimeKey) { - ASN1EncodableVector signedAttributes = new ASN1EncodableVector(); - IssuerAndSerialNumber issuerAndSerialNumber = getIssuerAndSerialNumber(smimeKey); - signedAttributes.add(new SMIMEEncryptionKeyPreferenceAttribute(issuerAndSerialNumber)); - signedAttributes.add(new SMIMECapabilitiesAttribute(getCapabilityVector())); - return signedAttributes; - } - - private static SMIMECapabilityVector getCapabilityVector() { - SMIMECapabilityVector capabilityVector = new SMIMECapabilityVector(); - capabilityVector.addCapability(SMIMECapability.dES_EDE3_CBC); - capabilityVector.addCapability(SMIMECapability.rC2_CBC, 128); - capabilityVector.addCapability(SMIMECapability.dES_CBC); - return capabilityVector; - } - - private static IssuerAndSerialNumber getIssuerAndSerialNumber(SmimeKey smimeKey) { - X509Certificate certificate = smimeKey.getCertificate(); - BigInteger serialNumber = certificate.getSerialNumber(); - X500Name issuerName = new X500Name(certificate.getIssuerDN().getName()); - return new IssuerAndSerialNumber(issuerName, serialNumber); - } - - private static JcaCertStore getCertificateStore(SmimeKey smimeKey) throws CertificateEncodingException { - Certificate[] certificateChain = smimeKey.getCertificateChain(); - X509Certificate certificate = smimeKey.getCertificate(); - - final List certificateList; - if (certificateChain != null && certificateChain.length > 0) { - certificateList = Arrays.asList(certificateChain); - } else { - certificateList = new ArrayList<>(); - certificateList.add(certificate); - } - return new JcaCertStore(certificateList); - } - - /** - * Signs a MIME message and yields a new S/MIME signed MIME message. - * - * @param session - * The {@link Session} that is used in conjunction with the - * original {@link MimeMessage}. - * @param mimeMessage - * The original {@link MimeMessage} or {@link SMTPMessage} to be signed. - * @param smimeKey - * The {@link SmimeKey} used to obtain the {@link PrivateKey} to - * sign the original message with. - * @return The new S/MIME signed {@link MimeMessage} or {@link SMTPMessage}. - */ - public static T sign(Session session, T mimeMessage, SmimeKey smimeKey) { - //noinspection unchecked - return (mimeMessage instanceof SMTPMessage) - ? sign(mimeMessage, (T) new SMTPMessage(session), smimeKey) - : sign(mimeMessage, (T) new MimeMessage(session), smimeKey); - } - - private static T sign(T mimeMessage, T signedMessage, SmimeKey smimeKey) { - try { - copyHeaderLines(mimeMessage, signedMessage); - copyContent(sign(extractMimeBodyPart(mimeMessage), smimeKey), signedMessage); - return signedMessage; - } catch (Exception e) { - throw handledException(e); - } - } - - private static MimeBodyPart extractMimeBodyPart(MimeMessage mimeMessage) throws IOException, MessagingException { - Object content = mimeMessage.getContent(); - UpdatableMimeBodyPart updateableMimeBodyPart = new UpdatableMimeBodyPart(); - if (content instanceof Multipart) { - updateableMimeBodyPart.setContent((Multipart) content); - } else { - updateableMimeBodyPart.setContent(content, mimeMessage.getDataHandler().getContentType()); - } - updateableMimeBodyPart.updateHeaders(); - return updateableMimeBodyPart; - } - - /** - * Checks the signature on an S/MIME signed MIME multipart. - * - * @param mimeMultipart - * The {@link MimeMultipart} to be checked. - * @return {@code true} if the multipart is correctly signed, {@code false} - * otherwise. - */ - public static boolean checkSignature(MimeMultipart mimeMultipart) { - try { - return checkSignature(new SMIMESigned(mimeMultipart)); - } catch (Exception e) { - throw handledException(e); - } - } - - /** - * Checks the signature on an S/MIME signed MIME part (i.e. MIME message). - * - * @param mimePart - * The {@link MimePart} to be checked. - * @return {@code true} if the part is correctly signed, {@code false} - * otherwise. - */ - public static boolean checkSignature(MimePart mimePart) { - try { - if (mimePart.isMimeType("multipart/signed")) { - return checkSignature(new SMIMESigned((MimeMultipart) mimePart.getContent())); - } else if (mimePart.isMimeType("application/pkcs7-mime") || mimePart.isMimeType("application/x-pkcs7-mime")) { - return checkSignature(new SMIMESigned(mimePart)); - } else { - throw new SmimeException("Message not signed"); - } - } catch (Exception e) { - throw handledException(e); - } - } - - /** - * Checks a SMIMESigned to make sure that the signature matches. - */ - private static boolean checkSignature(SMIMESigned smimeSigned) { - try { - boolean returnValue = true; - - @SuppressWarnings("rawtypes") - Store certificates = smimeSigned.getCertificates(); - Iterator signerInformations = smimeSigned.getSignerInfos().getSigners().iterator(); - - while (returnValue && signerInformations.hasNext()) { - SignerInformation signerInformation = signerInformations.next(); - X509Certificate certificate = getCertificate(certificates, signerInformation.getSID()); - SignerInformationVerifier verifier = getVerifier(certificate); - if (!signerInformation.verify(verifier)) { - returnValue = false; - } - } - return returnValue; - - } catch (Exception e) { - throw handledException(e); - } - } - - /** - * @param mimeMultipart - * The {@link MimeMultipart} to be checked. - * @return The subject / address to which the certificate was issued to. Email clients may use this to show - * {@code "Signed by: "} - */ - public static String getSignedByAddress(MimeMultipart mimeMultipart) { - try { - return getSignedByAddress(new SMIMESigned(mimeMultipart)); - } catch (Exception e) { - throw handledException(e); - } - } - - /** - * @param mimePart - * The {@link MimePart} to be checked. - * @return The subject / address to which the certificate was issued to. Email clients may use this to show - * {@code "Signed by: "} - */ - public static String getSignedByAddress(MimePart mimePart) { - try { - if (mimePart.isMimeType("multipart/signed")) { - return getSignedByAddress(new SMIMESigned((MimeMultipart) mimePart.getContent())); - } else if (mimePart.isMimeType("application/pkcs7-mime") || mimePart.isMimeType("application/x-pkcs7-mime")) { - return getSignedByAddress(new SMIMESigned(mimePart)); - } else { - throw new SmimeException("Message not signed"); - } - } catch (Exception e) { - throw handledException(e); - } - } - - /** - * Returns the subject / address to which the certificate was issued to. Email clients may use this to show - * {@code "Signed by: "} - */ - private static String getSignedByAddress(SMIMESigned smimeSigned) { - try { - @SuppressWarnings("rawtypes") - Store certificates = smimeSigned.getCertificates(); - - SignerInformation signerInformation = smimeSigned.getSignerInfos().getSigners().iterator().next(); - X509Certificate certificate = getCertificate(certificates, signerInformation.getSID()); - SignerInformationVerifier verifier = getVerifier(certificate); - X500Name x500name = verifier.getAssociatedCertificate().getSubject(); - RDN cn = x500name.getRDNs(BCStyle.CN)[0]; - return IETFUtils.valueToString(cn.getFirst().getValue()); - - } catch (Exception e) { - throw handledException(e); - } - } - - private static X509Certificate getCertificate(@SuppressWarnings("rawtypes") Store certificates, SignerId signerId) - throws CertificateException { - @SuppressWarnings({ "unchecked" }) - X509CertificateHolder certificateHolder = (X509CertificateHolder) certificates.getMatches(signerId).iterator() - .next(); - JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter(); - certificateConverter.setProvider(BouncyCastleProvider.PROVIDER_NAME); - return certificateConverter.getCertificate(certificateHolder); - } - - private static SignerInformationVerifier getVerifier(X509Certificate certificate) throws OperatorCreationException { - JcaSimpleSignerInfoVerifierBuilder builder = new JcaSimpleSignerInfoVerifierBuilder(); - builder.setProvider(BouncyCastleProvider.PROVIDER_NAME); - return builder.build(certificate); - } - - /** - * Returns the signed MIME body part of an S/MIME signed MIME multipart. - * - * @param mimeMultipart - * The {@link MimeMultipart} to be stripped off. - * @return The signed {@link MimeBodyPart} contained in the - * {@link MimeMultipart}. - */ - public static MimeBodyPart getSignedContent(MimeMultipart mimeMultipart) { - try { - return new SMIMESigned(mimeMultipart).getContent(); - } catch (Exception e) { - throw handledException(e); - } - } - - /** - * Returns the signed MIME body part of an S/MIME signed MIME part (i.e. MIME - * message). - * - * @param mimePart - * The {@link MimePart} to be stripped off. - * @return The signed {@link MimeBodyPart} contained in the {@link MimePart} - * . - */ - public static MimeBodyPart getSignedContent(MimePart mimePart) { - try { - if (mimePart.isMimeType("multipart/signed")) { - return new SMIMESigned((MimeMultipart) mimePart.getContent()).getContent(); - } else if (mimePart.isMimeType("application/pkcs7-mime") || mimePart.isMimeType("application/x-pkcs7-mime")) { - return new SMIMESigned(mimePart).getContent(); - } else { - throw new SmimeException("Message not signed"); - } - } catch (Exception e) { - throw handledException(e); - } - } - - /** - * Returns the S/MIME state of a MIME multipart. - * - * @param mimeMultipart - * The {@link MimeMultipart} to be checked. - * @return the {@link SmimeState} of the {@link MimeMultipart}. - */ - public static SmimeState getStatus(MimeMultipart mimeMultipart) { - try { - return getStatus(new ContentType(mimeMultipart.getContentType())); - } catch (Exception e) { - throw handledException(e); - } - } - - /** - * Returns the S/MIME state of a MIME part (i.e. MIME message). - * - * @param mimePart - * The {@link MimePart} to be checked. - * @return the {@link SmimeState} of the {@link MimePart}. - */ - public static SmimeState getStatus(MimePart mimePart) { - try { - return getStatus(new ContentType(mimePart.getContentType())); - } catch (Exception e) { - throw handledException(e); - } - } - - private static SmimeState getStatus(ContentType contentType) { - if (isSmimeSignatureContentType(contentType)) { - return SmimeState.SIGNED; - } else if (isSignatureSmimeType(contentType)) { - return SmimeState.SIGNED_ENVELOPED; - } else if (isSmimeEncryptionContenttype(contentType)) { - return SmimeState.ENCRYPTED; - } else { - return SmimeState.NEITHER; - } - } - - private static boolean isSmimeEncryptionContenttype(ContentType contentType) { - String baseContentType = contentType.getBaseType(); - return baseContentType.equalsIgnoreCase("application/pkcs7-mime") - || baseContentType.equalsIgnoreCase("application/x-pkcs7-mime"); - } - - private static boolean isSmimeSignatureContentType(ContentType contentType) { - String baseContentType = contentType.getBaseType(); - String protocol = contentType.getParameter("protocol"); - return baseContentType.equalsIgnoreCase("multipart/signed") - && protocol != null && isSmimeSignatureProtocoll(protocol); - } - - private static boolean isSignatureSmimeType(ContentType contentType) { - String baseContentType = contentType.getBaseType(); - return baseContentType.equalsIgnoreCase("application/x-pkcs7-mime") - && "signed-data".equals(contentType.getParameter("smime-type")); - } - - private static boolean isSmimeSignatureProtocoll(String protocol) { - return protocol.equalsIgnoreCase("application/pkcs7-signature") - || protocol.equalsIgnoreCase("application/x-pkcs7-signature"); - } - - private static SmimeException handledException(Exception e) { - if (e instanceof SmimeException) { - return (SmimeException) e; - } - return new SmimeException(e.getMessage(), e); - } + private static final String defaultSignatureAlgorithmName = "SHA256withRSA"; + private static final KeyEncapsulationAlgorithm defaultKeyEncapsulationAlgorithm = KeyEncapsulationAlgorithm.RSA; + private static final ASN1ObjectIdentifier defaultCipher = CMSAlgorithm.DES_EDE3_CBC; + + static { + if (null == Security.getProvider(BouncyCastleProvider.PROVIDER_NAME)) { + Security.addProvider(new BouncyCastleProvider()); + updateMailcapCommandMap(); + } + } + + @SuppressWarnings("unused") + private SmimeUtil() { + } + + private static void updateMailcapCommandMap() { + MailcapCommandMap map = (MailcapCommandMap) CommandMap.getDefaultCommandMap(); + map.addMailcap("application/pkcs7-signature;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_signature"); + map.addMailcap("application/pkcs7-mime;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_mime"); + map.addMailcap("application/x-pkcs7-signature;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_signature"); + map.addMailcap("application/x-pkcs7-mime;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_mime"); + map.addMailcap("multipart/signed;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.multipart_signed"); + CommandMap.setDefaultCommandMap(map); + } + + /** + * Encrypts a MIME message and yields a new S/MIME encrypted MIME message. + * + * @param session The {@link Session} that is used in conjunction with the + * original {@link MimeMessage}. + * @param mimeMessage The original {@link MimeMessage} to be encrypted. + * @param certificate The {@link X509Certificate} used to obtain the + * {@link PublicKey} to encrypt the original message with. + * @return The new S/MIME encrypted {@link MimeMessage}. + */ + public static MimeMessage encrypt(Session session, MimeMessage mimeMessage, X509Certificate certificate) { + return encrypt(session, mimeMessage, certificate, defaultKeyEncapsulationAlgorithm, defaultCipher); + } + + /** + * Encrypts a MIME message and yields a new S/MIME encrypted MIME message. + * + * @param session The {@link Session} that is used in conjunction with the + * original {@link MimeMessage}. + * @param mimeMessage The original {@link MimeMessage} to be encrypted. + * @param certificate The {@link X509Certificate} used to obtain the + * {@link PublicKey} to encrypt the original message with. + * @param keyEncapsulationAlgorithm Algorithm used to encapsulate the symmetric encryption key. + * Currently, RSA RSA-OAEP with various SHA digest lengths are supported. + * @param cmsAlgorithm Encryption algorithm for symmetric content encryption. + * @return The new S/MIME encrypted {@link MimeMessage}. + */ + public static MimeMessage encrypt(Session session, MimeMessage mimeMessage, X509Certificate certificate, KeyEncapsulationAlgorithm keyEncapsulationAlgorithm, ASN1ObjectIdentifier cmsAlgorithm) { + try { + MimeMessage encryptedMimeMessage = new MimeMessage(session); + copyHeaders(mimeMessage, encryptedMimeMessage); + + SMIMEEnvelopedGenerator generator = prepareGenerator(certificate, keyEncapsulationAlgorithm); + OutputEncryptor encryptor = prepareEncryptor(cmsAlgorithm); + + MimeBodyPart encryptedMimeBodyPart = generator.generate(mimeMessage, encryptor); + copyContent(encryptedMimeBodyPart, encryptedMimeMessage); + copyHeaders(encryptedMimeBodyPart, encryptedMimeMessage); + encryptedMimeMessage.saveChanges(); + return encryptedMimeMessage; + } catch (Exception e) { + throw handledException(e); + } + } + + /** + * Encrypts a MIME body part and yields a new S/MIME encrypted MIME body + * part. + * + * @param mimeBodyPart The original {@link MimeBodyPart} to be encrypted. + * @param certificate The {@link X509Certificate} used to obtain the + * {@link PublicKey} to encrypt the original body part with. + * @return The new S/MIME encrypted {@link MimeBodyPart}. + */ + public static MimeBodyPart encrypt(MimeBodyPart mimeBodyPart, X509Certificate certificate) { + return encrypt(mimeBodyPart, certificate, defaultKeyEncapsulationAlgorithm, defaultCipher); + } + + /** + * Encrypts a MIME body part and yields a new S/MIME encrypted MIME body + * part. + * + * @param mimeBodyPart The original {@link MimeBodyPart} to be encrypted. + * @param certificate The {@link X509Certificate} used to obtain the + * {@link PublicKey} to encrypt the original body part with. + * @param keyEncapsulationAlgorithm Algorithm used to encapsulate the symmetric encryption key. + * Currently, RSA RSA-OAEP with various SHA digest lengths are supported. + * @param cmsAlgorithm Encryption algorithm for symmetric content encryption. + * @return The new S/MIME encrypted {@link MimeBodyPart}. + */ + public static MimeBodyPart encrypt(MimeBodyPart mimeBodyPart, + X509Certificate certificate, + KeyEncapsulationAlgorithm keyEncapsulationAlgorithm, + ASN1ObjectIdentifier cmsAlgorithm) { + try { + SMIMEEnvelopedGenerator generator = prepareGenerator(certificate, keyEncapsulationAlgorithm); + OutputEncryptor encryptor = prepareEncryptor(cmsAlgorithm); + + return generator.generate(mimeBodyPart, encryptor); + + } catch (Exception e) { + throw handledException(e); + } + } + + private static void copyHeaders(MimeBodyPart fromBodyPart, MimeMessage toMessage) throws MessagingException { + Enumeration
headers = fromBodyPart.getAllHeaders(); + copyHeaders(headers, toMessage); + } + + private static void copyHeaders(MimeMessage fromMessage, MimeMessage toMessage) throws MessagingException { + Enumeration
headers = fromMessage.getAllHeaders(); + copyHeaders(headers, toMessage); + } + + private static void copyHeaders(Enumeration
headers, MimeMessage toMessage) throws MessagingException { + while (headers.hasMoreElements()) { + Header header = headers.nextElement(); + toMessage.setHeader(header.getName(), header.getValue()); + } + } + + private static SMIMEEnvelopedGenerator prepareGenerator(X509Certificate certificate, + KeyEncapsulationAlgorithm keyEncapsulationAlgorithm) + throws CertificateEncodingException, InvalidAlgorithmParameterException { + JceKeyTransRecipientInfoGenerator infoGenerator; + if (keyEncapsulationAlgorithm == KeyEncapsulationAlgorithm.RSA) { + infoGenerator = new JceKeyTransRecipientInfoGenerator(certificate); + } else { + String digestName; + if (keyEncapsulationAlgorithm == KeyEncapsulationAlgorithm.RSA_OAEP_SHA224) { + digestName = "SHA-234"; + } else if (keyEncapsulationAlgorithm == KeyEncapsulationAlgorithm.RSA_OAEP_SHA256) { + digestName = "SHA-256"; + } else if (keyEncapsulationAlgorithm == KeyEncapsulationAlgorithm.RSA_OAEP_SHA384) { + digestName = "SHA-384"; + } else if (keyEncapsulationAlgorithm == KeyEncapsulationAlgorithm.RSA_OAEP_SHA512) { + digestName = "SHA-512"; + } else { + throw new InvalidAlgorithmParameterException("Unknown S/MIME key encapsulation algorithm: " + + keyEncapsulationAlgorithm.name()); + } + JcaAlgorithmParametersConverter paramsConverter = new JcaAlgorithmParametersConverter(); + AlgorithmIdentifier oaepParams = paramsConverter.getAlgorithmIdentifier( + PKCSObjectIdentifiers.id_RSAES_OAEP, new OAEPParameterSpec( + digestName, "MGF1", new MGF1ParameterSpec(digestName), PSource.PSpecified.DEFAULT)); + infoGenerator = new JceKeyTransRecipientInfoGenerator(certificate, oaepParams); + } + infoGenerator.setProvider(BouncyCastleProvider.PROVIDER_NAME); + SMIMEEnvelopedGenerator generator = new SMIMEEnvelopedGenerator(); + generator.addRecipientInfoGenerator(infoGenerator); + return generator; + } + + private static OutputEncryptor prepareEncryptor(ASN1ObjectIdentifier cmsAlgorithm) throws CMSException { + return new JceCMSContentEncryptorBuilder(cmsAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(); + } + + /** + * Decrypts an S/MIME encrypted MIME message and yields a new MIME message. + * + * @param session The {@link Session} that is used in conjunction with the + * encrypted {@link MimeMessage}. + * @param mimeMessage The encrypted {@link MimeMessage} to be decrypted. + * @param smimeKey The {@link SmimeKey} used to obtain the {@link PrivateKey} to + * decrypt the encrypted message with. + * @return The new S/MIME decrypted {@link MimeMessage}. + */ + public static MimeMessage decrypt(Session session, MimeMessage mimeMessage, SmimeKey smimeKey) { + try { + byte[] content = decryptContent(new SMIMEEnveloped(mimeMessage), smimeKey); + MimeBodyPart mimeBodyPart = SMIMEUtil.toMimeBodyPart(content); + + MimeMessage decryptedMessage = new MimeMessage(session); + copyHeaderLines(mimeMessage, decryptedMessage); + copyContent(mimeBodyPart, decryptedMessage); + decryptedMessage.setHeader("Content-Type", mimeBodyPart.getContentType()); + return decryptedMessage; + + } catch (Exception e) { + throw handledException(e); + } + } + + /** + * Decrypts an S/MIME encrypted MIME body part and yields a new MIME body + * part. + * + * @param mimeBodyPart The encrypted {@link MimeBodyPart} to be decrypted. + * @param smimeKey The {@link SmimeKey} used to obtain the {@link PrivateKey} to + * decrypt the encrypted body part with. + * @return The new S/MIME decrypted {@link MimeBodyPart}. + */ + public static MimeBodyPart decrypt(MimeBodyPart mimeBodyPart, SmimeKey smimeKey) { + try { + return SMIMEUtil.toMimeBodyPart(decryptContent(new SMIMEEnveloped(mimeBodyPart), smimeKey)); + } catch (Exception e) { + throw handledException(e); + } + } + + /** + * Decrypts an S/MIME encrypted MIME multipart and yields a new MIME body + * part. + * + * @param mimeMultipart The encrypted {@link MimeMultipart} to be decrypted. + * @param smimeKey The {@link SmimeKey} used to obtain the {@link PrivateKey} to + * decrypt the encrypted multipart with. + * @return The new S/MIME decrypted {@link MimeBodyPart}. + */ + public static MimeBodyPart decrypt(MimeMultipart mimeMultipart, SmimeKey smimeKey) { + try { + MimeBodyPart mimeBodyPart = new MimeBodyPart(); + mimeBodyPart.setContent(mimeMultipart); + mimeBodyPart.setHeader("Content-Type", mimeMultipart.getContentType()); + return decrypt(mimeBodyPart, smimeKey); + } catch (Exception e) { + throw handledException(e); + } + } + + private static byte[] decryptContent(SMIMEEnveloped smimeEnveloped, SmimeKey smimeKey) throws MessagingException, CMSException { + X509Certificate certificate = smimeKey.getCertificate(); + PrivateKey privateKey = smimeKey.getPrivateKey(); + + RecipientInformationStore recipients = smimeEnveloped.getRecipientInfos(); + RecipientInformation recipient = recipients.get(new JceKeyTransRecipientId(certificate)); + + if (null == recipient) { + throw new MessagingException("no recipient"); + } + + JceKeyTransRecipient transportRecipient = new JceKeyTransEnvelopedRecipient(privateKey); + transportRecipient.setProvider(BouncyCastleProvider.PROVIDER_NAME); + return recipient.getContent(transportRecipient); + } + + private static void copyHeaderLines(MimeMessage fromMessage, MimeMessage toMessage) throws MessagingException { + Enumeration headerLines = fromMessage.getAllHeaderLines(); + while (headerLines.hasMoreElements()) { + String nextElement = headerLines.nextElement(); + toMessage.addHeaderLine(nextElement); + } + } + + private static void copyContent(MimeBodyPart fromBodyPart, MimeMessage toMessage) throws MessagingException, IOException { + toMessage.setContent(fromBodyPart.getContent(), fromBodyPart.getContentType()); + } + + /** + * Signs a MIME body part and yields a new S/MIME signed MIME body part. + * + * @param mimeBodyPart The original {@link MimeBodyPart} to be signed. + * @param smimeKey The {@link SmimeKey} used to obtain the {@link PrivateKey} to + * sign the original body part with. + * @return The new S/MIME signed {@link MimeBodyPart}. + */ + public static MimeBodyPart sign(MimeBodyPart mimeBodyPart, SmimeKey smimeKey) { + return sign(mimeBodyPart, smimeKey, defaultSignatureAlgorithmName); + } + + /** + * Signs a MIME body part and yields a new S/MIME signed MIME body part. + * + * @param mimeBodyPart The original {@link MimeBodyPart} to be signed. + * @param smimeKey The {@link SmimeKey} used to obtain the {@link PrivateKey} to + * sign the original body part with. + * @param algorithmName The name of the signature algorithm to use. Must be an algorithm + * supported by the Bouncy Castle security provider. + * @return The new S/MIME signed {@link MimeBodyPart}. + */ + public static MimeBodyPart sign(MimeBodyPart mimeBodyPart, SmimeKey smimeKey, String algorithmName) { + try { + SMIMESignedGenerator generator = getGenerator(smimeKey, algorithmName); + MimeMultipart signedMimeMultipart = generator.generate(MimeUtil.canonicalize(mimeBodyPart)); + MimeBodyPart signedMimeBodyPart = new MimeBodyPart(); + signedMimeBodyPart.setContent(signedMimeMultipart); + return signedMimeBodyPart; + + } catch (Exception e) { + throw handledException(e); + } + + } + + private static SMIMESignedGenerator getGenerator(SmimeKey smimeKey, String algorithmName) + throws CertificateEncodingException, OperatorCreationException { + SMIMESignedGenerator generator = new SMIMESignedGenerator(); + generator.addCertificates(getCertificateStore(smimeKey)); + generator.addSignerInfoGenerator(getInfoGenerator(smimeKey, algorithmName)); + return generator; + } + + private static SignerInfoGenerator getInfoGenerator(SmimeKey smimeKey, String algorithmName) + throws OperatorCreationException, CertificateEncodingException { + JcaSimpleSignerInfoGeneratorBuilder builder = new JcaSimpleSignerInfoGeneratorBuilder(); + builder.setSignedAttributeGenerator(new AttributeTable(getSignedAttributes(smimeKey))); + builder.setProvider(BouncyCastleProvider.PROVIDER_NAME); + + PrivateKey privateKey = smimeKey.getPrivateKey(); + X509Certificate certificate = smimeKey.getCertificate(); + return builder.build(algorithmName, privateKey, certificate); + } + + private static ASN1EncodableVector getSignedAttributes(SmimeKey smimeKey) { + ASN1EncodableVector signedAttributes = new ASN1EncodableVector(); + IssuerAndSerialNumber issuerAndSerialNumber = getIssuerAndSerialNumber(smimeKey); + signedAttributes.add(new SMIMEEncryptionKeyPreferenceAttribute(issuerAndSerialNumber)); + signedAttributes.add(new SMIMECapabilitiesAttribute(getCapabilityVector())); + return signedAttributes; + } + + private static SMIMECapabilityVector getCapabilityVector() { + SMIMECapabilityVector capabilityVector = new SMIMECapabilityVector(); + capabilityVector.addCapability(SMIMECapability.dES_EDE3_CBC); + capabilityVector.addCapability(SMIMECapability.rC2_CBC, 128); + capabilityVector.addCapability(SMIMECapability.dES_CBC); + return capabilityVector; + } + + private static IssuerAndSerialNumber getIssuerAndSerialNumber(SmimeKey smimeKey) { + X509Certificate certificate = smimeKey.getCertificate(); + BigInteger serialNumber = certificate.getSerialNumber(); + X500Name issuerName = new X500Name(certificate.getIssuerX500Principal().getName()); + return new IssuerAndSerialNumber(issuerName, serialNumber); + } + + private static JcaCertStore getCertificateStore(SmimeKey smimeKey) throws CertificateEncodingException { + Certificate[] certificateChain = smimeKey.getCertificateChain(); + X509Certificate certificate = smimeKey.getCertificate(); + + final List certificateList; + if (certificateChain != null && certificateChain.length > 0) { + certificateList = Arrays.asList(certificateChain); + } else { + certificateList = new ArrayList<>(); + certificateList.add(certificate); + } + return new JcaCertStore(certificateList); + } + + /** + * Signs a MIME message and yields a new S/MIME signed MIME message. + * + * @param session The {@link Session} that is used in conjunction with the + * original {@link MimeMessage}. + * @param mimeMessage The original {@link MimeMessage} or {@link SMTPMessage} to be signed. + * @param smimeKey The {@link SmimeKey} used to obtain the {@link PrivateKey} to + * sign the original message with. + * @return The new S/MIME signed {@link MimeMessage} or {@link SMTPMessage}. + */ + public static T sign(Session session, T mimeMessage, SmimeKey smimeKey) { + return sign(session, mimeMessage, smimeKey, defaultSignatureAlgorithmName); + } + + /** + * Signs a MIME message and yields a new S/MIME signed MIME message. + * + * @param session The {@link Session} that is used in conjunction with the + * original {@link MimeMessage}. + * @param mimeMessage The original {@link MimeMessage} or {@link SMTPMessage} to be signed. + * @param smimeKey The {@link SmimeKey} used to obtain the {@link PrivateKey} to + * sign the original message with. + * @param algorithmName The name of the signature algorithm to use. Must be an algorithm + * supported by the Bouncy Castle security provider. + * @return The new S/MIME signed {@link MimeMessage} or {@link SMTPMessage}. + */ + public static T sign(Session session, T mimeMessage, SmimeKey smimeKey, String algorithmName) { + //noinspection unchecked + return (mimeMessage instanceof SMTPMessage) + ? sign(mimeMessage, (T) new SMTPMessage(session), smimeKey, algorithmName) + : sign(mimeMessage, (T) new MimeMessage(session), smimeKey, algorithmName); + } + + private static T sign(T mimeMessage, T signedMessage, SmimeKey smimeKey, String algorithmName) { + try { + copyHeaderLines(mimeMessage, signedMessage); + copyContent(sign(extractMimeBodyPart(mimeMessage), smimeKey, algorithmName), signedMessage); + return signedMessage; + } catch (Exception e) { + throw handledException(e); + } + } + + private static MimeBodyPart extractMimeBodyPart(MimeMessage mimeMessage) throws IOException, MessagingException { + Object content = mimeMessage.getContent(); + UpdatableMimeBodyPart updateableMimeBodyPart = new UpdatableMimeBodyPart(); + if (content instanceof Multipart) { + updateableMimeBodyPart.setContent((Multipart) content); + } else { + updateableMimeBodyPart.setContent(content, mimeMessage.getDataHandler().getContentType()); + } + updateableMimeBodyPart.updateHeaders(); + return updateableMimeBodyPart; + } + + /** + * Checks the signature on an S/MIME signed MIME multipart. + * + * @param mimeMultipart The {@link MimeMultipart} to be checked. + * @return {@code true} if the multipart is correctly signed, {@code false} + * otherwise. + */ + public static boolean checkSignature(MimeMultipart mimeMultipart) { + try { + return checkSignature(new SMIMESigned(mimeMultipart)); + } catch (Exception e) { + throw handledException(e); + } + } + + /** + * Checks the signature on an S/MIME signed MIME part (i.e. MIME message). + * + * @param mimePart The {@link MimePart} to be checked. + * @return {@code true} if the part is correctly signed, {@code false} + * otherwise. + */ + public static boolean checkSignature(MimePart mimePart) { + try { + if (mimePart.isMimeType("multipart/signed")) { + return checkSignature(new SMIMESigned((MimeMultipart) mimePart.getContent())); + } else if (mimePart.isMimeType("application/pkcs7-mime") || mimePart.isMimeType("application/x-pkcs7-mime")) { + return checkSignature(new SMIMESigned(mimePart)); + } else { + throw new SmimeException("Message not signed"); + } + } catch (Exception e) { + throw handledException(e); + } + } + + /** + * Checks a SMIMESigned to make sure that the signature matches. + */ + private static boolean checkSignature(SMIMESigned smimeSigned) { + try { + boolean returnValue = true; + + @SuppressWarnings("rawtypes") + Store certificates = smimeSigned.getCertificates(); + Iterator signerInformations = smimeSigned.getSignerInfos().getSigners().iterator(); + + while (returnValue && signerInformations.hasNext()) { + SignerInformation signerInformation = signerInformations.next(); + X509Certificate certificate = getCertificate(certificates, signerInformation.getSID()); + SignerInformationVerifier verifier = getVerifier(certificate); + if (!signerInformation.verify(verifier)) { + returnValue = false; + } + } + return returnValue; + + } catch (Exception e) { + throw handledException(e); + } + } + + /** + * @param mimeMultipart The {@link MimeMultipart} to be checked. + * @return The subject / address to which the certificate was issued to. Email clients may use this to show + * {@code "Signed by: "} + */ + public static String getSignedByAddress(MimeMultipart mimeMultipart) { + try { + return getSignedByAddress(new SMIMESigned(mimeMultipart)); + } catch (Exception e) { + throw handledException(e); + } + } + + /** + * @param mimePart The {@link MimePart} to be checked. + * @return The subject / address to which the certificate was issued to. Email clients may use this to show + * {@code "Signed by: "} + */ + public static String getSignedByAddress(MimePart mimePart) { + try { + if (mimePart.isMimeType("multipart/signed")) { + return getSignedByAddress(new SMIMESigned((MimeMultipart) mimePart.getContent())); + } else if (mimePart.isMimeType("application/pkcs7-mime") || mimePart.isMimeType("application/x-pkcs7-mime")) { + return getSignedByAddress(new SMIMESigned(mimePart)); + } else { + throw new SmimeException("Message not signed"); + } + } catch (Exception e) { + throw handledException(e); + } + } + + /** + * Returns the subject / address to which the certificate was issued to. Email clients may use this to show + * {@code "Signed by: "} + */ + private static String getSignedByAddress(SMIMESigned smimeSigned) { + try { + @SuppressWarnings("rawtypes") + Store certificates = smimeSigned.getCertificates(); + + SignerInformation signerInformation = smimeSigned.getSignerInfos().getSigners().iterator().next(); + X509Certificate certificate = getCertificate(certificates, signerInformation.getSID()); + SignerInformationVerifier verifier = getVerifier(certificate); + X500Name x500name = verifier.getAssociatedCertificate().getSubject(); + RDN cn = x500name.getRDNs(BCStyle.CN)[0]; + return IETFUtils.valueToString(cn.getFirst().getValue()); + + } catch (Exception e) { + throw handledException(e); + } + } + + private static X509Certificate getCertificate(@SuppressWarnings("rawtypes") Store certificates, + SignerId signerId) throws CertificateException { + @SuppressWarnings({"unchecked"}) + X509CertificateHolder certificateHolder = (X509CertificateHolder) certificates.getMatches(signerId).iterator().next(); + JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter(); + certificateConverter.setProvider(BouncyCastleProvider.PROVIDER_NAME); + return certificateConverter.getCertificate(certificateHolder); + } + + private static SignerInformationVerifier getVerifier(X509Certificate certificate) throws OperatorCreationException { + JcaSimpleSignerInfoVerifierBuilder builder = new JcaSimpleSignerInfoVerifierBuilder(); + builder.setProvider(BouncyCastleProvider.PROVIDER_NAME); + return builder.build(certificate); + } + + /** + * Returns the signed MIME body part of an S/MIME signed MIME multipart. + * + * @param mimeMultipart The {@link MimeMultipart} to be stripped off. + * @return The signed {@link MimeBodyPart} contained in the + * {@link MimeMultipart}. + */ + public static MimeBodyPart getSignedContent(MimeMultipart mimeMultipart) { + try { + return new SMIMESigned(mimeMultipart).getContent(); + } catch (Exception e) { + throw handledException(e); + } + } + + /** + * Returns the signed MIME body part of an S/MIME signed MIME part (i.e. MIME + * message). + * + * @param mimePart The {@link MimePart} to be stripped off. + * @return The signed {@link MimeBodyPart} contained in the {@link MimePart} + * . + */ + public static MimeBodyPart getSignedContent(MimePart mimePart) { + try { + if (mimePart.isMimeType("multipart/signed")) { + return new SMIMESigned((MimeMultipart) mimePart.getContent()).getContent(); + } else if (mimePart.isMimeType("application/pkcs7-mime") || mimePart.isMimeType("application/x-pkcs7-mime")) { + return new SMIMESigned(mimePart).getContent(); + } else { + throw new SmimeException("Message not signed"); + } + } catch (Exception e) { + throw handledException(e); + } + } + + /** + * Returns the S/MIME state of a MIME multipart. + * + * @param mimeMultipart The {@link MimeMultipart} to be checked. + * @return the {@link SmimeState} of the {@link MimeMultipart}. + */ + public static SmimeState getStatus(MimeMultipart mimeMultipart) { + try { + return getStatus(new ContentType(mimeMultipart.getContentType())); + } catch (Exception e) { + throw handledException(e); + } + } + + /** + * Returns the S/MIME state of a MIME part (i.e. MIME message). + * + * @param mimePart The {@link MimePart} to be checked. + * @return the {@link SmimeState} of the {@link MimePart}. + */ + public static SmimeState getStatus(MimePart mimePart) { + try { + return getStatus(new ContentType(mimePart.getContentType())); + } catch (Exception e) { + throw handledException(e); + } + } + + private static SmimeState getStatus(ContentType contentType) { + if (isSmimeSignatureContentType(contentType)) { + return SmimeState.SIGNED; + } else if (isSignatureSmimeType(contentType)) { + return SmimeState.SIGNED_ENVELOPED; + } else if (isSmimeEncryptionContenttype(contentType)) { + return SmimeState.ENCRYPTED; + } else { + return SmimeState.NEITHER; + } + } + + private static boolean isSmimeEncryptionContenttype(ContentType contentType) { + String baseContentType = contentType.getBaseType(); + return baseContentType.equalsIgnoreCase("application/pkcs7-mime") + || baseContentType.equalsIgnoreCase("application/x-pkcs7-mime"); + } + + private static boolean isSmimeSignatureContentType(ContentType contentType) { + String baseContentType = contentType.getBaseType(); + String protocol = contentType.getParameter("protocol"); + return baseContentType.equalsIgnoreCase("multipart/signed") + && protocol != null && isSmimeSignatureProtocoll(protocol); + } + + private static boolean isSignatureSmimeType(ContentType contentType) { + String baseContentType = contentType.getBaseType(); + return baseContentType.equalsIgnoreCase("application/x-pkcs7-mime") + && "signed-data".equals(contentType.getParameter("smime-type")); + } + + private static boolean isSmimeSignatureProtocoll(String protocol) { + return protocol.equalsIgnoreCase("application/pkcs7-signature") + || protocol.equalsIgnoreCase("application/x-pkcs7-signature"); + } + + private static SmimeException handledException(Exception e) { + if (e instanceof SmimeException) { + return (SmimeException) e; + } + return new SmimeException(e.getMessage(), e); + } } diff --git a/src/test/java/org/simplejavamail/utils/mail/smime/SmimeUtilTest.java b/src/test/java/org/simplejavamail/utils/mail/smime/SmimeUtilTest.java new file mode 100644 index 0000000..1d34764 --- /dev/null +++ b/src/test/java/org/simplejavamail/utils/mail/smime/SmimeUtilTest.java @@ -0,0 +1,125 @@ +package org.simplejavamail.utils.mail.smime; + +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; +import org.bouncycastle.cms.CMSAlgorithm; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Security; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SmimeUtilTest { + + private static final String SignatureAlgorithmRsa = "SHA256withRSA"; + private static final String SignatureAlgorithmRsaPss = "SHA256WITHRSAANDMGF1"; + private SmimeKeyStore alicesKeyStore; + private SmimeKeyStore bobsKeyStore; + private Session mailSession; + + @BeforeAll + void setup() throws MessagingException, KeyStoreException, NoSuchProviderException, CertificateException, IOException, NoSuchAlgorithmException { + Security.addProvider(new BouncyCastleProvider()); + InputStream alicesKeystoreStream = this.getClass().getClassLoader().getResourceAsStream("alice.p12"); + this.alicesKeyStore = new SmimeKeyStore(alicesKeystoreStream, "alice".toCharArray()); + InputStream bobsKeystoreStream = this.getClass().getClassLoader().getResourceAsStream("bob.p12"); + this.bobsKeyStore = new SmimeKeyStore(bobsKeystoreStream, "bob".toCharArray()); + + Properties sessionProps = System.getProperties(); // new Properties(); // Fake properties for a fake session + this.mailSession = Session.getDefaultInstance(sessionProps); + } + + private MimeMessage createTestMessage(String from, String to) throws MessagingException { + MimeMessage testMessage = new MimeMessage(this.mailSession); + testMessage.setFrom(new InternetAddress(from)); + testMessage.setRecipient(MimeMessage.RecipientType.TO, new InternetAddress(to)); + testMessage.setSubject("This is a test email"); + testMessage.setContent("This is some test content for the test email's body", "text/plain; charset=utf-8"); + return testMessage; + } + + @Test + void SuccessfullySignAndValidate() throws MessagingException, IOException { + MimeMessage testMessage = createTestMessage("alice@testcorp.com", "alice@testcorp.com"); + SmimeKey alicesKey = this.alicesKeyStore.getPrivateKey("alice", "alice".toCharArray()); + MimeMessage signedMessage = SmimeUtil.sign(this.mailSession, testMessage, alicesKey); + MimeMultipart multipartContent = (MimeMultipart) signedMessage.getContent(); + assertEquals(SmimeState.SIGNED, SmimeUtil.getStatus(multipartContent)); + boolean isSignatureValid = SmimeUtil.checkSignature(multipartContent); + assertTrue(isSignatureValid); + } + + @Test + void SuccessfullyEnvelopeAndDecryptDefault() throws MessagingException { + MimeMessage testMessage = createTestMessage("alice@testcorp.com", "alice@testcorp.com"); + SmimeKey alicesKey = this.alicesKeyStore.getPrivateKey("alice", "alice".toCharArray()); + X509Certificate alicesCert = alicesKey.getCertificate(); + MimeMessage encryptedMessage = SmimeUtil.encrypt(this.mailSession, + SmimeUtil.sign(this.mailSession, testMessage, alicesKey), + alicesCert); + assertEquals(SmimeState.ENCRYPTED, SmimeUtil.getStatus((encryptedMessage))); + MimeMessage decryptedMessage = SmimeUtil.decrypt(this.mailSession, encryptedMessage, alicesKey); + boolean isSignatureValid = SmimeUtil.checkSignature(decryptedMessage); + assertTrue(isSignatureValid); + } + + @Test + void SuccessfullyEnvelopeAndDecrypt() throws MessagingException { + MimeMessage testMessage = createTestMessage("alice@testcorp.com", "alice@testcorp.com"); + SmimeKey alicesKey = this.alicesKeyStore.getPrivateKey("alice", "alice".toCharArray()); + X509Certificate alicesCert = alicesKey.getCertificate(); + MimeMessage encryptedMessage = SmimeUtil.encrypt(this.mailSession, + SmimeUtil.sign(this.mailSession, testMessage, alicesKey, SignatureAlgorithmRsaPss), + alicesCert, KeyEncapsulationAlgorithm.RSA_OAEP_SHA256, CMSAlgorithm.AES256_CBC); + assertEquals(SmimeState.ENCRYPTED, SmimeUtil.getStatus((encryptedMessage))); + MimeMessage decryptedMessage = SmimeUtil.decrypt(this.mailSession, encryptedMessage, alicesKey); + boolean isSignatureValid = SmimeUtil.checkSignature(decryptedMessage); + assertTrue(isSignatureValid); + } + + @Test + void AliceToBoEnvelopeAndDecrypt() throws MessagingException { + MimeMessage testMessage = createTestMessage("alice@testcorp.com", "bob@testcorp.com"); + SmimeKey alicesKey = this.alicesKeyStore.getPrivateKey("alice", "alice".toCharArray()); + SmimeKey bobsKey = this.bobsKeyStore.getPrivateKey("bob", "bob".toCharArray()); + X509Certificate bobsCert = bobsKey.getCertificate(); + MimeMessage encryptedMessage = SmimeUtil.encrypt(this.mailSession, + SmimeUtil.sign(this.mailSession, testMessage, alicesKey, SignatureAlgorithmRsaPss), + bobsCert, KeyEncapsulationAlgorithm.RSA_OAEP_SHA512, CMSAlgorithm.AES256_GCM); + assertEquals(SmimeState.ENCRYPTED, SmimeUtil.getStatus((encryptedMessage))); + MimeMessage decryptedMessage = SmimeUtil.decrypt(this.mailSession, encryptedMessage, bobsKey); + boolean isSignatureValid = SmimeUtil.checkSignature(decryptedMessage); + assertTrue(isSignatureValid); + } + + @Test + void BobToAliceEnvelopeAndDecrypt() throws MessagingException { + MimeMessage testMessage = createTestMessage("bob@testcorp.com", "alice@testcorp.com"); + SmimeKey bobsKey = this.bobsKeyStore.getPrivateKey("bob", "bob".toCharArray()); + SmimeKey alicesKey = this.alicesKeyStore.getPrivateKey("alice", "alice".toCharArray()); + X509Certificate alicesCert = alicesKey.getCertificate(); + MimeMessage encryptedMessage = SmimeUtil.encrypt(this.mailSession, + SmimeUtil.sign(this.mailSession, testMessage, bobsKey, SignatureAlgorithmRsaPss), + alicesCert, KeyEncapsulationAlgorithm.RSA_OAEP_SHA384, CMSAlgorithm.AES192_CCM); + assertEquals(SmimeState.ENCRYPTED, SmimeUtil.getStatus((encryptedMessage))); + MimeMessage decryptedMessage = SmimeUtil.decrypt(this.mailSession, encryptedMessage, alicesKey); + boolean isSignatureValid = SmimeUtil.checkSignature(decryptedMessage); + assertTrue(isSignatureValid); + } +} \ No newline at end of file diff --git a/src/test/resources/alice-certgen-rsa.sh b/src/test/resources/alice-certgen-rsa.sh new file mode 100755 index 0000000..b7a2063 --- /dev/null +++ b/src/test/resources/alice-certgen-rsa.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# This script can be used to generate a self-signed test-certificate for the fictional principal "Alice". +# The certificate is issued on the basis of a standard RSA key-pair. + +### Set the openssl version to use. +openssl_bin="/usr/local/opt/openssl@1.1/bin/openssl" +account_name="alice" +priv_key_name="${account_name}.priv" +certificate_config_filename="${account_name}.cnf" +validity_days=1825 # Five years, so the tests won't fail too soon. + +echo "Generating private RSA key" +$openssl_bin genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:4096 -pkeyopt rsa_keygen_pubexp:65537 -out ${priv_key_name}.rsakey +### Save the private key without password protection +$openssl_bin rsa -in ${priv_key_name}.rsakey -out ${priv_key_name}.nopass.rsakey + +echo "Generating self-signed certificate..." +$openssl_bin req -outform PEM -out ${account_name}.pem -key ${priv_key_name}.nopass.rsakey -keyform PEM -x509 -nodes -batch -days $validity_days -config $certificate_config_filename -pkeyopt rsa_keygen_bits:2048 -sha256 + +echo "Generating .p12 file with certificate and private key..." +$openssl_bin pkcs12 -export -in ${account_name}.pem -inkey ${priv_key_name}.nopass.rsakey -out ${account_name}.p12 diff --git a/src/test/resources/alice.cnf b/src/test/resources/alice.cnf new file mode 100644 index 0000000..e7ba56a --- /dev/null +++ b/src/test/resources/alice.cnf @@ -0,0 +1,34 @@ +[ req ] +default_bits = 4096 +distinguished_name = req_distinguished_name +x509_extensions = x509_ext +string_mask = utf8only + +[ req_distinguished_name ] +countryName = Country Name (2 letter code) +countryName_default = AA +countryName_min = 2 +countryName_max = 2 +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = Testprovince +localityName = Locality Name (eg, city) +localityName_default = Testtown +0.organizationName = Organization Name (eg, company) +0.organizationName_default = Testcorp +commonName = Common Name +commonName_default = Alice +commonName_max = 64 +emailAddress = Email Address +emailAddress_default = alice@testcorp.com +emailAddress_max = 64 + +# Section x509_ext is used when generating a self-signed certificate. I.e., openssl req -x509 ... +[ x509_ext ] + +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer + +basicConstraints = CA:FALSE +keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +extendedKeyUsage = clientAuth, emailProtection +#subjectAltName = email:copy \ No newline at end of file diff --git a/src/test/resources/alice.p12 b/src/test/resources/alice.p12 new file mode 100644 index 0000000..e0e73b7 Binary files /dev/null and b/src/test/resources/alice.p12 differ diff --git a/src/test/resources/bob-certgen-rsa.sh b/src/test/resources/bob-certgen-rsa.sh new file mode 100755 index 0000000..91cbb49 --- /dev/null +++ b/src/test/resources/bob-certgen-rsa.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# This script can be used to generate a self-signed test-certificate for the fictional principal "Bob". +# The certificate is issued on the basis of a RSASSA-PSS key-pair. + +### Set the openssl version to use. Must be OpenSSL 1.1 for RSASSA-PSS support +openssl_bin="/usr/local/opt/openssl@1.1/bin/openssl" +account_name="bob" +priv_key_name="${account_name}.priv" +certificate_config_filename="${account_name}.cnf" +validity_days=1825 # Five years, so the tests won't fail too soon. + +echo "Generating private RSASSA-PSS key" +$openssl_bin genpkey -algorithm rsa-pss -pkeyopt rsa_keygen_bits:4096 -pkeyopt rsa_keygen_pubexp:65537 -pkeyopt rsa_pss_keygen_md:sha256 -pkeyopt rsa_pss_keygen_mgf1_md:sha256 -pkeyopt rsa_pss_keygen_saltlen:32 -out ${priv_key_name}.rsapsskey +### Save the private key without password protection +$openssl_bin rsa -in ${priv_key_name}.rsapsskey -out ${priv_key_name}.nopass.rsapsskey + +echo "Generating self-signed certificate..." +$openssl_bin req -outform PEM -out ${account_name}.pem -key ${priv_key_name}.nopass.rsapsskey -keyform PEM -x509 -nodes -batch -days $validity_days -config $certificate_config_filename -pkeyopt rsa_keygen_bits:4096 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:32 -sigopt rsa_mgf1_md:sha256 -sha256 + +echo "Generating .p12 file with certificate and private key..." +$openssl_bin pkcs12 -export -in ${account_name}.pem -inkey ${priv_key_name}.nopass.rsapsskey -out ${account_name}.p12 diff --git a/src/test/resources/bob.cnf b/src/test/resources/bob.cnf new file mode 100644 index 0000000..9a7fd64 --- /dev/null +++ b/src/test/resources/bob.cnf @@ -0,0 +1,34 @@ +[ req ] +default_bits = 4096 +distinguished_name = req_distinguished_name +x509_extensions = x509_ext +string_mask = utf8only + +[ req_distinguished_name ] +countryName = Country Name (2 letter code) +countryName_default = BB +countryName_min = 2 +countryName_max = 2 +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = Testprovince +localityName = Locality Name (eg, city) +localityName_default = Testtown +0.organizationName = Organization Name (eg, company) +0.organizationName_default = Testcorp +commonName = Common Name +commonName_default = Bob +commonName_max = 64 +emailAddress = Email Address +emailAddress_default = bob@testcorp.com +emailAddress_max = 64 + +# Section x509_ext is used when generating a self-signed certificate. I.e., openssl req -x509 ... +[ x509_ext ] + +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer + +basicConstraints = CA:FALSE +keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +extendedKeyUsage = clientAuth, emailProtection +#subjectAltName = email:copy \ No newline at end of file diff --git a/src/test/resources/bob.p12 b/src/test/resources/bob.p12 new file mode 100644 index 0000000..030791a Binary files /dev/null and b/src/test/resources/bob.p12 differ