hk.hku.cecid.piazza.commons.security.SMimeMessage.java Source code

Java tutorial

Introduction

Here is the source code for hk.hku.cecid.piazza.commons.security.SMimeMessage.java

Source

/* 
 * Copyright(c) 2005 Center for E-Commerce Infrastructure Development, The
 * University of Hong Kong (HKU). All Rights Reserved.
 *
 * This software is licensed under the GNU GENERAL PUBLIC LICENSE Version 2.0 [1]
 * 
 * [1] http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
 */

package hk.hku.cecid.piazza.commons.security;

import hk.hku.cecid.piazza.commons.activation.Mailcap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.CertStore;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Iterator;

import javax.activation.CommandInfo;
import javax.activation.CommandMap;
import javax.activation.MailcapCommandMap;
import javax.mail.Session;
import javax.mail.internet.InternetHeaders;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMultipart;

import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.cms.AttributeTable;
import org.bouncycastle.asn1.cms.IssuerAndSerialNumber;
import org.bouncycastle.asn1.smime.SMIMECapabilitiesAttribute;
import org.bouncycastle.asn1.smime.SMIMECapability;
import org.bouncycastle.asn1.smime.SMIMECapabilityVector;
import org.bouncycastle.asn1.smime.SMIMEEncryptionKeyPreferenceAttribute;
import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.RecipientId;
import org.bouncycastle.cms.RecipientInformation;
import org.bouncycastle.cms.RecipientInformationStore;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationStore;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.mail.smime.SMIMECompressed;
import org.bouncycastle.mail.smime.SMIMECompressedGenerator;
import org.bouncycastle.mail.smime.SMIMEEnveloped;
import org.bouncycastle.mail.smime.SMIMEEnvelopedGenerator;
import org.bouncycastle.mail.smime.SMIMESigned;
import org.bouncycastle.mail.smime.SMIMESignedGenerator;
import org.bouncycastle.util.encoders.Base64;

/**
 * SMimeMessage represents a Secure MIME Message. It encapsulates a MIME body part 
 * and provides methods for digital signing, signature verification, encryption,
 * decryption, compression, and decompression.
 * 
 * @author Hugo Y. K. Lam
 *
 */
public class SMimeMessage {

    private static Mailcap[] mailcaps = null;

    static {
        mailcaps = new Mailcap[] {
                new Mailcap("application/pkcs7-signature", "content-handler",
                        "org.bouncycastle.mail.smime.handlers.pkcs7_signature"),
                new Mailcap("application/pkcs7-mime", "content-handler",
                        "org.bouncycastle.mail.smime.handlers.pkcs7_mime"),
                new Mailcap("application/x-pkcs7-signature", "content-handler",
                        "org.bouncycastle.mail.smime.handlers.x_pkcs7_signature"),
                new Mailcap("application/x-pkcs7-mime", "content-handler",
                        "org.bouncycastle.mail.smime.handlers.x_pkcs7_mime"),
                new Mailcap("multipart/signed", "content-handler",
                        "org.bouncycastle.mail.smime.handlers.multipart_signed"),
                new Mailcap("text/xml", "content-handler", "com.sun.mail.handlers.text_xml") };
    }

    /**
     * Digest algorithm: MD5
     */
    public static final String DIGEST_ALG_MD5 = SMIMESignedGenerator.DIGEST_MD5;

    /**
     * Digest algorithm: SHA
     */
    public static final String DIGEST_ALG_SHA1 = SMIMESignedGenerator.DIGEST_SHA1;

    /**
     * Encryption algorithm: DES EDE3
     */
    public static final String ENCRYPT_ALG_DES_EDE3_CBC = SMIMEEnvelopedGenerator.DES_EDE3_CBC;

    /**
     * Encryption algorithm: RC2
     */
    public static final String ENCRYPT_ALG_RC2_CBC = SMIMEEnvelopedGenerator.RC2_CBC;

    /**
     * Content transfer encoding: Base 64
     */
    public static final String CONTENT_TRANSFER_ENC_BASE64 = "base64";

    /**
     * Content transfer encoding: Binary
     */
    public static final String CONTENT_TRANSFER_ENC_BINARY = "binary";

    private static final String SECURITY_PROVIDER = "BC";

    private MimeBodyPart bodyPart;

    private Session session;

    private PrivateKey privateKey;

    private X509Certificate cert;

    private String digestAlgorithm;

    private String encryptAlgorithm;

