Monday, March 31, 2008

Secure JavaMail with JSSE

Java Tip 115: Secure JavaMail with JSSE

Add secure, SSL-based connections to JavaMail

The Java Secure Socket Extension (JSSE) API provides SSL functionality that you can add to any JavaMail storage provider. Before we get started, you must download and install JavaMail, JSSE, and all the required packages as described in the JSSE and JavaMail documentation on Sun's Website (most required downloads are included in Java 2 Standard Edition 1.4), as well as JavaMail and JSSE providers of your choice.

Before the JSSE framework can recognize any JSSE providers, you must register them permanently in the <java home>\jre\lib\security\java.security properties file or register them dynamically by calling the Security.addProvider() method in your code:

  Security.addProvider( new com.sun.net.ssl.internal.ssl.Provider());


Then you need to replace JavaMail's default socket factory with JSSE's SSL socket factory. This approach is similar to the one used in "Java Tip 96: Use HTTPS in Your Java Client Code"; however, in our case, we have no control over socket creation since sockets are created inside the JavaMail framework, not in the application code or storage provider. Fortunately, JavaMail accepts several undocumented properties, allowing you to set up a custom socket factory class and some other parameters. Those properties are:

  mail.<protocol>.socketFactory.class
mail.<protocol>.socketFactory.fallback
mail.<protocol>.socketFactory.port
mail.<protocol>.timeout


Now you can use the following code to replace a socket factory:

  final String SSL_FACTORY = "javax.net.ssl.SSLSocketFactory";
Properties props = System.getProperties();
// IMAP provider
props.setProperty( "mail.imap.socketFactory.class", SSL_FACTORY);
// POP3 provider
props.setProperty( "mail.pop3.socketFactory.class", SSL_FACTORY);
// NNTP provider (if any)
// props.setProperty( "mail.nntp.socketFactory.class", SSL_FACTORY);


If you want JavaMail to use only secure connections, you configure the appropriate properties so JavaMail doesn't fall back to an unsecure connection when a secure one fails:

  // IMAP provider
props.setProperty( "mail.imap.socketFactory.fallback", "false");
// POP3 provider
props.setProperty( "mail.pop3.socketFactory.fallback", "false");
// NNTP provider (if any)
// props.setProperty( "mail.nntp.socketFactory.fallback", "false");


You then change the default port number to the corresponding port that your protocol's secure version uses; otherwise, you must use a fully qualified address (that includes a port number) in the URL passed to JavaMail (for example, imap://id:password@your.imap.server.com:993/folder/), or else you get an "unrecognized SSL handshake" exception. You specify these properties like so:

  // IMAP provider
props.setProperty( "mail.imap.port", "993");
props.setProperty( "mail.imap.socketFactory.port", "993");
// POP3 provider
props.setProperty( "mail.pop3.port", "995");
props.setProperty( "mail.pop3.socketFactory.port", "995");
// NNTP provider (if any)
// props.setProperty( "mail.pop3.port", "563");
// props.setProperty( "mail.pop3.socketFactory.port", "563");


After setting up all the properties, you can open a secure JavaMail session:

  Session session = Session.getInstance(props);


Certificates

Unfortunately, you may realize that the code above throws an SSLException "untrusted server cert chain" if the mail server certificate is not installed locally. In that case, you should obtain a correct, valid certificate for the server and use the keytool utility to add it to a local key storage at <javahome>\jre\lib\security\cacerts.

Alternatively, you can replace the default TrustManager. However, since you can't control socket creation and you want your TrustManager to work, you must use your own socket factory with SSLSocketFactory, which creates secure sockets.

Our simple example TrustManager implementation, DummyTrustManager.java, accepts all certificates without validation, even if the certificate name differs from the server name. This could be a security issue you may want to address in your application. For example, you can show a nice GUI dialog asking if the user trusts the certificate, and then store the certificate locally (you've probably seen these dialogs in Netscape Navigator or MS Internet Explorer).

Here is the code:

  import com.sun.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
public class DummyTrustManager implements X509TrustManager {
public boolean isClientTrusted( X509Certificate[] cert) {
return true;
}
public boolean isServerTrusted( X509Certificate[] cert) {
return true;
}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[ 0];
}
}


You also need to implement a custom SSLSocketFactory, like DummySSLSocketFactor.java. We will use the Proxy pattern to change the TrustManager of a standard SSLSocketFactory:

  import com.sun.net.ssl.*;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
public class DummySSLSocketFactory extends SSLSocketFactory {
private SSLSocketFactory factory;
public DummySSLSocketFactory() {
System.out.println( "DummySocketFactory instantiated");
try {
SSLContext sslcontext = SSLContext.getInstance( "TLS");
sslcontext.init( null, // No KeyManager required
new TrustManager[] { new DummyTrustManager()},
new java.security.SecureRandom());
factory = ( SSLSocketFactory) sslcontext.getSocketFactory();
} catch( Exception ex) {
ex.printStackTrace();
}
}
public static SocketFactory getDefault() {
return new DummySSLSocketFactory();
}
public Socket createSocket( Socket socket, String s, int i, boolean
flag)
throws IOException {
return factory.createSocket( socket, s, i, flag);
}
public Socket createSocket( InetAddress inaddr, int i,
InetAddress inaddr1, int j) throws IOException {
return factory.createSocket( inaddr, i, inaddr1, j);
}
public Socket createSocket( InetAddress inaddr, int i) throws
IOException {
return factory.createSocket( inaddr, i);
}
public Socket createSocket( String s, int i, InetAddress inaddr, int j)
throws IOException {
return factory.createSocket( s, i, inaddr, j);
}
public Socket createSocket( String s, int i) throws IOException {
return factory.createSocket( s, i);
}
public String[] getDefaultCipherSuites() {
return factory.getSupportedCipherSuites();
}
public String[] getSupportedCipherSuites() {
return factory.getSupportedCipherSuites();
}
}


Finally you should install the SocketFactory provider like this:

  Security.setProperty( "ssl.SocketFactory.provider", 
"DummySSLSocketFactory");


You should execute this code before creating any JavaMail sessions. Also, don't forget to make the DummySSLSocketFactory class visible to your code.

Joining technologies

We've created a sample application, called FolderList.java, based on the JavaMail mail folder listing demo, which uses the above code. It shows that you can securely connect to the mail server using JSSE by configuring the JavaMail framework properly. With this approach, you're ready to make your JavaMail-based application support secure communication without any significant changes in your own code.

No comments:

Google+