package pdftk.org.bouncycastle.crypto.tls;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.SecureRandom;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import pdftk.org.bouncycastle.asn1.ASN1Primitive;
import pdftk.org.bouncycastle.asn1.x500.X500Name;
import pdftk.org.bouncycastle.crypto.prng.ThreadedSeedGenerator;
import pdftk.org.bouncycastle.util.Arrays;
import pdftk.org.bouncycastle.util.Integers;

/**
 * An implementation of all high level protocols in TLS 1.0.
 */
public class TlsProtocolHandler
{
    private static final Integer EXT_RenegotiationInfo = Integers.valueOf(ExtensionType.renegotiation_info);

    /*
     * Our Connection states
     */
    private static final short CS_CLIENT_HELLO_SEND = 1;
    private static final short CS_SERVER_HELLO_RECEIVED = 2;
    private static final short CS_SERVER_CERTIFICATE_RECEIVED = 3;
    private static final short CS_SERVER_KEY_EXCHANGE_RECEIVED = 4;
    private static final short CS_CERTIFICATE_REQUEST_RECEIVED = 5;
    private static final short CS_SERVER_HELLO_DONE_RECEIVED = 6;
    private static final short CS_CLIENT_KEY_EXCHANGE_SEND = 7;
    private static final short CS_CERTIFICATE_VERIFY_SEND = 8;
    private static final short CS_CLIENT_CHANGE_CIPHER_SPEC_SEND = 9;
    private static final short CS_CLIENT_FINISHED_SEND = 10;
    private static final short CS_SERVER_CHANGE_CIPHER_SPEC_RECEIVED = 11;
    private static final short CS_DONE = 12;

    private static final byte[] emptybuf = new byte[0];

    private static final String TLS_ERROR_MESSAGE = "Internal TLS error, this could be an attack";

    /*
     * Queues for data from some protocols.
     */
    private ByteQueue applicationDataQueue = new ByteQueue();
    private ByteQueue changeCipherSpecQueue = new ByteQueue();
    private ByteQueue alertQueue = new ByteQueue();
    private ByteQueue handshakeQueue = new ByteQueue();

    /*
     * The Record Stream we use
     */
    private RecordStream rs;
    private SecureRandom random;

    private TlsInputStream tlsInputStream = null;
    private TlsOutputStream tlsOutputStream = null;

    private boolean closed = false;
    private boolean failedWithError = false;
    private boolean appDataReady = false;
    private Hashtable clientExtensions;

    private SecurityParameters securityParameters = null;

    private TlsClientContextImpl tlsClientContext = null;
    private TlsClient tlsClient = null;
    private int[] offeredCipherSuites = null;
    private short[] offeredCompressionMethods = null;
    private TlsKeyExchange keyExchange = null;
    private TlsAuthentication authentication = null;
    private CertificateRequest certificateRequest = null;

    private short connection_state = 0;

    private static SecureRandom createSecureRandom()
    {
        /*
         * We use our threaded seed generator to generate a good random seed. If the user
         * has a better random seed, he should use the constructor with a SecureRandom.
         */
        ThreadedSeedGenerator tsg = new ThreadedSeedGenerator();
        SecureRandom random = new SecureRandom();

        /*
         * Hopefully, 20 bytes in fast mode are good enough.
         */
        random.setSeed(tsg.generateSeed(20, true));

        return random;
    }

    public TlsProtocolHandler(InputStream is, OutputStream os)
    {
        this(is, os, createSecureRandom());
    }

    public TlsProtocolHandler(InputStream is, OutputStream os, SecureRandom sr)
    {
        this.rs = new RecordStream(this, is, os);
        this.random = sr;
    }

    protected void processData(short protocol, byte[] buf, int offset, int len) throws IOException
    {
        /*
         * Have a look at the protocol type, and add it to the correct queue.
         */
        switch (protocol)
        {
            case ContentType.change_cipher_spec:
                changeCipherSpecQueue.addData(buf, offset, len);
                processChangeCipherSpec();
                break;
            case ContentType.alert:
                alertQueue.addData(buf, offset, len);
                processAlert();
                break;
            case ContentType.handshake:
                handshakeQueue.addData(buf, offset, len);
                processHandshake();
                break;
            case ContentType.application_data:
                if (!appDataReady)
                {
                    this.failWithError(AlertLevel.fatal, AlertDescription.unexpected_message);
                }
                applicationDataQueue.addData(buf, offset, len);
                processApplicationData();
                break;
            default:
                /*
                 * Uh, we don't know this protocol.
                 * 
                 * RFC2246 defines on page 13, that we should ignore this.
                 */
        }
    }

