mitm.application.djigzo.james.mailets.SMIMESign.java Source code

Java tutorial

Introduction

Here is the source code for mitm.application.djigzo.james.mailets.SMIMESign.java

Source

/*
 * Copyright (c) 2008-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.application.djigzo.james.mailets;

import java.io.IOException;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.cert.CertPath;
import java.security.cert.CertPathBuilderException;
import java.security.cert.CertPathBuilderResult;
import java.security.cert.CertStoreException;
import java.security.cert.Certificate;
import java.security.cert.PKIXCertPathBuilderResult;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.LinkedList;
import java.util.List;

import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

import mitm.application.djigzo.User;
import mitm.application.djigzo.james.MessageOriginatorIdentifier;
import mitm.application.djigzo.service.SystemServices;
import mitm.application.djigzo.workflow.UserWorkflow;
import mitm.common.hibernate.DatabaseAction;
import mitm.common.hibernate.DatabaseActionExecutor;
import mitm.common.hibernate.DatabaseActionExecutorBuilder;
import mitm.common.hibernate.DatabaseException;
import mitm.common.hibernate.DatabaseVoidAction;
import mitm.common.hibernate.SessionManager;
import mitm.common.mail.EmailAddressUtils;
import mitm.common.mail.MimeMessageWithID;
import mitm.common.properties.HierarchicalPropertiesException;
import mitm.common.security.KeyAndCertificate;
import mitm.common.security.certpath.CertificatePathBuilder;
import mitm.common.security.certpath.CertificatePathBuilderFactory;
import mitm.common.security.smime.SMIMEBuilder;
import mitm.common.security.smime.SMIMEBuilderException;
import mitm.common.security.smime.SMIMEBuilderImpl;
import mitm.common.security.smime.SMIMESignMode;
import mitm.common.security.smime.SMIMESigningAlgorithm;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.lang.text.StrBuilder;
import org.apache.mailet.Mail;
import org.hibernate.Session;
import org.hibernate.exception.ConstraintViolationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Mailet that digitally signs the email using S/MIME (if the sender has a valid signing key).
 * 
 * Supported mailet parameters:
 * 
 * algorithm               : The hash algorithm for the signature (default: SHA1)
 * signMode                : clear or opaque
 * protectedHeader         : Header that should be add to the encrypted blob (can be more than one protectedHeader) 
 * retainMessageID         : If true the original Message-ID will be used for the signed message                         
 * catchRuntimeExceptions  : If true all RunTimeExceptions are caught
 * catchErrors             : If true all Errors are caught
 * 
 * @author Martijn Brinkers
 *
 */
public class SMIMESign extends AbstractDjigzoMailet {
    private final static Logger logger = LoggerFactory.getLogger(SMIMESign.class);

    /*
     * The number of times a database action should be retried when a ConstraintViolation occurs
     */
    private final static int ACTION_RETRIES = 3;

    /*
     * The digest (hash) algorithm used for the signature.
     */
    private SMIMESigningAlgorithm signingAlgorithm = SMIMESigningAlgorithm.SHA1WITHRSA;

    /*
     * The Mail attribute from which the signing algorithm is read. If null or if there is no
     * attribute with the given name, the signingAlgorithm is used. 
     */
    private String signingAlgorithmAttribute;

    /*
     * A message can be clear or opaque signed.
     */
    private SMIMESignMode signMode = SMIMESignMode.CLEAR;

    /*
     * Manages database sessions
     */
    private SessionManager sessionManager;

    /*
     * Helper for database transactions
     */
    private DatabaseActionExecutor actionExecutor;

    /*
     * Identifies the 'sender' of the message (default version uses from as the identifier)
     */
    private MessageOriginatorIdentifier messageOriginatorIdentifier;

    /*
     * Used to add/get users
     */
    private UserWorkflow userWorkflow;

    /*
     * Used for building a certificate path for the signing certificate
     */
    private CertificatePathBuilderFactory certificatePathBuilderFactory;

    /*
     * If true the old x-pkcs7-* content types will be used
     */
    private boolean useDeprecatedContentTypes;

    /*
     * Determines which headers will be signed.
     */
    private String[] protectedHeaders = new String[] {};