    private String contentTransferEncoding;

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * Creates a new instance of SMimeMessage.
     * 
     * @param bodyPart the original MIME body part.
     */
    public SMimeMessage(MimeBodyPart bodyPart) {
        this(bodyPart, (X509Certificate) null);
    }

    /**
     * Creates a new instance of SMimeMessage.
     * 
     * @param bodyPart the original MIME body part.
     * @param cert the certificate for signature verification or encryption.
     */
    public SMimeMessage(MimeBodyPart bodyPart, X509Certificate cert) {
        this(bodyPart, cert, null, null);
    }

    /**
     * Creates a new instance of SMimeMessage.
     * 
     * @param bodyPart the original MIME body part.
     * @param cert the certificate for signature verification or encryption.
     * @param session the mail session.
     */
    public SMimeMessage(MimeBodyPart bodyPart, X509Certificate cert, Session session) {
        this(bodyPart, cert, null, session);
    }

    /**
     * Creates a new instance of SMimeMessage.
     * 
     * @param bodyPart the original MIME body part.
     * @param cert the certificate for signature verification or encryption.
     * @param privateKey the private key for digital signing or decryption.
     */
    public SMimeMessage(MimeBodyPart bodyPart, X509Certificate cert, PrivateKey privateKey) {
        this(bodyPart, cert, privateKey, null);
    }

    /**
     * Creates a new instance of SMimeMessage.
     * 
     * @param bodyPart the original MIME body part.
     * @param cert the certificate for signature verification or encryption.
     * @param privateKey the private key for digital signing or decryption.
     * @param session the mail session.
     */
    public SMimeMessage(MimeBodyPart bodyPart, X509Certificate cert, PrivateKey privateKey, Session session) {
        this.bodyPart = bodyPart;
        this.cert = cert;
        this.privateKey = privateKey;
        this.session = session;
    }

    /**
     * Creates a new instance of SMimeMessage.
     * 
     * @param bodyPart the original MIME body part.
     * @param smime the S/MIME message from which the configuration is copied.
     */
    protected SMimeMessage(MimeBodyPart bodyPart, SMimeMessage smime) {
        this(bodyPart, smime.cert, smime.privateKey, smime.session);
        this.digestAlgorithm = smime.digestAlgorithm;
        this.encryptAlgorithm = smime.encryptAlgorithm;
        this.contentTransferEncoding = smime.contentTransferEncoding;
    }

    /**
     * Signs the encapsulated MIME body part.  
     * 
     * @return an S/MIME message encapsulating the signed MIME body part. 
     * @throws SMimeException if unable to sign the body part.
     */
    public SMimeMessage sign() throws SMimeException {
        try {
            if (privateKey == null) {
                throw new SMimeException("Private key not found");
            }

            try {
                setDefaults();

                /* Create the SMIMESignedGenerator */
                SMIMECapabilityVector capabilities = new SMIMECapabilityVector();
                capabilities.addCapability(SMIMECapability.dES_EDE3_CBC);
                capabilities.addCapability(SMIMECapability.rC2_CBC, 128);
                capabilities.addCapability(SMIMECapability.dES_CBC);

                ASN1EncodableVector attributes = new ASN1EncodableVector();
                attributes.add(new SMIMEEncryptionKeyPreferenceAttribute(new IssuerAndSerialNumber(
                        new X509Name(cert.getIssuerDN().getName()), cert.getSerialNumber())));
                attributes.add(new SMIMECapabilitiesAttribute(capabilities));

                SMIMESignedGenerator signer = new SMIMESignedGenerator();
                signer.setContentTransferEncoding(getContentTransferEncoding());
                signer.addSigner(privateKey, cert, getDigestAlgorithm(), new AttributeTable(attributes), null);

                /* Add the list of certs to the generator */
                ArrayList certList = new ArrayList();
                certList.add(cert);
                CertStore certs = CertStore.getInstance("Collection", new CollectionCertStoreParameters(certList),
                        SECURITY_PROVIDER);
                signer.addCertificatesAndCRLs(certs);

                /* Sign the body part */
                MimeMultipart mm = signer.generate(bodyPart, SECURITY_PROVIDER);

                InternetHeaders headers = new InternetHeaders();
                boolean isContentTypeFolded = new Boolean(System.getProperty("mail.mime.foldtext", "true"))
                        .booleanValue();
                headers.setHeader("Content-Type",
                        isContentTypeFolded ? mm.getContentType() : mm.getContentType().replaceAll("\\s", " "));
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                mm.writeTo(baos);
                MimeBodyPart signedPart = new MimeBodyPart(headers, baos.toByteArray());

                return new SMimeMessage(signedPart, this);
            } catch (org.bouncycastle.mail.smime.SMIMEException ex) {
                throw new SMimeException(ex.getMessage(), ex.getUnderlyingException());
            }
        } catch (Exception e) {
            throw new SMimeException("Unable to sign body part", e);
        }
    }