    private void processHandshake() throws IOException
    {
        boolean read;
        do
        {
            read = false;
            /*
             * We need the first 4 bytes, they contain type and length of the message.
             */
            if (handshakeQueue.size() >= 4)
            {
                byte[] beginning = new byte[4];
                handshakeQueue.read(beginning, 0, 4, 0);
                ByteArrayInputStream bis = new ByteArrayInputStream(beginning);
                short type = TlsUtils.readUint8(bis);
                int len = TlsUtils.readUint24(bis);

                /*
                 * Check if we have enough bytes in the buffer to read the full message.
                 */
                if (handshakeQueue.size() >= (len + 4))
                {
                    /*
                     * Read the message.
                     */
                    byte[] buf = new byte[len];
                    handshakeQueue.read(buf, 0, len, 4);
                    handshakeQueue.removeData(len + 4);

                    /*
                     * RFC 2246 7.4.9. The value handshake_messages includes all handshake
                     * messages starting at client hello up to, but not including, this
                     * finished message. [..] Note: [Also,] Hello Request messages are
                     * omitted from handshake hashes.
                     */
                    switch (type)
                    {
                        case HandshakeType.hello_request:
                        case HandshakeType.finished:
                            break;
                        default:
                            rs.updateHandshakeData(beginning, 0, 4);
                            rs.updateHandshakeData(buf, 0, len);
                            break;
                    }

                    /*
                     * Now, parse the message.
                     */
                    processHandshakeMessage(type, buf);
                    read = true;
                }
            }
        }
        while (read);
    }

