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