    /**
     * Unsigns the encapsulated MIME body part.
     * 
     * @return the an S/MIME message encapsulating the signed content.
     * @throws SMimeException if unable to unsign the body part.
     */
    public SMimeMessage unsign() throws SMimeException {
        try {
            setDefaults();

            SMIMESigned signed = new SMIMESigned((MimeMultipart) bodyPart.getContent());
            MimeBodyPart signedPart = signed.getContent();
            if (signedPart == null) {
                throw new SMimeException("No signed part");
            }
            return new SMimeMessage(signedPart, this);
        } catch (Exception e) {
            if (e instanceof CMSException) {
                e = ((CMSException) e).getUnderlyingException();
            }
            throw new SMimeException("Unable to unsign body part", e);
        }
    }

    /**
     * Verifies the encapsulated MIME body part.
     * 
     * @return an S/MIME message encapsulating the signed content. 
     * @throws SMimeException if unable to verify the body part.
     */
    public SMimeMessage verify() throws SMimeException {
        return verify(cert);
    }

    /**
     * Verifies the encapsulated MIME body part.
     * 
     * @param cert the certificate for verification.
     * @return an S/MIME message encapsulating the signed content. 
     * @throws SMimeException if unable to verify the body part.
     */
    public SMimeMessage verify(X509Certificate cert) throws SMimeException {
        try {
            if (cert == null) {
                throw new SMimeException("No certificate for verification");
            }

            setDefaults();

            SMIMESigned signed = new SMIMESigned((MimeMultipart) bodyPart.getContent());
            // CertStore cs = signed.getCertificatesAndCRLs("Collection", "BC");
            SignerInformationStore signers = signed.getSignerInfos();
            Iterator signerInfos = signers.getSigners().iterator();

            while (signerInfos.hasNext()) {
                SignerInformation signerInfo = (SignerInformation) signerInfos.next();
                if (!signerInfo.verify(cert, "BC")) {
                    throw new SMimeException("Verification failed");
                }
            }

            MimeBodyPart signedPart = signed.getContent();
            if (signedPart == null) {
                throw new SMimeException("Unable to extract signed part");
            } else {
                return new SMimeMessage(signedPart, this);
            }
        } catch (Exception e) {
            if (e instanceof CMSException) {
                e = ((CMSException) e).getUnderlyingException();
            }
            throw new SMimeException("Unable to verify body part", e);
        }
    }

    /**
     * Encrypts the encapsulated MIME body part.
     * 
     * @return an S/MIME message encapsulating the encrypted MIME body part. 
     * @throws SMimeException if unable to encrpyt the body part.
     */
    public SMimeMessage encrypt() throws SMimeException {
        return encrypt(cert);
    }

    /**
     * Encrypts the encapsulated MIME body part.
     * 
     * @param cert the certificate for encryption.
     * @return an S/MIME message encapsulating the encrypted MIME body part. 
     * @throws SMimeException if unable to encrpyt the body part.
     */
    public SMimeMessage encrypt(X509Certificate cert) throws SMimeException {
        try {
            try {
                if (cert == null) {
                    throw new SMimeException("No certificate for encryption");
                }

                setDefaults();

                /* Create the encrypter */
                SMIMEEnvelopedGenerator encrypter = new SMIMEEnvelopedGenerator();
                encrypter.setContentTransferEncoding(getContentTransferEncoding());
                encrypter.addKeyTransRecipient(cert);

                /* Encrypt the body part */
                MimeBodyPart encryptedPart = encrypter.generate(bodyPart, getEncryptAlgorithm(), SECURITY_PROVIDER);
                return new SMimeMessage(encryptedPart, this);
            } catch (org.bouncycastle.mail.smime.SMIMEException ex) {
                throw new SMimeException(ex.getMessage(), ex.getUnderlyingException());
            }
        } catch (Exception e) {
            throw new SMimeException("Unable to encrypt body part", e);
        }
    }

    /**
     * Decrypts the encapsulated MIME body part.
     * 
     * @return an S/MIME message encapsulating the decrypted MIME body part. 
     * @throws SMimeException if unable to decrpyt the body part.
     */
    public SMimeMessage decrypt() throws SMimeException {
        return decrypt(privateKey);
    }