    private void processHandshakeMessage(short type, byte[] buf) throws IOException
    {
        ByteArrayInputStream is = new ByteArrayInputStream(buf);

        switch (type)
        {
            case HandshakeType.certificate:
            {
                switch (connection_state)
                {
                    case CS_SERVER_HELLO_RECEIVED:
                    {
                        // Parse the Certificate message and send to cipher suite

                        Certificate serverCertificate = Certificate.parse(is);

                        assertEmpty(is);

                        this.keyExchange.processServerCertificate(serverCertificate);

                        this.authentication = tlsClient.getAuthentication();
                        this.authentication.notifyServerCertificate(serverCertificate);

                        break;
                    }
                    default:
                        this.failWithError(AlertLevel.fatal, AlertDescription.unexpected_message);
                }

                connection_state = CS_SERVER_CERTIFICATE_RECEIVED;
                break;
            }
            case HandshakeType.finished:
                switch (connection_state)
                {
                    case CS_SERVER_CHANGE_CIPHER_SPEC_RECEIVED:
                        /*
                         * Read the checksum from the finished message, it has always 12
                         * bytes for TLS 1.0 and 36 for SSLv3.
                         */
                        boolean isTls = tlsClientContext.getServerVersion().getFullVersion() >= ProtocolVersion.TLSv10.getFullVersion();

                        int checksumLength = isTls ? 12 : 36;
                        byte[] serverVerifyData = new byte[checksumLength];
                        TlsUtils.readFully(serverVerifyData, is);

                        assertEmpty(is);

                        /*
                         * Calculate our own checksum.
                         */
                        byte[] expectedServerVerifyData = TlsUtils.calculateVerifyData(tlsClientContext,
                            "server finished", rs.getCurrentHash(TlsUtils.SSL_SERVER));

                        /*
                         * Compare both checksums.
                         */
                        if (!Arrays.constantTimeAreEqual(expectedServerVerifyData, serverVerifyData))
                        {
                            /*
                             * Wrong checksum in the finished message.
                             */
                            this.failWithError(AlertLevel.fatal, AlertDescription.handshake_failure);
                        }

                        connection_state = CS_DONE;

                        /*
                         * We are now ready to receive application data.
                         */
                        this.appDataReady = true;
                        break;
                    default:
                        this.failWithError(AlertLevel.fatal, AlertDescription.unexpected_message);
                }
                break;
            case HandshakeType.server_hello:
                switch (connection_state)
                {
                    case CS_CLIENT_HELLO_SEND:
                        /*
                         * Read the server hello message
                         */
                        ProtocolVersion server_version = TlsUtils.readVersion(is);
                        ProtocolVersion client_version = this.tlsClientContext.getClientVersion();
                        if (server_version.getFullVersion() > client_version.getFullVersion())
                        {
                            this.failWithError(AlertLevel.fatal, AlertDescription.illegal_parameter);
                        }

                        this.tlsClientContext.setServerVersion(server_version);
                        this.tlsClient.notifyServerVersion(server_version);

                        /*
                         * Read the server random
                         */
                        securityParameters.serverRandom = new byte[32];
                        TlsUtils.readFully(securityParameters.serverRandom, is);

                        byte[] sessionID = TlsUtils.readOpaque8(is);
                        if (sessionID.length > 32)
                        {
                            this.failWithError(AlertLevel.fatal, AlertDescription.illegal_parameter);
                        }

                        this.tlsClient.notifySessionID(sessionID);

                        /*
                         * Find out which CipherSuite the server has chosen and check that
                         * it was one of the offered ones.
                         */
                        int selectedCipherSuite = TlsUtils.readUint16(is);
                        if (!arrayContains(offeredCipherSuites, selectedCipherSuite)
                            || selectedCipherSuite == CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV)
                        {
                            this.failWithError(AlertLevel.fatal, AlertDescription.illegal_parameter);
                        }

                        this.tlsClient.notifySelectedCipherSuite(selectedCipherSuite);

                        /*
                         * Find out which CompressionMethod the server has chosen and check that
                         * it was one of the offered ones.
                         */
                        short selectedCompressionMethod = TlsUtils.readUint8(is);
                        if (!arrayContains(offeredCompressionMethods, selectedCompressionMethod))
                        {
                            this.failWithError(AlertLevel.fatal, AlertDescription.illegal_parameter);
                        }

                        this.tlsClient.notifySelectedCompressionMethod(selectedCompressionMethod);

                        /*
                         * RFC3546 2.2 The extended server hello message format MAY be
                         * sent in place of the server hello message when the client has
                         * requested extended functionality via the extended client hello
                         * message specified in Section 2.1. ... Note that the extended
                         * server hello message is only sent in response to an extended
                         * client hello message. This prevents the possibility that the
                         * extended server hello message could "break" existing TLS 1.0
                         * clients.
                         */

                        /*
                         * TODO RFC 3546 2.3 If [...] the older session is resumed, then
                         * the server MUST ignore extensions appearing in the client
                         * hello, and send a server hello containing no extensions.
                         */

                        // Integer -> byte[]
                        Hashtable serverExtensions = new Hashtable();

                        if (is.available() > 0)
                        {
                            // Process extensions from extended server hello
                            byte[] extBytes = TlsUtils.readOpaque16(is);

                            ByteArrayInputStream ext = new ByteArrayInputStream(extBytes);
                            while (ext.available() > 0)
                            {
                                Integer extType = Integers.valueOf(TlsUtils.readUint16(ext));
                                byte[] extValue = TlsUtils.readOpaque16(ext);

                                /*
                                 * RFC 5746 Note that sending a "renegotiation_info"
                                 * extension in response to a ClientHello containing only
                                 * the SCSV is an explicit exception to the prohibition in
                                 * RFC 5246, Section 7.4.1.4, on the server sending
                                 * unsolicited extensions and is only allowed because the
                                 * client is signaling its willingness to receive the
                                 * extension via the TLS_EMPTY_RENEGOTIATION_INFO_SCSV
                                 * SCSV. TLS implementations MUST continue to comply with
                                 * Section 7.4.1.4 for all other extensions.
                                 */

                                if (!extType.equals(EXT_RenegotiationInfo)
                                    && clientExtensions.get(extType) == null)
                                {
                                    /*
                                     * RFC 3546 2.3 Note that for all extension types
                                     * (including those defined in future), the extension
                                     * type MUST NOT appear in the extended server hello
                                     * unless the same extension type appeared in the
                                     * corresponding client hello. Thus clients MUST abort
                                     * the handshake if they receive an extension type in
                                     * the extended server hello that they did not request
                                     * in the associated (extended) client hello.
                                     */
                                    this.failWithError(AlertLevel.fatal,
                                        AlertDescription.unsupported_extension);
                                }

                                if (serverExtensions.containsKey(extType))
                                {
                                    /*
                                     * RFC 3546 2.3 Also note that when multiple
                                     * extensions of different types are present in the
                                     * extended client hello or the extended server hello,
                                     * the extensions may appear in any order. There MUST
                                     * NOT be more than one extension of the same type.
                                     */
                                    this.failWithError(AlertLevel.fatal,
                                        AlertDescription.illegal_parameter);
                                }

                                serverExtensions.put(extType, extValue);
                            }
                        }

                        assertEmpty(is);

                        /*
                         * RFC 5746 3.4. When a ServerHello is received, the client MUST
                         * check if it includes the "renegotiation_info" extension:
                         */
                        {
                            boolean secure_negotiation = serverExtensions.containsKey(EXT_RenegotiationInfo);

                            /*
                             * If the extension is present, set the secure_renegotiation
                             * flag to TRUE. The client MUST then verify that the length
                             * of the "renegotiated_connection" field is zero, and if it
                             * is not, MUST abort the handshake (by sending a fatal
                             * handshake_failure alert).
                             */
                            if (secure_negotiation)
                            {
                                byte[] renegExtValue = (byte[])serverExtensions.get(EXT_RenegotiationInfo);

                                if (!Arrays.constantTimeAreEqual(renegExtValue,
                                    createRenegotiationInfo(emptybuf)))
                                {
                                    this.failWithError(AlertLevel.fatal,
                                        AlertDescription.handshake_failure);
                                }
                            }

                            tlsClient.notifySecureRenegotiation(secure_negotiation);
                        }

                        if (clientExtensions != null)
                        {
                            tlsClient.processServerExtensions(serverExtensions);
                        }

                        this.keyExchange = tlsClient.getKeyExchange();

                        connection_state = CS_SERVER_HELLO_RECEIVED;
                        break;
                    default:
                        this.failWithError(AlertLevel.fatal, AlertDescription.unexpected_message);
                }
                break;
            case HandshakeType.server_hello_done:
                switch (connection_state)
                {
                    case CS_SERVER_HELLO_RECEIVED:

                        // There was no server certificate message; check it's OK
                        this.keyExchange.skipServerCertificate();
                        this.authentication = null;

                        // NB: Fall through to next case label

                    case CS_SERVER_CERTIFICATE_RECEIVED:

                        // There was no server key exchange message; check it's OK
                        this.keyExchange.skipServerKeyExchange();

                        // NB: Fall through to next case label

                    case CS_SERVER_KEY_EXCHANGE_RECEIVED:
                    case CS_CERTIFICATE_REQUEST_RECEIVED:

                        assertEmpty(is);

                        connection_state = CS_SERVER_HELLO_DONE_RECEIVED;

                        TlsCredentials clientCreds = null;
                        if (certificateRequest == null)
                        {
                            this.keyExchange.skipClientCredentials();
                        }
                        else
                        {
                            clientCreds = this.authentication.getClientCredentials(certificateRequest);

                            if (clientCreds == null)
                            {
                                this.keyExchange.skipClientCredentials();

                                boolean isTls = tlsClientContext.getServerVersion().getFullVersion() >= ProtocolVersion.TLSv10.getFullVersion();

                                if (isTls)
                                {
                                    sendClientCertificate(Certificate.EMPTY_CHAIN);
                                }
                                else
                                {
                                    sendAlert(AlertLevel.warning, AlertDescription.no_certificate);
                                }
                            }
                            else
                            {
                                this.keyExchange.processClientCredentials(clientCreds);

                                sendClientCertificate(clientCreds.getCertificate());
                            }
                        }

                        /*
                         * Send the client key exchange message, depending on the key
                         * exchange we are using in our CipherSuite.
                         */
                        sendClientKeyExchange();

                        connection_state = CS_CLIENT_KEY_EXCHANGE_SEND;

                        /*
                         * Calculate the master_secret
                         */
                        byte[] pms = this.keyExchange.generatePremasterSecret();

                        securityParameters.masterSecret = TlsUtils.calculateMasterSecret(
                            this.tlsClientContext, pms);

                        // TODO Is there a way to ensure the data is really overwritten?
                        /*
                         * RFC 2246 8.1. The pre_master_secret should be deleted from
                         * memory once the master_secret has been computed.
                         */
                        Arrays.fill(pms, (byte)0);

                        if (clientCreds != null && clientCreds instanceof TlsSignerCredentials)
                        {
                            TlsSignerCredentials signerCreds = (TlsSignerCredentials)clientCreds;
                            byte[] md5andsha1 = rs.getCurrentHash(null);
                            byte[] clientCertificateSignature = signerCreds.generateCertificateSignature(
                                md5andsha1);
                            sendCertificateVerify(clientCertificateSignature);

                            connection_state = CS_CERTIFICATE_VERIFY_SEND;
                        }

                        /*
                         * Now, we send change cipher state
                         */
                        byte[] cmessage = new byte[1];
                        cmessage[0] = 1;
                        rs.writeMessage(ContentType.change_cipher_spec, cmessage, 0,
                            cmessage.length);

                        connection_state = CS_CLIENT_CHANGE_CIPHER_SPEC_SEND;

                        /*
                         * Initialize our cipher suite
                         */
                        rs.clientCipherSpecDecided(tlsClient.getCompression(), tlsClient.getCipher());

                        /*
                         * Send our finished message.
                         */
                        byte[] clientVerifyData = TlsUtils.calculateVerifyData(tlsClientContext,
                            "client finished", rs.getCurrentHash(TlsUtils.SSL_CLIENT));

                        ByteArrayOutputStream bos = new ByteArrayOutputStream();
                        TlsUtils.writeUint8(HandshakeType.finished, bos);
                        TlsUtils.writeOpaque24(clientVerifyData, bos);
                        byte[] message = bos.toByteArray();

                        rs.writeMessage(ContentType.handshake, message, 0, message.length);

                        this.connection_state = CS_CLIENT_FINISHED_SEND;
                        break;
                    default:
                        this.failWithError(AlertLevel.fatal, AlertDescription.handshake_failure);
                }
                break;
            case HandshakeType.server_key_exchange:
            {
                switch (connection_state)
                {
                    case CS_SERVER_HELLO_RECEIVED:

                        // There was no server certificate message; check it's OK
                        this.keyExchange.skipServerCertificate();
                        this.authentication = null;

                        // NB: Fall through to next case label

                    case CS_SERVER_CERTIFICATE_RECEIVED:

                        this.keyExchange.processServerKeyExchange(is);

                        assertEmpty(is);
                        break;

                    default:
                        this.failWithError(AlertLevel.fatal, AlertDescription.unexpected_message);
                }

                this.connection_state = CS_SERVER_KEY_EXCHANGE_RECEIVED;
                break;
            }
            case HandshakeType.certificate_request:
            {
                switch (connection_state)
                {
                    case CS_SERVER_CERTIFICATE_RECEIVED:

                        // There was no server key exchange message; check it's OK
                        this.keyExchange.skipServerKeyExchange();

                        // NB: Fall through to next case label

                    case CS_SERVER_KEY_EXCHANGE_RECEIVED:
                    {
                    	if (this.authentication == null)
                    	{
                            /*
                             * RFC 2246 7.4.4. It is a fatal handshake_failure alert
                             * for an anonymous server to request client identification.
                             */
                    		this.failWithError(AlertLevel.fatal, AlertDescription.handshake_failure);
                    	}

                        int numTypes = TlsUtils.readUint8(is);
                        short[] certificateTypes = new short[numTypes];
                        for (int i = 0; i < numTypes; ++i)
                        {
                            certificateTypes[i] = TlsUtils.readUint8(is);
                        }

                        byte[] authorities = TlsUtils.readOpaque16(is);

                        assertEmpty(is);

                        Vector authorityDNs = new Vector();

                        ByteArrayInputStream bis = new ByteArrayInputStream(authorities);
                        while (bis.available() > 0)
                        {
                            byte[] dnBytes = TlsUtils.readOpaque16(bis);
                            authorityDNs.addElement(X500Name.getInstance(ASN1Primitive.fromByteArray(dnBytes)));
                        }

                        this.certificateRequest = new CertificateRequest(certificateTypes,
                            authorityDNs);
                        this.keyExchange.validateCertificateRequest(this.certificateRequest);

                        break;
                    }
                    default:
                        this.failWithError(AlertLevel.fatal, AlertDescription.unexpected_message);
                }

                this.connection_state = CS_CERTIFICATE_REQUEST_RECEIVED;
                break;
            }
            case HandshakeType.hello_request:
                /*
                 * RFC 2246 7.4.1.1 Hello request This message will be ignored by the
                 * client if the client is currently negotiating a session. This message
                 * may be ignored by the client if it does not wish to renegotiate a
                 * session, or the client may, if it wishes, respond with a
                 * no_renegotiation alert.
                 */
                if (connection_state == CS_DONE)
                {
                    // Renegotiation not supported yet
                    sendAlert(AlertLevel.warning, AlertDescription.no_renegotiation);
                }
                break;
            case HandshakeType.client_key_exchange:
            case HandshakeType.certificate_verify:
            case HandshakeType.client_hello:
            default:
                // We do not support this!
                this.failWithError(AlertLevel.fatal, AlertDescription.unexpected_message);
                break;
        }
    }