    /*
     * If true the root certificate will be added to the signed blob
     */
    private boolean addRoot = true;

    /*
     * If true the original Message-ID will be used for the signed message
     */
    private boolean retainMessageID = true;

    /*
     * The mailet initialization parameters used by this mailet.
     */
    private enum Parameter {
        ALGORITHM("algorithm"), ALGORITHM_ATTRIBUTE("algorithmAttribute"), SIGN_MODE(
                "signMode"), USE_DEPRECATED_CONTENT_TYPES("useDeprecatedContentTypes"), PROTECTED_HEADER(
                        "protectedHeader"), ADD_ROOT("addRoot"), RETAIN_MESSAGE_ID("retainMessageID");

        private String name;

        private Parameter(String name) {
            this.name = name;
        }
    };

    @Override
    protected Logger getLogger() {
        return logger;
    }

    private void initProtectedHeaders() {
        String param = getInitParameter(Parameter.PROTECTED_HEADER.name);

        if (param != null) {
            protectedHeaders = param.split("\\s*,\\s*");
        }
    }

    @Override
    final public void initMailet() {
        getLogger().info("Initializing mailet: " + getMailetName());

        String param = getInitParameter(Parameter.ALGORITHM.name);

        if (param != null) {
            signingAlgorithm = SMIMESigningAlgorithm.fromName(param);

            if (signingAlgorithm == null) {
                throw new IllegalArgumentException(param + " is not a valid signing algorithm.");
            }
        }

        signingAlgorithmAttribute = getInitParameter(Parameter.ALGORITHM_ATTRIBUTE.name);

        param = getInitParameter(Parameter.SIGN_MODE.name);

        if (param != null) {
            signMode = SMIMESignMode.fromName(param);

            if (signMode == null) {
                throw new IllegalArgumentException(param + " is not a valid SMIME sign mode.");
            }
        }

        param = getInitParameter(Parameter.ADD_ROOT.name);

        if (param != null) {
            addRoot = BooleanUtils.toBoolean(param);
        }

        param = getInitParameter(Parameter.RETAIN_MESSAGE_ID.name);

        if (param != null) {
            retainMessageID = BooleanUtils.toBoolean(param);
        }

        param = getInitParameter(Parameter.USE_DEPRECATED_CONTENT_TYPES.name);

        if (param != null) {
            useDeprecatedContentTypes = BooleanUtils.toBoolean(param);
        }

        initProtectedHeaders();

        StrBuilder sb = new StrBuilder();

        sb.append("Signing algorithm: ");
        sb.append(signingAlgorithm);
        sb.appendSeparator("; ");
        sb.append("Algorithm attribute: ");
        sb.append(signingAlgorithmAttribute);
        sb.appendSeparator("; ");
        sb.append("Sign mode: ");
        sb.append(signMode);
        sb.appendSeparator("; ");
        sb.append("Add root: ");
        sb.append(addRoot);
        sb.appendSeparator("; ");
        sb.append("Use deprecated content-type's: ");
        sb.append(useDeprecatedContentTypes);
        sb.appendSeparator("; ");
        sb.append("Retain Message-ID: ");
        sb.append(retainMessageID);
        sb.appendSeparator("; ");
        sb.append("Protected headers: ");
        sb.append(StringUtils.join(protectedHeaders, ","));

        getLogger().info(sb.toString());

        sessionManager = SystemServices.getSessionManager();

        actionExecutor = DatabaseActionExecutorBuilder.createDatabaseActionExecutor(sessionManager);

        assert (actionExecutor != null);

        messageOriginatorIdentifier = SystemServices.getMessageOriginatorIdentifier();

        userWorkflow = SystemServices.getUserWorkflow();

        certificatePathBuilderFactory = SystemServices.getCertificatePathBuilderFactory();
    }