    /**
     * Decrypts the encapsulated MIME body part.
     * 
     * @param privateKey the private key for decryption.
     * @return an S/MIME message encapsulating the decrypted MIME body part. 
     * @throws SMimeException if unable to decrpyt the body part.
     */
    public SMimeMessage decrypt(PrivateKey privateKey) throws SMimeException {
        if (privateKey == null) {
            throw new SMimeException("Private key not found");
        }

        try {
            setDefaults();

            SMIMEEnveloped m = new SMIMEEnveloped(bodyPart);
            RecipientId recId = new RecipientId();

            recId.setSerialNumber(cert.getSerialNumber());
            recId.setIssuer(cert.getIssuerX500Principal().getEncoded());

            RecipientInformationStore recipients = m.getRecipientInfos();
            RecipientInformation recipient = recipients.get(recId);

            if (recipient == null) {
                throw new SMimeException("Invalid encrypted content");
            }
            ByteArrayInputStream ins = new ByteArrayInputStream(recipient.getContent(privateKey, "BC"));
            MimeBodyPart decryptedPart = new MimeBodyPart(ins);
            return new SMimeMessage(decryptedPart, this);
        } catch (Exception e) {
            throw new SMimeException("Unable to decrypt body part", e);
        }
    }

    /**
     * Compresses the encapsulated MIME body part.
     * 
     * @return an S/MIME message encapsulating the compressed MIME body part. 
     * @throws SMimeException if unable to compress the body part.
     */
    public SMimeMessage compress() throws SMimeException {
        try {
            try {
                setDefaults();

                /* Create the generator for creating an smime/compressed body part */
                SMIMECompressedGenerator compressor = new SMIMECompressedGenerator();
                compressor.setContentTransferEncoding(getContentTransferEncoding());

                /* compress the body part */
                MimeBodyPart compressedPart = compressor.generate(bodyPart, SMIMECompressedGenerator.ZLIB);
                return new SMimeMessage(compressedPart, this);
            } catch (org.bouncycastle.mail.smime.SMIMEException ex) {
                throw new SMimeException(ex.getMessage(), ex.getUnderlyingException());
            }
        } catch (Exception e) {
            throw new SMimeException("Unable to compress body part", e);
        }
    }

    /**
     * Decompresses the encapsulated MIME body part.
     * 
     * @return an S/MIME message encapsulating the decompressed MIME body part. 
     * @throws SMimeException if unable to decompress the body part.
     */
    public SMimeMessage decompress() throws SMimeException {
        try {
            setDefaults();

            SMIMECompressed m = new SMIMECompressed(bodyPart);
            ByteArrayInputStream ins = new ByteArrayInputStream(m.getContent());

            MimeBodyPart decompressedPart = new MimeBodyPart(ins);
            return new SMimeMessage(decompressedPart, this);
        } catch (Exception e) {
            throw new SMimeException("Unable to decompress body part", e);
        }
    }

    /**
     * Digests the encapsulated MIME body part. 
     * 
     * @return the digested value in Base 64 format.
     * @throws SMimeException if unable to compute the digest value.
     */
    public String digest() throws SMimeException {
        return digest(getDigestAlgorithm(), true);
    }

    /**
     * Digests the encapsulated MIME body part. 
     * 
     * @param digestAlg digest algorithm.
     * @param isHeadersIncluded true if the digest should be computed on both 
     *        the headers and the content of the encapsulated body part.
     * @return the digested value in Base 64 format.
     * @throws SMimeException if unable to compute the digest value.
     */
    public String digest(String digestAlg, boolean isHeadersIncluded) throws SMimeException {
        try {
            if (digestAlg == null) {
                digestAlg = SMimeMessage.DIGEST_ALG_SHA1;
            }

            MessageDigest md = MessageDigest.getInstance(digestAlg, "BC");

            InputStream ins;

            if (isHeadersIncluded) {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                bodyPart.writeTo(baos);
                byte[] data = baos.toByteArray();
                ins = canonicalize(data);
            } else {
                ins = bodyPart.getInputStream();
            }

            DigestInputStream digIns = new DigestInputStream(ins, md);

            byte[] buf = new byte[1024];
            while (digIns.read(buf) >= 0) {
            }

            byte[] digest = digIns.getMessageDigest().digest();
            String digestString = new String(Base64.encode(digest));
            return digestString;
        } catch (Exception e) {
            throw new SMimeException("Unable to compute message digest", e);
        }
    }

