Java tutorial
/* * Copyright (c) 2012, Martijn Brinkers, Djigzo. * * This file is part of Djigzo email encryption. * * Djigzo is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License * version 3, 19 November 2007 as published by the Free Software * Foundation. * * Djigzo is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public * License along with Djigzo. If not, see <http://www.gnu.org/licenses/> * * Additional permission under GNU AGPL version 3 section 7 * * If you modify this Program, or any covered work, by linking or * combining it with aspectjrt.jar, aspectjweaver.jar, tyrex-1.0.3.jar, * freemarker.jar, dom4j.jar, mx4j-jmx.jar, mx4j-tools.jar, * spice-classman-1.0.jar, spice-loggerstore-0.5.jar, spice-salt-0.8.jar, * spice-xmlpolicy-1.0.jar, saaj-api-1.3.jar, saaj-impl-1.3.jar, * wsdl4j-1.6.1.jar (or modified versions of these libraries), * containing parts covered by the terms of Eclipse Public License, * tyrex license, freemarker license, dom4j license, mx4j license, * Spice Software License, Common Development and Distribution License * (CDDL), Common Public License (CPL) the licensors of this Program grant * you additional permission to convey the resulting work. */ package mitm.common.security.smime.handler; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.AlgorithmParameters; import java.security.cert.CertSelector; import java.security.cert.CertStoreException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.mail.MessagingException; import javax.mail.Part; import javax.mail.internet.MimeMessage; import mitm.common.mail.BodyPartUtils; import mitm.common.mail.HeaderUtils; import mitm.common.security.PKISecurityServices; import mitm.common.security.certificate.CertificateUtils; import mitm.common.security.certificate.X509CertificateInspector; import mitm.common.security.certificate.validator.CertificateValidatorChain; import mitm.common.security.certificate.validator.IsValidForSMIMESigning; import mitm.common.security.cms.CryptoMessageSyntaxException; import mitm.common.security.cms.RecipientInfo; import mitm.common.security.cms.SignerIdentifier; import mitm.common.security.cms.SignerInfo; import mitm.common.security.cms.SignerInfoException; import mitm.common.security.smime.SMIMEEncryptionAlgorithm; import mitm.common.security.smime.SMIMEEnvelopedInspector; import mitm.common.security.smime.SMIMEInspector; import mitm.common.security.smime.SMIMESecurityInfoHeader; import mitm.common.security.smime.SMIMESignedInspector; import mitm.common.util.BigIntegerUtils; import mitm.common.util.Check; import mitm.common.util.HexUtils; import mitm.common.util.MiscStringUtils; import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Default implemenation of SMIMEInfoHandler that adds security info to the headers (like signed by, recipient IDs etc.) * * @author Martijn Brinkers * */ public class SMIMEInfoHandlerImpl implements SMIMEInfoHandler { private static final Logger logger = LoggerFactory.getLogger(SMIMEInfoHandlerImpl.class); /* * Provides PKI related functionality */ private final PKISecurityServices securityServices; /* * The default max header length (see maxHeaderLength) */ private static final int DEFAULT_MAX_HEADER_LENGTH = 4096; /* * The default max recipients length (see maxRecipients) */ private static final int DEFAULT_MAX_RECIPIENTS = 50; /* * The maximum length of a header. A long header will always be folded. This maximum is the maximum * length of the header including folding. */ private int maxHeaderLength = DEFAULT_MAX_HEADER_LENGTH; /* * The maximum number of encryption recipient headers that will be added to the message */ private int maxRecipients = DEFAULT_MAX_RECIPIENTS; /* * If true the certificates from the CMS blob will be temporarily added to the * CertPathBuilder. */ private boolean addCertificates = false; public SMIMEInfoHandlerImpl(PKISecurityServices securityServices) { Check.notNull(securityServices, "securityServices"); this.securityServices = securityServices; } /* * Makes sure the header is encoded when it contains non ASCII characters is folded and has a * maximum length. */ private void setHeader(String headerName, String headerValue, int level, Part part) throws MessagingException { if (headerValue == null) { headerValue = ""; } headerName = headerName + "-" + Integer.toString(level); headerValue = MiscStringUtils.restrictLength(headerValue, maxHeaderLength); try { /* make sure the header only contains ASCII characters and is folded * when necessary */ headerValue = HeaderUtils.encodeHeaderValue(headerName, headerValue); } catch (UnsupportedEncodingException e) { logger.warn("Header value cannot be encoded. Message: " + e.getMessage()); } part.setHeader(headerName, headerValue); } protected void onSigned(MimeMessage message, SMIMEInspector sMIMEInspector, int level, boolean signatureValid, Set<String> signers) throws MessagingException { /* do nothing by default */ } private MimeMessage handleSigned(MimeMessage message, SMIMEInspector sMIMEInspector, int level) throws MessagingException { SMIMESignedInspector signedInspector = sMIMEInspector.getSignedInspector(); Collection<X509Certificate> certificates = null; try { certificates = signedInspector.getCertificates(); } catch (CryptoMessageSyntaxException e) { logger.error("Error getting certificates from signed message.", e); } Set<String> signerEmail = new HashSet<String>(); List<SignerInfo> signers; try { signers = signedInspector.getSigners(); } catch (CryptoMessageSyntaxException e) { throw new MessagingException("Error getting signers.", e); } Boolean signatureValid = null; for (int signerIndex = 0; signerIndex < signers.size(); signerIndex++) { Boolean thisSignatureValid = null; try { SignerInfo signer = signers.get(signerIndex); SignerIdentifier signerId; try { signerId = signer.getSignerId(); } catch (IOException e) { logger.error("Error getting signerId", e); /* * Continue with other signers */ continue; } addSignerIdentifierInfo(message, signerIndex, signerId, level); /* * try to get the signing certificate using a certificate selector */ CertSelector certSelector; try { certSelector = signerId.getSelector(); } catch (IOException e) { logger.error("Error getting selector for signer", e); /* * Continue with other signers */ continue; } /* first search through the certificates that are embedded in the CMS blob */ Collection<X509Certificate> signingCerts = CertificateUtils.getMatchingCertificates(certificates, certSelector); if (signingCerts.size() == 0 && securityServices.getKeyAndCertStore() != null) { /* * the certificate could not be found in the CMS blob. If the CertStore is * set we will see if the CertStore has a match. */ try { signingCerts = securityServices.getKeyAndCertStore().getCertificates(certSelector); } catch (CertStoreException e) { logger.error("Error getting certificates from the CertStore.", e); } } X509Certificate signingCertificate = null; if (signingCerts != null && signingCerts.size() > 0) { /* * there can be more than one match, although in practice this should not happen * often. Get the first one. If the sender is so stupid to send different * certificates with same issuer/serial, subjectKeyId (which is not likely) than * he/she cannot expect the signature to validate correctly. If the sender did * not add a certificate to the CMS blob but the certificate was found in the * CertStore it can happen that the wrong certificate was found. Solving this * requires that we step through all certificates found an verify until we found * the correct one. */ signingCertificate = signingCerts.iterator().next(); } if (signingCertificate != null) { if (!verifySignature(message, signerIndex, signer, signingCertificate, level)) { thisSignatureValid = false; } /* * we expect that the certificates from the CMS blob were already added to the stores * if the certificate is not in the store the path cannot be build. It is possible to * add them temporarily to the CertPathBuilder but that's not always as fast as adding * them to the global CertStore. */ if (!verifySigningCertificate(message, signerIndex, signingCertificate, certificates, level)) { thisSignatureValid = false; } if (thisSignatureValid == null) { thisSignatureValid = true; } try { signerEmail.addAll(new X509CertificateInspector(signingCertificate).getEmail()); } catch (Exception e) { logger.error("Error getting email addresses", e); } } else { String info = "Signing certificate could not be found."; logger.warn(info); setHeader(SMIMESecurityInfoHeader.SIGNER_VERIFIED + signerIndex, "False", level, message); setHeader(SMIMESecurityInfoHeader.SIGNER_VERIFICATION_INFO + signerIndex, info, level, message); } } finally { /* * Only make the overall signatureValid true if it was not already set and if the current * signature check is valid */ if (BooleanUtils.isTrue(thisSignatureValid) && signatureValid == null) { signatureValid = true; } if (BooleanUtils.isFalse(thisSignatureValid)) { signatureValid = false; } } } /* end for */ onSigned(message, sMIMEInspector, level, BooleanUtils.isTrue(signatureValid), signerEmail); return message; } private void signingCertificateNotTrusted(Part part, int signerIndex, Throwable t, int level) throws MessagingException { Throwable cause = ExceptionUtils.getRootCause(t); if (cause == null) { cause = t; } String info = signingCertificateNotTrusted(part, signerIndex, cause.getMessage(), level); logger.error(info, t); } private String signingCertificateNotTrusted(Part part, int signerIndex, String message, int level) throws MessagingException { String info = "Signing certificate not trusted"; if (message != null) { info = info + ". Message: " + message; } info = info + ". Timestamp: " + System.currentTimeMillis(); setHeader(SMIMESecurityInfoHeader.SIGNER_TRUSTED + signerIndex, "False", level, part); setHeader(SMIMESecurityInfoHeader.SIGNER_TRUSTED_INFO + signerIndex, info, level, part); return info; } private boolean verifySigningCertificate(Part part, int signerIndex, X509Certificate signingCertificate, Collection<? extends Certificate> certificates, int level) throws MessagingException { boolean valid = false; CertificateValidatorChain chain = new CertificateValidatorChain(); chain.addValidators(new IsValidForSMIMESigning()); chain.addValidators(securityServices.getPKITrustCheckCertificateValidatorFactory() .createValidator(addCertificates ? certificates : null)); try { valid = chain.isValid(signingCertificate); if (valid) { setHeader(SMIMESecurityInfoHeader.SIGNER_TRUSTED + signerIndex, "True", level, part); } else { signingCertificateNotTrusted(part, signerIndex, chain.getFailureMessage(), level); } } catch (CertificateException e) { signingCertificateNotTrusted(part, signerIndex, e, level); } return valid; } private boolean verifySignature(Part part, int signerIndex, SignerInfo signer, X509Certificate signingCertificate, int level) throws MessagingException { boolean valid = false; try { /* * check if the message can be verified using the public key of the signer. */ if (!signer.verify(signingCertificate.getPublicKey())) { throw new SignerInfoException("Message content cannot be verified with the signers public key."); } setHeader(SMIMESecurityInfoHeader.SIGNER_VERIFIED + signerIndex, "True", level, part); valid = true; } catch (SignerInfoException e) { String info = "Signature could not be verified. Message: " + e.getMessage(); if (logger.isDebugEnabled()) { logger.warn(info, e); } else { logger.warn(info); } setHeader(SMIMESecurityInfoHeader.SIGNER_VERIFIED + signerIndex, "False", level, part); setHeader(SMIMESecurityInfoHeader.SIGNER_VERIFICATION_INFO + signerIndex, info, level, part); } return valid; } private void addSignerIdentifierInfo(Part part, int signerIndex, SignerIdentifier signerId, int level) throws MessagingException { String headerName = SMIMESecurityInfoHeader.SIGNER_ID + signerIndex; String headerValue = ObjectUtils.toString(signerId.getIssuer()) + "/" + BigIntegerUtils.hexEncode(signerId.getSerialNumber()) + "/" + HexUtils.hexEncode(signerId.getSubjectKeyIdentifier(), ""); setHeader(headerName, headerValue, level, part); } protected void onEncrypted(MimeMessage message, SMIMEInspector sMIMEInspector, int level, boolean decrypted) throws MessagingException { /* do nothing by default */ } private MimeMessage handleEncrypted(MimeMessage message, SMIMEInspector sMIMEInspector, int level, boolean decrypted) throws MessagingException { SMIMEEnvelopedInspector envelopedInspector = sMIMEInspector.getEnvelopedInspector(); String encryptionAlgorithm = envelopedInspector.getEncryptionAlgorithmOID(); SMIMEEncryptionAlgorithm encryptionAlg = SMIMEEncryptionAlgorithm.fromOID(encryptionAlgorithm); if (encryptionAlg != null) { encryptionAlgorithm = encryptionAlg.toString(); AlgorithmParameters parameters; try { parameters = envelopedInspector.getEncryptionAlgorithmParameters(); encryptionAlgorithm = encryptionAlgorithm + ", Key size: " + SMIMEEncryptionAlgorithm.getKeySize(encryptionAlg, parameters); } catch (CryptoMessageSyntaxException e) { logger.error("Error getting encryption algorithm parameters"); } } setHeader(SMIMESecurityInfoHeader.ENCRYPTION_ALGORITHM, encryptionAlgorithm, level, message); List<RecipientInfo> recipients; try { recipients = envelopedInspector.getRecipients(); } catch (CryptoMessageSyntaxException e) { throw new MessagingException("Error getting encryption recipients.", e); } int nrOfRecipients = recipients.size(); if (nrOfRecipients > maxRecipients) { logger.warn("There are more recipients but the maximum has been reached."); nrOfRecipients = maxRecipients; } for (int recipientIndex = 0; recipientIndex < nrOfRecipients; recipientIndex++) { RecipientInfo recipient = recipients.get(recipientIndex); setHeader(SMIMESecurityInfoHeader.ENCRYPTION_RECIPIENT + recipientIndex, recipient.toString(), level, message); } onEncrypted(message, sMIMEInspector, level, decrypted); return message; } private MimeMessage handleCompressed(MimeMessage message, SMIMEInspector sMIMEInspector, int level) throws MessagingException { setHeader(SMIMESecurityInfoHeader.COMPRESSED, "True", level, message); return message; } private MimeMessage handlePart(MimeMessage message, SMIMEInspector sMIMEInspector, int level, boolean decrypted) throws MessagingException { switch (sMIMEInspector.getSMIMEType()) { case SIGNED: return handleSigned(message, sMIMEInspector, level); case ENCRYPTED: return handleEncrypted(message, sMIMEInspector, level, decrypted); case COMPRESSED: return handleCompressed(message, sMIMEInspector, level); default: break; } return message; } protected MimeMessage handle(MimeMessage message, SMIMEInspector inspector, int level, boolean decrypted) throws SMIMEHandlerException { MimeMessage result = message; try { Part handledPart = handlePart(message, inspector, level, decrypted); if (handledPart != null) { result = BodyPartUtils.toMessage(handledPart); } } catch (Exception e) { logger.error("Error handling part.", e); } return result; } @Override public MimeMessage handle(MimeMessage message, SMIMEHandler handler, int level) throws SMIMEHandlerException { return handle(message, handler.getSMIMEInspector(), level, handler.isDecrypted()); } /** * If true the certificates from the CMS blob will be temporarily added to the CertPathBuilder. * Normally the certificates should already be added to the store because it's faster in most * cases because in most cases the database CertStore is faster. */ public boolean isAddCertificates() { return addCertificates; } /** * If true the certificates from the CMS blob will be temporarily added to the CertPathBuilder. * Normally the certificates should already be added to the store because it's faster in most * cases because in most cases the database CertStore is faster. */ public void setAddCertificates(boolean addCertificates) { this.addCertificates = addCertificates; } public int getMaxHeaderLength() { return maxHeaderLength; } public void setMaxHeaderLength(int maxHeaderLength) { this.maxHeaderLength = maxHeaderLength; } public int getMaxRecipients() { return maxRecipients; } public void setMaxRecipients(int maxRecipients) { this.maxRecipients = maxRecipients; } }