Commit 5abb6386 authored by Gaston Dombiak's avatar Gaston Dombiak Committed by gato

Initial version. JM-387

git-svn-id: http://svn.igniterealtime.org/svn/repos/messenger/trunk@2767 b35dd754-fafc-0310-a699-88a17e54d16e
parent 03db6e06
/**
* $RCSfile$
* $Revision: $
* $Date: $
*
* Copyright (C) 2005 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.messenger.net;
/**
* A TLSStatus enum describing the current handshaking state of this TLS connection.
*
* @author Hao Chen
*/
public enum TLSStatus {
/**
* ust send data to the remote side before handshaking can continue.
*/
NEED_WRITE,
/**
* Need to receive data from the remote side before handshaking can continue.
*/
NEED_READ,
/**
* Not be able to unwrap the incoming data because there were not enough source bytes available
* to make a complete packet.
*/
UNDERFLOW,
/**
* The operation just closed this side of the SSLEngine, or the operation could not be completed
* because it was already closed.
*/
CLOSED,
/**
* Handshaking is OK.
*/
OK;
}
/**
* $RCSfile$
* $Revision: $
* $Date: $
*
* Copyright (C) 2005 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.messenger.net;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.WritableByteChannel;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
/**
* TLSStreamHandler is responsible for securing plain connections by negotiating TLS. By creating
* a new instance of this class the plain connection will be secured.
*
* @author Hao Chen
*/
public class TLSStreamHandler {
private TLSStreamWriter writer;
private TLSStreamReader reader;
private TLSWrapper wrapper;
private ReadableByteChannel rbc;
private WritableByteChannel wbc;
private SSLEngine tlsEngine;
/*
* During the initial handshake, keep track of the next SSLEngine operation that needs to occur:
*
* NEED_WRAP/NEED_UNWRAP
*
* Once the initial handshake has completed, we can short circuit handshake checks with
* initialHSComplete.
*/
private HandshakeStatus initialHSStatus;
private boolean initialHSComplete;
private int appBBSize;
private int netBBSize;
/*
* All I/O goes through these buffers. It might be nice to use a cache of ByteBuffers so we're
* not alloc/dealloc'ing ByteBuffer's for each new SSLEngine. Outbound application data is
* supplied to us by our callers.
*/
private ByteBuffer incomingNetBB;
private ByteBuffer outgoingNetBB;
private ByteBuffer appBB;
/*
* An empty ByteBuffer for use when one isn't available, say as a source buffer during initial
* handshake wraps or for close operations.
*/
private static ByteBuffer hsBB = ByteBuffer.allocate(0);
/**
* Creates a new TLSStreamHandler and secures the plain socket connection.
*
* @param socket the plain socket connection to secure
* @throws IOException
*/
public TLSStreamHandler(Socket socket) throws IOException {
wrapper = new TLSWrapper();
tlsEngine = wrapper.getTlsEngine();
reader = new TLSStreamReader(wrapper, socket);
writer = new TLSStreamWriter(wrapper, socket);
rbc = Channels.newChannel(socket.getInputStream());
wbc = Channels.newChannel(socket.getOutputStream());
initialHSStatus = HandshakeStatus.NEED_UNWRAP;
initialHSComplete = false;
netBBSize = tlsEngine.getSession().getPacketBufferSize();
appBBSize = tlsEngine.getSession().getApplicationBufferSize();
incomingNetBB = ByteBuffer.allocate(netBBSize);
outgoingNetBB = ByteBuffer.allocate(netBBSize);
outgoingNetBB.position(0);
outgoingNetBB.limit(0);
appBB = ByteBuffer.allocate(appBBSize);
//socket.setSoTimeout(0);
//socket.setKeepAlive(true);
while (!initialHSComplete) {
initialHSComplete = doHandshake(null);
}
}
public InputStream getInputStream(){
return reader.getInputStream();
}
public OutputStream getOutputStream(){
return writer.getOutputStream();
}
private boolean doHandshake(SelectionKey sk) throws IOException {
SSLEngineResult result;
if (initialHSComplete) {
return initialHSComplete;
}
/*
* Flush out the outgoing buffer, if there's anything left in it.
*/
if (outgoingNetBB.hasRemaining()) {
if (!flush(outgoingNetBB)) {
return false;
}
// See if we need to switch from write to read mode.
switch (initialHSStatus) {
/*
* Is this the last buffer?
*/
case FINISHED:
initialHSComplete = true;
case NEED_UNWRAP:
if (sk != null) {
sk.interestOps(SelectionKey.OP_READ);
}
break;
}
return initialHSComplete;
}
switch (initialHSStatus) {
case NEED_UNWRAP:
if (rbc.read(incomingNetBB) == -1) {
tlsEngine.closeInbound();
return initialHSComplete;
}
needIO: while (initialHSStatus == HandshakeStatus.NEED_UNWRAP) {
/*
* Don't need to resize requestBB, since no app data should be generated here.
*/
incomingNetBB.flip();
result = tlsEngine.unwrap(incomingNetBB, appBB);
incomingNetBB.compact();
initialHSStatus = result.getHandshakeStatus();
switch (result.getStatus()) {
case OK:
switch (initialHSStatus) {
case NOT_HANDSHAKING:
throw new IOException("Not handshaking during initial handshake");
case NEED_TASK:
initialHSStatus = doTasks();
break;
case FINISHED:
initialHSComplete = true;
break needIO;
}
break;
case BUFFER_UNDERFLOW:
/*
* Need to go reread the Channel for more data.
*/
if (sk != null) {
sk.interestOps(SelectionKey.OP_READ);
}
break needIO;
default: // BUFFER_OVERFLOW/CLOSED:
throw new IOException("Received" + result.getStatus()
+ "during initial handshaking");
}
}
/*
* Just transitioned from read to write.
*/
if (initialHSStatus != HandshakeStatus.NEED_WRAP) {
break;
}
// Fall through and fill the write buffers.
case NEED_WRAP:
/*
* The flush above guarantees the out buffer to be empty
*/
outgoingNetBB.clear();
result = tlsEngine.wrap(hsBB, outgoingNetBB);
outgoingNetBB.flip();
initialHSStatus = result.getHandshakeStatus();
switch (result.getStatus()) {
case OK:
if (initialHSStatus == HandshakeStatus.NEED_TASK) {
initialHSStatus = doTasks();
}
if (sk != null) {
sk.interestOps(SelectionKey.OP_WRITE);
}
break;
default: // BUFFER_OVERFLOW/BUFFER_UNDERFLOW/CLOSED:
throw new IOException("Received" + result.getStatus()
+ "during initial handshaking");
}
break;
default: // NOT_HANDSHAKING/NEED_TASK/FINISHED
throw new RuntimeException("Invalid Handshaking State" + initialHSStatus);
} // switch
return initialHSComplete;
}
/*
* Writes ByteBuffer to the SocketChannel. Returns true when the ByteBuffer has no remaining
* data.
*/
private boolean flush(ByteBuffer bb) throws IOException {
wbc.write(bb);
return !bb.hasRemaining();
}
/*
* Do all the outstanding handshake tasks in the current Thread.
*/
private SSLEngineResult.HandshakeStatus doTasks() {
Runnable runnable;
/*
* We could run this in a separate thread, but do in the current for now.
*/
while ((runnable = tlsEngine.getDelegatedTask()) != null) {
runnable.run();
}
return tlsEngine.getHandshakeStatus();
}
}
/**
* $RCSfile$
* $Revision: $
* $Date: $
*
* Copyright (C) 2005 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.messenger.net;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import org.jivesoftware.util.Log;
/**
* A <code>TLSStreamReader</code> that returns a special InputStream that hides the ByteBuffers
* used by the underlying Channels.
*
* @author Hao Chen
*/
public class TLSStreamReader {
/**
* <code>TLSWrapper</code> is a TLS wrapper for connections requiring TLS protocol.
*/
private TLSWrapper wrapper;
private ReadableByteChannel rbc;
/**
* <code>inNetBB</code> buffer keeps data read from socket.
*/
private ByteBuffer inNetBB;
/**
* <code>inAppBB</code> buffer keeps decypted data.
*/
private ByteBuffer inAppBB;
public TLSStreamReader(TLSWrapper tlsWrapper, Socket socket) throws IOException {
wrapper = tlsWrapper;
rbc = Channels.newChannel(socket.getInputStream());
inNetBB = ByteBuffer.allocate(wrapper.getNetBuffSize());
inAppBB = ByteBuffer.allocate(wrapper.getAppBuffSize());
}
/*
* Read TLS encrpyted data from SocketChannel, and use <code>decrypt</code> method to decypt.
*/
private void doRead() throws IOException {
inNetBB.clear();
inAppBB.clear();
final int cnt = rbc.read(inNetBB);
if (cnt > 0) {
ByteBuffer tlsInput = inNetBB;
inAppBB = decrypt(tlsInput, inAppBB);
inAppBB.flip();
} else {
if (cnt == -1) {
rbc.close();
}
}
}
/*
* This method uses <code>TLSWrapper</code> to decrypt TLS encrypted data.
*/
private ByteBuffer decrypt(final ByteBuffer input, final ByteBuffer output) throws IOException {
TLSStatus stat = null;
ByteBuffer out = output;
do {
input.flip();
out = wrapper.unwrap(input, out);
if (input.hasRemaining()) {
input.compact();
}
stat = wrapper.getStatus();
} while ((stat == TLSStatus.NEED_READ || stat == TLSStatus.OK) && input.hasRemaining());
if (input.hasRemaining()) {
input.rewind();
} else {
input.clear();
}
return out;
}
public InputStream getInputStream() {
return createInputStream();
}
/*
* Returns an input stream for a ByteBuffer. The read() methods use the relative ByteBuffer
* get() methods.
*/
private InputStream createInputStream() {
return new InputStream() {
public synchronized int read() throws IOException {
doRead();
if (!inAppBB.hasRemaining()) {
return -1;
}
return inAppBB.get();
}
public synchronized int read(byte[] bytes, int off, int len) throws IOException {
doRead();
len = Math.min(len, inAppBB.remaining());
inAppBB.get(bytes, off, len);
return len;
}
};
}
}
/**
* $RCSfile$
* $Revision: $
* $Date: $
*
* Copyright (C) 2005 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.messenger.net;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
/**
* A <code>TLSStreamWriter</code> that returns a special OutputStream that hides the ByteBuffers
* used by the underlying Channels.
*
* @author Hao Chen
*
*/
public class TLSStreamWriter {
/**
* <code>TLSWrapper</code> is a TLS wrapper for connections requiring TLS protocol.
*/
private TLSWrapper wrapper;
private WritableByteChannel wbc;
private ByteBuffer outAppData;
public TLSStreamWriter(TLSWrapper tlsWrapper, Socket socket) throws IOException {
wrapper = tlsWrapper;
wbc = Channels.newChannel(socket.getOutputStream());
outAppData = ByteBuffer.allocate(tlsWrapper.getAppBuffSize());
}
private void doWrite(ByteBuffer buff) throws IOException {
if (buff == null) {
// Possibly handshaking process
buff = ByteBuffer.allocate(0);
}
if (wrapper == null) {
writeToSocket(buff);
} else {
tlsWrite(buff);
}
}
private void tlsWrite(final ByteBuffer buf) throws IOException {
ByteBuffer tlsBuffer = null;
ByteBuffer tlsOutput = null;
do {
tlsBuffer = ByteBuffer.allocate(Math.min(buf.remaining(), wrapper.getAppBuffSize()));
tlsOutput = ByteBuffer.allocate(wrapper.getNetBuffSize());
while (tlsBuffer.hasRemaining() && buf.hasRemaining()) {
tlsBuffer.put(buf.get());
}
tlsBuffer.flip();
wrapper.wrap(tlsBuffer, tlsOutput);
tlsOutput.flip();
writeToSocket(tlsOutput);
tlsOutput.clear();
} while (buf.hasRemaining());
}
/*
* Writes outNetData to the SocketChannel. <P> Returns true when the ByteBuffer has no remaining
* data.
*/
private boolean writeToSocket(final ByteBuffer outNetData) throws IOException {
wbc.write(outNetData);
return !outNetData.hasRemaining();
}
public OutputStream getOutputStream() {
return createOutputStream();
}
/*
* Returns an output stream for a ByteBuffer. The write() methods use the relative ByteBuffer
* put() methods.
*/
private OutputStream createOutputStream() {
return new OutputStream() {
public synchronized void write(int b) throws IOException {
outAppData.put((byte) b);
outAppData.flip();
doWrite(outAppData);
outAppData.clear();
}
public synchronized void write(byte[] bytes, int off, int len) throws IOException {
outAppData.put(bytes, off, len);
outAppData.flip();
doWrite(outAppData);
outAppData.clear();
}
};
}
}
/**
* $RCSfile$
* $Revision: $
* $Date: $
*
* Copyright (C) 2005 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.messenger.net;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLEngineResult.Status;
import org.jivesoftware.util.Log;
/**
* Creates and initializes the SSLContext instance to use to secure the plain connection. This
* class is also responsible for encoding and decoding the encrypted data and place it into
* the corresponding the {@link ByteBuffer}.
*
* @author Hao Chen
*/
public class TLSWrapper {
/*
* Enables logging of the SSLEngine operations.
*/
private boolean logging = false;
/*
* Enables the JSSE system debugging system property:
*
* -Djavax.net.debug=all
*
* This gives a lot of low-level information about operations underway, including specific
* handshake messages, and might be best examined after gaining some familiarity with this
* application.
*/
private static boolean debug = false;
private static final String PROTOCOL = "TLS";
private SSLEngine tlsEngine;
private SSLEngineResult tlsEngineResult;
private int netBuffSize;
private int appBuffSize;
public TLSWrapper() {
if (debug) {
System.setProperty("javax.net.debug", "all");
}
// Create/initialize the SSLContext with key material
try {
// First initialize the key and trust material.
KeyStore ksKeys = SSLConfig.getKeyStore();
String keypass = SSLConfig.getKeyPassword();
KeyStore ksTrust = SSLConfig.getTrustStore();
String trustpass = SSLConfig.getTrustPassword();
// KeyManager's decide which key material to use.
KeyManager[] km = SSLJiveKeyManagerFactory.getKeyManagers(ksKeys, keypass);
// TrustManager's decide whether to allow connections.
TrustManager[] tm = SSLJiveTrustManagerFactory.getTrustManagers(ksTrust, trustpass);
SSLContext tlsContext = SSLContext.getInstance(PROTOCOL);
tlsContext.init(km, tm, null);
/*
* Configure the tlsEngine to act as a server in the SSL/TLS handshake. We're a server,
* so no need to use host/port variant.
*
* The first call for a server is a NEED_UNWRAP.
*/
tlsEngine = tlsContext.createSSLEngine();
tlsEngine.setUseClientMode(false);
SSLSession session = tlsEngine.getSession();
netBuffSize = session.getPacketBufferSize();
appBuffSize = session.getApplicationBufferSize();
} catch (KeyManagementException e) {
Log.error("TLSHandler startup problem.\n" + " SSLContext initialisation failed.", e);
} catch (NoSuchAlgorithmException e) {
Log.error("TLSHandler startup problem.\n" + " The " + PROTOCOL + " does not exist", e);
} catch (IOException e) {
Log.error("TLSHandler startup problem.\n"
+ " the KeyStore or TrustStore does not exist", e);
}
}
public int getNetBuffSize() {
return netBuffSize;
}
public int getAppBuffSize() {
return appBuffSize;
}
/**
* Returns whether unwrap(ByteBuffer, ByteBuffer) will accept any more inbound data messages and
* whether wrap(ByteBuffer, ByteBuffer) will produce any more outbound data messages.
*
* @return true if the TLSHandler will not consume anymore network data and will not produce any
* anymore network data.
*/
public boolean isEngineClosed() {
return (tlsEngine.isOutboundDone() && tlsEngine.isInboundDone());
}
public void enableLogging(boolean logging) {
this.logging = logging;
}
/**
* Attempts to decode SSL/TLS network data into a subsequence of plaintext application data
* buffers. Depending on the state of the TLSWrapper, this method may consume network data
* without producing any application data (for example, it may consume handshake data.)
*
* If this TLSWrapper has not yet started its initial handshake, this method will automatically
* start the handshake.
*
* @param net a ByteBuffer containing inbound network data
* @param app a ByteBuffer to hold inbound application data
* @return a ByteBuffer containing inbound application data
* @throws SSLException A problem was encountered while processing the data that caused the
* TLSHandler to abort.
*/
public ByteBuffer unwrap(ByteBuffer net, ByteBuffer app) throws SSLException {
ByteBuffer out = app;
out = resizeApplicationBuffer(out);// guarantees enough room for unwrap
tlsEngineResult = tlsEngine.unwrap(net, out);
log("server unwrap: ", tlsEngineResult);
if (tlsEngineResult.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
// If the result indicates that we have outstanding tasks to do, go
// ahead and run them in this thread.
doTasks();
}
return out;
}
/**
* Attempts to encode a buffer of plaintext application data into TLS network data. Depending on
* the state of the TLSWrapper, this method may produce network data without consuming any
* application data (for example, it may generate handshake data).
*
* If this TLSWrapper has not yet started its initial handshake, this method will automatically
* start the handshake.
*
* @param app a ByteBuffer containing outbound application data
* @param net a ByteBuffer to hold outbound network data
* @throws SSLException A problem was encountered while processing the data that caused the
* TLSWrapper to abort.
*/
public void wrap(ByteBuffer app, ByteBuffer net) throws SSLException {
tlsEngineResult = tlsEngine.wrap(app, net);
log("server wrap: ", tlsEngineResult);
if (tlsEngineResult.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
// If the result indicates that we have outstanding tasks to do, go
// ahead and run them in this thread.
doTasks();
}
}
/**
* Signals that no more outbound application data will be sent on this TLSHandler.
*
* @throws SSLException
*/
public void close() throws SSLException {
// Indicate that application is done with engine
tlsEngine.closeOutbound();
}
/**
* Returns the current status for this TLSHandler.
*
* @return the current TLSStatus
*/
public TLSStatus getStatus() {
TLSStatus status = null;
if (tlsEngineResult != null && tlsEngineResult.getStatus() == Status.BUFFER_UNDERFLOW) {
status = TLSStatus.UNDERFLOW;
} else {
if (tlsEngineResult != null && tlsEngineResult.getStatus() == Status.CLOSED) {
status = TLSStatus.CLOSED;
} else {
switch (tlsEngine.getHandshakeStatus()) {
case NEED_WRAP:
status = TLSStatus.NEED_WRITE;
break;
case NEED_UNWRAP:
status = TLSStatus.NEED_READ;
break;
default:
status = TLSStatus.OK;
break;
}
}
}
return status;
}
private ByteBuffer resizeApplicationBuffer(ByteBuffer app) {
if (app.remaining() < appBuffSize) {
ByteBuffer bb = ByteBuffer.allocate(app.capacity() + appBuffSize);
app.flip();
bb.put(app);
return bb;
} else {
return app;
}
}
/*
* Do all the outstanding handshake tasks in the current Thread.
*/
private SSLEngineResult.HandshakeStatus doTasks() {
Runnable runnable;
/*
* We could run this in a separate thread, but do in the current for now.
*/
while ((runnable = tlsEngine.getDelegatedTask()) != null) {
runnable.run();
}
return tlsEngine.getHandshakeStatus();
}
/*
* Logging code
*/
private boolean resultOnce = true;
private void log(String str, SSLEngineResult result) {
if (!logging) {
return;
}
if (resultOnce) {
resultOnce = false;
Log.info("The format of the SSLEngineResult is: \n"
+ "\t\"getStatus() / getHandshakeStatus()\" +\n"
+ "\t\"bytesConsumed() / bytesProduced()\"\n");
}
HandshakeStatus hsStatus = result.getHandshakeStatus();
Log.info(str + result.getStatus() + "/" + hsStatus + ", " + result.bytesConsumed() + "/"
+ result.bytesProduced() + " bytes");
if (hsStatus == HandshakeStatus.FINISHED) {
Log.info("\t...ready for application data");
}
}
protected SSLEngine getTlsEngine() {
return tlsEngine;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment