mitm.common.security.smime.handler.SMIMEInfoHandlerImpl.java Source code

Java tutorial

Introduction

Here is the source code for mitm.common.security.smime.handler.SMIMEInfoHandlerImpl.java

Source

/*
 * 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;
    }
}