mitm.application.djigzo.relay.RelayHandler.java Source code

Java tutorial

Introduction

Here is the source code for mitm.application.djigzo.relay.RelayHandler.java

Source

/*
 * Copyright (c) 2009-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.relay;

import java.io.IOException;
import java.security.cert.CertSelector;
import java.security.cert.CertStoreException;
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.BodyPart;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Part;
import javax.mail.internet.MimeMessage;
import javax.xml.bind.JAXBException;

import mitm.common.mail.BodyPartUtils;
import mitm.common.mail.EmailAddressUtils;
import mitm.common.security.PKISecurityServices;
import mitm.common.security.certificate.CertificateUtils;
import mitm.common.security.certificate.validator.CertificateValidator;
import mitm.common.security.certificate.validator.IsValidForSMIMESigning;
import mitm.common.security.certificate.validator.PKITrustCheckCertificateValidator;
import mitm.common.security.cms.CryptoMessageSyntaxException;
import mitm.common.security.cms.SignerIdentifier;
import mitm.common.security.cms.SignerInfo;
import mitm.common.security.cms.SignerInfoException;
import mitm.common.security.smime.SMIMESignedInspector;
import mitm.common.security.smime.SMIMEType;
import mitm.common.security.smime.handler.SMIMEHandler;
import mitm.common.security.smime.handler.SMIMEHandlerException;
import mitm.common.util.Check;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * RelayHandler checks whether a message is a valid relay message. If the message
 * is a valid relay message the embedded message is returned.
 * 
 * Note: this message is not thread safe
 * 
 * @author Martijn Brinkers
 *
 */
public class RelayHandler {
    private final static Logger logger = LoggerFactory.getLogger(RelayHandler.class);

    public static final String META_INFO_IS_NOT_XML = "Meta info is not XML.";
    public static final String META_XML_IS_INVALID = "Meta XML is invalid.";
    public static final String INVALID_SIGNATURE = "Invalid signature.";
    public static final String NO_VALID_CERTIFICATES = "There are no valid relaying certificates.";
    public static final String SMIME_ERROR = "S/MIME message cannot be handled.";
    public static final String MESSAGE_IS_NOT_SMIME = "Message is not S/MIME.";
    public static final String MESSAGE_IS_NOT_SIGNED = "Message is not signed.";
    public static final String META_PART_NOT_FOUND = "Meta part not found.";
    public static final String RELAY_MESSAGE_NOT_FOUND = "Relay message not found.";
    public static final String RELAY_INFO_NULL = "Relay info is null.";
    public static final String RELAY_NOT_ALLOWED = "Relaying not allowed.";
    public static final String RELAY_CERTS_MISSING = "Relay certificates not available.";
    public static final String MESSAGE_EXPIRED = "The message expired.";
    public static final String USER_INFO_MISSING = "User info missing.";

    /*
     * all PKI related services
     */
    private final PKISecurityServices securityServices;

    /*
     * Used to get some user info based on sender of the message.
     * This is used to decouple database code from this handler.
     */
    private final RelayUserInfoProvider relayUserInfoProvider;

    /*
     * RelayValidator can be used for external parties to check whether relay is allowed
     */
    private RelayValidator additionalRelayValidator;

    /*
     * The part with the XML meta info
     */
    private Part metaPart;

    /*
     * The message that should be relay'ed
     */
    private MimeMessage relayMessage;

    /*
     * The meta relay info
     */
    private RelayUserInfo userInfo;

    /*
     * The recipients of the relayed messages (is read from the meta info attached to the message)
     */
    private Set<String> recipients;

    public RelayHandler(PKISecurityServices securityServices, RelayUserInfoProvider relayUserInfoProvider) {
        Check.notNull(securityServices, "securityServices");
        Check.notNull(relayUserInfoProvider, "relayUserInfoProvider");

        this.securityServices = securityServices;
        this.relayUserInfoProvider = relayUserInfoProvider;
    }

