diff options
Diffstat (limited to 'mail/src/main/java/org/spongycastle/mail/smime')
43 files changed, 7179 insertions, 0 deletions
diff --git a/mail/src/main/java/org/spongycastle/mail/smime/CMSProcessableBodyPart.java b/mail/src/main/java/org/spongycastle/mail/smime/CMSProcessableBodyPart.java new file mode 100644 index 00000000..040def41 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/CMSProcessableBodyPart.java @@ -0,0 +1,44 @@ +package org.bouncycastle.mail.smime; + +import java.io.IOException; +import java.io.OutputStream; + +import javax.mail.BodyPart; +import javax.mail.MessagingException; + +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessable; + +/** + * a holding class for a BodyPart to be processed. + */ +public class CMSProcessableBodyPart + implements CMSProcessable +{ + private BodyPart bodyPart; + + public CMSProcessableBodyPart( + BodyPart bodyPart) + { + this.bodyPart = bodyPart; + } + + public void write( + OutputStream out) + throws IOException, CMSException + { + try + { + bodyPart.writeTo(out); + } + catch (MessagingException e) + { + throw new CMSException("can't write BodyPart to stream.", e); + } + } + + public Object getContent() + { + return bodyPart; + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/CMSProcessableBodyPartInbound.java b/mail/src/main/java/org/spongycastle/mail/smime/CMSProcessableBodyPartInbound.java new file mode 100644 index 00000000..1497590d --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/CMSProcessableBodyPartInbound.java @@ -0,0 +1,66 @@ +package org.bouncycastle.mail.smime; + +import java.io.IOException; +import java.io.OutputStream; + +import javax.mail.BodyPart; +import javax.mail.MessagingException; + +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessable; + +/** + * a holding class for a BodyPart to be processed which does CRLF canonicalisation if + * dealing with non-binary data. + */ +public class CMSProcessableBodyPartInbound + implements CMSProcessable +{ + private final BodyPart bodyPart; + private final String defaultContentTransferEncoding; + + /** + * Create a processable with the default transfer encoding of 7bit + * + * @param bodyPart body part to be processed + */ + public CMSProcessableBodyPartInbound( + BodyPart bodyPart) + { + this(bodyPart, "7bit"); + } + + /** + * Create a processable with the a default transfer encoding of + * the passed in value. + * + * @param bodyPart body part to be processed + * @param defaultContentTransferEncoding the new default to use. + */ + public CMSProcessableBodyPartInbound( + BodyPart bodyPart, + String defaultContentTransferEncoding) + { + this.bodyPart = bodyPart; + this.defaultContentTransferEncoding = defaultContentTransferEncoding; + } + + public void write( + OutputStream out) + throws IOException, CMSException + { + try + { + SMIMEUtil.outputBodyPart(out, bodyPart, defaultContentTransferEncoding); + } + catch (MessagingException e) + { + throw new CMSException("can't write BodyPart to stream: " + e, e); + } + } + + public Object getContent() + { + return bodyPart; + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/CMSProcessableBodyPartOutbound.java b/mail/src/main/java/org/spongycastle/mail/smime/CMSProcessableBodyPartOutbound.java new file mode 100644 index 00000000..4c4b3b11 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/CMSProcessableBodyPartOutbound.java @@ -0,0 +1,73 @@ +package org.bouncycastle.mail.smime; + +import java.io.IOException; +import java.io.OutputStream; + +import javax.mail.BodyPart; +import javax.mail.MessagingException; +import javax.mail.internet.MimeBodyPart; + +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessable; +import org.bouncycastle.mail.smime.util.CRLFOutputStream; + +/** + * a holding class for a BodyPart to be processed which does CRLF canocicalisation if + * dealing with non-binary data. + */ +public class CMSProcessableBodyPartOutbound + implements CMSProcessable +{ + private BodyPart bodyPart; + private String defaultContentTransferEncoding; + + /** + * Create a processable with the default transfer encoding of 7bit + * + * @param bodyPart body part to be processed + */ + public CMSProcessableBodyPartOutbound( + BodyPart bodyPart) + { + this.bodyPart = bodyPart; + } + + /** + * Create a processable with the a default transfer encoding of + * the passed in value. + * + * @param bodyPart body part to be processed + * @param defaultContentTransferEncoding the new default to use. + */ + public CMSProcessableBodyPartOutbound( + BodyPart bodyPart, + String defaultContentTransferEncoding) + { + this.bodyPart = bodyPart; + this.defaultContentTransferEncoding = defaultContentTransferEncoding; + } + + public void write( + OutputStream out) + throws IOException, CMSException + { + try + { + if (SMIMEUtil.isCanonicalisationRequired((MimeBodyPart)bodyPart, defaultContentTransferEncoding)) + { + out = new CRLFOutputStream(out); + } + + bodyPart.writeTo(out); + } + catch (MessagingException e) + { + throw new CMSException("can't write BodyPart to stream.", e); + } + } + + public Object getContent() + { + return bodyPart; + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/SMIMECompressed.java b/mail/src/main/java/org/spongycastle/mail/smime/SMIMECompressed.java new file mode 100644 index 00000000..2fca9366 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/SMIMECompressed.java @@ -0,0 +1,59 @@ +package org.bouncycastle.mail.smime; + +import java.io.IOException; +import java.io.InputStream; + +import javax.mail.MessagingException; +import javax.mail.Part; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimePart; + +import org.bouncycastle.cms.CMSCompressedData; +import org.bouncycastle.cms.CMSException; + +/** + * containing class for an S/MIME pkcs7-mime MimePart. + */ +public class SMIMECompressed + extends CMSCompressedData +{ + MimePart message; + + private static InputStream getInputStream( + Part bodyPart) + throws MessagingException + { + try + { + return bodyPart.getInputStream(); + } + catch (IOException e) + { + throw new MessagingException("can't extract input stream: " + e); + } + } + + public SMIMECompressed( + MimeBodyPart message) + throws MessagingException, CMSException + { + super(getInputStream(message)); + + this.message = message; + } + + public SMIMECompressed( + MimeMessage message) + throws MessagingException, CMSException + { + super(getInputStream(message)); + + this.message = message; + } + + public MimePart getCompressedContent() + { + return message; + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/SMIMECompressedGenerator.java b/mail/src/main/java/org/spongycastle/mail/smime/SMIMECompressedGenerator.java new file mode 100644 index 00000000..2701e9d0 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/SMIMECompressedGenerator.java @@ -0,0 +1,152 @@ +package org.bouncycastle.mail.smime; + +import java.io.IOException; +import java.io.OutputStream; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import javax.activation.CommandMap; +import javax.activation.MailcapCommandMap; +import javax.mail.MessagingException; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; + +import org.bouncycastle.cms.CMSCompressedDataGenerator; +import org.bouncycastle.cms.CMSCompressedDataStreamGenerator; +import org.bouncycastle.operator.OutputCompressor; + +/** + * General class for generating a pkcs7-mime compressed message. + * + * A simple example of usage. + * + * <pre> + * SMIMECompressedGenerator fact = new SMIMECompressedGenerator(); + * + * MimeBodyPart smime = fact.generate(content, algorithm); + * </pre> + * + * <b>Note:<b> Most clients expect the MimeBodyPart to be in a MimeMultipart + * when it's sent. + */ +public class SMIMECompressedGenerator + extends SMIMEGenerator +{ + public static final String ZLIB = CMSCompressedDataGenerator.ZLIB; + + private static final String COMPRESSED_CONTENT_TYPE = "application/pkcs7-mime; name=\"smime.p7z\"; smime-type=compressed-data"; + + static + { + final MailcapCommandMap mc = (MailcapCommandMap)CommandMap.getDefaultCommandMap(); + + mc.addMailcap("application/pkcs7-mime;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_mime"); + mc.addMailcap("application/x-pkcs7-mime;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_mime"); + + AccessController.doPrivileged(new PrivilegedAction() + { + public Object run() + { + CommandMap.setDefaultCommandMap(mc); + + return null; + } + }); + } + + /** + * generate an compressed object that contains an SMIME Compressed + * object using the given compression algorithm. + */ + private MimeBodyPart make( + MimeBodyPart content, + OutputCompressor compressor) + throws SMIMEException + { + try + { + MimeBodyPart data = new MimeBodyPart(); + + data.setContent(new ContentCompressor(content, compressor), COMPRESSED_CONTENT_TYPE); + data.addHeader("Content-Type", COMPRESSED_CONTENT_TYPE); + data.addHeader("Content-Disposition", "attachment; filename=\"smime.p7z\""); + data.addHeader("Content-Description", "S/MIME Compressed Message"); + data.addHeader("Content-Transfer-Encoding", encoding); + + return data; + } + catch (MessagingException e) + { + throw new SMIMEException("exception putting multi-part together.", e); + } + } + + /** + * generate an compressed object that contains an SMIME Compressed + * object using the given provider from the contents of the passed in + * message + */ + public MimeBodyPart generate( + MimeBodyPart content, + OutputCompressor compressor) + throws SMIMEException + { + return make(makeContentBodyPart(content), compressor); + } + + /** + * generate an compressed object that contains an SMIME Compressed + * object using the given provider from the contents of the passed in + * message + */ + public MimeBodyPart generate( + MimeMessage message, + OutputCompressor compressor) + throws SMIMEException + { + try + { + message.saveChanges(); // make sure we're up to date. + } + catch (MessagingException e) + { + throw new SMIMEException("unable to save message", e); + } + + return make(makeContentBodyPart(message), compressor); + } + + private class ContentCompressor + implements SMIMEStreamingProcessor + { + private final MimeBodyPart content; + private final OutputCompressor compressor; + + ContentCompressor( + MimeBodyPart content, + OutputCompressor compressor) + { + this.content = content; + this.compressor = compressor; + } + + public void write(OutputStream out) + throws IOException + { + CMSCompressedDataStreamGenerator cGen = new CMSCompressedDataStreamGenerator(); + + OutputStream compressed = cGen.open(out, compressor); + + try + { + content.writeTo(compressed); + + compressed.close(); + } + catch (MessagingException e) + { + throw new IOException(e.toString()); + } + } + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/SMIMECompressedParser.java b/mail/src/main/java/org/spongycastle/mail/smime/SMIMECompressedParser.java new file mode 100644 index 00000000..23214a4d --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/SMIMECompressedParser.java @@ -0,0 +1,100 @@ +package org.bouncycastle.mail.smime; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.mail.MessagingException; +import javax.mail.Part; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimePart; + +import org.bouncycastle.cms.CMSCompressedDataParser; +import org.bouncycastle.cms.CMSException; + +/** + * Stream based containing class for an S/MIME pkcs7-mime compressed MimePart. + */ +public class SMIMECompressedParser + extends CMSCompressedDataParser +{ + private final MimePart message; + + private static InputStream getInputStream( + Part bodyPart, + int bufferSize) + throws MessagingException + { + try + { + InputStream in = bodyPart.getInputStream(); + + if (bufferSize == 0) + { + return new BufferedInputStream(in); + } + else + { + return new BufferedInputStream(in, bufferSize); + } + } + catch (IOException e) + { + throw new MessagingException("can't extract input stream: " + e); + } + } + + public SMIMECompressedParser( + MimeBodyPart message) + throws MessagingException, CMSException + { + this(message, 0); + } + + public SMIMECompressedParser( + MimeMessage message) + throws MessagingException, CMSException + { + this(message, 0); + } + + /** + * Create a parser from a MimeBodyPart using the passed in buffer size + * for reading it. + * + * @param message body part to be parsed. + * @param bufferSize bufferSoze to be used. + */ + public SMIMECompressedParser( + MimeBodyPart message, + int bufferSize) + throws MessagingException, CMSException + { + super(getInputStream(message, bufferSize)); + + this.message = message; + } + + /** + * Create a parser from a MimeMessage using the passed in buffer size + * for reading it. + * + * @param message message to be parsed. + * @param bufferSize bufferSoze to be used. + */ + public SMIMECompressedParser( + MimeMessage message, + int bufferSize) + throws MessagingException, CMSException + { + super(getInputStream(message, bufferSize)); + + this.message = message; + } + + public MimePart getCompressedContent() + { + return message; + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/SMIMEEnveloped.java b/mail/src/main/java/org/spongycastle/mail/smime/SMIMEEnveloped.java new file mode 100644 index 00000000..bf7a7ff4 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/SMIMEEnveloped.java @@ -0,0 +1,59 @@ +package org.bouncycastle.mail.smime; + +import java.io.IOException; +import java.io.InputStream; + +import javax.mail.MessagingException; +import javax.mail.Part; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimePart; + +import org.bouncycastle.cms.CMSEnvelopedData; +import org.bouncycastle.cms.CMSException; + +/** + * containing class for an S/MIME pkcs7-mime encrypted MimePart. + */ +public class SMIMEEnveloped + extends CMSEnvelopedData +{ + MimePart message; + + private static InputStream getInputStream( + Part bodyPart) + throws MessagingException + { + try + { + return bodyPart.getInputStream(); + } + catch (IOException e) + { + throw new MessagingException("can't extract input stream: " + e); + } + } + + public SMIMEEnveloped( + MimeBodyPart message) + throws MessagingException, CMSException + { + super(getInputStream(message)); + + this.message = message; + } + + public SMIMEEnveloped( + MimeMessage message) + throws MessagingException, CMSException + { + super(getInputStream(message)); + + this.message = message; + } + + public MimePart getEncryptedContent() + { + return message; + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/SMIMEEnvelopedGenerator.java b/mail/src/main/java/org/spongycastle/mail/smime/SMIMEEnvelopedGenerator.java new file mode 100644 index 00000000..dd547d35 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/SMIMEEnvelopedGenerator.java @@ -0,0 +1,282 @@ +package org.bouncycastle.mail.smime; + +import java.io.IOException; +import java.io.OutputStream; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.List; + +import javax.activation.CommandMap; +import javax.activation.MailcapCommandMap; +import javax.mail.MessagingException; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; + +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.cms.CMSEnvelopedDataGenerator; +import org.bouncycastle.cms.CMSEnvelopedDataStreamGenerator; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.RecipientInfoGenerator; +import org.bouncycastle.operator.OutputEncryptor; + +/** + * General class for generating a pkcs7-mime message. + * + * A simple example of usage. + * + * <pre> + * SMIMEEnvelopedGenerator fact = new SMIMEEnvelopedGenerator(); + * + * fact.addRecipientInfoGenerator(new JceKeyTransRecipientInfoGenerator(recipientCert).setProvider("BC")); + * + * MimeBodyPart mp = fact.generate(content, new JceCMSContentEncryptorBuilder(CMSAlgorithm.RC2_CBC, 40).setProvider("BC").build()); + * </pre> + * + * <b>Note:<b> Most clients expect the MimeBodyPart to be in a MimeMultipart + * when it's sent. + */ +public class SMIMEEnvelopedGenerator + extends SMIMEGenerator +{ + public static final String DES_EDE3_CBC = CMSEnvelopedDataGenerator.DES_EDE3_CBC; + public static final String RC2_CBC = CMSEnvelopedDataGenerator.RC2_CBC; + public static final String IDEA_CBC = CMSEnvelopedDataGenerator.IDEA_CBC; + public static final String CAST5_CBC = CMSEnvelopedDataGenerator.CAST5_CBC; + + public static final String AES128_CBC = CMSEnvelopedDataGenerator.AES128_CBC; + public static final String AES192_CBC = CMSEnvelopedDataGenerator.AES192_CBC; + public static final String AES256_CBC = CMSEnvelopedDataGenerator.AES256_CBC; + + public static final String CAMELLIA128_CBC = CMSEnvelopedDataGenerator.CAMELLIA128_CBC; + public static final String CAMELLIA192_CBC = CMSEnvelopedDataGenerator.CAMELLIA192_CBC; + public static final String CAMELLIA256_CBC = CMSEnvelopedDataGenerator.CAMELLIA256_CBC; + + public static final String SEED_CBC = CMSEnvelopedDataGenerator.SEED_CBC; + + public static final String DES_EDE3_WRAP = CMSEnvelopedDataGenerator.DES_EDE3_WRAP; + public static final String AES128_WRAP = CMSEnvelopedDataGenerator.AES128_WRAP; + public static final String AES256_WRAP = CMSEnvelopedDataGenerator.AES256_WRAP; + public static final String CAMELLIA128_WRAP = CMSEnvelopedDataGenerator.CAMELLIA128_WRAP; + public static final String CAMELLIA192_WRAP = CMSEnvelopedDataGenerator.CAMELLIA192_WRAP; + public static final String CAMELLIA256_WRAP = CMSEnvelopedDataGenerator.CAMELLIA256_WRAP; + public static final String SEED_WRAP = CMSEnvelopedDataGenerator.SEED_WRAP; + + public static final String ECDH_SHA1KDF = CMSEnvelopedDataGenerator.ECDH_SHA1KDF; + + private static final String ENCRYPTED_CONTENT_TYPE = "application/pkcs7-mime; name=\"smime.p7m\"; smime-type=enveloped-data"; + + private EnvelopedGenerator fact; + private List recipients = new ArrayList(); + + static + { + AccessController.doPrivileged(new PrivilegedAction() + { + public Object run() + { + CommandMap.setDefaultCommandMap(addCommands(CommandMap.getDefaultCommandMap())); + + return null; + } + }); + } + + private static MailcapCommandMap addCommands(CommandMap cm) + { + MailcapCommandMap mc = (MailcapCommandMap)cm; + + mc.addMailcap("application/pkcs7-signature;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_signature"); + mc.addMailcap("application/pkcs7-mime;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_mime"); + mc.addMailcap("application/x-pkcs7-signature;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_signature"); + mc.addMailcap("application/x-pkcs7-mime;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_mime"); + mc.addMailcap("multipart/signed;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.multipart_signed"); + + return mc; + } + + /** + * base constructor + */ + public SMIMEEnvelopedGenerator() + { + fact = new EnvelopedGenerator(); + } + + /** + * add a recipientInfoGenerator. + */ + public void addRecipientInfoGenerator( + RecipientInfoGenerator recipientInfoGen) + throws IllegalArgumentException + { + fact.addRecipientInfoGenerator(recipientInfoGen); + } + + /** + * Use a BER Set to store the recipient information + */ + public void setBerEncodeRecipients( + boolean berEncodeRecipientSet) + { + fact.setBEREncodeRecipients(berEncodeRecipientSet); + } + + /** + * if we get here we expect the Mime body part to be well defined. + */ + private MimeBodyPart make( + MimeBodyPart content, + OutputEncryptor encryptor) + throws SMIMEException + { + try + { + MimeBodyPart data = new MimeBodyPart(); + + data.setContent(new ContentEncryptor(content, encryptor), ENCRYPTED_CONTENT_TYPE); + data.addHeader("Content-Type", ENCRYPTED_CONTENT_TYPE); + data.addHeader("Content-Disposition", "attachment; filename=\"smime.p7m\""); + data.addHeader("Content-Description", "S/MIME Encrypted Message"); + data.addHeader("Content-Transfer-Encoding", encoding); + + return data; + } + catch (MessagingException e) + { + throw new SMIMEException("exception putting multi-part together.", e); + } + } + + /** + * generate an enveloped object that contains an SMIME Enveloped + * object using the given content encryptor + */ + public MimeBodyPart generate( + MimeBodyPart content, + OutputEncryptor encryptor) + throws SMIMEException + { + return make(makeContentBodyPart(content), encryptor); + } + + /** + * generate an enveloped object that contains an SMIME Enveloped + * object using the given provider from the contents of the passed in + * message + */ + public MimeBodyPart generate( + MimeMessage message, + OutputEncryptor encryptor) + throws SMIMEException + { + try + { + message.saveChanges(); // make sure we're up to date. + } + catch (MessagingException e) + { + throw new SMIMEException("unable to save message", e); + } + + return make(makeContentBodyPart(message), encryptor); + } + + private class ContentEncryptor + implements SMIMEStreamingProcessor + { + private final MimeBodyPart _content; + private OutputEncryptor _encryptor; + + private boolean _firstTime = true; + + ContentEncryptor( + MimeBodyPart content, + OutputEncryptor encryptor) + { + _content = content; + _encryptor = encryptor; + } + + public void write(OutputStream out) + throws IOException + { + OutputStream encrypted; + + try + { + if (_firstTime) + { + encrypted = fact.open(out, _encryptor); + + _firstTime = false; + } + else + { + encrypted = fact.regenerate(out, _encryptor); + } + + _content.getDataHandler().setCommandMap(addCommands(CommandMap.getDefaultCommandMap())); + + _content.writeTo(encrypted); + + encrypted.close(); + } + catch (MessagingException e) + { + throw new WrappingIOException(e.toString(), e); + } + catch (CMSException e) + { + throw new WrappingIOException(e.toString(), e); + } + } + } + + private class EnvelopedGenerator + extends CMSEnvelopedDataStreamGenerator + { + private ASN1ObjectIdentifier dataType; + private ASN1EncodableVector recipientInfos; + + protected OutputStream open( + ASN1ObjectIdentifier dataType, + OutputStream out, + ASN1EncodableVector recipientInfos, + OutputEncryptor encryptor) + throws IOException + { + this.dataType = dataType; + this.recipientInfos = recipientInfos; + + return super.open(dataType, out, recipientInfos, encryptor); + } + + OutputStream regenerate( + OutputStream out, + OutputEncryptor encryptor) + throws IOException + { + return super.open(dataType, out, recipientInfos, encryptor); + } + } + + private static class WrappingIOException + extends IOException + { + private Throwable cause; + + WrappingIOException(String msg, Throwable cause) + { + super(msg); + + this.cause = cause; + } + + public Throwable getCause() + { + return cause; + } + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/SMIMEEnvelopedParser.java b/mail/src/main/java/org/spongycastle/mail/smime/SMIMEEnvelopedParser.java new file mode 100644 index 00000000..95849472 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/SMIMEEnvelopedParser.java @@ -0,0 +1,100 @@ +package org.bouncycastle.mail.smime; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.mail.MessagingException; +import javax.mail.Part; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimePart; + +import org.bouncycastle.cms.CMSEnvelopedDataParser; +import org.bouncycastle.cms.CMSException; + +/** + * Stream based containing class for an S/MIME pkcs7-mime encrypted MimePart. + */ +public class SMIMEEnvelopedParser + extends CMSEnvelopedDataParser +{ + private final MimePart message; + + private static InputStream getInputStream( + Part bodyPart, + int bufferSize) + throws MessagingException + { + try + { + InputStream in = bodyPart.getInputStream(); + + if (bufferSize == 0) + { + return new BufferedInputStream(in); + } + else + { + return new BufferedInputStream(in, bufferSize); + } + } + catch (IOException e) + { + throw new MessagingException("can't extract input stream: " + e); + } + } + + public SMIMEEnvelopedParser( + MimeBodyPart message) + throws IOException, MessagingException, CMSException + { + this(message, 0); + } + + public SMIMEEnvelopedParser( + MimeMessage message) + throws IOException, MessagingException, CMSException + { + this(message, 0); + } + + /** + * Create a parser from a MimeBodyPart using the passed in buffer size + * for reading it. + * + * @param message body part to be parsed. + * @param bufferSize bufferSoze to be used. + */ + public SMIMEEnvelopedParser( + MimeBodyPart message, + int bufferSize) + throws IOException, MessagingException, CMSException + { + super(getInputStream(message, bufferSize)); + + this.message = message; + } + + /** + * Create a parser from a MimeMessage using the passed in buffer size + * for reading it. + * + * @param message message to be parsed. + * @param bufferSize bufferSoze to be used. + */ + public SMIMEEnvelopedParser( + MimeMessage message, + int bufferSize) + throws IOException, MessagingException, CMSException + { + super(getInputStream(message, bufferSize)); + + this.message = message; + } + + public MimePart getEncryptedContent() + { + return message; + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/SMIMEException.java b/mail/src/main/java/org/spongycastle/mail/smime/SMIMEException.java new file mode 100644 index 00000000..fe7499cf --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/SMIMEException.java @@ -0,0 +1,32 @@ +package org.bouncycastle.mail.smime; + +public class SMIMEException + extends Exception +{ + Exception e; + + public SMIMEException( + String name) + { + super(name); + } + + public SMIMEException( + String name, + Exception e) + { + super(name); + + this.e = e; + } + + public Exception getUnderlyingException() + { + return e; + } + + public Throwable getCause() + { + return e; + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/SMIMEGenerator.java b/mail/src/main/java/org/spongycastle/mail/smime/SMIMEGenerator.java new file mode 100644 index 00000000..168cc4a4 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/SMIMEGenerator.java @@ -0,0 +1,223 @@ +package org.bouncycastle.mail.smime; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.KeyGenerator; +import javax.mail.Header; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.Session; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; + +import org.bouncycastle.cms.CMSEnvelopedGenerator; +import org.bouncycastle.util.Strings; + +/** + * super class of the various generators. + */ +public class SMIMEGenerator +{ + private static Map BASE_CIPHER_NAMES = new HashMap(); + + static + { + BASE_CIPHER_NAMES.put(CMSEnvelopedGenerator.DES_EDE3_CBC, "DESEDE"); + BASE_CIPHER_NAMES.put(CMSEnvelopedGenerator.AES128_CBC, "AES"); + BASE_CIPHER_NAMES.put(CMSEnvelopedGenerator.AES192_CBC, "AES"); + BASE_CIPHER_NAMES.put(CMSEnvelopedGenerator.AES256_CBC, "AES"); + } + + protected boolean useBase64 = true; + protected String encoding = "base64"; // default sets base64 + + /** + * base constructor + */ + protected SMIMEGenerator() + { + } + + /** + * set the content-transfer-encoding for the CMS block (enveloped data, signature, etc...) in the message. + * + * @param encoding the encoding to use, default "base64", use "binary" for a binary encoding. + */ + public void setContentTransferEncoding( + String encoding) + { + this.encoding = encoding; + this.useBase64 = Strings.toLowerCase(encoding).equals("base64"); + } + + /** + * Make sure we have a valid content body part - setting the headers + * with defaults if neccessary. + */ + protected MimeBodyPart makeContentBodyPart( + MimeBodyPart content) + throws SMIMEException + { + // + // add the headers to the body part - if they are missing, in + // the event they have already been set the content settings override + // any defaults that might be set. + // + try + { + MimeMessage msg = new MimeMessage((Session)null); + + Enumeration e = content.getAllHeaders(); + + msg.setDataHandler(content.getDataHandler()); + + while (e.hasMoreElements()) + { + Header hdr =(Header)e.nextElement(); + + msg.setHeader(hdr.getName(), hdr.getValue()); + } + + msg.saveChanges(); + + // + // we do this to make sure at least the default headers are + // set in the body part. + // + e = msg.getAllHeaders(); + + while (e.hasMoreElements()) + { + Header hdr =(Header)e.nextElement(); + + if (Strings.toLowerCase(hdr.getName()).startsWith("content-")) + { + content.setHeader(hdr.getName(), hdr.getValue()); + } + } + } + catch (MessagingException e) + { + throw new SMIMEException("exception saving message state.", e); + } + + return content; + } + + /** + * extract an appropriate body part from the passed in MimeMessage + */ + protected MimeBodyPart makeContentBodyPart( + MimeMessage message) + throws SMIMEException + { + MimeBodyPart content = new MimeBodyPart(); + + // + // add the headers to the body part. + // + try + { + message.removeHeader("Message-Id"); + message.removeHeader("Mime-Version"); + + // JavaMail has a habit of reparsing some content types, if the bodypart is + // a multipart it might be signed, we rebuild the body part using the raw input stream for the message. + try + { + if (message.getContent() instanceof Multipart) + { + content.setContent(message.getRawInputStream(), message.getContentType()); + + extractHeaders(content, message); + + return content; + } + } + catch (MessagingException e) + { + // fall back to usual method below + } + + content.setContent(message.getContent(), message.getContentType()); + + content.setDataHandler(message.getDataHandler()); + + extractHeaders(content, message); + } + catch (MessagingException e) + { + throw new SMIMEException("exception saving message state.", e); + } + catch (IOException e) + { + throw new SMIMEException("exception getting message content.", e); + } + + return content; + } + + private void extractHeaders(MimeBodyPart content, MimeMessage message) + throws MessagingException + { + Enumeration e = message.getAllHeaders(); + + while (e.hasMoreElements()) + { + Header hdr =(Header)e.nextElement(); + + content.addHeader(hdr.getName(), hdr.getValue()); + } + } + + protected KeyGenerator createSymmetricKeyGenerator( + String encryptionOID, + Provider provider) + throws NoSuchAlgorithmException + { + try + { + return createKeyGenerator(encryptionOID, provider); + } + catch (NoSuchAlgorithmException e) + { + try + { + String algName = (String)BASE_CIPHER_NAMES.get(encryptionOID); + if (algName != null) + { + return createKeyGenerator(algName, provider); + } + } + catch (NoSuchAlgorithmException ex) + { + // ignore + } + if (provider != null) + { + return createSymmetricKeyGenerator(encryptionOID, null); + } + throw e; + } + } + + private KeyGenerator createKeyGenerator( + String algName, + Provider provider) + throws NoSuchAlgorithmException + { + if (provider != null) + { + return KeyGenerator.getInstance(algName, provider); + } + else + { + return KeyGenerator.getInstance(algName); + } + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/SMIMESigned.java b/mail/src/main/java/org/spongycastle/mail/smime/SMIMESigned.java new file mode 100644 index 00000000..9f279398 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/SMIMESigned.java @@ -0,0 +1,241 @@ +package org.bouncycastle.mail.smime; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import javax.activation.CommandMap; +import javax.activation.MailcapCommandMap; +import javax.mail.MessagingException; +import javax.mail.Part; +import javax.mail.Session; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; +import javax.mail.internet.MimePart; + +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessable; +import org.bouncycastle.cms.CMSSignedData; + +/** + * general class for handling a pkcs7-signature message. + * <p> + * A simple example of usage - note, in the example below the validity of + * the certificate isn't verified, just the fact that one of the certs + * matches the given signer... + * <p> + * <pre> + * CertStore certs = s.getCertificates("Collection", "BC"); + * SignerInformationStore signers = s.getSignerInfos(); + * Collection c = signers.getSigners(); + * Iterator it = c.iterator(); + * + * while (it.hasNext()) + * { + * SignerInformation signer = (SignerInformation)it.next(); + * Collection certCollection = certs.getCertificates(signer.getSID()); + * + * Iterator certIt = certCollection.iterator(); + * X509Certificate cert = (X509Certificate)certIt.next(); + * + * if (signer.verify(cert.getPublicKey())) + * { + * verified++; + * } + * } + * </pre> + * <p> + * Note: if you are using this class with AS2 or some other protocol + * that does not use 7bit as the default content transfer encoding you + * will need to use the constructor that allows you to specify the default + * content transfer encoding, such as "binary". + * </p> + */ +public class SMIMESigned + extends CMSSignedData +{ + Object message; + MimeBodyPart content; + + private static InputStream getInputStream( + Part bodyPart) + throws MessagingException + { + try + { + if (bodyPart.isMimeType("multipart/signed")) + { + throw new MessagingException("attempt to create signed data object from multipart content - use MimeMultipart constructor."); + } + + return bodyPart.getInputStream(); + } + catch (IOException e) + { + throw new MessagingException("can't extract input stream: " + e); + } + } + + static + { + final MailcapCommandMap mc = (MailcapCommandMap)CommandMap.getDefaultCommandMap(); + + mc.addMailcap("application/pkcs7-signature;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_signature"); + mc.addMailcap("application/pkcs7-mime;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_mime"); + mc.addMailcap("application/x-pkcs7-signature;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_signature"); + mc.addMailcap("application/x-pkcs7-mime;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_mime"); + mc.addMailcap("multipart/signed;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.multipart_signed"); + + AccessController.doPrivileged(new PrivilegedAction() + { + public Object run() + { + CommandMap.setDefaultCommandMap(mc); + + return null; + } + }); + } + + /** + * base constructor using a defaultContentTransferEncoding of 7bit + * + * @exception MessagingException on an error extracting the signature or + * otherwise processing the message. + * @exception CMSException if some other problem occurs. + */ + public SMIMESigned( + MimeMultipart message) + throws MessagingException, CMSException + { + super(new CMSProcessableBodyPartInbound(message.getBodyPart(0)), getInputStream(message.getBodyPart(1))); + + this.message = message; + this.content = (MimeBodyPart)message.getBodyPart(0); + } + + /** + * base constructor with settable contentTransferEncoding + * + * @param message the signed message + * @param defaultContentTransferEncoding new default to use + * @exception MessagingException on an error extracting the signature or + * otherwise processing the message. + * @exception CMSException if some other problem occurs. + */ + public SMIMESigned( + MimeMultipart message, + String defaultContentTransferEncoding) + throws MessagingException, CMSException + { + super(new CMSProcessableBodyPartInbound(message.getBodyPart(0), defaultContentTransferEncoding), getInputStream(message.getBodyPart(1))); + + this.message = message; + this.content = (MimeBodyPart)message.getBodyPart(0); + } + + /** + * base constructor for a signed message with encapsulated content. + * + * @exception MessagingException on an error extracting the signature or + * otherwise processing the message. + * @exception SMIMEException if the body part encapsulated in the message cannot be extracted. + * @exception CMSException if some other problem occurs. + */ + public SMIMESigned( + Part message) + throws MessagingException, CMSException, SMIMEException + { + super(getInputStream(message)); + + this.message = message; + + CMSProcessable cont = this.getSignedContent(); + + if (cont != null) + { + byte[] contBytes = (byte[])cont.getContent(); + + this.content = SMIMEUtil.toMimeBodyPart(contBytes); + } + } + + /** + * return the content that was signed. + */ + public MimeBodyPart getContent() + { + return content; + } + + /** + * Return the content that was signed as a mime message. + * + * @param session + * @return a MimeMessage holding the content. + * @throws MessagingException + */ + public MimeMessage getContentAsMimeMessage(Session session) + throws MessagingException, IOException + { + Object content = getSignedContent().getContent(); + byte[] contentBytes = null; + + if (content instanceof byte[]) + { + contentBytes = (byte[])content; + } + else if (content instanceof MimePart) + { + MimePart part = (MimePart)content; + ByteArrayOutputStream out; + + if (part.getSize() > 0) + { + out = new ByteArrayOutputStream(part.getSize()); + } + else + { + out = new ByteArrayOutputStream(); + } + + part.writeTo(out); + contentBytes = out.toByteArray(); + } + else + { + String type = "<null>"; + if (content != null) + { + type = content.getClass().getName(); + } + + throw new MessagingException( + "Could not transfrom content of type " + + type + + " into MimeMessage."); + } + + if (contentBytes != null) + { + ByteArrayInputStream in = new ByteArrayInputStream(contentBytes); + + return new MimeMessage(session, in); + } + + return null; + } + + /** + * return the content that was signed - depending on whether this was + * unencapsulated or not it will return a MimeMultipart or a MimeBodyPart + */ + public Object getContentWithSignature() + { + return message; + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/SMIMESignedGenerator.java b/mail/src/main/java/org/spongycastle/mail/smime/SMIMESignedGenerator.java new file mode 100644 index 00000000..e4c96e86 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/SMIMESignedGenerator.java @@ -0,0 +1,616 @@ +package org.bouncycastle.mail.smime; + +import java.io.IOException; +import java.io.OutputStream; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import javax.activation.CommandMap; +import javax.activation.MailcapCommandMap; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.internet.ContentType; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.cryptopro.CryptoProObjectIdentifiers; +import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; +import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.teletrust.TeleTrusTObjectIdentifiers; +import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; +import org.bouncycastle.cms.CMSAlgorithm; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSSignedDataStreamGenerator; +import org.bouncycastle.cms.SignerInfoGenerator; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.SignerInformationStore; +import org.bouncycastle.mail.smime.util.CRLFOutputStream; +import org.bouncycastle.util.Store; + +/** + * general class for generating a pkcs7-signature message. + * <p> + * A simple example of usage. + * + * <pre> + * X509Certificate signCert = ... + * KeyPair signKP = ... + * + * List certList = new ArrayList(); + * + * certList.add(signCert); + * + * Store certs = new JcaCertStore(certList); + * + * SMIMESignedGenerator gen = new SMIMESignedGenerator(); + * + * gen.addSignerInfoGenerator(new JcaSimpleSignerInfoGeneratorBuilder().setProvider("BC").build("SHA1withRSA", signKP.getPrivate(), signCert)); + * + * gen.addCertificates(certs); + * + * MimeMultipart smime = fact.generate(content); + * </pre> + * <p> + * Note 1: if you are using this class with AS2 or some other protocol + * that does not use "7bit" as the default content transfer encoding you + * will need to use the constructor that allows you to specify the default + * content transfer encoding, such as "binary". + * </p> + * <p> + * Note 2: between RFC 3851 and RFC 5751 the values used in the micalg parameter + * for signed messages changed. We will accept both, but the default is now to use + * RFC 5751. In the event you are dealing with an older style system you will also need + * to use a constructor that sets the micalgs table and call it with RFC3851_MICALGS. + * </p> + */ +public class SMIMESignedGenerator + extends SMIMEGenerator +{ + public static final String DIGEST_SHA1 = OIWObjectIdentifiers.idSHA1.getId(); + public static final String DIGEST_MD5 = PKCSObjectIdentifiers.md5.getId(); + public static final String DIGEST_SHA224 = NISTObjectIdentifiers.id_sha224.getId(); + public static final String DIGEST_SHA256 = NISTObjectIdentifiers.id_sha256.getId(); + public static final String DIGEST_SHA384 = NISTObjectIdentifiers.id_sha384.getId(); + public static final String DIGEST_SHA512 = NISTObjectIdentifiers.id_sha512.getId(); + public static final String DIGEST_GOST3411 = CryptoProObjectIdentifiers.gostR3411.getId(); + public static final String DIGEST_RIPEMD128 = TeleTrusTObjectIdentifiers.ripemd128.getId(); + public static final String DIGEST_RIPEMD160 = TeleTrusTObjectIdentifiers.ripemd160.getId(); + public static final String DIGEST_RIPEMD256 = TeleTrusTObjectIdentifiers.ripemd256.getId(); + + public static final String ENCRYPTION_RSA = PKCSObjectIdentifiers.rsaEncryption.getId(); + public static final String ENCRYPTION_DSA = X9ObjectIdentifiers.id_dsa_with_sha1.getId(); + public static final String ENCRYPTION_ECDSA = X9ObjectIdentifiers.ecdsa_with_SHA1.getId(); + public static final String ENCRYPTION_RSA_PSS = PKCSObjectIdentifiers.id_RSASSA_PSS.getId(); + public static final String ENCRYPTION_GOST3410 = CryptoProObjectIdentifiers.gostR3410_94.getId(); + public static final String ENCRYPTION_ECGOST3410 = CryptoProObjectIdentifiers.gostR3410_2001.getId(); + + private static final String CERTIFICATE_MANAGEMENT_CONTENT = "application/pkcs7-mime; name=smime.p7c; smime-type=certs-only"; + private static final String DETACHED_SIGNATURE_TYPE = "application/pkcs7-signature; name=smime.p7s; smime-type=signed-data"; + private static final String ENCAPSULATED_SIGNED_CONTENT_TYPE = "application/pkcs7-mime; name=smime.p7m; smime-type=signed-data"; + + public static final Map RFC3851_MICALGS; + public static final Map RFC5751_MICALGS; + public static final Map STANDARD_MICALGS; + + private static MailcapCommandMap addCommands(CommandMap cm) + { + MailcapCommandMap mc = (MailcapCommandMap)cm; + + mc.addMailcap("application/pkcs7-signature;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_signature"); + mc.addMailcap("application/pkcs7-mime;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_mime"); + mc.addMailcap("application/x-pkcs7-signature;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_signature"); + mc.addMailcap("application/x-pkcs7-mime;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_mime"); + mc.addMailcap("multipart/signed;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.multipart_signed"); + + return mc; + } + + static + { + AccessController.doPrivileged(new PrivilegedAction() + { + public Object run() + { + CommandMap.setDefaultCommandMap(addCommands(CommandMap.getDefaultCommandMap())); + + return null; + } + }); + + Map stdMicAlgs = new HashMap(); + + stdMicAlgs.put(CMSAlgorithm.MD5, "md5"); + stdMicAlgs.put(CMSAlgorithm.SHA1, "sha-1"); + stdMicAlgs.put(CMSAlgorithm.SHA224, "sha-224"); + stdMicAlgs.put(CMSAlgorithm.SHA256, "sha-256"); + stdMicAlgs.put(CMSAlgorithm.SHA384, "sha-384"); + stdMicAlgs.put(CMSAlgorithm.SHA512, "sha-512"); + stdMicAlgs.put(CMSAlgorithm.GOST3411, "gostr3411-94"); + + RFC5751_MICALGS = Collections.unmodifiableMap(stdMicAlgs); + + Map oldMicAlgs = new HashMap(); + + oldMicAlgs.put(CMSAlgorithm.MD5, "md5"); + oldMicAlgs.put(CMSAlgorithm.SHA1, "sha1"); + oldMicAlgs.put(CMSAlgorithm.SHA224, "sha224"); + oldMicAlgs.put(CMSAlgorithm.SHA256, "sha256"); + oldMicAlgs.put(CMSAlgorithm.SHA384, "sha384"); + oldMicAlgs.put(CMSAlgorithm.SHA512, "sha512"); + oldMicAlgs.put(CMSAlgorithm.GOST3411, "gostr3411-94"); + + RFC3851_MICALGS = Collections.unmodifiableMap(oldMicAlgs); + + STANDARD_MICALGS = RFC5751_MICALGS; + } + + private final String defaultContentTransferEncoding; + private final Map micAlgs; + + private List _certStores = new ArrayList(); + private List certStores = new ArrayList(); + private List crlStores = new ArrayList(); + private List attrCertStores = new ArrayList(); + private List signerInfoGens = new ArrayList(); + private List _signers = new ArrayList(); + private List _oldSigners = new ArrayList(); + private List _attributeCerts = new ArrayList(); + private Map _digests = new HashMap(); + + /** + * base constructor - default content transfer encoding 7bit + */ + public SMIMESignedGenerator() + { + this("7bit", STANDARD_MICALGS); + } + + /** + * base constructor - default content transfer encoding explicitly set + * + * @param defaultContentTransferEncoding new default to use. + */ + public SMIMESignedGenerator( + String defaultContentTransferEncoding) + { + this(defaultContentTransferEncoding, STANDARD_MICALGS); + } + + /** + * base constructor - default content transfer encoding explicitly set + * + * @param micAlgs a map of ANS1ObjectIdentifiers to strings hash algorithm names. + */ + public SMIMESignedGenerator( + Map micAlgs) + { + this("7bit", micAlgs); + } + + /** + * base constructor - default content transfer encoding explicitly set + * + * @param defaultContentTransferEncoding new default to use. + * @param micAlgs a map of ANS1ObjectIdentifiers to strings hash algorithm names. + */ + public SMIMESignedGenerator( + String defaultContentTransferEncoding, + Map micAlgs) + { + this.defaultContentTransferEncoding = defaultContentTransferEncoding; + this.micAlgs = micAlgs; + } + + /** + * Add a store of precalculated signers to the generator. + * + * @param signerStore store of signers + */ + public void addSigners( + SignerInformationStore signerStore) + { + Iterator it = signerStore.getSigners().iterator(); + + while (it.hasNext()) + { + _oldSigners.add(it.next()); + } + } + + /** + * + * @param sigInfoGen + */ + public void addSignerInfoGenerator(SignerInfoGenerator sigInfoGen) + { + signerInfoGens.add(sigInfoGen); + } + + public void addCertificates( + Store certStore) + { + certStores.add(certStore); + } + + public void addCRLs( + Store crlStore) + { + crlStores.add(crlStore); + } + + public void addAttributeCertificates( + Store certStore) + { + attrCertStores.add(certStore); + } + + private void addHashHeader( + StringBuffer header, + List signers) + { + int count = 0; + + // + // build the hash header + // + Iterator it = signers.iterator(); + Set micAlgSet = new TreeSet(); + + while (it.hasNext()) + { + Object signer = it.next(); + ASN1ObjectIdentifier digestOID; + + if (signer instanceof SignerInformation) + { + digestOID = ((SignerInformation)signer).getDigestAlgorithmID().getAlgorithm(); + } + else + { + digestOID = ((SignerInfoGenerator)signer).getDigestAlgorithm().getAlgorithm(); + } + + String micAlg = (String)micAlgs.get(digestOID); + + if (micAlg == null) + { + micAlgSet.add("unknown"); + } + else + { + micAlgSet.add(micAlg); + } + } + + it = micAlgSet.iterator(); + + while (it.hasNext()) + { + String alg = (String)it.next(); + + if (count == 0) + { + if (micAlgSet.size() != 1) + { + header.append("; micalg=\""); + } + else + { + header.append("; micalg="); + } + } + else + { + header.append(','); + } + + header.append(alg); + + count++; + } + + if (count != 0) + { + if (micAlgSet.size() != 1) + { + header.append('\"'); + } + } + } + + private MimeMultipart make( + MimeBodyPart content) + throws SMIMEException + { + try + { + MimeBodyPart sig = new MimeBodyPart(); + + sig.setContent(new ContentSigner(content, false), DETACHED_SIGNATURE_TYPE); + sig.addHeader("Content-Type", DETACHED_SIGNATURE_TYPE); + sig.addHeader("Content-Disposition", "attachment; filename=\"smime.p7s\""); + sig.addHeader("Content-Description", "S/MIME Cryptographic Signature"); + sig.addHeader("Content-Transfer-Encoding", encoding); + + // + // build the multipart header + // + StringBuffer header = new StringBuffer( + "signed; protocol=\"application/pkcs7-signature\""); + + List allSigners = new ArrayList(_signers); + + allSigners.addAll(_oldSigners); + + allSigners.addAll(signerInfoGens); + + addHashHeader(header, allSigners); + + MimeMultipart mm = new MimeMultipart(header.toString()); + + mm.addBodyPart(content); + mm.addBodyPart(sig); + + return mm; + } + catch (MessagingException e) + { + throw new SMIMEException("exception putting multi-part together.", e); + } + } + + /* + * at this point we expect our body part to be well defined - generate with data in the signature + */ + private MimeBodyPart makeEncapsulated( + MimeBodyPart content) + throws SMIMEException + { + try + { + MimeBodyPart sig = new MimeBodyPart(); + + sig.setContent(new ContentSigner(content, true), ENCAPSULATED_SIGNED_CONTENT_TYPE); + sig.addHeader("Content-Type", ENCAPSULATED_SIGNED_CONTENT_TYPE); + sig.addHeader("Content-Disposition", "attachment; filename=\"smime.p7m\""); + sig.addHeader("Content-Description", "S/MIME Cryptographic Signed Data"); + sig.addHeader("Content-Transfer-Encoding", encoding); + + return sig; + } + catch (MessagingException e) + { + throw new SMIMEException("exception putting body part together.", e); + } + } + + /** + * Return a map of oids and byte arrays representing the digests calculated on the content during + * the last generate. + * + * @return a map of oids (as String objects) and byte[] representing digests. + */ + public Map getGeneratedDigests() + { + return new HashMap(_digests); + } + + public MimeMultipart generate( + MimeBodyPart content) + throws SMIMEException + { + return make(makeContentBodyPart(content)); + } + + public MimeMultipart generate( + MimeMessage message) + throws SMIMEException + { + try + { + message.saveChanges(); // make sure we're up to date. + } + catch (MessagingException e) + { + throw new SMIMEException("unable to save message", e); + } + + return make(makeContentBodyPart(message)); + } + + /** + * generate a signed message with encapsulated content + * <p> + * Note: doing this is strongly <b>not</b> recommended as it means a + * recipient of the message will have to be able to read the signature to read the + * message. + */ + public MimeBodyPart generateEncapsulated( + MimeBodyPart content) + throws SMIMEException + { + return makeEncapsulated(makeContentBodyPart(content)); + } + + public MimeBodyPart generateEncapsulated( + MimeMessage message) + throws SMIMEException + { + try + { + message.saveChanges(); // make sure we're up to date. + } + catch (MessagingException e) + { + throw new SMIMEException("unable to save message", e); + } + + return makeEncapsulated(makeContentBodyPart(message)); + } + + /** + * Creates a certificate management message which is like a signed message with no content + * or signers but that still carries certificates and CRLs. + * + * @return a MimeBodyPart containing the certs and CRLs. + */ + public MimeBodyPart generateCertificateManagement() + throws SMIMEException + { + try + { + MimeBodyPart sig = new MimeBodyPart(); + + sig.setContent(new ContentSigner(null, true), CERTIFICATE_MANAGEMENT_CONTENT); + sig.addHeader("Content-Type", CERTIFICATE_MANAGEMENT_CONTENT); + sig.addHeader("Content-Disposition", "attachment; filename=\"smime.p7c\""); + sig.addHeader("Content-Description", "S/MIME Certificate Management Message"); + sig.addHeader("Content-Transfer-Encoding", encoding); + + return sig; + } + catch (MessagingException e) + { + throw new SMIMEException("exception putting body part together.", e); + } + } + + private class ContentSigner + implements SMIMEStreamingProcessor + { + private final MimeBodyPart content; + private final boolean encapsulate; + private final boolean noProvider; + + ContentSigner( + MimeBodyPart content, + boolean encapsulate) + { + this.content = content; + this.encapsulate = encapsulate; + this.noProvider = true; + } + + protected CMSSignedDataStreamGenerator getGenerator() + throws CMSException + { + CMSSignedDataStreamGenerator gen = new CMSSignedDataStreamGenerator(); + + for (Iterator it = certStores.iterator(); it.hasNext();) + { + gen.addCertificates((Store)it.next()); + } + + for (Iterator it = crlStores.iterator(); it.hasNext();) + { + gen.addCRLs((Store)it.next()); + } + + for (Iterator it = attrCertStores.iterator(); it.hasNext();) + { + gen.addAttributeCertificates((Store)it.next()); + } + + for (Iterator it = signerInfoGens.iterator(); it.hasNext();) + { + gen.addSignerInfoGenerator((SignerInfoGenerator)it.next()); + } + + gen.addSigners(new SignerInformationStore(_oldSigners)); + + return gen; + } + + private void writeBodyPart( + OutputStream out, + MimeBodyPart bodyPart) + throws IOException, MessagingException + { + if (bodyPart.getContent() instanceof Multipart) + { + Multipart mp = (Multipart)bodyPart.getContent(); + ContentType contentType = new ContentType(mp.getContentType()); + String boundary = "--" + contentType.getParameter("boundary"); + + SMIMEUtil.LineOutputStream lOut = new SMIMEUtil.LineOutputStream(out); + + Enumeration headers = bodyPart.getAllHeaderLines(); + while (headers.hasMoreElements()) + { + lOut.writeln((String)headers.nextElement()); + } + + lOut.writeln(); // CRLF separator + + SMIMEUtil.outputPreamble(lOut, bodyPart, boundary); + + for (int i = 0; i < mp.getCount(); i++) + { + lOut.writeln(boundary); + writeBodyPart(out, (MimeBodyPart)mp.getBodyPart(i)); + lOut.writeln(); // CRLF terminator + } + + lOut.writeln(boundary + "--"); + } + else + { + if (SMIMEUtil.isCanonicalisationRequired(bodyPart, defaultContentTransferEncoding)) + { + out = new CRLFOutputStream(out); + } + + bodyPart.writeTo(out); + } + } + + public void write(OutputStream out) + throws IOException + { + try + { + CMSSignedDataStreamGenerator gen = getGenerator(); + + OutputStream signingStream = gen.open(out, encapsulate); + + if (content != null) + { + if (!encapsulate) + { + writeBodyPart(signingStream, content); + } + else + { + content.getDataHandler().setCommandMap(addCommands(CommandMap.getDefaultCommandMap())); + + content.writeTo(signingStream); + } + } + + signingStream.close(); + + _digests = gen.getGeneratedDigests(); + } + catch (MessagingException e) + { + throw new IOException(e.toString()); + } + catch (CMSException e) + { + throw new IOException(e.toString()); + } + } + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/SMIMESignedParser.java b/mail/src/main/java/org/spongycastle/mail/smime/SMIMESignedParser.java new file mode 100644 index 00000000..0f497697 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/SMIMESignedParser.java @@ -0,0 +1,368 @@ +package org.bouncycastle.mail.smime; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import javax.activation.CommandMap; +import javax.activation.MailcapCommandMap; +import javax.mail.BodyPart; +import javax.mail.MessagingException; +import javax.mail.Part; +import javax.mail.Session; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; + +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSSignedDataParser; +import org.bouncycastle.cms.CMSTypedStream; +import org.bouncycastle.operator.DigestCalculatorProvider; + +/** + * general class for handling a pkcs7-signature message. + * <p> + * A simple example of usage - note, in the example below the validity of + * the certificate isn't verified, just the fact that one of the certs + * matches the given signer... + * <p> + * <pre> + * CertStore certs = s.getCertificates("Collection", "BC"); + * SignerInformationStore signers = s.getSignerInfos(); + * Collection c = signers.getSigners(); + * Iterator it = c.iterator(); + * + * while (it.hasNext()) + * { + * SignerInformation signer = (SignerInformation)it.next(); + * Collection certCollection = certs.getCertificates(signer.getSID()); + * + * Iterator certIt = certCollection.iterator(); + * X509Certificate cert = (X509Certificate)certIt.next(); + * + * if (signer.verify(cert.getPublicKey())) + * { + * verified++; + * } + * } + * </pre> + * <p> + * Note: if you are using this class with AS2 or some other protocol + * that does not use 7bit as the default content transfer encoding you + * will need to use the constructor that allows you to specify the default + * content transfer encoding, such as "binary". + * </p> + */ +public class SMIMESignedParser + extends CMSSignedDataParser +{ + Object message; + MimeBodyPart content; + + private static InputStream getInputStream( + Part bodyPart) + throws MessagingException + { + try + { + if (bodyPart.isMimeType("multipart/signed")) + { + throw new MessagingException("attempt to create signed data object from multipart content - use MimeMultipart constructor."); + } + + return bodyPart.getInputStream(); + } + catch (IOException e) + { + throw new MessagingException("can't extract input stream: " + e); + } + } + + private static File getTmpFile() + throws MessagingException + { + try + { + return File.createTempFile("bcMail", ".mime"); + } + catch (IOException e) + { + throw new MessagingException("can't extract input stream: " + e); + } + } + + private static CMSTypedStream getSignedInputStream( + BodyPart bodyPart, + String defaultContentTransferEncoding, + File backingFile) + throws MessagingException + { + try + { + OutputStream out = new BufferedOutputStream(new FileOutputStream(backingFile)); + + SMIMEUtil.outputBodyPart(out, bodyPart, defaultContentTransferEncoding); + + out.close(); + + InputStream in = new TemporaryFileInputStream(backingFile); + + return new CMSTypedStream(in); + } + catch (IOException e) + { + throw new MessagingException("can't extract input stream: " + e); + } + } + + static + { + final MailcapCommandMap mc = (MailcapCommandMap)CommandMap.getDefaultCommandMap(); + + mc.addMailcap("application/pkcs7-signature;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_signature"); + mc.addMailcap("application/pkcs7-mime;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_mime"); + mc.addMailcap("application/x-pkcs7-signature;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_signature"); + mc.addMailcap("application/x-pkcs7-mime;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_mime"); + mc.addMailcap("multipart/signed;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.multipart_signed"); + + AccessController.doPrivileged(new PrivilegedAction() + { + public Object run() + { + CommandMap.setDefaultCommandMap(mc); + + return null; + } + }); + } + + /** + * base constructor using a defaultContentTransferEncoding of 7bit. A temporary backing file + * will be created for the signed data. + * + * @param digCalcProvider provider for digest calculators. + * @param message signed message with signature. + * @exception MessagingException on an error extracting the signature or + * otherwise processing the message. + * @exception CMSException if some other problem occurs. + */ + public SMIMESignedParser( + DigestCalculatorProvider digCalcProvider, + MimeMultipart message) + throws MessagingException, CMSException + { + this(digCalcProvider, message, getTmpFile()); + } + + /** + * base constructor using a defaultContentTransferEncoding of 7bit and a specified backing file. + * + * @param digCalcProvider provider for digest calculators. + * @param message signed message with signature. + * @param backingFile the temporary file to use to back the signed data. + * @exception MessagingException on an error extracting the signature or + * otherwise processing the message. + * @exception CMSException if some other problem occurs. + */ + public SMIMESignedParser( + DigestCalculatorProvider digCalcProvider, + MimeMultipart message, + File backingFile) + throws MessagingException, CMSException + { + this(digCalcProvider, message, "7bit", backingFile); + } + + /** + * base constructor with settable contentTransferEncoding. A temporary backing file will be created + * to contain the signed data. + * + * @param digCalcProvider provider for digest calculators. + * @param message the signed message with signature. + * @param defaultContentTransferEncoding new default to use. + * @exception MessagingException on an error extracting the signature or + * otherwise processing the message. + * @exception CMSException if some other problem occurs.r + */ + public SMIMESignedParser( + DigestCalculatorProvider digCalcProvider, + MimeMultipart message, + String defaultContentTransferEncoding) + throws MessagingException, CMSException + { + this(digCalcProvider, message, defaultContentTransferEncoding, getTmpFile()); + } + + /** + * base constructor with settable contentTransferEncoding and a specified backing file. + * + * @param digCalcProvider provider for digest calculators. + * @param message the signed message with signature. + * @param defaultContentTransferEncoding new default to use. + * @param backingFile the temporary file to use to back the signed data. + * @exception MessagingException on an error extracting the signature or + * otherwise processing the message. + * @exception CMSException if some other problem occurs. + */ + public SMIMESignedParser( + DigestCalculatorProvider digCalcProvider, + MimeMultipart message, + String defaultContentTransferEncoding, + File backingFile) + throws MessagingException, CMSException + { + super(digCalcProvider, getSignedInputStream(message.getBodyPart(0), defaultContentTransferEncoding, backingFile), getInputStream(message.getBodyPart(1))); + + this.message = message; + this.content = (MimeBodyPart)message.getBodyPart(0); + + drainContent(); + } + + /** + * base constructor for a signed message with encapsulated content. + * <p> + * Note: in this case the encapsulated MimeBody part will only be suitable for a single + * writeTo - once writeTo has been called the file containing the body part will be deleted. If writeTo is not + * called the file will be left in the temp directory. + * </p> + * @param digCalcProvider provider for digest calculators. + * @param message the message containing the encapsulated signed data. + * @exception MessagingException on an error extracting the signature or + * otherwise processing the message. + * @exception SMIMEException if the body part encapsulated in the message cannot be extracted. + * @exception CMSException if some other problem occurs. + */ + public SMIMESignedParser( + DigestCalculatorProvider digCalcProvider, + Part message) + throws MessagingException, CMSException, SMIMEException + { + super(digCalcProvider, getInputStream(message)); + + this.message = message; + + CMSTypedStream cont = this.getSignedContent(); + + if (cont != null) + { + this.content = SMIMEUtil.toWriteOnceBodyPart(cont); + } + } + + /** + * Constructor for a signed message with encapsulated content. The encapsulated + * content, if it exists, is written to the file represented by the File object + * passed in. + * + * @param digCalcProvider provider for digest calculators. + * @param message the Part containing the signed content. + * @param file the file the encapsulated part is to be written to after it has been decoded. + * + * @exception MessagingException on an error extracting the signature or + * otherwise processing the message. + * @exception SMIMEException if the body part encapsulated in the message cannot be extracted. + * @exception CMSException if some other problem occurs. + */ + public SMIMESignedParser( + DigestCalculatorProvider digCalcProvider, + Part message, + File file) + throws MessagingException, CMSException, SMIMEException + { + super(digCalcProvider, getInputStream(message)); + + this.message = message; + + CMSTypedStream cont = this.getSignedContent(); + + if (cont != null) + { + this.content = SMIMEUtil.toMimeBodyPart(cont, file); + } + } + + /** + * return the content that was signed. + * @return the signed body part in this message. + */ + public MimeBodyPart getContent() + { + return content; + } + + /** + * Return the content that was signed as a mime message. + * + * @param session the session to base the MimeMessage around. + * @return a MimeMessage holding the content. + * @throws MessagingException if there is an issue creating the MimeMessage. + * @throws IOException if there is an issue reading the content. + */ + public MimeMessage getContentAsMimeMessage(Session session) + throws MessagingException, IOException + { + if (message instanceof MimeMultipart) + { + BodyPart bp = ((MimeMultipart)message).getBodyPart(0); + return new MimeMessage(session, bp.getInputStream()); + } + else + { + return new MimeMessage(session, getSignedContent().getContentStream()); + } + } + + /** + * return the content that was signed with its signature attached. + * @return depending on whether this was unencapsulated or not it will return a MimeMultipart + * or a MimeBodyPart + */ + public Object getContentWithSignature() + { + return message; + } + + private void drainContent() + throws CMSException + { + try + { + this.getSignedContent().drain(); + } + catch (IOException e) + { + throw new CMSException("unable to read content for verification: " + e, e); + } + } + + private static class TemporaryFileInputStream + extends BufferedInputStream + { + private final File _file; + + TemporaryFileInputStream(File file) + throws FileNotFoundException + { + super(new FileInputStream(file)); + + _file = file; + } + + public void close() + throws IOException + { + super.close(); + + _file.delete(); + } + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/SMIMEStreamingProcessor.java b/mail/src/main/java/org/spongycastle/mail/smime/SMIMEStreamingProcessor.java new file mode 100644 index 00000000..e773232d --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/SMIMEStreamingProcessor.java @@ -0,0 +1,10 @@ +package org.bouncycastle.mail.smime; + +import java.io.IOException; +import java.io.OutputStream; + +public interface SMIMEStreamingProcessor +{ + public void write(OutputStream out) + throws IOException; +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/SMIMEUtil.java b/mail/src/main/java/org/spongycastle/mail/smime/SMIMEUtil.java new file mode 100644 index 00000000..b2f36e6f --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/SMIMEUtil.java @@ -0,0 +1,623 @@ +package org.bouncycastle.mail.smime; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.NoSuchProviderException; +import java.security.Provider; +import java.security.Security; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Enumeration; + +import javax.mail.BodyPart; +import javax.mail.MessagingException; +import javax.mail.internet.ContentType; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMultipart; + +import org.bouncycastle.asn1.cms.IssuerAndSerialNumber; +import org.bouncycastle.cms.CMSTypedStream; +import org.bouncycastle.jce.PrincipalUtil; +import org.bouncycastle.mail.smime.util.CRLFOutputStream; +import org.bouncycastle.mail.smime.util.FileBackedMimeBodyPart; + +public class SMIMEUtil +{ + private static final int BUF_SIZE = 32760; + + static boolean isCanonicalisationRequired( + MimeBodyPart bodyPart, + String defaultContentTransferEncoding) + throws MessagingException + { + String[] cte = bodyPart.getHeader("Content-Transfer-Encoding"); + String contentTransferEncoding; + + if (cte == null) + { + contentTransferEncoding = defaultContentTransferEncoding; + } + else + { + contentTransferEncoding = cte[0]; + } + + return !contentTransferEncoding.equalsIgnoreCase("binary"); + } + + public static Provider getProvider(String providerName) + throws NoSuchProviderException + { + if (providerName != null) + { + Provider prov = Security.getProvider(providerName); + + if (prov != null) + { + return prov; + } + + throw new NoSuchProviderException("provider " + providerName + " not found."); + } + + return null; + } + + static class LineOutputStream extends FilterOutputStream + { + private static byte newline[]; + + public LineOutputStream(OutputStream outputstream) + { + super(outputstream); + } + + public void writeln(String s) + throws MessagingException + { + try + { + byte abyte0[] = getBytes(s); + super.out.write(abyte0); + super.out.write(newline); + } + catch(Exception exception) + { + throw new MessagingException("IOException", exception); + } + } + + public void writeln() + throws MessagingException + { + try + { + super.out.write(newline); + } + catch(Exception exception) + { + throw new MessagingException("IOException", exception); + } + } + + static + { + newline = new byte[2]; + newline[0] = 13; + newline[1] = 10; + } + + private static byte[] getBytes(String s) + { + char ac[] = s.toCharArray(); + int i = ac.length; + byte abyte0[] = new byte[i]; + int j = 0; + + while (j < i) + { + abyte0[j] = (byte)ac[j++]; + } + + return abyte0; + } + } + + /** + * internal preamble is generally included in signatures, while this is technically wrong, + * if we find internal preamble we include it by default. + */ + static void outputPreamble(LineOutputStream lOut, MimeBodyPart part, String boundary) + throws MessagingException, IOException + { + InputStream in; + + try + { + in = part.getRawInputStream(); + } + catch (MessagingException e) + { + return; // no underlying content rely on default generation + } + + String line; + + while ((line = readLine(in)) != null) + { + if (line.equals(boundary)) + { + break; + } + + lOut.writeln(line); + } + + in.close(); + + if (line == null) + { + throw new MessagingException("no boundary found"); + } + } + + /** + * internal postamble is generally included in signatures, while this is technically wrong, + * if we find internal postamble we include it by default. + */ + static void outputPostamble(LineOutputStream lOut, MimeBodyPart part, int count, String boundary) + throws MessagingException, IOException + { + InputStream in; + + try + { + in = part.getRawInputStream(); + } + catch (MessagingException e) + { + return; // no underlying content rely on default generation + } + + String line; + int boundaries = count + 1; + + while ((line = readLine(in)) != null) + { + if (line.startsWith(boundary)) + { + boundaries--; + + if (boundaries == 0) + { + break; + } + } + } + + while ((line = readLine(in)) != null) + { + lOut.writeln(line); + } + + in.close(); + + if (boundaries != 0) + { + throw new MessagingException("all boundaries not found for: " + boundary); + } + } + + static void outputPostamble(LineOutputStream lOut, BodyPart parent, String parentBoundary, BodyPart part) + throws MessagingException, IOException + { + InputStream in; + + try + { + in = ((MimeBodyPart)parent).getRawInputStream(); + } + catch (MessagingException e) + { + return; // no underlying content rely on default generation + } + + + MimeMultipart multipart = (MimeMultipart)part.getContent(); + ContentType contentType = new ContentType(multipart.getContentType()); + String boundary = "--" + contentType.getParameter("boundary"); + int count = multipart.getCount() + 1; + String line; + while (count != 0 && (line = readLine(in)) != null) + { + if (line.startsWith(boundary)) + { + count--; + } + } + + while ((line = readLine(in)) != null) + { + if (line.startsWith(parentBoundary)) + { + break; + } + lOut.writeln(line); + } + + in.close(); + } + + /* + * read a line of input stripping of the tailing \r\n + */ + private static String readLine(InputStream in) + throws IOException + { + StringBuffer b = new StringBuffer(); + + int ch; + while ((ch = in.read()) >= 0 && ch != '\n') + { + if (ch != '\r') + { + b.append((char)ch); + } + } + + if (ch < 0 && b.length() == 0) + { + return null; + } + + return b.toString(); + } + + static void outputBodyPart( + OutputStream out, + BodyPart bodyPart, + String defaultContentTransferEncoding) + throws MessagingException, IOException + { + if (bodyPart instanceof MimeBodyPart) + { + MimeBodyPart mimePart = (MimeBodyPart)bodyPart; + String[] cte = mimePart.getHeader("Content-Transfer-Encoding"); + String contentTransferEncoding; + + if (mimePart.getContent() instanceof MimeMultipart) + { + MimeMultipart mp = (MimeMultipart)bodyPart.getContent(); + ContentType contentType = new ContentType(mp.getContentType()); + String boundary = "--" + contentType.getParameter("boundary"); + + SMIMEUtil.LineOutputStream lOut = new SMIMEUtil.LineOutputStream(out); + + Enumeration headers = mimePart.getAllHeaderLines(); + while (headers.hasMoreElements()) + { + String header = (String)headers.nextElement(); + lOut.writeln(header); + } + + lOut.writeln(); // CRLF separator + + outputPreamble(lOut, mimePart, boundary); + + for (int i = 0; i < mp.getCount(); i++) + { + lOut.writeln(boundary); + BodyPart part = mp.getBodyPart(i); + outputBodyPart(out, part, defaultContentTransferEncoding); + if (!(part.getContent() instanceof MimeMultipart)) + { + lOut.writeln(); // CRLF terminator needed + } + else + { + outputPostamble(lOut, mimePart, boundary, part); + } + } + + lOut.writeln(boundary + "--"); + + outputPostamble(lOut, mimePart, mp.getCount(), boundary); + + return; + } + + if (cte == null) + { + contentTransferEncoding = defaultContentTransferEncoding; + } + else + { + contentTransferEncoding = cte[0]; + } + + if (!contentTransferEncoding.equalsIgnoreCase("base64") + && !contentTransferEncoding.equalsIgnoreCase("quoted-printable")) + { + if (!contentTransferEncoding.equalsIgnoreCase("binary")) + { + out = new CRLFOutputStream(out); + } + bodyPart.writeTo(out); + out.flush(); + return; + } + + boolean base64 = contentTransferEncoding.equalsIgnoreCase("base64"); + + // + // Write raw content, performing canonicalization + // + InputStream inRaw; + + try + { + inRaw = mimePart.getRawInputStream(); + } + catch (MessagingException e) + { + // this is less than ideal, but if the raw output stream is unavailable it's the + // best option we've got. + out = new CRLFOutputStream(out); + bodyPart.writeTo(out); + out.flush(); + return; + } + + // + // Write headers + // + LineOutputStream outLine = new LineOutputStream(out); + for (Enumeration e = mimePart.getAllHeaderLines(); e.hasMoreElements();) + { + String header = (String)e.nextElement(); + + outLine.writeln(header); + } + + outLine.writeln(); + outLine.flush(); + + + OutputStream outCRLF; + + if (base64) + { + outCRLF = new Base64CRLFOutputStream(out); + } + else + { + outCRLF = new CRLFOutputStream(out); + } + + byte[] buf = new byte[BUF_SIZE]; + + int len; + while ((len = inRaw.read(buf, 0, buf.length)) > 0) + { + + outCRLF.write(buf, 0, len); + } + + outCRLF.flush(); + } + else + { + if (!defaultContentTransferEncoding.equalsIgnoreCase("binary")) + { + out = new CRLFOutputStream(out); + } + + bodyPart.writeTo(out); + + out.flush(); + } + } + + /** + * return the MimeBodyPart described in the raw bytes provided in content + */ + public static MimeBodyPart toMimeBodyPart( + byte[] content) + throws SMIMEException + { + return toMimeBodyPart(new ByteArrayInputStream(content)); + } + + /** + * return the MimeBodyPart described in the input stream content + */ + public static MimeBodyPart toMimeBodyPart( + InputStream content) + throws SMIMEException + { + try + { + return new MimeBodyPart(content); + } + catch (MessagingException e) + { + throw new SMIMEException("exception creating body part.", e); + } + } + + static FileBackedMimeBodyPart toWriteOnceBodyPart( + CMSTypedStream content) + throws SMIMEException + { + try + { + return new WriteOnceFileBackedMimeBodyPart(content.getContentStream(), File.createTempFile("bcMail", ".mime")); + } + catch (IOException e) + { + throw new SMIMEException("IOException creating tmp file:" + e.getMessage(), e); + } + catch (MessagingException e) + { + throw new SMIMEException("can't create part: " + e, e); + } + } + + /** + * return a file backed MimeBodyPart described in {@link CMSTypedStream} content. + * </p> + */ + public static FileBackedMimeBodyPart toMimeBodyPart( + CMSTypedStream content) + throws SMIMEException + { + try + { + return toMimeBodyPart(content, File.createTempFile("bcMail", ".mime")); + } + catch (IOException e) + { + throw new SMIMEException("IOException creating tmp file:" + e.getMessage(), e); + } + } + + /** + * Return a file based MimeBodyPart represented by content and backed + * by the file represented by file. + * + * @param content content stream containing body part. + * @param file file to store the decoded body part in. + * @return the decoded body part. + * @throws SMIMEException + */ + public static FileBackedMimeBodyPart toMimeBodyPart( + CMSTypedStream content, + File file) + throws SMIMEException + { + try + { + return new FileBackedMimeBodyPart(content.getContentStream(), file); + } + catch (IOException e) + { + throw new SMIMEException("can't save content to file: " + e, e); + } + catch (MessagingException e) + { + throw new SMIMEException("can't create part: " + e, e); + } + } + + /** + * Return a CMS IssuerAndSerialNumber structure for the passed in X.509 certificate. + * + * @param cert the X.509 certificate to get the issuer and serial number for. + * @return an IssuerAndSerialNumber structure representing the certificate. + */ + public static IssuerAndSerialNumber createIssuerAndSerialNumberFor( + X509Certificate cert) + throws CertificateParsingException + { + try + { + return new IssuerAndSerialNumber(PrincipalUtil.getIssuerX509Principal(cert), cert.getSerialNumber()); + } + catch (Exception e) + { + throw new CertificateParsingException("exception extracting issuer and serial number: " + e); + } + } + + private static class WriteOnceFileBackedMimeBodyPart + extends FileBackedMimeBodyPart + { + public WriteOnceFileBackedMimeBodyPart(InputStream content, File file) + throws MessagingException, IOException + { + super(content, file); + } + + public void writeTo(OutputStream out) + throws MessagingException, IOException + { + super.writeTo(out); + + this.dispose(); + } + } + + static class Base64CRLFOutputStream extends FilterOutputStream + { + protected int lastb; + protected static byte newline[]; + private boolean isCrlfStream; + + public Base64CRLFOutputStream(OutputStream outputstream) + { + super(outputstream); + lastb = -1; + } + + public void write(int i) + throws IOException + { + if (i == '\r') + { + out.write(newline); + } + else if (i == '\n') + { + if (lastb != '\r') + { // imagine my joy... + if (!(isCrlfStream && lastb == '\n')) + { + out.write(newline); + } + } + else + { + isCrlfStream = true; + } + } + else + { + out.write(i); + } + + lastb = i; + } + + public void write(byte[] buf) + throws IOException + { + this.write(buf, 0, buf.length); + } + + public void write(byte buf[], int off, int len) + throws IOException + { + for (int i = off; i != off + len; i++) + { + this.write(buf[i]); + } + } + + public void writeln() + throws IOException + { + super.out.write(newline); + } + + static + { + newline = new byte[2]; + newline[0] = '\r'; + newline[1] = '\n'; + } + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateCompressedMail.java b/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateCompressedMail.java new file mode 100644 index 00000000..5d1df695 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateCompressedMail.java @@ -0,0 +1,57 @@ +package org.bouncycastle.mail.smime.examples; + +import java.io.FileOutputStream; +import java.util.Properties; + +import javax.mail.Address; +import javax.mail.Message; +import javax.mail.Session; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; + +import org.bouncycastle.cms.jcajce.ZlibCompressor; +import org.bouncycastle.mail.smime.SMIMECompressedGenerator; + +/** + * a simple example that creates a single compressed mail message. + */ +public class CreateCompressedMail +{ + public static void main( + String args[]) + throws Exception + { + // + // create the generator for creating an smime/compressed message + // + SMIMECompressedGenerator gen = new SMIMECompressedGenerator(); + + // + // create the base for our message + // + MimeBodyPart msg = new MimeBodyPart(); + + msg.setText("Hello world!"); + + MimeBodyPart mp = gen.generate(msg, new ZlibCompressor()); + + // + // Get a Session object and create the mail message + // + Properties props = System.getProperties(); + Session session = Session.getDefaultInstance(props, null); + + Address fromUser = new InternetAddress("\"Eric H. Echidna\"<eric@bouncycastle.org>"); + Address toUser = new InternetAddress("example@bouncycastle.org"); + + MimeMessage body = new MimeMessage(session); + body.setFrom(fromUser); + body.setRecipient(Message.RecipientType.TO, toUser); + body.setSubject("example compressed message"); + body.setContent(mp.getContent(), mp.getContentType()); + body.saveChanges(); + + body.writeTo(new FileOutputStream("compressed.message")); + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateEncryptedMail.java b/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateEncryptedMail.java new file mode 100644 index 00000000..6ef42d9d --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateEncryptedMail.java @@ -0,0 +1,128 @@ +package org.bouncycastle.mail.smime.examples; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.security.KeyStore; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Enumeration; +import java.util.Properties; + +import javax.mail.Address; +import javax.mail.Message; +import javax.mail.Session; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; + +import org.bouncycastle.cms.CMSAlgorithm; +import org.bouncycastle.cms.jcajce.JceCMSContentEncryptorBuilder; +import org.bouncycastle.cms.jcajce.JceKeyTransRecipientInfoGenerator; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.mail.smime.SMIMEEnvelopedGenerator; + +/** + * a simple example that creates a single encrypted mail message. + * <p> + * The key store can be created using the class in + * org.bouncycastle.jce.examples.PKCS12Example - the program expects only one + * key to be present in the key file. + * <p> + * Note: while this means that both the private key is available to + * the program, the private key is retrieved from the keystore only for + * the purposes of locating the corresponding public key, in normal circumstances + * you would only be doing this with a certificate available. + */ +public class CreateEncryptedMail +{ + public static void main( + String args[]) + throws Exception + { + if (args.length != 2) + { + System.err.println("usage: CreateEncryptedMail pkcs12Keystore password"); + System.exit(0); + } + + if (Security.getProvider("BC") == null) + { + Security.addProvider(new BouncyCastleProvider()); + } + + // + // Open the key store + // + KeyStore ks = KeyStore.getInstance("PKCS12", "BC"); + + ks.load(new FileInputStream(args[0]), args[1].toCharArray()); + + Enumeration e = ks.aliases(); + String keyAlias = null; + + while (e.hasMoreElements()) + { + String alias = (String)e.nextElement(); + + if (ks.isKeyEntry(alias)) + { + keyAlias = alias; + } + } + + if (keyAlias == null) + { + System.err.println("can't find a private key!"); + System.exit(0); + } + + Certificate[] chain = ks.getCertificateChain(keyAlias); + + // + // create the generator for creating an smime/encrypted message + // + SMIMEEnvelopedGenerator gen = new SMIMEEnvelopedGenerator(); + + gen.addRecipientInfoGenerator(new JceKeyTransRecipientInfoGenerator((X509Certificate)chain[0]).setProvider("BC")); + + // + // create a subject key id - this has to be done the same way as + // it is done in the certificate associated with the private key + // version 3 only. + // + /* + MessageDigest dig = MessageDigest.getInstance("SHA1", "BC"); + + dig.update(cert.getPublicKey().getEncoded()); + + gen.addKeyTransRecipient(cert.getPublicKey(), dig.digest()); + */ + + // + // create the base for our message + // + MimeBodyPart msg = new MimeBodyPart(); + + msg.setText("Hello world!"); + + MimeBodyPart mp = gen.generate(msg, new JceCMSContentEncryptorBuilder(CMSAlgorithm.RC2_CBC).setProvider("BC").build()); + // + // Get a Session object and create the mail message + // + Properties props = System.getProperties(); + Session session = Session.getDefaultInstance(props, null); + + Address fromUser = new InternetAddress("\"Eric H. Echidna\"<eric@bouncycastle.org>"); + Address toUser = new InternetAddress("example@bouncycastle.org"); + + MimeMessage body = new MimeMessage(session); + body.setFrom(fromUser); + body.setRecipient(Message.RecipientType.TO, toUser); + body.setSubject("example encrypted message"); + body.setContent(mp.getContent(), mp.getContentType()); + body.saveChanges(); + + body.writeTo(new FileOutputStream("encrypted.message")); + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateLargeCompressedMail.java b/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateLargeCompressedMail.java new file mode 100644 index 00000000..63c5125d --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateLargeCompressedMail.java @@ -0,0 +1,63 @@ +package org.bouncycastle.mail.smime.examples; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.Properties; + +import javax.activation.DataHandler; +import javax.activation.FileDataSource; +import javax.mail.Address; +import javax.mail.Message; +import javax.mail.Session; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; + +import org.bouncycastle.cms.jcajce.ZlibCompressor; +import org.bouncycastle.mail.smime.SMIMECompressedGenerator; + +/** + * a simple example that creates a single compressed mail message using the large + * file model. + */ +public class CreateLargeCompressedMail +{ + public static void main( + String args[]) + throws Exception + { + // + // create the generator for creating an smime/compressed message + // + SMIMECompressedGenerator gen = new SMIMECompressedGenerator(); + + // + // create the base for our message + // + MimeBodyPart msg = new MimeBodyPart(); + + msg.setDataHandler(new DataHandler(new FileDataSource(new File(args[0])))); + msg.setHeader("Content-Type", "application/octet-stream"); + msg.setHeader("Content-Transfer-Encoding", "binary"); + + MimeBodyPart mp = gen.generate(msg, new ZlibCompressor()); + + // + // Get a Session object and create the mail message + // + Properties props = System.getProperties(); + Session session = Session.getDefaultInstance(props, null); + + Address fromUser = new InternetAddress("\"Eric H. Echidna\"<eric@bouncycastle.org>"); + Address toUser = new InternetAddress("example@bouncycastle.org"); + + MimeMessage body = new MimeMessage(session); + body.setFrom(fromUser); + body.setRecipient(Message.RecipientType.TO, toUser); + body.setSubject("example compressed message"); + body.setContent(mp.getContent(), mp.getContentType()); + body.saveChanges(); + + body.writeTo(new FileOutputStream("compressed.message")); + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateLargeEncryptedMail.java b/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateLargeEncryptedMail.java new file mode 100644 index 00000000..5fc7663a --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateLargeEncryptedMail.java @@ -0,0 +1,105 @@ +package org.bouncycastle.mail.smime.examples; + +import java.io.File; +import java.io.FileOutputStream; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Properties; + +import javax.activation.DataHandler; +import javax.activation.FileDataSource; +import javax.mail.Address; +import javax.mail.Message; +import javax.mail.Session; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; + +import org.bouncycastle.cms.CMSAlgorithm; +import org.bouncycastle.cms.jcajce.JceCMSContentEncryptorBuilder; +import org.bouncycastle.cms.jcajce.JceKeyTransRecipientInfoGenerator; +import org.bouncycastle.mail.smime.SMIMEEnvelopedGenerator; + +/** + * a simple example that creates a single encrypted mail message. + * <p> + * The key store can be created using the class in + * org.bouncycastle.jce.examples.PKCS12Example - the program expects only one + * key to be present in the key file. + * <p> + * Note: while this means that both the private key is available to + * the program, the private key is retrieved from the keystore only for + * the purposes of locating the corresponding public key, in normal circumstances + * you would only be doing this with a certificate available. + */ +public class CreateLargeEncryptedMail +{ + public static void main( + String args[]) + throws Exception + { + if (args.length != 3) + { + System.err.println("usage: CreateLargeEncryptedMail pkcs12Keystore password inputFile"); + System.exit(0); + } + + // + // Open the key store + // + KeyStore ks = KeyStore.getInstance("PKCS12", "BC"); + String keyAlias = ExampleUtils.findKeyAlias(ks, args[0], args[1].toCharArray()); + + Certificate[] chain = ks.getCertificateChain(keyAlias); + + // + // create the generator for creating an smime/encrypted message + // + SMIMEEnvelopedGenerator gen = new SMIMEEnvelopedGenerator(); + + gen.addRecipientInfoGenerator(new JceKeyTransRecipientInfoGenerator((X509Certificate)chain[0]).setProvider("BC")); + + // + // create a subject key id - this has to be done the same way as + // it is done in the certificate associated with the private key + // version 3 only. + // + /* + MessageDigest dig = MessageDigest.getInstance("SHA1", "BC"); + + dig.update(cert.getPublicKey().getEncoded()); + + gen.addKeyTransRecipient(cert.getPublicKey(), dig.digest()); + */ + + // + // create the base for our message + // + MimeBodyPart msg = new MimeBodyPart(); + + msg.setDataHandler(new DataHandler(new FileDataSource(new File(args[2])))); + msg.setHeader("Content-Type", "application/octet-stream"); + msg.setHeader("Content-Transfer-Encoding", "binary"); + + MimeBodyPart mp = gen.generate(msg, new JceCMSContentEncryptorBuilder(CMSAlgorithm.RC2_CBC).setProvider("BC").build()); + + // + // Get a Session object and create the mail message + // + Properties props = System.getProperties(); + Session session = Session.getDefaultInstance(props, null); + + Address fromUser = new InternetAddress("\"Eric H. Echidna\"<eric@bouncycastle.org>"); + Address toUser = new InternetAddress("example@bouncycastle.org"); + + MimeMessage body = new MimeMessage(session); + body.setFrom(fromUser); + body.setRecipient(Message.RecipientType.TO, toUser); + body.setSubject("example encrypted message"); + body.setContent(mp.getContent(), mp.getContentType()); + body.saveChanges(); + + body.writeTo(new FileOutputStream("encrypted.message")); + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateLargeSignedMail.java b/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateLargeSignedMail.java new file mode 100644 index 00000000..ffb092ab --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateLargeSignedMail.java @@ -0,0 +1,198 @@ +package org.bouncycastle.mail.smime.examples; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Properties; + +import javax.activation.DataHandler; +import javax.activation.FileDataSource; +import javax.mail.Address; +import javax.mail.Message; +import javax.mail.Session; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +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.x500.X500Name; +import org.bouncycastle.asn1.x509.X509Extension; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoGeneratorBuilder; +import org.bouncycastle.mail.smime.SMIMESignedGenerator; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.util.Store; + +/** + * a simple example that creates a single signed mail message. + */ +public class CreateLargeSignedMail +{ + // + // certificate serial number seed. + // + static int serialNo = 1; + + /** + * create a basic X509 certificate from the given keys + */ + static X509Certificate makeCertificate( + KeyPair subKP, + String subDN, + KeyPair issKP, + String issDN) + throws GeneralSecurityException, IOException, OperatorCreationException + { + PublicKey subPub = subKP.getPublic(); + PrivateKey issPriv = issKP.getPrivate(); + PublicKey issPub = issKP.getPublic(); + + JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils(); + X509v3CertificateBuilder v3CertGen = new JcaX509v3CertificateBuilder(new X500Name(issDN), BigInteger.valueOf(serialNo++), new Date(System.currentTimeMillis()), new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 100)), new X500Name(subDN), subPub); + + v3CertGen.addExtension( + X509Extension.subjectKeyIdentifier, + false, + extUtils.createSubjectKeyIdentifier(subPub)); + + v3CertGen.addExtension( + X509Extension.authorityKeyIdentifier, + false, + extUtils.createAuthorityKeyIdentifier(issPub)); + + return new JcaX509CertificateConverter().setProvider("BC").getCertificate(v3CertGen.build(new JcaContentSignerBuilder("MD5withRSA").setProvider("BC").build(issPriv))); + } + + public static void main( + String args[]) + throws Exception + { + // + // set up our certs + // + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "BC"); + + kpg.initialize(1024, new SecureRandom()); + + // + // cert that issued the signing certificate + // + String signDN = "O=Bouncy Castle, C=AU"; + KeyPair signKP = kpg.generateKeyPair(); + X509Certificate signCert = makeCertificate( + signKP, signDN, signKP, signDN); + + // + // cert we sign against + // + String origDN = "CN=Eric H. Echidna, E=eric@bouncycastle.org, O=Bouncy Castle, C=AU"; + KeyPair origKP = kpg.generateKeyPair(); + X509Certificate origCert = makeCertificate( + origKP, origDN, signKP, signDN); + + List certList = new ArrayList(); + + certList.add(origCert); + certList.add(signCert); + + // + // create a CertStore containing the certificates we want carried + // in the signature + // + Store certs = new JcaCertStore(certList); + + // + // create some smime capabilities in case someone wants to respond + // + ASN1EncodableVector signedAttrs = new ASN1EncodableVector(); + SMIMECapabilityVector caps = new SMIMECapabilityVector(); + + caps.addCapability(SMIMECapability.dES_EDE3_CBC); + caps.addCapability(SMIMECapability.rC2_CBC, 128); + caps.addCapability(SMIMECapability.dES_CBC); + + signedAttrs.add(new SMIMECapabilitiesAttribute(caps)); + + // + // add an encryption key preference for encrypted responses - + // normally this would be different from the signing certificate... + // + IssuerAndSerialNumber issAndSer = new IssuerAndSerialNumber( + new X500Name(signDN), origCert.getSerialNumber()); + + signedAttrs.add(new SMIMEEncryptionKeyPreferenceAttribute(issAndSer)); + + // + // create the generator for creating an smime/signed message + // + SMIMESignedGenerator gen = new SMIMESignedGenerator(); + + // + // add a signer to the generator - this specifies we are using SHA1 and + // adding the smime attributes above to the signed attributes that + // will be generated as part of the signature. The encryption algorithm + // used is taken from the key - in this RSA with PKCS1Padding + // + gen.addSignerInfoGenerator(new JcaSimpleSignerInfoGeneratorBuilder().setProvider("BC").setSignedAttributeGenerator(new AttributeTable(signedAttrs)).build("SHA1withRSA", origKP.getPrivate(), origCert)); + + // + // add our pool of certs and cerls (if any) to go with the signature + // + gen.addCertificates(certs); + + // + // create the base for our message + // + MimeBodyPart msg = new MimeBodyPart(); + + msg.setDataHandler(new DataHandler(new FileDataSource(new File(args[0])))); + msg.setHeader("Content-Type", "application/octet-stream"); + msg.setHeader("Content-Transfer-Encoding", "base64"); + + // + // extract the multipart object from the SMIMESigned object. + // + MimeMultipart mm = gen.generate(msg); + + // + // Get a Session object and create the mail message + // + Properties props = System.getProperties(); + Session session = Session.getDefaultInstance(props, null); + + Address fromUser = new InternetAddress("\"Eric H. Echidna\"<eric@bouncycastle.org>"); + Address toUser = new InternetAddress("example@bouncycastle.org"); + + MimeMessage body = new MimeMessage(session); + body.setFrom(fromUser); + body.setRecipient(Message.RecipientType.TO, toUser); + body.setSubject("example signed message"); + body.setContent(mm, mm.getContentType()); + body.saveChanges(); + + body.writeTo(new FileOutputStream("signed.message")); + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateSignedMail.java b/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateSignedMail.java new file mode 100644 index 00000000..8f3fb787 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateSignedMail.java @@ -0,0 +1,221 @@ +package org.bouncycastle.mail.smime.examples; + +import java.io.ByteArrayInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Properties; + +import javax.mail.Address; +import javax.mail.Message; +import javax.mail.Session; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; + +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Sequence; +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.x500.X500Name; +import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier; +import org.bouncycastle.asn1.x509.SubjectKeyIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.asn1.x509.X509Extension; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.bc.BcX509ExtensionUtils; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoGeneratorBuilder; +import org.bouncycastle.mail.smime.SMIMESignedGenerator; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.util.Store; + +/** + * a simple example that creates a single signed mail message. + */ +public class CreateSignedMail +{ + // + // certificate serial number seed. + // + static int serialNo = 1; + + static AuthorityKeyIdentifier createAuthorityKeyId( + PublicKey pub) + throws IOException + { + ByteArrayInputStream bIn = new ByteArrayInputStream(pub.getEncoded()); + SubjectPublicKeyInfo info = new SubjectPublicKeyInfo( + (ASN1Sequence)new ASN1InputStream(bIn).readObject()); + + return new AuthorityKeyIdentifier(info); + } + + static SubjectKeyIdentifier createSubjectKeyId( + PublicKey pub) + throws IOException + { + ByteArrayInputStream bIn = new ByteArrayInputStream(pub.getEncoded()); + + SubjectPublicKeyInfo info = new SubjectPublicKeyInfo( + (ASN1Sequence)new ASN1InputStream(bIn).readObject()); + + return new BcX509ExtensionUtils().createSubjectKeyIdentifier(info); + } + + /** + * create a basic X509 certificate from the given keys + */ + static X509Certificate makeCertificate( + KeyPair subKP, + String subDN, + KeyPair issKP, + String issDN) + throws GeneralSecurityException, IOException, OperatorCreationException + { + PublicKey subPub = subKP.getPublic(); + PrivateKey issPriv = issKP.getPrivate(); + PublicKey issPub = issKP.getPublic(); + + X509v3CertificateBuilder v3CertGen = new JcaX509v3CertificateBuilder(new X500Name(issDN), BigInteger.valueOf(serialNo++), new Date(System.currentTimeMillis()), new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 100)), new X500Name(subDN), subPub); + + v3CertGen.addExtension( + X509Extension.subjectKeyIdentifier, + false, + createSubjectKeyId(subPub)); + + v3CertGen.addExtension( + X509Extension.authorityKeyIdentifier, + false, + createAuthorityKeyId(issPub)); + + return new JcaX509CertificateConverter().setProvider("BC").getCertificate(v3CertGen.build(new JcaContentSignerBuilder("MD5withRSA").setProvider("BC").build(issPriv))); + } + + public static void main( + String args[]) + throws Exception + { + // + // set up our certs + // + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "BC"); + + kpg.initialize(1024, new SecureRandom()); + + // + // cert that issued the signing certificate + // + String signDN = "O=Bouncy Castle, C=AU"; + KeyPair signKP = kpg.generateKeyPair(); + X509Certificate signCert = makeCertificate( + signKP, signDN, signKP, signDN); + + // + // cert we sign against + // + String origDN = "CN=Eric H. Echidna, E=eric@bouncycastle.org, O=Bouncy Castle, C=AU"; + KeyPair origKP = kpg.generateKeyPair(); + X509Certificate origCert = makeCertificate( + origKP, origDN, signKP, signDN); + + List certList = new ArrayList(); + + certList.add(origCert); + certList.add(signCert); + + // + // create a CertStore containing the certificates we want carried + // in the signature + // + Store certs = new JcaCertStore(certList); + + // + // create some smime capabilities in case someone wants to respond + // + ASN1EncodableVector signedAttrs = new ASN1EncodableVector(); + SMIMECapabilityVector caps = new SMIMECapabilityVector(); + + caps.addCapability(SMIMECapability.dES_EDE3_CBC); + caps.addCapability(SMIMECapability.rC2_CBC, 128); + caps.addCapability(SMIMECapability.dES_CBC); + + signedAttrs.add(new SMIMECapabilitiesAttribute(caps)); + + // + // add an encryption key preference for encrypted responses - + // normally this would be different from the signing certificate... + // + IssuerAndSerialNumber issAndSer = new IssuerAndSerialNumber( + new X500Name(signDN), origCert.getSerialNumber()); + + signedAttrs.add(new SMIMEEncryptionKeyPreferenceAttribute(issAndSer)); + + // + // create the generator for creating an smime/signed message + // + SMIMESignedGenerator gen = new SMIMESignedGenerator(); + + // + // add a signer to the generator - this specifies we are using SHA1 and + // adding the smime attributes above to the signed attributes that + // will be generated as part of the signature. The encryption algorithm + // used is taken from the key - in this RSA with PKCS1Padding + // + gen.addSignerInfoGenerator(new JcaSimpleSignerInfoGeneratorBuilder().setProvider("BC").setSignedAttributeGenerator(new AttributeTable(signedAttrs)).build("SHA1withRSA", origKP.getPrivate(), origCert)); + + // + // add our pool of certs and cerls (if any) to go with the signature + // + gen.addCertificates(certs); + + // + // create the base for our message + // + MimeBodyPart msg = new MimeBodyPart(); + + msg.setText("Hello world!"); + + // + // extract the multipart object from the SMIMESigned object. + // + MimeMultipart mm = gen.generate(msg); + + // + // Get a Session object and create the mail message + // + Properties props = System.getProperties(); + Session session = Session.getDefaultInstance(props, null); + + Address fromUser = new InternetAddress("\"Eric H. Echidna\"<eric@bouncycastle.org>"); + Address toUser = new InternetAddress("example@bouncycastle.org"); + + MimeMessage body = new MimeMessage(session); + body.setFrom(fromUser); + body.setRecipient(Message.RecipientType.TO, toUser); + body.setSubject("example signed message"); + body.setContent(mm, mm.getContentType()); + body.saveChanges(); + + body.writeTo(new FileOutputStream("signed.message")); + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateSignedMultipartMail.java b/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateSignedMultipartMail.java new file mode 100644 index 00000000..20d1b3ea --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/examples/CreateSignedMultipartMail.java @@ -0,0 +1,213 @@ +package org.bouncycastle.mail.smime.examples; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Properties; + +import javax.mail.Address; +import javax.mail.Message; +import javax.mail.Session; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +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.x500.X500Name; +import org.bouncycastle.asn1.x509.X509Extension; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoGeneratorBuilder; +import org.bouncycastle.mail.smime.SMIMESignedGenerator; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.util.Store; + +/** + * a simple example that creates a single signed multipart mail message. + */ +public class CreateSignedMultipartMail +{ + // + // certificate serial number seed. + // + static int serialNo = 1; + + /** + * create a basic X509 certificate from the given keys + */ + static X509Certificate makeCertificate( + KeyPair subKP, + String subDN, + KeyPair issKP, + String issDN) + throws GeneralSecurityException, IOException, OperatorCreationException + { + PublicKey subPub = subKP.getPublic(); + PrivateKey issPriv = issKP.getPrivate(); + PublicKey issPub = issKP.getPublic(); + + JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils(); + X509v3CertificateBuilder v3CertGen = new JcaX509v3CertificateBuilder(new X500Name(issDN), BigInteger.valueOf(serialNo++), new Date(System.currentTimeMillis()), new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 100)), new X500Name(subDN), subPub); + + v3CertGen.addExtension( + X509Extension.subjectKeyIdentifier, + false, + extUtils.createSubjectKeyIdentifier(subPub)); + + v3CertGen.addExtension( + X509Extension.authorityKeyIdentifier, + false, + extUtils.createAuthorityKeyIdentifier(issPub)); + + return new JcaX509CertificateConverter().setProvider("BC").getCertificate(v3CertGen.build(new JcaContentSignerBuilder("MD5withRSA").setProvider("BC").build(issPriv))); + } + + public static void main( + String args[]) + throws Exception + { + // + // set up our certs + // + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "BC"); + + kpg.initialize(1024, new SecureRandom()); + + // + // cert that issued the signing certificate + // + String signDN = "O=Bouncy Castle, C=AU"; + KeyPair signKP = kpg.generateKeyPair(); + X509Certificate signCert = makeCertificate( + signKP, signDN, signKP, signDN); + + // + // cert we sign against + // + String origDN = "CN=Eric H. Echidna, E=eric@bouncycastle.org, O=Bouncy Castle, C=AU"; + KeyPair origKP = kpg.generateKeyPair(); + X509Certificate origCert = makeCertificate( + origKP, origDN, signKP, signDN); + + List certList = new ArrayList(); + + certList.add(origCert); + certList.add(signCert); + + // + // create a CertStore containing the certificates we want carried + // in the signature + // + Store certs = new JcaCertStore(certList); + + // + // create some smime capabilities in case someone wants to respond + // + ASN1EncodableVector signedAttrs = new ASN1EncodableVector(); + SMIMECapabilityVector caps = new SMIMECapabilityVector(); + + caps.addCapability(SMIMECapability.dES_EDE3_CBC); + caps.addCapability(SMIMECapability.rC2_CBC, 128); + caps.addCapability(SMIMECapability.dES_CBC); + + signedAttrs.add(new SMIMECapabilitiesAttribute(caps)); + + // + // add an encryption key preference for encrypted responses - + // normally this would be different from the signing certificate... + // + IssuerAndSerialNumber issAndSer = new IssuerAndSerialNumber( + new X500Name(signDN), origCert.getSerialNumber()); + + signedAttrs.add(new SMIMEEncryptionKeyPreferenceAttribute(issAndSer)); + + // + // create the generator for creating an smime/signed message + // + SMIMESignedGenerator gen = new SMIMESignedGenerator(); + + // + // add a signer to the generator - this specifies we are using SHA1 and + // adding the smime attributes above to the signed attributes that + // will be generated as part of the signature. The encryption algorithm + // used is taken from the key - in this RSA with PKCS1Padding + // + gen.addSignerInfoGenerator(new JcaSimpleSignerInfoGeneratorBuilder().setProvider("BC").setSignedAttributeGenerator(new AttributeTable(signedAttrs)).build("SHA1withRSA", origKP.getPrivate(), origCert)); + + // + // add our pool of certs and cerls (if any) to go with the signature + // + gen.addCertificates(certs); + + // + // create the base for our message + // + MimeBodyPart msg1 = new MimeBodyPart(); + + msg1.setText("Hello part 1!"); + + MimeBodyPart msg2 = new MimeBodyPart(); + + msg2.setText("Hello part 2!"); + + MimeMultipart mp = new MimeMultipart(); + + mp.addBodyPart(msg1); + mp.addBodyPart(msg2); + + MimeBodyPart m = new MimeBodyPart(); + + // + // be careful about setting extra headers here. Some mail clients + // ignore the To and From fields (for example) in the body part + // that contains the multipart. The result of this will be that the + // signature fails to verify... Outlook Express is an example of + // a client that exhibits this behaviour. + // + m.setContent(mp); + + // + // extract the multipart object from the SMIMESigned object. + // + MimeMultipart mm = gen.generate(m); + + // + // Get a Session object and create the mail message + // + Properties props = System.getProperties(); + Session session = Session.getDefaultInstance(props, null); + + Address fromUser = new InternetAddress("\"Eric H. Echidna\"<eric@bouncycastle.org>"); + Address toUser = new InternetAddress("example@bouncycastle.org"); + + MimeMessage body = new MimeMessage(session); + body.setFrom(fromUser); + body.setRecipient(Message.RecipientType.TO, toUser); + body.setSubject("example signed message"); + body.setContent(mm, mm.getContentType()); + body.saveChanges(); + + body.writeTo(new FileOutputStream("signed.message")); + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/examples/ExampleUtils.java b/mail/src/main/java/org/spongycastle/mail/smime/examples/ExampleUtils.java new file mode 100644 index 00000000..10c0f06c --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/examples/ExampleUtils.java @@ -0,0 +1,77 @@ +package org.bouncycastle.mail.smime.examples; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.KeyStore; +import java.util.Enumeration; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeBodyPart; + +public class ExampleUtils +{ + /** + * Dump the content of the passed in BodyPart to the file fileName. + * + * @throws MessagingException + * @throws IOException + */ + public static void dumpContent( + MimeBodyPart bodyPart, + String fileName) + throws MessagingException, IOException + { + // + // print mime type of compressed content + // + System.out.println("content type: " + bodyPart.getContentType()); + + // + // recover the compressed content + // + OutputStream out = new FileOutputStream(fileName); + InputStream in = bodyPart.getInputStream(); + + byte[] buf = new byte[10000]; + int len; + + while ((len = in.read(buf, 0, buf.length)) > 0) + { + out.write(buf, 0, len); + } + + out.close(); + } + + public static String findKeyAlias( + KeyStore store, + String storeName, + char[] password) + throws Exception + { + store.load(new FileInputStream(storeName), password); + + Enumeration e = store.aliases(); + String keyAlias = null; + + while (e.hasMoreElements()) + { + String alias = (String)e.nextElement(); + + if (store.isKeyEntry(alias)) + { + keyAlias = alias; + } + } + + if (keyAlias == null) + { + throw new IllegalArgumentException("can't find a private key in keyStore: " + storeName); + } + + return keyAlias; + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/examples/ReadCompressedMail.java b/mail/src/main/java/org/spongycastle/mail/smime/examples/ReadCompressedMail.java new file mode 100644 index 00000000..b462b336 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/examples/ReadCompressedMail.java @@ -0,0 +1,41 @@ +package org.bouncycastle.mail.smime.examples; + +import java.io.FileInputStream; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; + +import org.bouncycastle.cms.jcajce.ZlibExpanderProvider; +import org.bouncycastle.mail.smime.SMIMECompressed; +import org.bouncycastle.mail.smime.SMIMEUtil; + +/** + * a simple example that reads a compressed email. + * <p> + */ +public class ReadCompressedMail +{ + public static void main( + String args[]) + throws Exception + { + // + // Get a Session object with the default properties. + // + Properties props = System.getProperties(); + + Session session = Session.getDefaultInstance(props, null); + + MimeMessage msg = new MimeMessage(session, new FileInputStream("compressed.message")); + + SMIMECompressed m = new SMIMECompressed(msg); + + MimeBodyPart res = SMIMEUtil.toMimeBodyPart(m.getContent(new ZlibExpanderProvider())); + + System.out.println("Message Contents"); + System.out.println("----------------"); + System.out.println(res.getContent()); + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/examples/ReadEncryptedMail.java b/mail/src/main/java/org/spongycastle/mail/smime/examples/ReadEncryptedMail.java new file mode 100644 index 00000000..a180994f --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/examples/ReadEncryptedMail.java @@ -0,0 +1,94 @@ +package org.bouncycastle.mail.smime.examples; + +import java.io.FileInputStream; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Enumeration; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; + +import org.bouncycastle.cms.RecipientId; +import org.bouncycastle.cms.RecipientInformation; +import org.bouncycastle.cms.RecipientInformationStore; +import org.bouncycastle.cms.jcajce.JceKeyTransEnvelopedRecipient; +import org.bouncycastle.cms.jcajce.JceKeyTransRecipientId; +import org.bouncycastle.mail.smime.SMIMEEnveloped; +import org.bouncycastle.mail.smime.SMIMEUtil; + +/** + * a simple example that reads an encrypted email. + * <p> + * The key store can be created using the class in + * org.bouncycastle.jce.examples.PKCS12Example - the program expects only one + * key to be present. + */ +public class ReadEncryptedMail +{ + public static void main( + String args[]) + throws Exception + { + if (args.length != 2) + { + System.err.println("usage: ReadEncryptedMail pkcs12Keystore password"); + System.exit(0); + } + + // + // Open the key store + // + KeyStore ks = KeyStore.getInstance("PKCS12", "BC"); + + ks.load(new FileInputStream(args[0]), args[1].toCharArray()); + + Enumeration e = ks.aliases(); + String keyAlias = null; + + while (e.hasMoreElements()) + { + String alias = (String)e.nextElement(); + + if (ks.isKeyEntry(alias)) + { + keyAlias = alias; + } + } + + if (keyAlias == null) + { + System.err.println("can't find a private key!"); + System.exit(0); + } + + // + // find the certificate for the private key and generate a + // suitable recipient identifier. + // + X509Certificate cert = (X509Certificate)ks.getCertificate(keyAlias); + RecipientId recId = new JceKeyTransRecipientId(cert); + + // + // Get a Session object with the default properties. + // + Properties props = System.getProperties(); + + Session session = Session.getDefaultInstance(props, null); + + MimeMessage msg = new MimeMessage(session, new FileInputStream("encrypted.message")); + + SMIMEEnveloped m = new SMIMEEnveloped(msg); + + RecipientInformationStore recipients = m.getRecipientInfos(); + RecipientInformation recipient = recipients.get(recId); + + MimeBodyPart res = SMIMEUtil.toMimeBodyPart(recipient.getContent(new JceKeyTransEnvelopedRecipient((PrivateKey)ks.getKey(keyAlias, null)).setProvider("BC"))); + + System.out.println("Message Contents"); + System.out.println("----------------"); + System.out.println(res.getContent()); + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/examples/ReadLargeCompressedMail.java b/mail/src/main/java/org/spongycastle/mail/smime/examples/ReadLargeCompressedMail.java new file mode 100644 index 00000000..795d0497 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/examples/ReadLargeCompressedMail.java @@ -0,0 +1,38 @@ +package org.bouncycastle.mail.smime.examples; + +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; + +import org.bouncycastle.cms.jcajce.ZlibExpanderProvider; +import org.bouncycastle.mail.smime.SMIMECompressedParser; +import org.bouncycastle.mail.smime.SMIMEUtil; +import org.bouncycastle.mail.smime.util.SharedFileInputStream; + +/** + * a simple example that reads an oversize compressed email and writes data contained + * in the compressed part into a file. + */ +public class ReadLargeCompressedMail +{ + public static void main( + String args[]) + throws Exception + { + // + // Get a Session object with the default properties. + // + Properties props = System.getProperties(); + + Session session = Session.getDefaultInstance(props, null); + + MimeMessage msg = new MimeMessage(session, new SharedFileInputStream("compressed.message")); + + SMIMECompressedParser m = new SMIMECompressedParser(msg); + MimeBodyPart res = SMIMEUtil.toMimeBodyPart(m.getContent(new ZlibExpanderProvider())); + + ExampleUtils.dumpContent(res, args[0]); + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/examples/ReadLargeEncryptedMail.java b/mail/src/main/java/org/spongycastle/mail/smime/examples/ReadLargeEncryptedMail.java new file mode 100644 index 00000000..8389b443 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/examples/ReadLargeEncryptedMail.java @@ -0,0 +1,71 @@ +package org.bouncycastle.mail.smime.examples; + +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; + +import org.bouncycastle.cms.RecipientId; +import org.bouncycastle.cms.RecipientInformation; +import org.bouncycastle.cms.RecipientInformationStore; +import org.bouncycastle.cms.jcajce.JceKeyTransEnvelopedRecipient; +import org.bouncycastle.cms.jcajce.JceKeyTransRecipientId; +import org.bouncycastle.mail.smime.SMIMEEnvelopedParser; +import org.bouncycastle.mail.smime.SMIMEUtil; +import org.bouncycastle.mail.smime.util.SharedFileInputStream; + +/** + * a simple example that reads an encrypted email using the large file model. + * <p> + * The key store can be created using the class in + * org.bouncycastle.jce.examples.PKCS12Example - the program expects only one + * key to be present. + */ +public class ReadLargeEncryptedMail +{ + public static void main( + String args[]) + throws Exception + { + if (args.length != 3) + { + System.err.println("usage: ReadLargeEncryptedMail pkcs12Keystore password outputFile"); + System.exit(0); + } + + // + // Open the key store + // + KeyStore ks = KeyStore.getInstance("PKCS12", "BC"); + String keyAlias = ExampleUtils.findKeyAlias(ks, args[0], args[1].toCharArray()); + + // + // find the certificate for the private key and generate a + // suitable recipient identifier. + // + X509Certificate cert = (X509Certificate)ks.getCertificate(keyAlias); + RecipientId recId = new JceKeyTransRecipientId(cert); + + // + // Get a Session object with the default properties. + // + Properties props = System.getProperties(); + + Session session = Session.getDefaultInstance(props, null); + + MimeMessage msg = new MimeMessage(session, new SharedFileInputStream("encrypted.message")); + + SMIMEEnvelopedParser m = new SMIMEEnvelopedParser(msg); + + RecipientInformationStore recipients = m.getRecipientInfos(); + RecipientInformation recipient = recipients.get(recId); + + MimeBodyPart res = SMIMEUtil.toMimeBodyPart(recipient.getContentStream(new JceKeyTransEnvelopedRecipient((PrivateKey)ks.getKey(keyAlias, null)).setProvider("BC"))); + + ExampleUtils.dumpContent(res, args[2]); + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/examples/ReadLargeSignedMail.java b/mail/src/main/java/org/spongycastle/mail/smime/examples/ReadLargeSignedMail.java new file mode 100644 index 00000000..91074337 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/examples/ReadLargeSignedMail.java @@ -0,0 +1,125 @@ +package org.bouncycastle.mail.smime.examples; + +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Iterator; +import java.util.Properties; + +import javax.mail.Session; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; + +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.SignerInformationStore; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.mail.smime.SMIMESignedParser; +import org.bouncycastle.mail.smime.util.SharedFileInputStream; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import org.bouncycastle.util.Store; + +/** + * a simple example that reads a basic SMIME signed mail file. + */ +public class ReadLargeSignedMail +{ + private static final String BC = BouncyCastleProvider.PROVIDER_NAME; + + /** + * verify the signature (assuming the cert is contained in the message) + */ + private static void verify( + SMIMESignedParser s) + throws Exception + { + // + // extract the information to verify the signatures. + // + + // + // certificates and crls passed in the signature - this must happen before + // s.getSignerInfos() + // + Store certs = s.getCertificates(); + + // + // SignerInfo blocks which contain the signatures + // + SignerInformationStore signers = s.getSignerInfos(); + + Collection c = signers.getSigners(); + Iterator it = c.iterator(); + + // + // check each signer + // + while (it.hasNext()) + { + SignerInformation signer = (SignerInformation)it.next(); + Collection certCollection = certs.getMatches(signer.getSID()); + + Iterator certIt = certCollection.iterator(); + X509Certificate cert = new JcaX509CertificateConverter().setProvider(BC).getCertificate((X509CertificateHolder)certIt.next()); + + + // + // verify that the sig is correct and that it was generated + // when the certificate was current + // + if (signer.verify(new JcaSimpleSignerInfoVerifierBuilder().setProvider(BC).build(cert))) + { + System.out.println("signature verified"); + } + else + { + System.out.println("signature failed!"); + } + } + } + + public static void main( + String[] args) + throws Exception + { + // + // Get a Session object with the default properties. + // + Properties props = System.getProperties(); + + Session session = Session.getDefaultInstance(props, null); + + MimeMessage msg = new MimeMessage(session, new SharedFileInputStream("signed.message")); + + // + // make sure this was a multipart/signed message - there should be + // two parts as we have one part for the content that was signed and + // one part for the actual signature. + // + if (msg.isMimeType("multipart/signed")) + { + SMIMESignedParser s = new SMIMESignedParser(new JcaDigestCalculatorProviderBuilder().build(), + (MimeMultipart)msg.getContent()); + + System.out.println("Status:"); + + verify(s); + } + else if (msg.isMimeType("application/pkcs7-mime")) + { + // + // in this case the content is wrapped in the signature block. + // + SMIMESignedParser s = new SMIMESignedParser(new JcaDigestCalculatorProviderBuilder().build(), msg); + + System.out.println("Status:"); + + verify(s); + } + else + { + System.err.println("Not a signed message!"); + } + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/examples/ReadSignedMail.java b/mail/src/main/java/org/spongycastle/mail/smime/examples/ReadSignedMail.java new file mode 100644 index 00000000..370106d9 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/examples/ReadSignedMail.java @@ -0,0 +1,176 @@ +package org.bouncycastle.mail.smime.examples; + +import java.io.FileInputStream; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Iterator; +import java.util.Properties; + +import javax.mail.BodyPart; +import javax.mail.Multipart; +import javax.mail.Session; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; + +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.SignerInformationStore; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.mail.smime.SMIMESigned; +import org.bouncycastle.util.Store; + +/** + * a simple example that reads a basic SMIME signed mail file. + */ +public class ReadSignedMail +{ + private static final String BC = BouncyCastleProvider.PROVIDER_NAME; + + /** + * verify the signature (assuming the cert is contained in the message) + */ + private static void verify( + SMIMESigned s) + throws Exception + { + // + // extract the information to verify the signatures. + // + + // + // certificates and crls passed in the signature + // + Store certs = s.getCertificates(); + + // + // SignerInfo blocks which contain the signatures + // + SignerInformationStore signers = s.getSignerInfos(); + + Collection c = signers.getSigners(); + Iterator it = c.iterator(); + + // + // check each signer + // + while (it.hasNext()) + { + SignerInformation signer = (SignerInformation)it.next(); + Collection certCollection = certs.getMatches(signer.getSID()); + + Iterator certIt = certCollection.iterator(); + X509Certificate cert = new JcaX509CertificateConverter().setProvider(BC).getCertificate((X509CertificateHolder)certIt.next()); + + // + // verify that the sig is correct and that it was generated + // when the certificate was current + // + if (signer.verify(new JcaSimpleSignerInfoVerifierBuilder().setProvider(BC).build(cert))) + { + System.out.println("signature verified"); + } + else + { + System.out.println("signature failed!"); + } + } + } + + public static void main( + String[] args) + throws Exception + { + // + // Get a Session object with the default properties. + // + Properties props = System.getProperties(); + + Session session = Session.getDefaultInstance(props, null); + + MimeMessage msg = new MimeMessage(session, new FileInputStream("signed.message")); + + // + // make sure this was a multipart/signed message - there should be + // two parts as we have one part for the content that was signed and + // one part for the actual signature. + // + if (msg.isMimeType("multipart/signed")) + { + SMIMESigned s = new SMIMESigned( + (MimeMultipart)msg.getContent()); + + // + // extract the content + // + MimeBodyPart content = s.getContent(); + + System.out.println("Content:"); + + Object cont = content.getContent(); + + if (cont instanceof String) + { + System.out.println((String)cont); + } + else if (cont instanceof Multipart) + { + Multipart mp = (Multipart)cont; + int count = mp.getCount(); + for (int i = 0; i < count; i++) + { + BodyPart m = mp.getBodyPart(i); + Object part = m.getContent(); + + System.out.println("Part " + i); + System.out.println("---------------------------"); + + if (part instanceof String) + { + System.out.println((String)part); + } + else + { + System.out.println("can't print..."); + } + } + } + + System.out.println("Status:"); + + verify(s); + } + else if (msg.isMimeType("application/pkcs7-mime") + || msg.isMimeType("application/x-pkcs7-mime")) + { + // + // in this case the content is wrapped in the signature block. + // + SMIMESigned s = new SMIMESigned(msg); + + // + // extract the content + // + MimeBodyPart content = s.getContent(); + + System.out.println("Content:"); + + Object cont = content.getContent(); + + if (cont instanceof String) + { + System.out.println((String)cont); + } + + System.out.println("Status:"); + + verify(s); + } + else + { + System.err.println("Not a signed message!"); + } + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/examples/SendSignedAndEncryptedMail.java b/mail/src/main/java/org/spongycastle/mail/smime/examples/SendSignedAndEncryptedMail.java new file mode 100644 index 00000000..8861152e --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/examples/SendSignedAndEncryptedMail.java @@ -0,0 +1,192 @@ +package org.bouncycastle.mail.smime.examples; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Properties; + +import javax.activation.CommandMap; +import javax.activation.MailcapCommandMap; +import javax.mail.Message; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +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.x500.X500Name; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cms.CMSAlgorithm; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoGeneratorBuilder; +import org.bouncycastle.cms.jcajce.JceCMSContentEncryptorBuilder; +import org.bouncycastle.cms.jcajce.JceKeyTransRecipientInfoGenerator; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.mail.smime.SMIMEEnvelopedGenerator; +import org.bouncycastle.mail.smime.SMIMEException; +import org.bouncycastle.mail.smime.SMIMESignedGenerator; +import org.bouncycastle.util.Store; +import org.bouncycastle.util.Strings; + +/** + * Example that sends a signed and encrypted mail message. + */ +public class SendSignedAndEncryptedMail +{ + public static void main(String args[]) + { + if (args.length != 5) + { + System.err + .println("usage: SendSignedAndEncryptedMail <pkcs12Keystore> <password> <keyalias> <smtp server> <email address>"); + System.exit(0); + } + + try + { + MailcapCommandMap mailcap = (MailcapCommandMap)CommandMap + .getDefaultCommandMap(); + + mailcap + .addMailcap("application/pkcs7-signature;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_signature"); + mailcap + .addMailcap("application/pkcs7-mime;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_mime"); + mailcap + .addMailcap("application/x-pkcs7-signature;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_signature"); + mailcap + .addMailcap("application/x-pkcs7-mime;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_mime"); + mailcap + .addMailcap("multipart/signed;; x-java-content-handler=org.bouncycastle.mail.smime.handlers.multipart_signed"); + + CommandMap.setDefaultCommandMap(mailcap); + + /* Add BC */ + Security.addProvider(new BouncyCastleProvider()); + + /* Open the keystore */ + KeyStore keystore = KeyStore.getInstance("PKCS12", "BC"); + keystore.load(new FileInputStream(args[0]), args[1].toCharArray()); + Certificate[] chain = keystore.getCertificateChain(args[2]); + + /* Get the private key to sign the message with */ + PrivateKey privateKey = (PrivateKey)keystore.getKey(args[2], + args[1].toCharArray()); + if (privateKey == null) + { + throw new Exception("cannot find private key for alias: " + + args[2]); + } + + /* Create the message to sign and encrypt */ + Properties props = System.getProperties(); + props.put("mail.smtp.host", args[3]); + Session session = Session.getDefaultInstance(props, null); + + MimeMessage body = new MimeMessage(session); + body.setFrom(new InternetAddress(args[4])); + body.setRecipient(Message.RecipientType.TO, new InternetAddress( + args[4])); + body.setSubject("example encrypted message"); + body.setContent("example encrypted message", "text/plain"); + body.saveChanges(); + + /* 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 X500Name(((X509Certificate)chain[0]) + .getIssuerDN().getName()), + ((X509Certificate)chain[0]).getSerialNumber()))); + attributes.add(new SMIMECapabilitiesAttribute(capabilities)); + + SMIMESignedGenerator signer = new SMIMESignedGenerator(); + signer.addSignerInfoGenerator(new JcaSimpleSignerInfoGeneratorBuilder().setProvider("BC").setSignedAttributeGenerator(new AttributeTable(attributes)).build("DSA".equals(privateKey.getAlgorithm()) ? "SHA1withDSA" : "MD5withRSA", privateKey, (X509Certificate)chain[0])); + + + /* Add the list of certs to the generator */ + List certList = new ArrayList(); + certList.add(chain[0]); + Store certs = new JcaCertStore(certList); + signer.addCertificates(certs); + + /* Sign the message */ + MimeMultipart mm = signer.generate(body); + MimeMessage signedMessage = new MimeMessage(session); + + /* Set all original MIME headers in the signed message */ + Enumeration headers = body.getAllHeaderLines(); + while (headers.hasMoreElements()) + { + signedMessage.addHeaderLine((String)headers.nextElement()); + } + + /* Set the content of the signed message */ + signedMessage.setContent(mm); + signedMessage.saveChanges(); + + /* Create the encrypter */ + SMIMEEnvelopedGenerator encrypter = new SMIMEEnvelopedGenerator(); + encrypter.addRecipientInfoGenerator(new JceKeyTransRecipientInfoGenerator((X509Certificate)chain[0]).setProvider("BC")); + + /* Encrypt the message */ + MimeBodyPart encryptedPart = encrypter.generate(signedMessage, + new JceCMSContentEncryptorBuilder(CMSAlgorithm.RC2_CBC).setProvider("BC").build()); + + /* + * Create a new MimeMessage that contains the encrypted and signed + * content + */ + ByteArrayOutputStream out = new ByteArrayOutputStream(); + encryptedPart.writeTo(out); + + MimeMessage encryptedMessage = new MimeMessage(session, + new ByteArrayInputStream(out.toByteArray())); + + /* Set all original MIME headers in the encrypted message */ + headers = body.getAllHeaderLines(); + while (headers.hasMoreElements()) + { + String headerLine = (String)headers.nextElement(); + /* + * Make sure not to override any content-* headers from the + * original message + */ + if (!Strings.toLowerCase(headerLine).startsWith("content-")) + { + encryptedMessage.addHeaderLine(headerLine); + } + } + + Transport.send(encryptedMessage); + } + catch (SMIMEException ex) + { + ex.getUnderlyingException().printStackTrace(System.err); + ex.printStackTrace(System.err); + } + catch (Exception ex) + { + ex.printStackTrace(System.err); + } + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/examples/ValidateSignedMail.java b/mail/src/main/java/org/spongycastle/mail/smime/examples/ValidateSignedMail.java new file mode 100644 index 00000000..31961f1e --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/examples/ValidateSignedMail.java @@ -0,0 +1,352 @@ +package org.bouncycastle.mail.smime.examples; + +import java.io.FileInputStream; +import java.io.InputStream; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.CertStore; +import java.security.cert.CertificateFactory; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.PKIXParameters; +import java.security.cert.TrustAnchor; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; + +import javax.mail.Session; +import javax.mail.internet.MimeMessage; +import javax.security.auth.x500.X500Principal; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.x509.X509Extension; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.i18n.ErrorBundle; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.mail.smime.validator.SignedMailValidator; +import org.bouncycastle.x509.PKIXCertPathReviewer; +import org.bouncycastle.x509.extension.X509ExtensionUtil; + +/** + * An Example that reads a signed mail and validates its signature. Also + * validating the certificate path from the signers key to a trusted entity + */ +public class ValidateSignedMail +{ + + /* + * Use trusted certificates from $JAVA_HOME/lib/security/cacerts as + * trustanchors + */ + public static final boolean useCaCerts = false; + + public static void main(String[] args) throws Exception + { + + Security.addProvider(new BouncyCastleProvider()); + + // + // Get a Session object with the default properties. + // + Properties props = System.getProperties(); + + Session session = Session.getDefaultInstance(props, null); + + // read message + MimeMessage msg = new MimeMessage(session, new FileInputStream( + "signed.message")); + + // create PKIXparameters + PKIXParameters param; + + if (useCaCerts) + { + KeyStore caCerts = KeyStore.getInstance("JKS"); + String javaHome = System.getProperty("java.home"); + caCerts.load( + new FileInputStream(javaHome + "/lib/security/cacerts"), + "changeit".toCharArray()); + + param = new PKIXParameters(caCerts); + } + else + { + // load trustanchors from files (here we only load one) + Set trustanchors = new HashSet(); + TrustAnchor trust = getTrustAnchor("trustanchor"); + + // create a dummy trustanchor if we can not find any trustanchor. so + // we can still try to validate the message + if (trust == null) + { + System.out + .println("no trustanchor file found, using a dummy trustanchor"); + trust = getDummyTrustAnchor(); + } + trustanchors.add(trust); + + param = new PKIXParameters(trustanchors); + } + + // load one ore more crls from files (here we only load one crl) + List crls = new ArrayList(); + X509CRL crl = loadCRL("crl.file"); + if (crl != null) + { + crls.add(crl); + } + CertStore certStore = CertStore.getInstance("Collection", + new CollectionCertStoreParameters(crls), "BC"); + + // add crls and enable revocation checking + param.addCertStore(certStore); + param.setRevocationEnabled(true); + + // or disable revocation checking + // param.setRevocationEnabled(false); + + verifySignedMail(msg, param); + } + + public static final int TITLE = 0; + public static final int TEXT = 1; + public static final int SUMMARY = 2; + public static final int DETAIL = 3; + + static int dbgLvl = DETAIL; + + private static final String RESOURCE_NAME = "org.bouncycastle.mail.smime.validator.SignedMailValidatorMessages"; + + public static void verifySignedMail(MimeMessage msg, PKIXParameters param) + throws Exception + { + // set locale for the output + Locale loc = Locale.ENGLISH; + // Locale loc = Locale.GERMAN; + + // validate signatures + SignedMailValidator validator = new SignedMailValidator(msg, param); + + // iterate over all signatures and print results + Iterator it = validator.getSignerInformationStore().getSigners() + .iterator(); + while (it.hasNext()) + { + SignerInformation signer = (SignerInformation) it.next(); + SignedMailValidator.ValidationResult result = validator + .getValidationResult(signer); + if (result.isValidSignature()) + { + ErrorBundle errMsg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.sigValid"); + System.out.println(errMsg.getText(loc)); + } + else + { + ErrorBundle errMsg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.sigInvalid"); + System.out.println(errMsg.getText(loc)); + // print errors + System.out.println("Errors:"); + Iterator errorsIt = result.getErrors().iterator(); + while (errorsIt.hasNext()) + { + ErrorBundle errorMsg = (ErrorBundle) errorsIt.next(); + if (dbgLvl == DETAIL) + { + System.out.println("\t\t" + errorMsg.getDetail(loc)); + } + else + { + System.out.println("\t\t" + errorMsg.getText(loc)); + } + } + } + if (!result.getNotifications().isEmpty()) + { + System.out.println("Notifications:"); + Iterator notIt = result.getNotifications().iterator(); + while (notIt.hasNext()) + { + ErrorBundle notMsg = (ErrorBundle) notIt.next(); + if (dbgLvl == DETAIL) + { + System.out.println("\t\t" + notMsg.getDetail(loc)); + } + else + { + System.out.println("\t\t" + notMsg.getText(loc)); + } + } + } + PKIXCertPathReviewer review = result.getCertPathReview(); + if (review != null) + { + if (review.isValidCertPath()) + { + System.out.println("Certificate path valid"); + } + else + { + System.out.println("Certificate path invalid"); + } + + System.out.println("\nCertificate path validation results:"); + // global errors + System.out.println("Errors:"); + Iterator errorsIt = review.getErrors(-1).iterator(); + while (errorsIt.hasNext()) + { + ErrorBundle errorMsg = (ErrorBundle) errorsIt.next(); + if (dbgLvl == DETAIL) + { + System.out.println("\t\t" + errorMsg.getDetail(loc)); + } + else + { + System.out.println("\t\t" + errorMsg.getText(loc)); + } + } + + System.out.println("Notifications:"); + Iterator notificationsIt = review.getNotifications(-1) + .iterator(); + while (notificationsIt.hasNext()) + { + ErrorBundle noteMsg = (ErrorBundle) notificationsIt.next(); + System.out.println("\t" + noteMsg.getText(loc)); + } + + // per certificate errors and notifications + Iterator certIt = review.getCertPath().getCertificates() + .iterator(); + int i = 0; + while (certIt.hasNext()) + { + X509Certificate cert = (X509Certificate) certIt.next(); + System.out.println("\nCertificate " + i + "\n========"); + System.out.println("Issuer: " + + cert.getIssuerDN().getName()); + System.out.println("Subject: " + + cert.getSubjectDN().getName()); + + // errors + System.out.println("\tErrors:"); + errorsIt = review.getErrors(i).iterator(); + while (errorsIt.hasNext()) + { + ErrorBundle errorMsg = (ErrorBundle) errorsIt.next(); + if (dbgLvl == DETAIL) + { + System.out + .println("\t\t" + errorMsg.getDetail(loc)); + } + else + { + System.out.println("\t\t" + errorMsg.getText(loc)); + } + } + + // notifications + System.out.println("\tNotifications:"); + notificationsIt = review.getNotifications(i).iterator(); + while (notificationsIt.hasNext()) + { + ErrorBundle noteMsg = (ErrorBundle) notificationsIt + .next(); + if (dbgLvl == DETAIL) + { + System.out.println("\t\t" + noteMsg.getDetail(loc)); + } + else + { + System.out.println("\t\t" + noteMsg.getText(loc)); + } + } + + i++; + } + } + } + + } + + protected static TrustAnchor getTrustAnchor(String trustcert) + throws Exception + { + X509Certificate cert = loadCert(trustcert); + if (cert != null) + { + byte[] ncBytes = cert + .getExtensionValue(X509Extension.nameConstraints.getId()); + + if (ncBytes != null) + { + ASN1Encodable extValue = X509ExtensionUtil + .fromExtensionValue(ncBytes); + return new TrustAnchor(cert, extValue.toASN1Primitive().getEncoded(ASN1Encoding.DER)); + } + return new TrustAnchor(cert, null); + } + return null; + } + + protected static X509Certificate loadCert(String certfile) + { + X509Certificate cert = null; + try + { + InputStream in = new FileInputStream(certfile); + + CertificateFactory cf = CertificateFactory.getInstance("X.509", + "BC"); + cert = (X509Certificate) cf.generateCertificate(in); + } + catch (Exception e) + { + System.out.println("certfile \"" + certfile + + "\" not found - classpath is " + + System.getProperty("java.class.path")); + } + return cert; + } + + protected static X509CRL loadCRL(String crlfile) + { + X509CRL crl = null; + try + { + InputStream in = new FileInputStream(crlfile); + + CertificateFactory cf = CertificateFactory.getInstance("X.509", + "BC"); + crl = (X509CRL) cf.generateCRL(in); + } + catch (Exception e) + { + System.out.println("crlfile \"" + crlfile + + "\" not found - classpath is " + + System.getProperty("java.class.path")); + } + return crl; + } + + private static TrustAnchor getDummyTrustAnchor() throws Exception + { + X500Principal principal = new X500Principal("CN=Dummy Trust Anchor"); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "BC"); + kpg.initialize(1024, new SecureRandom()); + PublicKey trustPubKey = kpg.generateKeyPair().getPublic(); + return new TrustAnchor(principal, trustPubKey, null); + } + +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/handlers/PKCS7ContentHandler.java b/mail/src/main/java/org/spongycastle/mail/smime/handlers/PKCS7ContentHandler.java new file mode 100644 index 00000000..d3db7fd6 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/handlers/PKCS7ContentHandler.java @@ -0,0 +1,110 @@ +package org.bouncycastle.mail.smime.handlers; + +import java.awt.datatransfer.DataFlavor; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.activation.ActivationDataFlavor; +import javax.activation.DataContentHandler; +import javax.activation.DataSource; +import javax.mail.MessagingException; +import javax.mail.internet.MimeBodyPart; + +import org.bouncycastle.mail.smime.SMIMEStreamingProcessor; + +public class PKCS7ContentHandler + implements DataContentHandler +{ + private final ActivationDataFlavor _adf; + private final DataFlavor[] _dfs; + + PKCS7ContentHandler( + ActivationDataFlavor adf, + DataFlavor[] dfs) + { + _adf = adf; + _dfs = dfs; + } + + public Object getContent( + DataSource ds) + throws IOException + { + return ds.getInputStream(); + } + + public Object getTransferData( + DataFlavor df, + DataSource ds) + throws IOException + { + if (_adf.equals(df)) + { + return getContent(ds); + } + else + { + return null; + } + } + + public DataFlavor[] getTransferDataFlavors() + { + return _dfs; + } + + public void writeTo( + Object obj, + String mimeType, + OutputStream os) + throws IOException + { + if (obj instanceof MimeBodyPart) + { + try + { + ((MimeBodyPart)obj).writeTo(os); + } + catch (MessagingException ex) + { + throw new IOException(ex.getMessage()); + } + } + else if (obj instanceof byte[]) + { + os.write((byte[])obj); + } + else if (obj instanceof InputStream) + { + int b; + InputStream in = (InputStream)obj; + + if (!(in instanceof BufferedInputStream)) + { + in = new BufferedInputStream(in); + } + + while ((b = in.read()) >= 0) + { + os.write(b); + } + } + else if (obj instanceof SMIMEStreamingProcessor) + { + SMIMEStreamingProcessor processor = (SMIMEStreamingProcessor)obj; + + processor.write(os); + } + else + { + // TODO it would be even nicer if we could attach the object to the exception + // as well since in deeply nested messages, it is not always clear which + // part caused the problem. Thus I guess we would have to subclass the + // IOException + + throw new IOException("unknown object in writeTo " + obj); + } + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/handlers/multipart_signed.java b/mail/src/main/java/org/spongycastle/mail/smime/handlers/multipart_signed.java new file mode 100644 index 00000000..dd5ef193 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/handlers/multipart_signed.java @@ -0,0 +1,280 @@ +package org.bouncycastle.mail.smime.handlers; + +import org.bouncycastle.mail.smime.SMIMEStreamingProcessor; + +import javax.activation.ActivationDataFlavor; +import javax.activation.DataContentHandler; +import javax.activation.DataSource; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.internet.ContentType; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMultipart; +import java.awt.datatransfer.DataFlavor; +import java.io.BufferedInputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Enumeration; + +public class multipart_signed + implements DataContentHandler +{ + private static final ActivationDataFlavor ADF = new ActivationDataFlavor(MimeMultipart.class, "multipart/signed", "Multipart Signed"); + private static final DataFlavor[] DFS = new DataFlavor[] { ADF }; + + public Object getContent(DataSource ds) + throws IOException + { + try + { + return new MimeMultipart(ds); + } + catch (MessagingException ex) + { + return null; + } + } + + public Object getTransferData(DataFlavor df, DataSource ds) + throws IOException + { + if (ADF.equals(df)) + { + return getContent(ds); + } + else + { + return null; + } + } + + public DataFlavor[] getTransferDataFlavors() + { + return DFS; + } + + public void writeTo(Object obj, String _mimeType, OutputStream os) + throws IOException + { + + if (obj instanceof MimeMultipart) + { + try + { + outputBodyPart(os, obj); + } + catch (MessagingException ex) + { + throw new IOException(ex.getMessage()); + } + } + else if(obj instanceof byte[]) + { + os.write((byte[])obj); + } + else if (obj instanceof InputStream) + { + int b; + InputStream in = (InputStream)obj; + + if (!(in instanceof BufferedInputStream)) + { + in = new BufferedInputStream(in); + } + + while ((b = in.read()) >= 0) + { + os.write(b); + } + } + else if (obj instanceof SMIMEStreamingProcessor) + { + SMIMEStreamingProcessor processor = (SMIMEStreamingProcessor)obj; + + processor.write(os); + } + else + { + throw new IOException("unknown object in writeTo " + obj); + } + } + + /* + * Output the mulitpart as a collection of leaves to make sure preamble text is not included. + */ + private void outputBodyPart( + OutputStream out, + Object bodyPart) + throws MessagingException, IOException + { + if (bodyPart instanceof Multipart) + { + Multipart mp = (Multipart)bodyPart; + ContentType contentType = new ContentType(mp.getContentType()); + String boundary = "--" + contentType.getParameter("boundary"); + + LineOutputStream lOut = new LineOutputStream(out); + + for (int i = 0; i < mp.getCount(); i++) + { + lOut.writeln(boundary); + outputBodyPart(out, mp.getBodyPart(i)); + lOut.writeln(); // CRLF terminator + } + + lOut.writeln(boundary + "--"); + return; + } + + MimeBodyPart mimePart = (MimeBodyPart)bodyPart; + + if (mimePart.getContent() instanceof Multipart) + { + Multipart mp = (Multipart)mimePart.getContent(); + ContentType contentType = new ContentType(mp.getContentType()); + String boundary = "--" + contentType.getParameter("boundary"); + + LineOutputStream lOut = new LineOutputStream(out); + + Enumeration headers = mimePart.getAllHeaderLines(); + while (headers.hasMoreElements()) + { + lOut.writeln((String)headers.nextElement()); + } + + lOut.writeln(); // CRLF separator + + outputPreamble(lOut, mimePart, boundary); + + outputBodyPart(out, mp); + return; + } + + mimePart.writeTo(out); + } + + /** + * internal preamble is generally included in signatures, while this is technically wrong, + * if we find internal preamble we include it by default. + */ + static void outputPreamble(LineOutputStream lOut, MimeBodyPart part, String boundary) + throws MessagingException, IOException + { + InputStream in; + + try + { + in = part.getRawInputStream(); + } + catch (MessagingException e) + { + return; // no underlying content, rely on default generation + } + + String line; + + while ((line = readLine(in)) != null) + { + if (line.equals(boundary)) + { + break; + } + + lOut.writeln(line); + } + + in.close(); + + if (line == null) + { + throw new MessagingException("no boundary found"); + } + } + + /* + * read a line of input stripping of the tailing \r\n + */ + private static String readLine(InputStream in) + throws IOException + { + StringBuffer b = new StringBuffer(); + + int ch; + while ((ch = in.read()) >= 0 && ch != '\n') + { + if (ch != '\r') + { + b.append((char)ch); + } + } + + if (ch < 0) + { + return null; + } + + return b.toString(); + } + + private static class LineOutputStream extends FilterOutputStream + { + private static byte newline[]; + + public LineOutputStream(OutputStream outputstream) + { + super(outputstream); + } + + public void writeln(String s) + throws MessagingException + { + try + { + byte abyte0[] = getBytes(s); + super.out.write(abyte0); + super.out.write(newline); + } + catch(Exception exception) + { + throw new MessagingException("IOException", exception); + } + } + + public void writeln() + throws MessagingException + { + try + { + super.out.write(newline); + } + catch(Exception exception) + { + throw new MessagingException("IOException", exception); + } + } + + static + { + newline = new byte[2]; + newline[0] = 13; + newline[1] = 10; + } + + private static byte[] getBytes(String s) + { + char ac[] = s.toCharArray(); + int i = ac.length; + byte abyte0[] = new byte[i]; + int j = 0; + + while (j < i) + { + abyte0[j] = (byte)ac[j++]; + } + + return abyte0; + } + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/handlers/pkcs7_mime.java b/mail/src/main/java/org/spongycastle/mail/smime/handlers/pkcs7_mime.java new file mode 100644 index 00000000..abdf1251 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/handlers/pkcs7_mime.java @@ -0,0 +1,18 @@ +package org.bouncycastle.mail.smime.handlers; + +import java.awt.datatransfer.DataFlavor; + +import javax.activation.ActivationDataFlavor; +import javax.mail.internet.MimeBodyPart; + +public class pkcs7_mime + extends PKCS7ContentHandler +{ + private static final ActivationDataFlavor ADF = new ActivationDataFlavor(MimeBodyPart.class, "application/pkcs7-mime", "Encrypted Data"); + private static final DataFlavor[] DFS = new DataFlavor[] { ADF }; + + public pkcs7_mime() + { + super(ADF, DFS); + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/handlers/pkcs7_signature.java b/mail/src/main/java/org/spongycastle/mail/smime/handlers/pkcs7_signature.java new file mode 100644 index 00000000..0c669508 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/handlers/pkcs7_signature.java @@ -0,0 +1,18 @@ +package org.bouncycastle.mail.smime.handlers; + +import java.awt.datatransfer.DataFlavor; + +import javax.activation.ActivationDataFlavor; +import javax.mail.internet.MimeBodyPart; + +public class pkcs7_signature + extends PKCS7ContentHandler +{ + private static final ActivationDataFlavor ADF = new ActivationDataFlavor(MimeBodyPart.class, "application/pkcs7-signature", "Signature"); + private static final DataFlavor[] DFS = new DataFlavor[] { ADF }; + + public pkcs7_signature() + { + super(ADF, DFS); + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/handlers/x_pkcs7_mime.java b/mail/src/main/java/org/spongycastle/mail/smime/handlers/x_pkcs7_mime.java new file mode 100644 index 00000000..7e28f281 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/handlers/x_pkcs7_mime.java @@ -0,0 +1,18 @@ +package org.bouncycastle.mail.smime.handlers; + +import java.awt.datatransfer.DataFlavor; + +import javax.activation.ActivationDataFlavor; +import javax.mail.internet.MimeBodyPart; + +public class x_pkcs7_mime + extends PKCS7ContentHandler +{ + private static final ActivationDataFlavor ADF = new ActivationDataFlavor(MimeBodyPart.class, "application/x-pkcs7-mime", "Encrypted Data"); + private static final DataFlavor[] DFS = new DataFlavor[] { ADF }; + + public x_pkcs7_mime() + { + super(ADF, DFS); + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/handlers/x_pkcs7_signature.java b/mail/src/main/java/org/spongycastle/mail/smime/handlers/x_pkcs7_signature.java new file mode 100644 index 00000000..a58fd531 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/handlers/x_pkcs7_signature.java @@ -0,0 +1,90 @@ +package org.bouncycastle.mail.smime.handlers; + +import java.awt.datatransfer.DataFlavor; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.activation.ActivationDataFlavor; +import javax.activation.DataContentHandler; +import javax.activation.DataSource; +import javax.mail.MessagingException; +import javax.mail.internet.MimeBodyPart; + +public class x_pkcs7_signature + implements DataContentHandler +{ + + /* + * + * VARIABLES + * + */ + + private static final ActivationDataFlavor ADF; + private static final DataFlavor[] ADFs; + + static + { + ADF = new ActivationDataFlavor(MimeBodyPart.class, "application/x-pkcs7-signature", "Signature"); + ADFs = new DataFlavor[] { ADF }; + } + + public Object getContent(DataSource _ds) + throws IOException + { + return _ds.getInputStream(); + } + + public Object getTransferData(DataFlavor _df, DataSource _ds) + throws IOException + { + if (ADF.equals(_df)) + { + return getContent(_ds); + } + else + { + return null; + } + } + + public DataFlavor[] getTransferDataFlavors() + { + return ADFs; + } + + public void writeTo(Object _obj, String _mimeType, OutputStream _os) + throws IOException + { + if (_obj instanceof MimeBodyPart) + { + try + { + ((MimeBodyPart)_obj).writeTo(_os); + } + catch (MessagingException ex) + { + throw new IOException(ex.getMessage()); + } + } + else if (_obj instanceof byte[]) + { + _os.write((byte[])_obj); + } + else if (_obj instanceof InputStream) + { + int b; + InputStream in = (InputStream)_obj; + + while ((b = in.read()) >= 0) + { + _os.write(b); + } + } + else + { + throw new IOException("unknown object in writeTo " + _obj); + } + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/util/CRLFOutputStream.java b/mail/src/main/java/org/spongycastle/mail/smime/util/CRLFOutputStream.java new file mode 100644 index 00000000..b11583d0 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/util/CRLFOutputStream.java @@ -0,0 +1,67 @@ +package org.bouncycastle.mail.smime.util; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +public class CRLFOutputStream extends FilterOutputStream +{ + protected int lastb; + protected static byte newline[]; + + public CRLFOutputStream(OutputStream outputstream) + { + super(outputstream); + lastb = -1; + } + + public void write(int i) + throws IOException + { + if (i == '\r') + { + out.write(newline); + } + else if (i == '\n') + { + if (lastb != '\r') + { + out.write(newline); + } + } + else + { + out.write(i); + } + + lastb = i; + } + + public void write(byte[] buf) + throws IOException + { + this.write(buf, 0, buf.length); + } + + public void write(byte buf[], int off, int len) + throws IOException + { + for (int i = off; i != off + len; i++) + { + this.write(buf[i]); + } + } + + public void writeln() + throws IOException + { + super.out.write(newline); + } + + static + { + newline = new byte[2]; + newline[0] = '\r'; + newline[1] = '\n'; + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/util/FileBackedMimeBodyPart.java b/mail/src/main/java/org/spongycastle/mail/smime/util/FileBackedMimeBodyPart.java new file mode 100644 index 00000000..6bae91c9 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/util/FileBackedMimeBodyPart.java @@ -0,0 +1,162 @@ +package org.bouncycastle.mail.smime.util; + +import javax.mail.MessagingException; +import javax.mail.internet.InternetHeaders; +import javax.mail.internet.MimeBodyPart; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Enumeration; + +public class FileBackedMimeBodyPart + extends MimeBodyPart +{ + private static final int BUF_SIZE = 32760; + + private final File _file; + + /** + * Create a MimeBodyPart backed by the data in file. + * + * @param file file containing the body part. + * @throws MessagingException an exception occurs parsing file. + * @throws IOException an exception occurs accessing file. + */ + public FileBackedMimeBodyPart( + File file) + throws MessagingException, IOException + { + super(new SharedFileInputStream(file)); + + _file = file; + } + + /** + * Create a MimeBodyPart backed by file based on the headers and + * content data in content. + * + * @param content an inputstream containing the body part. + * @param file a handle to the backing file to use for storage. + * @throws MessagingException an exception occurs parsing the resulting body part in file. + * @throws IOException an exception occurs accessing file or content. + */ + public FileBackedMimeBodyPart( + InputStream content, + File file) + throws MessagingException, IOException + { + this(saveStreamToFile(content, file)); + } + + /** + * Create a MimeBodyPart backed by file, with the headers + * given in headers and body content taken from the stream body. + * + * @param headers headers for the body part. + * @param body internal content for the body part. + * @param file backing file to use. + * + * @throws MessagingException if the body part can't be produced. + * @throws IOException if there is an issue reading stream or writing to file. + */ + public FileBackedMimeBodyPart( + InternetHeaders headers, + InputStream body, + File file) + throws MessagingException, IOException + { + this(saveStreamToFile(headers, body, file)); + } + + public void writeTo( + OutputStream out) + throws IOException, MessagingException + { + if (!_file.exists()) + { + throw new IOException("file " + _file.getCanonicalPath() + " no longer exists."); + } + + super.writeTo(out); + } + + /** + * Close off the underlying shared streams and remove the backing file. + * + * @throws IOException if streams cannot be closed or the file cannot be deleted. + */ + public void dispose() + throws IOException + { + ((SharedFileInputStream)contentStream).getRoot().dispose(); + + if (_file.exists() && !_file.delete()) + { + throw new IOException("deletion of underlying file <" + _file.getCanonicalPath() + "> failed."); + } + } + + private static File saveStreamToFile(InputStream content, File tempFile) + throws IOException + { + saveContentToStream(new FileOutputStream(tempFile), content); + + return tempFile; + } + + private static File saveStreamToFile(InternetHeaders headers, InputStream content, File tempFile) + throws IOException + { + OutputStream out = new FileOutputStream(tempFile); + Enumeration en = headers.getAllHeaderLines(); + + while (en.hasMoreElements()) + { + writeHeader(out, (String)en.nextElement()); + } + + writeSeperator(out); + + saveContentToStream(out, content); + + return tempFile; + } + + + private static void writeHeader(OutputStream out, String header) + throws IOException + { + for (int i = 0; i != header.length(); i++) + { + out.write(header.charAt(i)); + } + + writeSeperator(out); + } + + private static void writeSeperator(OutputStream out) + throws IOException + { + out.write('\r'); + out.write('\n'); + } + + private static void saveContentToStream( + OutputStream out, + InputStream content) + throws IOException + { + byte[] buf = new byte[BUF_SIZE]; + int len; + + while ((len = content.read(buf, 0, buf.length)) > 0) + { + out.write(buf, 0, len); + } + + out.close(); + content.close(); + } + } diff --git a/mail/src/main/java/org/spongycastle/mail/smime/util/SharedFileInputStream.java b/mail/src/main/java/org/spongycastle/mail/smime/util/SharedFileInputStream.java new file mode 100644 index 00000000..97cfd9c0 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/util/SharedFileInputStream.java @@ -0,0 +1,241 @@ +package org.bouncycastle.mail.smime.util; + +import javax.mail.internet.SharedInputStream; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +public class SharedFileInputStream + extends FilterInputStream + implements SharedInputStream +{ + private final SharedFileInputStream _parent; + private final File _file; + private final long _start; + private final long _length; + + private long _position; + private long _markedPosition; + + private List _subStreams = new LinkedList(); + + public SharedFileInputStream( + String fileName) + throws IOException + { + this(new File(fileName)); + } + + public SharedFileInputStream( + File file) + throws IOException + { + this(file, 0, file.length()); + } + + private SharedFileInputStream( + File file, + long start, + long length) + throws IOException + { + super(new BufferedInputStream(new FileInputStream(file))); + + _parent = null; + _file = file; + _start = start; + _length = length; + + in.skip(start); + } + + private SharedFileInputStream( + SharedFileInputStream parent, + long start, + long length) + throws IOException + { + super(new BufferedInputStream(new FileInputStream(parent._file))); + + _parent = parent; + _file = parent._file; + _start = start; + _length = length; + + in.skip(start); + } + + public long getPosition() + { + return _position; + } + + public InputStream newStream(long start, long finish) + { + try + { + SharedFileInputStream stream; + + if (finish < 0) + { + if (_length > 0) + { + stream = new SharedFileInputStream(this, _start + start, _length - start); + } + else if (_length == 0) + { + stream = new SharedFileInputStream(this, _start + start, 0); + } + else + { + stream = new SharedFileInputStream(this, _start + start, -1); + } + } + else + { + stream = new SharedFileInputStream(this, _start + start, finish - start); + } + + _subStreams.add(stream); + + return stream; + } + catch (IOException e) + { + throw new IllegalStateException("unable to create shared stream: " + e); + } + } + + public int read( + byte[] buf) + throws IOException + { + return this.read(buf, 0, buf.length); + } + + public int read( + byte[] buf, + int off, + int len) + throws IOException + { + int count = 0; + + if (len == 0) + { + return 0; + } + + while (count < len) + { + int ch = this.read(); + + if (ch < 0) + { + break; + } + + buf[off + count] = (byte)ch; + count++; + } + + if (count == 0) + { + return -1; // EOF + } + + return count; + } + + public int read() + throws IOException + { + if (_position == _length) + { + return -1; + } + + _position++; + return in.read(); + } + + public boolean markSupported() + { + return true; + } + + public long skip(long n) + throws IOException + { + long count; + + for (count = 0; count != n; count++) + { + if (this.read() < 0) + { + break; + } + } + + return count; + } + + public void mark( + int readLimit) + { + _markedPosition = _position; + in.mark(readLimit); + } + + public void reset() + throws IOException + { + _position = _markedPosition; + in.reset(); + } + + /** + * Return the shared stream that represents the top most stream that + * this stream inherits from. + * @return the base of the shared stream tree. + */ + public SharedFileInputStream getRoot() + { + if (_parent != null) + { + return _parent.getRoot(); + } + + return this; + } + + /** + * Close of this stream and any substreams that have been created from it. + * @throws IOException on problem closing the main stream. + */ + public void dispose() + throws IOException + { + Iterator it = _subStreams.iterator(); + + while (it.hasNext()) + { + try + { + ((SharedFileInputStream)it.next()).dispose(); + } + catch (IOException e) + { + // ignore + } + } + + in.close(); + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/validator/SignedMailValidator.java b/mail/src/main/java/org/spongycastle/mail/smime/validator/SignedMailValidator.java new file mode 100644 index 00000000..01c1c514 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/validator/SignedMailValidator.java @@ -0,0 +1,957 @@ +package org.bouncycastle.mail.smime.validator; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.PublicKey; +import java.security.cert.CertPath; +import java.security.cert.CertStore; +import java.security.cert.CertStoreException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateFactory; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.PKIXParameters; +import java.security.cert.TrustAnchor; +import java.security.cert.X509CertSelector; +import java.security.cert.X509Certificate; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Vector; + +import javax.mail.Address; +import javax.mail.MessagingException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; + +import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1TaggedObject; +import org.bouncycastle.asn1.DERIA5String; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.cms.Attribute; +import org.bouncycastle.asn1.cms.AttributeTable; +import org.bouncycastle.asn1.cms.CMSAttributes; +import org.bouncycastle.asn1.cms.Time; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.X509Extensions; +import org.bouncycastle.cert.jcajce.JcaCertStoreBuilder; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.SignerInformationStore; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; +import org.bouncycastle.cms.jcajce.JcaX509CertSelectorConverter; +import org.bouncycastle.i18n.ErrorBundle; +import org.bouncycastle.i18n.filter.TrustedInput; +import org.bouncycastle.i18n.filter.UntrustedInput; +import org.bouncycastle.jce.PrincipalUtil; +import org.bouncycastle.jce.X509Principal; +import org.bouncycastle.mail.smime.SMIMESigned; +import org.bouncycastle.util.Integers; +import org.bouncycastle.x509.CertPathReviewerException; +import org.bouncycastle.x509.PKIXCertPathReviewer; + +public class SignedMailValidator +{ + private static final String RESOURCE_NAME = "org.bouncycastle.mail.smime.validator.SignedMailValidatorMessages"; + + private static final Class DEFAULT_CERT_PATH_REVIEWER = PKIXCertPathReviewer.class; + + private static final String EXT_KEY_USAGE = X509Extensions.ExtendedKeyUsage + .getId(); + + private static final String SUBJECT_ALTERNATIVE_NAME = X509Extensions.SubjectAlternativeName + .getId(); + + private static final int shortKeyLength = 512; + + // (365.25*30)*24*3600*1000 + private static final long THIRTY_YEARS_IN_MILLI_SEC = 21915l * 12l * 3600l * 1000l; + + private static final JcaX509CertSelectorConverter selectorConverter = new JcaX509CertSelectorConverter(); + + private CertStore certs; + + private SignerInformationStore signers; + + private Map results; + + private String[] fromAddresses; + + private Class certPathReviewerClass; + + /** + * Validates the signed {@link MimeMessage} message. The + * {@link PKIXParameters} from param are used for the certificate path + * validation. The actual PKIXParameters used for the certificate path + * validation is a copy of param with the followin changes: <br> - The + * validation date is changed to the signature time <br> - A CertStore with + * certificates and crls from the mail message is added to the CertStores.<br> + * <br> + * In <code>param</code> it's also possible to add additional CertStores + * with intermediate Certificates and/or CRLs which then are also used for + * the validation. + * + * @param message the signed MimeMessage + * @param param the parameters for the certificate path validation + * @throws SignedMailValidatorException if the message is no signed message or if an exception occurs + * reading the message + */ + public SignedMailValidator(MimeMessage message, PKIXParameters param) + throws SignedMailValidatorException + { + this(message, param, DEFAULT_CERT_PATH_REVIEWER); + } + + /** + * Validates the signed {@link MimeMessage} message. The + * {@link PKIXParameters} from param are used for the certificate path + * validation. The actual PKIXParameters used for the certificate path + * validation is a copy of param with the followin changes: <br> - The + * validation date is changed to the signature time <br> - A CertStore with + * certificates and crls from the mail message is added to the CertStores.<br> + * <br> + * In <code>param</code> it's also possible to add additional CertStores + * with intermediate Certificates and/or CRLs which then are also used for + * the validation. + * + * @param message the signed MimeMessage + * @param param the parameters for the certificate path validation + * @param certPathReviewerClass a subclass of {@link PKIXCertPathReviewer}. The SignedMailValidator + * uses objects of this type for the cert path vailidation. The class must + * have an empty constructor. + * @throws SignedMailValidatorException if the message is no signed message or if an exception occurs + * reading the message + * @throws IllegalArgumentException if the certPathReviewerClass is not a + * subclass of {@link PKIXCertPathReviewer} or objects of + * certPathReviewerClass can not be instantiated + */ + public SignedMailValidator(MimeMessage message, PKIXParameters param, Class certPathReviewerClass) + throws SignedMailValidatorException + { + this.certPathReviewerClass = certPathReviewerClass; + boolean isSubclass = DEFAULT_CERT_PATH_REVIEWER.isAssignableFrom(certPathReviewerClass); + if (!isSubclass) + { + throw new IllegalArgumentException("certPathReviewerClass is not a subclass of " + DEFAULT_CERT_PATH_REVIEWER.getName()); + } + + SMIMESigned s; + + try + { + // check if message is multipart signed + if (message.isMimeType("multipart/signed")) + { + MimeMultipart mimemp = (MimeMultipart)message.getContent(); + s = new SMIMESigned(mimemp); + } + else if (message.isMimeType("application/pkcs7-mime") + || message.isMimeType("application/x-pkcs7-mime")) + { + s = new SMIMESigned(message); + } + else + { + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.noSignedMessage"); + throw new SignedMailValidatorException(msg); + } + + // save certstore and signerInformationStore + certs = new JcaCertStoreBuilder().addCertificates(s.getCertificates()).addCRLs(s.getCRLs()).setProvider("BC").build(); + signers = s.getSignerInfos(); + + // save "from" addresses from message + Address[] froms = message.getFrom(); + InternetAddress sender = null; + try + { + if (message.getHeader("Sender") != null) + { + sender = new InternetAddress(message.getHeader("Sender")[0]); + } + } + catch (MessagingException ex) + { + //ignore garbage in Sender: header + } + + int fromsLength = (froms != null) ? froms.length : 0; + fromAddresses = new String[fromsLength + ((sender != null) ? 1 : 0)]; + for (int i = 0; i < froms.length; i++) + { + InternetAddress inetAddr = (InternetAddress)froms[i]; + fromAddresses[i] = inetAddr.getAddress(); + } + if (sender != null) + { + fromAddresses[froms.length] = sender.getAddress(); + } + + // initialize results + results = new HashMap(); + } + catch (Exception e) + { + if (e instanceof SignedMailValidatorException) + { + throw (SignedMailValidatorException)e; + } + // exception reading message + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.exceptionReadingMessage", + new Object[]{e.getMessage(), e, e.getClass().getName()}); + throw new SignedMailValidatorException(msg, e); + } + + // validate signatues + validateSignatures(param); + } + + protected void validateSignatures(PKIXParameters pkixParam) + { + PKIXParameters usedParameters = (PKIXParameters)pkixParam.clone(); + + // add crls and certs from mail + usedParameters.addCertStore(certs); + + Collection c = signers.getSigners(); + Iterator it = c.iterator(); + + // check each signer + while (it.hasNext()) + { + List errors = new ArrayList(); + List notifications = new ArrayList(); + + SignerInformation signer = (SignerInformation)it.next(); + // signer certificate + X509Certificate cert = null; + + try + { + Collection certCollection = findCerts(usedParameters + .getCertStores(), selectorConverter.getCertSelector(signer.getSID())); + + Iterator certIt = certCollection.iterator(); + if (certIt.hasNext()) + { + cert = (X509Certificate)certIt.next(); + } + } + catch (CertStoreException cse) + { + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.exceptionRetrievingSignerCert", + new Object[]{cse.getMessage(), cse, cse.getClass().getName()}); + errors.add(msg); + } + + if (cert != null) + { + // check signature + boolean validSignature = false; + try + { + validSignature = signer.verify(new JcaSimpleSignerInfoVerifierBuilder().setProvider("BC").build(cert.getPublicKey())); + if (!validSignature) + { + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.signatureNotVerified"); + errors.add(msg); + } + } + catch (Exception e) + { + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.exceptionVerifyingSignature", + new Object[]{e.getMessage(), e, e.getClass().getName()}); + errors.add(msg); + } + + // check signer certificate (mail address, key usage, etc) + checkSignerCert(cert, errors, notifications); + + // notify if a signed receip request is in the message + AttributeTable atab = signer.getSignedAttributes(); + if (atab != null) + { + Attribute attr = atab.get(PKCSObjectIdentifiers.id_aa_receiptRequest); + if (attr != null) + { + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.signedReceiptRequest"); + notifications.add(msg); + } + } + + // check certificate path + + // get signing time if possible, otherwise use current time as + // signing time + Date signTime = getSignatureTime(signer); + if (signTime == null) // no signing time was found + { + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.noSigningTime"); + errors.add(msg); + signTime = new Date(); + } + else + { + // check if certificate was valid at signing time + try + { + cert.checkValidity(signTime); + } + catch (CertificateExpiredException e) + { + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.certExpired", + new Object[]{new TrustedInput(signTime), new TrustedInput(cert.getNotAfter())}); + errors.add(msg); + } + catch (CertificateNotYetValidException e) + { + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.certNotYetValid", + new Object[]{new TrustedInput(signTime), new TrustedInput(cert.getNotBefore())}); + errors.add(msg); + } + } + usedParameters.setDate(signTime); + + try + { + // construct cert chain + CertPath certPath; + List userProvidedList; + + List userCertStores = new ArrayList(); + userCertStores.add(certs); + Object[] cpres = createCertPath(cert, usedParameters.getTrustAnchors(), pkixParam.getCertStores(), userCertStores); + certPath = (CertPath)cpres[0]; + userProvidedList = (List)cpres[1]; + + // validate cert chain + PKIXCertPathReviewer review; + try + { + review = (PKIXCertPathReviewer)certPathReviewerClass.newInstance(); + } + catch (IllegalAccessException e) + { + throw new IllegalArgumentException("Cannot instantiate object of type " + + certPathReviewerClass.getName() + ": " + e.getMessage()); + } + catch (InstantiationException e) + { + throw new IllegalArgumentException("Cannot instantiate object of type " + + certPathReviewerClass.getName() + ": " + e.getMessage()); + } + review.init(certPath, usedParameters); + if (!review.isValidCertPath()) + { + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.certPathInvalid"); + errors.add(msg); + } + results.put(signer, new ValidationResult(review, + validSignature, errors, notifications, userProvidedList)); + } + catch (GeneralSecurityException gse) + { + // cannot create cert path + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.exceptionCreateCertPath", + new Object[]{gse.getMessage(), gse, gse.getClass().getName()}); + errors.add(msg); + results.put(signer, new ValidationResult(null, + validSignature, errors, notifications, null)); + } + catch (CertPathReviewerException cpre) + { + // cannot initialize certpathreviewer - wrong parameters + errors.add(cpre.getErrorMessage()); + results.put(signer, new ValidationResult(null, + validSignature, errors, notifications, null)); + } + } + else + // no signer certificate found + { + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.noSignerCert"); + errors.add(msg); + results.put(signer, new ValidationResult(null, false, errors, + notifications, null)); + } + } + } + + public static Set getEmailAddresses(X509Certificate cert) + throws IOException, CertificateEncodingException + { + Set addresses = new HashSet(); + + X509Principal name = PrincipalUtil.getSubjectX509Principal(cert); + Vector oids = name.getOIDs(); + Vector names = name.getValues(); + for (int i = 0; i < oids.size(); i++) + { + if (oids.get(i).equals(X509Principal.EmailAddress)) + { + String email = ((String)names.get(i)).toLowerCase(); + addresses.add(email); + break; + } + } + + byte[] ext = cert.getExtensionValue(SUBJECT_ALTERNATIVE_NAME); + if (ext != null) + { + ASN1Sequence altNames = ASN1Sequence.getInstance(getObject(ext)); + for (int j = 0; j < altNames.size(); j++) + { + ASN1TaggedObject o = (ASN1TaggedObject)altNames + .getObjectAt(j); + + if (o.getTagNo() == 1) + { + String email = DERIA5String.getInstance(o, false) + .getString().toLowerCase(); + addresses.add(email); + } + } + } + + return addresses; + } + + private static ASN1Primitive getObject(byte[] ext) + throws IOException + { + ASN1InputStream aIn = new ASN1InputStream(ext); + ASN1OctetString octs = (ASN1OctetString)aIn.readObject(); + + aIn = new ASN1InputStream(octs.getOctets()); + return aIn.readObject(); + } + + protected void checkSignerCert(X509Certificate cert, List errors, + List notifications) + { + // get key length + PublicKey key = cert.getPublicKey(); + int keyLenght = -1; + if (key instanceof RSAPublicKey) + { + keyLenght = ((RSAPublicKey)key).getModulus().bitLength(); + } + else if (key instanceof DSAPublicKey) + { + keyLenght = ((DSAPublicKey)key).getParams().getP().bitLength(); + } + if (keyLenght != -1 && keyLenght <= shortKeyLength) + { + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.shortSigningKey", + new Object[]{Integers.valueOf(keyLenght)}); + notifications.add(msg); + } + + // warn if certificate has very long validity period + long validityPeriod = cert.getNotAfter().getTime() - cert.getNotBefore().getTime(); + if (validityPeriod > THIRTY_YEARS_IN_MILLI_SEC) + { + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.longValidity", + new Object[]{new TrustedInput(cert.getNotBefore()), new TrustedInput(cert.getNotAfter())}); + notifications.add(msg); + } + + // check key usage if digitalSignature or nonRepudiation is set + boolean[] keyUsage = cert.getKeyUsage(); + if (keyUsage != null && !keyUsage[0] && !keyUsage[1]) + { + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.signingNotPermitted"); + errors.add(msg); + } + + // check extended key usage + try + { + byte[] ext = cert.getExtensionValue(EXT_KEY_USAGE); + if (ext != null) + { + ExtendedKeyUsage extKeyUsage = ExtendedKeyUsage + .getInstance(getObject(ext)); + if (!extKeyUsage + .hasKeyPurposeId(KeyPurposeId.anyExtendedKeyUsage) + && !extKeyUsage + .hasKeyPurposeId(KeyPurposeId.id_kp_emailProtection)) + { + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.extKeyUsageNotPermitted"); + errors.add(msg); + } + } + } + catch (Exception e) + { + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.extKeyUsageError", new Object[]{ + e.getMessage(), e, e.getClass().getName()} + ); + errors.add(msg); + } + + // cert has an email address + try + { + Set certEmails = getEmailAddresses(cert); + if (certEmails.isEmpty()) + { + // error no email address in signing certificate + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.noEmailInCert"); + errors.add(msg); + } + else + { + // check if email in cert is equal to the from address in the + // message + boolean equalsFrom = false; + for (int i = 0; i < fromAddresses.length; i++) + { + if (certEmails.contains(fromAddresses[i].toLowerCase())) + { + equalsFrom = true; + break; + } + } + if (!equalsFrom) + { + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.emailFromCertMismatch", + new Object[]{ + new UntrustedInput( + addressesToString(fromAddresses)), + new UntrustedInput(certEmails)} + ); + errors.add(msg); + } + } + } + catch (Exception e) + { + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.certGetEmailError", new Object[]{ + e.getMessage(), e, e.getClass().getName()} + ); + errors.add(msg); + } + } + + static String addressesToString(Object[] a) + { + if (a == null) + { + return "null"; + } + + StringBuffer b = new StringBuffer(); + b.append('['); + + for (int i = 0; i != a.length; i++) + { + if (i > 0) + { + b.append(", "); + } + b.append(String.valueOf(a[i])); + } + + return b.append(']').toString(); + } + + public static Date getSignatureTime(SignerInformation signer) + { + AttributeTable atab = signer.getSignedAttributes(); + Date result = null; + if (atab != null) + { + Attribute attr = atab.get(CMSAttributes.signingTime); + if (attr != null) + { + Time t = Time.getInstance(attr.getAttrValues().getObjectAt(0) + .toASN1Primitive()); + result = t.getDate(); + } + } + return result; + } + + private static List findCerts(List certStores, X509CertSelector selector) + throws CertStoreException + { + List result = new ArrayList(); + Iterator it = certStores.iterator(); + while (it.hasNext()) + { + CertStore store = (CertStore)it.next(); + Collection coll = store.getCertificates(selector); + result.addAll(coll); + } + return result; + } + + private static X509Certificate findNextCert(List certStores, X509CertSelector selector, Set certSet) + throws CertStoreException + { + Iterator certIt = findCerts(certStores, selector).iterator(); + + boolean certFound = false; + X509Certificate nextCert = null; + while (certIt.hasNext()) + { + nextCert = (X509Certificate)certIt.next(); + if (!certSet.contains(nextCert)) + { + certFound = true; + break; + } + } + + return certFound ? nextCert : null; + } + + /** + * @param signerCert the end of the path + * @param trustanchors trust anchors for the path + * @param certStores + * @return the resulting certificate path. + * @throws GeneralSecurityException + */ + public static CertPath createCertPath(X509Certificate signerCert, + Set trustanchors, List certStores) + throws GeneralSecurityException + { + Object[] results = createCertPath(signerCert, trustanchors, certStores, null); + return (CertPath)results[0]; + } + + /** + * Returns an Object array containing a CertPath and a List of Booleans. The list contains the value <code>true</code> + * if the corresponding certificate in the CertPath was taken from the user provided CertStores. + * + * @param signerCert the end of the path + * @param trustanchors trust anchors for the path + * @param systemCertStores list of {@link CertStore} provided by the system + * @param userCertStores list of {@link CertStore} provided by the user + * @return a CertPath and a List of booleans. + * @throws GeneralSecurityException + */ + public static Object[] createCertPath(X509Certificate signerCert, + Set trustanchors, List systemCertStores, List userCertStores) + throws GeneralSecurityException + { + Set certSet = new LinkedHashSet(); + List userProvidedList = new ArrayList(); + + // add signer certificate + + X509Certificate cert = signerCert; + certSet.add(cert); + userProvidedList.add(new Boolean(true)); + + boolean trustAnchorFound = false; + + X509Certificate taCert = null; + + // add other certs to the cert path + while (cert != null && !trustAnchorFound) + { + // check if cert Issuer is Trustanchor + Iterator trustIt = trustanchors.iterator(); + while (trustIt.hasNext()) + { + TrustAnchor anchor = (TrustAnchor)trustIt.next(); + X509Certificate anchorCert = anchor.getTrustedCert(); + if (anchorCert != null) + { + if (anchorCert.getSubjectX500Principal().equals( + cert.getIssuerX500Principal())) + { + try + { + cert.verify(anchorCert.getPublicKey(), "BC"); + trustAnchorFound = true; + taCert = anchorCert; + break; + } + catch (Exception e) + { + // trustanchor not found + } + } + } + else + { + if (anchor.getCAName().equals( + cert.getIssuerX500Principal().getName())) + { + try + { + cert.verify(anchor.getCAPublicKey(), "BC"); + trustAnchorFound = true; + break; + } + catch (Exception e) + { + // trustanchor not found + } + } + } + } + + if (!trustAnchorFound) + { + // add next cert to path + X509CertSelector select = new X509CertSelector(); + try + { + select.setSubject(cert.getIssuerX500Principal().getEncoded()); + } + catch (IOException e) + { + throw new IllegalStateException(e.toString()); + } + byte[] authKeyIdentBytes = cert.getExtensionValue(X509Extensions.AuthorityKeyIdentifier.getId()); + if (authKeyIdentBytes != null) + { + try + { + AuthorityKeyIdentifier kid = AuthorityKeyIdentifier.getInstance(getObject(authKeyIdentBytes)); + if (kid.getKeyIdentifier() != null) + { + select.setSubjectKeyIdentifier(new DEROctetString(kid.getKeyIdentifier()).getEncoded(ASN1Encoding.DER)); + } + } + catch (IOException ioe) + { + // ignore + } + } + boolean userProvided = false; + + cert = findNextCert(systemCertStores, select, certSet); + if (cert == null && userCertStores != null) + { + userProvided = true; + cert = findNextCert(userCertStores, select, certSet); + } + + if (cert != null) + { + // cert found + certSet.add(cert); + userProvidedList.add(new Boolean(userProvided)); + } + } + } + + // if a trustanchor was found - try to find a selfsigned certificate of + // the trustanchor + if (trustAnchorFound) + { + if (taCert != null && taCert.getSubjectX500Principal().equals(taCert.getIssuerX500Principal())) + { + certSet.add(taCert); + userProvidedList.add(new Boolean(false)); + } + else + { + X509CertSelector select = new X509CertSelector(); + + try + { + select.setSubject(cert.getIssuerX500Principal().getEncoded()); + select.setIssuer(cert.getIssuerX500Principal().getEncoded()); + } + catch (IOException e) + { + throw new IllegalStateException(e.toString()); + } + + boolean userProvided = false; + + taCert = findNextCert(systemCertStores, select, certSet); + if (taCert == null && userCertStores != null) + { + userProvided = true; + taCert = findNextCert(userCertStores, select, certSet); + } + if (taCert != null) + { + try + { + cert.verify(taCert.getPublicKey(), "BC"); + certSet.add(taCert); + userProvidedList.add(new Boolean(userProvided)); + } + catch (GeneralSecurityException gse) + { + // wrong cert + } + } + } + } + + CertPath certPath = CertificateFactory.getInstance("X.509", "BC").generateCertPath(new ArrayList(certSet)); + return new Object[]{certPath, userProvidedList}; + } + + public CertStore getCertsAndCRLs() + { + return certs; + } + + public SignerInformationStore getSignerInformationStore() + { + return signers; + } + + public ValidationResult getValidationResult(SignerInformation signer) + throws SignedMailValidatorException + { + if (signers.getSigners(signer.getSID()).isEmpty()) + { + // the signer is not part of the SignerInformationStore + // he has not signed the message + ErrorBundle msg = new ErrorBundle(RESOURCE_NAME, + "SignedMailValidator.wrongSigner"); + throw new SignedMailValidatorException(msg); + } + else + { + return (ValidationResult)results.get(signer); + } + } + + public class ValidationResult + { + + private PKIXCertPathReviewer review; + + private List errors; + + private List notifications; + + private List userProvidedCerts; + + private boolean signVerified; + + ValidationResult(PKIXCertPathReviewer review, boolean verified, + List errors, List notifications, List userProvidedCerts) + { + this.review = review; + this.errors = errors; + this.notifications = notifications; + signVerified = verified; + this.userProvidedCerts = userProvidedCerts; + } + + /** + * Returns a list of error messages of type {@link ErrorBundle}. + * + * @return List of error messages + */ + public List getErrors() + { + return errors; + } + + /** + * Returns a list of notification messages of type {@link ErrorBundle}. + * + * @return List of notification messages + */ + public List getNotifications() + { + return notifications; + } + + /** + * @return the PKIXCertPathReviewer for the CertPath of this signature + * or null if an Exception occured. + */ + public PKIXCertPathReviewer getCertPathReview() + { + return review; + } + + /** + * @return the CertPath for this signature + * or null if an Exception occured. + */ + public CertPath getCertPath() + { + return review != null ? review.getCertPath() : null; + } + + /** + * @return a List of Booleans that are true if the corresponding certificate in the CertPath was taken from + * the CertStore of the SMIME message + */ + public List getUserProvidedCerts() + { + return userProvidedCerts; + } + + /** + * @return true if the signature corresponds to the public key of the + * signer + */ + public boolean isVerifiedSignature() + { + return signVerified; + } + + /** + * @return true if the signature is valid (ie. if it corresponds to the + * public key of the signer and the cert path for the signers + * certificate is also valid) + */ + public boolean isValidSignature() + { + if (review != null) + { + return signVerified && review.isValidCertPath() + && errors.isEmpty(); + } + else + { + return false; + } + } + + } +} diff --git a/mail/src/main/java/org/spongycastle/mail/smime/validator/SignedMailValidatorException.java b/mail/src/main/java/org/spongycastle/mail/smime/validator/SignedMailValidatorException.java new file mode 100644 index 00000000..06f14618 --- /dev/null +++ b/mail/src/main/java/org/spongycastle/mail/smime/validator/SignedMailValidatorException.java @@ -0,0 +1,19 @@ +package org.bouncycastle.mail.smime.validator; + +import org.bouncycastle.i18n.ErrorBundle; +import org.bouncycastle.i18n.LocalizedException; + +public class SignedMailValidatorException extends LocalizedException +{ + + public SignedMailValidatorException(ErrorBundle errorMessage, Throwable throwable) + { + super(errorMessage, throwable); + } + + public SignedMailValidatorException(ErrorBundle errorMessage) + { + super(errorMessage); + } + +} |