    private void processApplicationData()
    {
        /*
         * There is nothing we need to do here.
         * 
         * This function could be used for callbacks when application data arrives in the
         * future.
         */
    }

    private void processAlert() throws IOException
    {
        while (alertQueue.size() >= 2)
        {
            /*
             * An alert is always 2 bytes. Read the alert.
             */
            byte[] tmp = new byte[2];
            alertQueue.read(tmp, 0, 2, 0);
            alertQueue.removeData(2);
            short level = tmp[0];
            short description = tmp[1];
            if (level == AlertLevel.fatal)
            {
                /*
                 * This is a fatal error.
                 */
                this.failedWithError = true;
                this.closed = true;
                /*
                 * Now try to close the stream, ignore errors.
                 */
                try
                {
                    rs.close();
                }
                catch (Exception e)
                {

                }
                throw new IOException(TLS_ERROR_MESSAGE);
            }
            else
            {
                /*
                 * This is just a warning.
                 */
                if (description == AlertDescription.close_notify)
                {
                    /*
                     * Close notify
                     */
                    this.failWithError(AlertLevel.warning, AlertDescription.close_notify);
                }
                /*
                 * If it is just a warning, we continue.
                 */
            }
        }
    }

    /**
     * This method is called, when a change cipher spec message is received.
     * 
     * @throws IOException If the message has an invalid content or the handshake is not
     *             in the correct state.
     */
    private void processChangeCipherSpec() throws IOException
    {
        while (changeCipherSpecQueue.size() > 0)
        {
            /*
             * A change cipher spec message is only one byte with the value 1.
             */
            byte[] b = new byte[1];
            changeCipherSpecQueue.read(b, 0, 1, 0);
            changeCipherSpecQueue.removeData(1);
            if (b[0] != 1)
            {
                /*
                 * This should never happen.
                 */
                this.failWithError(AlertLevel.fatal, AlertDescription.unexpected_message);
            }

            /*
             * Check if we are in the correct connection state.
             */
            if (this.connection_state != CS_CLIENT_FINISHED_SEND)
            {
                this.failWithError(AlertLevel.fatal, AlertDescription.handshake_failure);
            }

            rs.serverClientSpecReceived();

            this.connection_state = CS_SERVER_CHANGE_CIPHER_SPEC_RECEIVED;
        }
    }

