Java Nio SSL Example
This is an example of a non-blocking I/O provided by java.nio
using SSL handshake.
1. Definition of Secure Sockets Layer Protocol (SSL)
SSL is the secure communication protocol of choice for a large part of the Internet community. There are many applications of SSL in existence, since it is capable of securing any transmission over TCP. Secure HTTP, or HTTPS, is a familiar application of SSL in e-commerce or password transactions. Along with this popularity comes demands to use it with different I/O and threading models in order to satisfy the applications’ performance, scalability, footprint, and other requirements. There are demands to use it with blocking and non-blocking I/O channels, asynchronous I/O, input and output streams and byte buffers.The main point of the protocol is to provide privacy and reliability between two communicating applications. The following fundamental characteristics provide connection security:
- Privacy – connection using encryption
- Identity authentication – identification using certificates
- Reliability – dependable maintenance of a secure connection through
message integrity
Many developers may be wondering how to use SSL with Java NIO. With the traditional blocking sockets API, security is a simple issue: just set up an SSLContext
instance with the appropriate key material, use it to create instances of SSLSocketFactory
or SSLServerSocketFactory
and finally use these factories to create instances of SSLServerSocket
or SSLSocket
. In Java 1.6, a new abstraction was introduced to allow applications to use the SSL/TLS protocols in a transport independent way, and thus freeing applications to choose transport and computing models that best meet their needs. Not only does this new abstraction allow applications to use non-blocking I/O channels and other I/O models, it also accommodates different threading models.
2. The SSL Engine API
The new abstraction is therefore an advanced API having as core class the javax.net.ssl.SSLEngine
. It encapsulates an SSL/TLS state machine and operates on inbound and outbound byte buffers supplied by the user of the SSLEngine.
2.1 Lifecycle
The SSLEngine must first go through the handshake, where the server and the client negotiate the cipher suite and the session keys. This phase typically involves the exchange of several messages. After completing the handshake, the application can start sending and receiving application data. This is the main state of the engine and will typically last until the connection is CLOSED (see image below). In some situations, one of the peers may ask for a renegotiation of the session parameters, either to generate new session keys or to change the cipher suite. This forces a re-handshake. When one of the peers is done with the connection, it should initiate a graceful shutdown, as specified in the SSL/TLS protocol. This involves exchanging a couple of closure messages between the client and the server to terminate the logical session before physically closing the socket.
2.2 SSL Handshake
The two main SSLEngine methods wrap()
and unwrap()
are responsible for generating and consuming network data respectively. Depending on the state of the SSLEngine, this data might be handshake or application data. Each SSLEngine has several phases during its lifetime. Before application data can be sent/received, the SSL/TLS protocol requires a handshake to establish cryptographic parameters. This handshake requires a series of back-and-forth steps by the SSLEngine. The SSL Process can provide more details about the handshake itself. During the initial handshaking, wrap()
and unwrap()
generate and consume handshake data, and the application is responsible for transporting the data. This sequence is repeated until the handshake is finished. Each SSLEngine operation generates a SSLEngineResult
, of which the SSLEngineResult.HandshakeStatus
field is used to determine what operation needs to occur next to move the handshake along. Below is an example of the handshake process:
3. Nio SSL Example
The following example creates a connection to https://www.amazon.com/ and displays the decrypted HTTP response.
3.1 Main class
import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLSession; public class NioSSLExample { public static void main(String[] args) throws Exception { InetSocketAddress address = new InetSocketAddress("www.amazon.com", 443); Selector selector = Selector.open(); SocketChannel channel = SocketChannel.open(); channel.connect(address); channel.configureBlocking(false); int ops = SelectionKey.OP_CONNECT | SelectionKey.OP_READ; SelectionKey key = channel.register(selector, ops); // create the worker threads final Executor ioWorker = Executors.newSingleThreadExecutor(); final Executor taskWorkers = Executors.newFixedThreadPool(2); // create the SSLEngine final SSLEngine engine = SSLContext.getDefault().createSSLEngine(); engine.setUseClientMode(true); engine.beginHandshake(); final int ioBufferSize = 32 * 1024; final NioSSLProvider ssl = new NioSSLProvider(key, engine, ioBufferSize, ioWorker, taskWorkers) { @Override public void onFailure(Exception ex) { System.out.println("handshake failure"); ex.printStackTrace(); } @Override public void onSuccess() { System.out.println("handshake success"); SSLSession session = engine.getSession(); try { System.out.println("local principal: " + session.getLocalPrincipal()); System.out.println("remote principal: " + session.getPeerPrincipal()); System.out.println("cipher: " + session.getCipherSuite()); } catch (Exception exc) { exc.printStackTrace(); } //HTTP request StringBuilder http = new StringBuilder(); http.append("GET / HTTP/1.0\r\n"); http.append("Connection: close\r\n"); http.append("\r\n"); byte[] data = http.toString().getBytes(); ByteBuffer send = ByteBuffer.wrap(data); this.sendAsync(send); } @Override public void onInput(ByteBuffer decrypted) { // HTTP response byte[] dst = new byte[decrypted.remaining()]; decrypted.get(dst); String response = new String(dst); System.out.print(response); System.out.flush(); } @Override public void onClosed() { System.out.println("ssl session closed"); } }; // NIO selector while (true) { key.selector().select(); Iterator keys = key.selector().selectedKeys().iterator(); while (keys.hasNext()) { keys.next(); keys.remove(); ssl.processInput(); } } } }
From the above code:
- In the
main()
method on lines 18-25, aSelector
is created and aSocketChannel
is registered having a selection key interested in socket-connect and socket-read operations for the connection to the amazon url:InetSocketAddress address = new InetSocketAddress("www.amazon.com", 443); Selector selector = Selector.open(); SocketChannel channel = SocketChannel.open(); channel.connect(address); channel.configureBlocking(false); int ops = SelectionKey.OP_CONNECT | SelectionKey.OP_READ; SelectionKey key = channel.register(selector, ops);
- On lines 28-29, an
ioWorker
thread is created for executing theSSLProvider
runnable and also aThreadPool
containing 2 threads for executing the delegated runnable task for the SSL Engine. - On lines 32-34, the
SSLEngine
is initiated in client mode and with initial handshaking:final SSLEngine engine = SSLContext.getDefault().createSSLEngine(); engine.setUseClientMode(true); engine.beginHandshake();
- On lines 36-59, the
NioSSLProvider
object is instantiated. This is responsible for writing and reading from theByteChannel
and also as the entry point for the SSL Handshaking. Upon successful negotiation with the amazon server, the local and remote principals are printed and also the name of the SSL cipher suite which is used for all connections in the session. - The HTTP request is sent from the client after successful handshake on lines 62-67:
StringBuilder http = new StringBuilder(); http.append("GET / HTTP/1.0\r\n"); http.append("Connection: close\r\n"); http.append("\r\n"); byte[] data = http.toString().getBytes(); ByteBuffer send = ByteBuffer.wrap(data);
- On line 72, the
onInput
method is called whenever the SSL Engine completed an operation withjavax.net.ssl.SSLEngineResult.Status.OK
. The partial decrypted response is printed each time:public void onInput(ByteBuffer decrypted) { // HTTP response byte[] dst = new byte[decrypted.remaining()]; decrypted.get(dst); String response = new String(dst); System.out.print(response); System.out.flush(); }
- Finally, the nio
Selector
loop is started on line 90 by processing the selection keys which remain valid until the channel is closed.
3.2 NioSSLProvider class
import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; import java.nio.channels.SelectionKey; import java.nio.channels.WritableByteChannel; import java.util.concurrent.Executor; import javax.net.ssl.SSLEngine; public abstract class NioSSLProvider extends SSLProvider { private final ByteBuffer buffer = ByteBuffer.allocate(32 * 1024); private final SelectionKey key; public NioSSLProvider(SelectionKey key, SSLEngine engine, int bufferSize, Executor ioWorker, Executor taskWorkers) { super(engine, bufferSize, ioWorker, taskWorkers); this.key = key; } @Override public void onOutput(ByteBuffer encrypted) { try { ((WritableByteChannel) this.key.channel()).write(encrypted); } catch (IOException exc) { throw new IllegalStateException(exc); } } public boolean processInput() { buffer.clear(); int bytes; try { bytes = ((ReadableByteChannel) this.key.channel()).read(buffer); } catch (IOException ex) { bytes = -1; } if (bytes == -1) { return false; } buffer.flip(); ByteBuffer copy = ByteBuffer.allocate(bytes); copy.put(buffer); copy.flip(); this.notify(copy); return true; } }
From the above code:
- A sequence of bytes is read from the channel on line 40:
bytes = ((ReadableByteChannel) this.key.channel()).read(buffer);
and a new byte buffer is allocated on line 50:
ByteBuffer copy = ByteBuffer.allocate(bytes);
- The
notify
method is called on line 53, which triggers the ssl handshake procedure and via the helper methodisHandShaking
on line 1 of the SSLProvider class, the wrap/unwrap sequence starts. - If the
wrap()
helper method from the SSLProvider class is called, then the buffered data are encoded into SSL/TLS network data:wrapResult = engine.wrap(clientWrap, serverWrap);
and if the return value of the SSLEngine operation is OK then the
onOutput()
method on line 22 is called in order to write the encrypted response from the server into theByteChannel
:((WritableByteChannel) this.key.channel()).write(encrypted);
- If the
unwrap()
helper method from the SSLProvider class is called, then an attempt to decode the SSL network data from the server is made on line 95 of the SSLProvider class:unwrapResult = engine.unwrap(clientUnwrap, serverUnwrap);
and if the return value of the SSLEngine operation is OK, the decrypted message from the server is printed.
3.3 SSLProvider class
For simplicity, we present the basic helper methods of this class:
private synchronized boolean isHandShaking() { switch (engine.getHandshakeStatus()) { case NOT_HANDSHAKING: boolean occupied = false; { if (clientWrap.position() > 0) occupied |= this.wrap(); if (clientUnwrap.position() > 0) occupied |= this.unwrap(); } return occupied; case NEED_WRAP: if (!this.wrap()) return false; break; case NEED_UNWRAP: if (!this.unwrap()) return false; break; case NEED_TASK: final Runnable sslTask = engine.getDelegatedTask(); Runnable wrappedTask = new Runnable() { @Override public void run() { sslTask.run(); ioWorker.execute(SSLProvider.this); } }; taskWorkers.execute(wrappedTask); return false; case FINISHED: throw new IllegalStateException("FINISHED"); } return true; } private boolean wrap() { SSLEngineResult wrapResult; try { clientWrap.flip(); wrapResult = engine.wrap(clientWrap, serverWrap); clientWrap.compact(); } catch (SSLException exc) { this.onFailure(exc); return false; } switch (wrapResult.getStatus()) { case OK: if (serverWrap.position() > 0) { serverWrap.flip(); this.onOutput(serverWrap); serverWrap.compact(); } break; case BUFFER_UNDERFLOW: // try again later break; case BUFFER_OVERFLOW: throw new IllegalStateException("failed to wrap"); case CLOSED: this.onClosed(); return false; } return true; } private boolean unwrap() { SSLEngineResult unwrapResult; try { clientUnwrap.flip(); unwrapResult = engine.unwrap(clientUnwrap, serverUnwrap); clientUnwrap.compact(); } catch (SSLException ex) { this.onFailure(ex); return false; } switch (unwrapResult.getStatus()) { case OK: if (serverUnwrap.position() > 0) { serverUnwrap.flip(); this.onInput(serverUnwrap); serverUnwrap.compact(); } break; case CLOSED: this.onClosed(); return false; case BUFFER_OVERFLOW: throw new IllegalStateException("failed to unwrap"); case BUFFER_UNDERFLOW: return false; } if (unwrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED) { this.onSuccess(); return false; } return true; }
4. Download Java Source Code
This was an example of SSL handshake with java.nio
You can download the full source code of this example here: NioSSLExample
This is what I get when I run the example:
* Exception in thread “pool-2-thread-2” java.lang.NullPointerException
at two.nio.ssl.SSLProvider$3.run(SSLProvider.java:93)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
*/
Great work! I think it might be more clear if you change
clientWrap/clientUnwrap/serverWrap/serverUnwrap
tooutboundPlain/InboundEncrypted/outboundEncrypted/inboundDecrypted
respectively.