    /*
     * Find the message to relay and the meta XML info
     */
    private void extractMessageParts(MimeMessage message) throws MessagingException, IOException {
        /*
         * Fast fail. Only multipart mixed messages are supported. 
         */
        if (!message.isMimeType("multipart/mixed")) {
            return;
        }

        Multipart mp;

        try {
            mp = (Multipart) message.getContent();
        } catch (IOException e) {
            throw new MessagingException("Error getting message content.", e);
        }

        for (int i = 0; i < mp.getCount(); i++) {
            BodyPart part = mp.getBodyPart(i);

            if (part.isMimeType("message/rfc822")) {
                relayMessage = BodyPartUtils.extractFromRFC822(part);
            } else if (part.isMimeType("text/xml")) {
                metaPart = part;
            }

            if (metaPart != null && relayMessage != null) {
                break;
            }
        }
    }

    /*
     * Unmarshals the XML from the meta info part
     */
    private RelayInfo getRelayInfo() throws IOException, MessagingException, RelayException {
        Object content = metaPart.getContent();

        if (!(content instanceof String)) {
            throw new RelayException(RelayStep.INVALID, META_INFO_IS_NOT_XML);
        }

        String xml = (String) content;

        try {
            return RelayInfoFactory.unmarshall(IOUtils.toInputStream(xml, "UTF-8"));
        } catch (JAXBException e) {
            throw new RelayException(RelayStep.INVALID, META_XML_IS_INVALID);
        }
    }

    private boolean isValid(X509Certificate certificate) {
        PKITrustCheckCertificateValidator certificateValidator = securityServices
                .getPKITrustCheckCertificateValidatorFactory().createValidator(null);

        boolean valid = false;

        try {
            /*
             * Check if the certificate is PKI wise valid (trusted, not revoked, not expired etc.)
             */
            if (certificateValidator.isValid(certificate)) {
                CertificateValidator validator = new IsValidForSMIMESigning();

                if (validator.isValid(certificate)) {
                    valid = true;
                } else {
                    logger.warn("Signing certificate is not valid for S/MIME signing.");
                }
            } else {
                logger.warn("Signing certificate is not valid. " + certificateValidator.getFailureMessage());
            }
        } catch (CertificateException e) {
            /*
             * In case of a CertificateException we won't add the certificate
             */
            if (logger.isDebugEnabled()) {
                logger.warn("Error while validating the relay certificate.", e);
            } else {
                logger.warn(
                        "Error while validating the relay certificate. " + ExceptionUtils.getRootCauseMessage(e));
            }

            valid = false;
        }

        return valid;
    }