    private void sendClientCertificate(Certificate clientCert) throws IOException
    {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        TlsUtils.writeUint8(HandshakeType.certificate, bos);

        // Reserve space for length
        TlsUtils.writeUint24(0, bos);

        clientCert.encode(bos);
        byte[] message = bos.toByteArray();

        // Patch actual length back in
        TlsUtils.writeUint24(message.length - 4, message, 1);

        rs.writeMessage(ContentType.handshake, message, 0, message.length);
    }

    private void sendClientKeyExchange() throws IOException
    {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();

        TlsUtils.writeUint8(HandshakeType.client_key_exchange, bos);
        
        // Reserve space for length
        TlsUtils.writeUint24(0, bos);

        this.keyExchange.generateClientKeyExchange(bos);
        byte[] message = bos.toByteArray();

        // Patch actual length back in
        TlsUtils.writeUint24(message.length - 4, message, 1);

        rs.writeMessage(ContentType.handshake, message, 0, message.length);
    }

    private void sendCertificateVerify(byte[] data) throws IOException
    {
        /*
         * Send signature of handshake messages so far to prove we are the owner of the
         * cert See RFC 2246 sections 4.7, 7.4.3 and 7.4.8
         */
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        TlsUtils.writeUint8(HandshakeType.certificate_verify, bos);
        TlsUtils.writeUint24(data.length + 2, bos);
        TlsUtils.writeOpaque16(data, bos);
        byte[] message = bos.toByteArray();

        rs.writeMessage(ContentType.handshake, message, 0, message.length);
    }

    /**
     * Connects to the remote system.
     * 
     * @param verifyer Will be used when a certificate is received to verify that this
     *            certificate is accepted by the client.
     * @throws IOException If handshake was not successful.
     * 
     * @deprecated use version taking TlsClient
     */
    public void connect(CertificateVerifyer verifyer) throws IOException
    {
        this.connect(new LegacyTlsClient(verifyer));
    }

    /**
     * Connects to the remote system using client authentication
     * 
     * @param tlsClient
     * @throws IOException If handshake was not successful.
     */
    public void connect(TlsClient tlsClient) throws IOException
    {
        if (tlsClient == null)
        {
            throw new IllegalArgumentException("'tlsClient' cannot be null");
        }
        if (this.tlsClient != null)
        {
            throw new IllegalStateException("connect can only be called once");
        }

        /*
         * Send Client hello
         * 
         * First, generate some random data.
         */
        this.securityParameters = new SecurityParameters();
        this.securityParameters.clientRandom = new byte[32];
        random.nextBytes(securityParameters.clientRandom);
        TlsUtils.writeGMTUnixTime(securityParameters.clientRandom, 0);

        this.tlsClientContext = new TlsClientContextImpl(random, securityParameters);

        this.rs.init(tlsClientContext);

        this.tlsClient = tlsClient;
        this.tlsClient.init(tlsClientContext);

        ByteArrayOutputStream os = new ByteArrayOutputStream();

        ProtocolVersion client_version = this.tlsClient.getClientVersion();
        this.tlsClientContext.setClientVersion(client_version);
        // TODO For SSLv3 support, server version needs to be set to ProtocolVersion.SSLv3
        this.tlsClientContext.setServerVersion(client_version);
        TlsUtils.writeVersion(client_version, os);

        os.write(securityParameters.clientRandom);

        /*
         * Length of Session id
         */
        TlsUtils.writeUint8((short)0, os);

        /*
         * Cipher suites
         */
        this.offeredCipherSuites = this.tlsClient.getCipherSuites();

        // Integer -> byte[]
        this.clientExtensions = this.tlsClient.getClientExtensions();

        // Cipher Suites (and SCSV)
        {
            /*
             * RFC 5746 3.4. The client MUST include either an empty "renegotiation_info"
             * extension, or the TLS_EMPTY_RENEGOTIATION_INFO_SCSV signaling cipher suite
             * value in the ClientHello. Including both is NOT RECOMMENDED.
             */
            boolean noRenegExt = clientExtensions == null
                || clientExtensions.get(EXT_RenegotiationInfo) == null;

            int count = offeredCipherSuites.length;
            if (noRenegExt)
            {
                // Note: 1 extra slot for TLS_EMPTY_RENEGOTIATION_INFO_SCSV
                ++count;
            }

            TlsUtils.writeUint16(2 * count, os);
            TlsUtils.writeUint16Array(offeredCipherSuites, os);

            if (noRenegExt)
            {
                TlsUtils.writeUint16(CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV, os);
            }
        }

        // Compression methods
        this.offeredCompressionMethods = this.tlsClient.getCompressionMethods();

        TlsUtils.writeUint8((short)offeredCompressionMethods.length, os);
        TlsUtils.writeUint8Array(offeredCompressionMethods, os);

        // Extensions
        if (clientExtensions != null)
        {
            ByteArrayOutputStream ext = new ByteArrayOutputStream();

            Enumeration keys = clientExtensions.keys();
            while (keys.hasMoreElements())
            {
                Integer extType = (Integer)keys.nextElement();
                writeExtension(ext, extType, (byte[])clientExtensions.get(extType));
            }

            TlsUtils.writeOpaque16(ext.toByteArray(), os);
        }

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        TlsUtils.writeUint8(HandshakeType.client_hello, bos);
        TlsUtils.writeUint24(os.size(), bos);
        bos.write(os.toByteArray());
        byte[] message = bos.toByteArray();

        safeWriteMessage(ContentType.handshake, message, 0, message.length);

        connection_state = CS_CLIENT_HELLO_SEND;

        /*
         * We will now read data, until we have completed the handshake.
         */
        while (connection_state != CS_DONE)
        {
            safeReadData();
        }

        this.tlsInputStream = new TlsInputStream(this);
        this.tlsOutputStream = new TlsOutputStream(this);
    }

