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