    private void verifySignature(SignerInfo signer, X509Certificate signingCertificate) throws RelayException {
        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.");
            }
        } catch (SignerInfoException e) {
            throw new RelayException(RelayStep.INVALID_SIGNATURE, INVALID_SIGNATURE, e);
        }
    }

    /*
     * Checks the signature of the message and checks if the signing certificate is a valid relay certificate
     */
    private void checkSignature(SMIMESignedInspector signedInspector,
            Collection<X509Certificate> usersRelayCertificates) throws MessagingException, RelayException {
        Collection<X509Certificate> certificates = null;

        try {
            certificates = signedInspector.getCertificates();
        } catch (CryptoMessageSyntaxException e) {
            logger.error("Error getting certificates from signed message.", e);
        }

        List<SignerInfo> signers;

        try {
            signers = signedInspector.getSigners();
        } catch (CryptoMessageSyntaxException e) {
            throw new MessagingException("Error getting signers.", e);
        }

        boolean relayingAllowed = false;

        try {
            /*
             * Validate the signature and check if the signing certificate can be used for relaying
             */
            for (int signerIndex = 0; signerIndex < signers.size(); signerIndex++) {
                SignerInfo signer = signers.get(signerIndex);

                SignerIdentifier signerId = signer.getSignerId();

                /* 
                 * try to get the signing certificate using a certificate selector
                 */
                CertSelector certSelector;

                certSelector = signerId.getSelector();

                /* 
                 * 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 check 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 (!isValid(signingCertificate)) {
                        continue;
                    }

                    /*
                     * Check if the message was signed by the signer and not tampered
                     */
                    verifySignature(signer, signingCertificate);

                    /*
                     * Now see if the signingCertificate can be used for relaying. We do not need to check 
                     * whether the usersRelayCertificates are valid because we have already checked the 
                     * signingCertificate for validity (see check above)
                     */
                    if (usersRelayCertificates.contains(signingCertificate)) {
                        relayingAllowed = true;

                        break;
                    }
                } else {
                    logger.warn("Signing certificate could not be found.");
                }
            }

            if (!relayingAllowed) {
                throw new RelayException(RelayStep.RELAYING_NOT_ALLOWED, NO_VALID_CERTIFICATES);
            }
        } catch (IOException e) {
            throw new RelayException("Error checking the signature", e);
        }
    }

    private void initializeRecipients(RelayInfo relayInfo) throws RelayException {
        recipients = new HashSet<String>();

        for (RelayRecipient recipient : relayInfo.getRecipients()) {
            String email = EmailAddressUtils.canonicalizeAndValidate(recipient.getEmail(), true);

            if (email == null) {
                throw new RelayException(RelayStep.INVALID_RECIPIENT,
                        recipient.getEmail() + " is not a valid email address.");
            }

            recipients.add(email);
        }
    }

    private void checkExpired(RelayInfo relayInfo, RelayUserInfo userInfo) throws RelayException {
        if (userInfo.getValidityInterval() != 0) {
            if ((System.currentTimeMillis() - relayInfo.getTimestamp()) > userInfo.getValidityInterval()) {
                throw new RelayException(RelayStep.MESSAGE_EXPIRED, MESSAGE_EXPIRED);
            }
        }
    }

    public MimeMessage handleRelayMessage(MimeMessage message)
            throws RelayException, MessagingException, IOException {
        relayMessage = null;
        userInfo = null;
        metaPart = null;
        recipients = null;

        SMIMEHandler sMIMEHandler = new SMIMEHandler(securityServices);

        /*
         * We need to remove the signature because we want the raw message
         */
        sMIMEHandler.setRemoveSignature(true);

        MimeMessage sMIMEMessage = null;

        try {
            sMIMEMessage = sMIMEHandler.handlePart(message);
        } catch (SMIMEHandlerException e) {
            throw new RelayException(RelayStep.INVALID, SMIME_ERROR);
        }

        if (sMIMEMessage == null) {
            throw new RelayException(RelayStep.INVALID, MESSAGE_IS_NOT_SMIME);
        }

        if (sMIMEHandler.getSMIMEType() != SMIMEType.SIGNED) {
            throw new RelayException(RelayStep.INVALID, MESSAGE_IS_NOT_SIGNED);
        }

        extractMessageParts(sMIMEMessage);

        if (metaPart == null) {
            throw new RelayException(RelayStep.INVALID, META_PART_NOT_FOUND);
        }

        if (relayMessage == null) {
            throw new RelayException(RelayStep.INVALID, RELAY_MESSAGE_NOT_FOUND);
        }

        RelayInfo relayInfo = getRelayInfo();

        if (relayInfo == null) {
            throw new RelayException(RelayStep.RELAYING_NOT_ALLOWED, RELAY_INFO_NULL);
        }

        userInfo = relayUserInfoProvider.getRelayUserInfo(relayMessage);

        if (userInfo == null) {
            throw new RelayException(RelayStep.RELAYING_NOT_ALLOWED, USER_INFO_MISSING);
        }

        if (!userInfo.isRelayAllowed()) {
            throw new RelayException(RelayStep.RELAYING_NOT_ALLOWED, RELAY_NOT_ALLOWED);
        }

        checkExpired(relayInfo, userInfo);

        Collection<X509Certificate> usersRelayCertificates = userInfo.getRelayCertificates();

        if (usersRelayCertificates == null || usersRelayCertificates.size() == 0) {
            throw new RelayException(RelayStep.RELAYING_NOT_ALLOWED, RELAY_CERTS_MISSING);
        }

        SMIMESignedInspector signedInspector = sMIMEHandler.getSMIMEInspector().getSignedInspector();

        checkSignature(signedInspector, usersRelayCertificates);

        initializeRecipients(relayInfo);

        if (additionalRelayValidator != null) {
            additionalRelayValidator.validate(relayInfo);
        }

        return relayMessage;
    }

    public Set<String> getRecipients() {
        return recipients;
    }

    public RelayUserInfo getUserInfo() {
        return userInfo;
    }

    public void setAdditionalRelayValidator(RelayValidator relayValidator) {
        this.additionalRelayValidator = relayValidator;
    }
}