    /**
     * Read data from the network. The method will return immediately, if there is still
     * some data left in the buffer, or block until some application data has been read
     * from the network.
     * 
     * @param buf The buffer where the data will be copied to.
     * @param offset The position where the data will be placed in the buffer.
     * @param len The maximum number of bytes to read.
     * @return The number of bytes read.
     * @throws IOException If something goes wrong during reading data.
     */
    protected int readApplicationData(byte[] buf, int offset, int len) throws IOException
    {
        while (applicationDataQueue.size() == 0)
        {
            /*
             * We need to read some data.
             */
            if (this.closed)
            {
                if (this.failedWithError)
                {
                    /*
                     * Something went terribly wrong, we should throw an IOException
                     */
                    throw new IOException(TLS_ERROR_MESSAGE);
                }

                /*
                 * Connection has been closed, there is no more data to read.
                 */
                return -1;
            }

            safeReadData();
        }
        len = Math.min(len, applicationDataQueue.size());
        applicationDataQueue.read(buf, offset, len, 0);
        applicationDataQueue.removeData(len);
        return len;
    }

    private void safeReadData() throws IOException
    {
        try
        {
            rs.readData();
        }
        catch (TlsFatalAlert e)
        {
            if (!this.closed)
            {
                this.failWithError(AlertLevel.fatal, e.getAlertDescription());
            }
            throw e;
        }
        catch (IOException e)
        {
            if (!this.closed)
            {
                this.failWithError(AlertLevel.fatal, AlertDescription.internal_error);
            }
            throw e;
        }
        catch (RuntimeException e)
        {
            if (!this.closed)
            {
                this.failWithError(AlertLevel.fatal, AlertDescription.internal_error);
            }
            throw e;
        }
    }

    private void safeWriteMessage(short type, byte[] buf, int offset, int len) throws IOException
    {
        try
        {
            rs.writeMessage(type, buf, offset, len);
        }
        catch (TlsFatalAlert e)
        {
            if (!this.closed)
            {
                this.failWithError(AlertLevel.fatal, e.getAlertDescription());
            }
            throw e;
        }
        catch (IOException e)
        {
            if (!closed)
            {
                this.failWithError(AlertLevel.fatal, AlertDescription.internal_error);
            }
            throw e;
        }
        catch (RuntimeException e)
        {
            if (!closed)
            {
                this.failWithError(AlertLevel.fatal, AlertDescription.internal_error);
            }
            throw e;
        }
    }