    private SMIMESigningAlgorithm getSigningAlgorithm(Mail mail) {
        SMIMESigningAlgorithm signingAlgorithm = null;

        /*
         * Check if the Mail attribute contains the signing algorithm
         */
        if (StringUtils.isNotEmpty(signingAlgorithmAttribute)) {
            Object attributeValue = mail.getAttribute(signingAlgorithmAttribute);

            if (attributeValue != null) {
                if (attributeValue instanceof String) {
                    signingAlgorithm = SMIMESigningAlgorithm.fromName((String) attributeValue);

                    if (signingAlgorithm == null) {
                        getLogger().warn("The attribute value {} is not a valid algorithm.", attributeValue);
                    }
                } else {
                    getLogger().warn("Attribute {} is not a String but a ", signingAlgorithm,
                            attributeValue.getClass());
                }
            } else {
                getLogger().debug("Attribute with name {} was not found", signingAlgorithmAttribute);
            }
        }

        if (signingAlgorithm == null) {
            /*
             * Use the default signing algorithm
             */
            signingAlgorithm = this.signingAlgorithm;
        }

        getLogger().debug("Signing algorithm: {}", signingAlgorithm);

        return signingAlgorithm;
    }

    @Override
    public void serviceMail(Mail mail) {
        try {
            final InternetAddress originator = messageOriginatorIdentifier.getOriginator(mail);

            KeyAndCertificate signingKeyAndCertificate = actionExecutor
                    .executeTransaction(new DatabaseAction<KeyAndCertificate>() {
                        @Override
                        public KeyAndCertificate doAction(Session session) throws DatabaseException {
                            Session previousSession = sessionManager.getSession();

                            sessionManager.setSession(session);

                            try {
                                return getSigningKeyAndCertificateAction(session, originator);
                            } finally {
                                sessionManager.setSession(previousSession);
                            }
                        }
                    }, ACTION_RETRIES /* retry on a ConstraintViolationException */);

            MimeMessage message = mail.getMessage();

            if (signingKeyAndCertificate != null && message != null) {
                SMIMEBuilder sMIMEBuilder = new SMIMEBuilderImpl(message, protectedHeaders);

                sMIMEBuilder.setUseDeprecatedContentTypes(useDeprecatedContentTypes);

                X509Certificate signingCertificate = signingKeyAndCertificate.getCertificate();

                PrivateKey privateKey = signingKeyAndCertificate.getPrivateKey();

                if (privateKey != null && signingCertificate != null) {
                    X509Certificate[] chain = getCertificateChain(signingCertificate);

                    sMIMEBuilder.addCertificates(chain);

                    SMIMESigningAlgorithm localAlgorithm = getSigningAlgorithm(mail);

                    sMIMEBuilder.addSigner(privateKey, signingCertificate, localAlgorithm);

                    getLogger().debug("Signing message. Signing algorithm: {}, Sign mode: {}", localAlgorithm,
                            signMode);

                    sMIMEBuilder.sign(signMode);

                    MimeMessage signed = sMIMEBuilder.buildMessage();

                    if (signed != null) {
                        signed.saveChanges();

                        /*
                         * A new MimeMessage instance will be created. This makes ure that the 
                         * MimeMessage can be written by James (using writeTo()). The message-ID
                         * of the source message will be retained if told to do so
                         */
                        signed = retainMessageID ? new MimeMessageWithID(signed, message.getMessageID())
                                : new MimeMessage(signed);

                        mail.setMessage(signed);
                    }
                } else {
                    if (privateKey == null) {
                        getLogger().warn("PrivateKey is missing. Message cannot be signed.");
                    }

                    if (signingCertificate == null) {
                        getLogger().warn("signingCertificate is missing. Message cannot be signed.");
                    }
                }
            } else {
                getLogger().debug("No signing certificate found, or message is null.");
            }
        } catch (MessagingException e) {
            getLogger().error("Error reading the message.", e);
        } catch (DatabaseException e) {
            getLogger().error("Error getting signing keyAndCertificate.", e);
        } catch (IOException e) {
            getLogger().error("Error signing the message.", e);
        } catch (SMIMEBuilderException e) {
            getLogger().error("Error signing the message.", e);
        }
    }

    private void makePersistentAction(User user) {
        userWorkflow.makePersistent(user);
    }