    /**
     * Canonicalizes the given data by removing the starting new lines.
     * 
     * @param data the data to be canonicalized.
     * @return the canonicalized data as an input stream.
     */
    private static InputStream canonicalize(byte[] data) {
        if (data == null) {
            data = new byte[] {};
        }

        int pos = 0;
        for (int i = 0; i + 1 < data.length; i += 2) {
            if (data[i] == '\r' && data[i + 1] == '\n') {
                pos += 2;
            } else {
                break;
            }
        }

        return new ByteArrayInputStream(data, pos, data.length);
    }

    /**
     * Checks if the encapsulated MIME body part is encrypted.
     * 
     * @return true if the encapsulated MIME body part is encrypted.
     * @throws SMimeException if error occurred in checking.
     */
    public boolean isEncrypted() throws SMimeException {
        try {
            String contentType = bodyPart.getContentType();
            return (contentType != null && contentType.toLowerCase().indexOf("enveloped-data") != -1);
        } catch (Exception e) {
            throw new SMimeException("Unable to check if body part is encrypted.", e);
        }
    }

    /**
     * Checks if the encapsulated MIME body part is compressed.
     * 
     * @return true if the encapsulated MIME body part is compressed.
     * @throws SMimeException if error occurred in checking.
     */
    public boolean isCompressed() throws SMimeException {
        try {
            String contentType = bodyPart.getContentType();
            return (contentType != null && contentType.toLowerCase().indexOf("compressed-data") != -1);
        } catch (Exception e) {
            throw new SMimeException("Unable to check if body part is compressed.", e);
        }
    }

    /**
     * Checks if the encapsulated MIME body part is signed.
     * 
     * @return true if the encapsulated MIME body part is signed.
     * @throws SMimeException if error occurred in checking.
     */
    public boolean isSigned() throws SMimeException {
        try {
            return bodyPart.isMimeType("multipart/signed");
        } catch (Exception e) {
            throw new SMimeException("Unable to check if body part is signed.", e);
        }
    }

    /**
     * Gets the encapsulated MIME body part.
     * 
     * @return the encapsulated MIME body part.
     */
    public MimeBodyPart getBodyPart() {
        return bodyPart;
    }

    /**
     * Gets the digest algorithm which will be used in digital signing.
     * 
     * @return the digest algorithm.
     */
    public String getDigestAlgorithm() {
        if (digestAlgorithm == null) {
            if (privateKey == null) {
                return null;
            } else {
                return "DSA".equals(privateKey.getAlgorithm()) ? DIGEST_ALG_SHA1 : DIGEST_ALG_MD5;
            }
        } else {
            return digestAlgorithm;
        }
    }

    /**
     * Sets the digest algorithm to used in digital signing.
     * 
     * @param digestAlgorithm the digest algorithm.
     */
    public void setDigestAlgorithm(String digestAlgorithm) {
        this.digestAlgorithm = digestAlgorithm;
    }

    /**
     * Gets the encryption algorithm which will be used in encryption.
     * 
     * @return the encryption algorithm.
     */
    public String getEncryptAlgorithm() {
        return encryptAlgorithm == null ? ENCRYPT_ALG_DES_EDE3_CBC : encryptAlgorithm;
    }

    /**
     * Sets the encryption algorithm to be used in encryption.
     * 
     * @param encryptAlgorithm the encryption algorithm.
     */
    public void setEncryptAlgorithm(String encryptAlgorithm) {
        this.encryptAlgorithm = encryptAlgorithm;
    }

    /**
     * Gets the content transfer encoding which will be used in encryption,
     * digital signing, and compression. 
     * 
     * @return the content transfer encoding.
     */
    public String getContentTransferEncoding() {
        return contentTransferEncoding == null ? CONTENT_TRANSFER_ENC_BASE64 : contentTransferEncoding;
    }

    /**
     * Sets the content transfer encoding to used in encryption, digital 
     * signing, and compression. 
     * 
     * @param contentTransferEncoding the content transfer encoding.
     */
    public void setContentTransferEncoding(String contentTransferEncoding) {
        this.contentTransferEncoding = contentTransferEncoding;
    }

    /**
     * Sets the default mail caps.
     */
    private void setDefaults() {
        MailcapCommandMap mailcap = (MailcapCommandMap) CommandMap.getDefaultCommandMap();

        for (int i = 0; i < mailcaps.length; i++) {
            CommandInfo command = mailcap.getCommand(mailcaps[i].getMimeType(), mailcaps[i].getCommandName());
            if (command == null || !command.getCommandClass().equals(mailcaps[i].getClassName())) {
                mailcap.addMailcap(mailcaps[i].toString());
            }
        }

        CommandMap.setDefaultCommandMap(mailcap);
    }
}