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