All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.airepublic.http.common.SslSupport Maven / Gradle / Ivy

package com.airepublic.http.common;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

/**
 * Class to support creating {@link SSLContext} and {@link SSLEngine}.
 * 
 * @author Torsten Oltmanns
 *
 */
public class SslSupport {
    private final static Logger LOG = Logger.getLogger(SslSupport.class.getName());
    private final static ExecutorService executorService = Executors.newSingleThreadExecutor();
    private static int applicationBufferSize = 16 * 1024;
    private static int packetBufferSize = 16 * 1024;


    /**
     * Creates a {@link SSLContext} in client mode.
     * 
     * @return the {@link SSLContext}
     * @throws IOException if creating the {@link SSLContext} fails
     */
    public static SSLContext createClientSSLContext() throws IOException {
        return createSSLContext(true, null, null, null, null);
    }


    /**
     * Creates a {@link SSLContext} in server mode.
     *
     * @param keystoreFile the path to the keystore
     * @param keystorePassword the password for the keystore
     * @param truststoreFile the path to the truststore
     * @param truststorePassword the password for the truststore
     * @return the {@link SSLContext}
     * @throws IOException if creating the {@link SSLContext} fails
     */
    public static SSLContext createServerSSLContext(final String keystoreFile, final String keystorePassword, final String truststoreFile, final String truststorePassword) throws IOException {
        return createSSLContext(false, keystoreFile, keystorePassword, truststoreFile, truststorePassword);
    }


    /**
     * Creates a {@link SSLContext} in client or server mode.
     *
     * @param isClient flag whether to create the {@link SSLContext} in client or server mode.
     * @param keystoreFile the path to the keystore
     * @param keystorePassword the password for the keystore
     * @param truststoreFile the path to the truststore
     * @param truststorePassword the password for the truststore
     * @return the {@link SSLContext}
     * @throws IOException if creating the {@link SSLContext} fails
     */
    static SSLContext createSSLContext(final boolean isClient, final String keystoreFile, final String keystorePassword, final String truststoreFile, final String truststorePassword) throws IOException {
        final SSLContext sslContext;
        try {
            sslContext = SSLContext.getInstance("SSL");

            try {
                if (isClient) {
                    // create client context
                    sslContext.init(null, null, null);
                } else {
                    // create server context
                    final KeyManager[] keyManagers = createKeyManagers(keystoreFile, truststorePassword, keystorePassword);
                    final TrustManager[] trustManagers = createTrustManagers(truststoreFile, truststorePassword);
                    sslContext.init(keyManagers, trustManagers, new SecureRandom());
                }
            } catch (final Exception e) {
                throw new IOException("Could not get initialize SSLContext!", e);
            }

            final SSLSession dummySession = sslContext.createSSLEngine().getSession();
            setApplicationBufferSize(dummySession.getApplicationBufferSize());
            setPacketBufferSize(dummySession.getPacketBufferSize());
            dummySession.invalidate();

        } catch (final Exception e) {
            throw new IOException("Could not get instance of SSLContext!", e);
        }

        return sslContext;
    }


    /**
     * Creates the key managers required to initiate the {@link SSLContext}, using a JKS keystore as
     * an input.
     *
     * @param filepath - the path to the JKS keystore.
     * @param keystorePassword - the keystore's password.
     * @param keyPassword - the key's passsword.
     * @return {@link KeyManager} array that will be used to initiate the {@link SSLContext}.
     * @throws Exception if keymanagers could not be created
     */
    public static KeyManager[] createKeyManagers(final String filepath, final String keystorePassword, final String keyPassword) throws Exception {
        final KeyStore keyStore = KeyStore.getInstance("JKS");
        final InputStream keyStoreIS = new FileInputStream(filepath);

        try {
            keyStore.load(keyStoreIS, keystorePassword.toCharArray());
        } finally {
            if (keyStoreIS != null) {
                keyStoreIS.close();
            }
        }

        final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(keyStore, keyPassword.toCharArray());
        return kmf.getKeyManagers();
    }


    /**
     * Creates the trust managers required to initiate the {@link SSLContext}, using a JKS keystore
     * as an input.
     *
     * @param filepath - the path to the JKS keystore.
     * @param keystorePassword - the keystore's password.
     * @return {@link TrustManager} array, that will be used to initiate the {@link SSLContext}.
     * @throws Exception if trustmanagers could not be created
     */
    public static TrustManager[] createTrustManagers(final String filepath, final String keystorePassword) throws Exception {
        final KeyStore trustStore = KeyStore.getInstance("JKS");
        final InputStream trustStoreIS = new FileInputStream(filepath);

        try {
            trustStore.load(trustStoreIS, keystorePassword.toCharArray());
        } finally {
            if (trustStoreIS != null) {
                trustStoreIS.close();
            }
        }

        final TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustFactory.init(trustStore);
        return trustFactory.getTrustManagers();
    }