    private void makePersistent(final User user) throws DatabaseException {
        if (!userWorkflow.isPersistent(user)) {
            /*
             * We will persist the user in it's own session so we can catch any ConstraintViolationException. When multiple
             * messages are sent from a new user it can happen that multiple threads create the same user. We want to
             * catch the ConstraintViolationException log and continue.
             */
            try {
                actionExecutor.executeTransaction(new DatabaseVoidAction() {
                    @Override
                    public void doAction(Session session) throws DatabaseException {
                        Session previousSession = sessionManager.getSession();

                        sessionManager.setSession(session);

                        try {
                            makePersistentAction(user);
                        } finally {
                            sessionManager.setSession(previousSession);
                        }
                    }
                });
            } catch (ConstraintViolationException e) {
                /*
                 * Should normally not happen because makePersistent updates the user if the user 
                 * is already persistent
                 */
                getLogger().error(
                        "Error making the user persistent. The user was probably already persisted. Message: "
                                + e.getMessage());
            }
        }
    }

    /*
     * Returns the signing certificate for the user or null if the user does not have a valid signing certificate
     */
    private KeyAndCertificate getSigningKeyAndCertificateAction(Session session, InternetAddress originator)
            throws DatabaseException {
        KeyAndCertificate keyAndCertificate = null;

        if (originator != null) {
            try {
                User user;

                try {
                    String userEmail = originator.getAddress();

                    userEmail = EmailAddressUtils.canonicalizeAndValidate(userEmail, false);

                    user = userWorkflow.getUser(userEmail, UserWorkflow.GetUserMode.CREATE_IF_NOT_EXIST);
                } catch (HierarchicalPropertiesException e) {
                    throw new DatabaseException(e);
                }

                keyAndCertificate = user.getSigningKeyAndCertificate();

                if (keyAndCertificate != null) {
                    makePersistent(user);
                }
            } catch (CertStoreException e) {
                throw new DatabaseException(e);
            } catch (KeyStoreException e) {
                throw new DatabaseException(e);
            } catch (AddressException e) {
                throw new DatabaseException(e);
            } catch (HierarchicalPropertiesException e) {
                throw new DatabaseException(e);
            }
        }

        return keyAndCertificate;
    }

    /*
     * Returns the certificate chain of the signingCertificate.
     */
    private X509Certificate[] getCertificateChain(X509Certificate signingCertificate) {
        X509Certificate[] chain = null;

        try {
            /*
             * Use CertificatePathBuilderFactory instead of PKITrustCheckCertificateValidator because we
             * assume that the signing certificate was already checked for revocation etc. 
             * CertificatePathBuilderFactory is faster than PKITrustCheckCertificateValidator
             */
            CertificatePathBuilder pathBuilder = certificatePathBuilderFactory.createCertificatePathBuilder();

            CertPathBuilderResult pathBuilderResult = pathBuilder.buildPath(signingCertificate);

            CertPath certPath = pathBuilderResult.getCertPath();

            if (certPath != null && CollectionUtils.isNotEmpty(certPath.getCertificates())) {
                X509Certificate root = null;

                if (addRoot && pathBuilderResult instanceof PKIXCertPathBuilderResult) {
                    TrustAnchor trustAnchor = ((PKIXCertPathBuilderResult) pathBuilderResult).getTrustAnchor();

                    if (trustAnchor != null) {
                        root = trustAnchor.getTrustedCert();
                    }
                }

                List<X509Certificate> completePath = new LinkedList<X509Certificate>();

                for (Certificate fromPath : certPath.getCertificates()) {
                    if (!(fromPath instanceof X509Certificate)) {
                        /*
                         * only X509Certificates are supported
                         */
                        continue;
                    }

                    completePath.add((X509Certificate) fromPath);
                }

                if (root != null && addRoot) {
                    completePath.add(root);
                }

                chain = new X509Certificate[completePath.size()];

                chain = completePath.toArray(chain);
            }
        } catch (CertPathBuilderException e) {
            if (getLogger().isDebugEnabled()) {
                getLogger().warn("Error building path for signing certificate.", e);
            } else {
                getLogger().warn(
                        "Error building path for signing certificate. " + ExceptionUtils.getRootCauseMessage(e));
            }
        }

        if (chain == null) {
            chain = new X509Certificate[] { signingCertificate };
        }

        return chain;
    }
}