    /**
     * Send some application data to the remote system.
     * <p/>
     * The method will handle fragmentation internally.
     * 
     * @param buf The buffer with the data.
     * @param offset The position in the buffer where the data is placed.
     * @param len The length of the data.
     * @throws IOException If something goes wrong during sending.
     */
    protected void writeData(byte[] buf, int offset, int len) throws IOException
    {
        if (this.closed)
        {
            if (this.failedWithError)
            {
                throw new IOException(TLS_ERROR_MESSAGE);
            }

            throw new IOException("Sorry, connection has been closed, you cannot write more data");
        }

        /*
         * Protect against known IV attack!
         * 
         * DO NOT REMOVE THIS LINE, EXCEPT YOU KNOW EXACTLY WHAT YOU ARE DOING HERE.
         */
        safeWriteMessage(ContentType.application_data, emptybuf, 0, 0);

        do
        {
            /*
             * We are only allowed to write fragments up to 2^14 bytes.
             */
            int toWrite = Math.min(len, 1 << 14);

            safeWriteMessage(ContentType.application_data, buf, offset, toWrite);

            offset += toWrite;
            len -= toWrite;
        }
        while (len > 0);

    }

    /**
     * @return An OutputStream which can be used to send data.
     */
    public OutputStream getOutputStream()
    {
        return this.tlsOutputStream;
    }

    /**
     * @return An InputStream which can be used to read data.
     */
    public InputStream getInputStream()
    {
        return this.tlsInputStream;
    }

    /**
     * Terminate this connection with an alert.
     * <p/>
     * Can be used for normal closure too.
     * 
     * @param alertLevel The level of the alert, an be AlertLevel.fatal or AL_warning.
     * @param alertDescription The exact alert message.
     * @throws IOException If alert was fatal.
     */
    private void failWithError(short alertLevel, short alertDescription) throws IOException
    {
        /*
         * Check if the connection is still open.
         */
        if (!closed)
        {
            /*
             * Prepare the message
             */
            this.closed = true;

            if (alertLevel == AlertLevel.fatal)
            {
                /*
                 * This is a fatal message.
                 */
                this.failedWithError = true;
            }
            sendAlert(alertLevel, alertDescription);
            rs.close();
            if (alertLevel == AlertLevel.fatal)
            {
                throw new IOException(TLS_ERROR_MESSAGE);
            }
        }
        else
        {
            throw new IOException(TLS_ERROR_MESSAGE);
        }
    }

    private void sendAlert(short alertLevel, short alertDescription) throws IOException
    {
        byte[] error = new byte[2];
        error[0] = (byte)alertLevel;
        error[1] = (byte)alertDescription;

        rs.writeMessage(ContentType.alert, error, 0, 2);
    }

    /**
     * Closes this connection.
     * 
     * @throws IOException If something goes wrong during closing.
     */
    public void close() throws IOException
    {
        if (!closed)
        {
            this.failWithError(AlertLevel.warning, AlertDescription.close_notify);
        }
    }

    /**
     * Make sure the InputStream is now empty. Fail otherwise.
     * 
     * @param is The InputStream to check.
     * @throws IOException If is is not empty.
     */
    protected void assertEmpty(ByteArrayInputStream is) throws IOException
    {
        if (is.available() > 0)
        {
            throw new TlsFatalAlert(AlertDescription.decode_error);
        }
    }

    protected void flush() throws IOException
    {
        rs.flush();
    }

    private static boolean arrayContains(short[] a, short n)
    {
        for (int i = 0; i < a.length; ++i)
        {
            if (a[i] == n)
            {
                return true;
            }
        }
        return false;
    }

    private static boolean arrayContains(int[] a, int n)
    {
        for (int i = 0; i < a.length; ++i)
        {
            if (a[i] == n)
            {
                return true;
            }
        }
        return false;
    }

    private static byte[] createRenegotiationInfo(byte[] renegotiated_connection)
        throws IOException
    {
        ByteArrayOutputStream buf = new ByteArrayOutputStream();
        TlsUtils.writeOpaque8(renegotiated_connection, buf);
        return buf.toByteArray();
    }

    private static void writeExtension(OutputStream output, Integer extType, byte[] extValue)
        throws IOException
    {
        TlsUtils.writeUint16(extType.intValue(), output);
        TlsUtils.writeOpaque16(extValue, output);
    }
}