    public static ByteBuffer enlargeApplicationBuffer(final SSLEngine engine, final ByteBuffer buffer) {
        return enlargeBuffer(buffer, engine.getSession().getApplicationBufferSize());
    }


    /**
     * Implements the handshake protocol between two peers, required for the establishment of the
     * SSL/TLS connection. During the handshake, encryption configuration information - such as the
     * list of available cipher suites - will be exchanged and if the handshake is successful will
     * lead to an established SSL/TLS session.
     *
     * 

* A typical handshake will usually contain the following steps: *

*
    *
  • 1. wrap: ClientHello
  • *
  • 2. unwrap: ServerHello/Cert/ServerHelloDone
  • *
  • 3. wrap: ClientKeyExchange
  • *
  • 4. wrap: ChangeCipherSpec
  • *
  • 5. wrap: Finished
  • *
  • 6. unwrap: ChangeCipherSpec
  • *
  • 7. unwrap: Finished
  • *
*

* Handshake is also used during the end of the session, in order to properly close the * connection between the two peers. A proper connection close will typically include the one * peer sending a CLOSE message to another, and then wait for the other's CLOSE message to close * the transport link. The other peer from his perspective would read a CLOSE message from his * peer and then enter the handshake procedure to send his own CLOSE message as well. *

* * @param socketChannel - the socket channel that connects the two peers. * @param engine - the engine that will be used for encryption/decryption of the data exchanged * with the other peer. * @return True if the connection handshake was successful or false if an error occurred. * @throws IOException - if an error occurs during read/write to the socket channel. */ public static boolean doHandshake(final SocketChannel socketChannel, final SSLEngine engine) throws IOException { LOG.fine("Performing SSL handshake..."); ByteBuffer packetBuffer = ByteBuffer.allocate(packetBufferSize); ByteBuffer peerPacketBuffer = ByteBuffer.allocate(packetBufferSize); SSLEngineResult result; HandshakeStatus handshakeStatus; // NioSslPeer's fields myAppData and peerAppData are supposed to be large enough to hold all // message data the peer // will send and expects to receive from the other peer respectively. Since the messages to // be exchanged will usually be less // than 16KB long the capacity of these fields should also be smaller. Here we initialize // these two local buffers // to be used for the handshake, while keeping client's buffers at the same size. final int appBufferSize = engine.getSession().getApplicationBufferSize(); final ByteBuffer myAppData = ByteBuffer.allocate(appBufferSize); ByteBuffer peerAppData = ByteBuffer.allocate(appBufferSize); packetBuffer.clear(); peerPacketBuffer.clear(); handshakeStatus = engine.getHandshakeStatus(); while (handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED && handshakeStatus != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) { switch (handshakeStatus) { case NEED_UNWRAP: if (socketChannel.read(peerPacketBuffer) < 0) { if (engine.isInboundDone() && engine.isOutboundDone()) { return false; } try { engine.closeInbound(); } catch (final SSLException e) { LOG.log(Level.SEVERE, "This engine was forced to close inbound, without having received the proper SSL/TLS close notification message from the peer, due to end of stream.", e); } engine.closeOutbound(); // After closeOutbound the engine will be set to WRAP state, in order to try // to send a close message to the client. handshakeStatus = engine.getHandshakeStatus(); break; } peerPacketBuffer.flip(); try { result = engine.unwrap(peerPacketBuffer, peerAppData); peerPacketBuffer.compact(); handshakeStatus = result.getHandshakeStatus(); } catch (final SSLException e) { LOG.log(Level.SEVERE, "A problem was encountered while processing the data that caused the SSLEngine to abort. Will try to properly close connection...", e); engine.closeOutbound(); handshakeStatus = engine.getHandshakeStatus(); break; } switch (result.getStatus()) { case OK: break; case BUFFER_OVERFLOW: // Will occur when peerAppData's capacity is smaller than the data // derived from peerNetData's unwrap. peerAppData = SslSupport.enlargeApplicationBuffer(engine, peerAppData); break; case BUFFER_UNDERFLOW: // Will occur either when no data was read from the peer or when the // peerNetData buffer was too small to hold all peer's data. peerPacketBuffer = SslSupport.handleBufferUnderflow(engine, peerPacketBuffer); break; case CLOSED: if (engine.isOutboundDone()) { return false; } else { engine.closeOutbound(); handshakeStatus = engine.getHandshakeStatus(); break; } default: throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); } break; case NEED_WRAP: packetBuffer.clear(); try { result = engine.wrap(myAppData, packetBuffer); handshakeStatus = result.getHandshakeStatus(); } catch (final SSLException e) { LOG.log(Level.SEVERE, "A problem was encountered while processing the data that caused the SSLEngine to abort. Will try to properly close connection...", e); engine.closeOutbound(); handshakeStatus = engine.getHandshakeStatus(); break; } switch (result.getStatus()) { case OK: packetBuffer.flip(); while (packetBuffer.hasRemaining()) { socketChannel.write(packetBuffer); } break; case BUFFER_OVERFLOW: // Will occur if there is not enough space in myNetData buffer to write // all the data that would be generated by the method wrap. // Since myNetData is set to session's packet size we should not get to // this point because SSLEngine is supposed // to produce messages smaller or equal to that, but a general handling // would be the following: packetBuffer = SslSupport.enlargePacketBuffer(engine, packetBuffer); break; case BUFFER_UNDERFLOW: throw new SSLException("Buffer underflow occured after a wrap. I don't think we should ever get here."); case CLOSED: try { packetBuffer.flip(); while (packetBuffer.hasRemaining()) { socketChannel.write(packetBuffer); } // At this point the handshake status will probably be NEED_UNWRAP // so we make sure that peerNetData is clear to read. peerPacketBuffer.clear(); } catch (final Exception e) { LOG.log(Level.SEVERE, "Failed to send server's CLOSE message due to socket channel's failure."); handshakeStatus = engine.getHandshakeStatus(); } break; default: throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); } break; case NEED_TASK: Runnable task; while ((task = engine.getDelegatedTask()) != null) { executorService.execute(task); } handshakeStatus = engine.getHandshakeStatus(); break; case FINISHED: break; case NOT_HANDSHAKING: break; default: throw new IllegalStateException("Invalid SSL status: " + handshakeStatus); } } return true; } /** * Compares sessionProposedCapacity with buffer's capacity. If buffer's capacity is * smaller, returns a buffer with the proposed capacity. If it's equal or larger, returns a * buffer with capacity twice the size of the initial one. * * @param buffer - the buffer to be enlarged. * @param sessionProposedCapacity - the minimum size of the new buffer, proposed by * {@link SSLSession}. * @return A new buffer with a larger capacity. */ public static ByteBuffer enlargeBuffer(ByteBuffer buffer, final int sessionProposedCapacity) { if (sessionProposedCapacity > buffer.capacity()) { buffer = ByteBuffer.allocate(sessionProposedCapacity); } else { buffer = ByteBuffer.allocate(buffer.capacity() * 2); } return buffer; } public static ByteBuffer enlargePacketBuffer(final SSLEngine engine, final ByteBuffer buffer) { return enlargeBuffer(buffer, engine.getSession().getPacketBufferSize()); } /** * Handles {@link SSLEngineResult.Status#BUFFER_UNDERFLOW}. Will check if the buffer is already * filled, and if there is no space problem will return the same buffer, so the client tries to * read again. If the buffer is already filled will try to enlarge the buffer either to * session's proposed size or to a larger capacity. A buffer underflow can happen only after an * unwrap, so the buffer will always be a peerNetData buffer. * * @param buffer - will always be peerNetData buffer. * @param engine - the engine used for encryption/decryption of the data exchanged between the * two peers. * @return The same buffer if there is no space problem or a new buffer with the same data but * more space. */ public static ByteBuffer handleBufferUnderflow(final SSLEngine engine, final ByteBuffer buffer) { if (engine.getSession().getPacketBufferSize() < buffer.limit()) { return buffer; } else { final ByteBuffer replaceBuffer = enlargePacketBuffer(engine, buffer); buffer.flip(); replaceBuffer.put(buffer); return replaceBuffer; } } /** * This method should be called when this peer wants to explicitly close the connection or when * a close message has arrived from the other peer, in order to provide an orderly shutdown. *

* It first calls {@link SSLEngine#closeOutbound()} which prepares this peer to send its own * close message and sets {@link SSLEngine} to the NEED_WRAP state. Then, it * delegates the exchange of close messages to the handshake method and finally, it closes * socket channel. *

* * @param socketChannel - the transport link used between the two peers. * @param engine - the engine used for encryption/decryption of the data exchanged between the * two peers. * @throws IOException if an I/O error occurs to the socket channel. */ public static void closeConnection(final SocketChannel socketChannel, final SSLEngine engine) throws IOException { engine.closeOutbound(); doHandshake(socketChannel, engine); socketChannel.close(); } /** * Gets the packet buffer size. * * @return the packet buffer size */ public static int getPacketBufferSize() { return packetBufferSize; } /** * Sets the packet buffer size. * * @param size the packet buffer size */ public static void setPacketBufferSize(final int size) { packetBufferSize = size; } /** * Gets the application buffer size. * * @return the application buffer size */ public static int getApplicationBufferSize() { return applicationBufferSize; } /** * Sets the application buffer size. * * @param size the application buffer size */ public static void setApplicationBufferSize(final int size) { applicationBufferSize = size; } /** * Unwraps the content of the {@link ByteBuffer} using the {@link SSLEngine}. If the connection * is closed by the peer it will also close the channel. * * @param sslEngine the {@link SSLEngine} * @param channel the {@link SocketChannel} * @param buffer the {@link ByteBuffer} * @return the unwrapped {@link ByteBuffer} * @throws IOException if decryption fails */ public static ByteBuffer unwrap(final SSLEngine sslEngine, final SocketChannel channel, final ByteBuffer buffer) throws IOException { if (sslEngine == null) { return buffer; } final ByteBuffer unwrapBuffer = ByteBuffer.allocate(buffer.capacity()); final SSLEngineResult result = sslEngine.unwrap(buffer, unwrapBuffer); switch (result.getStatus()) { case OK: unwrapBuffer.flip(); return unwrapBuffer; case BUFFER_OVERFLOW: return SslSupport.enlargeApplicationBuffer(sslEngine, unwrapBuffer); case BUFFER_UNDERFLOW: return SslSupport.handleBufferUnderflow(sslEngine, buffer); case CLOSED: LOG.fine("Closing SSL connection..."); closeConnection(channel, sslEngine); return null; default: throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); } } /** * Wraps the contents of the {@link ByteBuffer}s using the {@link SSLEngine}. If the connection * is closed by the peer it will also close the channel. * * @param sslEngine the {@link SSLEngine} * @param channel the {@link SocketChannel} * @param buffers the {@link ByteBuffer}s * @return the unwrapped {@link ByteBuffer} * @throws IOException if encryption fails */ public static ByteBuffer[] wrap(final SSLEngine sslEngine, final SocketChannel channel, final ByteBuffer... buffers) throws IOException { if (sslEngine == null) { return buffers; } final ByteBuffer[] wrappedBuffers = new ByteBuffer[buffers.length]; for (int i = 0; i < buffers.length; i++) { final ByteBuffer buffer = buffers[i]; boolean retry = false; do { ByteBuffer wrappedBuffer = ByteBuffer.allocate(SslSupport.getPacketBufferSize()); final SSLEngineResult result = sslEngine.wrap(buffer, wrappedBuffer); retry = false; switch (result.getStatus()) { case OK: wrappedBuffer.flip(); wrappedBuffers[i] = wrappedBuffer; break; case BUFFER_OVERFLOW: wrappedBuffer = SslSupport.enlargePacketBuffer(sslEngine, wrappedBuffer); retry = true; break; case BUFFER_UNDERFLOW: throw new SSLException("Buffer underflow occured after a wrap. I don't think we should ever get here."); case CLOSED: LOG.fine("Closing SSL connection..."); closeConnection(channel, sslEngine); return null; default: throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); } } while (retry); } return wrappedBuffers; } /** * Performs a SSL handshake in client mode. * * @param sslContext the {@link SSLContext} in client mode * @param channel the {@link SocketChannel} * @param uri the {@link URI} to connect to * @return the {@link SSLEngine} created for the connection * @throws IOException if handshaking fails */ public static SSLEngine clientSSLHandshake(final SSLContext sslContext, final SocketChannel channel, final URI uri) throws IOException { SSLEngine sslEngine; boolean success = false; try { final String host = uri.getHost(); Integer port = uri.getPort(); if (port == null || port == -1) { port = 443; } if (host == null || port == null) { throw new IOException("Peer host and port not specified for client SSL connection!"); } sslEngine = sslContext.createSSLEngine(host, port); sslEngine.setUseClientMode(true); sslEngine.beginHandshake(); success = SslSupport.doHandshake(channel, sslEngine); } catch (final Exception e) { throw new IOException("Could not perform SSL handshake!", e); } if (!success) { channel.close(); throw new IOException("Connection closed due to handshake failure."); } return sslEngine; } /** * Performs a SSL handshake in server mode. * * @param sslContext the {@link SSLContext} in server mode * @param channel the {@link SocketChannel} * @return the {@link SSLEngine} created for the connection * @throws IOException if handshaking fails */ public static SSLEngine serverSSLHandshake(final SSLContext sslContext, final SocketChannel channel) throws IOException { SSLEngine sslEngine; boolean success = false; try { sslEngine = sslContext.createSSLEngine(); sslEngine.setUseClientMode(false); sslEngine.beginHandshake(); success = SslSupport.doHandshake(channel, sslEngine); } catch (final Exception e) { throw new IOException("Could not perform SSL handshake!", e); } if (!success) { channel.close(); throw new IOException("Connection closed due to handshake failure."); } return sslEngine; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy